Skip to content
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

allow editing of submission by the user #1690

Open
wants to merge 27 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6f518d2
patch for AllowEdit for Forms5
tpokorra Dec 31, 2024
9034c1f
drop DeleteSubmission, and make Clear button work for AllowEdit
tpokorra Dec 31, 2024
1fea35e
fix php-cs lint issues
tpokorra Dec 31, 2024
fd18a62
small fixes
tpokorra Dec 31, 2024
95cb3a4
run prettier
tpokorra Dec 31, 2024
a16ad5c
more fixes
tpokorra Dec 31, 2024
1c18301
fix existing tests by adding AllowEdit
tpokorra Jan 3, 2025
c5e44e1
fix lint issue
tpokorra Jan 3, 2025
17f4e75
fix existing tests by adding AllowEdit
tpokorra Jan 3, 2025
f967505
fix testCanSubmit with AllowEdit=false
tpokorra Jan 3, 2025
4db993b
extend testCanSubmit by allowEditGood and allowEditNotGood
tpokorra Jan 3, 2025
a4e97e3
add unit test testUpdateSubmission_answers
tpokorra Jan 4, 2025
6a3a98b
add unit test testGetFormWithAnswers for AllowEdit
tpokorra Jan 4, 2025
360a729
add unit test testGetFormAllowEditWithoutAnswers
tpokorra Jan 4, 2025
47252a6
improve unit tests testGetFormAllowEditWithoutAnswers
tpokorra Jan 4, 2025
f686a59
use existing function loadFormForSubmission instead of new function c…
tpokorra Jan 4, 2025
b0dc7a7
new function canDeleteSubmission with Tests. improve tests for canDel…
tpokorra Jan 4, 2025
3fe01f8
drop function canDeleteSubmission. it is unrelated to this PR
tpokorra Jan 4, 2025
a29d569
adjust label for AllowEdit
tpokorra Jan 4, 2025
74f5f1e
rename migration to Version 5 and current date
tpokorra Jan 9, 2025
1c127c4
drop IconDeleteSvg from Submit.vue
tpokorra Jan 9, 2025
6ec4990
fix error messages with multiple ors
tpokorra Jan 9, 2025
556b5f2
use PUT for updating submission
tpokorra Jan 9, 2025
a875749
fail silently for MultipleObjectsReturnedException from findByFormAnd…
tpokorra Jan 9, 2025
ca947b5
updateSubmission: first check if editing is allowed and by this user
tpokorra Jan 9, 2025
19cd709
add integration test testUpdateSubmission
tpokorra Jan 9, 2025
095264b
fix missing MultipleObjectsReturnedException for FormsService
tpokorra Jan 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/DataStructure.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ This document describes the Object-Structure, that is used within the Forms App
| isAnonymous | Boolean | | If Answers will be stored anonymously |
| state | Integer | [Form state](#form-state) | The state of the form |
| submitMultiple | Boolean | | If users are allowed to submit multiple times to the form |
| allowEdit | Boolean | | If users are allowed to edit or delete their response |
| showExpiration | Boolean | | If the expiration date will be shown on the form |
| canSubmit | Boolean | | If the user can Submit to the form, i.e. calculated information out of `submitMultiple` and existing submissions. |
| permissions | Array of [Permissions](#permissions) | Array of permissions regarding the form |
Expand All @@ -46,6 +47,7 @@ This document describes the Object-Structure, that is used within the Forms App
"expires": 0,
"isAnonymous": false,
"submitMultiple": true,
"allowEdit": false,
"showExpiration": false,
"canSubmit": true,
"permissions": [
Expand Down
155 changes: 147 additions & 8 deletions lib/Controller/ApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@
'showToAllUsers' => false,
]);
$form->setSubmitMultiple(false);
$form->setAllowEdit(false);
$form->setShowExpiration(false);
$form->setExpires(0);
$form->setIsAnonymous(false);
Expand Down Expand Up @@ -1164,7 +1165,7 @@
continue;
}

$this->storeAnswersForQuestion($form, $submission->getId(), $questions[$questionIndex], $answerArray);
$this->storeAnswersForQuestion($form, $submission->getId(), $questions[$questionIndex], $answerArray, false);
}

$this->formMapper->update($form);
Expand All @@ -1179,6 +1180,85 @@
return new DataResponse();
}

/**
* Update an existing submission
*
* @param int $formId the form id
* @param int $submissionId the submission id
* @param array $answers [question_id => arrayOfString]
* @param string $shareHash public share-hash -> Necessary to submit on public link-shares.
* @return DataResponse
* @throws OCSBadRequestException
* @throws OCSForbiddenException
*/
#[CORS()]
#[NoAdminRequired()]
#[NoCSRFRequired()]
#[PublicPage()]
#[ApiRoute(verb: 'PUT', url: Constants::API_BASE . 'forms/{formId}/submissions/{submissionId}', requirements: Constants::API_V3_REQUIREMENTS)]
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() && $this->currentUser)) {
throw new OCSBadRequestException('Can only update if AllowEdit is set');

Check warning on line 1209 in lib/Controller/ApiController.php

View check run for this annotation

Codecov / codecov/patch

lib/Controller/ApiController.php#L1209

Added line #L1209 was not covered by tests
}

$questions = $this->formsService->getQuestions($formId);
// Is the submission valid
$isSubmissionValid = $this->submissionService->validateSubmission($questions, $answers, $form->getOwnerId());
if (is_string($isSubmissionValid)) {
throw new OCSBadRequestException($isSubmissionValid);

Check warning on line 1216 in lib/Controller/ApiController.php

View check run for this annotation

Codecov / codecov/patch

lib/Controller/ApiController.php#L1216

Added line #L1216 was not covered by tests
}
if ($isSubmissionValid === false) {
throw new OCSBadRequestException('At least one submitted answer is not valid');

Check warning on line 1219 in lib/Controller/ApiController.php

View check run for this annotation

Codecov / codecov/patch

lib/Controller/ApiController.php#L1219

Added line #L1219 was not covered by tests
}

// 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');

Check warning on line 1226 in lib/Controller/ApiController.php

View check run for this annotation

Codecov / codecov/patch

lib/Controller/ApiController.php#L1225-L1226

Added lines #L1225 - L1226 were not covered by tests
}

if ($submissionId != $submission->getId()) {
throw new OCSBadRequestException('Can only update your own submissions');

Check warning on line 1230 in lib/Controller/ApiController.php

View check run for this annotation

Codecov / codecov/patch

lib/Controller/ApiController.php#L1230

Added line #L1230 was not covered by tests
}

$submission->setTimestamp(time());
$this->submissionMapper->update($submission);

if (empty($answers)) {
// Clear Answers
foreach ($questions as $question) {
$this->storeAnswersForQuestion($form, $submission->getId(), $question, [''], true);

Check warning on line 1239 in lib/Controller/ApiController.php

View check run for this annotation

Codecov / codecov/patch

lib/Controller/ApiController.php#L1238-L1239

Added lines #L1238 - L1239 were not covered by tests
}
} 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();
}

/**
* Delete a specific submission
*
Expand Down Expand Up @@ -1391,14 +1471,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) {
private function storeAnswersForQuestion(Form $form, int $submissionId, array $question, array $answerArray, bool $update) {
// get stored answers for this question
$storedAnswers = [];
if ($update) {
$storedAnswers = $this->answerMapper->findBySubmissionAndQuestion($submissionId, $question['id']);
}

$newAnswerTexts = [];

foreach ($answerArray as $answer) {
$answerEntity = new Answer();
$answerEntity->setSubmissionId($submissionId);
Expand All @@ -1415,6 +1504,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) {

Check warning on line 1516 in lib/Controller/ApiController.php

View check run for this annotation

Codecov / codecov/patch

lib/Controller/ApiController.php#L1516

Added line #L1516 was not covered by tests
// nothing to be changed
$foundAnswer = true;
break;

Check warning on line 1519 in lib/Controller/ApiController.php

View check run for this annotation

Codecov / codecov/patch

lib/Controller/ApiController.php#L1518-L1519

Added lines #L1518 - L1519 were not covered by tests
}
}
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);
}

} elseif ($question['type'] === Constants::ANSWER_TYPE_FILE) {
$uploadedFile = $this->uploadedFileMapper->getByUploadedFileId($answer['uploadedFileId']);
$answerEntity->setFileId($uploadedFile->getFileId());
Expand All @@ -1434,20 +1550,43 @@
$file->move($folder->getPath() . '/' . $name);

$answerText = $name;

$answerEntity->setText($answerText);
$this->answerMapper->insert($answerEntity);
} 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);

Check warning on line 1562 in lib/Controller/ApiController.php

View check run for this annotation

Codecov / codecov/patch

lib/Controller/ApiController.php#L1560-L1562

Added lines #L1560 - L1562 were not covered by tests
} else {
if ($answerText === '') {
continue;

Check warning on line 1565 in lib/Controller/ApiController.php

View check run for this annotation

Codecov / codecov/patch

lib/Controller/ApiController.php#L1565

Added line #L1565 was not covered by tests
}
$answerEntity = new Answer();
$answerEntity->setSubmissionId($submissionId);
$answerEntity->setQuestionId($question['id']);
$answerEntity->setText($answerText);
$this->answerMapper->insert($answerEntity);
}
}

$answerEntity->setText($answerText);
$this->answerMapper->insert($answerEntity);
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();

Check warning on line 1583 in lib/Controller/ApiController.php

View check run for this annotation

Codecov / codecov/patch

lib/Controller/ApiController.php#L1583

Added line #L1583 was not covered by tests

if (empty($newAnswerTexts[$questionId]) || !in_array($storedAnswer->getText(), $newAnswerTexts[$questionId])) {
$this->answerMapper->delete($storedAnswer);

Check warning on line 1586 in lib/Controller/ApiController.php

View check run for this annotation

Codecov / codecov/patch

lib/Controller/ApiController.php#L1585-L1586

Added lines #L1585 - L1586 were not covered by tests
}
}
}
}

private function loadFormForSubmission(int $formId, string $shareHash): Form {
Expand Down
20 changes: 20 additions & 0 deletions lib/Db/AnswerMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,26 @@
return $this->findEntities($qb);
}

/**
* @param int $submissionId
* @param int $questionId
* @throws \OCP\AppFramework\Db\DoesNotExistException if not found
* @return Answer[]
*/

public function findBySubmissionAndQuestion(int $submissionId, int $questionId): array {
$qb = $this->db->getQueryBuilder();

Check warning on line 51 in lib/Db/AnswerMapper.php

View check run for this annotation

Codecov / codecov/patch

lib/Db/AnswerMapper.php#L50-L51

Added lines #L50 - L51 were not covered by tests

$qb->select('*')
->from($this->getTableName())
->where(
$qb->expr()->eq('submission_id', $qb->createNamedParameter($submissionId, IQueryBuilder::PARAM_INT)),
$qb->expr()->eq('question_id', $qb->createNamedParameter($questionId, IQueryBuilder::PARAM_INT))
);

Check warning on line 58 in lib/Db/AnswerMapper.php

View check run for this annotation

Codecov / codecov/patch

lib/Db/AnswerMapper.php#L53-L58

Added lines #L53 - L58 were not covered by tests

return $this->findEntities($qb);

Check warning on line 60 in lib/Db/AnswerMapper.php

View check run for this annotation

Codecov / codecov/patch

lib/Db/AnswerMapper.php#L60

Added line #L60 was not covered by tests
}

/**
* @param int $submissionId
*/
Expand Down
5 changes: 5 additions & 0 deletions lib/Db/Form.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
* @method void setIsAnonymous(bool $value)
* @method int getSubmitMultiple()
* @method void setSubmitMultiple(bool $value)
* @method int getAllowEdit()
* @method void setAllowEdit(bool $value)
* @method int getShowExpiration()
* @method void setShowExpiration(bool $value)
* @method int getLastUpdated()
Expand All @@ -56,6 +58,7 @@ class Form extends Entity {
protected $expires;
protected $isAnonymous;
protected $submitMultiple;
protected $allowEdit;
protected $showExpiration;
protected $submissionMessage;
protected $lastUpdated;
Expand All @@ -69,6 +72,7 @@ public function __construct() {
$this->addType('expires', 'integer');
$this->addType('isAnonymous', 'boolean');
$this->addType('submitMultiple', 'boolean');
$this->addType('allowEdit', 'boolean');
$this->addType('showExpiration', 'boolean');
$this->addType('lastUpdated', 'integer');
$this->addType('state', 'integer');
Expand Down Expand Up @@ -131,6 +135,7 @@ public function read() {
'expires' => (int)$this->getExpires(),
'isAnonymous' => (bool)$this->getIsAnonymous(),
'submitMultiple' => (bool)$this->getSubmitMultiple(),
'allowEdit' => (bool)$this->getAllowEdit(),
'showExpiration' => (bool)$this->getShowExpiration(),
'lastUpdated' => (int)$this->getLastUpdated(),
'submissionMessage' => $this->getSubmissionMessage(),
Expand Down
23 changes: 23 additions & 0 deletions lib/Db/SubmissionMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,29 @@
return $this->findEntities($qb);
}

/**
* @param int $formId
* @param string $userId
*
* @return Submission
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException if more than one result
* @throws \OCP\AppFramework\Db\DoesNotExistException if not found
*/
public function findByFormAndUser(int $formId, string $userId): Submission {
$qb = $this->db->getQueryBuilder();

Check warning on line 58 in lib/Db/SubmissionMapper.php

View check run for this annotation

Codecov / codecov/patch

lib/Db/SubmissionMapper.php#L57-L58

Added lines #L57 - L58 were not covered by tests

$qb->select('*')
->from($this->getTableName())
->where(
$qb->expr()->eq('form_id', $qb->createNamedParameter($formId, IQueryBuilder::PARAM_INT)),
$qb->expr()->eq('user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR))
)
//Newest submissions first
->orderBy('timestamp', 'DESC');

Check warning on line 67 in lib/Db/SubmissionMapper.php

View check run for this annotation

Codecov / codecov/patch

lib/Db/SubmissionMapper.php#L60-L67

Added lines #L60 - L67 were not covered by tests

return $this->findEntity($qb);

Check warning on line 69 in lib/Db/SubmissionMapper.php

View check run for this annotation

Codecov / codecov/patch

lib/Db/SubmissionMapper.php#L69

Added line #L69 was not covered by tests
}

/**
* @param int $id
* @return Submission
Expand Down
1 change: 1 addition & 0 deletions lib/FormsMigrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ public function import(IUser $user, IImportSource $importSource, OutputInterface
$form->setExpires($formData['expires']);
$form->setIsAnonymous($formData['isAnonymous']);
$form->setSubmitMultiple($formData['submitMultiple']);
$form->setAllowEdit($formData['allowEdit']);
$form->setShowExpiration($formData['showExpiration']);

$this->formMapper->insert($form);
Expand Down
42 changes: 42 additions & 0 deletions lib/Migration/Version050000Date20250109201500.php
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 {

Check warning on line 26 in lib/Migration/Version050000Date20250109201500.php

View check run for this annotation

Codecov / codecov/patch

lib/Migration/Version050000Date20250109201500.php#L26

Added line #L26 was not covered by tests
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
$table = $schema->getTable('forms_v2_forms');

Check warning on line 29 in lib/Migration/Version050000Date20250109201500.php

View check run for this annotation

Codecov / codecov/patch

lib/Migration/Version050000Date20250109201500.php#L28-L29

Added lines #L28 - L29 were not covered by tests

if (!$table->hasColumn('allow_edit')) {
$table->addColumn('allow_edit', Types::BOOLEAN, [
'notnull' => false,
'default' => 0,
]);

Check warning on line 35 in lib/Migration/Version050000Date20250109201500.php

View check run for this annotation

Codecov / codecov/patch

lib/Migration/Version050000Date20250109201500.php#L31-L35

Added lines #L31 - L35 were not covered by tests

return $schema;

Check warning on line 37 in lib/Migration/Version050000Date20250109201500.php

View check run for this annotation

Codecov / codecov/patch

lib/Migration/Version050000Date20250109201500.php#L37

Added line #L37 was not covered by tests
}

return null;

Check warning on line 40 in lib/Migration/Version050000Date20250109201500.php

View check run for this annotation

Codecov / codecov/patch

lib/Migration/Version050000Date20250109201500.php#L40

Added line #L40 was not covered by tests
}
}
Loading
Loading