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
5 changes: 5 additions & 0 deletions docs/DataStructure.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ Currently supported Question-Types are:
| _`datetime`_ | _deprecated: No longer available for new questions. Showing a dropdown calendar to select a date **and** a time._ |
| `time` | Showing a dropdown menu to select a time. |
| `file` | One or multiple files. It is possible to specify which mime types are allowed |
| `linearscale` | A linear or Likert scale question where you choose an option that best fits your opinion |

## Extra Settings

Expand All @@ -230,3 +231,7 @@ Optional extra settings for some [Question Types](#question-types)
| `dateMax` | `date` | Integer | - | Maximum allowed date to be chosen (as Unix timestamp) |
| `dateMin` | `date` | Integer | - | Minimum allowed date to be chosen (as Unix timestamp) |
| `dateRange` | `date` | Boolean | `true/false` | The date picker should query a date range |
| `optionsLowest` | `linearscale` | Integer | `0, 1` | Set the lowest value of the scale, default: `1` |
| `optionsHighest` | `linearscale` | Integer | `2, 3, 4, 5, 6, 7, 8, 9, 10` | Set the highest value of the scale, default: `5` |
| `optionsLabelLowest` | `linearscale` | string | - | Set the label of the lowest value, default: `'Strongly disagree'` |
| `optionsLabelHighest` | `linearscale` | string | - | Set the label of the highest value, default: `'Strongly agree'` |
12 changes: 11 additions & 1 deletion lib/Constants.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ class Constants {
public const ANSWER_TYPE_DATETIME = 'datetime';
public const ANSWER_TYPE_TIME = 'time';
public const ANSWER_TYPE_FILE = 'file';
public const ANSWER_TYPE_LINEARSCALE = 'linearscale';

// All AnswerTypes
public const ANSWER_TYPES = [
Expand All @@ -87,13 +88,15 @@ class Constants {
self::ANSWER_TYPE_DATETIME,
self::ANSWER_TYPE_TIME,
self::ANSWER_TYPE_FILE,
self::ANSWER_TYPE_LINEARSCALE,
];

// AnswerTypes, that need/have predefined Options
public const ANSWER_TYPES_PREDEFINED = [
self::ANSWER_TYPE_MULTIPLE,
self::ANSWER_TYPE_MULTIPLEUNIQUE,
self::ANSWER_TYPE_DROPDOWN
self::ANSWER_TYPE_DROPDOWN,
self::ANSWER_TYPE_LINEARSCALE,
];

// AnswerTypes for date/time questions
Expand Down Expand Up @@ -161,6 +164,13 @@ class Constants {
'x-office/spreadsheet',
];

public const EXTRA_SETTINGS_LINEARSCALE = [
'optionsLowest' => ['integer', 'NULL'],
'optionsHighest' => ['integer', 'NULL'],
'optionsLabelLowest' => ['string', 'NULL'],
'optionsLabelHighest' => ['string', 'NULL'],
];

public const FILENAME_INVALID_CHARS = [
"\n",
'/',
Expand Down
2 changes: 1 addition & 1 deletion lib/Controller/ApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -1538,7 +1538,7 @@ private function storeAnswersForQuestion(Form $form, $submissionId, array $quest
$answerText = '';
$uploadedFile = null;
// Are we using answer ids as values
if (in_array($question['type'], Constants::ANSWER_TYPES_PREDEFINED)) {
if (in_array($question['type'], Constants::ANSWER_TYPES_PREDEFINED) && $question['type'] !== Constants::ANSWER_TYPE_LINEARSCALE) {
// Search corresponding option, skip processing if not found
$optionIndex = array_search($answer, array_column($question['options'], 'id'));
if ($optionIndex !== false) {
Expand Down
4 changes: 4 additions & 0 deletions lib/ResponseDefinitions.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,12 @@
* dateRange?: bool,
* maxAllowedFilesCount?: int,
* maxFileSize?: int,
* optionsHighest?: 2|3|4|5|6|7|8|9|10,
* optionsLabelHighest?: string,
* optionsLabelLowest?: string,
* optionsLimitMax?: int,
* optionsLimitMin?: int,
* optionsLowest?: 0|1,
* shuffleOptions?: bool,
* validationRegex?: string,
* validationType?: string
Expand Down
11 changes: 11 additions & 0 deletions lib/Service/FormsService.php
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,9 @@
case Constants::ANSWER_TYPE_DATE:
$allowed = Constants::EXTRA_SETTINGS_DATE;
break;
case Constants::ANSWER_TYPE_LINEARSCALE:

Check warning on line 635 in lib/Service/FormsService.php

View check run for this annotation

Codecov / codecov/patch

lib/Service/FormsService.php#L635

Added line #L635 was not covered by tests
$allowed = Constants::EXTRA_SETTINGS_LINEARSCALE;
break;
default:
$allowed = [];
}
Expand Down Expand Up @@ -708,6 +711,14 @@
return false;
}
}

// Special handling of linear scale validation
} elseif ($questionType === Constants::ANSWER_TYPE_LINEARSCALE) {
// Ensure limits are sane
if (isset($extraSettings['optionsLowest']) && ($extraSettings['optionsLowest'] < 0 || $extraSettings['optionsLowest'] > 1) ||
isset($extraSettings['optionsHighest']) && ($extraSettings['optionsHighest'] < 2 || $extraSettings['optionsHighest'] > 10)) {
return false;
}
}
return true;
}
Expand Down
13 changes: 11 additions & 2 deletions lib/Service/SubmissionService.php
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ public function validateSubmission(array $questions, array $answers, string $for
continue;
}

// Check number of answers
// Check number of answers for multiple answers
$answersCount = count($answers[$questionId]);
if ($question['type'] === Constants::ANSWER_TYPE_MULTIPLE) {
$minOptions = $question['extraSettings']['optionsLimitMin'] ?? -1;
Expand Down Expand Up @@ -399,8 +399,16 @@ public function validateSubmission(array $questions, array $answers, string $for
// Check if all answers are within the possible options
if (in_array($question['type'], Constants::ANSWER_TYPES_PREDEFINED) && empty($question['extraSettings']['allowOtherAnswer'])) {
foreach ($answers[$questionId] as $answer) {
// Handle linear scale questions
if ($question['type'] === Constants::ANSWER_TYPE_LINEARSCALE) {
$optionsLowest = $question['extraSettings']['optionsLowest'] ?? 1;
$optionsHighest = $question['extraSettings']['optionsHighest'] ?? 5;
if (!ctype_digit($answer) || intval($answer) < $optionsLowest || intval($answer) > $optionsHighest) {
throw new \InvalidArgumentException(sprintf('The answer for question "%s" must be an integer between %d and %d.', $question['text'], $optionsLowest, $optionsHighest));
}
}
// Search corresponding option, return false if non-existent
if (!in_array($answer, array_column($question['options'], 'id'))) {
elseif (!in_array($answer, array_column($question['options'], 'id'))) {
throw new \InvalidArgumentException(sprintf('Answer "%s" for question "%s" is not a valid option.', $answer, $question['text']));
}
}
Expand All @@ -411,6 +419,7 @@ public function validateSubmission(array $questions, array $answers, string $for
throw new \InvalidArgumentException(sprintf('Invalid input for question "%s".', $question['text']));
}

// Handle file questions
if ($question['type'] === Constants::ANSWER_TYPE_FILE) {
$maxAllowedFilesCount = $question['extraSettings']['maxAllowedFilesCount'] ?? 0;
if ($maxAllowedFilesCount > 0 && count($answers[$questionId]) > $maxAllowedFilesCount) {
Expand Down
29 changes: 29 additions & 0 deletions openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,27 @@
"type": "integer",
"format": "int64"
},
"optionsHighest": {
"type": "integer",
"format": "int64",
"enum": [
2,
3,
4,
5,
6,
7,
8,
9,
10
]
},
"optionsLabelHighest": {
"type": "string"
},
"optionsLabelLowest": {
"type": "string"
},
"optionsLimitMax": {
"type": "integer",
"format": "int64"
Expand All @@ -445,6 +466,14 @@
"type": "integer",
"format": "int64"
},
"optionsLowest": {
"type": "integer",
"format": "int64",
"enum": [
0,
1
]
},
"shuffleOptions": {
"type": "boolean"
},
Expand Down
45 changes: 45 additions & 0 deletions src/components/Icons/IconLinearScale.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<!--
- SPDX-FileCopyrightText: 2025 Christian Hartmann <chris-hartmann@gmx.de>
- SPDX-License-Identifier: AGPL-3.0-or-later
-->

<template>
<span
:aria-hidden="!title"
:aria-label="title"
class="material-design-icon linear-scale-icon"
role="img"
v-bind="$attrs"
@click="$emit('click', $event)">
<svg
:fill="fillColor"
class="material-design-icon__svg"
:height="size"
:width="size"
viewBox="0 -960 960 960">
<path
d="M672-288q-71 0-123.38-44.36Q496.24-376.73 482.9-444H281q-11 26-35 43t-54 17q-40.32 0-68.16-27.77Q96-439.55 96-479.77 96-520 123.84-548q27.84-28 68.16-28 30 0 54 17t35.46 43H484q13.21-67.28 65.1-111.64Q601-672 672-672q79.68 0 135.84 56.23 56.16 56.22 56.16 136Q864-400 807.84-344 751.68-288 672-288Zm0-72q50 0 85-35t35-85q0-50-35-85t-85-35q-50 0-85 35t-35 85q0 50 35 85t85 35Z" />
<title v-if="title">{{ title }}</title>
</svg>
</span>
</template>

<script>
export default {
name: 'IconLinearScale',
props: {
title: {
type: String,
default: '',
},
fillColor: {
type: String,
default: 'currentColor',
},
size: {
type: Number,
default: 20,
},
},
}
</script>
Loading
Loading