Skip to content
Open
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
4 changes: 2 additions & 2 deletions docs/DataStructure.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,8 +255,8 @@ Optional extra settings for some [Question Types](#question-types)
| `allowedFileExtensions` | `file` | Array of strings | `'jpg', 'png'` | Allowed file extensions for file upload |
| `maxAllowedFilesCount` | `file` | Integer | - | Maximum number of files that can be uploaded, 0 means no limit |
| `maxFileSize` | `file` | Integer | - | Maximum file size in bytes, 0 means no limit |
| `dateMax` | `date` | Integer | - | Maximum allowed date to be chosen (as Unix timestamp) |
| `dateMin` | `date` | Integer | - | Minimum allowed date to be chosen (as Unix timestamp) |
| `dateMax` | `date` | string / Integer | - | Maximum allowed date to be chosen (as `YYYY-MM-DD` string, or Unix timestamp) |
| `dateMin` | `date` | string / Integer | - | Minimum allowed date to be chosen (as `YYYY-MM-DD` string, or 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) |
Expand Down
4 changes: 2 additions & 2 deletions lib/Constants.php
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,8 @@ class Constants {
];

public const EXTRA_SETTINGS_DATE = [
'dateMax' => ['integer', 'NULL'],
'dateMin' => ['integer', 'NULL'],
'dateMax' => ['string', 'integer', 'NULL'],
'dateMin' => ['string', 'integer', 'NULL'],
'dateRange' => ['boolean', 'NULL'],
];

Expand Down
8 changes: 4 additions & 4 deletions lib/ResponseDefinitions.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
* allowOtherAnswer?: bool,
* allowedFileExtensions?: list<string>,
* allowedFileTypes?: list<string>,
* dateMax?: int,
* dateMin?: int,
* dateMax?: string|int,
* dateMin?: string|int,
* dateRange?: bool,
* maxAllowedFilesCount?: int,
* maxFileSize?: int,
Expand All @@ -36,8 +36,8 @@
* optionsLimitMin?: int,
* optionsLowest?: 0|1,
* shuffleOptions?: bool,
* timeMax?: int,
* timeMin?: int,
* timeMax?: string,
* timeMin?: string,
* timeRange?: bool,
* validationRegex?: string,
* validationType?: string,
Expand Down
33 changes: 31 additions & 2 deletions lib/Service/FormsService.php
Original file line number Diff line number Diff line change
Expand Up @@ -861,9 +861,38 @@ public function areExtraSettingsValid(array $extraSettings, string $questionType

// Validate extraSettings for specific question types
if ($questionType === Constants::ANSWER_TYPE_DATE) {
$format = Constants::ANSWER_PHPDATETIME_FORMAT['date'];
$dateMinDate = null;
$dateMaxDate = null;

// Validate dateMin format
if (isset($extraSettings['dateMin'])) {
if (is_int($extraSettings['dateMin'])) {
$dateMinDate = (new \DateTime())->setTimestamp($extraSettings['dateMin'])->setTime(0, 0, 0);
} else {
$dateMinString = $extraSettings['dateMin'];
$dateMinDate = \DateTime::createFromFormat('!' . $format, $dateMinString);
if (!$dateMinDate || $dateMinDate->format($format) !== $dateMinString) {
return false;
}
}
}

// Validate dateMax format
if (isset($extraSettings['dateMax'])) {
if (is_int($extraSettings['dateMax'])) {
$dateMaxDate = (new \DateTime())->setTimestamp($extraSettings['dateMax'])->setTime(0, 0, 0);
} else {
$dateMaxString = $extraSettings['dateMax'];
$dateMaxDate = \DateTime::createFromFormat('!' . $format, $dateMaxString);
if (!$dateMaxDate || $dateMaxDate->format($format) !== $dateMaxString) {
return false;
}
}
}

// Ensure dateMin and dateMax don't overlap
if (isset($extraSettings['dateMin']) && isset($extraSettings['dateMax'])
&& $extraSettings['dateMin'] > $extraSettings['dateMax']) {
if ($dateMinDate !== null && $dateMaxDate !== null && $dateMinDate > $dateMaxDate) {
return false;
}
} elseif ($questionType === Constants::ANSWER_TYPE_TIME) {
Expand Down
29 changes: 25 additions & 4 deletions lib/Service/SubmissionService.php
Original file line number Diff line number Diff line change
Expand Up @@ -664,10 +664,31 @@ private function validateDateTime(array $answers, string $format, ?string $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['timeMin']) && $d < DateTime::createFromFormat($format, $extraSettings['timeMin']))
|| (isset($extraSettings['timeMax']) && $d > DateTime::createFromFormat($format, $extraSettings['timeMax']))
$dateMin = isset($extraSettings['dateMin'])
? (is_int($extraSettings['dateMin'])
? (new DateTime())->setTimestamp($extraSettings['dateMin'])->setTime(0, 0, 0)
: DateTime::createFromFormat('!' . $format, $extraSettings['dateMin']))
: null;
$dateMax = isset($extraSettings['dateMax'])
? (is_int($extraSettings['dateMax'])
? (new DateTime())->setTimestamp($extraSettings['dateMax'])->setTime(0, 0, 0)
: DateTime::createFromFormat('!' . $format, $extraSettings['dateMax']))
: null;
$timeMin = isset($extraSettings['timeMin'])
? DateTime::createFromFormat($format, $extraSettings['timeMin'])
: null;
$timeMax = isset($extraSettings['timeMax'])
? DateTime::createFromFormat($format, $extraSettings['timeMax'])
: null;

$compareDate = ($dateMin !== null || $dateMax !== null)
? (DateTime::createFromFormat('!' . $format, $dateStr) ?: $d)
: $d;

if (($dateMin !== null && $compareDate < $dateMin)
|| ($dateMax !== null && $compareDate > $dateMax)
|| ($timeMin !== null && $d < $timeMin)
|| ($timeMax !== null && $d > $timeMax)
) {
throw new \InvalidArgumentException(sprintf('Date/time is not in the allowed range for question "%s".', $text));
}
Expand Down
28 changes: 20 additions & 8 deletions openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -496,12 +496,26 @@
}
},
"dateMax": {
"type": "integer",
"format": "int64"
"oneOf": [
{
"type": "string"
},
{
"type": "integer",
"format": "int64"
}
]
},
"dateMin": {
"type": "integer",
"format": "int64"
"oneOf": [
{
"type": "string"
},
{
"type": "integer",
"format": "int64"
}
]
},
"dateRange": {
"type": "boolean"
Expand Down Expand Up @@ -555,12 +569,10 @@
"type": "boolean"
},
"timeMax": {
"type": "integer",
"format": "int64"
"type": "string"
},
"timeMin": {
"type": "integer",
"format": "int64"
"type": "string"
},
"timeRange": {
"type": "boolean"
Expand Down
28 changes: 20 additions & 8 deletions src/components/Questions/QuestionDate.vue
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,10 @@ export default {
*/
dateMax() {
return this.extraSettings?.dateMax
? moment(this.extraSettings.dateMax, 'X').toDate()
? moment(this.extraSettings.dateMax, [
this.answerType.storageFormat,
'X',
]).toDate()
: null
},

Expand All @@ -196,7 +199,10 @@ export default {
*/
dateMin() {
return this.extraSettings?.dateMin
? moment(this.extraSettings.dateMin, 'X').toDate()
? moment(this.extraSettings.dateMin, [
this.answerType.storageFormat,
'X',
]).toDate()
: null
},

Expand Down Expand Up @@ -275,23 +281,29 @@ export default {
* Handles the change event for the maximum date input.
* Updates the maximum allowable date based on the provided value.
*
* @param {string | Date} value - The new maximum date value. Can be a string or a Date object.
* @param {string | Date | null} value - The new maximum date value. Can be a string or a Date object.
*/
onDateMaxChange(value) {
this.onExtraSettingsChange({
dateMax: parseInt(moment(value).format('X')),
dateMax:
value === null || value === ''
? null
: moment(value).format(this.answerType.storageFormat),
})
},

/**
* Handles the change event for the minimum date input.
* Updates the minimum allowable date based on the provided value.
*
* @param {string | Date} value - The new minimum date value. Can be a string or a Date object.
* @param {string | Date | null} value - The new minimum date value. Can be a string or a Date object.
*/
onDateMinChange(value) {
this.onExtraSettingsChange({
dateMin: parseInt(moment(value).format('X')),
dateMin:
value === null || value === ''
? null
: moment(value).format(this.answerType.storageFormat),
})
},

Expand Down Expand Up @@ -417,11 +429,11 @@ export default {
/**
* Form expires timestamp to Date of the datepicker
*
* @param {number} value the expires timestamp
* @param {number|string} value the expires timestamp or formatted date string
* @return {Date}
*/
parseTimestampToDate(value) {
return moment(value, 'X').toDate()
return moment(value, [this.answerType.storageFormat, 'X']).toDate()
},
},
}
Expand Down
16 changes: 16 additions & 0 deletions tests/Unit/Service/FormsServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1444,6 +1444,14 @@ public static function dataAreExtraSettingsValid() {
'questionType' => Constants::ANSWER_TYPE_DATE,
'expected' => true
],
'valid-date-settings-string' => [
'extraSettings' => [
'dateMin' => '2026-08-20',
'dateMax' => '2026-08-25',
],
'questionType' => Constants::ANSWER_TYPE_DATE,
'expected' => true
],
'invalid-date-settings' => [
'extraSettings' => [
'dateMin' => 'today',
Expand All @@ -1452,6 +1460,14 @@ public static function dataAreExtraSettingsValid() {
'questionType' => Constants::ANSWER_TYPE_DATE,
'expected' => false
],
'invalid-date-settings-overlap' => [
'extraSettings' => [
'dateMin' => '2026-08-25',
'dateMax' => '2026-08-20',
],
'questionType' => Constants::ANSWER_TYPE_DATE,
'expected' => false
],
'invalid-date-limits' => [
// max < min
'extraSettings' => [
Expand Down
36 changes: 36 additions & 0 deletions tests/Unit/Service/SubmissionServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -994,6 +994,42 @@ public static function dataValidateSubmission() {
// Expected Result
'Date/time is not in the allowed range for question "q1".',
],
'date-exact-limits-question-string' => [
// Questions
[
['id' => 1, 'type' => 'date', 'text' => 'q1', 'isRequired' => false, 'extraSettings' => ['dateMin' => '2026-08-20', 'dateMax' => '2026-08-24']]
],
// Answers
[
'1' => ['2026-08-24']
],
// Expected Result
null,
],
'date-below-min-question-string' => [
// Questions
[
['id' => 1, 'type' => 'date', 'text' => 'q1', 'isRequired' => false, 'extraSettings' => ['dateMin' => '2026-08-20', 'dateMax' => '2026-08-24']]
],
// Answers
[
'1' => ['2026-08-19']
],
// Expected Result
'Date/time is not in the allowed range for question "q1".',
],
'date-above-max-question-string' => [
// Questions
[
['id' => 1, 'type' => 'date', 'text' => 'q1', 'isRequired' => false, 'extraSettings' => ['dateMin' => '2026-08-20', 'dateMax' => '2026-08-24']]
],
// Answers
[
'1' => ['2026-08-25']
],
// Expected Result
'Date/time is not in the allowed range for question "q1".',
],
'valid-date-range' => [
// Questions
[
Expand Down
Loading