diff --git a/lib/Controller/ApiController.php b/lib/Controller/ApiController.php index f832705bc..0cbe6a73e 100644 --- a/lib/Controller/ApiController.php +++ b/lib/Controller/ApiController.php @@ -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); diff --git a/lib/Db/Form.php b/lib/Db/Form.php index fe1637eda..d3c9999df 100644 --- a/lib/Db/Form.php +++ b/lib/Db/Form.php @@ -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 { @@ -71,6 +73,7 @@ class Form extends Entity { protected $state; protected $lockedBy; protected $lockedUntil; + protected $maxSubmissions; /** * Form constructor. @@ -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. @@ -159,6 +163,7 @@ public function setAccess(array $access): void { * state: 0|1|2, * lockedBy: ?string, * lockedUntil: ?int, + * maxSubmissions: ?int, * } */ public function read() { @@ -182,6 +187,7 @@ public function read() { 'state' => $this->getState(), 'lockedBy' => $this->getLockedBy(), 'lockedUntil' => $this->getLockedUntil(), + 'maxSubmissions' => $this->getMaxSubmissions(), ]; } } diff --git a/lib/Migration/Version050300Date20260303000000.php b/lib/Migration/Version050300Date20260303000000.php new file mode 100644 index 000000000..7cb8c076c --- /dev/null +++ b/lib/Migration/Version050300Date20260303000000.php @@ -0,0 +1,41 @@ +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; + } +} diff --git a/lib/Service/FormsService.php b/lib/Service/FormsService.php index 5a7492329..e2ea51ec9 100644 --- a/lib/Service/FormsService.php +++ b/lib/Service/FormsService.php @@ -204,6 +204,10 @@ public function getForm(Form $form): array { $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'])) { @@ -484,6 +488,12 @@ public function canDeleteResults(Form $form): bool { * @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; diff --git a/src/components/SidebarTabs/SettingsSidebarTab.vue b/src/components/SidebarTabs/SettingsSidebarTab.vue index 15bcea0d6..aa2a2d920 100644 --- a/src/components/SidebarTabs/SettingsSidebarTab.vue +++ b/src/components/SidebarTabs/SettingsSidebarTab.vue @@ -78,6 +78,25 @@ {{ t('forms', 'Show expiration date on form') }} + + {{ t('forms', 'Limit number of responses') }} + +
+ +

+ {{ t('forms', 'Form will be closed automatically when the limit is reached.') }} +

+
this.form.expires }, @@ -365,6 +399,17 @@ export default { ) }, + 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', diff --git a/src/views/Submit.vue b/src/views/Submit.vue index 5b52df938..88f285f82 100644 --- a/src/views/Submit.vue +++ b/src/views/Submit.vue @@ -58,7 +58,7 @@ + + +