diff --git a/docs/DataStructure.md b/docs/DataStructure.md
index 6c9e6221e..b2e6169cc 100644
--- a/docs/DataStructure.md
+++ b/docs/DataStructure.md
@@ -231,6 +231,9 @@ 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 |
+| `timeMax` | `time` | string | - | Maximum allowed time to be chosen (as `HH:mm` string) |
+| `timeMin` | `time` | string | - | Minimum allowed time to be chosen (as `HH:mm` string) |
+| `timeRange` | `time` | Boolean | `true/false` | The time picker should query a time 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'` |
diff --git a/img/clock_loader_20.svg b/img/clock_loader_20.svg
new file mode 100644
index 000000000..f1e60ca09
--- /dev/null
+++ b/img/clock_loader_20.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/img/clock_loader_80.svg b/img/clock_loader_80.svg
new file mode 100644
index 000000000..d718ed581
--- /dev/null
+++ b/img/clock_loader_80.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/lib/Constants.php b/lib/Constants.php
index 2ee8cdfc9..27b945f1d 100644
--- a/lib/Constants.php
+++ b/lib/Constants.php
@@ -156,6 +156,12 @@ class Constants {
'dateRange' => ['boolean', 'NULL'],
];
+ public const EXTRA_SETTINGS_TIME = [
+ 'timeMax' => ['string', 'NULL'],
+ 'timeMin' => ['string', 'NULL'],
+ 'timeRange' => ['boolean', 'NULL'],
+ ];
+
// should be in sync with FileTypes.js
public const EXTRA_SETTINGS_ALLOWED_FILE_TYPES = [
'image',
diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php
index b3faf1426..2b0fcffdf 100644
--- a/lib/ResponseDefinitions.php
+++ b/lib/ResponseDefinitions.php
@@ -35,6 +35,9 @@
* optionsLimitMin?: int,
* optionsLowest?: 0|1,
* shuffleOptions?: bool,
+ * timeMax?: int,
+ * timeMin?: int,
+ * timeRange?: bool,
* validationRegex?: string,
* validationType?: string
* }
diff --git a/lib/Service/FormsService.php b/lib/Service/FormsService.php
index 4aa353a2c..104aba27f 100644
--- a/lib/Service/FormsService.php
+++ b/lib/Service/FormsService.php
@@ -632,6 +632,9 @@ public function areExtraSettingsValid(array $extraSettings, string $questionType
case Constants::ANSWER_TYPE_DATE:
$allowed = Constants::EXTRA_SETTINGS_DATE;
break;
+ case Constants::ANSWER_TYPE_TIME:
+ $allowed = Constants::EXTRA_SETTINGS_TIME;
+ break;
case Constants::ANSWER_TYPE_LINEARSCALE:
$allowed = Constants::EXTRA_SETTINGS_LINEARSCALE;
break;
@@ -659,6 +662,32 @@ public function areExtraSettingsValid(array $extraSettings, string $questionType
&& $extraSettings['dateMin'] > $extraSettings['dateMax']) {
return false;
}
+ } elseif ($questionType === Constants::ANSWER_TYPE_TIME) {
+ $format = Constants::ANSWER_PHPDATETIME_FORMAT['time'];
+
+ // Validate timeMin format
+ if (isset($extraSettings['timeMin'])) {
+ $timeMinString = $extraSettings['timeMin'];
+ $timeMinDate = \DateTime::createFromFormat($format, $timeMinString);
+ if (!$timeMinDate || $timeMinDate->format($format) !== $timeMinString) {
+ return false;
+ }
+ }
+
+ // Validate timeMax format
+ if (isset($extraSettings['timeMax'])) {
+ $timeMaxString = $extraSettings['timeMax'];
+ $timeMaxDate = \DateTime::createFromFormat($format, $timeMaxString);
+ if (!$timeMaxDate || $timeMaxDate->format($format) !== $timeMaxString) {
+ return false;
+ }
+ }
+
+ // Ensure timeMin and timeMax don't overlap
+ if (isset($extraSettings['timeMin']) && isset($extraSettings['timeMax'])
+ && $timeMinDate > $timeMaxDate) {
+ return false;
+ }
} elseif ($questionType === Constants::ANSWER_TYPE_MULTIPLE) {
// Ensure limits are sane
if (isset($extraSettings['optionsLimitMax']) && isset($extraSettings['optionsLimitMin'])
diff --git a/lib/Service/SubmissionService.php b/lib/Service/SubmissionService.php
index 61235f3ba..b6cf09be7 100644
--- a/lib/Service/SubmissionService.php
+++ b/lib/Service/SubmissionService.php
@@ -382,7 +382,13 @@ public function validateSubmission(array $questions, array $answers, string $for
} elseif ($answersCount != 2 && $question['type'] === Constants::ANSWER_TYPE_DATE && isset($question['extraSettings']['dateRange'])) {
// Check if date range questions have exactly two answers
throw new \InvalidArgumentException(sprintf('Question "%s" can only have two answers.', $question['text']));
- } elseif ($answersCount > 1 && $question['type'] !== Constants::ANSWER_TYPE_FILE && !($question['type'] === Constants::ANSWER_TYPE_DATE && isset($question['extraSettings']['dateRange']))) {
+ } elseif ($answersCount != 2 && $question['type'] === Constants::ANSWER_TYPE_TIME && isset($question['extraSettings']['timeRange'])) {
+ // Check if date range questions have exactly two answers
+ throw new \InvalidArgumentException(sprintf('Question "%s" can only have two answers.', $question['text']));
+ } elseif ($answersCount > 1
+ && $question['type'] !== Constants::ANSWER_TYPE_FILE
+ && !($question['type'] === Constants::ANSWER_TYPE_DATE && isset($question['extraSettings']['dateRange'])
+ || $question['type'] === Constants::ANSWER_TYPE_TIME && isset($question['extraSettings']['timeRange']))) {
// Check if non-multiple questions have not more than one answer
throw new \InvalidArgumentException(sprintf('Question "%s" can only have one answer.', $question['text']));
}
@@ -466,15 +472,17 @@ private function validateDateTime(array $answers, string $format, ?string $text
}
if ($previousDate !== null && $d < $previousDate) {
- throw new \InvalidArgumentException(sprintf('Dates for question "%s" must be in ascending order.', $text));
+ throw new \InvalidArgumentException(sprintf('Date/time values for question "%s" must be in ascending order.', $text));
}
$previousDate = $d;
if ($extraSettings) {
if ((isset($extraSettings['dateMin']) && $d < (new DateTime())->setTimestamp($extraSettings['dateMin'])) ||
- (isset($extraSettings['dateMax']) && $d > (new DateTime())->setTimestamp($extraSettings['dateMax']))
+ (isset($extraSettings['dateMax']) && $d > (new DateTime())->setTimestamp($extraSettings['dateMax'])) ||
+ (isset($extraSettings['timeMin']) && $d < DateTime::createFromFormat($format, $extraSettings['timeMin'])) ||
+ (isset($extraSettings['timeMax']) && $d > DateTime::createFromFormat($format, $extraSettings['timeMax']))
) {
- throw new \InvalidArgumentException(sprintf('Date is not in the allowed range for question "%s".', $text));
+ throw new \InvalidArgumentException(sprintf('Date/time is not in the allowed range for question "%s".', $text));
}
}
}
diff --git a/openapi.json b/openapi.json
index f82bfc9e5..5e7254bf3 100644
--- a/openapi.json
+++ b/openapi.json
@@ -477,6 +477,17 @@
"shuffleOptions": {
"type": "boolean"
},
+ "timeMax": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "timeMin": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "timeRange": {
+ "type": "boolean"
+ },
"validationRegex": {
"type": "string"
},
diff --git a/src/components/Questions/QuestionDate.vue b/src/components/Questions/QuestionDate.vue
index 44e0cf118..0353514f0 100644
--- a/src/components/Questions/QuestionDate.vue
+++ b/src/components/Questions/QuestionDate.vue
@@ -46,6 +46,41 @@
+
+