diff --git a/api/v1/_submissions/PKPBackendSubmissionsController.php b/api/v1/_submissions/PKPBackendSubmissionsController.php
index 3b54238b4a0..87f91d3c6fe 100644
--- a/api/v1/_submissions/PKPBackendSubmissionsController.php
+++ b/api/v1/_submissions/PKPBackendSubmissionsController.php
@@ -20,6 +20,7 @@
use APP\core\Application;
use APP\facades\Repo;
use APP\submission\Collector;
+use APP\submission\Submission;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
@@ -87,6 +88,14 @@ public function getGroupRoutes(): void
),
]);
+ Route::delete('', $this->bulkDeleteIncompleteSubmissions(...))
+ ->name('_submission.incomplete.delete')
+ ->middleware([
+ self::roleAuthorizer([
+ Role::ROLE_ID_SITE_ADMIN,
+ ]),
+ ]);
+
Route::delete('{submissionId}', $this->delete(...))
->name('_submission.delete')
->middleware([
@@ -430,6 +439,70 @@ public function delete(Request $illuminateRequest): JsonResponse
return response()->json([], Response::HTTP_OK);
}
+ /**
+ * Delete a list of incomplete submissions
+ */
+ public function bulkDeleteIncompleteSubmissions(Request $illuminateRequest): JsonResponse
+ {
+ $submissionIdsRaw = paramToArray($illuminateRequest->query('ids') ?? []);
+
+ if (empty($submissionIdsRaw)) {
+ return response()->json([
+ 'error' => __('api.submission.400.missingQueryParam'),
+ ], Response::HTTP_BAD_REQUEST);
+ }
+
+ $submissionIds = [];
+
+ foreach ($submissionIdsRaw as $id) {
+ $integerId = intval($id);
+
+ if (!$integerId) {
+ return response()->json([
+ 'error' => __('api.submission.400.invalidId', ['id' => $id])
+ ], Response::HTTP_BAD_REQUEST);
+ }
+
+ $submissionIds[] = $id;
+ }
+
+ $submissions = $this->getSubmissionCollector($illuminateRequest->query())
+ ->filterBySubmissionIds($submissionIds)
+ ->filterByIncomplete(true)
+ ->getMany()
+ ->all();
+
+ $submissionIdsFound = array_map(fn (Submission $submission) => $submission->getData('id'), $submissions);
+
+ if (array_diff($submissionIds, $submissionIdsFound)) {
+ return response()->json([
+ 'error' => __('api.404.resourceNotFound')
+ ], Response::HTTP_NOT_FOUND);
+ }
+
+ $context = $this->getRequest()->getContext();
+
+ foreach ($submissions as $submission) {
+ if ($context->getId() != $submission->getData('contextId')) {
+ return response()->json([
+ 'error' => __('api.submissions.403.deleteSubmissionOutOfContext'),
+ ], Response::HTTP_FORBIDDEN);
+ }
+
+ if (!Repo::submission()->canCurrentUserDelete($submission)) {
+ return response()->json([
+ 'error' => __('api.submissions.403.unauthorizedDeleteSubmission'),
+ ], Response::HTTP_FORBIDDEN);
+ }
+ }
+
+ foreach ($submissions as $submission) {
+ Repo::submission()->delete($submission);
+ }
+
+ return response()->json([], Response::HTTP_OK);
+ }
+
/**
* Configure a submission Collector based on the query params
*/
diff --git a/classes/submission/Collector.php b/classes/submission/Collector.php
index b2c73684adb..0264c11f8ec 100644
--- a/classes/submission/Collector.php
+++ b/classes/submission/Collector.php
@@ -53,6 +53,7 @@ abstract class Collector implements CollectorInterface, ViewsCount
public DAO $dao;
public ?array $categoryIds = null;
public ?array $contextIds = null;
+ public ?array $submissionIds = null;
public ?int $count = null;
public ?int $daysInactive = null;
public bool $isIncomplete = false;
@@ -250,6 +251,17 @@ public function filterByRevisionsSubmitted(?bool $revisionsSubmitted): AppCollec
return $this;
}
+ /**
+ * Limit results to only submissions with the specified IDs
+ *
+ * @param ?int[] $submissionIds Submission IDs
+ */
+ public function filterBySubmissionIds(?array $submissionIds): static
+ {
+ $this->submissionIds = $submissionIds;
+ return $this;
+ }
+
/**
* Limit results to submissions assigned to these users
*
@@ -370,6 +382,10 @@ public function getQueryBuilder(): Builder
$q->whereIn('s.context_id', $this->contextIds);
}
+ if (isset($this->submissionIds)) {
+ $q->whereIn('s.submission_id', array_map(intval(...), $this->submissionIds));
+ }
+
// Prepare keywords (allows short and numeric words)
$keywords = collect(Application::getSubmissionSearchIndex()->filterKeywords($this->searchPhrase, false, true, true))
->unique()
diff --git a/locale/en/admin.po b/locale/en/admin.po
index 039a4a81888..a76dbbd2208 100644
--- a/locale/en/admin.po
+++ b/locale/en/admin.po
@@ -1014,3 +1014,22 @@ msgstr "No matching scheduled task found."
msgid "admin.cli.tool.scheduler.run.prompt"
msgstr "Which command would you like to run?"
+
+msgid "admin.submissions.incomplete.bulkDelete.button"
+msgstr "Delete Incomplete Submissions"
+
+msgid "admin.submissions.incomplete.bulkDelete.column.description"
+msgstr "Select incomplete submissions to be deleted."
+
+
+msgid "admin.submissions.incomplete.bulkDelete.confirm"
+msgstr "Confirm Delete of Incomplete Submissions"
+
+msgid "admin.submissions.incomplete.bulkDelete.body"
+msgstr "Are you sure you want to delete the selected items? This action cannot be undone. Please confirm to proceed."
+
+msgid "admin.submissions.incomplete.bulkDelete.selectionStatus"
+msgstr "Incomplete Submissions Selected."
+
+msgid "admin.submissions.incomplete.bulkDelete.success"
+msgstr "Submissions deleted successfully!"
diff --git a/locale/en/api.po b/locale/en/api.po
index 6a5aa36f309..6cb0eae72f5 100644
--- a/locale/en/api.po
+++ b/locale/en/api.po
@@ -347,5 +347,11 @@ msgstr "Only 'accept' and 'decline' are valid values"
msgid "api.submission.400.sectionDoesNotExist"
msgstr "The provided section does not exist."
+msgid "api.submission.400.missingQueryParam"
+msgstr "The request is missing the required query parameter `ids`. Please provide the `ids` of the submissions you wish to delete."
+
+msgid "api.submission.400.invalidId"
+msgstr "Invalid ID: \"{$id}\" provided."
+
msgid "api.publications.403.noEnabledIdentifiers"
msgstr "Publication identifiers form is unavailable as there are no enabled Identifiers."
diff --git a/locale/en/common.po b/locale/en/common.po
index e2f967f80eb..8fff2a644e9 100644
--- a/locale/en/common.po
+++ b/locale/en/common.po
@@ -370,6 +370,9 @@ msgstr "Delete Selection"
msgid "common.deselect"
msgstr "Deselect"
+msgid "common.deselectAll"
+msgstr "Deselect All"
+
msgid "common.designation"
msgstr "Designation"
@@ -806,7 +809,7 @@ msgid "common.replaceFile"
msgstr "Replace file"
msgid "common.requiredField"
-msgstr "Required fields are marked with an asterisk: *"
+msgstr "Required fields are marked with an asterisk: *"
msgid "common.required"
msgstr "Required"