From 7016b30de9402e9bd891bc35c33a099b355906f1 Mon Sep 17 00:00:00 2001 From: Taslan Graham Date: Fri, 26 Jul 2024 17:08:34 -0500 Subject: [PATCH] pkp#6528 implement endpoint to perform bulk delete of incomplete submissions --- .../PKPBackendSubmissionsController.php | 73 +++++++++++++++++++ classes/submission/Collector.php | 16 ++++ locale/en/admin.po | 5 +- locale/en/api.po | 6 ++ locale/en/common.po | 2 +- 5 files changed, 100 insertions(+), 2 deletions(-) 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 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 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 78c9317a292..8fff2a644e9 100644 --- a/locale/en/common.po +++ b/locale/en/common.po @@ -809,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"