diff --git a/api/v1/_submissions/PKPBackendSubmissionsController.php b/api/v1/_submissions/PKPBackendSubmissionsController.php
index 56e2d4c70c4..8012343ec75 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/classes/submission/DAO.php b/classes/submission/DAO.php
index 7f46d566b21..532248d4426 100644
--- a/classes/submission/DAO.php
+++ b/classes/submission/DAO.php
@@ -26,6 +26,7 @@
use PKP\db\DAORegistry;
use PKP\log\event\EventLogEntry;
use PKP\note\NoteDAO;
+use PKP\notification\Notification;
use PKP\query\QueryDAO;
use PKP\services\PKPSchemaService;
use PKP\stageAssignment\StageAssignment;
diff --git a/locale/en/admin.po b/locale/en/admin.po
index 51f9d0fac1a..56c22843b8a 100644
--- a/locale/en/admin.po
+++ b/locale/en/admin.po
@@ -1022,4 +1022,7 @@ 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 "{$selected} of {$total} Incomplete Submissions Selected"
\ No newline at end of file
+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 6ca82ab4935..2c67dcabcd6 100644
--- a/locale/en/api.po
+++ b/locale/en/api.po
@@ -331,3 +331,9 @@ msgstr "The reviewer for the assignment could not be found"
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."
diff --git a/locale/en/common.po b/locale/en/common.po
index 56211e5ae0e..03a5d967a86 100644
--- a/locale/en/common.po
+++ b/locale/en/common.po
@@ -803,7 +803,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"