Skip to content

Commit

Permalink
Use time strings formatted for the locale for homework set dates.
Browse files Browse the repository at this point in the history
Instead of formatting the unix timestamp for display in the inputs for
dates in the instructor tools, just use the unix timestamp in those
inputs.  The flatpickr javascript now hides those inputs and adds an
input that the user actually sees.  That input contains a localized time
string that is formatted for the locale (language) of the course via
native javascript's Intl.DateTimeFormat.  The flatpickr javascript takes
care of updating the value of the hidden input with a unix timestamp as
the user changes the visible input.

To make this work well the dates that are only used for display are
formatted with a different approach by Perl.  The DateTime format_cldr
is used for this instead of strftime.  This allows for the use of the
DateTime::Locale methods which provides uniform formatting for different
languages.  A special case is added to the lib/WeBWorK/Utils.pm
formatDateTime function to handle this.

To be precise, javascript uses Intl.DateTimeFormat with the options
`{ dateStyle: 'short', timeStyle: 'short' }`, and perl uses the
formatting string returned by `$dt->locale->datetime_format_short`.

This also makes things work cleaner in the way that the server handles
dates because you don't need to worry about parsing a date string back
to a unix timestamp.

For most languages the perl DateTime::Locale formatting and javascript
Intl.DateTimeFormat formatting are the same.  Unfortunately, for a few
languages Perl's DateTime::Locale formatting is not correct.  This can
be seen when editing a problem set values for a user.  The user values
are formatted by javascript, and the class values by perl.  The
languages that seem to have problems here are any of the Asian
languages.  In Korean javascript gives `22. 3. 16. 오전 12:00` and perl
gives `23. 3. 16. AM 12:00`.  Clearly perl is not properly translating
the AM/PM part.  For zh_CN javascript gives `2022/3/16 00:00` and perl
gives `2022/3/16 上午12:00`.  Javscript gives a 24 hour time, and perl
uses a 12 hour time.  For zh_hk javascript gives `16/3/2022 上午12:00`
and perl gives `2022/3/16 上午12:00`.  So perl does not seem to use the
correct ordering of the year, month, and day for that locale.

Of course this means that the old strftime format `%m/%d/%Y at %I:%M%P`
is not used for set dates anymore.  For English the CLDR format returned
by `datetime_format_short` that is now used is `M/d/yy, h:mm a`.  This
means instead of `03/16/2022 at 12:00am` you get `3/16/22, 12:00 AM`.  I
would prefer `03/16/2022, 12:00 AM`, but that is what our locale gives
consistently for javascript and perl.  So it is probably best to go with
the standard.

Note that the old format is still used for set export and import files.
In fact that is the only place that the parseDateTime method is still
used.  It is used there to ensure that the correct timezone is set for
the saved dates.

Note that the `useDateTimePicker` option has been removed.  That could
be re-implemented if desired, but it would be a bit tedious to do so.
The problem is that the date/time picker does not format and parse
dates, but if that option is disabled then you would need to.  I don't
really see the need for that option anymore though.

There is a minor change in the lib/WeBWorK/Utils/LanguageAndDirection.pm
file.  That was using the po filenames directly for the html `lang`
attribute.  Those filenames use underscores which are not valid for
the `lang` attribute.  So the underscores are translated to dashes.
  • Loading branch information
drgrice1 committed Mar 31, 2022
1 parent 2b235a6 commit f53e98e
Show file tree
Hide file tree
Showing 16 changed files with 481 additions and 394 deletions.
9 changes: 0 additions & 9 deletions conf/defaults.config
Original file line number Diff line number Diff line change
Expand Up @@ -982,11 +982,7 @@ $pg{displayModes} = [
#### Default settings for homework editor pages
##########################################################################################

# Whether the homework editor pages should show the datetimepicker
$options{useDateTimePicker} = 1;

# Whether the homework editor pages should show options for conditional release

$options{enableConditionalRelease} = 0;

# In the hmwk sets editor, how deep to search within templates for .def files;
Expand Down Expand Up @@ -1559,11 +1555,6 @@ $ConfigValues = [
type => 'popuplist',
hashVar => '{hardcopyTheme}'
},
{ var => 'options{useDateTimePicker}',
doc => 'Use Date Picker',
doc2 => 'Enables the use of the date picker on the Homework Sets Editor 2 page and the Problem Set Detail page',
type => 'boolean'
},
{ var => 'showCourseHomeworkTotals',
doc => 'Show Total Homework Grade on Grades Page',
doc2 => 'When this is on students will see a line on the Grades page which has their total cumulative homework score. This score includes all sets assigned to the student.',
Expand Down
53 changes: 44 additions & 9 deletions htdocs/js/apps/DatePicker/datepicker.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
(() => {
document.querySelectorAll('.datepicker-group').forEach((open_rule) => {
if (open_rule.dataset.enableDatepicker !== '1') return;

const name = open_rule.name.replace('.open_date', '');

const groupRules = [
Expand Down Expand Up @@ -30,19 +28,56 @@
allowInput: true,
enableTime: true,
minuteIncrement: 1,
dateFormat: 'm/d/Y at h:iK',
altInput: true,
dateFormat: 'U',
defaultDate: orig_value,
defaultHour: 0,
locale: rule.dataset.locale ? rule.dataset.locale.substring(0, 2) : 'en',
clickOpens: false,
disableMobile: true,
wrap: true,
plugins: [ new confirmDatePlugin({ confirmText: rule.dataset.doneText ?? 'Done', showAlways: true }) ],
onChange() {
if (rule.value.toLowerCase() !== orig_value) rule.classList.add('changed');
else rule.classList.remove('changed');
onChange(selectedDates) {
// If the altInput field has been emptied, then the formatDate method still sets the hidden input.
// So set that back to empty again.
if (!selectedDates.length) this.input.value = '';

if (this.input.value === orig_value) this.altInput.classList.remove('changed');
else this.altInput.classList.add('changed');
},
onClose: update
});
onClose: update,
onReady(selectedDates) {
// Flatpickr hides the original input and adds the alternate input after it. That messes up the
// bootstrap input group styling. So move the now hidden original input after the created alternate
// input to fix that.
this.altInput.after(this.input);

rule.addEventListener('blur', update);
this.altInput.addEventListener('blur', update);

// If the inital value is empty, then the formatDate method still sets the hidden input.
// So set that back to empty again.
if (!selectedDates.length) this.input.value = '';
},
parseDate(datestr, format) {
// Deal with the case of a unix timestamp on initial load.
if (format === 'U') return new Date(parseInt(datestr) * 1000);
// Next attempt to parse the datestr with the current format.
const date = new Date(Date.parse(datestr, format));
if (!isNaN(date.getTime())) return date;
// Finally, fall back to the previous value in the original input if that failed.
return new Date(parseInt(rule.value) * 1000);
},
formatDate(date) {
// Flatpickr sets the value of the original input to the parsed time.
// So set that back to the unix timestamp.
rule.value = date.getTime() / 1000;

// Return the localized time string.
return Intl.DateTimeFormat(rule.dataset.locale?.replaceAll(/_/g, '-') ?? 'en',
{ dateStyle: 'short', timeStyle: 'short', timeZone: rule.dataset.timezone ?? 'UTC' })
.format(date);
}
});
}
});
})();
25 changes: 25 additions & 0 deletions htdocs/js/apps/ProblemSetDetail/problemsetdetail.js
Original file line number Diff line number Diff line change
Expand Up @@ -444,4 +444,29 @@

// Render all problems on page load if requested.
if (document.getElementById('auto_render')?.checked) renderAll();

// Make the override checkboxes checked or unchecked appropriately
// as determined by the value of the input when that value changes.
document.querySelectorAll('input[data-override]').forEach((input) => {
const overrideCheck = document.getElementById(input.dataset.override);
if (!overrideCheck) return;
const changeHandler = () => overrideCheck.checked = input.value != '';
input.addEventListener('change', changeHandler);
if (input.parentElement.classList.contains('flatpickr')) {
// Attach the keyup and blur handlers to the flatpickr alternate input.
input.previousElementSibling?.addEventListener('keyup', changeHandler);
input.previousElementSibling?.addEventListener('blur',
() => { if (input.previousElementSibling.value == '') overrideCheck.checked = false; });
} else {
input.addEventListener('keyup', changeHandler);
input.addEventListener('blur', () => { if (input.value == '') overrideCheck.checked = false; });
}
});

// Do the same for selects.
document.querySelectorAll('select[data-override]').forEach((select) => {
const overrideCheck = document.getElementById(select.dataset.override);
if (!overrideCheck) return;
select.addEventListener('change', () => overrideCheck.checked = select.value != '');
});
})();
42 changes: 39 additions & 3 deletions htdocs/js/apps/ProblemSetList/problemsetlist.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,48 @@
allowInput: true,
enableTime: true,
minuteIncrement: 1,
dateFormat: 'm/d/Y at h:iK',
altInput: true,
dateFormat: 'U',
defaultHour: 0,
locale: importDateShift.dataset.locale ? importDateShift.dataset.locale.substring(0, 2) : 'en',
clickOpens: false,
disableMobile: true,
wrap: true,
plugins: [ new confirmDatePlugin({ confirmText: importDateShift.dataset.doneText, showAlways: true })
],
plugins: [ new confirmDatePlugin({ confirmText: importDateShift.dataset.doneText, showAlways: true }) ],
onReady(selectedDates) {
// Flatpickr hides the original input and adds the alternate input after it. That messes up the
// bootstrap input group styling. So move the now hidden original input after the created alternate
// input to fix that.
this.altInput.after(this.input);

// If the inital value is empty, then the formatDate method still sets the hidden input.
// So set that back to empty again.
if (!selectedDates.length) this.input.value = '';
},
onChange(selectedDates) {
// If the altInput field has been emptied, then the formatDate method still sets the hidden input.
// So set that back to empty again.
if (!selectedDates.length) this.input.value = '';
},
parseDate(datestr, format) {
// Deal with the case of a unix timestamp.
if (format === 'U') return new Date(parseInt(datestr) * 1000);
// Next attempt to parse the datestr with the current format.
const date = new Date(Date.parse(datestr, format));
if (!isNaN(date.getTime())) return date;
// Finally, fall back to the previous value in the original input if that failed.
return new Date(parseInt(importDateShift.value) * 1000);
},
formatDate(date) {
// Flatpickr sets the value of the original input to the parsed time.
// So set that back to the unix timestamp.
importDateShift.value = date.getTime() / 1000;

// Return the localized time string.
return Intl.DateTimeFormat(importDateShift.dataset.locale?.replaceAll(/_/g, '-') ?? 'en',
{ dateStyle: 'short', timeStyle: 'short', timeZone: importDateShift.dataset.timezone ?? 'UTC' })
.format(date);
}
});
}
})();
8 changes: 5 additions & 3 deletions htdocs/js/apps/UserDetail/userdetail.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@

// Make the date override checkboxes checked or unchecked appropriately
// as determined by the value of the date input when that value changes.
document.querySelectorAll('input[type="text"][data-override]').forEach((input) => {
document.querySelectorAll('input[data-override]').forEach((input) => {
const overrideCheck = document.getElementById(input.dataset.override);
if (!overrideCheck) return;
const changeHandler = () => overrideCheck.checked = input.value != '';
input.addEventListener('change', changeHandler);
input.addEventListener('keyup', changeHandler);
input.addEventListener('blur', () => { if (input.value == '') overrideCheck.checked = false; });
// Attach the keyup and blur handlers to the flatpickr alternate input.
input.previousElementSibling?.addEventListener('keyup', changeHandler);
input.previousElementSibling?.addEventListener('blur',
() => { if (input.previousElementSibling.value == '') overrideCheck.checked = false; });
});

// If the "Assign All Sets to Current User" button is clicked, then check all assignments.
Expand Down
2 changes: 1 addition & 1 deletion htdocs/themes/math4/math4.scss
Original file line number Diff line number Diff line change
Expand Up @@ -908,7 +908,7 @@ span {

/* Problem set list */
.set_table .input-group .form-control {
max-width: 11rem;
max-width: 10rem;
}

/* Problem graders */
Expand Down
9 changes: 9 additions & 0 deletions htdocs/third-party-assets.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,15 @@
"node_modules/codemirror/lib/codemirror.js": "https://cdn.jsdelivr.net/npm/[email protected]/lib/codemirror.min.js",
"node_modules/flatpickr/dist/flatpickr.min.css": "https://cdn.jsdelivr.net/npm/[email protected]/dist/flatpickr.min.css",
"node_modules/flatpickr/dist/flatpickr.min.js": "https://cdn.jsdelivr.net/npm/[email protected]/dist/flatpickr.min.js",
"node_modules/flatpickr/dist/l10n/cs.js": "https://cdn.jsdelivr.net/npm/[email protected]/dist/l10n/cs.min.js",
"node_modules/flatpickr/dist/l10n/es.js": "https://cdn.jsdelivr.net/npm/[email protected]/dist/l10n/es.min.js",
"node_modules/flatpickr/dist/l10n/fr.js": "https://cdn.jsdelivr.net/npm/[email protected]/dist/l10n/fr.min.js",
"node_modules/flatpickr/dist/l10n/he.js": "https://cdn.jsdelivr.net/npm/[email protected]/dist/l10n/he.min.js",
"node_modules/flatpickr/dist/l10n/hu.js": "https://cdn.jsdelivr.net/npm/[email protected]/dist/l10n/hu.min.js",
"node_modules/flatpickr/dist/l10n/ko.js": "https://cdn.jsdelivr.net/npm/[email protected]/dist/l10n/ko.min.js",
"node_modules/flatpickr/dist/l10n/ru.js": "https://cdn.jsdelivr.net/npm/[email protected]/dist/l10n/ru.min.js",
"node_modules/flatpickr/dist/l10n/tr.js": "https://cdn.jsdelivr.net/npm/[email protected]/dist/l10n/tr.min.js",
"node_modules/flatpickr/dist/l10n/zh.js": "https://cdn.jsdelivr.net/npm/[email protected]/dist/l10n/zh.min.js",
"node_modules/flatpickr/dist/plugins/confirmDate/confirmDate.css": "https://cdn.jsdelivr.net/npm/[email protected]/dist/plugins/confirmDate/confirmDate.min.css",
"node_modules/flatpickr/dist/plugins/confirmDate/confirmDate.js": "https://cdn.jsdelivr.net/npm/[email protected]/dist/plugins/confirmDate/confirmDate.min.js",
"node_modules/iframe-resizer/js/iframeResizer.contentWindow.min.js": "https://cdn.jsdelivr.net/npm/[email protected]/js/iframeResizer.contentWindow.min.js",
Expand Down
Loading

0 comments on commit f53e98e

Please sign in to comment.