Skip to content

Commit 09b9ad7

Browse files
committed
Add pagination for submissions on a backend
Signed-off-by: Kostiantyn Miakshyn <molodchick@gmail.com>
1 parent ab65126 commit 09b9ad7

16 files changed

Lines changed: 628 additions & 214 deletions

docs/API_v3.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,12 @@ Get all Submissions to a Form
658658
| Parameter | Type | Description |
659659
|-----------|---------|-------------|
660660
| _formId_ | Integer | ID of the form to get the submissions for |
661+
- Parameters:
662+
| Parameter | Type | Description |
663+
|------------------|----------|-------------|
664+
| _query_ | String | Phrase for full text search |
665+
| _limit_ | Integer | How many items to get |
666+
| _offset_ | Integer | How many items to skip for a pagination |
661667
- Response: An Array of all submissions, sorted as newest first, as well as an array of the corresponding questions.
662668

663669
```
@@ -746,7 +752,8 @@ Get all Submissions to a Form
746752
"options": [],
747753
"extraSettings": {}
748754
}
749-
]
755+
],
756+
"filteredSubmissionsCount": 40
750757
}
751758
```
752759

lib/Controller/ApiController.php

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1134,7 +1134,10 @@ public function reorderOptions(int $formId, int $questionId, array $newOrder) {
11341134
* Get all the submissions of a given form
11351135
*
11361136
* @param int $formId of the form
1137-
* @param ?string $fileFormat the file format that should be used for the download. Defaults to `null`
1137+
* @param ?string $query (optional) A search query to filter submissions
1138+
* @param ?int $limit (optional) The maximum number of submissions to retrieve. Defaults to `null`
1139+
* @param int $offset (optional) The offset for pagination. Defaults to `0`
1140+
* @param ?string $fileFormat (optional) The file format that should be used for the download. Defaults to `null`
11381141
* Possible values:
11391142
* - `csv`: Comma-separated value
11401143
* - `ods`: OpenDocument Spreadsheet
@@ -1149,7 +1152,7 @@ public function reorderOptions(int $formId, int $questionId, array $newOrder) {
11491152
#[NoAdminRequired()]
11501153
#[BruteForceProtection(action: 'form')]
11511154
#[ApiRoute(verb: 'GET', url: '/api/v3/forms/{formId}/submissions')]
1152-
public function getSubmissions(int $formId, ?string $fileFormat = null): DataResponse|DataDownloadResponse {
1155+
public function getSubmissions(int $formId, ?string $query = null, ?int $limit = null, int $offset = 0, ?string $fileFormat = null): DataResponse|DataDownloadResponse {
11531156
$form = $this->getFormIfAllowed($formId, Constants::PERMISSION_RESULTS);
11541157

11551158
if ($fileFormat !== null) {
@@ -1161,9 +1164,12 @@ public function getSubmissions(int $formId, ?string $fileFormat = null): DataRes
11611164

11621165
// Load submissions and currently active questions
11631166
if (in_array(Constants::PERMISSION_RESULTS, $this->formsService->getPermissions($form))) {
1164-
$submissions = $this->submissionService->getSubmissions($formId);
1167+
$submissions = $this->submissionService->getSubmissions($formId, null, $query, $limit, $offset);
1168+
$filteredSubmissionsCount = $this->submissionMapper->countSubmissions($formId, null, $query);
11651169
} else {
1166-
$submissions = $this->submissionService->getSubmissions($formId, $this->currentUser->getUID());
1170+
$userId = $this->currentUser->getUID();
1171+
$submissions = $this->submissionService->getSubmissions($formId, $userId, $query, $limit, $offset);
1172+
$filteredSubmissionsCount = $this->submissionMapper->countSubmissions($formId, $userId, $query);
11671173
}
11681174
$questions = $this->formsService->getQuestions($formId);
11691175

@@ -1196,6 +1202,7 @@ public function getSubmissions(int $formId, ?string $fileFormat = null): DataRes
11961202
$response = [
11971203
'submissions' => $submissions,
11981204
'questions' => $questions,
1205+
'filteredSubmissionsCount' => $filteredSubmissionsCount,
11991206
];
12001207

12011208
return new DataResponse($response);

lib/Db/SubmissionMapper.php

Lines changed: 78 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -29,42 +29,49 @@ public function __construct(
2929
}
3030

3131
/**
32-
* @param int $formId
33-
* @throws DoesNotExistException if not found
34-
* @return Submission[]
35-
*/
36-
public function findByForm(int $formId): array {
37-
$qb = $this->db->getQueryBuilder();
38-
39-
$qb->select('*')
40-
->from($this->getTableName())
41-
->where(
42-
$qb->expr()->eq('form_id', $qb->createNamedParameter($formId, IQueryBuilder::PARAM_INT))
43-
)
44-
//Newest submissions first
45-
->orderBy('timestamp', 'DESC');
46-
47-
return $this->findEntities($qb);
48-
}
49-
50-
/**
51-
* @param int $formId
52-
* @param string $userId
32+
* Retrieves a list of submissions for a specific form.
33+
*
34+
* @param int $formId The ID of the form whose submissions are being retrieved.
35+
* @param string|null $userId An optional user ID to filter the submissions.
36+
* @param string|null $query An optional search query to filter the submissions.
37+
* @param int|null $limit The maximum number of submissions to retrieve, default: all submissions
38+
* @param int $offset The number of submissions to skip before starting to retrieve, default: 0
39+
*
40+
* @return Submission[] An array of Submission objects.
41+
* @throws DoesNotExistException If no submissions are found for the given form ID.
5342
*
54-
* @return Submission[]
55-
* @throws \OCP\AppFramework\Db\DoesNotExistException if not found
5643
*/
57-
public function findByFormAndUser(int $formId, string $userId): array {
44+
public function findByForm(int $formId, ?string $userId = null, ?string $query = null, ?int $limit = null, int $offset = 0): array {
5845
$qb = $this->db->getQueryBuilder();
5946

60-
$qb->select('*')
61-
->from($this->getTableName())
62-
->where(
63-
$qb->expr()->eq('form_id', $qb->createNamedParameter($formId, IQueryBuilder::PARAM_INT)),
64-
$qb->expr()->eq('user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR))
47+
$filters = [
48+
$qb->expr()->eq('submissions.form_id', $qb->createNamedParameter($formId, IQueryBuilder::PARAM_INT)),
49+
];
50+
if ($userId) {
51+
$filters[] = $qb->expr()->eq('submissions.user_id', $qb->createNamedParameter($userId));
52+
}
53+
54+
// Select all columns from the submissions table
55+
$qb->selectDistinct('submissions.*')
56+
->from($this->getTableName(), 'submissions')
57+
->where(...$filters)
58+
// Newest submissions first
59+
->orderBy('submissions.timestamp', 'DESC')
60+
->setFirstResult($offset)
61+
->setMaxResults($limit);
62+
63+
// If a query is provided, join the answers table and filter by the query text
64+
if (!is_null($query) && $query !== '') {
65+
$qb->join(
66+
'submissions',
67+
$this->answerMapper->getTableName(),
68+
'answers',
69+
$qb->expr()->eq('submissions.id', 'answers.submission_id')
6570
)
66-
//Newest submissions first
67-
->orderBy('timestamp', 'DESC');
71+
->andWhere(
72+
$qb->expr()->like('answers.text', $qb->createNamedParameter('%' . $query . '%'))
73+
);
74+
}
6875

6976
return $this->findEntities($qb);
7077
}
@@ -106,40 +113,62 @@ public function hasFormSubmissionsByUser(Form $form, string $userId): bool {
106113
}
107114

108115
/**
109-
* Count submissions by form
110-
* @param int $formId ID of the form to count submissions
111-
* @param null|string $userId (optional) ID of the current user, defaults to `null`
112-
* @throws \Exception
116+
* Counts the number of submissions associated with a specific form.
117+
*
118+
* @param int $formId The ID of the form for which submissions are to be counted.
119+
* @param ?string $searchString An optional search string to filter submissions by their answers.
120+
* @return int The total number of submissions for the specified form.
121+
* @throws \Exception If an error occurs during the count operation.
113122
*/
114-
public function countSubmissions(int $formId, ?string $userId = null): int {
115-
return $this->countSubmissionsWithFilters($formId, $userId, -1);
123+
public function countSubmissions(int $formId, ?string $userId = null, ?string $searchString = null): int {
124+
return $this->countSubmissionsWithFilters($formId, $userId, -1, $searchString);
116125
}
117126

118127
/**
119-
* Count submissions by form with optional filters
120-
* @param int $formId ID of the form to count submissions
121-
* @param string|null $userId optionally limit submissions to the one of that user
122-
* @param int $limit allows to limit the query selection. If -1, the restriction is ignored
123-
* @throws \Exception
128+
* Count submissions by form with optional filters.
129+
*
130+
* @param int $formId The ID of the form for which submissions are to be counted.
131+
* @param string|null $userId Optionally limit submissions to those made by the specified user.
132+
* @param int $limit The maximum number of submissions to count. If -1, no limit is applied.
133+
* @param string|null $searchString An optional search string to filter submissions by their answers.
134+
*
135+
* @return int The total number of submissions matching the specified filters.
136+
*
137+
* @throws \Exception If an error occurs during the count operation.
124138
*/
125-
protected function countSubmissionsWithFilters(int $formId, ?string $userId = null, int $limit = -1): int {
139+
protected function countSubmissionsWithFilters(int $formId, ?string $userId = null, int $limit = -1, ?string $searchString = null): int {
126140
$qb = $this->db->getQueryBuilder();
127141

128-
$query = $qb->select($qb->func()->count('*', 'num_submissions'))
129-
->from($this->getTableName())
130-
->where($qb->expr()->eq('form_id', $qb->createNamedParameter($formId, IQueryBuilder::PARAM_INT)));
142+
$query = $qb->select('submissions.id')
143+
->from($this->getTableName(), 'submissions')
144+
->where($qb->expr()->eq('submissions.form_id', $qb->createNamedParameter($formId, IQueryBuilder::PARAM_INT)))
145+
->groupBy('submissions.id');
146+
131147
if (!is_null($userId)) {
132-
$query->andWhere($qb->expr()->eq('user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)));
148+
$query->andWhere($qb->expr()->eq('submissions.user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)));
133149
}
150+
151+
if (!is_null($searchString) && $searchString !== '') {
152+
$query->join(
153+
'submissions',
154+
$this->answerMapper->getTableName(),
155+
'answers',
156+
$qb->expr()->eq('submissions.id', 'answers.submission_id')
157+
)
158+
->andWhere(
159+
$qb->expr()->like('answers.text', $qb->createNamedParameter('%' . $searchString . '%'))
160+
);
161+
}
162+
134163
if ($limit !== -1) {
135164
$query->setMaxResults($limit);
136165
}
137166

138167
$result = $query->executeQuery();
139-
$row = $result->fetch();
168+
$rows = $result->fetchAll();
140169
$result->closeCursor();
141170

142-
return (int)($row['num_submissions'] ?? 0);
171+
return count($rows);
143172
}
144173

145174
/**

lib/ResponseDefinitions.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@
7777
*
7878
* @psalm-type FormsSubmissions = array{
7979
* submissions: list<FormsSubmission>,
80-
* questions: list<FormsQuestion>
80+
* questions: list<FormsQuestion>,
81+
* filteredSubmissionsCount: int
8182
* }
8283
*
8384
* @psalm-type FormsAccess = array{

lib/Service/SubmissionService.php

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ private function getAnswers(int $submissionId): array {
9292
*
9393
* @param int $formId the form id
9494
* @param string|null $userId optional user id to filter submissions
95+
* @param string|null $query optional search query to filter submissions
96+
* @param int|null $limit the maximum number of submissions to return
97+
* @param int $offset the number of submissions to skip
9598
* @return list<array{
9699
* id: int,
97100
* formId: int,
@@ -100,14 +103,10 @@ private function getAnswers(int $submissionId): array {
100103
* answers: list<FormsAnswer>,
101104
* }>
102105
*/
103-
public function getSubmissions(int $formId, ?string $userId = null): array {
106+
public function getSubmissions(int $formId, ?string $userId = null, ?string $query = null, ?int $limit = null, int $offset = 0): array {
104107
$submissionList = [];
105108
try {
106-
if (is_null($userId)) {
107-
$submissionEntities = $this->submissionMapper->findByForm($formId);
108-
} else {
109-
$submissionEntities = $this->submissionMapper->findByFormAndUser($formId, $userId);
110-
}
109+
$submissionEntities = $this->submissionMapper->findByForm($formId, $userId, $query, $limit, $offset);
111110

112111
foreach ($submissionEntities as $submissionEntity) {
113112
$submission = $submissionEntity->read();

openapi.json

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -592,7 +592,8 @@
592592
"type": "object",
593593
"required": [
594594
"submissions",
595-
"questions"
595+
"questions",
596+
"filteredSubmissionsCount"
596597
],
597598
"properties": {
598599
"submissions": {
@@ -606,6 +607,10 @@
606607
"items": {
607608
"$ref": "#/components/schemas/Question"
608609
}
610+
},
611+
"filteredSubmissionsCount": {
612+
"type": "integer",
613+
"format": "int64"
609614
}
610615
}
611616
},
@@ -3019,10 +3024,39 @@
30193024
"format": "int64"
30203025
}
30213026
},
3027+
{
3028+
"name": "query",
3029+
"in": "query",
3030+
"description": "(optional) A search query to filter submissions",
3031+
"schema": {
3032+
"type": "string",
3033+
"nullable": true
3034+
}
3035+
},
3036+
{
3037+
"name": "limit",
3038+
"in": "query",
3039+
"description": "(optional) The maximum number of submissions to retrieve. Defaults to `null`",
3040+
"schema": {
3041+
"type": "integer",
3042+
"format": "int64",
3043+
"nullable": true
3044+
}
3045+
},
3046+
{
3047+
"name": "offset",
3048+
"in": "query",
3049+
"description": "(optional) The offset for pagination. Defaults to `0`",
3050+
"schema": {
3051+
"type": "integer",
3052+
"format": "int64",
3053+
"default": 0
3054+
}
3055+
},
30223056
{
30233057
"name": "fileFormat",
30243058
"in": "query",
3025-
"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",
3059+
"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",
30263060
"schema": {
30273061
"type": "string",
30283062
"nullable": true

0 commit comments

Comments
 (0)