Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for file question #2040

Merged
merged 1 commit into from
Jun 8, 2024
Merged
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
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}',
Koc marked this conversation as resolved.
Show resolved Hide resolved
'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
susnux marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php
/**
* @copyright Copyright (c) 2024 Kostiantyn Miakshyn <[email protected]>
*
* @author Kostiantyn Miakshyn <[email protected]>
*
* @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
Loading