Skip to content

Commit d694698

Browse files
committed
Add support for file question
Signed-off-by: Konstantin Myakshin <[email protected]>
1 parent 28901cc commit d694698

26 files changed

+1300
-109
lines changed

appinfo/info.xml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
- **🙋 Get involved!** We have lots of stuff planned like more question types, collaboration on forms, [and much more](https://github.com/nextcloud/forms/milestones)!
1414
]]></description>
1515

16-
<version>4.2.3</version>
16+
<version>4.2.4</version>
1717
<licence>agpl</licence>
1818

1919
<author>Affan Hussain</author>
@@ -54,6 +54,10 @@
5454
<nextcloud min-version="28" max-version="29" />
5555
</dependencies>
5656

57+
<background-jobs>
58+
<job>OCA\Forms\BackgroundJob\CleanupUploadedFilesJob</job>
59+
</background-jobs>
60+
5761
<settings>
5862
<admin>OCA\Forms\Settings\Settings</admin>
5963
<admin-section>OCA\Forms\Settings\SettingsSection</admin-section>

appinfo/routes.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,14 @@
331331
'apiVersion' => 'v2(\.[1-4])?'
332332
]
333333
],
334+
[
335+
'name' => 'api#uploadFiles',
336+
'url' => '/api/{apiVersion}/uploadFiles/{formId}/{questionId}',
337+
'verb' => 'POST',
338+
'requirements' => [
339+
'apiVersion' => 'v2.5'
340+
]
341+
],
334342
[
335343
'name' => 'api#insertSubmission',
336344
'url' => '/api/{apiVersion}/submission/insert',

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030
"phpunit/phpunit": "^9"
3131
},
3232
"require": {
33-
"phpoffice/phpspreadsheet": "^2.0"
33+
"phpoffice/phpspreadsheet": "^2.0",
34+
"symfony/polyfill-uuid": "^1.29"
3435
},
3536
"extra": {
3637
"bamarni-bin": {

composer.lock

Lines changed: 80 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/API.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ This file contains the API-Documentation. For more information on the returned D
2828
- Completely new way of handling access & shares.
2929

3030
### Other API changes
31+
- In API version 2.5 the following endpoints were introduced:
32+
- `POST /api/2.5/uploadFiles/{formId}/{questionId}` to upload files to answer before form submitting
3133
- In API version 2.4 the following endpoints were introduced:
3234
- `POST /api/2.4/form/link/{fileFormat}` to link form to a file
3335
- `POST /api/2.4/form/unlink` to unlink form from a file
@@ -629,6 +631,21 @@ Delete all Submissions to a form
629631
"data": 3
630632
```
631633

634+
### Upload a file
635+
Upload a files to answer before form submitting
636+
- Endpoint: `/api/2.5/uploadFiles/{formId}/{questionId}`
637+
- Method: `POST`
638+
- Parameters:
639+
| Parameter | Type | Description |
640+
|-----------|---------|-------------|
641+
| _formId_ | Integer | ID of the form to upload the file to |
642+
| _questionId_ | Integer | ID of the question to upload the file to |
643+
| _files_ | Array of files | Files to upload |
644+
- Response: **Status-Code OK**, as well as the id of the uploaded file and it's name.
645+
```
646+
"data": {"uploadedFileId": "uuid4", "fileName": "string"}
647+
```
648+
632649
### Insert a Submission
633650
Store Submission to Database
634651
- Endpoint: `/api/v2.4/submission/insert`
@@ -644,10 +661,15 @@ Store Submission to Database
644661
- QuestionID as key
645662
- An **array** of values as value --> Even for short Text Answers, wrapped into Array.
646663
- For Question-Types with pre-defined answers (`multiple`, `multiple_unique`, `dropdown`), the array contains the corresponding option-IDs.
664+
- For File-Uploads, the array contains the objects with key `uploadedFileId` (value from Upload a file endpoint).
647665
```
648666
{
649667
"1":[27,32], // dropdown or multiple
650668
"2":["ShortTextAnswer"], // All Text-Based Question-Types
669+
"3":[ // File-Upload
670+
{"uploadedFileId": "uuid4"},
671+
{"uploadedFileId": "uuid4"}
672+
],
651673
}
652674
```
653675
- Response: **Status-Code OK**.

docs/DataStructure.md

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -194,11 +194,15 @@ Currently supported Question-Types are:
194194
## Extra Settings
195195
Optional extra settings for some [Question Types](#question-types)
196196

197-
| Extra Setting | Question Type | Type | Values | Description |
198-
|--------------------|---------------|---------|--------|-------------|
199-
| `allowOtherAnswer` | `multiple, multiple_unique` | Boolean | `true/false` | Allows the user to specify a custom answer |
200-
| `shuffleOptions` | `dropdown, multiple, multiple_unique` | Boolean | `true/false` | The list of options should be shuffled |
201-
| `optionsLimitMax` | `multiple` | Integer | - | Maximum number of options that can be selected |
202-
| `optionsLimitMin` | `multiple` | Integer | - | Minimum number of options that must be selected |
203-
| `validationType` | `short` | string | `null, 'phone', 'email', 'regex', 'number'` | Custom validation for checking a submission |
204-
| `validationRegex` | `short` | string | regular expression | if `validationType` is 'regex' this defines the regular expression to apply |
197+
| Extra Setting | Question Type | Type | Values | Description |
198+
|-------------------------|---------------------------------------|------------------|---------------------------------------------|-----------------------------------------------------------------------------|
199+
| `allowOtherAnswer` | `multiple, multiple_unique` | Boolean | `true/false` | Allows the user to specify a custom answer |
200+
| `shuffleOptions` | `dropdown, multiple, multiple_unique` | Boolean | `true/false` | The list of options should be shuffled |
201+
| `optionsLimitMax` | `multiple` | Integer | - | Maximum number of options that can be selected |
202+
| `optionsLimitMin` | `multiple` | Integer | - | Minimum number of options that must be selected |
203+
| `validationType` | `short` | string | `null, 'phone', 'email', 'regex', 'number'` | Custom validation for checking a submission |
204+
| `validationRegex` | `short` | string | regular expression | if `validationType` is 'regex' this defines the regular expression to apply |
205+
| `allowedFileTypes` | `file` | Array of strings | `'image', 'x-office/document'` | Allowed file types for file upload |
206+
| `allowedFileExtensions` | `file` | Array of strings | `'jpg', 'png'` | Allowed file extensions for file upload |
207+
| `maxAllowedFilesCount` | `file` | Integer | - | Maximum number of files that can be uploaded, 0 means no limit |
208+
| `maxFileSize` | `file` | Integer | - | Maximum file size in bytes, 0 means no limit |
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
3+
namespace OCA\Forms\BackgroundJob;
4+
5+
use OCA\Forms\Constants;
6+
use OCA\Forms\Db\FormMapper;
7+
use OCA\Forms\Db\UploadedFileMapper;
8+
use OCP\AppFramework\Utility\ITimeFactory;
9+
use OCP\BackgroundJob\TimedJob;
10+
use OCP\Files\Folder;
11+
use OCP\Files\IRootFolder;
12+
use Psr\Log\LoggerInterface;
13+
14+
class CleanupUploadedFilesJob extends TimedJob {
15+
private const FILE_LIFETIME = '-2 days';
16+
17+
public function __construct(
18+
private IRootFolder $storage,
19+
private FormMapper $formMapper,
20+
private UploadedFileMapper $uploadedFileMapper,
21+
private LoggerInterface $logger,
22+
ITimeFactory $time) {
23+
parent::__construct($time);
24+
25+
$this->setInterval(60 * 60 * 24);
26+
27+
}
28+
29+
/**
30+
* @param array $argument
31+
*/
32+
public function run($argument): void {
33+
$dateTime = new \DateTimeImmutable(self::FILE_LIFETIME);
34+
35+
$this->logger->info('Deleting files that were uploaded before {before} and still not submitted.', [
36+
'before' => $dateTime->format(\DateTimeImmutable::ATOM),
37+
]);
38+
39+
$uploadedFiles = $this->uploadedFileMapper->findUploadedEarlierThan($dateTime);
40+
41+
$deleted = 0;
42+
$usersToCleanup = [];
43+
foreach ($uploadedFiles as $uploadedFile) {
44+
$this->logger->info('Deleting uploaded file "{originalFileName}" for form {formId}.', [
45+
'originalFileName' => $uploadedFile->getOriginalFileName(),
46+
'formId' => $uploadedFile->getFormId(),
47+
]);
48+
49+
$form = $this->formMapper->findById($uploadedFile->getFormId());
50+
$usersToCleanup[$form->getOwnerId()] = true;
51+
$userFolder = $this->storage->getUserFolder($form->getOwnerId());
52+
53+
$nodes = $userFolder->getById($uploadedFile->getFileId());
54+
55+
if (!empty($nodes)) {
56+
$node = $nodes[0];
57+
$node->delete();
58+
} else {
59+
$this->logger->warning('Could not find uploaded file "{fileId}" for deletion.', [
60+
'fileId' => $uploadedFile->getFileId(),
61+
]);
62+
}
63+
64+
$this->uploadedFileMapper->delete($uploadedFile);
65+
66+
$deleted++;
67+
}
68+
69+
$this->logger->info('Deleted {deleted} uploaded files.', ['deleted' => $deleted]);
70+
71+
// now delete empty folders in user folders
72+
$deleted = 0;
73+
foreach (array_keys($usersToCleanup) as $userId) {
74+
$this->logger->info('Cleaning up empty folders for user {userId}.', ['userId' => $userId]);
75+
$userFolder = $this->storage->getUserFolder($userId);
76+
77+
$unsubmittedFilesFolder = $userFolder->get(Constants::UNSUBMITTED_FILES_FOLDER);
78+
if (!$unsubmittedFilesFolder instanceof Folder) {
79+
continue;
80+
}
81+
82+
foreach ($unsubmittedFilesFolder->getDirectoryListing() as $node) {
83+
if ($node->getName() < $dateTime->getTimestamp()) {
84+
$node->delete();
85+
$deleted++;
86+
}
87+
}
88+
}
89+
90+
$this->logger->info('Deleted {deleted} folders.', ['deleted' => $deleted]);
91+
}
92+
}

lib/Constants.php

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ class Constants {
9191
public const ANSWER_TYPE_DATE = 'date';
9292
public const ANSWER_TYPE_DATETIME = 'datetime';
9393
public const ANSWER_TYPE_TIME = 'time';
94+
public const ANSWER_TYPE_FILE = 'file';
9495

9596
// All AnswerTypes
9697
public const ANSWER_TYPES = [
@@ -101,7 +102,8 @@ class Constants {
101102
self::ANSWER_TYPE_LONG,
102103
self::ANSWER_TYPE_DATE,
103104
self::ANSWER_TYPE_DATETIME,
104-
self::ANSWER_TYPE_TIME
105+
self::ANSWER_TYPE_TIME,
106+
self::ANSWER_TYPE_FILE,
105107
];
106108

107109
// AnswerTypes, that need/have predefined Options
@@ -155,6 +157,21 @@ class Constants {
155157
'validationRegex' => ['string'],
156158
];
157159

160+
public const EXTRA_SETTINGS_FILE = [
161+
'allowedFileTypes',
162+
'allowedFileExtensions',
163+
'maxAllowedFilesCount',
164+
'maxFileSize',
165+
];
166+
167+
// should be in sync with FileTypes.js
168+
public const EXTRA_SETTINGS_ALLOWED_FILE_TYPES = [
169+
'image',
170+
'x-office/document',
171+
'x-office/presentation',
172+
'x-office/spreadsheet',
173+
];
174+
158175
/**
159176
* !! Keep in sync with src/mixins/ShareTypes.js !!
160177
*/
@@ -204,4 +221,8 @@ class Constants {
204221
];
205222

206223
public const DEFAULT_FILE_FORMAT = 'csv';
224+
225+
public const UNSUBMITTED_FILES_FOLDER = 'forms/unsubmitted';
226+
227+
public const FILES_FOLDER = 'forms';
207228
}

0 commit comments

Comments
 (0)