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 @@ +
@@ -64,6 +99,8 @@