Skip to content
Open
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
6 changes: 6 additions & 0 deletions lib/Controller/ApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -1375,6 +1375,12 @@ public function newSubmission(int $formId, array $answers, string $shareHash = '
throw new OCSForbiddenException('Already submitted');
}

// Check if max submissions limit is reached
$maxSubmissions = $form->getMaxSubmissions();
if ($maxSubmissions > 0 && $this->submissionMapper->countSubmissions($formId) >= $maxSubmissions) {
throw new OCSForbiddenException('Maximum number of submissions reached');
}

// Insert new submission
$this->submissionMapper->insert($submission);

Expand Down
6 changes: 6 additions & 0 deletions lib/Db/Form.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
* @method string getLockedBy()
* @method void setLockedBy(string|null $value)
* @method int getLockedUntil()
* @method int|null getMaxSubmissions()
* @method void setMaxSubmissions(int|null $value)
* @method void setLockedUntil(int|null $value)
*/
class Form extends Entity {
Expand All @@ -71,6 +73,7 @@ class Form extends Entity {
protected $state;
protected $lockedBy;
protected $lockedUntil;
protected $maxSubmissions;

/**
* Form constructor.
Expand All @@ -86,6 +89,7 @@ public function __construct() {
$this->addType('state', 'integer');
$this->addType('lockedBy', 'string');
$this->addType('lockedUntil', 'integer');
$this->addType('maxSubmissions', 'integer');
}

// JSON-Decoding of access-column.
Expand Down Expand Up @@ -159,6 +163,7 @@ public function setAccess(array $access): void {
* state: 0|1|2,
* lockedBy: ?string,
* lockedUntil: ?int,
* maxSubmissions: ?int,
* }
*/
public function read() {
Expand All @@ -182,6 +187,7 @@ public function read() {
'state' => $this->getState(),
'lockedBy' => $this->getLockedBy(),
'lockedUntil' => $this->getLockedUntil(),
'maxSubmissions' => $this->getMaxSubmissions(),
];
}
}
41 changes: 41 additions & 0 deletions lib/Migration/Version050300Date20260303000000.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?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 Version050300Date20260303000000 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('max_submissions')) {
$table->addColumn('max_submissions', Types::INTEGER, [
'notnull' => false,
'default' => null,
'comment' => 'Maximum number of submissions, null means unlimited',
]);
}

return $schema;
}
}
10 changes: 10 additions & 0 deletions lib/Service/FormsService.php
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@
* Get a form data
*
* @param Form $form
* @return FormsForm

Check failure on line 195 in lib/Service/FormsService.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable32

InvalidReturnType

lib/Service/FormsService.php:195:13: InvalidReturnType: The declared return type 'array{access: array{permitAllUsers?: bool, showToAllUsers?: bool}, allowEditSubmissions: bool, canSubmit: bool, created: int, description: string, expires: int, fileFormat: null|string, fileId: int|null, filePath?: null|string, hash: string, id: int, isAnonymous: bool, lastUpdated: int, lockedBy: null|string, lockedUntil: int|null, ownerId: string, permissions: list<'edit'|'embed'|'results'|'results_delete'|'submit'>, questions: list<array{accept: list<string>, description: string, extraSettings: array{allowOtherAnswer?: bool, allowedFileExtensions?: list<string>, allowedFileTypes?: list<string>, dateMax?: int, dateMin?: int, dateRange?: bool, maxAllowedFilesCount?: int, maxFileSize?: int, optionsHighest?: 2|3|4|5|6|7|8|9|10, optionsLabelHighest?: string, optionsLabelLowest?: string, optionsLimitMax?: int, optionsLimitMin?: int, optionsLowest?: 0|1, questionType?: string, shuffleOptions?: bool, timeMax?: int, timeMin?: int, timeRange?: bool, validationRegex?: string, validationType?: string}|stdClass, formId: int, id: int, isRequired: bool, name: string, options: list<array{id: int, order: int|null, questionId: float|int, text: string}>, order: int, text: string, type: 'date'|'datetime'|'dropdown'|'file'|'grid'|'long'|'multiple'|'multiple_unique'|'short'|'time'}>, shares: list<array{displayName: string, formId: int, id: int, permissions: list<'edit'|'embed'|'results'|'results_delete'|'submit'>, shareType: int, shareWith: string}>, showExpiration: bool, state: 0|1|2, submissionCount?: int, submissionMessage: null|string, submitMultiple: bool, title: string}' for OCA\Forms\Service\FormsService::getForm is incorrect, got 'array{access: array{permitAllUsers?: bool, showToAllUsers?: bool}, allowEditSubmissions: bool, canSubmit: bool, created: int, description: string, expires: int, fileFormat: null|string, fileId: int|null, filePath?: null|string, hash: string, id: int, isAnonymous: bool, isMaxSubmissionsReached: bool, lastUpdated: int, lockedBy: null|string, lockedUntil: int|null, maxSubmissions: int|null, ownerId: string, permissions: list<'edit'|'embed'|'results'|'results_delete'|'submit'>, questions: list<array{accept: list<string>, description: string, extraSettings: array{allowOtherAnswer?: bool, allowedFileExtensions?: list<string>, allowedFileTypes?: list<string>, dateMax?: int, dateMin?: int, dateRange?: bool, maxAllowedFilesCount?: int, maxFileSize?: int, optionsHighest?: 2|3|4|5|6|7|8|9|10, optionsLabelHighest?: string, optionsLabelLowest?: string, optionsLimitMax?: int, optionsLimitMin?: int, optionsLowest?: 0|1, questionType?: string, shuffleOptions?: bool, timeMax?: int, timeMin?: int, timeRange?: bool, validationRegex?: string, validationType?: string}|stdClass, formId: int, id: int, isRequired: bool, name: string, options: list<array{id: int, order: int|null, questionId: float|int, text: string}>, order: int, text: string, type: 'date'|'datetime'|'dropdown'|'file'|'grid'|'long'|'multiple'|'multiple_unique'|'short'|'time'}>, shares: list<array{displayName: string, formId: int, id: int, permissions: list<'edit'|'embed'|'results'|'results_delete'|'submit'>, shareType: int, shareWith: string}>, showExpiration: bool, state: 0|1|2, submissionCount?: int, submissionMessage: null|string, submitMultiple: bool, title: string}' which is different due to additional array shape fields (maxSubmissions, isMaxSubmissionsReached) (see https://psalm.dev/011)

Check failure on line 195 in lib/Service/FormsService.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable33

InvalidReturnType

lib/Service/FormsService.php:195:13: InvalidReturnType: The declared return type 'array{access: array{permitAllUsers?: bool, showToAllUsers?: bool}, allowEditSubmissions: bool, canSubmit: bool, created: int, description: string, expires: int, fileFormat: null|string, fileId: int|null, filePath?: null|string, hash: string, id: int, isAnonymous: bool, lastUpdated: int, lockedBy: null|string, lockedUntil: int|null, ownerId: string, permissions: list<'edit'|'embed'|'results'|'results_delete'|'submit'>, questions: list<array{accept: list<string>, description: string, extraSettings: array{allowOtherAnswer?: bool, allowedFileExtensions?: list<string>, allowedFileTypes?: list<string>, dateMax?: int, dateMin?: int, dateRange?: bool, maxAllowedFilesCount?: int, maxFileSize?: int, optionsHighest?: 2|3|4|5|6|7|8|9|10, optionsLabelHighest?: string, optionsLabelLowest?: string, optionsLimitMax?: int, optionsLimitMin?: int, optionsLowest?: 0|1, questionType?: string, shuffleOptions?: bool, timeMax?: int, timeMin?: int, timeRange?: bool, validationRegex?: string, validationType?: string}|stdClass, formId: int, id: int, isRequired: bool, name: string, options: list<array{id: int, order: int|null, questionId: float|int, text: string}>, order: int, text: string, type: 'date'|'datetime'|'dropdown'|'file'|'grid'|'long'|'multiple'|'multiple_unique'|'short'|'time'}>, shares: list<array{displayName: string, formId: int, id: int, permissions: list<'edit'|'embed'|'results'|'results_delete'|'submit'>, shareType: int, shareWith: string}>, showExpiration: bool, state: 0|1|2, submissionCount?: int, submissionMessage: null|string, submitMultiple: bool, title: string}' for OCA\Forms\Service\FormsService::getForm is incorrect, got 'array{access: array{permitAllUsers?: bool, showToAllUsers?: bool}, allowEditSubmissions: bool, canSubmit: bool, created: int, description: string, expires: int, fileFormat: null|string, fileId: int|null, filePath?: null|string, hash: string, id: int, isAnonymous: bool, isMaxSubmissionsReached: bool, lastUpdated: int, lockedBy: null|string, lockedUntil: int|null, maxSubmissions: int|null, ownerId: string, permissions: list<'edit'|'embed'|'results'|'results_delete'|'submit'>, questions: list<array{accept: list<string>, description: string, extraSettings: array{allowOtherAnswer?: bool, allowedFileExtensions?: list<string>, allowedFileTypes?: list<string>, dateMax?: int, dateMin?: int, dateRange?: bool, maxAllowedFilesCount?: int, maxFileSize?: int, optionsHighest?: 2|3|4|5|6|7|8|9|10, optionsLabelHighest?: string, optionsLabelLowest?: string, optionsLimitMax?: int, optionsLimitMin?: int, optionsLowest?: 0|1, questionType?: string, shuffleOptions?: bool, timeMax?: int, timeMin?: int, timeRange?: bool, validationRegex?: string, validationType?: string}|stdClass, formId: int, id: int, isRequired: bool, name: string, options: list<array{id: int, order: int|null, questionId: float|int, text: string}>, order: int, text: string, type: 'date'|'datetime'|'dropdown'|'file'|'grid'|'long'|'multiple'|'multiple_unique'|'short'|'time'}>, shares: list<array{displayName: string, formId: int, id: int, permissions: list<'edit'|'embed'|'results'|'results_delete'|'submit'>, shareType: int, shareWith: string}>, showExpiration: bool, state: 0|1|2, submissionCount?: int, submissionMessage: null|string, submitMultiple: bool, title: string}' which is different due to additional array shape fields (maxSubmissions, isMaxSubmissionsReached) (see https://psalm.dev/011)

Check failure on line 195 in lib/Service/FormsService.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable31

InvalidReturnType

lib/Service/FormsService.php:195:13: InvalidReturnType: The declared return type 'array{access: array{permitAllUsers?: bool, showToAllUsers?: bool}, allowEditSubmissions: bool, canSubmit: bool, created: int, description: string, expires: int, fileFormat: null|string, fileId: int|null, filePath?: null|string, hash: string, id: int, isAnonymous: bool, lastUpdated: int, lockedBy: null|string, lockedUntil: int|null, ownerId: string, permissions: list<'edit'|'embed'|'results'|'results_delete'|'submit'>, questions: list<array{accept: list<string>, description: string, extraSettings: array{allowOtherAnswer?: bool, allowedFileExtensions?: list<string>, allowedFileTypes?: list<string>, dateMax?: int, dateMin?: int, dateRange?: bool, maxAllowedFilesCount?: int, maxFileSize?: int, optionsHighest?: 2|3|4|5|6|7|8|9|10, optionsLabelHighest?: string, optionsLabelLowest?: string, optionsLimitMax?: int, optionsLimitMin?: int, optionsLowest?: 0|1, questionType?: string, shuffleOptions?: bool, timeMax?: int, timeMin?: int, timeRange?: bool, validationRegex?: string, validationType?: string}|stdClass, formId: int, id: int, isRequired: bool, name: string, options: list<array{id: int, order: int|null, questionId: float|int, text: string}>, order: int, text: string, type: 'date'|'datetime'|'dropdown'|'file'|'grid'|'long'|'multiple'|'multiple_unique'|'short'|'time'}>, shares: list<array{displayName: string, formId: int, id: int, permissions: list<'edit'|'embed'|'results'|'results_delete'|'submit'>, shareType: int, shareWith: string}>, showExpiration: bool, state: 0|1|2, submissionCount?: int, submissionMessage: null|string, submitMultiple: bool, title: string}' for OCA\Forms\Service\FormsService::getForm is incorrect, got 'array{access: array{permitAllUsers?: bool, showToAllUsers?: bool}, allowEditSubmissions: bool, canSubmit: bool, created: int, description: string, expires: int, fileFormat: null|string, fileId: int|null, filePath?: null|string, hash: string, id: int, isAnonymous: bool, isMaxSubmissionsReached: bool, lastUpdated: int, lockedBy: null|string, lockedUntil: int|null, maxSubmissions: int|null, ownerId: string, permissions: list<'edit'|'embed'|'results'|'results_delete'|'submit'>, questions: list<array{accept: list<string>, description: string, extraSettings: array{allowOtherAnswer?: bool, allowedFileExtensions?: list<string>, allowedFileTypes?: list<string>, dateMax?: int, dateMin?: int, dateRange?: bool, maxAllowedFilesCount?: int, maxFileSize?: int, optionsHighest?: 2|3|4|5|6|7|8|9|10, optionsLabelHighest?: string, optionsLabelLowest?: string, optionsLimitMax?: int, optionsLimitMin?: int, optionsLowest?: 0|1, questionType?: string, shuffleOptions?: bool, timeMax?: int, timeMin?: int, timeRange?: bool, validationRegex?: string, validationType?: string}|stdClass, formId: int, id: int, isRequired: bool, name: string, options: list<array{id: int, order: int|null, questionId: float|int, text: string}>, order: int, text: string, type: 'date'|'datetime'|'dropdown'|'file'|'grid'|'long'|'multiple'|'multiple_unique'|'short'|'time'}>, shares: list<array{displayName: string, formId: int, id: int, permissions: list<'edit'|'embed'|'results'|'results_delete'|'submit'>, shareType: int, shareWith: string}>, showExpiration: bool, state: 0|1|2, submissionCount?: int, submissionMessage: null|string, submitMultiple: bool, title: string}' which is different due to additional array shape fields (maxSubmissions, isMaxSubmissionsReached) (see https://psalm.dev/011)
* @throws IMapperException
*/
public function getForm(Form $form): array {
Expand All @@ -204,6 +204,10 @@
$result['permissions'] = $this->getPermissions($form);
// Append canSubmit, to be able to show proper EmptyContent on internal view.
$result['canSubmit'] = $this->canSubmit($form);
// Append isMaxSubmissionsReached to show proper message on submit view.
$maxSubmissions = $form->getMaxSubmissions();
$result['isMaxSubmissionsReached'] = $maxSubmissions !== null
&& $this->submissionMapper->countSubmissions($form->getId()) >= $maxSubmissions;

// Append submissionCount if currentUser has permissions to see results
if (in_array(Constants::PERMISSION_RESULTS, $result['permissions'])) {
Expand All @@ -224,7 +228,7 @@
}
}

return $result;

Check failure on line 231 in lib/Service/FormsService.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable32

InvalidReturnStatement

lib/Service/FormsService.php:231:10: InvalidReturnStatement: The inferred type 'array{access: array{permitAllUsers?: bool, showToAllUsers?: bool}, allowEditSubmissions: bool, canSubmit: bool, created: int, description: string, expires: int, fileFormat: null|string, fileId: int|null, filePath?: null|string, hash: string, id: int, isAnonymous: bool, isMaxSubmissionsReached: bool, lastUpdated: int, lockedBy: null|string, lockedUntil: int|null, maxSubmissions: int|null, ownerId: string, permissions: list<'edit'|'embed'|'results'|'results_delete'|'submit'>, questions: list<array{accept: list<string>, description: string, extraSettings: array{allowOtherAnswer?: bool, allowedFileExtensions?: list<string>, allowedFileTypes?: list<string>, dateMax?: int, dateMin?: int, dateRange?: bool, maxAllowedFilesCount?: int, maxFileSize?: int, optionsHighest?: 2|3|4|5|6|7|8|9|10, optionsLabelHighest?: string, optionsLabelLowest?: string, optionsLimitMax?: int, optionsLimitMin?: int, optionsLowest?: 0|1, questionType?: string, shuffleOptions?: bool, timeMax?: int, timeMin?: int, timeRange?: bool, validationRegex?: string, validationType?: string}|stdClass, formId: int, id: int, isRequired: bool, name: string, options: list<array{id: int, order: int|null, questionId: float|int, text: string}>, order: int, text: string, type: 'date'|'datetime'|'dropdown'|'file'|'grid'|'long'|'multiple'|'multiple_unique'|'short'|'time'}>, shares: list<array{displayName: string, formId: int, id: int, permissions: list<'edit'|'embed'|'results'|'results_delete'|'submit'>, shareType: int, shareWith: string}>, showExpiration: bool, state: 0|1|2, submissionCount?: int, submissionMessage: null|string, submitMultiple: bool, title: string}' does not match the declared return type 'array{access: array{permitAllUsers?: bool, showToAllUsers?: bool}, allowEditSubmissions: bool, canSubmit: bool, created: int, description: string, expires: int, fileFormat: null|string, fileId: int|null, filePath?: null|string, hash: string, id: int, isAnonymous: bool, lastUpdated: int, lockedBy: null|string, lockedUntil: int|null, ownerId: string, permissions: list<'edit'|'embed'|'results'|'results_delete'|'submit'>, questions: list<array{accept: list<string>, description: string, extraSettings: array{allowOtherAnswer?: bool, allowedFileExtensions?: list<string>, allowedFileTypes?: list<string>, dateMax?: int, dateMin?: int, dateRange?: bool, maxAllowedFilesCount?: int, maxFileSize?: int, optionsHighest?: 2|3|4|5|6|7|8|9|10, optionsLabelHighest?: string, optionsLabelLowest?: string, optionsLimitMax?: int, optionsLimitMin?: int, optionsLowest?: 0|1, questionType?: string, shuffleOptions?: bool, timeMax?: int, timeMin?: int, timeRange?: bool, validationRegex?: string, validationType?: string}|stdClass, formId: int, id: int, isRequired: bool, name: string, options: list<array{id: int, order: int|null, questionId: float|int, text: string}>, order: int, text: string, type: 'date'|'datetime'|'dropdown'|'file'|'grid'|'long'|'multiple'|'multiple_unique'|'short'|'time'}>, shares: list<array{displayName: string, formId: int, id: int, permissions: list<'edit'|'embed'|'results'|'results_delete'|'submit'>, shareType: int, shareWith: string}>, showExpiration: bool, state: 0|1|2, submissionCount?: int, submissionMessage: null|string, submitMultiple: bool, title: string}' for OCA\Forms\Service\FormsService::getForm due to additional array shape fields (maxSubmissions, isMaxSubmissionsReached) (see https://psalm.dev/128)

Check failure on line 231 in lib/Service/FormsService.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable33

InvalidReturnStatement

lib/Service/FormsService.php:231:10: InvalidReturnStatement: The inferred type 'array{access: array{permitAllUsers?: bool, showToAllUsers?: bool}, allowEditSubmissions: bool, canSubmit: bool, created: int, description: string, expires: int, fileFormat: null|string, fileId: int|null, filePath?: null|string, hash: string, id: int, isAnonymous: bool, isMaxSubmissionsReached: bool, lastUpdated: int, lockedBy: null|string, lockedUntil: int|null, maxSubmissions: int|null, ownerId: string, permissions: list<'edit'|'embed'|'results'|'results_delete'|'submit'>, questions: list<array{accept: list<string>, description: string, extraSettings: array{allowOtherAnswer?: bool, allowedFileExtensions?: list<string>, allowedFileTypes?: list<string>, dateMax?: int, dateMin?: int, dateRange?: bool, maxAllowedFilesCount?: int, maxFileSize?: int, optionsHighest?: 2|3|4|5|6|7|8|9|10, optionsLabelHighest?: string, optionsLabelLowest?: string, optionsLimitMax?: int, optionsLimitMin?: int, optionsLowest?: 0|1, questionType?: string, shuffleOptions?: bool, timeMax?: int, timeMin?: int, timeRange?: bool, validationRegex?: string, validationType?: string}|stdClass, formId: int, id: int, isRequired: bool, name: string, options: list<array{id: int, order: int|null, questionId: float|int, text: string}>, order: int, text: string, type: 'date'|'datetime'|'dropdown'|'file'|'grid'|'long'|'multiple'|'multiple_unique'|'short'|'time'}>, shares: list<array{displayName: string, formId: int, id: int, permissions: list<'edit'|'embed'|'results'|'results_delete'|'submit'>, shareType: int, shareWith: string}>, showExpiration: bool, state: 0|1|2, submissionCount?: int, submissionMessage: null|string, submitMultiple: bool, title: string}' does not match the declared return type 'array{access: array{permitAllUsers?: bool, showToAllUsers?: bool}, allowEditSubmissions: bool, canSubmit: bool, created: int, description: string, expires: int, fileFormat: null|string, fileId: int|null, filePath?: null|string, hash: string, id: int, isAnonymous: bool, lastUpdated: int, lockedBy: null|string, lockedUntil: int|null, ownerId: string, permissions: list<'edit'|'embed'|'results'|'results_delete'|'submit'>, questions: list<array{accept: list<string>, description: string, extraSettings: array{allowOtherAnswer?: bool, allowedFileExtensions?: list<string>, allowedFileTypes?: list<string>, dateMax?: int, dateMin?: int, dateRange?: bool, maxAllowedFilesCount?: int, maxFileSize?: int, optionsHighest?: 2|3|4|5|6|7|8|9|10, optionsLabelHighest?: string, optionsLabelLowest?: string, optionsLimitMax?: int, optionsLimitMin?: int, optionsLowest?: 0|1, questionType?: string, shuffleOptions?: bool, timeMax?: int, timeMin?: int, timeRange?: bool, validationRegex?: string, validationType?: string}|stdClass, formId: int, id: int, isRequired: bool, name: string, options: list<array{id: int, order: int|null, questionId: float|int, text: string}>, order: int, text: string, type: 'date'|'datetime'|'dropdown'|'file'|'grid'|'long'|'multiple'|'multiple_unique'|'short'|'time'}>, shares: list<array{displayName: string, formId: int, id: int, permissions: list<'edit'|'embed'|'results'|'results_delete'|'submit'>, shareType: int, shareWith: string}>, showExpiration: bool, state: 0|1|2, submissionCount?: int, submissionMessage: null|string, submitMultiple: bool, title: string}' for OCA\Forms\Service\FormsService::getForm due to additional array shape fields (maxSubmissions, isMaxSubmissionsReached) (see https://psalm.dev/128)

Check failure on line 231 in lib/Service/FormsService.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable31

InvalidReturnStatement

lib/Service/FormsService.php:231:10: InvalidReturnStatement: The inferred type 'array{access: array{permitAllUsers?: bool, showToAllUsers?: bool}, allowEditSubmissions: bool, canSubmit: bool, created: int, description: string, expires: int, fileFormat: null|string, fileId: int|null, filePath?: null|string, hash: string, id: int, isAnonymous: bool, isMaxSubmissionsReached: bool, lastUpdated: int, lockedBy: null|string, lockedUntil: int|null, maxSubmissions: int|null, ownerId: string, permissions: list<'edit'|'embed'|'results'|'results_delete'|'submit'>, questions: list<array{accept: list<string>, description: string, extraSettings: array{allowOtherAnswer?: bool, allowedFileExtensions?: list<string>, allowedFileTypes?: list<string>, dateMax?: int, dateMin?: int, dateRange?: bool, maxAllowedFilesCount?: int, maxFileSize?: int, optionsHighest?: 2|3|4|5|6|7|8|9|10, optionsLabelHighest?: string, optionsLabelLowest?: string, optionsLimitMax?: int, optionsLimitMin?: int, optionsLowest?: 0|1, questionType?: string, shuffleOptions?: bool, timeMax?: int, timeMin?: int, timeRange?: bool, validationRegex?: string, validationType?: string}|stdClass, formId: int, id: int, isRequired: bool, name: string, options: list<array{id: int, order: int|null, questionId: float|int, text: string}>, order: int, text: string, type: 'date'|'datetime'|'dropdown'|'file'|'grid'|'long'|'multiple'|'multiple_unique'|'short'|'time'}>, shares: list<array{displayName: string, formId: int, id: int, permissions: list<'edit'|'embed'|'results'|'results_delete'|'submit'>, shareType: int, shareWith: string}>, showExpiration: bool, state: 0|1|2, submissionCount?: int, submissionMessage: null|string, submitMultiple: bool, title: string}' does not match the declared return type 'array{access: array{permitAllUsers?: bool, showToAllUsers?: bool}, allowEditSubmissions: bool, canSubmit: bool, created: int, description: string, expires: int, fileFormat: null|string, fileId: int|null, filePath?: null|string, hash: string, id: int, isAnonymous: bool, lastUpdated: int, lockedBy: null|string, lockedUntil: int|null, ownerId: string, permissions: list<'edit'|'embed'|'results'|'results_delete'|'submit'>, questions: list<array{accept: list<string>, description: string, extraSettings: array{allowOtherAnswer?: bool, allowedFileExtensions?: list<string>, allowedFileTypes?: list<string>, dateMax?: int, dateMin?: int, dateRange?: bool, maxAllowedFilesCount?: int, maxFileSize?: int, optionsHighest?: 2|3|4|5|6|7|8|9|10, optionsLabelHighest?: string, optionsLabelLowest?: string, optionsLimitMax?: int, optionsLimitMin?: int, optionsLowest?: 0|1, questionType?: string, shuffleOptions?: bool, timeMax?: int, timeMin?: int, timeRange?: bool, validationRegex?: string, validationType?: string}|stdClass, formId: int, id: int, isRequired: bool, name: string, options: list<array{id: int, order: int|null, questionId: float|int, text: string}>, order: int, text: string, type: 'date'|'datetime'|'dropdown'|'file'|'grid'|'long'|'multiple'|'multiple_unique'|'short'|'time'}>, shares: list<array{displayName: string, formId: int, id: int, permissions: list<'edit'|'embed'|'results'|'results_delete'|'submit'>, shareType: int, shareWith: string}>, showExpiration: bool, state: 0|1|2, submissionCount?: int, submissionMessage: null|string, submitMultiple: bool, title: string}' for OCA\Forms\Service\FormsService::getForm due to additional array shape fields (maxSubmissions, isMaxSubmissionsReached) (see https://psalm.dev/128)
}

/**
Expand Down Expand Up @@ -484,6 +488,12 @@
* @return boolean
*/
public function canSubmit(Form $form): bool {
// Check if max submissions limit is reached
$maxSubmissions = $form->getMaxSubmissions();
if ($maxSubmissions !== null && $this->submissionMapper->countSubmissions($form->getId()) >= $maxSubmissions) {
return false;
}

// We cannot control how many time users can submit if public link available
if ($this->hasPublicLink($form)) {
return true;
Expand Down
45 changes: 45 additions & 0 deletions src/components/SidebarTabs/SettingsSidebarTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,25 @@
{{ t('forms', 'Show expiration date on form') }}
</NcCheckboxRadioSwitch>
</div>
<NcCheckboxRadioSwitch
:model-value="hasMaxSubmissions"
:disabled="formArchived || locked"
type="switch"
@update:model-value="onMaxSubmissionsChange">
{{ t('forms', 'Limit number of responses') }}
</NcCheckboxRadioSwitch>
<div v-show="hasMaxSubmissions && !formArchived" class="settings-div--indent">

Check failure on line 88 in src/components/SidebarTabs/SettingsSidebarTab.vue

View workflow job for this annotation

GitHub Actions / NPM lint

Replace `·v-show="hasMaxSubmissions·&&·!formArchived"·` with `⏎↹↹↹v-show="hasMaxSubmissions·&&·!formArchived"⏎↹↹↹`
<NcInputField
v-model="maxSubmissionsValue"
type="number"
:min="1"
:disabled="locked"
:label="t('forms', 'Maximum number of responses')"
@update:model-value="onMaxSubmissionsValueChange" />
<p class="settings-hint">
{{ t('forms', 'Form will be closed automatically when the limit is reached.') }}

Check failure on line 97 in src/components/SidebarTabs/SettingsSidebarTab.vue

View workflow job for this annotation

GitHub Actions / NPM lint

Replace `·t('forms',·'Form·will·be·closed·automatically·when·the·limit·is·reached.')·` with `⏎↹↹↹↹↹t(⏎↹↹↹↹↹↹'forms',⏎↹↹↹↹↹↹'Form·will·be·closed·automatically·when·the·limit·is·reached.',⏎↹↹↹↹↹)⏎↹↹↹↹`
</p>
</div>
<NcCheckboxRadioSwitch
:model-value="formClosed"
:disabled="formArchived || locked"
Expand Down Expand Up @@ -181,7 +200,8 @@
import moment from '@nextcloud/moment'
import { directive as ClickOutside } from 'v-click-outside'
import NcButton from '@nextcloud/vue/components/NcButton'
import NcInputField from '@nextcloud/vue/components/NcInputField'
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'

Check failure on line 204 in src/components/SidebarTabs/SettingsSidebarTab.vue

View workflow job for this annotation

GitHub Actions / NPM lint

Expected "@nextcloud/vue/components/NcCheckboxRadioSwitch" to come before "@nextcloud/vue/components/NcInputField"
import NcDateTimePicker from '@nextcloud/vue/components/NcDateTimePicker'
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
Expand All @@ -193,6 +213,7 @@
export default {
components: {
NcButton,
NcInputField,
NcCheckboxRadioSwitch,
NcDateTimePicker,
NcIconSvgWrapper,
Expand Down Expand Up @@ -302,6 +323,19 @@
return this.form.state !== FormState.FormActive
},

hasMaxSubmissions() {
return this.form.maxSubmissions !== null && this.form.maxSubmissions !== undefined

Check failure on line 327 in src/components/SidebarTabs/SettingsSidebarTab.vue

View workflow job for this annotation

GitHub Actions / NPM lint

Replace `this.form.maxSubmissions·!==·null·&&·this.form.maxSubmissions·!==·undefined` with `(⏎↹↹↹↹this.form.maxSubmissions·!==·null⏎↹↹↹↹&&·this.form.maxSubmissions·!==·undefined⏎↹↹↹)`
},

maxSubmissionsValue: {
get() {
return this.form.maxSubmissions ?? 1
},

Check failure on line 333 in src/components/SidebarTabs/SettingsSidebarTab.vue

View workflow job for this annotation

GitHub Actions / NPM lint

Enforce new lines between multi-line properties in Vue components
set(value) {
this.$emit('update:form-prop', 'maxSubmissions', value)
},
},

isExpired() {
return this.form.expires && moment().unix() > this.form.expires
},
Expand Down Expand Up @@ -365,6 +399,17 @@
)
},

onMaxSubmissionsChange(checked) {
this.$emit('update:form-prop', 'maxSubmissions', checked ? 1 : null)
},

onMaxSubmissionsValueChange(event) {
const value = parseInt(event.target.value)
if (value > 0) {
this.$emit('update:form-prop', 'maxSubmissions', value)
}
},

onFormClosedChange(isClosed) {
this.$emit(
'update:form-prop',
Expand Down
14 changes: 13 additions & 1 deletion src/views/Submit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
</template>
</NcEmptyContent>
<NcEmptyContent
v-else-if="success || !form.canSubmit"
v-else-if="success || (!form.canSubmit && !isMaxSubmissionsReached)"
class="forms-emptycontent"
:name="
form.submissionMessage
Expand All @@ -74,6 +74,15 @@
<p class="submission-message" v-html="submissionMessageHTML" />
</template>
</NcEmptyContent>
<NcEmptyContent
v-else-if="isMaxSubmissionsReached"
class="forms-emptycontent"
:name="t('forms', 'Form is full')"
:description="t('forms', 'This form has reached the maximum number of answers')">

Check failure on line 81 in src/views/Submit.vue

View workflow job for this annotation

GitHub Actions / NPM lint

Replace `t('forms',·'This·form·has·reached·the·maximum·number·of·answers')` with `⏎↹↹↹↹↹t('forms',·'This·form·has·reached·the·maximum·number·of·answers')⏎↹↹↹↹`
<template #icon>
<NcIconSvgWrapper :svg="IconCheckSvg" size="64" />
</template>
</NcEmptyContent>
<NcEmptyContent
v-else-if="isExpired"
class="forms-emptycontent"
Expand Down Expand Up @@ -358,7 +367,10 @@

isClosed() {
return this.form.state === FormState.FormClosed
},

Check failure on line 370 in src/views/Submit.vue

View workflow job for this annotation

GitHub Actions / NPM lint

Enforce new lines between multi-line properties in Vue components
isMaxSubmissionsReached() {
return this.form.isMaxSubmissionsReached === true
},

/**
* Checks if the current state is active.
Expand Down
Loading