Skip to content
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
9 changes: 8 additions & 1 deletion docs/API_v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

```
Expand Down Expand Up @@ -746,7 +752,8 @@ Get all Submissions to a Form
"options": [],
"extraSettings": {}
}
]
],
"filteredSubmissionsCount": 40
}
```

Expand Down
15 changes: 11 additions & 4 deletions lib/Controller/ApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -1134,7 +1134,10 @@
* 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
Expand All @@ -1149,7 +1152,7 @@
#[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) {
Expand All @@ -1161,9 +1164,12 @@

// 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);

Check warning on line 1168 in lib/Controller/ApiController.php

View check run for this annotation

Codecov / codecov/patch

lib/Controller/ApiController.php#L1167-L1168

Added lines #L1167 - L1168 were not covered by tests
} 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);

Expand Down Expand Up @@ -1196,6 +1202,7 @@
$response = [
'submissions' => $submissions,
'questions' => $questions,
'filteredSubmissionsCount' => $filteredSubmissionsCount,
];

return new DataResponse($response);
Expand Down
127 changes: 78 additions & 49 deletions lib/Db/SubmissionMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
}

/**
Expand Down
3 changes: 2 additions & 1 deletion lib/ResponseDefinitions.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@
*
* @psalm-type FormsSubmissions = array{
* submissions: list<FormsSubmission>,
* questions: list<FormsQuestion>
* questions: list<FormsQuestion>,
* filteredSubmissionsCount: int
* }
*
* @psalm-type FormsAccess = array{
Expand Down
11 changes: 5 additions & 6 deletions lib/Service/SubmissionService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<array{
* id: int,
* formId: int,
Expand All @@ -100,14 +103,10 @@ private function getAnswers(int $submissionId): array {
* answers: list<FormsAnswer>,
* }>
*/
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();
Expand Down
38 changes: 36 additions & 2 deletions openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,8 @@
"type": "object",
"required": [
"submissions",
"questions"
"questions",
"filteredSubmissionsCount"
],
"properties": {
"submissions": {
Expand All @@ -606,6 +607,10 @@
"items": {
"$ref": "#/components/schemas/Question"
}
},
"filteredSubmissionsCount": {
"type": "integer",
"format": "int64"
}
}
},
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading