diff --git a/docs/API_v3.md b/docs/API_v3.md index 006f95bbd..0f26ba77f 100644 --- a/docs/API_v3.md +++ b/docs/API_v3.md @@ -822,6 +822,43 @@ Upload a file to an answer before form submission "data": {"uploadedFileId": integer, "fileName": "string"} ``` +### Get a specific submission + +Get all Submissions to a Form + +- Endpoint: `/api/v3/forms/{formId}/submissions/{submissionId}` +- Method: `GET` +- Url-Parameters: + | Parameter | Type | Description | + |-----------|---------|-------------| + | _formId_ | Integer | ID of the form to get the submissions for | + | _submissionId_ | Integer | ID of the submission to get | +- Response: The submission + +``` +"data": { + "id": 6, + "formId": 3, + "userId": "jonas", + "timestamp": 1611274453, + "answers": [ + { + "id": 8, + "submissionId": 6, + "questionId": 1, + "text": "Option 3" + }, + { + "id": 9, + "submissionId": 6, + "questionId": 2, + "text": "One more." + }, + ], + "userDisplayName": "jonas" +} +``` + ### Insert a Submission Store Submission to Database diff --git a/docs/DataStructure.md b/docs/DataStructure.md index b2e6169cc..9e3abff23 100644 --- a/docs/DataStructure.md +++ b/docs/DataStructure.md @@ -13,26 +13,27 @@ This document describes the Object-Structure, that is used within the Forms App ### Form -| Property | Type | Restrictions | Description | -| ----------------- | ------------------------------------ | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -| id | Integer | unique | An instance-wide unique id of the form | -| hash | 16-char String | unique | An instance-wide unique hash | -| title | String | max. 256 ch. | The form title | -| description | String | max. 8192 ch. | The Form description | -| ownerId | String | | The nextcloud userId of the form owner | -| submissionMessage | String | max. 2048 ch. | Optional custom message, with Markdown support, to be shown to users when the form is submitted (default is used if set to null) | -| created | unix timestamp | | When the form has been created | -| access | [Access-Object](#access-object) | | Describing access-settings of the form | -| expires | unix-timestamp | | When the form should expire. Timestamp `0` indicates _never_ | -| 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 | -| 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 | -| questions | Array of [Questions](#question) | | Array of questions belonging to the form | -| shares | Array of [Shares](#share) | | Array of shares of the form | -| submissions | Array of [Submissions](#submission) | | Array of submissions belonging to the form | +| Property | Type | Restrictions | Description | +| -------------------- | ------------------------------------ | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| id | Integer | unique | An instance-wide unique id of the form | +| hash | 16-char String | unique | An instance-wide unique hash | +| title | String | max. 256 ch. | The form title | +| description | String | max. 8192 ch. | The Form description | +| ownerId | String | | The nextcloud userId of the form owner | +| submissionMessage | String | max. 2048 ch. | Optional custom message, with Markdown support, to be shown to users when the form is submitted (default is used if set to null) | +| created | unix timestamp | | When the form has been created | +| access | [Access-Object](#access-object) | | Describing access-settings of the form | +| expires | unix-timestamp | | When the form should expire. Timestamp `0` indicates _never_ | +| 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 | +| allowEditSubmissions | 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 | +| questions | Array of [Questions](#question) | | Array of questions belonging to the form | +| shares | Array of [Shares](#share) | | Array of shares of the form | +| submissions | Array of [Submissions](#submission) | | Array of submissions belonging to the form | ``` { @@ -46,6 +47,7 @@ This document describes the Object-Structure, that is used within the Forms App "expires": 0, "isAnonymous": false, "submitMultiple": true, + "allowEditSubmissions": false, "showExpiration": false, "canSubmit": true, "permissions": [ diff --git a/lib/Controller/ApiController.php b/lib/Controller/ApiController.php index 15d9e9d50..41849153b 100644 --- a/lib/Controller/ApiController.php +++ b/lib/Controller/ApiController.php @@ -62,6 +62,7 @@ * @psalm-import-type FormsPartialForm from ResponseDefinitions * @psalm-import-type FormsQuestion from ResponseDefinitions * @psalm-import-type FormsQuestionType from ResponseDefinitions + * @psalm-import-type FormsSubmission from ResponseDefinitions * @psalm-import-type FormsSubmissions from ResponseDefinitions * @psalm-import-type FormsUploadedFile from ResponseDefinitions */ @@ -172,6 +173,7 @@ public function newForm(?int $fromId = null): DataResponse { 'showToAllUsers' => false, ]); $form->setSubmitMultiple(false); + $form->setAllowEditSubmissions(false); $form->setShowExpiration(false); $form->setExpires(0); $form->setIsAnonymous(false); @@ -1158,7 +1160,11 @@ public function getSubmissions(int $formId, ?string $fileFormat = null): DataRes } // Load submissions and currently active questions - $submissions = $this->submissionService->getSubmissions($formId); + if (in_array(Constants::PERMISSION_RESULTS, $this->formsService->getPermissions($form))) { + $submissions = $this->submissionService->getSubmissions($formId); + } else { + $submissions = $this->submissionService->getSubmissions($formId, $this->currentUser->getUID()); + } $questions = $this->formsService->getQuestions($formId); // Append Display Names @@ -1195,6 +1201,54 @@ public function getSubmissions(int $formId, ?string $fileFormat = null): DataRes return new DataResponse($response); } + /** + * Get a specific submission + * + * @param int $formId of the form + * @param int $submissionId of the submission + * @return DataResponse + * @throws OCSBadRequestException Submission doesn't belong to given form + * @throws OCSNotFoundException Could not find form + * @throws OCSNotFoundException Submission doesn't exist + * @throws OCSForbiddenException The current user has no permission to get this submission + * + * 200: the submissions of the form + */ + #[CORS()] + #[NoAdminRequired()] + #[BruteForceProtection(action: 'form')] + #[ApiRoute(verb: 'GET', url: '/api/v3/forms/{formId}/submissions/{submissionId}')] + public function getSubmission(int $formId, int $submissionId): DataResponse|DataDownloadResponse { + $form = $this->getFormIfAllowed($formId, Constants::PERMISSION_RESULTS); + + $submission = $this->submissionService->getSubmission($submissionId); + if ($submission === null) { + throw new OCSNotFoundException('Submission doesn\'t exist'); + } + + if ($submission['formId'] !== $formId) { + throw new OCSBadRequestException('Submission doesn\'t belong to given form'); + } + + // Append Display Names + if (substr($submission['userId'], 0, 10) === 'anon-user-') { + // Anonymous User + // TRANSLATORS On Results when listing the single Responses to the form, this text is shown as heading of the Response. + $submission['userDisplayName'] = $this->l10n->t('Anonymous response'); + } else { + $userEntity = $this->userManager->get($submission['userId']); + + if ($userEntity instanceof IUser) { + $submission['userDisplayName'] = $userEntity->getDisplayName(); + } else { + // Fallback, should not occur regularly. + $submission['userDisplayName'] = $submission['userId']; + } + } + + return new DataResponse($submission); + } + /** * Delete all submissions of a specified form * @@ -1309,6 +1363,84 @@ public function newSubmission(int $formId, array $answers, string $shareHash = ' 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> $answers [question_id => arrayOfString] + * @return DataResponse + * @throws OCSBadRequestException Can only update submission if allowEditSubmissions 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): DataResponse { + $this->logger->debug('Updating submission: formId: {formId}, answers: {answers}', [ + 'formId' => $formId, + 'answers' => $answers, + ]); + + // submissions can't be updated on public shares, so passing empty shareHash + $form = $this->loadFormForSubmission($formId, ''); + + if (!$form->getAllowEditSubmissions()) { + throw new OCSBadRequestException('Can only update if allowEditSubmissions is set'); + } + + $questions = $this->formsService->getQuestions($formId); + try { + // Is the submission valid + $this->submissionService->validateSubmission($questions, $answers, $form->getOwnerId()); + } catch (\InvalidArgumentException $e) { + throw new OCSBadRequestException($e->getMessage()); + } + + // get existing submission of this user + try { + $submission = $this->submissionMapper->findById($submissionId); + } catch (DoesNotExistException $e) { + throw new OCSBadRequestException('Submission doesn\'t exist'); + } + + if ($formId !== $submission->getFormId()) { + throw new OCSBadRequestException('Submission doesn\'t belong to given form'); + } + + if ($this->currentUser->getUID() !== $submission->getUserId()) { + throw new OCSForbiddenException('Can only update your own submissions'); + } + + $submission->setTimestamp(time()); + $this->submissionMapper->update($submission); + + // Delete current answers + $this->answerMapper->deleteBySubmission($submissionId); + + // 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); + } + + //Create Activity + $this->formsService->notifyNewSubmission($form, $submission); + + return new DataResponse($submissionId); + } + /** * Delete a specific submission * @@ -1343,6 +1475,13 @@ public function deleteSubmission(int $formId, int $submissionId): DataResponse { throw new OCSBadRequestException('Submission doesn\'t belong to given form'); } + if ( + !in_array(Constants::PERMISSION_RESULTS_DELETE, $this->formsService->getPermissions($form)) + && $this->currentUser->getUID() !== $submission->getUserId() + ) { + throw new OCSForbiddenException('Can only delete your own submissions'); + } + // Delete submission (incl. Answers) $this->submissionMapper->deleteById($submissionId); $this->formMapper->update($form); diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php index 6ef59227e..43db2366a 100644 --- a/lib/Controller/PageController.php +++ b/lib/Controller/PageController.php @@ -11,6 +11,7 @@ use OCA\Forms\Db\Form; use OCA\Forms\Db\FormMapper; use OCA\Forms\Db\ShareMapper; +use OCA\Forms\Db\SubmissionMapper; use OCA\Forms\Service\ConfigService; use OCA\Forms\Service\FormsService; @@ -45,6 +46,7 @@ public function __construct( IRequest $request, private FormMapper $formMapper, private ShareMapper $shareMapper, + private SubmissionMapper $submissionMapper, private ConfigService $configService, private FormsService $formsService, private IAccountManager $accountManager, @@ -63,7 +65,7 @@ public function __construct( #[NoAdminRequired()] #[NoCSRFRequired()] #[FrontpageRoute(verb: 'GET', url: '/')] - public function index(?string $hash = null): TemplateResponse { + public function index(?string $hash = null, ?int $submissionId = null): TemplateResponse { Util::addScript($this->appName, 'forms-main'); Util::addStyle($this->appName, 'forms'); Util::addStyle($this->appName, 'forms-style'); @@ -81,6 +83,15 @@ public function index(?string $hash = null): TemplateResponse { } } + if (isset($submissionId)) { + try { + $submission = $this->submissionMapper->findById($submissionId); + $this->initialState->provideInitialState('submissionId', $submission->id); + } catch (DoesNotExistException $e) { + // Ignore exception and just don't set the initialState value + } + } + return new TemplateResponse($this->appName, self::TEMPLATE_MAIN, [ 'id-app-content' => '#app-content-vue', 'id-app-navigation' => '#app-navigation-vue', @@ -97,6 +108,16 @@ public function views(string $hash): TemplateResponse { return $this->index($hash); } + /** + * @return TemplateResponse + */ + #[NoAdminRequired()] + #[NoCSRFRequired()] + #[FrontpageRoute(verb: 'GET', url: '/{hash}/submit/{submissionId}', requirements: ['hash' => '[a-zA-Z0-9]{16,}', 'submissionId' => '\d+'])] + public function submitViewWithSubmission(string $hash, int $submissionId): TemplateResponse { + return $this->formMapper->findByHash($hash)->getAllowEditSubmissions() ? $this->index($hash, $submissionId) : $this->index($hash); + } + /** * @param string $hash * @return RedirectResponse|TemplateResponse Redirect to login or internal view. diff --git a/lib/Db/Form.php b/lib/Db/Form.php index 77fcf1e7f..a49181be6 100644 --- a/lib/Db/Form.php +++ b/lib/Db/Form.php @@ -35,6 +35,8 @@ * @method void setIsAnonymous(bool $value) * @method int getSubmitMultiple() * @method void setSubmitMultiple(bool $value) + * @method int getAllowEditSubmissions() + * @method void setAllowEditSubmissions(bool $value) * @method int getShowExpiration() * @method void setShowExpiration(bool $value) * @method int getLastUpdated() @@ -58,6 +60,7 @@ class Form extends Entity { protected $expires; protected $isAnonymous; protected $submitMultiple; + protected $allowEditSubmissions; protected $showExpiration; protected $submissionMessage; protected $lastUpdated; @@ -71,6 +74,7 @@ public function __construct() { $this->addType('expires', 'integer'); $this->addType('isAnonymous', 'boolean'); $this->addType('submitMultiple', 'boolean'); + $this->addType('allowEditSubmissions', 'boolean'); $this->addType('showExpiration', 'boolean'); $this->addType('lastUpdated', 'integer'); $this->addType('state', 'integer'); @@ -140,6 +144,7 @@ public function setAccess(array $access): void { * expires: int, * isAnonymous: bool, * submitMultiple: bool, + * allowEditSubmissions: bool, * showExpiration: bool, * lastUpdated: int, * submissionMessage: ?string, @@ -160,6 +165,7 @@ public function read() { 'expires' => (int)$this->getExpires(), 'isAnonymous' => (bool)$this->getIsAnonymous(), 'submitMultiple' => (bool)$this->getSubmitMultiple(), + 'allowEditSubmissions' => (bool)$this->getAllowEditSubmissions(), 'showExpiration' => (bool)$this->getShowExpiration(), 'lastUpdated' => (int)$this->getLastUpdated(), 'submissionMessage' => $this->getSubmissionMessage(), diff --git a/lib/Db/SubmissionMapper.php b/lib/Db/SubmissionMapper.php index a6c5d8031..dd1a4e585 100644 --- a/lib/Db/SubmissionMapper.php +++ b/lib/Db/SubmissionMapper.php @@ -47,6 +47,28 @@ public function findByForm(int $formId): array { return $this->findEntities($qb); } + /** + * @param int $formId + * @param string $userId + * + * @return Submission[] + * @throws \OCP\AppFramework\Db\DoesNotExistException if not found + */ + public function findByFormAndUser(int $formId, string $userId): array { + $qb = $this->db->getQueryBuilder(); + + $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'); + + return $this->findEntities($qb); + } + /** * @param int $id * @return Submission @@ -86,10 +108,11 @@ public function hasFormSubmissionsByUser(Form $form, string $userId): bool { /** * Count submissions by form * @param int $formId ID of the form to count submissions + * @param null|string $userId (optional) ID of the current user, defaults to `null` * @throws \Exception */ - public function countSubmissions(int $formId): int { - return $this->countSubmissionsWithFilters($formId, null, -1); + public function countSubmissions(int $formId, ?string $userId = null): int { + return $this->countSubmissionsWithFilters($formId, $userId, -1); } /** diff --git a/lib/FormsMigrator.php b/lib/FormsMigrator.php index 4a56c3c61..9ea85f18a 100644 --- a/lib/FormsMigrator.php +++ b/lib/FormsMigrator.php @@ -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->setAllowEditSubmissions($formData['allowEditSubmissions']); $form->setShowExpiration($formData['showExpiration']); $this->formMapper->insert($form); diff --git a/lib/Migration/Version050200Date20250109201500.php b/lib/Migration/Version050200Date20250109201500.php new file mode 100644 index 000000000..08b310847 --- /dev/null +++ b/lib/Migration/Version050200Date20250109201500.php @@ -0,0 +1,42 @@ +getTable('forms_v2_forms'); + + if (!$table->hasColumn('allow_edit_submissions')) { + $table->addColumn('allow_edit_submissions', Types::BOOLEAN, [ + 'notnull' => false, + 'default' => 0, + ]); + + return $schema; + } + + return null; + } +} diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php index 2b0fcffdf..c3d0d1d7b 100644 --- a/lib/ResponseDefinitions.php +++ b/lib/ResponseDefinitions.php @@ -121,6 +121,7 @@ * isAnonymous: bool, * lastUpdated: int, * submitMultiple: bool, + * allowEditSubmissions: bool, * showExpiration: bool, * canSubmit: bool, * permissions: list, diff --git a/lib/Service/FormsService.php b/lib/Service/FormsService.php index 104aba27f..bd4e1e77a 100644 --- a/lib/Service/FormsService.php +++ b/lib/Service/FormsService.php @@ -203,6 +203,13 @@ public function getForm(Form $form): array { // Append submissionCount if currentUser has permissions to see results if (in_array(Constants::PERMISSION_RESULTS, $result['permissions'])) { $result['submissionCount'] = $this->submissionMapper->countSubmissions($form->getId()); + } elseif ($this->currentUser) { + $userSubmissionCount = $this->submissionMapper->countSubmissions($form->getId(), $this->currentUser->getUID()); + if ($userSubmissionCount > 0) { + $result['submissionCount'] = $userSubmissionCount; + // Append `results` permission if user has submitted to the form + $result['permissions'][] = Constants::PERMISSION_RESULTS; + } } if ($result['fileId']) { @@ -239,6 +246,13 @@ public function getPartialFormArray(Form $form): array { // Append submissionCount if currentUser has permissions to see results if (in_array(Constants::PERMISSION_RESULTS, $result['permissions'])) { $result['submissionCount'] = $this->submissionMapper->countSubmissions($form->getId()); + } else { + $userSubmissionCount = $this->submissionMapper->countSubmissions($form->getId(), $this->currentUser->getUID()); + if ($userSubmissionCount > 0) { + $result['submissionCount'] = $userSubmissionCount; + // Append `results` permission if user has submitted to the form + $result['permissions'][] = Constants::PERMISSION_RESULTS; + } } return $result; @@ -309,29 +323,47 @@ public function canEditForm(Form $form): bool { } /** - * Can the current user see results of a form + * Determines if the current user has permission to view the results of a given form. * - * @param Form $form - * @return boolean + * A user can see the results of a form if they have made at least one submission + * to the form or possess the required permission to view results. + * + * @param Form $form The form for which the results visibility is being checked. + * @return bool True if the user can see the results, false otherwise. */ public function canSeeResults(Form $form): bool { - return in_array(Constants::PERMISSION_RESULTS, $this->getPermissions($form)); + return $this->submissionMapper->countSubmissions($form->getId(), $this->currentUser->getUID()) > 0 + || in_array(Constants::PERMISSION_RESULTS, $this->getPermissions($form)); } /** - * Can the current user delete results of a form + * Determines if the current user has permission to delete the results of a given form. * - * @param Form $form - * @return boolean + * A user can delete the results of a form if the form is not archived and one of the following conditions is met: + * - The user has the "results_delete" permission. + * - The user has not submitted any responses, and the form allows editing. + * - The form is not archived. + * + * @param Form $form The form for which the results deletion permission is being checked. + * @return bool True if the user can delete the results, false otherwise. */ public function canDeleteResults(Form $form): bool { - // Check permissions - if (!in_array(Constants::PERMISSION_RESULTS_DELETE, $this->getPermissions($form))) { + // Do not allow deleting results on archived forms + if ($this->isFormArchived($form)) { return false; } + + // Allow deleting results if the current user has the "results_delete" permission + if (in_array(Constants::PERMISSION_RESULTS_DELETE, $this->getPermissions($form))) { + return true; + } - // Do not allow deleting results on archived forms - return !$this->isFormArchived($form); + // Allow deleting results if the current user has already submitted + if ($form->getAllowEditSubmissions() && $this->submissionMapper->countSubmissions($form->getId(), $this->currentUser->getUID()) > 0) { + return true; + } + + return false; } /** @@ -351,7 +383,7 @@ public function canSubmit(Form $form): bool { return true; } - // Refuse access, if SubmitMultiple is not set and user already has taken part. + // Refuse access, if submitMultiple is not set and user already has taken part. if ( !$form->getSubmitMultiple() && $this->submissionMapper->hasFormSubmissionsByUser($form, $this->currentUser->getUID()) @@ -445,7 +477,7 @@ private function isSharedFormShown(Form $form): bool { return false; } - // Shown if permitall and showntoall are both set. + // Shown if permitAll and showToAll are both set. if ($form->getAccess()['permitAllUsers'] && $form->getAccess()['showToAllUsers'] && $this->configService->getAllowPermitAll() && diff --git a/lib/Service/SubmissionService.php b/lib/Service/SubmissionService.php index b6cf09be7..b43fa2039 100644 --- a/lib/Service/SubmissionService.php +++ b/lib/Service/SubmissionService.php @@ -91,6 +91,7 @@ private function getAnswers(int $submissionId): array { * Get all submissions of a form * * @param int $formId the form id + * @param string|null $userId optional user id to filter submissions * @return list, * }> */ - public function getSubmissions(int $formId): array { + public function getSubmissions(int $formId, ?string $userId = null): array { $submissionList = []; try { - $submissionEntities = $this->submissionMapper->findByForm($formId); + if (is_null($userId)) { + $submissionEntities = $this->submissionMapper->findByForm($formId); + } else { + $submissionEntities = $this->submissionMapper->findByFormAndUser($formId, $userId); + } + foreach ($submissionEntities as $submissionEntity) { $submission = $submissionEntity->read(); $submission['answers'] = $this->getAnswers($submission['id']); @@ -115,6 +121,29 @@ public function getSubmissions(int $formId): array { } } + /** + * Load specific submission + * + * @param integer $submissionId id of the submission + * @return array{ + * id: int, + * formId: int, + * userId: string, + * timestamp: int, + * answers: list, + * } + */ + public function getSubmission(int $submissionId): ?array { + try { + $submissionEntity = $this->submissionMapper->findById($submissionId); + $submission = $submissionEntity->read(); + $submission['answers'] = $this->getAnswers($submission['id']); + return $submission; + } catch (DoesNotExistException $e) { + return null; + } + } + /** * Export Submissions to Cloud-Filesystem * diff --git a/openapi.json b/openapi.json index 5e7254bf3..2064ddd35 100644 --- a/openapi.json +++ b/openapi.json @@ -105,6 +105,7 @@ "isAnonymous", "lastUpdated", "submitMultiple", + "allowEditSubmissions", "showExpiration", "canSubmit", "permissions", @@ -164,6 +165,9 @@ "submitMultiple": { "type": "boolean" }, + "allowEditSubmissions": { + "type": "boolean" + }, "showExpiration": { "type": "boolean" }, @@ -3450,6 +3454,327 @@ } }, "/ocs/v2.php/apps/forms/api/v3/forms/{formId}/submissions/{submissionId}": { + "get": { + "operationId": "api-get-submission", + "summary": "Get a specific submission", + "description": "This endpoint allows CORS requests", + "tags": [ + "api" + ], + "security": [ + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "formId", + "in": "path", + "description": "of the form", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "submissionId", + "in": "path", + "description": "of the submission", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "the submissions of the form", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "$ref": "#/components/schemas/Submission" + } + } + } + } + } + } + } + }, + "400": { + "description": "Submission doesn't belong to given form", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "404": { + "description": "Submission doesn't exist", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "The current user has no permission to get this submission", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + }, + "put": { + "operationId": "api-update-submission", + "summary": "Update an existing submission", + "description": "This endpoint allows CORS requests", + "tags": [ + "api" + ], + "security": [ + {}, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "answers" + ], + "properties": { + "answers": { + "type": "object", + "description": "[question_id => arrayOfString]", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "formId", + "in": "path", + "description": "the form id", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "submissionId", + "in": "path", + "description": "the submission id", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "the id of the updated submission", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "integer", + "format": "int64" + } + } + } + } + } + } + } + }, + "400": { + "description": "Can only update submission if allowEditSubmissions is set and the answers are valid", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + }, + "403": { + "description": "Can only update your own submission", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + }, "delete": { "operationId": "api-delete-submission", "summary": "Delete a specific submission", diff --git a/src/Forms.vue b/src/Forms.vue index 73d5be05a..c8f24f018 100644 --- a/src/Forms.vue +++ b/src/Forms.vue @@ -286,6 +286,9 @@ export default { return {} }, set(form) { + // always close sidebar + this.sidebarOpened = false + // If a owned form let index = this.forms.findIndex( (search) => search.hash === this.routeHash, diff --git a/src/FormsSubmit.vue b/src/FormsSubmit.vue index 4b5999ce6..743ff0950 100644 --- a/src/FormsSubmit.vue +++ b/src/FormsSubmit.vue @@ -9,7 +9,8 @@ :form="form" public-view :share-hash="shareHash" - :is-logged-in="isLoggedIn" /> + :is-logged-in="isLoggedIn" + :sidebar-opened="false" /> diff --git a/src/components/Questions/QuestionDropdown.vue b/src/components/Questions/QuestionDropdown.vue index f58d75d2d..9c891018f 100644 --- a/src/components/Questions/QuestionDropdown.vue +++ b/src/components/Questions/QuestionDropdown.vue @@ -150,7 +150,7 @@ export default { } const selected = this.values.map((id) => - this.options.find((option) => option.id === id), + this.options.find((option) => option.id === parseInt(id)), ) return this.isMultiple ? selected : selected[0] diff --git a/src/components/Results/Submission.vue b/src/components/Results/Submission.vue index 9daed1983..28a16e1c7 100644 --- a/src/components/Results/Submission.vue +++ b/src/components/Results/Submission.vue @@ -10,6 +10,17 @@ {{ submission.userDisplayName }} + + + {{ t('forms', 'Edit this response') }} +