From 5c5cabba48263fdcbfaf360e474e4be9d45e8f1c Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Thu, 8 Jan 2026 06:00:10 -0600 Subject: [PATCH] Add the capability to mass edit a particular set date when editing sets in the sets manager. When editing sets in the sets manager, there are now some new inputs and such above the set list table. There is a select to choose which set date to change (the open date, reduced scoring date, close date, or answer date), and which date to set that to for all selected sets. Obviously the reduced scoring date is not shown if not enabled for the course. Then there is an "Apply to Selected Sets" button which if clicked will set the selected date type to the chosen date for all selected sets. There now needs to be the set check boxes on the edit page (these were not shown in edit mode before). Internally those checks have a different input name so that they don't conflict with the behavior of the usual checks and since they are for a different purpose. There is validation, and if the button is used when the inputs haven't been set or if no sets are selected, then messages are displayed informing the user of what is needed. Note that the usual ordering of dates applies. So if a set date is out of order then the other dates are adjusted just as if that date were edited directly for the particular set. This is how I envision what was requested in https://forums.openwebwork.org/mod/forum/discuss.php?d=8795#p22520. I don't see the suggested way of this working in that forum post as actually being feasible or even really making sense. The suggestion was to add a button that would copy a date to all sets below it, but the order of sets in the list is not a reasonable way of managing this since that order could depend on the set dates to begin with. --- htdocs/js/ProblemSetList/problemsetlist.js | 82 ++++++++++++++----- .../ProblemSetList/set_list_row.html.ep | 5 +- .../ProblemSetList/set_list_table.html.ep | 74 +++++++++++++---- 3 files changed, 124 insertions(+), 37 deletions(-) diff --git a/htdocs/js/ProblemSetList/problemsetlist.js b/htdocs/js/ProblemSetList/problemsetlist.js index ca9f334853..39d9964939 100644 --- a/htdocs/js/ProblemSetList/problemsetlist.js +++ b/htdocs/js/ProblemSetList/problemsetlist.js @@ -7,12 +7,12 @@ for (const id of ids) elements.push(document.getElementById(id)); for (const element of elements) { if (element?.id.endsWith('_err_msg')) { - element?.classList.remove('d-none'); - } else { - element?.classList.add('is-invalid'); + element.classList.remove('d-none'); + } else if (element) { + element.classList.add('is-invalid'); if (!(element.id in event_listeners)) { event_listeners[element.id] = hide_errors([], elements); - element?.addEventListener('change', event_listeners[element.id]); + element.addEventListener('change', event_listeners[element.id]); } } } @@ -23,17 +23,17 @@ for (const id of ids) elements.push(document.getElementById(id)); for (const element of elements) { if (element?.id.endsWith('_err_msg')) { - element?.classList.add('d-none'); + element.classList.add('d-none'); if (element.id === 'select_set_err_msg' && 'set_table_id' in event_listeners) { document .getElementById('set_table_id') ?.removeEventListener('change', event_listeners.set_table_id); delete event_listeners.set_table_id; } - } else { - element?.classList.remove('is-invalid'); + } else if (element) { + element.classList.remove('is-invalid'); if (element.id in event_listeners) { - element?.removeEventListener('change', event_listeners[element.id]); + element.removeEventListener('change', event_listeners[element.id]); delete event_listeners[element.id]; } } @@ -174,10 +174,15 @@ 'zh-HK': 'yyyy/L/d ah:mm' }; - // Initialize the date/time picker for the import form. + // Initialize the date/time picker for the import form and common date editor. + const dateInputs = []; const importDateShift = document.getElementById('import_date_shift'); - if (importDateShift) { - luxon.Settings.defaultLocale = importDateShift.dataset.locale ?? 'en'; + if (importDateShift) dateInputs.push(importDateShift); + const commonDateInput = document.getElementById('common-date'); + if (commonDateInput) dateInputs.push(commonDateInput); + + for (const dateInput of dateInputs) { + luxon.Settings.defaultLocale = dateInput.dataset.locale ?? 'en'; // Compute the time difference between a time in the browser timezone and the same time in the course timezone. // flatpickr gives the time in the browser's timezone, and this is used to adjust to the course timezone. @@ -189,17 +194,17 @@ new Date(dateTime.toLocaleString('en-US')).getTime() - new Date( dateTime.toLocaleString('en-US', { - timeZone: importDateShift.dataset.timezone ?? 'America/New_York' + timeZone: dateInput.dataset.timezone ?? 'America/New_York' }) ).getTime() ); }; - let fallbackDate = importDateShift.value - ? new Date(parseInt(importDateShift.value) * 1000 - timezoneAdjustment(parseInt(importDateShift.value))) + let fallbackDate = dateInput.value + ? new Date(parseInt(dateInput.value) * 1000 - timezoneAdjustment(parseInt(dateInput.value))) : new Date(); - const fp = flatpickr(importDateShift.parentNode, { + const fp = flatpickr(dateInput.parentNode, { allowInput: true, enableTime: true, minuteIncrement: 1, @@ -216,15 +221,15 @@ disableMobile: true, wrap: true, plugins: [ - new confirmDatePlugin({ confirmText: importDateShift.dataset.doneText, showAlways: true }), + new confirmDatePlugin({ confirmText: dateInput.dataset.doneText, showAlways: true }), new ShortcutButtonsPlugin({ button: [ { - label: importDateShift.dataset.todayText ?? 'Today', + label: dateInput.dataset.todayText ?? 'Today', attributes: { class: 'btn btn-sm btn-secondary ms-auto me-1 mb-1' } }, { - label: importDateShift.dataset.nowText ?? 'Now', + label: dateInput.dataset.nowText ?? 'Now', attributes: { class: 'btn btn-sm btn-secondary mx-auto mb-1' } } ], @@ -251,6 +256,10 @@ // Make the alternate input left-to-right even for right-to-left languages. this.altInput.dir = 'ltr'; + + // Move the id of the now hidden input onto the added input so the labels still work. + this.altInput.id = this.input.id; + this.input.removeAttribute('id'); }, parseDate(datestr, format) { // Deal with the case of a unix timestamp. The timezone needs to be adjusted back as this is for @@ -278,11 +287,46 @@ } }); - importDateShift.nextElementSibling.addEventListener('keydown', (e) => { + dateInput.nextElementSibling.addEventListener('keydown', (e) => { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); fp.open(); } }); } + + if (commonDateInput) { + document.getElementById('apply-common-date')?.addEventListener('click', () => { + const dateTypeInput = document.getElementById('set-date-choice'); + if (!dateTypeInput?.value) { + show_errors(['choose_date_type_err_msg'], [dateTypeInput]); + return; + } + + if (!commonDateInput.value) { + show_errors( + ['choose_common_date_err_msg'], + [commonDateInput.parentNode?._flatpickr?.input, commonDateInput.parentNode?._flatpickr?.altInput] + ); + return; + } + + const selectedSets = Array.from(document.getElementsByName('apply_date_sets')).filter((c) => c.checked); + if (!selectedSets.length) { + show_errors(['select_set_err_msg'], []); + event_listeners.set_table_id = hide_errors( + ['set_table_id'], + [document.getElementById('select_set_err_msg')] + ); + document.getElementById('set_table_id')?.addEventListener('change', event_listeners.set_table_id); + } + + for (const set of selectedSets) { + const inputPicker = document.getElementsByName(`set.${set.value}.${dateTypeInput.value}`)[0]?.parentNode + ?._flatpickr; + inputPicker?.setDate(commonDateInput.value, true); + inputPicker?.close(); // The picker isn't actually open, but this triggers the onClose handler. + } + }); + } })(); diff --git a/templates/ContentGenerator/Instructor/ProblemSetList/set_list_row.html.ep b/templates/ContentGenerator/Instructor/ProblemSetList/set_list_row.html.ep index 1054859cb3..30bedc5b6e 100644 --- a/templates/ContentGenerator/Instructor/ProblemSetList/set_list_row.html.ep +++ b/templates/ContentGenerator/Instructor/ProblemSetList/set_list_row.html.ep @@ -16,6 +16,7 @@ % % if ($c->{editMode}) { + <%= check_box apply_date_sets => $set_id, class => 'form-check-input' =%> % if ($iconClass) { @@ -71,9 +72,9 @@ % for my $field (@$fieldNames) { % next unless defined $fieldTypes->{$field}; - +
<%= include 'ContentGenerator/Instructor/ProblemSetList/set_list_field', name => "set.$set_id.$field", value => $set->$field, type => $fieldTypes->{$field} =%> - +
% } diff --git a/templates/ContentGenerator/Instructor/ProblemSetList/set_list_table.html.ep b/templates/ContentGenerator/Instructor/ProblemSetList/set_list_table.html.ep index 6717411106..eac74e342f 100644 --- a/templates/ContentGenerator/Instructor/ProblemSetList/set_list_table.html.ep +++ b/templates/ContentGenerator/Instructor/ProblemSetList/set_list_table.html.ep @@ -10,6 +10,50 @@ % answer_date => maketext('Answer Date') % ); % +% if ($c->{editMode}) { +
+ +
+ +
+ +
+ <%= text_field 'common-date' => '', + id => 'common-date', class => 'form-control', + data => { + input => undef, + done_text => maketext('Done'), + today_text => maketext('Today'), + now_text => maketext('Now'), + locale => $ce->{language}, + timezone => $ce->{siteDefaults}{timezone} + } =%> + + + +
+
+ +
+
+
+ <%= maketext('Please choose a set date type.') %> +
+
+ <%= maketext('Please select a date.') %> +
+% }
<%= maketext('Please select at least one set.') %>
@@ -21,22 +65,20 @@ % - % if (!$c->{editMode}) { - - <%= label_for 'select-all', begin =%> - <%= check_box 'select-all' => '', id => 'select-all', - class => 'select-all form-check-input set-id-tooltip', - 'aria-label' => maketext('Select all sets'), - data => { - select_group => 'selected_sets', - bs_toggle => 'tooltip', - bs_placement => 'right', - bs_title => maketext('Select all sets') - } =%> - - <% end =%> - - % } + + <%= label_for 'select-all', begin =%> + <%= check_box 'select-all' => '', id => 'select-all', + class => 'select-all form-check-input set-id-tooltip', + 'aria-label' => maketext('Select all sets'), + data => { + select_group => $c->{editMode} ? 'apply_date_sets' : 'selected_sets', + bs_toggle => 'tooltip', + bs_placement => 'right', + bs_title => maketext('Select all sets') + } =%> + + <% end =%> + % for (@$fieldNames) { % if (!$c->{editMode} && $sortableFields->{$_}) {