Skip to content

Commit a1a2295

Browse files
committed
feat(date): add dateMin and dateMax fields to question settings
Signed-off-by: Christian Hartmann <chris-hartmann@gmx.de>
1 parent 87ed14d commit a1a2295

9 files changed

Lines changed: 179 additions & 11 deletions

File tree

docs/DataStructure.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,3 +226,5 @@ Optional extra settings for some [Question Types](#question-types)
226226
| `allowedFileExtensions` | `file` | Array of strings | `'jpg', 'png'` | Allowed file extensions for file upload |
227227
| `maxAllowedFilesCount` | `file` | Integer | - | Maximum number of files that can be uploaded, 0 means no limit |
228228
| `maxFileSize` | `file` | Integer | - | Maximum file size in bytes, 0 means no limit |
229+
| `dateMax` | `date` | Integer | - | Maximum allowed date to be chosen (as Unix timestamp) |
230+
| `dateMin` | `date` | Integer | - | Minimum allowed date to be chosen (as Unix timestamp) |

lib/Constants.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,11 @@ class Constants {
147147
'maxFileSize' => ['integer'],
148148
];
149149

150+
public const EXTRA_SETTINGS_DATE = [
151+
'dateMax' => ['integer', 'NULL'],
152+
'dateMin' => ['integer', 'NULL'],
153+
];
154+
150155
// should be in sync with FileTypes.js
151156
public const EXTRA_SETTINGS_ALLOWED_FILE_TYPES = [
152157
'image',

lib/ResponseDefinitions.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
* allowOtherAnswer?: bool,
2424
* allowedFileExtensions?: list<string>,
2525
* allowedFileTypes?: list<string>,
26+
* dateMax?: int,
27+
* dateMin?: int,
2628
* maxAllowedFilesCount?: int,
2729
* maxFileSize?: int,
2830
* optionsLimitMax?: int,

lib/Service/FormsService.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -629,6 +629,9 @@ public function areExtraSettingsValid(array $extraSettings, string $questionType
629629
case Constants::ANSWER_TYPE_FILE:
630630
$allowed = Constants::EXTRA_SETTINGS_FILE;
631631
break;
632+
case Constants::ANSWER_TYPE_DATE:
633+
$allowed = Constants::EXTRA_SETTINGS_DATE;
634+
break;
632635
default:
633636
$allowed = [];
634637
}
@@ -646,9 +649,16 @@ public function areExtraSettingsValid(array $extraSettings, string $questionType
646649
}
647650
}
648651

649-
if ($questionType === Constants::ANSWER_TYPE_MULTIPLE) {
652+
// Validate extraSettings for specific question types
653+
if ($questionType === Constants::ANSWER_TYPE_DATE) {
654+
// Ensure dateMin and dateMax don't overlap
655+
if (isset($extraSettings['dateMin']) && isset($extraSettings['dateMax'])
656+
&& $extraSettings['dateMin'] > $extraSettings['dateMax']) {
657+
return false;
658+
}
659+
} elseif ($questionType === Constants::ANSWER_TYPE_MULTIPLE) {
650660
// Ensure limits are sane
651-
if (isset($extraSettings['optionsLimitMax']) && isset($extraSettings['optionsLimitMin'])
661+
if (isset($extraSettings['optionsLimitMax']) && isset($extraSettings['optionsLimitMin'])
652662
&& $extraSettings['optionsLimitMax'] < $extraSettings['optionsLimitMin']) {
653663
return false;
654664
}

lib/Service/SubmissionService.php

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -383,9 +383,8 @@ public function validateSubmission(array $questions, array $answers, string $for
383383
* Check if date questions have valid answers
384384
* $answers[$questionId][0] -> date/time questions can only have one answer
385385
*/
386-
if (in_array($question['type'], Constants::ANSWER_TYPES_DATETIME) &&
387-
!$this->validateDateTime($answers[$questionId][0], Constants::ANSWER_PHPDATETIME_FORMAT[$question['type']])) {
388-
throw new \InvalidArgumentException(sprintf('Invalid date/time format for question "%s".', $question['text']));
386+
if (in_array($question['type'], Constants::ANSWER_TYPES_DATETIME)) {
387+
$this->validateDateTime($answers[$questionId][0], Constants::ANSWER_PHPDATETIME_FORMAT[$question['type']], $question['text'] ?? null, $question['extraSettings'] ?? null);
389388
}
390389

391390
// Check if all answers are within the possible options
@@ -436,11 +435,22 @@ public function validateSubmission(array $questions, array $answers, string $for
436435
* Validate correct date/time formats
437436
* @param string $dateStr String with date from answer
438437
* @param string $format String with the format to validate
439-
* @return boolean If the submitted date/time is valid
438+
* @param string|null $text String with the title of the question
439+
* @param array|null $extraSettings Array with extra settings for validation
440440
*/
441-
private function validateDateTime(string $dateStr, string $format) {
441+
private function validateDateTime(string $dateStr, string $format, ?string $text = null, ?array $extraSettings = null): void {
442442
$d = DateTime::createFromFormat($format, $dateStr);
443-
return $d && $d->format($format) === $dateStr;
443+
if (!$d || $d->format($format) !== $dateStr) {
444+
throw new \InvalidArgumentException(sprintf('Invalid date/time format for question "%s".', $text));
445+
}
446+
447+
if ($extraSettings) {
448+
if ((isset($extraSettings['dateMin']) && $d < (new DateTime())->setTimestamp($extraSettings['dateMin'])) ||
449+
(isset($extraSettings['dateMax']) && $d > (new DateTime())->setTimestamp($extraSettings['dateMax']))
450+
) {
451+
throw new \InvalidArgumentException(sprintf('Date is not in the allowed range for question "%s".', $text));
452+
}
453+
}
444454
}
445455

446456
/**

openapi.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,14 @@
418418
"type": "string"
419419
}
420420
},
421+
"dateMax": {
422+
"type": "integer",
423+
"format": "int64"
424+
},
425+
"dateMin": {
426+
"type": "integer",
427+
"format": "int64"
428+
},
421429
"maxAllowedFilesCount": {
422430
"type": "integer",
423431
"format": "int64"

src/components/Questions/QuestionDate.vue

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,32 @@
99
:title-placeholder="answerType.titlePlaceholder"
1010
:warning-invalid="answerType.warningInvalid"
1111
v-on="commonListeners">
12+
<template v-if="answerType.pickerType === 'date'" #actions>
13+
<NcActionInput
14+
v-model="dateMin"
15+
type="date"
16+
:label="t('forms', 'Pick minimum date')"
17+
hide-label
18+
:formatter="extraSettingsFormatter"
19+
is-native-picker
20+
:max="dateMax">
21+
<template #icon>
22+
<Pencil :size="20" />
23+
</template>
24+
</NcActionInput>
25+
<NcActionInput
26+
v-model="dateMax"
27+
type="date"
28+
:label="t('forms', 'Pick maximum date')"
29+
hide-label
30+
:formatter="extraSettingsFormatter"
31+
is-native-picker
32+
:min="dateMin">
33+
<template #icon>
34+
<Pencil :size="20" />
35+
</template>
36+
</NcActionInput>
37+
</template>
1238
<div class="question__content">
1339
<NcDateTimePicker
1440
:value="time"
@@ -17,6 +43,7 @@
1743
:placeholder="datetimePickerPlaceholder"
1844
:show-second="false"
1945
:type="answerType.pickerType"
46+
:disabled-date="disabledDates"
2047
:input-attr="inputAttr"
2148
@change="onValueChange" />
2249
</div>
@@ -25,15 +52,18 @@
2552

2653
<script>
2754
import moment from '@nextcloud/moment'
28-
2955
import QuestionMixin from '../../mixins/QuestionMixin.js'
56+
import NcActionInput from '@nextcloud/vue/components/NcActionInput'
3057
import NcDateTimePicker from '@nextcloud/vue/components/NcDateTimePicker'
58+
import Pencil from 'vue-material-design-icons/Pencil.vue'
3159
3260
export default {
3361
name: 'QuestionDate',
3462
3563
components: {
64+
NcActionInput,
3665
NcDateTimePicker,
66+
Pencil,
3767
},
3868
3969
mixins: [QuestionMixin],
@@ -44,6 +74,10 @@ export default {
4474
stringify: this.stringify,
4575
parse: this.parse,
4676
},
77+
extraSettingsFormatter: {
78+
stringify: this.stringifyDate,
79+
parse: this.parseTimestampToDate,
80+
},
4781
}
4882
},
4983
@@ -70,6 +104,38 @@ export default {
70104
time() {
71105
return this.values ? this.parse(this.values[0]) : null
72106
},
107+
108+
/**
109+
* The maximum allowable date for the date input field
110+
*/
111+
dateMax: {
112+
get() {
113+
return this.extraSettings?.dateMax
114+
? moment(this.extraSettings.dateMax, 'X').toDate()
115+
: null
116+
},
117+
set(value) {
118+
this.onExtraSettingsChange({
119+
dateMax: parseInt(moment(value).format('X')),
120+
})
121+
},
122+
},
123+
124+
/**
125+
* The minimum allowable date for the date input field
126+
*/
127+
dateMin: {
128+
get() {
129+
return this.extraSettings?.dateMin
130+
? moment(this.extraSettings.dateMin, 'X').toDate()
131+
: null
132+
},
133+
set(value) {
134+
this.onExtraSettingsChange({
135+
dateMin: parseInt(moment(value).format('X')),
136+
})
137+
},
138+
},
73139
},
74140
75141
methods: {
@@ -106,6 +172,39 @@ export default {
106172
moment(date).format(this.answerType.storageFormat),
107173
])
108174
},
175+
176+
/**
177+
* Determines if a given date should be disabled.
178+
*
179+
* @param {Date} date - The date to check.
180+
* @return {boolean} - Returns true if the date should be disabled, otherwise false.
181+
*/
182+
disabledDates(date) {
183+
return (
184+
(this.dateMin && date < this.dateMin) ||
185+
(this.dateMax && date > this.dateMax)
186+
)
187+
},
188+
189+
/**
190+
* Datepicker timestamp to string
191+
*
192+
* @param {Date} datetime the datepicker Date
193+
* @return {string}
194+
*/
195+
stringifyDate(datetime) {
196+
return moment(datetime).format('L')
197+
},
198+
199+
/**
200+
* Form expires timestamp to Date of the datepicker
201+
*
202+
* @param {number} value the expires timestamp
203+
* @return {Date}
204+
*/
205+
parseTimestampToDate(value) {
206+
return moment(value, 'X').toDate()
207+
},
109208
},
110209
}
111210
</script>

tests/Unit/Service/FormsServiceTest.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1372,6 +1372,31 @@ public function dataAreExtraSettingsValid() {
13721372
'questionType' => Constants::ANSWER_TYPE_SHORT,
13731373
'rval' => false
13741374
],
1375+
'valid-date-settings' => [
1376+
'extraSettings' => [
1377+
'dateMin' => 1234567890,
1378+
'dateMax' => null,
1379+
],
1380+
'questionType' => Constants::ANSWER_TYPE_DATE,
1381+
'expected' => true
1382+
],
1383+
'invalid-date-settings' => [
1384+
'extraSettings' => [
1385+
'dateMin' => 'today',
1386+
'dateMax2' => null,
1387+
],
1388+
'questionType' => Constants::ANSWER_TYPE_DATE,
1389+
'expected' => false
1390+
],
1391+
'invalid-date-limits' => [
1392+
// max < min
1393+
'extraSettings' => [
1394+
'dateMin' => 1234567890,
1395+
'dateMax' => 1234567889,
1396+
],
1397+
'questionType' => Constants::ANSWER_TYPE_MULTIPLE,
1398+
'expected' => false
1399+
],
13751400
'valid-dropdown-settings' => [
13761401
'extraSettings' => [
13771402
'shuffleOptions' => false,

tests/Unit/Service/SubmissionServiceTest.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -794,11 +794,13 @@ public function dataValidateSubmission() {
794794
'invalid-date-question' => [
795795
// Questions
796796
[
797-
['id' => 1, 'type' => 'date', 'text' => 'q1', 'isRequired' => false]
797+
['id' => 1, 'type' => 'date', 'text' => 'q1', 'isRequired' => false],
798+
['id' => 2, 'type' => 'date', 'text' => 'q2', 'isRequired' => false, 'extraSettings' => ['dateMin' => 1742857200]]
798799
],
799800
// Answers
800801
[
801-
'1' => ['31.12.2022']
802+
'1' => ['31.12.2022'],
803+
'2' => ['2025-03-24']
802804
],
803805
// Expected Result
804806
'Invalid date/time format for question "q1".',
@@ -832,6 +834,10 @@ public function dataValidateSubmission() {
832834
['id' => 11, 'type' => 'short', 'isRequired' => false, 'extraSettings' => ['validationType' => 'number']],
833835
['id' => 12, 'type' => 'short', 'isRequired' => false, 'extraSettings' => ['validationType' => 'phone']],
834836
['id' => 13, 'type' => 'short', 'isRequired' => false, 'extraSettings' => ['validationType' => 'regex', 'validationRegex' => '/[a-z]{3}[0-9]{3}/']],
837+
['id' => 16, 'type' => 'date', 'isRequired' => false, 'extraSettings' => [
838+
'dateMin' => 1742857200,
839+
'dateMax' => 1743030000]
840+
],
835841
],
836842
// Answers
837843
[
@@ -848,6 +854,7 @@ public function dataValidateSubmission() {
848854
'11' => ['100.45'],
849855
'12' => ['+49 711 25 24 28 90'],
850856
'13' => ['abc123'],
857+
'16' => ['2025-03-26']
851858
],
852859
// Expected Result
853860
null,

0 commit comments

Comments
 (0)