Skip to content
Draft
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
6 changes: 6 additions & 0 deletions REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ precedence = "aggregate"
SPDX-FileCopyrightText = "2019 Nextcloud GmbH and Nextcloud contributors"
SPDX-License-Identifier = "AGPL-3.0-or-later"

[[annotations]]
path = ["tests/fixtures/jump-paths.json"]
precedence = "aggregate"
SPDX-FileCopyrightText = "2026 Nextcloud GmbH and Nextcloud contributors"
SPDX-License-Identifier = "AGPL-3.0-or-later"

[[annotations]]
path = ["img/*"]
precedence = "aggregate"
Expand Down
26 changes: 21 additions & 5 deletions docs/API_v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ This file contains the API-Documentation. For more information on the returned D
- The OCS-Endpoint _always returns_ an object called `ocs`. This contains an object `meta` holding some meta-data, as well as an object `data` holding the actual data. In this document, the response-blocks only show the `data`, if not explicitely stated different.

- OpenAPI spec: a machine-readable OpenAPI description is available at `/openapi.json` in the repository root. Prefer it as source-of-truth for exact request/response shapes.
- When the server changes data beyond what a request asked for, it reports this in the response header `X-Forms-Adjusted`, see [Adjustments by the server](#adjustments-by-the-server).

```
"ocs": {
Expand All @@ -30,6 +31,18 @@ This file contains the API-Documentation. For more information on the returned D
}
```

### Adjustments by the server

Some requests succeed with their usual status, and their payload keeps its shape, but the server changes data beyond what the request asked for. The response header `X-Forms-Adjusted` then reports this as a [structured field dictionary (RFC 8941)](https://www.rfc-editor.org/rfc/rfc8941#name-dictionaries); the header is absent when nothing was adjusted. `Access-Control-Expose-Headers` makes it readable for cross-origin clients. Clients should ignore keys they do not know, as more keys may follow.

| Key | Value | Set by | Meaning |
|-----|-------|--------|---------|
| `removed-jumps` | Inner list of question ids, e.g. `(3 7)` | Updating, reordering and copying questions; deleting options | Jumps to the question itself or to an earlier question, or jumps of the deleted option, were removed from the `extraSettings` of these questions, see [Jumps](DataStructure.md#jumps) |

```
X-Forms-Adjusted: removed-jumps=(3 7)
```

## API changes

### Deprecation info
Expand All @@ -49,6 +62,7 @@ This file contains the API-Documentation. For more information on the returned D

### Other API changes

- Questions can jump to a later question or to the end of the form with the extra settings `goTo` (per answer, on dropdown and radio questions) or `next` (after the question, on the other types with jumps), never both (see [Jumps](DataStructure.md#jumps)). Jumps to questions of other forms or to options that are not options of the question are rejected, and deleting an option removes its jumps. Submissions and submission updates ignore answers to the questions the jumps skip, and an answer "Other" needs a text. Updating, reordering and copying questions removes jumps to the question itself or to an earlier question; the response header `X-Forms-Adjusted` then lists the questions concerned under `removed-jumps` (see [Adjustments by the server](#adjustments-by-the-server)). Copying or importing a form drops such jumps as well, without the header, as these create a new form.
- In API version 3.0 the following endpoints were introduced/changed:
- `GET /api/v3/forms/{formId}/questions` to get all questions of a form
- `GET /api/v3/forms/{formId}/questions/{questionId}` to get a single question
Expand Down Expand Up @@ -260,7 +274,7 @@ Creates a clone of a form (without submissions).
| Parameter | Type | Description |
|-----------|---------|-------------|
| _formId_ | Integer | ID of the form to clone |
- Response: Returns the full object of the new form. See [Request full data of a Form](#request-full-data-of-a-form)
- Response: Returns the full object of the new form. See [Request full data of a Form](#request-full-data-of-a-form). The jumps of the copied questions point at the copied questions and options; jumps whose target or option was not copied, jumps that would go to the question itself or to an earlier question, and `goTo` or `next` on a question type without it, are dropped, without the header `X-Forms-Adjusted` (see [Jumps](DataStructure.md#jumps)).

```
See section 'Request full data of a form'.
Expand Down Expand Up @@ -452,7 +466,7 @@ Update a single or multiple properties of a question-object.
|-----------|---------|-------------|
| _keyValuePairs_ | Array | Array of key-value pairs to update |
- Restrictions: It is **not allowed** to update one of the following key-value pairs: _id, formId, order, type_.
- Response: **Status-Code OK**, as well as the id of the updated question.
- Response: **Status-Code OK**, as well as the id of the updated question. Jumps in `extraSettings` that go to the question itself or to an earlier question are removed before saving (see [Jumps](DataStructure.md#jumps)), and the response header `X-Forms-Adjusted` then holds `removed-jumps=(<question id>)` (see [Adjustments by the server](#adjustments-by-the-server)). Jumps with an invalid structure, `goTo` or `next` on a question type without it, and ids of questions of other forms or of options that are not options of the question are rejected with **Status-Code Bad Request** "Invalid extraSettings, will not update."

```
"data": 1
Expand All @@ -473,7 +487,7 @@ Reorders all Questions of a single form
|-----------|---------|-------------|
| _newOrder_ | Array | Array of **all** Question-IDs, ordered in the desired order |
- Restrictions: The Array **must** contain all Question-IDs corresponding to the specified form and **must not** contain any duplicates.
- Response: Array of questionIDs and their corresponding order.
- Response: Array of questionIDs and their corresponding order. Jumps that would go to the question itself or to an earlier question are removed (see [Jumps](DataStructure.md#jumps)): the questions concerned also carry their new `extraSettings`, and the response header `X-Forms-Adjusted` then lists their ids, e.g. `X-Forms-Adjusted: removed-jumps=(3 7)` (see [Adjustments by the server](#adjustments-by-the-server)).

```
"data": {
Expand Down Expand Up @@ -519,7 +533,7 @@ Creates a clone of a question with all its options.
| Parameter | Type | Optional | Description |
|-----------|---------|----------|-------------|
| _position_ | Integer | yes | _(optional)_ 1-based position to insert the cloned question. When omitted the clone is appended at the end. |
- Response: Returns cloned question object with the new ID set.
- Response: Returns cloned question object with the new ID set. Jumps that would go to the clone itself or to an earlier question, for example when it is appended at the end, are removed from its `extraSettings` (see [Jumps](DataStructure.md#jumps)), and the response header `X-Forms-Adjusted` then holds `removed-jumps=(<clone id>)` (see [Adjustments by the server](#adjustments-by-the-server)).

```
See section 'Create a new question'.
Expand Down Expand Up @@ -590,7 +604,7 @@ Update a single or all properties of an option-object
| _formId_ | Integer | ID of the form containing the question and option |
| _questionId_ | Integer | ID of the question, the new option will belong to |
| _optionId_ | Integer | ID of the option to delete |
- Response: **Status-Code OK**, as well as the id of the deleted option.
- Response: **Status-Code OK**, as well as the id of the deleted option. The jumps of the question for this option are removed (see [Jumps](DataStructure.md#jumps)), and the response header `X-Forms-Adjusted` then holds `removed-jumps=(<question id>)` (see [Adjustments by the server](#adjustments-by-the-server)).

```
"data": 7
Expand Down Expand Up @@ -951,6 +965,8 @@ Store Submission to Database
- 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 objects with `uploadedFileId` and `uploadToken` (both from the [Upload a file] endpoint for the same `questionId`).
- Answers to questions that the [jumps](DataStructure.md#jumps) of the answers skip are ignored, and a skipped required question needs no answer.
- An answer "Other" is `system-other-answer:` followed by its text, and the text must not be empty.

```
{
Expand Down
19 changes: 19 additions & 0 deletions docs/DataStructure.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,25 @@ Optional extra settings for some [Question Types](#question-types)
| `columns` | `grid` | Array | - | Array of column identifiers / labels for grid questions |
| `rows` | `grid` | Array | - | Array of row identifiers / labels for grid questions |
| `questionType` | `grid` | String | `checkbox`, `number`, `radio` | Type of cell for grid questions (checkbox, numeric input, or radio) |
| `goTo` | `dropdown, multiple_unique` | Array | see [Jumps](#jumps) | Where to go next for an answer |
| `next` | `color, file, grid, long, ranking, short` | Integer / string | question id, `'submit'` | Where to go after the question, see [Jumps](#jumps) |

### Jumps

An answer can lead to a later question or to the end of the form, as in Microsoft and Google Forms. The form stays a flat list of questions.

- `goTo` is a list of jumps per answer: `{"optionId": 12, "target": 34}` for an option, `{"other": true, "target": "submit"}` for the answer "Other". There is at most one jump per answer.
- `next` is one target after the whole question.
- A question has either jumps per answer or one jump after the question, depending on its type: `goTo` only on `dropdown` and `multiple_unique`, `next` only on the other types listed above. Sending `next` on a question type with `goTo`, or `goTo` on a question type with `next`, is rejected with 400 "Invalid extraSettings". An answer without a `goTo` entry leads to the following question.
- A target is the id of a later question of the same form or `"submit"` for the end of the form, and `optionId` is the id of an option of the question. Ids of questions of other forms and option ids that are not options of the question are rejected as invalid `extraSettings`; ids of deleted questions of the form are accepted. Without a jump the respondent goes to the following question.
- Jumps never go back: the server removes jumps to the question itself or to an earlier question when a question is updated, reordered or copied, and when a form is copied or imported; copying or importing a form also drops `goTo` or `next` on question types without it. Updating, reordering and copying a question still succeed with their usual status, and the payload keeps its shape: a reorder adds the new `extraSettings` of the questions concerned, and a copied question is returned with its reduced `extraSettings`. The response header `X-Forms-Adjusted` lists the questions that lost jumps, e.g. `removed-jumps=(3 7)`, see [Adjustments by the server](API_v3.md#adjustments-by-the-server).
- Deleting an option removes the jumps of the question for that option; the response header `X-Forms-Adjusted` then holds `removed-jumps` with the id of the question.
- Saving a question rejects jumps with an invalid structure with 400, as any invalid `extraSettings`. Copying or importing a form keeps the jumps whose target and option it can map, and drops the others and jumps per answer with an invalid structure. The path walks ignore malformed entries.
- Question types in neither list, such as `datetime`, accept neither `goTo` nor `next` (400).
- A jump of the answer "Other" only matches while `allowOtherAnswer` is on.
- `next` also applies when the question is left unanswered.

The respondent's path starts at the first question and follows these targets. The first value of an answer decides, and answers to questions the submit view does not show, such as questions without a title, do not count. A target that is not a later question of the form, for example after the target was deleted, counts as the following question, and a jump of an option that no longer exists never matches. Questions off the path are skipped: answers to them are ignored, and their required flag does not apply.

### Option Types

Expand Down
16 changes: 16 additions & 0 deletions lib/Constants.php
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,22 @@ class Constants {
'shuffleOptions' => ['boolean'],
];

public const JUMP_TARGET_SUBMIT = 'submit';

public const QUESTION_TYPES_WITH_GOTO = [
self::ANSWER_TYPE_DROPDOWN,
self::ANSWER_TYPE_MULTIPLEUNIQUE,
];

public const QUESTION_TYPES_WITH_NEXT = [
self::ANSWER_TYPE_COLOR,
self::ANSWER_TYPE_FILE,
self::ANSWER_TYPE_GRID,
self::ANSWER_TYPE_LONG,
self::ANSWER_TYPE_RANKING,
self::ANSWER_TYPE_SHORT,
];

public const EXTRA_SETTINGS_GRID_QUESTION_TYPE = [
self::ANSWER_GRID_TYPE_CHECKBOX,
self::ANSWER_GRID_TYPE_NUMBER,
Expand Down
Loading
Loading