-
Notifications
You must be signed in to change notification settings - Fork 112
feat: allow editing of submission by the user #1690
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
179802c
13b633e
e72d912
262844f
02e544f
558fb29
5168c83
b6a6e18
84cee8c
0a82bbc
15194ac
e67f7d7
117e71e
20393a3
b3f46d7
a4495bb
abfc068
6a5d365
f8c54c1
e597620
87e3b25
bc6761d
217736e
68296b2
88ef56a
50c64bc
e504d77
155ab5d
09cdbb6
bf941df
b821904
821c9e4
94d2c9a
73be15f
f301377
a347549
53e1d77
cea5b82
712dd45
053d913
9a20f25
5d1e27b
264b656
31af1b3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -172,6 +172,7 @@ | |
| 'showToAllUsers' => false, | ||
| ]); | ||
| $form->setSubmitMultiple(false); | ||
| $form->setAllowEdit(false); | ||
| $form->setShowExpiration(false); | ||
| $form->setExpires(0); | ||
| $form->setIsAnonymous(false); | ||
|
|
@@ -1294,7 +1295,7 @@ | |
| continue; | ||
| } | ||
|
|
||
| $this->storeAnswersForQuestion($form, $submission->getId(), $questions[$questionIndex], $answerArray); | ||
| $this->storeAnswersForQuestion($form, $submission->getId(), $questions[$questionIndex], $answerArray, false); | ||
| } | ||
|
|
||
| $this->formMapper->update($form); | ||
|
|
@@ -1309,6 +1310,87 @@ | |
| return new DataResponse(null, Http::STATUS_CREATED); | ||
| } | ||
|
|
||
| /** | ||
| * Update an existing submission | ||
| * | ||
| * @param int $formId the form id | ||
| * @param int $submissionId the submission id | ||
| * @param array<string, list<string>> $answers [question_id => arrayOfString] | ||
| * @param string $shareHash public share-hash -> Necessary to submit on public link-shares. | ||
| * @return DataResponse<Http::STATUS_OK, int, array{}> | ||
| * @throws OCSBadRequestException Can only update submission if AllowEdit is set and the answers are valid | ||
| * @throws OCSForbiddenException Can only update your own submission | ||
| * | ||
| * 200: the id of the updated submission | ||
| */ | ||
| #[CORS()] | ||
| #[NoAdminRequired()] | ||
| #[NoCSRFRequired()] | ||
| #[PublicPage()] | ||
| #[ApiRoute(verb: 'PUT', url: '/api/v3/forms/{formId}/submissions/{submissionId}')] | ||
| public function updateSubmission(int $formId, int $submissionId, array $answers, string $shareHash = ''): DataResponse { | ||
| $this->logger->debug('Updating submission: formId: {formId}, answers: {answers}, shareHash: {shareHash}', [ | ||
| 'formId' => $formId, | ||
| 'answers' => $answers, | ||
| 'shareHash' => $shareHash, | ||
| ]); | ||
|
|
||
| $form = $this->loadFormForSubmission($formId, $shareHash); | ||
|
|
||
| if (!$form->getAllowEdit()) { | ||
| throw new OCSBadRequestException('Can only update if AllowEdit is set'); | ||
| } | ||
|
|
||
| $questions = $this->formsService->getQuestions($formId); | ||
| // Is the submission valid | ||
| $isSubmissionValid = $this->submissionService->validateSubmission($questions, $answers, $form->getOwnerId()); | ||
|
Check failure on line 1346 in lib/Controller/ApiController.php
|
||
| if (is_string($isSubmissionValid)) { | ||
|
Check failure on line 1347 in lib/Controller/ApiController.php
|
||
| throw new OCSBadRequestException($isSubmissionValid); | ||
|
Check failure on line 1348 in lib/Controller/ApiController.php
|
||
| } | ||
| if ($isSubmissionValid === false) { | ||
|
Check failure on line 1350 in lib/Controller/ApiController.php
|
||
| throw new OCSBadRequestException('At least one submitted answer is not valid'); | ||
| } | ||
|
|
||
| // get existing submission of this user | ||
| try { | ||
| $submission = $this->submissionMapper->findByFormAndUser($form->getId(), $this->currentUser->getUID()); | ||
| } catch (DoesNotExistException $e) { | ||
| throw new OCSBadRequestException('Cannot update a non existing submission'); | ||
| } | ||
|
|
||
| if ($submissionId != $submission->getId()) { | ||
| throw new OCSForbiddenException('Can only update your own submissions'); | ||
| } | ||
|
|
||
| $submission->setTimestamp(time()); | ||
| $this->submissionMapper->update($submission); | ||
|
|
||
| if (empty($answers)) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In what cases can this be? When deleting a submission? |
||
| // Clear Answers | ||
| foreach ($questions as $question) { | ||
| $this->storeAnswersForQuestion($form, $submission->getId(), $question, [''], true); | ||
| } | ||
| } else { | ||
| // Process Answers | ||
| foreach ($answers as $questionId => $answerArray) { | ||
| // Search corresponding Question, skip processing if not found | ||
| $questionIndex = array_search($questionId, array_column($questions, 'id')); | ||
| if ($questionIndex === false) { | ||
| continue; | ||
| } | ||
|
|
||
| $question = $questions[$questionIndex]; | ||
|
|
||
| $this->storeAnswersForQuestion($form, $submission->getId(), $question, $answerArray, true); | ||
| } | ||
| } | ||
|
|
||
| //Create Activity | ||
| $this->formsService->notifyNewSubmission($form, $submission); | ||
|
|
||
| return new DataResponse($submissionId); | ||
| } | ||
|
|
||
| /** | ||
| * Delete a specific submission | ||
| * | ||
|
|
@@ -1522,14 +1604,23 @@ | |
| // private functions | ||
|
|
||
| /** | ||
| * Insert answers for a question | ||
| * Insert or update answers for a question | ||
| * | ||
| * @param Form $form | ||
| * @param int $submissionId | ||
| * @param array $question | ||
| * @param string[]|array<array{uploadedFileId: string, uploadedFileName: string}> $answerArray | ||
| * @param bool $update | ||
| */ | ||
| private function storeAnswersForQuestion(Form $form, $submissionId, array $question, array $answerArray): void { | ||
| private function storeAnswersForQuestion(Form $form, int $submissionId, array $question, array $answerArray, bool $update): void { | ||
| // get stored answers for this question | ||
| $storedAnswers = []; | ||
| if ($update) { | ||
| $storedAnswers = $this->answerMapper->findBySubmissionAndQuestion($submissionId, $question['id']); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To generate fewer database queries, before foreach where the storeAnswersForQuestion method is called, get all the answers in one call through $this->answerMapper->findBySubmission and pass here |
||
| } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will try to propose a simpler solution for this method $deletingStoredAnswers = []; |
||
|
|
||
| $newAnswerTexts = []; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. delete |
||
|
|
||
| foreach ($answerArray as $answer) { | ||
| $answerEntity = new Answer(); | ||
| $answerEntity->setSubmissionId($submissionId); | ||
|
|
@@ -1546,6 +1637,33 @@ | |
| } elseif (!empty($question['extraSettings']['allowOtherAnswer']) && strpos($answer, Constants::QUESTION_EXTRASETTINGS_OTHER_PREFIX) === 0) { | ||
| $answerText = str_replace(Constants::QUESTION_EXTRASETTINGS_OTHER_PREFIX, '', $answer); | ||
| } | ||
|
|
||
| if (!array_key_exists($question['id'], $newAnswerTexts)) { | ||
| $newAnswerTexts[$question['id']] = []; | ||
| } | ||
| $newAnswerTexts[$question['id']][] = $answerText; | ||
|
|
||
| // has this answer already been stored? | ||
| $foundAnswer = false; | ||
| foreach ($storedAnswers as $storedAnswer) { | ||
| if ($storedAnswer->getText() == $answerText) { | ||
| // nothing to be changed | ||
| $foundAnswer = true; | ||
| break; | ||
| } | ||
| } | ||
| if (!$foundAnswer) { | ||
| if ($answerText === '') { | ||
| continue; | ||
| } | ||
| // need to add answer | ||
| $answerEntity = new Answer(); | ||
| $answerEntity->setSubmissionId($submissionId); | ||
| $answerEntity->setQuestionId($question['id']); | ||
| $answerEntity->setText($answerText); | ||
| $this->answerMapper->insert($answerEntity); | ||
| } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. delete |
||
|
|
||
| } elseif ($question['type'] === Constants::ANSWER_TYPE_FILE) { | ||
| $uploadedFile = $this->uploadedFileMapper->getByUploadedFileId($answer['uploadedFileId']); | ||
| $answerEntity->setFileId($uploadedFile->getFileId()); | ||
|
|
@@ -1565,20 +1683,43 @@ | |
| $file->move($folder->getPath() . '/' . $name); | ||
|
|
||
| $answerText = $name; | ||
|
|
||
| $answerEntity->setText($answerText); | ||
| $this->answerMapper->insert($answerEntity); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. delete |
||
| } else { | ||
| $answerText = $answer; // Not a multiple-question, answerText is given answer | ||
| } | ||
|
|
||
| if ($answerText === '') { | ||
| continue; | ||
| if (!empty($storedAnswers)) { | ||
| $answerEntity = $storedAnswers[0]; | ||
| $answerEntity->setText($answerText); | ||
| $this->answerMapper->update($answerEntity); | ||
| } else { | ||
| if ($answerText === '') { | ||
| continue; | ||
| } | ||
| $answerEntity = new Answer(); | ||
| $answerEntity->setSubmissionId($submissionId); | ||
| $answerEntity->setQuestionId($question['id']); | ||
| $answerEntity->setText($answerText); | ||
| $this->answerMapper->insert($answerEntity); | ||
| } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. revert |
||
| } | ||
|
|
||
| $answerEntity->setText($answerText); | ||
| $this->answerMapper->insert($answerEntity); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if (array_key_exists($answerText, $deletingStoredAnswers)) { $answerEntity->setText($answerText); |
||
| if ($uploadedFile) { | ||
| $this->uploadedFileMapper->delete($uploadedFile); | ||
| } | ||
| } | ||
|
|
||
| if (in_array($question['type'], Constants::ANSWER_TYPES_PREDEFINED)) { | ||
| // drop all answers that are not in new set of answers | ||
| foreach ($storedAnswers as $storedAnswer) { | ||
| $questionId = $storedAnswer->getQuestionId(); | ||
|
|
||
| if (empty($newAnswerTexts[$questionId]) || !in_array($storedAnswer->getText(), $newAnswerTexts[$questionId])) { | ||
| $this->answerMapper->delete($storedAnswer); | ||
| } | ||
| } | ||
| } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if (!empty( $deletingStoredAnswers) { |
||
| } | ||
|
|
||
| private function loadFormForSubmission(int $formId, string $shareHash): Form { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| /** | ||
| * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors | ||
| * SPDX-License-Identifier: AGPL-3.0-or-later | ||
| */ | ||
|
|
||
| namespace OCA\Forms\Migration; | ||
|
|
||
| use Closure; | ||
| use OCP\DB\ISchemaWrapper; | ||
| use OCP\DB\Types; | ||
| use OCP\Migration\IOutput; | ||
| use OCP\Migration\SimpleMigrationStep; | ||
|
|
||
| class Version050000Date20250109201500 extends SimpleMigrationStep { | ||
|
|
||
| /** | ||
| * @param IOutput $output | ||
| * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` | ||
| * @param array $options | ||
| * @return null|ISchemaWrapper | ||
| */ | ||
| public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { | ||
| /** @var ISchemaWrapper $schema */ | ||
| $schema = $schemaClosure(); | ||
| $table = $schema->getTable('forms_v2_forms'); | ||
|
|
||
| if (!$table->hasColumn('allow_edit')) { | ||
| $table->addColumn('allow_edit', Types::BOOLEAN, [ | ||
| 'notnull' => false, | ||
| 'default' => 0, | ||
| ]); | ||
|
|
||
| return $schema; | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.