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')