Skip to content

Commit

Permalink
Add support for file question
Browse files Browse the repository at this point in the history
Signed-off-by: Konstantin Myakshin <molodchick@gmail.com>
  • Loading branch information
Koc committed Jun 1, 2024
1 parent 82e01be commit c0ce60e
Show file tree
Hide file tree
Showing 32 changed files with 1,808 additions and 214 deletions.
4 changes: 4 additions & 0 deletions appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@
<nextcloud min-version="28" max-version="29" />
</dependencies>

<background-jobs>
<job>OCA\Forms\BackgroundJob\CleanupUploadedFilesJob</job>
</background-jobs>

<settings>
<admin>OCA\Forms\Settings\Settings</admin>
<admin-section>OCA\Forms\Settings\SettingsSection</admin-section>
Expand Down
8 changes: 8 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,14 @@
'apiVersion' => 'v2(\.[1-4])?'
]
],
[
'name' => 'api#uploadFiles',
'url' => '/api/{apiVersion}/uploadFiles/{formId}/{questionId}',
'verb' => 'POST',
'requirements' => [
'apiVersion' => 'v2.5'
]
],
[
'name' => 'api#insertSubmission',
'url' => '/api/{apiVersion}/submission/insert',
Expand Down
26 changes: 25 additions & 1 deletion docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ This file contains the API-Documentation. For more information on the returned D
- Completely new way of handling access & shares.

### Other API changes
- In API version 2.5 the following endpoints were introduced:
- `POST /api/2.5/uploadFiles/{formId}/{questionId}` to upload files to answer before form submitting
- In API version 2.4 the following endpoints were introduced:
- `POST /api/2.4/form/link/{fileFormat}` to link form to a file
- `POST /api/2.4/form/unlink` to unlink form from a file
Expand Down Expand Up @@ -176,18 +178,20 @@ Returns the full-depth object of the requested form (without submissions).
"text": "Option 2"
}
],
"accept": [],
"extraSettings": {}
},
{
"id": 2,
"formId": 3,
"order": 2,
"type": "short",
"type": "file",
"isRequired": true,
"text": "Question 2",
"name": "something_other",
"options": [],
"extraSettings": {}
"accept": ["image/*", ".pdf"],
}
],
"shares": [
Expand Down Expand Up @@ -629,6 +633,21 @@ Delete all Submissions to a form
"data": 3
```

### Upload a file
Upload a files to answer before form submitting
- Endpoint: `/api/2.5/uploadFiles/{formId}/{questionId}`
- Method: `POST`
- Parameters:
| Parameter | Type | Description |
|--------------|----------------|-------------|
| _formId_ | Integer | ID of the form to upload the file to |
| _questionId_ | Integer | ID of the question to upload the file to |
| _files_ | Array of files | Files to upload |
- Response: **Status-Code OK**, as well as the id of the uploaded file and it's name.
```
"data": {"uploadedFileId": integer, "fileName": "string"}
```

### Insert a Submission
Store Submission to Database
- Endpoint: `/api/v2.4/submission/insert`
Expand All @@ -644,10 +663,15 @@ Store Submission to Database
- QuestionID as key
- An **array** of values as value --> Even for short Text Answers, wrapped into Array.
- For Question-Types with pre-defined answers (`multiple`, `multiple_unique`, `dropdown`), the array contains the corresponding option-IDs.
- For File-Uploads, the array contains the objects with key `uploadedFileId` (value from Upload a file endpoint).
```
{
"1":[27,32], // dropdown or multiple
"2":["ShortTextAnswer"], // All Text-Based Question-Types
"3":[ // File-Upload
{"uploadedFileId": integer},
{"uploadedFileId": integer}
],
}
```
- Response: **Status-Code OK**.
Expand Down
20 changes: 12 additions & 8 deletions docs/DataStructure.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,11 +194,15 @@ Currently supported Question-Types are:
## Extra Settings
Optional extra settings for some [Question Types](#question-types)

| Extra Setting | Question Type | Type | Values | Description |
|--------------------|---------------|---------|--------|-------------|
| `allowOtherAnswer` | `multiple, multiple_unique` | Boolean | `true/false` | Allows the user to specify a custom answer |
| `shuffleOptions` | `dropdown, multiple, multiple_unique` | Boolean | `true/false` | The list of options should be shuffled |
| `optionsLimitMax` | `multiple` | Integer | - | Maximum number of options that can be selected |
| `optionsLimitMin` | `multiple` | Integer | - | Minimum number of options that must be selected |
| `validationType` | `short` | string | `null, 'phone', 'email', 'regex', 'number'` | Custom validation for checking a submission |
| `validationRegex` | `short` | string | regular expression | if `validationType` is 'regex' this defines the regular expression to apply |
| Extra Setting | Question Type | Type | Values | Description |
|-------------------------|---------------------------------------|------------------|---------------------------------------------|-----------------------------------------------------------------------------|
| `allowOtherAnswer` | `multiple, multiple_unique` | Boolean | `true/false` | Allows the user to specify a custom answer |
| `shuffleOptions` | `dropdown, multiple, multiple_unique` | Boolean | `true/false` | The list of options should be shuffled |
| `optionsLimitMax` | `multiple` | Integer | - | Maximum number of options that can be selected |
| `optionsLimitMin` | `multiple` | Integer | - | Minimum number of options that must be selected |
| `validationType` | `short` | string | `null, 'phone', 'email', 'regex', 'number'` | Custom validation for checking a submission |
| `validationRegex` | `short` | string | regular expression | if `validationType` is 'regex' this defines the regular expression to apply |
| `allowedFileTypes` | `file` | Array of strings | `'image', 'x-office/document'` | Allowed file types for file upload |
| `allowedFileExtensions` | `file` | Array of strings | `'jpg', 'png'` | Allowed file extensions for file upload |
| `maxAllowedFilesCount` | `file` | Integer | - | Maximum number of files that can be uploaded, 0 means no limit |
| `maxFileSize` | `file` | Integer | - | Maximum file size in bytes, 0 means no limit |
112 changes: 112 additions & 0 deletions lib/BackgroundJob/CleanupUploadedFilesJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php
/**
* @copyright Copyright (c) 2024 Kostiantyn Miakshyn <molodchick@gmail.com>
*
* @author Kostiantyn Miakshyn <molodchick@gmail.com>
*
* @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\BackgroundJob;

use OCA\Forms\Constants;
use OCA\Forms\Db\FormMapper;
use OCA\Forms\Db\UploadedFileMapper;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use Psr\Log\LoggerInterface;

class CleanupUploadedFilesJob extends TimedJob {
private const FILE_LIFETIME = '-1 hour';

public function __construct(
private IRootFolder $storage,
private FormMapper $formMapper,
private UploadedFileMapper $uploadedFileMapper,
private LoggerInterface $logger,
ITimeFactory $time) {
parent::__construct($time);

$this->setInterval(60 * 60);
}

/**
* @param array $argument
*/
public function run($argument): void {
$dateTime = new \DateTimeImmutable(self::FILE_LIFETIME);

$this->logger->info('Deleting files that were uploaded before {before} and still not submitted.', [
'before' => $dateTime->format(\DateTimeImmutable::ATOM),
]);

$uploadedFiles = $this->uploadedFileMapper->findUploadedEarlierThan($dateTime);

$deleted = 0;
$usersToCleanup = [];
foreach ($uploadedFiles as $uploadedFile) {
$this->logger->info('Deleting uploaded file "{originalFileName}" for form {formId}.', [
'originalFileName' => $uploadedFile->getOriginalFileName(),
'formId' => $uploadedFile->getFormId(),
]);

$form = $this->formMapper->findById($uploadedFile->getFormId());
$usersToCleanup[$form->getOwnerId()] = true;
$userFolder = $this->storage->getUserFolder($form->getOwnerId());

$nodes = $userFolder->getById($uploadedFile->getFileId());

if (!empty($nodes)) {
$node = $nodes[0];
$node->delete();

Check warning on line 77 in lib/BackgroundJob/CleanupUploadedFilesJob.php

View check run for this annotation

Codecov / codecov/patch

lib/BackgroundJob/CleanupUploadedFilesJob.php#L76-L77

Added lines #L76 - L77 were not covered by tests
} else {
$this->logger->warning('Could not find uploaded file "{fileId}" for deletion.', [
'fileId' => $uploadedFile->getFileId(),
]);
}

$this->uploadedFileMapper->delete($uploadedFile);

$deleted++;
}

$this->logger->info('Deleted {deleted} uploaded files.', ['deleted' => $deleted]);

// now delete empty folders in user folders
$deleted = 0;
foreach (array_keys($usersToCleanup) as $userId) {
$this->logger->info('Cleaning up empty folders for user {userId}.', ['userId' => $userId]);
$userFolder = $this->storage->getUserFolder($userId);

$unsubmittedFilesFolder = $userFolder->get(Constants::UNSUBMITTED_FILES_FOLDER);
if (!$unsubmittedFilesFolder instanceof Folder) {
continue;
}

foreach ($unsubmittedFilesFolder->getDirectoryListing() as $node) {
if ($node->getName() < $dateTime->getTimestamp()) {
$node->delete();
$deleted++;

Check warning on line 105 in lib/BackgroundJob/CleanupUploadedFilesJob.php

View check run for this annotation

Codecov / codecov/patch

lib/BackgroundJob/CleanupUploadedFilesJob.php#L102-L105

Added lines #L102 - L105 were not covered by tests
}
}
}

$this->logger->info('Deleted {deleted} folders.', ['deleted' => $deleted]);
}
}
23 changes: 22 additions & 1 deletion lib/Constants.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ class Constants {
public const ANSWER_TYPE_DATE = 'date';
public const ANSWER_TYPE_DATETIME = 'datetime';
public const ANSWER_TYPE_TIME = 'time';
public const ANSWER_TYPE_FILE = 'file';

// All AnswerTypes
public const ANSWER_TYPES = [
Expand All @@ -101,7 +102,8 @@ class Constants {
self::ANSWER_TYPE_LONG,
self::ANSWER_TYPE_DATE,
self::ANSWER_TYPE_DATETIME,
self::ANSWER_TYPE_TIME
self::ANSWER_TYPE_TIME,
self::ANSWER_TYPE_FILE,
];

// AnswerTypes, that need/have predefined Options
Expand Down Expand Up @@ -155,6 +157,21 @@ class Constants {
'validationRegex' => ['string'],
];

public const EXTRA_SETTINGS_FILE = [
'allowedFileTypes' => ['array'],
'allowedFileExtensions' => ['array'],
'maxAllowedFilesCount' => ['integer'],
'maxFileSize' => ['integer'],
];

// should be in sync with FileTypes.js
public const EXTRA_SETTINGS_ALLOWED_FILE_TYPES = [
'image',
'x-office/document',
'x-office/presentation',
'x-office/spreadsheet',
];

/**
* !! Keep in sync with src/mixins/ShareTypes.js !!
*/
Expand Down Expand Up @@ -204,4 +221,8 @@ class Constants {
];

public const DEFAULT_FILE_FORMAT = 'csv';

public const UNSUBMITTED_FILES_FOLDER = self::FILES_FOLDER . '/unsubmitted';

public const FILES_FOLDER = 'forms';
}
Loading

0 comments on commit c0ce60e

Please sign in to comment.