diff --git a/docs/API_v3.md b/docs/API_v3.md
index 17ff34d80..89a4d8d39 100644
--- a/docs/API_v3.md
+++ b/docs/API_v3.md
@@ -658,6 +658,12 @@ Get all Submissions to a Form
| Parameter | Type | Description |
|-----------|---------|-------------|
| _formId_ | Integer | ID of the form to get the submissions for |
+- Parameters:
+ | Parameter | Type | Description |
+ |------------------|----------|-------------|
+ | _query_ | String | Phrase for full text search |
+ | _limit_ | Integer | How many items to get |
+ | _offset_ | Integer | How many items to skip for a pagination |
- Response: An Array of all submissions, sorted as newest first, as well as an array of the corresponding questions.
```
@@ -746,7 +752,8 @@ Get all Submissions to a Form
"options": [],
"extraSettings": {}
}
- ]
+ ],
+ "filteredSubmissionsCount": 40
}
```
diff --git a/lib/Controller/ApiController.php b/lib/Controller/ApiController.php
index 41849153b..049998b1c 100644
--- a/lib/Controller/ApiController.php
+++ b/lib/Controller/ApiController.php
@@ -1134,7 +1134,10 @@ public function reorderOptions(int $formId, int $questionId, array $newOrder) {
* Get all the submissions of a given form
*
* @param int $formId of the form
- * @param ?string $fileFormat the file format that should be used for the download. Defaults to `null`
+ * @param ?string $query (optional) A search query to filter submissions
+ * @param ?int $limit (optional) The maximum number of submissions to retrieve. Defaults to `null`
+ * @param int $offset (optional) The offset for pagination. Defaults to `0`
+ * @param ?string $fileFormat (optional) The file format that should be used for the download. Defaults to `null`
* Possible values:
* - `csv`: Comma-separated value
* - `ods`: OpenDocument Spreadsheet
@@ -1149,7 +1152,7 @@ public function reorderOptions(int $formId, int $questionId, array $newOrder) {
#[NoAdminRequired()]
#[BruteForceProtection(action: 'form')]
#[ApiRoute(verb: 'GET', url: '/api/v3/forms/{formId}/submissions')]
- public function getSubmissions(int $formId, ?string $fileFormat = null): DataResponse|DataDownloadResponse {
+ public function getSubmissions(int $formId, ?string $query = null, ?int $limit = null, int $offset = 0, ?string $fileFormat = null): DataResponse|DataDownloadResponse {
$form = $this->getFormIfAllowed($formId, Constants::PERMISSION_RESULTS);
if ($fileFormat !== null) {
@@ -1161,9 +1164,12 @@ public function getSubmissions(int $formId, ?string $fileFormat = null): DataRes
// Load submissions and currently active questions
if (in_array(Constants::PERMISSION_RESULTS, $this->formsService->getPermissions($form))) {
- $submissions = $this->submissionService->getSubmissions($formId);
+ $submissions = $this->submissionService->getSubmissions($formId, null, $query, $limit, $offset);
+ $filteredSubmissionsCount = $this->submissionMapper->countSubmissions($formId, null, $query);
} else {
- $submissions = $this->submissionService->getSubmissions($formId, $this->currentUser->getUID());
+ $userId = $this->currentUser->getUID();
+ $submissions = $this->submissionService->getSubmissions($formId, $userId, $query, $limit, $offset);
+ $filteredSubmissionsCount = $this->submissionMapper->countSubmissions($formId, $userId, $query);
}
$questions = $this->formsService->getQuestions($formId);
@@ -1196,6 +1202,7 @@ public function getSubmissions(int $formId, ?string $fileFormat = null): DataRes
$response = [
'submissions' => $submissions,
'questions' => $questions,
+ 'filteredSubmissionsCount' => $filteredSubmissionsCount,
];
return new DataResponse($response);
diff --git a/lib/Db/SubmissionMapper.php b/lib/Db/SubmissionMapper.php
index dd1a4e585..a67434d3a 100644
--- a/lib/Db/SubmissionMapper.php
+++ b/lib/Db/SubmissionMapper.php
@@ -29,42 +29,49 @@ public function __construct(
}
/**
- * @param int $formId
- * @throws DoesNotExistException if not found
- * @return Submission[]
- */
- public function findByForm(int $formId): array {
- $qb = $this->db->getQueryBuilder();
-
- $qb->select('*')
- ->from($this->getTableName())
- ->where(
- $qb->expr()->eq('form_id', $qb->createNamedParameter($formId, IQueryBuilder::PARAM_INT))
- )
- //Newest submissions first
- ->orderBy('timestamp', 'DESC');
-
- return $this->findEntities($qb);
- }
-
- /**
- * @param int $formId
- * @param string $userId
+ * Retrieves a list of submissions for a specific form.
+ *
+ * @param int $formId The ID of the form whose submissions are being retrieved.
+ * @param string|null $userId An optional user ID to filter the submissions.
+ * @param string|null $query An optional search query to filter the submissions.
+ * @param int|null $limit The maximum number of submissions to retrieve, default: all submissions
+ * @param int $offset The number of submissions to skip before starting to retrieve, default: 0
+ *
+ * @return Submission[] An array of Submission objects.
+ * @throws DoesNotExistException If no submissions are found for the given form ID.
*
- * @return Submission[]
- * @throws \OCP\AppFramework\Db\DoesNotExistException if not found
*/
- public function findByFormAndUser(int $formId, string $userId): array {
+ public function findByForm(int $formId, ?string $userId = null, ?string $query = null, ?int $limit = null, int $offset = 0): array {
$qb = $this->db->getQueryBuilder();
- $qb->select('*')
- ->from($this->getTableName())
- ->where(
- $qb->expr()->eq('form_id', $qb->createNamedParameter($formId, IQueryBuilder::PARAM_INT)),
- $qb->expr()->eq('user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR))
+ $filters = [
+ $qb->expr()->eq('submissions.form_id', $qb->createNamedParameter($formId, IQueryBuilder::PARAM_INT)),
+ ];
+ if ($userId) {
+ $filters[] = $qb->expr()->eq('submissions.user_id', $qb->createNamedParameter($userId));
+ }
+
+ // Select all columns from the submissions table
+ $qb->selectDistinct('submissions.*')
+ ->from($this->getTableName(), 'submissions')
+ ->where(...$filters)
+ // Newest submissions first
+ ->orderBy('submissions.timestamp', 'DESC')
+ ->setFirstResult($offset)
+ ->setMaxResults($limit);
+
+ // If a query is provided, join the answers table and filter by the query text
+ if (!is_null($query) && $query !== '') {
+ $qb->join(
+ 'submissions',
+ $this->answerMapper->getTableName(),
+ 'answers',
+ $qb->expr()->eq('submissions.id', 'answers.submission_id')
)
- //Newest submissions first
- ->orderBy('timestamp', 'DESC');
+ ->andWhere(
+ $qb->expr()->like('answers.text', $qb->createNamedParameter('%' . $query . '%'))
+ );
+ }
return $this->findEntities($qb);
}
@@ -106,40 +113,62 @@ public function hasFormSubmissionsByUser(Form $form, string $userId): bool {
}
/**
- * Count submissions by form
- * @param int $formId ID of the form to count submissions
- * @param null|string $userId (optional) ID of the current user, defaults to `null`
- * @throws \Exception
+ * Counts the number of submissions associated with a specific form.
+ *
+ * @param int $formId The ID of the form for which submissions are to be counted.
+ * @param ?string $searchString An optional search string to filter submissions by their answers.
+ * @return int The total number of submissions for the specified form.
+ * @throws \Exception If an error occurs during the count operation.
*/
- public function countSubmissions(int $formId, ?string $userId = null): int {
- return $this->countSubmissionsWithFilters($formId, $userId, -1);
+ public function countSubmissions(int $formId, ?string $userId = null, ?string $searchString = null): int {
+ return $this->countSubmissionsWithFilters($formId, $userId, -1, $searchString);
}
/**
- * Count submissions by form with optional filters
- * @param int $formId ID of the form to count submissions
- * @param string|null $userId optionally limit submissions to the one of that user
- * @param int $limit allows to limit the query selection. If -1, the restriction is ignored
- * @throws \Exception
+ * Count submissions by form with optional filters.
+ *
+ * @param int $formId The ID of the form for which submissions are to be counted.
+ * @param string|null $userId Optionally limit submissions to those made by the specified user.
+ * @param int $limit The maximum number of submissions to count. If -1, no limit is applied.
+ * @param string|null $searchString An optional search string to filter submissions by their answers.
+ *
+ * @return int The total number of submissions matching the specified filters.
+ *
+ * @throws \Exception If an error occurs during the count operation.
*/
- protected function countSubmissionsWithFilters(int $formId, ?string $userId = null, int $limit = -1): int {
+ protected function countSubmissionsWithFilters(int $formId, ?string $userId = null, int $limit = -1, ?string $searchString = null): int {
$qb = $this->db->getQueryBuilder();
- $query = $qb->select($qb->func()->count('*', 'num_submissions'))
- ->from($this->getTableName())
- ->where($qb->expr()->eq('form_id', $qb->createNamedParameter($formId, IQueryBuilder::PARAM_INT)));
+ $query = $qb->select('submissions.id')
+ ->from($this->getTableName(), 'submissions')
+ ->where($qb->expr()->eq('submissions.form_id', $qb->createNamedParameter($formId, IQueryBuilder::PARAM_INT)))
+ ->groupBy('submissions.id');
+
if (!is_null($userId)) {
- $query->andWhere($qb->expr()->eq('user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)));
+ $query->andWhere($qb->expr()->eq('submissions.user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)));
}
+
+ if (!is_null($searchString) && $searchString !== '') {
+ $query->join(
+ 'submissions',
+ $this->answerMapper->getTableName(),
+ 'answers',
+ $qb->expr()->eq('submissions.id', 'answers.submission_id')
+ )
+ ->andWhere(
+ $qb->expr()->like('answers.text', $qb->createNamedParameter('%' . $searchString . '%'))
+ );
+ }
+
if ($limit !== -1) {
$query->setMaxResults($limit);
}
$result = $query->executeQuery();
- $row = $result->fetch();
+ $rows = $result->fetchAll();
$result->closeCursor();
- return (int)($row['num_submissions'] ?? 0);
+ return count($rows);
}
/**
diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php
index c3d0d1d7b..05e8e08cc 100644
--- a/lib/ResponseDefinitions.php
+++ b/lib/ResponseDefinitions.php
@@ -77,7 +77,8 @@
*
* @psalm-type FormsSubmissions = array{
* submissions: list,
- * questions: list
+ * questions: list,
+ * filteredSubmissionsCount: int
* }
*
* @psalm-type FormsAccess = array{
diff --git a/lib/Service/SubmissionService.php b/lib/Service/SubmissionService.php
index b43fa2039..8b4e0aecc 100644
--- a/lib/Service/SubmissionService.php
+++ b/lib/Service/SubmissionService.php
@@ -92,6 +92,9 @@ private function getAnswers(int $submissionId): array {
*
* @param int $formId the form id
* @param string|null $userId optional user id to filter submissions
+ * @param string|null $query optional search query to filter submissions
+ * @param int|null $limit the maximum number of submissions to return
+ * @param int $offset the number of submissions to skip
* @return list,
* }>
*/
- public function getSubmissions(int $formId, ?string $userId = null): array {
+ public function getSubmissions(int $formId, ?string $userId = null, ?string $query = null, ?int $limit = null, int $offset = 0): array {
$submissionList = [];
try {
- if (is_null($userId)) {
- $submissionEntities = $this->submissionMapper->findByForm($formId);
- } else {
- $submissionEntities = $this->submissionMapper->findByFormAndUser($formId, $userId);
- }
+ $submissionEntities = $this->submissionMapper->findByForm($formId, $userId, $query, $limit, $offset);
foreach ($submissionEntities as $submissionEntity) {
$submission = $submissionEntity->read();
diff --git a/openapi.json b/openapi.json
index 2064ddd35..365f9116c 100644
--- a/openapi.json
+++ b/openapi.json
@@ -592,7 +592,8 @@
"type": "object",
"required": [
"submissions",
- "questions"
+ "questions",
+ "filteredSubmissionsCount"
],
"properties": {
"submissions": {
@@ -606,6 +607,10 @@
"items": {
"$ref": "#/components/schemas/Question"
}
+ },
+ "filteredSubmissionsCount": {
+ "type": "integer",
+ "format": "int64"
}
}
},
@@ -3019,10 +3024,39 @@
"format": "int64"
}
},
+ {
+ "name": "query",
+ "in": "query",
+ "description": "(optional) A search query to filter submissions",
+ "schema": {
+ "type": "string",
+ "nullable": true
+ }
+ },
+ {
+ "name": "limit",
+ "in": "query",
+ "description": "(optional) The maximum number of submissions to retrieve. Defaults to `null`",
+ "schema": {
+ "type": "integer",
+ "format": "int64",
+ "nullable": true
+ }
+ },
+ {
+ "name": "offset",
+ "in": "query",
+ "description": "(optional) The offset for pagination. Defaults to `0`",
+ "schema": {
+ "type": "integer",
+ "format": "int64",
+ "default": 0
+ }
+ },
{
"name": "fileFormat",
"in": "query",
- "description": "the file format that should be used for the download. Defaults to `null` Possible values: - `csv`: Comma-separated value - `ods`: OpenDocument Spreadsheet - `xlsx`: Excel Open XML Spreadsheet",
+ "description": "(optional) The file format that should be used for the download. Defaults to `null` Possible values: - `csv`: Comma-separated value - `ods`: OpenDocument Spreadsheet - `xlsx`: Excel Open XML Spreadsheet",
"schema": {
"type": "string",
"nullable": true
diff --git a/src/components/PaginationToolbar.vue b/src/components/PaginationToolbar.vue
new file mode 100644
index 000000000..8b3e65313
--- /dev/null
+++ b/src/components/PaginationToolbar.vue
@@ -0,0 +1,165 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/components/Results/Answer.vue b/src/components/Results/Answer.vue
index f601f8922..3c5dafc19 100644
--- a/src/components/Results/Answer.vue
+++ b/src/components/Results/Answer.vue
@@ -18,23 +18,25 @@
dir="auto">
- {{ answer.text }}
+
- {{ answerText }}
+
diff --git a/src/components/Results/Submission.vue b/src/components/Results/Submission.vue
index 28a16e1c7..fc79ef3cf 100644
--- a/src/components/Results/Submission.vue
+++ b/src/components/Results/Submission.vue
@@ -36,6 +36,7 @@
@@ -86,6 +87,10 @@ export default {
type: Boolean,
required: true,
},
+ highlight: {
+ type: String,
+ default: null,
+ },
},
computed: {
diff --git a/src/views/Results.vue b/src/views/Results.vue
index a169ccc04..25c178f46 100644
--- a/src/views/Results.vue
+++ b/src/views/Results.vue
@@ -44,7 +44,7 @@
{{
t('forms', '{amount} responses', {
- amount: submissions.length ?? 0,
+ amount: filteredSubmissionsCount,
})
}}
@@ -165,12 +165,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -242,6 +282,7 @@ import NcActionSeparator from '@nextcloud/vue/components/NcActionSeparator'
import NcAppContent from '@nextcloud/vue/components/NcAppContent'
import NcButton from '@nextcloud/vue/components/NcButton'
import NcDialog from '@nextcloud/vue/components/NcDialog'
+import NcTextField from '@nextcloud/vue/components/NcTextField'
import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
import axios from '@nextcloud/axios'
@@ -266,8 +307,9 @@ import IconRefresh from 'vue-material-design-icons/Refresh.vue'
import IconShareVariant from 'vue-material-design-icons/ShareVariant.vue'
import IconTable from 'vue-material-design-icons/Table.vue'
import IconTableSvg from '@mdi/svg/svg/table.svg?raw'
+import IconMagnify from 'vue-material-design-icons/Magnify.vue'
-import { FormState } from '../models/Constants.ts'
+import { FormState, INPUT_DEBOUNCE_MS } from '../models/Constants.ts'
import ResultsSummary from '../components/Results/ResultsSummary.vue'
import Submission from '../components/Results/Submission.vue'
import TopBar from '../components/TopBar.vue'
@@ -276,8 +318,10 @@ import answerTypes from '../models/AnswerTypes.js'
import logger from '../utils/Logger.js'
import SetWindowTitle from '../utils/SetWindowTitle.js'
import OcsResponse2Data from '../utils/OcsResponse2Data.js'
+import PaginationToolbar from '../components/PaginationToolbar.vue'
import PermissionTypes from '../mixins/PermissionTypes.js'
import PillMenu from '../components/PillMenu.vue'
+import debounce from 'debounce'
const SUPPORTED_FILE_FORMATS = {
ods: IconTableSvg,
@@ -319,6 +363,9 @@ export default {
NcAppContent,
NcButton,
NcDialog,
+ NcTextField,
+ PaginationToolbar,
+ IconMagnify,
NcEmptyContent,
NcLoadingIcon,
PillMenu,
@@ -344,13 +391,19 @@ export default {
questions: [],
submissions: [],
+ filteredSubmissionsCount: 0,
isDownloadActionOpened: false,
loadingResults: true,
+ skipReloadOnOffsetChange: false,
picker: null,
showConfirmDeleteDialog: false,
+ submissionSearch: '',
+ limit: 20,
+ offset: 0,
+
linkedFileNotAvailableButtons: [
{
label: t('forms', 'Unlink spreadsheet'),
@@ -434,12 +487,29 @@ export default {
},
watch: {
- // Reload results, when form changes
+ // Reload results when form changes
async hash() {
await this.fetchFullForm(this.form.id)
this.loadFormResults()
SetWindowTitle(this.formTitle)
},
+ limit() {
+ this.loadFormResults()
+ },
+ offset() {
+ // Only load results if we're not changing offset from submissionSearch watch
+ if (!this.skipReloadOnOffsetChange) {
+ this.loadFormResults()
+ }
+ },
+ submissionSearch: debounce(function () {
+ this.skipReloadOnOffsetChange = true
+ this.offset = 0
+ this.$nextTick(() => {
+ this.skipReloadOnOffsetChange = false
+ })
+ this.loadFormResults()
+ }, INPUT_DEBOUNCE_MS),
},
async beforeMount() {
@@ -478,22 +548,25 @@ export default {
try {
const response = await axios.get(
- generateOcsUrl('apps/forms/api/v3/forms/{id}/submissions', {
- id: this.form.id,
- }),
- )
-
- let loadedSubmissions = OcsResponse2Data(response).submissions
- const loadedQuestions = OcsResponse2Data(response).questions
-
- loadedSubmissions = this.formatDateAnswers(
- loadedSubmissions,
- loadedQuestions,
+ generateOcsUrl(
+ 'apps/forms/api/v3/forms/{id}/submissions?limit={limit}&offset={offset}&query={query}',
+ {
+ id: this.form.id,
+ limit: this.limit,
+ offset: this.offset,
+ query: this.submissionSearch,
+ },
+ ),
)
+ const data = OcsResponse2Data(response)
// Append questions & submissions
- this.submissions = loadedSubmissions
- this.questions = loadedQuestions
+ this.submissions = this.formatDateAnswers(
+ data.submissions,
+ data.questions,
+ )
+ this.questions = data.questions
+ this.filteredSubmissionsCount = data.filteredSubmissionsCount
} catch (error) {
logger.error('Error while loading results', { error })
showError(t('forms', 'There was an error while loading the results'))
@@ -869,4 +942,14 @@ export default {
}
}
}
+
+.search-wrapper {
+ margin-block-start: calc(-1 * var(--default-grid-baseline));
+ margin-inline-start: auto;
+ margin-inline-end: var(--default-clickable-area);
+}
+
+.bottom-pagination {
+ margin-bottom: 24px;
+}
diff --git a/tests/Integration/Api/ApiV3Test.php b/tests/Integration/Api/ApiV3Test.php
index b25187ed0..f1b051edf 100644
--- a/tests/Integration/Api/ApiV3Test.php
+++ b/tests/Integration/Api/ApiV3Test.php
@@ -1116,7 +1116,8 @@ public function dataGetSubmissions() {
]
]
],
- 'questions' => $this->dataGetFullForm()['getFullForm']['expected']['questions']
+ 'questions' => $this->dataGetFullForm()['getFullForm']['expected']['questions'],
+ 'filteredSubmissionsCount' => 3,
]
]
];
@@ -1236,6 +1237,7 @@ public function testExportToCloud() {
public function dataDeleteSubmissions() {
$submissionsExpected = $this->dataGetSubmissions()['getSubmissions']['expected'];
$submissionsExpected['submissions'] = [];
+ $submissionsExpected['filteredSubmissionsCount'] = 0;
return [
'deleteSubmissions' => [
@@ -1368,6 +1370,7 @@ public function dataDeleteSingleSubmission() {
* @param array $submissionsExpected
*/
public function testDeleteSingleSubmission(array $submissionsExpected) {
+ $submissionsExpected['filteredSubmissionsCount'] = $submissionsExpected['filteredSubmissionsCount'] - 1;
$resp = $this->http->request('DELETE', "api/v3/forms/{$this->testForms[0]['id']}/submissions/{$this->testForms[0]['submissions'][0]['id']}");
$data = $this->OcsResponse2Data($resp);
diff --git a/tests/Integration/DB/SharedFormsTest.php b/tests/Integration/DB/SharedFormsTest.php
index 2998cbd4e..29bdb9f8f 100644
--- a/tests/Integration/DB/SharedFormsTest.php
+++ b/tests/Integration/DB/SharedFormsTest.php
@@ -6,7 +6,7 @@
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
-namespace OCA\Forms\Tests\Integration\Api;
+namespace OCA\Forms\Tests\Integration\Db;
use OCA\Forms\AppInfo\Application;
use OCA\Forms\Constants;
diff --git a/tests/Integration/DB/SubmissionMapperTest.php b/tests/Integration/DB/SubmissionMapperTest.php
new file mode 100644
index 000000000..e204d56c7
--- /dev/null
+++ b/tests/Integration/DB/SubmissionMapperTest.php
@@ -0,0 +1,184 @@
+ 'Test user',
+ 'user1' => 'User One',
+ 'user2' => 'User Two',
+ ];
+
+ private function setTestForms() {
+ $this->testForms = [
+ [
+ 'hash' => 'test_form_1',
+ 'title' => 'Test Form 1',
+ 'description' => 'Form for submission testing',
+ 'owner_id' => 'test',
+ 'access_enum' => 0,
+ 'created' => 12345,
+ 'expires' => 0,
+ 'state' => 0,
+ 'is_anonymous' => false,
+ 'submit_multiple' => true,
+ 'show_expiration' => false,
+ 'last_updated' => 123456789,
+ 'submission_message' => '',
+ 'file_id' => null,
+ 'file_format' => null,
+ 'questions' => [
+ [
+ 'type' => 'short',
+ 'text' => 'First Question?',
+ 'isRequired' => true,
+ 'name' => '',
+ 'order' => 1,
+ 'options' => [],
+ 'accept' => [],
+ 'description' => 'Please answer this.',
+ 'extraSettings' => []
+ ]
+ ],
+ 'shares' => [],
+ 'submissions' => [
+ [
+ 'userId' => 'user1',
+ 'timestamp' => 100000,
+ 'answers' => [
+ [
+ 'questionIndex' => 0,
+ 'text' => 'Answer 1'
+ ]
+ ]
+ ],
+ [
+ 'userId' => 'user1',
+ 'timestamp' => 100001,
+ 'answers' => [
+ [
+ 'questionIndex' => 0,
+ 'text' => 'Answer 2'
+ ]
+ ]
+ ],
+ [
+ 'userId' => 'user2',
+ 'timestamp' => 100002,
+ 'answers' => [
+ [
+ 'questionIndex' => 0,
+ 'text' => 'Search term'
+ ]
+ ]
+ ]
+ ]
+ ],
+ [
+ 'hash' => 'test_form_2',
+ 'title' => 'Test Form 2',
+ 'description' => 'Empty form',
+ 'owner_id' => 'test',
+ 'access_enum' => 0,
+ 'created' => 12345,
+ 'expires' => 0,
+ 'state' => 0,
+ 'is_anonymous' => false,
+ 'submit_multiple' => false,
+ 'show_expiration' => false,
+ 'last_updated' => 123456789,
+ 'submission_message' => '',
+ 'file_id' => null,
+ 'file_format' => null,
+ 'questions' => [],
+ 'shares' => [],
+ 'submissions' => []
+ ]
+ ];
+ }
+
+ public function setUp(): void {
+ $this->setTestForms();
+ parent::setUp();
+
+ $db = \OCP\Server::get(IDBConnection::class);
+ $answerMapper = \OCP\Server::get(\OCA\Forms\Db\AnswerMapper::class);
+ $this->submissionMapper = new SubmissionMapper($db, $answerMapper);
+ }
+
+ public function testFindByFormBasic(): void {
+ $submissions = $this->submissionMapper->findByForm($this->testForms[0]['id']);
+
+ $this->assertCount(3, $submissions);
+ $this->assertEquals('user2', $submissions[0]->getUserId());
+ $this->assertEquals(100002, $submissions[0]->getTimestamp());
+ }
+
+ public function testFindByFormWithUser(): void {
+ $submissions = $this->submissionMapper->findByForm($this->testForms[0]['id'], 'user1');
+
+ $this->assertCount(2, $submissions);
+ foreach ($submissions as $submission) {
+ $this->assertEquals('user1', $submission->getUserId());
+ }
+ }
+
+ public function testFindByFormWithSearchQuery(): void {
+ $submissions = $this->submissionMapper->findByForm($this->testForms[0]['id'], null, 'Search term');
+
+ $this->assertCount(1, $submissions);
+ $this->assertEquals('user2', $submissions[0]->getUserId());
+ }
+
+ public function testFindByFormWithLimit(): void {
+ $submissions = $this->submissionMapper->findByForm($this->testForms[0]['id'], null, null, 2);
+
+ $this->assertCount(2, $submissions);
+ }
+
+ public function testFindByFormWithOffset(): void {
+ $submissions = $this->submissionMapper->findByForm($this->testForms[0]['id'], null, null, null, 1);
+
+ $this->assertCount(2, $submissions);
+ }
+
+ public function testCountSubmissionsBasic(): void {
+ $count = $this->submissionMapper->countSubmissions($this->testForms[0]['id']);
+
+ $this->assertEquals(3, $count);
+ }
+
+ public function testCountSubmissionsWithUser(): void {
+ $count = $this->submissionMapper->countSubmissions($this->testForms[0]['id'], 'user1');
+
+ $this->assertEquals(2, $count);
+ }
+
+ public function testCountSubmissionsWithSearch(): void {
+ $count = $this->submissionMapper->countSubmissions($this->testForms[0]['id'], null, 'Search term');
+
+ $this->assertEquals(1, $count);
+ }
+
+ public function testCountSubmissionsEmptyForm(): void {
+ $count = $this->submissionMapper->countSubmissions($this->testForms[1]['id']);
+
+ $this->assertEquals(0, $count);
+ }
+}
diff --git a/tests/Unit/Controller/ApiControllerTest.php b/tests/Unit/Controller/ApiControllerTest.php
index 71f6d7248..f7e4addf5 100644
--- a/tests/Unit/Controller/ApiControllerTest.php
+++ b/tests/Unit/Controller/ApiControllerTest.php
@@ -245,6 +245,7 @@ public function dataGetSubmissions() {
'extraSettings' => new \stdClass(),
],
],
+ 'filteredSubmissionsCount' => 1,
]
],
'user' => [
@@ -265,6 +266,7 @@ public function dataGetSubmissions() {
'extraSettings' => new \stdClass(),
],
],
+ 'filteredSubmissionsCount' => 1,
]
]
];
@@ -298,6 +300,11 @@ public function testGetSubmissions(array $submissions, array $questions, array $
->with(1)
->willReturn($questions);
+ $this->submissionMapper->expects($this->once())
+ ->method('countSubmissions')
+ ->with(1)
+ ->willReturn(1);
+
$this->assertEquals(new DataResponse($expected), $this->apiController->getSubmissions(1));
}
@@ -357,7 +364,7 @@ public function testExportSubmissions() {
->with($form, 'csv')
->willReturn($fileName);
- $this->assertEquals(new DataDownloadResponse($csv, $fileName, 'text/csv'), $this->apiController->getSubmissions(1, 'csv'));
+ $this->assertEquals(new DataDownloadResponse($csv, $fileName, 'text/csv'), $this->apiController->getSubmissions(1, fileFormat: 'csv'));
}
public function testExportSubmissionsToCloud_invalidForm() {
diff --git a/tests/Unit/Db/SubmissionMapperTest.php b/tests/Unit/Db/SubmissionMapperTest.php
deleted file mode 100644
index 6c27a217b..000000000
--- a/tests/Unit/Db/SubmissionMapperTest.php
+++ /dev/null
@@ -1,122 +0,0 @@
-mockSubmissionMapper = $this->getMockBuilder(SubmissionMapper::class)
- ->disableOriginalConstructor()
- ->setMethods(['countSubmissionsWithFilters'])
- ->getMock();
- }
-
- /**
- * @dataProvider dataHasMultipleFormSubmissionsByUser
- */
- public function testHasMultipleFormSubmissionsByUser(int $numberOfSubmissions, bool $expected) {
- $this->mockSubmissionMapper->expects($this->once())
- ->method('countSubmissionsWithFilters')
- ->will($this->returnValue($numberOfSubmissions));
-
- $form = new Form();
- $form->setId(1);
-
- $this->assertEquals($expected, $this->mockSubmissionMapper->hasMultipleFormSubmissionsByUser($form, 'user1'));
- }
-
- public function dataHasMultipleFormSubmissionsByUser() {
- return [
- [
- 'numberOfSubmissions' => 0,
- 'expected' => false,
- ],
- [
- 'numberOfSubmissions' => 1,
- 'expected' => false,
- ],
- [
- 'numberOfSubmissions' => 2,
- 'expected' => true,
- ],
- [
- 'numberOfSubmissions' => 3,
- 'expected' => true,
- ],
- ];
- }
-
- /**
- * @dataProvider dataHasFormSubmissionsByUser
- */
- public function testHasFormSubmissionsByUser(int $numberOfSubmissions, bool $expected) {
- $this->mockSubmissionMapper->expects($this->once())
- ->method('countSubmissionsWithFilters')
- ->will($this->returnValue($numberOfSubmissions));
-
- $form = new Form();
- $form->setId(1);
-
- $this->assertEquals($expected, $this->mockSubmissionMapper->hasFormSubmissionsByUser($form, 'user1'));
- }
-
- public function dataHasFormSubmissionsByUser() {
- return [
- [
- 'numberOfSubmissions' => 0,
- 'expected' => false,
- ],
- [
- 'numberOfSubmissions' => 1,
- 'expected' => true,
- ],
- [
- 'numberOfSubmissions' => 2,
- 'expected' => true,
- ],
- ];
- }
-
- /**
- * @dataProvider dataCountSubmissions
- */
- public function testCountSubmissions(int $numberOfSubmissions, int $expected) {
- $this->mockSubmissionMapper->expects($this->once())
- ->method('countSubmissionsWithFilters')
- ->will($this->returnValue($numberOfSubmissions));
-
- $this->assertEquals($expected, $this->mockSubmissionMapper->countSubmissions(1));
- }
-
- public function dataCountSubmissions() {
- return [
- [
- 'numberOfSubmissions' => 0,
- 'expected' => 0,
- ],
- [
- 'numberOfSubmissions' => 1,
- 'expected' => 1,
- ],
- [
- 'numberOfSubmissions' => 20,
- 'expected' => 20,
- ],
- ];
- }
-}
diff --git a/tests/Unit/Service/SubmissionServiceTest.php b/tests/Unit/Service/SubmissionServiceTest.php
index 5550404bd..42b48cfe2 100644
--- a/tests/Unit/Service/SubmissionServiceTest.php
+++ b/tests/Unit/Service/SubmissionServiceTest.php
@@ -158,15 +158,15 @@ public function testGetSubmissions() {
$submission_2->setUserId('someOtherUser');
$submission_2->setTimestamp(1234);
- $this->submissionMapper->expects($this->once())
+ $this->submissionMapper->expects($this->any())
->method('findByForm')
- ->with(5)
- ->willReturn([$submission_1, $submission_2]);
+ ->willReturnCallback(function ($formId, $userId = null) use ($submission_1, $submission_2) {
+ if ($userId === 'someOtherUser') {
+ return [$submission_2];
+ }
- $this->submissionMapper->expects($this->once())
- ->method('findByFormAndUser')
- ->with(5, 'someOtherUser')
- ->willReturn([$submission_2]);
+ return [$submission_1, $submission_2];
+ });
$this->answerMapper->expects($this->any())
->method('findBySubmission')