Skip to content

Commit

Permalink
feat: Add state to forms to allow manually close or archive forms
Browse files Browse the repository at this point in the history
Archived forms can not be changed (except from being un-archived).
Closed forms behave like expired forms and just do not allow new submissions.
By default forms are in state `active`.

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
  • Loading branch information
susnux committed Mar 22, 2024
1 parent 0e47c54 commit afbea98
Show file tree
Hide file tree
Showing 11 changed files with 250 additions and 29 deletions.
10 changes: 7 additions & 3 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ Returns condensed objects of all Forms beeing owned by the authenticated user.
"results",
"submit"
],
"partial": true
"partial": true,
"state": 0
},
{
"id": 3,
Expand All @@ -70,7 +71,8 @@ Returns condensed objects of all Forms beeing owned by the authenticated user.
"results",
"submit"
],
"partial": true
"partial": true,
"state": 0
}
]
```
Expand Down Expand Up @@ -103,7 +105,8 @@ Returns a single partial form object, corresponding to owned/shared form-listing
"permissions": [
"submit"
],
"partial": true
"partial": true,
"state": 0
}
```

Expand Down Expand Up @@ -146,6 +149,7 @@ Returns the full-depth object of the requested form (without submissions).
"submitMultiple": true,
"showExpiration": false,
"canSubmit": true,
"state": 0,
"permissions": [
"edit",
"results",
Expand Down
13 changes: 12 additions & 1 deletion docs/DataStructure.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ This document describes the Object-Structure, that is used within the Forms App
| 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. |
Expand Down Expand Up @@ -45,11 +46,21 @@ This document describes the Object-Structure, that is used within the Forms App
"submit"
],
"questions": [],
"submissions": [],
"state": 0,
"shares": []
"submissions": [],
}
```

#### Form state
The form state is used for additional states, currently following states are defined:

| Value | Meaning |
|----------------|---------------------------------------------|
| 0 | Form is active and open for new submissions |
| 1 | Form is closed and does not allow new submissions |
| 2 | Form is archived, it does not allow new submissions and can also not be modified anymore |

### Question
| Property | Type | Restrictions | Description |
|----------------|-----------------|--------------|-------------|
Expand Down
7 changes: 7 additions & 0 deletions lib/Constants.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ class Constants {
'answerText' => 4096,
];

/**
* State flags of a form
*/
public const FORM_STATE_ACTIVE = 0;
public const FORM_STATE_CLOSED = 1;
public const FORM_STATE_ARCHIVED = 2;

/**
* !! Keep in sync with src/models/AnswerTypes.js !!
*/
Expand Down
46 changes: 44 additions & 2 deletions lib/Controller/ApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* @copyright Copyright (c) 2017 Vinzenz Rosenkranz <vinzenz.rosenkranz@gmail.com>
*
* @author affan98 <affan98@gmail.com>
* @author Ferdinand Thiessen <opensource@fthiessen.de>
* @author Jan-Christoph Borchardt <hey@jancborchardt.net>
* @author John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
* @author Jonas Rittershofer <jotoeri@users.noreply.github.com>
Expand Down Expand Up @@ -471,6 +472,11 @@ public function newQuestion(int $formId, string $type, string $text = ''): DataR
throw new OCSForbiddenException();
}

if ($this->formsService->isFormArchived($form)) {
$this->logger->debug('This form is archived and can not be modified');
throw new OCSForbiddenException();
}

// Retrieve all active questions sorted by Order. Takes the order of the last array-element and adds one.
$questions = $this->questionMapper->findByForm($formId);
$lastQuestion = array_pop($questions);
Expand Down Expand Up @@ -530,6 +536,11 @@ public function reorderQuestions(int $formId, array $newOrder): DataResponse {
throw new OCSForbiddenException();
}

if ($this->formsService->isFormArchived($form)) {
$this->logger->debug('This form is archived and can not be modified');
throw new OCSForbiddenException();
}

// Check if array contains duplicates
if (array_unique($newOrder) !== $newOrder) {
$this->logger->debug('The given Array contains duplicates');
Expand Down Expand Up @@ -627,6 +638,11 @@ public function updateQuestion(int $id, array $keyValuePairs): DataResponse {
throw new OCSForbiddenException();
}

if ($this->formsService->isFormArchived($form)) {
$this->logger->debug('This form is archived and can not be modified');
throw new OCSForbiddenException();
}

// Don't allow empty array
if (sizeof($keyValuePairs) === 0) {
$this->logger->info('Empty keyValuePairs, will not update.');
Expand Down Expand Up @@ -690,6 +706,11 @@ public function deleteQuestion(int $id): DataResponse {
throw new OCSForbiddenException();
}

if ($this->formsService->isFormArchived($form)) {
$this->logger->debug('This form is archived and can not be modified');
throw new OCSForbiddenException();
}

// Store Order of deleted Question
$deletedOrder = $question->getOrder();

Expand Down Expand Up @@ -741,6 +762,11 @@ public function cloneQuestion(int $id): DataResponse {
throw new OCSForbiddenException();
}

if ($this->formsService->isFormArchived($form)) {
$this->logger->debug('This form is archived and can not be modified');
throw new OCSForbiddenException();
}

$allQuestions = $this->questionMapper->findByForm($form->getId());

$questionData = $sourceQuestion->read();
Expand Down Expand Up @@ -797,6 +823,11 @@ public function newOption(int $questionId, string $text): DataResponse {
throw new OCSForbiddenException();
}

if ($this->formsService->isFormArchived($form)) {
$this->logger->debug('This form is archived and can not be modified');
throw new OCSForbiddenException();
}

$option = new Option();

$option->setQuestionId($questionId);
Expand Down Expand Up @@ -841,6 +872,11 @@ public function updateOption(int $id, array $keyValuePairs): DataResponse {
throw new OCSForbiddenException();
}

if ($this->formsService->isFormArchived($form)) {
$this->logger->debug('This form is archived and can not be modified');
throw new OCSForbiddenException();
}

// Don't allow empty array
if (sizeof($keyValuePairs) === 0) {
$this->logger->info('Empty keyValuePairs, will not update.');
Expand Down Expand Up @@ -895,6 +931,11 @@ public function deleteOption(int $id): DataResponse {
throw new OCSForbiddenException();
}

if ($this->formsService->isFormArchived($form)) {
$this->logger->debug('This form is archived and can not be modified');
throw new OCSForbiddenException();
}

$this->optionMapper->delete($option);

$this->formsService->setLastUpdatedTimestamp($form->getId());
Expand Down Expand Up @@ -1180,8 +1221,9 @@ public function deleteAllSubmissions(int $formId): DataResponse {
throw new OCSBadRequestException();
}

if ($form->getOwnerId() !== $this->currentUser->getUID()) {
$this->logger->debug('This form is not owned by the current user');
// The current user has permissions to remove submissions
if (!$this->formsService->canDeleteResults($form)) {
$this->logger->debug('This form is not owned by the current user and user has no `results_delete` permission');
throw new OCSForbiddenException();
}

Expand Down
24 changes: 15 additions & 9 deletions lib/Db/Form.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*
* @author affan98 <affan98@gmail.com>
* @author Jonas Rittershofer <jotoeri@users.noreply.github.com>
* @author Ferdinand Thiessen <opensource@fthiessen.de>
*
* @license AGPL-3.0-or-later
*
Expand Down Expand Up @@ -44,20 +45,22 @@
* @method void setFileFormat(string|null $value)
* @method array getAccess()
* @method void setAccess(array $value)
* @method integer getCreated()
* @method void setCreated(integer $value)
* @method integer getExpires()
* @method void setExpires(integer $value)
* @method integer getIsAnonymous()
* @method int getCreated()
* @method void setCreated(int $value)
* @method int getExpires()
* @method void setExpires(int $value)
* @method int getIsAnonymous()
* @method void setIsAnonymous(bool $value)
* @method integer getSubmitMultiple()
* @method int getSubmitMultiple()
* @method void setSubmitMultiple(bool $value)
* @method integer getShowExpiration()
* @method int getShowExpiration()
* @method void setShowExpiration(bool $value)
* @method integer getLastUpdated()
* @method void setLastUpdated(integer $value)
* @method int getLastUpdated()
* @method void setLastUpdated(int $value)
* @method ?string getSubmissionMessage()
* @method void setSubmissionMessage(?string $value)
* @method int getState()
* @method void setState(?int $value)
*/
class Form extends Entity {
protected $hash;
Expand All @@ -74,6 +77,7 @@ class Form extends Entity {
protected $showExpiration;
protected $submissionMessage;
protected $lastUpdated;
protected $state;

/**
* Form constructor.
Expand All @@ -85,6 +89,7 @@ public function __construct() {
$this->addType('submitMultiple', 'bool');
$this->addType('showExpiration', 'bool');
$this->addType('lastUpdated', 'integer');
$this->addType('state', 'integer');
}

// JSON-Decoding of access-column.
Expand Down Expand Up @@ -115,6 +120,7 @@ public function read() {
'showExpiration' => (bool)$this->getShowExpiration(),
'lastUpdated' => (int)$this->getLastUpdated(),
'submissionMessage' => $this->getSubmissionMessage(),
'state' => $this->getState(),
];
}
}
78 changes: 78 additions & 0 deletions lib/Migration/Version040200Date20240219201500.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

declare(strict_types=1);

/**
* @copyright Copyright (c) 2024 Ferdinand Thiessen <opensource@fthiessen.de>
*
* @author Ferdinand Thiessen <opensource@fthiessen.de>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

namespace OCA\Forms\Migration;

use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\DB\Types;
use OCP\IDBConnection;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;

/**
* This migration adds the `state` property to forms for closed, archived or active forms
*/
class Version040200Date20240219201500 extends SimpleMigrationStep {

public function __construct(
protected IDBConnection $db,
) {
}

/**
* @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('state')) {
$table->addColumn('state', Types::SMALLINT, [
'notnull' => true,
'default' => 0,
'unsigned' => true,
]);
}

return $schema;
}

/**
* Set all old forms to active state
*/
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options) {
$query = $this->db->getQueryBuilder();
$query->update('forms_v2_forms')
->set('state', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT))
->executeStatement();
}
}
27 changes: 23 additions & 4 deletions lib/Service/FormsService.php
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,8 @@ public function getPartialFormArray(Form $form): array {
'expires' => $form->getExpires(),
'lastUpdated' => $form->getLastUpdated(),
'permissions' => $this->getPermissions($form),
'partial' => true
'partial' => true,
'state' => $form->getState(),
];

// Append submissionCount if currentUser has permissions to see results
Expand Down Expand Up @@ -285,7 +286,13 @@ public function canSeeResults(Form $form): bool {
* @return boolean
*/
public function canDeleteResults(Form $form): bool {
return in_array(Constants::PERMISSION_RESULTS_DELETE, $this->getPermissions($form));
// Check permissions
if (!in_array(Constants::PERMISSION_RESULTS_DELETE, $this->getPermissions($form))) {
return false;
}

// Do not allow deleting results on archived forms
return !$this->isFormArchived($form);
}

/**
Expand Down Expand Up @@ -421,13 +428,25 @@ public function isSharedToUser(int $formId): bool {
return count($shareEntities) > 0;
}

/*
* Has the form expired?
/**
* Check if the form is archived
* If a form is archived no changes are allowed
*/
public function isFormArchived(Form $form): bool {
return $form->getState() === Constants::FORM_STATE_ARCHIVED;
}

/**
* Check if the form was closed or archived or has expired.
*
* @param Form $form
* @return boolean
*/
public function hasFormExpired(Form $form): bool {
// Check for form state first
if ($form->getState() !== Constants::FORM_STATE_ACTIVE) {
return true;
}
return ($form->getExpires() !== 0 && $form->getExpires() < time());
}

Expand Down
Loading

0 comments on commit afbea98

Please sign in to comment.