diff --git a/api/v1/_submissions/PKPBackendSubmissionsController.php b/api/v1/_submissions/PKPBackendSubmissionsController.php index 09e29bdb00c..ca3f184e443 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,16 @@ public function getGroupRoutes(): void ), ]); + Route::delete('', $this->bulkDeleteIncompleteSubmissions(...)) + ->name('_submission.incomplete.delete') + ->middleware([ + self::roleAuthorizer([ + Role::ROLE_ID_SITE_ADMIN, + Role::ROLE_ID_MANAGER, + Role::ROLE_ID_AUTHOR, + ]), + ]); + Route::delete('{submissionId}', $this->delete(...)) ->name('_submission.delete') ->middleware([ @@ -430,6 +441,78 @@ 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; + } + + $collector = $this->getSubmissionCollector($illuminateRequest->query()) + ->filterBySubmissionIds($submissionIds) + ->filterByIncomplete(true); + + $request = Application::get()->getRequest(); + $context = $this->getRequest()->getContext(); + $user = $request->getUser(); + + if ($user->hasRole([Role::ROLE_ID_AUTHOR], $context->getId())) { + $userId = $request->getUser()->getId(); + $collector->assignedTo([$userId]); + } + + $submissions = $collector->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); + } + + + 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 a6e70fa18c1..6b3e3703cf1 100644 --- a/classes/submission/Collector.php +++ b/classes/submission/Collector.php @@ -55,6 +55,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; @@ -252,6 +253,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 * @@ -372,6 +384,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/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..0cf9741dbfd 100644 --- a/locale/en/common.po +++ b/locale/en/common.po @@ -806,7 +806,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" diff --git a/locale/en/submission.po b/locale/en/submission.po index 52cfb1960ae..74021324efd 100644 --- a/locale/en/submission.po +++ b/locale/en/submission.po @@ -2633,4 +2633,16 @@ msgid "fileManager.productionReadyFilesDescription" msgstr "These are the files that will be sent for publication" msgid "reviewerManager.reviewerStatus" -msgstr "Reviewer status" \ No newline at end of file +msgstr "Reviewer status" + +msgid "dashboard.submissions.incomplete.bulkDelete.confirm" +msgstr "Confirm Delete of Incomplete Submissions" + +msgid "dashboard.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 "dashboard.submissions.incomplete.bulkDelete.column.description" +msgstr "Select incomplete submissions to be deleted." + +msgid "dashboard.submissions.incomplete.bulkDelete.button" +msgstr "Delete Incomplete Submissions"