Skip to content

Commit 3997750

Browse files
committed
feat(date): add dateRange option to question settings and update date handling logic
Signed-off-by: Christian Hartmann <chris-hartmann@gmx.de>
1 parent a1a2295 commit 3997750

7 files changed

Lines changed: 111 additions & 43 deletions

File tree

docs/DataStructure.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,3 +228,4 @@ Optional extra settings for some [Question Types](#question-types)
228228
| `maxFileSize` | `file` | Integer | - | Maximum file size in bytes, 0 means no limit |
229229
| `dateMax` | `date` | Integer | - | Maximum allowed date to be chosen (as Unix timestamp) |
230230
| `dateMin` | `date` | Integer | - | Minimum allowed date to be chosen (as Unix timestamp) |
231+
| `dateRange` | `date` | Boolean | `true/false` | The date picker should query a date range |

lib/Constants.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ class Constants {
150150
public const EXTRA_SETTINGS_DATE = [
151151
'dateMax' => ['integer', 'NULL'],
152152
'dateMin' => ['integer', 'NULL'],
153+
'dateRange' => ['boolean', 'NULL'],
153154
];
154155

155156
// should be in sync with FileTypes.js

lib/Service/SubmissionService.php

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -374,17 +374,21 @@ public function validateSubmission(array $questions, array $answers, string $for
374374
} elseif ($maxOptions > 0 && $answersCount > $maxOptions) {
375375
throw new \InvalidArgumentException(sprintf('Question "%s" requires at most %d answers.', $question['text'], $maxOptions));
376376
}
377-
} elseif ($answersCount > 1 && $question['type'] !== Constants::ANSWER_TYPE_FILE) {
377+
} elseif ($answersCount != 2 && $question['type'] !== Constants::ANSWER_TYPE_DATE && isset($question['extraSettings']['dateRange'])) {
378+
// Check if date range questions have exactly two answers
379+
throw new \InvalidArgumentException(sprintf('Question "%s" can only have two answers.', $question['text']));
380+
} elseif ($answersCount > 1 && $question['type'] !== Constants::ANSWER_TYPE_FILE && !($question['type'] === Constants::ANSWER_TYPE_DATE && isset($question['extraSettings']['dateRange']))) {
378381
// Check if non-multiple questions have not more than one answer
379382
throw new \InvalidArgumentException(sprintf('Question "%s" can only have one answer.', $question['text']));
380383
}
381384

382385
/*
383-
* Check if date questions have valid answers
384-
* $answers[$questionId][0] -> date/time questions can only have one answer
386+
* Validate answers for date/time questions
387+
* If a date range is specified, validate all answers in the range
388+
* Otherwise, validate the single answer for the date/time question
385389
*/
386390
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);
391+
$this->validateDateTime($answers[$questionId], Constants::ANSWER_PHPDATETIME_FORMAT[$question['type']], $question['text'] ?? null, $question['extraSettings'] ?? null);
388392
}
389393

390394
// Check if all answers are within the possible options
@@ -433,22 +437,31 @@ public function validateSubmission(array $questions, array $answers, string $for
433437

434438
/**
435439
* Validate correct date/time formats
436-
* @param string $dateStr String with date from answer
440+
* @param array $answers Array with date from answer
437441
* @param string $format String with the format to validate
438442
* @param string|null $text String with the title of the question
439443
* @param array|null $extraSettings Array with extra settings for validation
440444
*/
441-
private function validateDateTime(string $dateStr, string $format, ?string $text = null, ?array $extraSettings = null): void {
442-
$d = DateTime::createFromFormat($format, $dateStr);
443-
if (!$d || $d->format($format) !== $dateStr) {
444-
throw new \InvalidArgumentException(sprintf('Invalid date/time format for question "%s".', $text));
445-
}
445+
private function validateDateTime(array $answers, string $format, ?string $text = null, ?array $extraSettings = null): void {
446+
$previousDate = null;
446447

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));
448+
foreach ($answers as $dateStr) {
449+
$d = DateTime::createFromFormat($format, $dateStr);
450+
if (!$d || $d->format($format) !== $dateStr) {
451+
throw new \InvalidArgumentException(sprintf('Invalid date/time format for question "%s".', $text));
452+
}
453+
454+
if ($previousDate !== null && $d < $previousDate) {
455+
throw new \InvalidArgumentException(sprintf('Dates for question "%s" must be in ascending order.', $text));
456+
}
457+
$previousDate = $d;
458+
459+
if ($extraSettings) {
460+
if ((isset($extraSettings['dateMin']) && $d < (new DateTime())->setTimestamp($extraSettings['dateMin'])) ||
461+
(isset($extraSettings['dateMax']) && $d > (new DateTime())->setTimestamp($extraSettings['dateMax']))
462+
) {
463+
throw new \InvalidArgumentException(sprintf('Date is not in the allowed range for question "%s".', $text));
464+
}
452465
}
453466
}
454467
}

src/components/Questions/QuestionDate.vue

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
1010
:warning-invalid="answerType.warningInvalid"
1111
v-on="commonListeners">
1212
<template v-if="answerType.pickerType === 'date'" #actions>
13+
<NcActionCheckbox
14+
:checked="extraSettings?.dateRange"
15+
@update:checked="onDateRangeChange">
16+
{{ t('forms', 'Query date range') }}
17+
</NcActionCheckbox>
1318
<NcActionInput
1419
v-model="dateMin"
1520
type="date"
@@ -45,6 +50,8 @@
4550
:type="answerType.pickerType"
4651
:disabled-date="disabledDates"
4752
:input-attr="inputAttr"
53+
:range="extraSettings?.dateRange"
54+
range-separator=" - "
4855
@change="onValueChange" />
4956
</div>
5057
</Question>
@@ -53,6 +60,7 @@
5360
<script>
5461
import moment from '@nextcloud/moment'
5562
import QuestionMixin from '../../mixins/QuestionMixin.js'
63+
import NcActionCheckbox from '@nextcloud/vue/components/NcActionCheckbox'
5664
import NcActionInput from '@nextcloud/vue/components/NcActionInput'
5765
import NcDateTimePicker from '@nextcloud/vue/components/NcDateTimePicker'
5866
import Pencil from 'vue-material-design-icons/Pencil.vue'
@@ -61,6 +69,7 @@ export default {
6169
name: 'QuestionDate',
6270
6371
components: {
72+
NcActionCheckbox,
6473
NcActionInput,
6574
NcDateTimePicker,
6675
Pencil,
@@ -102,6 +111,11 @@ export default {
102111
},
103112
104113
time() {
114+
if (this.extraSettings?.dateRange) {
115+
return this.values
116+
? [this.parse(this.values[0]), this.parse(this.values[1])]
117+
: null
118+
}
105119
return this.values ? this.parse(this.values[0]) : null
106120
},
107121
@@ -165,12 +179,30 @@ export default {
165179
/**
166180
* Store Value
167181
*
168-
* @param {Date} date The date to store
182+
* @param {Date|Array<Date>} date The date or date range to store
169183
*/
170184
onValueChange(date) {
171-
this.$emit('update:values', [
172-
moment(date).format(this.answerType.storageFormat),
173-
])
185+
if (this.extraSettings?.dateRange) {
186+
this.$emit('update:values', [
187+
moment(date[0]).format(this.answerType.storageFormat),
188+
moment(date[1]).format(this.answerType.storageFormat),
189+
])
190+
} else {
191+
this.$emit('update:values', [
192+
moment(date).format(this.answerType.storageFormat),
193+
])
194+
}
195+
},
196+
197+
/**
198+
* Handles the change event for the date range setting.
199+
*
200+
* @param {boolean} checked - Indicates whether the date range option is enabled (true) or disabled (false).
201+
* Updates the extra settings with the date range value. If `checked` is true, the date range is set;
202+
* otherwise, it is set to null.
203+
*/
204+
onDateRangeChange(checked) {
205+
this.onExtraSettingsChange({ dateRange: checked === true ?? null })
174206
},
175207
176208
/**

src/components/Results/Answer.vue

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@
1616
:key="answer.id"
1717
class="answer__text"
1818
dir="auto">
19-
<a :href="answer.url" target="_blank"
20-
><IconFile :size="20" class="answer__text-icon" />
21-
{{ answer.text }}</a
22-
>
19+
<a :href="answer.url" target="_blank">
20+
<IconFile :size="20" class="answer__text-icon" />
21+
{{ answer.text }}
22+
</a>
2323
</p>
2424
</template>
2525
<p v-else class="answer__text" dir="auto">

src/components/Results/ResultsSummary.vue

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@
4343
<!-- eslint-disable-next-line -->
4444
<li v-for="answer in answers" :key="answer.id" dir="auto">
4545
<template v-if="answer.url">
46-
<a :href="answer.url" target="_blank"
47-
><IconFile :size="20" class="question-summary__text-icon" />
48-
{{ answer.text }}</a
49-
>
46+
<a :href="answer.url" target="_blank">
47+
<IconFile :size="20" class="question-summary__text-icon" />
48+
{{ answer.text }}
49+
</a>
5050
</template>
5151
<template v-else>
5252
{{ answer.text }}
@@ -179,22 +179,33 @@ export default {
179179
}
180180
181181
// Add text answers
182-
answers.forEach((answer) => {
183-
if (answer.fileId) {
184-
answersModels.push({
185-
id: answer.id,
186-
text: answer.text,
187-
url: generateUrl('/f/{fileId}', {
188-
fileId: answer.fileId,
189-
}),
190-
})
191-
} else {
192-
answersModels.push({
193-
id: answer.id,
194-
text: answer.text,
195-
})
196-
}
197-
})
182+
if (
183+
this.question.type === 'date' &&
184+
answers.length === 2
185+
) {
186+
// Combine the first two answers in order for date range questions
187+
answersModels.push({
188+
id: `${answers[0].id}-${answers[1].id}`,
189+
text: `${answers[0].text} - ${answers[1].text}`,
190+
})
191+
} else {
192+
answers.forEach((answer) => {
193+
if (answer.fileId) {
194+
answersModels.push({
195+
id: answer.id,
196+
text: answer.text,
197+
url: generateUrl('/f/{fileId}', {
198+
fileId: answer.fileId,
199+
}),
200+
})
201+
} else {
202+
answersModels.push({
203+
id: answer.id,
204+
text: answer.text,
205+
})
206+
}
207+
})
208+
}
198209
})
199210
200211
// Calculate no response percentage

src/components/Results/Submission.vue

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,16 @@ export default {
102102
}
103103
}),
104104
})
105+
} else if (question.type === 'date') {
106+
const squashedAnswers = answers
107+
.map((answer) => answer.text)
108+
.join(' - ')
109+
110+
answeredQuestionsArray.push({
111+
id: question.id,
112+
text: question.text,
113+
squashedAnswers,
114+
})
105115
} else {
106116
const squashedAnswers = answers
107117
.map((answer) => answer.text)

0 commit comments

Comments
 (0)