Skip to content

Commit

Permalink
Introduce DELETE /{p}/paper API
Browse files Browse the repository at this point in the history
Also, fix a nit, and export `modified_at` in paper export JSON.
  • Loading branch information
kohler committed Mar 8, 2025
1 parent 8692084 commit f396f79
Show file tree
Hide file tree
Showing 16 changed files with 345 additions and 56 deletions.
8 changes: 3 additions & 5 deletions batch/apispec.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ class APISpec_Batch {

function __construct(Conf $conf, $arg) {
$this->conf = $conf;
$this->conf->set_opt("allowApiPostGet", false);
$this->user = $conf->root_user();
$this->xtp = new XtParams($this->conf, null);
$this->base = isset($arg["x"]);
Expand Down Expand Up @@ -333,13 +334,10 @@ private function add_field($name, $f) {

/** @param string $fn */
private function expand_paths($fn) {
foreach (["get", "post"] as $lmethod) {
foreach (["get", "post", "delete"] as $lmethod) {
if (!($uf = $this->conf->api($fn, null, strtoupper($lmethod)))) {
continue;
}
if ($lmethod === "post" && !($uf->post ?? false) && ($uf->get ?? false)) {
continue;
}

// parse subset of parameters
$this->cur_fieldf = [];
Expand Down Expand Up @@ -1235,7 +1233,7 @@ function run() {
}
}
foreach ($this->description_map as $name => $djs) {
if (preg_match('/\A(get|post)\s+(\S+)\z/', $name, $m)
if (preg_match('/\A(get|post|delete)\s+(\S+)\z/', $name, $m)
&& !isset($this->paths->{$m[2]}->{$m[1]})
&& ($dj = $this->find_description($name))) {
fwrite(STDERR, "{$dj->landmark}: description path {$m[1]}.{$m[2]} not specified\n");
Expand Down
61 changes: 60 additions & 1 deletion batch/cli/cli_paper.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ class Paper_CLIBatch implements CLIBatchCommand {
/** @var bool */
public $edit;
/** @var bool */
public $delete;
/** @var bool */
public $dry_run;
/** @var bool */
public $notify = true;
/** @var bool */
public $notify_authors = true;
/** @var HotCLI_File */
public $cf;

Expand All @@ -28,9 +34,27 @@ function run(HotCLI_Batch $clib) {
} else {
$this->urlbase = "{$clib->site}/papers?q=" . urlencode($this->q);
}
if (!$this->edit && !$this->delete) {
return $this->run_get($clib);
}
if ($this->dry_run) {
$this->urlbase .= "&dry_run=1";
}
if (!$this->notify) {
$this->urlbase .= "&notify=0";
}
if (!$this->notify_authors) {
$this->urlbase .= "&notify_authors=0";
}
if ($this->edit) {
return $this->run_edit($clib);
} else {
return $this->run_delete($clib);
}
}

/** @return int */
function run_get(HotCLI_Batch $clib) {
curl_setopt($clib->curlh, CURLOPT_CUSTOMREQUEST, "GET");
curl_setopt($clib->curlh, CURLOPT_URL, $this->urlbase);
if (!$clib->exec_api(null)) {
Expand Down Expand Up @@ -73,11 +97,13 @@ function run_edit(HotCLI_Batch $clib) {
]);
}
$ok = $clib->exec_api(null);
if ($this->p) {
if ($this->p && isset($clib->content_json->valid)) {
if (!$clib->content_json->valid) {
$clib->error_at(null, "<0>Changes invalid");
} else if (empty($clib->content_json->change_list)) {
$clib->success("<0>No changes");
} else if ($clib->content_json->dry_run ?? false) {
$clib->success("<0>Would change " . commajoin($clib->content_json->change_list));
} else {
$clib->success("<0>Saved changes to " . commajoin($clib->content_json->change_list));
}
Expand All @@ -91,6 +117,26 @@ function run_edit(HotCLI_Batch $clib) {
return $ok ? 0 : 1;
}

/** @return int */
function run_delete(HotCLI_Batch $clib) {
curl_setopt($clib->curlh, CURLOPT_URL, $this->urlbase);
curl_setopt($clib->curlh, CURLOPT_CUSTOMREQUEST, "DELETE");
$ok = $clib->exec_api(null);
if (isset($clib->content_json->valid)) {
if (!$clib->content_json->valid) {
$clib->error_at(null, "<0>Delete invalid");
} else if ($clib->content_json->dry_run ?? false) {
$clib->error_at(null, "<0>Would delete #{$this->p}");
} else {
$clib->success("<0>Deleted #{$this->p}");
}
}
if ($clib->verbose) {
fwrite(STDERR, $clib->content_string);
}
return $ok ? 0 : 1;
}

/** @return Paper_CLIBatch */
static function make_arg(HotCLI_Batch $clib, Getopt $getopt, $arg) {
$pcb = new Paper_CLIBatch;
Expand All @@ -115,13 +161,26 @@ static function make_arg(HotCLI_Batch $clib, Getopt $getopt, $arg) {
}
}

$pcb->delete = isset($arg["delete"]);
$pcb->edit = isset($arg["e"]);
if ($pcb->edit) {
$pcb->cf = HotCLI_File::make($arg["_"][$narg] ?? "-");
++$narg;
}
$pcb->dry_run = isset($arg["dry-run"]);
if (isset($arg["no-notify"])) {
$pcb->notify = false;
}
if (isset($arg["no-notify-authors"])) {
$pcb->notify_authors = false;
}

if ($pcb->delete && $pcb->edit) {
throw new CommandLineException("`--delete` conflicts with `--edit`");
}
if ($pcb->delete && !$pcb->p) {
throw new CommandLineException("`--delete` requires `-p`");
}
if (count($arg["_"]) > $narg) {
throw new CommandLineException("Too many arguments");
}
Expand Down
5 changes: 4 additions & 1 deletion batch/hotcli.php
Original file line number Diff line number Diff line change
Expand Up @@ -348,8 +348,11 @@ static function make_args($argv) {
"mimetype:,m: =MIMETYPE !upload Type for uploaded file",
"p:,paper: =PID !paper Submission ID",
"q:,query: =SEARCH !paper Submission search",
"e,edit !paper Change papers",
"e,edit !paper Change submissions",
"delete !paper Delete submission",
"dry-run,d !paper Don’t actually edit",
"no-notify Don’t notify users",
"no-notify-authors Don’t notify authors",
"chunk: =CHUNKSIZE Maximum upload chunk size [5M]",
"quiet Do not print error messages",
)->subcommand(true,
Expand Down
7 changes: 3 additions & 4 deletions devel/apidoc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,9 @@ Responses are formatted as JSON. Every response has an `ok` property; `ok` is
`true` if the request succeeded and `false` otherwise. Messages about the
request, if any, are expressed in a `message_list` property.

The `GET` method is used to retrieve information and the `POST` method to
modify information. Other methods are generally not used; for instance,
deleting a comment uses a `delete=1` parameter for a `POST` request, rather
than a `DELETE` request.
`GET` operations retrieve system state and `POST` operations modify system
state. Other operations are occasionally used when semantically meaningful—for
example, the `/paper` endpoint supports `DELETE`.


### Common parameters
Expand Down
16 changes: 16 additions & 0 deletions devel/apidoc/submissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,22 @@ existing submission, set the JSON’s `status`.`if_unmodified_since` to `0`.
* response ?+valid boolean: True if the modification was valid


# delete /{p}/paper

> Delete submission
Delete the submission specified by `p`, a submission ID.

* param ?dry_run boolean: True checks input for errors, but does not save changes
* param ?notify boolean: False disables all email notifications (site
administrators only)
* param ?notify_authors boolean: False disables email notifications to authors
(paper administrators only)
* param ?reason string: Optional text included in notification emails
* param ?if_unmodified_since string: Don’t delete if modified since this time
* badge admin


# get /papers

> Retrieve multiple submissions
Expand Down
93 changes: 91 additions & 2 deletions devel/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
"openapi": "3.1.0",
"info": {
"title": "HotCRP REST API",
"version": "2025-03-08:5377030c4",
"version": "2025-03-08:869208450",
"summary": "HotCRP conference management software API",
"description": "[HotCRP](https://github.com/kohler/hotcrp) is conference review software. It\nis open source; a supported version runs on [hotcrp.com](https://hotcrp.com/).\nThis documentation is for the HotCRP REST-like API.\n\nTo request documentation for an API method, please open a [GitHub\nissue](https://github.com/kohler/hotcrp/issues). We also welcome [pull\nrequests](https://github.com/kohler/hotcrp/pulls).\n\n\n## Overview\n\nAPI calls use paths under `api`. For instance, to call the `paper` endpoint on\na server at `https://example.hotcrp.org/funconf25`, you might use a URL like\n`https://example.hotcrp.org/funconf25/api/paper?p=1`.\n\nParameters are provided in query strings or the request body, typically using\n[form encoding](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST).\nSome parameters are formatted as JSON. Some complex requests define logical\nobjects using structured keys, such as `named_search/1/q`. Use\n`multipart/form-data` encoding for requests that include uploaded files.\nSince servers limit upload size, you may need to use the upload API to upload\na large file before processing it with another call.\n\nResponses are formatted as JSON. Every response has an `ok` property; `ok` is\n`true` if the request succeeded and `false` otherwise. Messages about the\nrequest, if any, are expressed in a `message_list` property.\n\nThe `GET` method is used to retrieve information and the `POST` method to\nmodify information. Other methods are generally not used; for instance,\ndeleting a comment uses a `delete=1` parameter for a `POST` request, rather\nthan a `DELETE` request.\n\n\n### Common parameters\n\nThe `p` parameter defines a submission ID. It can appear either in the query\nstring or immediately following `api/` in the query path: `api/comment?p=1`\nand `api/1/comment` are the same API call. `p` is a positive decimal integer,\nbut some API calls accept `p=new` when defining a new submission.\n\nThe `forceShow` boolean parameter allows administrators to override their\nconflicts when that is possible.\n\n\n### Authentication\n\nExternal applications should authenticate to HotCRP’s API using bearer tokens\n(an `Authorization: bearer` HTTP header). Obtain API tokens using Account\nsettings > Developer. HotCRP Javascript makes API calls using session cookies\nfor authentication.\n"
"description": "[HotCRP](https://github.com/kohler/hotcrp) is conference review software. It\nis open source; a supported version runs on [hotcrp.com](https://hotcrp.com/).\nThis documentation is for the HotCRP REST-like API.\n\nTo request documentation for an API method, please open a [GitHub\nissue](https://github.com/kohler/hotcrp/issues). We also welcome [pull\nrequests](https://github.com/kohler/hotcrp/pulls).\n\n\n## Overview\n\nAPI calls use paths under `api`. For instance, to call the `paper` endpoint on\na server at `https://example.hotcrp.org/funconf25`, you might use a URL like\n`https://example.hotcrp.org/funconf25/api/paper?p=1`.\n\nParameters are provided in query strings or the request body, typically using\n[form encoding](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST).\nSome parameters are formatted as JSON. Some complex requests define logical\nobjects using structured keys, such as `named_search/1/q`. Use\n`multipart/form-data` encoding for requests that include uploaded files.\nSince servers limit upload size, you may need to use the upload API to upload\na large file before processing it with another call.\n\nResponses are formatted as JSON. Every response has an `ok` property; `ok` is\n`true` if the request succeeded and `false` otherwise. Messages about the\nrequest, if any, are expressed in a `message_list` property.\n\n`GET` operations retrieve system state and `POST` operations modify system\nstate. Other operations are occasionally used when semantically meaningful—for\nexample, the `/paper` endpoint supports `DELETE`.\n\n\n### Common parameters\n\nThe `p` parameter defines a submission ID. It can appear either in the query\nstring or immediately following `api/` in the query path: `api/comment?p=1`\nand `api/1/comment` are the same API call. `p` is a positive decimal integer,\nbut some API calls accept `p=new` when defining a new submission.\n\nThe `forceShow` boolean parameter allows administrators to override their\nconflicts when that is possible.\n\n\n### Authentication\n\nExternal applications should authenticate to HotCRP’s API using bearer tokens\n(an `Authorization: bearer` HTTP header). Obtain API tokens using Account\nsettings > Developer. HotCRP Javascript makes API calls using session cookies\nfor authentication.\n"
},
"servers": [
{
Expand Down Expand Up @@ -211,6 +211,95 @@
}
}
},
"/{p}/paper": {
"delete": {
"summary": "Delete submission",
"description": "Delete the submission specified by `p`, a submission ID.\n",
"tags": [
"Submissions"
],
"x-badges": [
{
"name": "Admin only"
}
],
"parameters": [
{
"$ref": "#/components/parameters/p.path"
},
{
"name": "dry_run",
"in": "query",
"required": false,
"schema": {
"type": "boolean"
},
"description": "True checks input for errors, but does not save changes"
},
{
"name": "notify",
"in": "query",
"required": false,
"schema": {
"type": "boolean"
},
"description": "False disables all email notifications (site\n administrators only)"
},
{
"name": "notify_authors",
"in": "query",
"required": false,
"schema": {
"type": "boolean"
},
"description": "False disables email notifications to authors\n (paper administrators only)"
},
{
"name": "reason",
"in": "query",
"required": false,
"schema": {
"type": "string"
},
"description": "Optional text included in notification emails"
},
{
"name": "if_unmodified_since",
"in": "query",
"required": false,
"schema": {
"type": "string"
},
"description": "Don’t delete if modified since this time"
},
{
"$ref": "#/components/parameters/forceShow"
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/minimal_response"
}
}
}
},
"default": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
}
}
}
},
"/papers": {
"get": {
"summary": "Retrieve multiple submissions",
Expand Down
4 changes: 4 additions & 0 deletions etc/apiexpansions.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
"name": "paper", "post": true, "merge": true,
"tags": ["Submissions"], "order": 0
},
{
"name": "paper", "delete": true, "merge": true,
"tags": ["Submissions"], "order": 0
},
{
"name": "papers", "get": true, "merge": true,
"tags": ["Submissions"], "order": 1
Expand Down
5 changes: 5 additions & 0 deletions etc/apifunctions.json
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,11 @@
"parameters": "?p ?+dry_run ?+disable_users ?+add_topics ?+notify ?+notify_authors ?+reason ?+json ?forceShow",
"response": "?paper ?+dry_run ?+change_list ?+valid"
},
{
"name": "paper", "delete": true, "paper": true,
"function": "Paper_API::run_one",
"parameters": "p ?dry_run ?notify ?notify_authors ?reason ?forceShow"
},
{
"name": "papers", "get": true,
"function": "Paper_API::run_multi",
Expand Down
Loading

0 comments on commit f396f79

Please sign in to comment.