Skip to content

Commit 44f0534

Browse files
tpokorraChartman123
authored andcommitted
feat: add submission editing
Signed-off-by: Timotheus Pokorra <timotheus.pokorra@solidcharity.com>
1 parent 6d539be commit 44f0534

30 files changed

Lines changed: 1515 additions & 46 deletions

docs/API_v3.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -822,6 +822,43 @@ Upload a file to an answer before form submission
822822
"data": {"uploadedFileId": integer, "fileName": "string"}
823823
```
824824

825+
### Get a specific submission
826+
827+
Get all Submissions to a Form
828+
829+
- Endpoint: `/api/v3/forms/{formId}/submissions/{submissionId}`
830+
- Method: `GET`
831+
- Url-Parameters:
832+
| Parameter | Type | Description |
833+
|-----------|---------|-------------|
834+
| _formId_ | Integer | ID of the form to get the submissions for |
835+
| _submissionId_ | Integer | ID of the submission to get |
836+
- Response: The submission
837+
838+
```
839+
"data": {
840+
"id": 6,
841+
"formId": 3,
842+
"userId": "jonas",
843+
"timestamp": 1611274453,
844+
"answers": [
845+
{
846+
"id": 8,
847+
"submissionId": 6,
848+
"questionId": 1,
849+
"text": "Option 3"
850+
},
851+
{
852+
"id": 9,
853+
"submissionId": 6,
854+
"questionId": 2,
855+
"text": "One more."
856+
},
857+
],
858+
"userDisplayName": "jonas"
859+
}
860+
```
861+
825862
### Insert a Submission
826863

827864
Store Submission to Database

docs/DataStructure.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ This document describes the Object-Structure, that is used within the Forms App
2727
| isAnonymous | Boolean | | If Answers will be stored anonymously |
2828
| state | Integer | [Form state](#form-state) | The state of the form |
2929
| submitMultiple | Boolean | | If users are allowed to submit multiple times to the form |
30+
| allowEditSubmissions | Boolean | | If users are allowed to edit or delete their response |
3031
| showExpiration | Boolean | | If the expiration date will be shown on the form |
3132
| canSubmit | Boolean | | If the user can Submit to the form, i.e. calculated information out of `submitMultiple` and existing submissions. |
3233
| permissions | Array of [Permissions](#permissions) | Array of permissions regarding the form |
@@ -46,6 +47,7 @@ This document describes the Object-Structure, that is used within the Forms App
4647
"expires": 0,
4748
"isAnonymous": false,
4849
"submitMultiple": true,
50+
"allowEditSubmissions": false,
4951
"showExpiration": false,
5052
"canSubmit": true,
5153
"permissions": [

lib/Controller/ApiController.php

Lines changed: 140 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
* @psalm-import-type FormsPartialForm from ResponseDefinitions
6363
* @psalm-import-type FormsQuestion from ResponseDefinitions
6464
* @psalm-import-type FormsQuestionType from ResponseDefinitions
65+
* @psalm-import-type FormsSubmission from ResponseDefinitions
6566
* @psalm-import-type FormsSubmissions from ResponseDefinitions
6667
* @psalm-import-type FormsUploadedFile from ResponseDefinitions
6768
*/
@@ -172,6 +173,7 @@ public function newForm(?int $fromId = null): DataResponse {
172173
'showToAllUsers' => false,
173174
]);
174175
$form->setSubmitMultiple(false);
176+
$form->setAllowEditSubmissions(false);
175177
$form->setShowExpiration(false);
176178
$form->setExpires(0);
177179
$form->setIsAnonymous(false);
@@ -1158,7 +1160,11 @@ public function getSubmissions(int $formId, ?string $fileFormat = null): DataRes
11581160
}
11591161

11601162
// Load submissions and currently active questions
1161-
$submissions = $this->submissionService->getSubmissions($formId);
1163+
if (in_array(Constants::PERMISSION_RESULTS, $this->formsService->getPermissions($form))) {
1164+
$submissions = $this->submissionService->getSubmissions($formId);
1165+
} else {
1166+
$submissions = $this->submissionService->getSubmissions($formId, $this->currentUser->getUID());
1167+
}
11621168
$questions = $this->formsService->getQuestions($formId);
11631169

11641170
// Append Display Names
@@ -1195,6 +1201,54 @@ public function getSubmissions(int $formId, ?string $fileFormat = null): DataRes
11951201
return new DataResponse($response);
11961202
}
11971203

1204+
/**
1205+
* Get a specific submission
1206+
*
1207+
* @param int $formId of the form
1208+
* @param int $submissionId of the submission
1209+
* @return DataResponse<Http::STATUS_OK, FormsSubmission, array{}>
1210+
* @throws OCSBadRequestException Submission doesn't belong to given form
1211+
* @throws OCSNotFoundException Could not find form
1212+
* @throws OCSNotFoundException Submission doesn't exist
1213+
* @throws OCSForbiddenException The current user has no permission to get this submission
1214+
*
1215+
* 200: the submissions of the form
1216+
*/
1217+
#[CORS()]
1218+
#[NoAdminRequired()]
1219+
#[BruteForceProtection(action: 'form')]
1220+
#[ApiRoute(verb: 'GET', url: '/api/v3/forms/{formId}/submissions/{submissionId}')]
1221+
public function getSubmission(int $formId, int $submissionId): DataResponse|DataDownloadResponse {
1222+
$form = $this->getFormIfAllowed($formId, Constants::PERMISSION_RESULTS);
1223+
1224+
$submission = $this->submissionService->getSubmission($submissionId);
1225+
if ($submission === null) {
1226+
throw new OCSNotFoundException('Submission doesn\'t exist');
1227+
}
1228+
1229+
if ($submission['formId'] !== $formId) {
1230+
throw new OCSBadRequestException('Submission doesn\'t belong to given form');
1231+
}
1232+
1233+
// Append Display Names
1234+
if (substr($submission['userId'], 0, 10) === 'anon-user-') {
1235+
// Anonymous User
1236+
// TRANSLATORS On Results when listing the single Responses to the form, this text is shown as heading of the Response.
1237+
$submission['userDisplayName'] = $this->l10n->t('Anonymous response');
1238+
} else {
1239+
$userEntity = $this->userManager->get($submission['userId']);
1240+
1241+
if ($userEntity instanceof IUser) {
1242+
$submission['userDisplayName'] = $userEntity->getDisplayName();
1243+
} else {
1244+
// Fallback, should not occur regularly.
1245+
$submission['userDisplayName'] = $submission['userId'];
1246+
}
1247+
}
1248+
1249+
return new DataResponse($submission);
1250+
}
1251+
11981252
/**
11991253
* Delete all submissions of a specified form
12001254
*
@@ -1309,6 +1363,84 @@ public function newSubmission(int $formId, array $answers, string $shareHash = '
13091363
return new DataResponse(null, Http::STATUS_CREATED);
13101364
}
13111365

1366+
/**
1367+
* Update an existing submission
1368+
*
1369+
* @param int $formId the form id
1370+
* @param int $submissionId the submission id
1371+
* @param array<string, list<string>> $answers [question_id => arrayOfString]
1372+
* @return DataResponse<Http::STATUS_OK, int, array{}>
1373+
* @throws OCSBadRequestException Can only update submission if allowEditSubmissions is set and the answers are valid
1374+
* @throws OCSForbiddenException Can only update your own submission
1375+
*
1376+
* 200: the id of the updated submission
1377+
*/
1378+
#[CORS()]
1379+
#[NoAdminRequired()]
1380+
#[NoCSRFRequired()]
1381+
#[PublicPage()]
1382+
#[ApiRoute(verb: 'PUT', url: '/api/v3/forms/{formId}/submissions/{submissionId}')]
1383+
public function updateSubmission(int $formId, int $submissionId, array $answers): DataResponse {
1384+
$this->logger->debug('Updating submission: formId: {formId}, answers: {answers}', [
1385+
'formId' => $formId,
1386+
'answers' => $answers,
1387+
]);
1388+
1389+
// submissions can't be updated on public shares, so passing empty shareHash
1390+
$form = $this->loadFormForSubmission($formId, '');
1391+
1392+
if (!$form->getAllowEditSubmissions()) {
1393+
throw new OCSBadRequestException('Can only update if allowEditSubmissions is set');
1394+
}
1395+
1396+
$questions = $this->formsService->getQuestions($formId);
1397+
try {
1398+
// Is the submission valid
1399+
$this->submissionService->validateSubmission($questions, $answers, $form->getOwnerId());
1400+
} catch (\InvalidArgumentException $e) {
1401+
throw new OCSBadRequestException($e->getMessage());
1402+
}
1403+
1404+
// get existing submission of this user
1405+
try {
1406+
$submission = $this->submissionMapper->findById($submissionId);
1407+
} catch (DoesNotExistException $e) {
1408+
throw new OCSBadRequestException('Submission doesn\'t exist');
1409+
}
1410+
1411+
if ($formId !== $submission->getFormId()) {
1412+
throw new OCSBadRequestException('Submission doesn\'t belong to given form');
1413+
}
1414+
1415+
if ($this->currentUser->getUID() !== $submission->getUserId()) {
1416+
throw new OCSForbiddenException('Can only update your own submissions');
1417+
}
1418+
1419+
$submission->setTimestamp(time());
1420+
$this->submissionMapper->update($submission);
1421+
1422+
// Delete current answers
1423+
$this->answerMapper->deleteBySubmission($submissionId);
1424+
1425+
// Process Answers
1426+
foreach ($answers as $questionId => $answerArray) {
1427+
// Search corresponding Question, skip processing if not found
1428+
$questionIndex = array_search($questionId, array_column($questions, 'id'));
1429+
if ($questionIndex === false) {
1430+
continue;
1431+
}
1432+
1433+
$question = $questions[$questionIndex];
1434+
1435+
$this->storeAnswersForQuestion($form, $submission->getId(), $question, $answerArray);
1436+
}
1437+
1438+
//Create Activity
1439+
$this->formsService->notifyNewSubmission($form, $submission);
1440+
1441+
return new DataResponse($submissionId);
1442+
}
1443+
13121444
/**
13131445
* Delete a specific submission
13141446
*
@@ -1343,6 +1475,13 @@ public function deleteSubmission(int $formId, int $submissionId): DataResponse {
13431475
throw new OCSBadRequestException('Submission doesn\'t belong to given form');
13441476
}
13451477

1478+
if (
1479+
!in_array(Constants::PERMISSION_RESULTS_DELETE, $this->formsService->getPermissions($form))
1480+
&& $this->currentUser->getUID() !== $submission->getUserId()
1481+
) {
1482+
throw new OCSForbiddenException('Can only delete your own submissions');
1483+
}
1484+
13461485
// Delete submission (incl. Answers)
13471486
$this->submissionMapper->deleteById($submissionId);
13481487
$this->formMapper->update($form);

lib/Controller/PageController.php

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use OCA\Forms\Db\Form;
1212
use OCA\Forms\Db\FormMapper;
1313
use OCA\Forms\Db\ShareMapper;
14+
use OCA\Forms\Db\SubmissionMapper;
1415
use OCA\Forms\Service\ConfigService;
1516
use OCA\Forms\Service\FormsService;
1617

@@ -45,6 +46,7 @@ public function __construct(
4546
IRequest $request,
4647
private FormMapper $formMapper,
4748
private ShareMapper $shareMapper,
49+
private SubmissionMapper $submissionMapper,
4850
private ConfigService $configService,
4951
private FormsService $formsService,
5052
private IAccountManager $accountManager,
@@ -63,7 +65,7 @@ public function __construct(
6365
#[NoAdminRequired()]
6466
#[NoCSRFRequired()]
6567
#[FrontpageRoute(verb: 'GET', url: '/')]
66-
public function index(?string $hash = null): TemplateResponse {
68+
public function index(?string $hash = null, ?int $submissionId = null): TemplateResponse {
6769
Util::addScript($this->appName, 'forms-main');
6870
Util::addStyle($this->appName, 'forms');
6971
Util::addStyle($this->appName, 'forms-style');
@@ -81,6 +83,15 @@ public function index(?string $hash = null): TemplateResponse {
8183
}
8284
}
8385

86+
if (isset($submissionId)) {
87+
try {
88+
$submission = $this->submissionMapper->findById($submissionId);
89+
$this->initialState->provideInitialState('submissionId', $submission->id);
90+
} catch (DoesNotExistException $e) {
91+
// Ignore exception and just don't set the initialState value
92+
}
93+
}
94+
8495
return new TemplateResponse($this->appName, self::TEMPLATE_MAIN, [
8596
'id-app-content' => '#app-content-vue',
8697
'id-app-navigation' => '#app-navigation-vue',
@@ -97,6 +108,16 @@ public function views(string $hash): TemplateResponse {
97108
return $this->index($hash);
98109
}
99110

111+
/**
112+
* @return TemplateResponse
113+
*/
114+
#[NoAdminRequired()]
115+
#[NoCSRFRequired()]
116+
#[FrontpageRoute(verb: 'GET', url: '/{hash}/submit/{submissionId}', requirements: ['hash' => '[a-zA-Z0-9]{16,}', 'submissionId' => '\d+'])]
117+
public function submitViewWithSubmission(string $hash, int $submissionId): TemplateResponse {
118+
return $this->formMapper->findByHash($hash)->getAllowEditSubmissions() ? $this->index($hash, $submissionId) : $this->index($hash);
119+
}
120+
100121
/**
101122
* @param string $hash
102123
* @return RedirectResponse|TemplateResponse Redirect to login or internal view.

lib/Db/Form.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
* @method void setIsAnonymous(bool $value)
3636
* @method int getSubmitMultiple()
3737
* @method void setSubmitMultiple(bool $value)
38+
* @method int getAllowEditSubmissions()
39+
* @method void setAllowEditSubmissions(bool $value)
3840
* @method int getShowExpiration()
3941
* @method void setShowExpiration(bool $value)
4042
* @method int getLastUpdated()
@@ -58,6 +60,7 @@ class Form extends Entity {
5860
protected $expires;
5961
protected $isAnonymous;
6062
protected $submitMultiple;
63+
protected $allowEditSubmissions;
6164
protected $showExpiration;
6265
protected $submissionMessage;
6366
protected $lastUpdated;
@@ -71,6 +74,7 @@ public function __construct() {
7174
$this->addType('expires', 'integer');
7275
$this->addType('isAnonymous', 'boolean');
7376
$this->addType('submitMultiple', 'boolean');
77+
$this->addType('allowEditSubmissions', 'boolean');
7478
$this->addType('showExpiration', 'boolean');
7579
$this->addType('lastUpdated', 'integer');
7680
$this->addType('state', 'integer');
@@ -140,6 +144,7 @@ public function setAccess(array $access): void {
140144
* expires: int,
141145
* isAnonymous: bool,
142146
* submitMultiple: bool,
147+
* allowEditSubmissions: bool,
143148
* showExpiration: bool,
144149
* lastUpdated: int,
145150
* submissionMessage: ?string,
@@ -160,6 +165,7 @@ public function read() {
160165
'expires' => (int)$this->getExpires(),
161166
'isAnonymous' => (bool)$this->getIsAnonymous(),
162167
'submitMultiple' => (bool)$this->getSubmitMultiple(),
168+
'allowEditSubmissions' => (bool)$this->getAllowEditSubmissions(),
163169
'showExpiration' => (bool)$this->getShowExpiration(),
164170
'lastUpdated' => (int)$this->getLastUpdated(),
165171
'submissionMessage' => $this->getSubmissionMessage(),

lib/Db/SubmissionMapper.php

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,28 @@ public function findByForm(int $formId): array {
4747
return $this->findEntities($qb);
4848
}
4949

50+
/**
51+
* @param int $formId
52+
* @param string $userId
53+
*
54+
* @return Submission[]
55+
* @throws \OCP\AppFramework\Db\DoesNotExistException if not found
56+
*/
57+
public function findByFormAndUser(int $formId, string $userId): array {
58+
$qb = $this->db->getQueryBuilder();
59+
60+
$qb->select('*')
61+
->from($this->getTableName())
62+
->where(
63+
$qb->expr()->eq('form_id', $qb->createNamedParameter($formId, IQueryBuilder::PARAM_INT)),
64+
$qb->expr()->eq('user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR))
65+
)
66+
//Newest submissions first
67+
->orderBy('timestamp', 'DESC');
68+
69+
return $this->findEntities($qb);
70+
}
71+
5072
/**
5173
* @param int $id
5274
* @return Submission
@@ -86,10 +108,11 @@ public function hasFormSubmissionsByUser(Form $form, string $userId): bool {
86108
/**
87109
* Count submissions by form
88110
* @param int $formId ID of the form to count submissions
111+
* @param null|string $userId (optional) ID of the current user, defaults to `null`
89112
* @throws \Exception
90113
*/
91-
public function countSubmissions(int $formId): int {
92-
return $this->countSubmissionsWithFilters($formId, null, -1);
114+
public function countSubmissions(int $formId, ?string $userId = null): int {
115+
return $this->countSubmissionsWithFilters($formId, $userId, -1);
93116
}
94117

95118
/**

lib/FormsMigrator.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ public function import(IUser $user, IImportSource $importSource, OutputInterface
146146
$form->setExpires($formData['expires']);
147147
$form->setIsAnonymous($formData['isAnonymous']);
148148
$form->setSubmitMultiple($formData['submitMultiple']);
149+
$form->setAllowEditSubmissions($formData['allowEditSubmissions']);
149150
$form->setShowExpiration($formData['showExpiration']);
150151

151152
$this->formMapper->insert($form);

0 commit comments

Comments
 (0)