Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
37 changes: 37 additions & 0 deletions docs/API_v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
42 changes: 22 additions & 20 deletions docs/DataStructure.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

```
{
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,
"allowEditSubmissions": false,
"showExpiration": false,
"canSubmit": true,
"permissions": [
Expand Down
141 changes: 140 additions & 1 deletion lib/Controller/ApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -172,6 +173,7 @@
'showToAllUsers' => false,
]);
$form->setSubmitMultiple(false);
$form->setAllowEditSubmissions(false);
$form->setShowExpiration(false);
$form->setExpires(0);
$form->setIsAnonymous(false);
Expand Down Expand Up @@ -1158,7 +1160,11 @@
}

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

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

View check run for this annotation

Codecov / codecov/patch

lib/Controller/ApiController.php#L1164

Added line #L1164 was not covered by tests
} else {
$submissions = $this->submissionService->getSubmissions($formId, $this->currentUser->getUID());
}
$questions = $this->formsService->getQuestions($formId);

// Append Display Names
Expand Down Expand Up @@ -1195,6 +1201,54 @@
return new DataResponse($response);
}

/**
* Get a specific submission
*
* @param int $formId of the form
* @param int $submissionId of the submission
* @return DataResponse<Http::STATUS_OK, FormsSubmission, array{}>
* @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
*
Expand Down Expand Up @@ -1309,6 +1363,84 @@
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]
* @return DataResponse<Http::STATUS_OK, int, array{}>
* @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;

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

View check run for this annotation

Codecov / codecov/patch

lib/Controller/ApiController.php#L1430

Added line #L1430 was not covered by tests
}

$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
*
Expand Down Expand Up @@ -1343,6 +1475,13 @@
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');

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

View check run for this annotation

Codecov / codecov/patch

lib/Controller/ApiController.php#L1482

Added line #L1482 was not covered by tests
}

// Delete submission (incl. Answers)
$this->submissionMapper->deleteById($submissionId);
$this->formMapper->update($form);
Expand Down
23 changes: 22 additions & 1 deletion lib/Controller/PageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -45,6 +46,7 @@
IRequest $request,
private FormMapper $formMapper,
private ShareMapper $shareMapper,
private SubmissionMapper $submissionMapper,
private ConfigService $configService,
private FormsService $formsService,
private IAccountManager $accountManager,
Expand All @@ -63,7 +65,7 @@
#[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');
Expand All @@ -81,6 +83,15 @@
}
}

if (isset($submissionId)) {

Check warning on line 86 in lib/Controller/PageController.php

View check run for this annotation

Codecov / codecov/patch

lib/Controller/PageController.php#L86

Added line #L86 was not covered by tests
try {
$submission = $this->submissionMapper->findById($submissionId);
$this->initialState->provideInitialState('submissionId', $submission->id);
} catch (DoesNotExistException $e) {

Check warning on line 90 in lib/Controller/PageController.php

View check run for this annotation

Codecov / codecov/patch

lib/Controller/PageController.php#L88-L90

Added lines #L88 - L90 were not covered by tests
// 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',
Expand All @@ -97,6 +108,16 @@
return $this->index($hash);
}

/**
* @return TemplateResponse
*/
#[NoAdminRequired()]

Check warning on line 114 in lib/Controller/PageController.php

View check run for this annotation

Codecov / codecov/patch

lib/Controller/PageController.php#L114

Added line #L114 was not covered by tests
#[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);

Check warning on line 118 in lib/Controller/PageController.php

View check run for this annotation

Codecov / codecov/patch

lib/Controller/PageController.php#L118

Added line #L118 was not covered by tests
}

/**
* @param string $hash
* @return RedirectResponse|TemplateResponse Redirect to login or internal view.
Expand Down
Loading
Loading