diff --git a/conf/defaults.config b/conf/defaults.config
index fe66785415..e32636b0eb 100644
--- a/conf/defaults.config
+++ b/conf/defaults.config
@@ -983,11 +983,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;
@@ -1627,15 +1623,6 @@ $ConfigValues = [
type => 'popuplist',
hashVar => '{hardcopyTheme}'
},
- {
- var => 'options{useDateTimePicker}',
- doc => x('Use Date Picker'),
- doc2 => x(
- '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 => x('Show Total Homework Grade on Grades Page'),
diff --git a/htdocs/js/apps/DatePicker/datepicker.js b/htdocs/js/apps/DatePicker/datepicker.js
index 4ea63626db..9b2c077d7a 100644
--- a/htdocs/js/apps/DatePicker/datepicker.js
+++ b/htdocs/js/apps/DatePicker/datepicker.js
@@ -1,7 +1,25 @@
(() => {
- document.querySelectorAll('.datepicker-group').forEach((open_rule) => {
- if (open_rule.dataset.enableDatepicker !== '1') return;
+ // Date/time formats for the languages supported by webwork.
+ // Note that these formats are chosen to match the perl DateTime::Locale formats.
+ // Make sure that anytime a new language is added, its format is added here.
+ const datetimeFormats = {
+ en: 'L/d/yy, h:mm a',
+ 'en-US': 'L/d/yy, h:mm a',
+ 'cs-CZ': 'dd.LL.yy H:mm',
+ de: 'dd.LL.yy, HH:mm',
+ es: 'd/L/yy H:mm',
+ 'fr-CA': "yyyy-LL-dd HH 'h' mm",
+ fr: 'dd/LL/yyyy HH:mm',
+ 'he-IL': 'd.L.yyyy, H:mm',
+ hu: 'yyyy. LL. dd. H:mm',
+ ko: 'yy. L. d. a h:mm',
+ 'ru-RU': 'dd.LL.yyyy, HH:mm',
+ tr: 'd.LL.yyyy HH:mm',
+ 'zh-CN': 'yyyy/L/d ah:mm',
+ 'zh-HK': 'yyyy/L/d ah:mm'
+ };
+ document.querySelectorAll('.datepicker-group').forEach((open_rule) => {
const name = open_rule.name.replace('.open_date', '');
const groupRules = [
@@ -26,23 +44,83 @@
for (const rule of groupRules) {
const orig_value = rule.value;
- flatpickr(rule.parentNode, {
+ luxon.Settings.defaultLocale = rule.dataset.locale ?? 'en';
+
+ // Compute the time difference between the current browser timezone and the the course timezone.
+ // flatpickr gives the time in the browser's timezone, and this is used to adjust to the course timezone.
+ // Note that this is converted to seconds.
+ const timezoneAdjustment =
+ parseInt(Intl.DateTimeFormat('en-US', { timeZoneName: 'shortOffset' })
+ .format(new Date).split(' ')[1].slice(3) || '0') * 3600000
+ - parseInt(Intl.DateTimeFormat('en-US',
+ { timeZone: rule.dataset.timezone ?? 'America/New_York', timeZoneName: 'shortOffset' })
+ .format(new Date).split(' ')[1].slice(3) || '0') * 3600000;
+
+ const fp = flatpickr(rule.parentNode, {
allowInput: true,
enableTime: true,
minuteIncrement: 1,
- dateFormat: 'm/d/Y at h:iK',
+ altInput: true,
+ dateFormat: 'U',
+ altFormat: datetimeFormats[luxon.Settings.defaultLocale],
+ ariaDateFormat: datetimeFormats[luxon.Settings.defaultLocale],
+ 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 (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);
+
+ // Make the alternate input left-to-right even for right-to-left languages.
+ this.altInput.dir = 'ltr';
+
+ this.altInput.addEventListener('blur', update);
+ },
+ parseDate(datestr, format) {
+ // Deal with the case of a unix timestamp. The timezone needs to be adjusted back as this is for
+ // the unix timestamp stored in the hidden input whose value will be sent to the server.
+ if (format === 'U') return new Date(parseInt(datestr) * 1000 - timezoneAdjustment);
+
+ // Next attempt to parse the datestr with the current format. This should not be adjusted. It is
+ // for display only.
+ const date = luxon.DateTime.fromFormat(datestr, format);
+ if (date.isValid) return date.toJSDate();
+
+ // Finally, fall back to the previous value in the original input if that failed. This is the case
+ // that the user typed a time that isn't in the valid format. So fallback to the last valid time
+ // that was displayed. This also should not be adjusted.
+ return new Date(this.lastFormattedDate.getTime());
+ },
+ formatDate(date, format) {
+ // Save this date for the fallback in parseDate.
+ this.lastFormattedDate = date;
+
+ // In this case the date provided is in the browser's time zone. So it needs to be adjusted to the
+ // timezone of the course.
+ if (format === 'U') return (date.getTime() + timezoneAdjustment) / 1000;
+
+ return luxon.DateTime.fromMillis(date.getTime())
+ .toFormat(datetimeFormats[luxon.Settings.defaultLocale]);
+ }
});
- rule.addEventListener('blur', update);
+ rule.nextElementSibling.addEventListener('keydown', (e) => {
+ if (e.key === ' ' || e.key === 'Enter') {
+ e.preventDefault();
+ fp.open();
+ }
+ });
}
});
})();
diff --git a/htdocs/js/apps/ProblemSetDetail/problemsetdetail.js b/htdocs/js/apps/ProblemSetDetail/problemsetdetail.js
index fa3893347d..0c4ac3e7c1 100644
--- a/htdocs/js/apps/ProblemSetDetail/problemsetdetail.js
+++ b/htdocs/js/apps/ProblemSetDetail/problemsetdetail.js
@@ -463,8 +463,15 @@
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; });
+ 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; });
+ }
});
// Make the override checkboxes for selects checked or unchecked appropriately
diff --git a/htdocs/js/apps/ProblemSetList/problemsetlist.js b/htdocs/js/apps/ProblemSetList/problemsetlist.js
index b511f8294d..a27281a954 100644
--- a/htdocs/js/apps/ProblemSetList/problemsetlist.js
+++ b/htdocs/js/apps/ProblemSetList/problemsetlist.js
@@ -41,19 +41,98 @@
});
}
+ // Date/time formats for the languages supported by webwork.
+ // Note that these formats are chosen to match the perl DateTime::Locale formats.
+ // Make sure that anytime a new language is added, its format is added here.
+ const datetimeFormats = {
+ en: 'L/d/yy, h:mm a',
+ 'en-US': 'L/d/yy, h:mm a',
+ 'cs-CZ': 'dd.LL.yy H:mm',
+ de: 'dd.LL.yy, HH:mm',
+ es: 'd/L/yy H:mm',
+ 'fr-CA': "yyyy-LL-dd HH 'h' mm",
+ fr: 'dd/LL/yyyy HH:mm',
+ 'he-IL': 'd.L.yyyy, H:mm',
+ hu: 'yyyy. LL. dd. H:mm',
+ ko: 'yy. L. d. a h:mm',
+ 'ru-RU': 'dd.LL.yyyy, HH:mm',
+ tr: 'd.LL.yyyy HH:mm',
+ 'zh-CN': 'yyyy/L/d ah:mm',
+ 'zh-HK': 'yyyy/L/d ah:mm'
+ };
+
// Initialize the date/time picker for the import form.
const importDateShift = document.getElementById('import_date_shift');
if (importDateShift) {
- flatpickr(importDateShift.parentNode, {
+
+ luxon.Settings.defaultLocale = importDateShift.dataset.locale ?? 'en';
+
+ // Compute the time difference between the current browser timezone and the the course timezone.
+ // flatpickr gives the time in the browser's timezone, and this is used to adjust to the course timezone.
+ // Note that this is converted to microseconds.
+ const timezoneAdjustment =
+ parseInt(Intl.DateTimeFormat('en-US', { timeZoneName: 'shortOffset' })
+ .format(new Date).split(' ')[1].slice(3) || '0') * 3600000
+ - parseInt(Intl.DateTimeFormat('en-US',
+ { timeZone: importDateShift.dataset.timezone ?? 'UTC', timeZoneName: 'shortOffset' })
+ .format(new Date).split(' ')[1].slice(3) || '0') * 3600000
+
+ const fp = flatpickr(importDateShift.parentNode, {
allowInput: true,
enableTime: true,
minuteIncrement: 1,
- dateFormat: 'm/d/Y at h:iK',
+ altInput: true,
+ dateFormat: 'U',
+ altFormat: datetimeFormats[luxon.Settings.defaultLocale],
+ ariaDateFormat: datetimeFormats[luxon.Settings.defaultLocale],
+ 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);
+
+ // Make the alternate input left-to-right even for right-to-left languages.
+ this.altInput.dir = 'ltr';
+ },
+ parseDate(datestr, format) {
+ // Deal with the case of a unix timestamp. The timezone needs to be adjusted back as this is for
+ // the unix timestamp stored in the hidden input whose value will be sent to the server.
+ if (format === 'U') return new Date(parseInt(datestr) * 1000 - timezoneAdjustment);
+
+ // Next attempt to parse the datestr with the current format. This should not be adjusted. It is
+ // for display only.
+ const date = luxon.DateTime.fromFormat(datestr, format);
+ if (date.isValid) return date.toJSDate();
+
+ // Finally, fall back to the previous value in the original input if that failed. This is the case
+ // that the user typed a time that isn't in the valid format. So fallback to the last valid time
+ // that was displayed. This also should not be adjusted.
+ return new Date(this.lastFormattedDate.getTime());
+ },
+ formatDate(date, format) {
+ // Save this date for the fallback in parseDate.
+ this.lastFormattedDate = date;
+
+ // In this case the date provided is in the browser's time zone. So it needs to be adjusted to the
+ // timezone of the course.
+ if (format === 'U') return (date.getTime() + timezoneAdjustment) / 1000;
+
+ return luxon.DateTime.fromMillis(date.getTime())
+ .toFormat(datetimeFormats[luxon.Settings.defaultLocale]);
+ }
+ });
+
+ importDateShift.nextElementSibling.addEventListener('keydown', (e) => {
+ if (e.key === ' ' || e.key === 'Enter') {
+ e.preventDefault();
+ fp.open();
+ }
});
}
})();
diff --git a/htdocs/js/apps/UserDetail/userdetail.js b/htdocs/js/apps/UserDetail/userdetail.js
index c0c84656db..fa84ce4247 100644
--- a/htdocs/js/apps/UserDetail/userdetail.js
+++ b/htdocs/js/apps/UserDetail/userdetail.js
@@ -29,8 +29,10 @@
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.
diff --git a/htdocs/package.json b/htdocs/package.json
index a5f6e74a95..d759d10e90 100644
--- a/htdocs/package.json
+++ b/htdocs/package.json
@@ -18,6 +18,7 @@
"iframe-resizer": "^4.3.2",
"jquery": "^3.6.0",
"jquery-ui-dist": "^1.13.1",
+ "luxon": "^2.3.1",
"mathjax": "^3.2.0",
"sortablejs": "^1.14.0"
},
diff --git a/htdocs/themes/math4/math4.scss b/htdocs/themes/math4/math4.scss
index 281b7dfa75..01c0144cfb 100644
--- a/htdocs/themes/math4/math4.scss
+++ b/htdocs/themes/math4/math4.scss
@@ -915,7 +915,7 @@ span {
/* Problem set list */
.set_table .input-group .form-control {
- max-width: 11rem;
+ max-width: 10rem;
}
/* Problem graders */
diff --git a/htdocs/third-party-assets.json b/htdocs/third-party-assets.json
index bfede668b3..89957c826b 100644
--- a/htdocs/third-party-assets.json
+++ b/htdocs/third-party-assets.json
@@ -82,6 +82,15 @@
"node_modules/codemirror/lib/codemirror.js": "https://cdn.jsdelivr.net/npm/codemirror@5.65.2/lib/codemirror.min.js",
"node_modules/flatpickr/dist/flatpickr.min.css": "https://cdn.jsdelivr.net/npm/flatpickr@4.6.9/dist/flatpickr.min.css",
"node_modules/flatpickr/dist/flatpickr.min.js": "https://cdn.jsdelivr.net/npm/flatpickr@4.6.9/dist/flatpickr.min.js",
+ "node_modules/flatpickr/dist/l10n/cs.js": "https://cdn.jsdelivr.net/npm/flatpickr@4.6.9/dist/l10n/cs.min.js",
+ "node_modules/flatpickr/dist/l10n/es.js": "https://cdn.jsdelivr.net/npm/flatpickr@4.6.9/dist/l10n/es.min.js",
+ "node_modules/flatpickr/dist/l10n/fr.js": "https://cdn.jsdelivr.net/npm/flatpickr@4.6.9/dist/l10n/fr.min.js",
+ "node_modules/flatpickr/dist/l10n/he.js": "https://cdn.jsdelivr.net/npm/flatpickr@4.6.9/dist/l10n/he.min.js",
+ "node_modules/flatpickr/dist/l10n/hu.js": "https://cdn.jsdelivr.net/npm/flatpickr@4.6.9/dist/l10n/hu.min.js",
+ "node_modules/flatpickr/dist/l10n/ko.js": "https://cdn.jsdelivr.net/npm/flatpickr@4.6.9/dist/l10n/ko.min.js",
+ "node_modules/flatpickr/dist/l10n/ru.js": "https://cdn.jsdelivr.net/npm/flatpickr@4.6.9/dist/l10n/ru.min.js",
+ "node_modules/flatpickr/dist/l10n/tr.js": "https://cdn.jsdelivr.net/npm/flatpickr@4.6.9/dist/l10n/tr.min.js",
+ "node_modules/flatpickr/dist/l10n/zh.js": "https://cdn.jsdelivr.net/npm/flatpickr@4.6.9/dist/l10n/zh.min.js",
"node_modules/flatpickr/dist/plugins/confirmDate/confirmDate.css": "https://cdn.jsdelivr.net/npm/flatpickr@4.6.9/dist/plugins/confirmDate/confirmDate.min.css",
"node_modules/flatpickr/dist/plugins/confirmDate/confirmDate.js": "https://cdn.jsdelivr.net/npm/flatpickr@4.6.9/dist/plugins/confirmDate/confirmDate.min.js",
"node_modules/iframe-resizer/js/iframeResizer.contentWindow.min.js": "https://cdn.jsdelivr.net/npm/iframe-resizer@4.3.2/js/iframeResizer.contentWindow.min.js",
@@ -89,6 +98,7 @@
"node_modules/jquery-ui-dist/jquery-ui.min.css": "https://cdn.jsdelivr.net/npm/jquery-ui-dist@1.13.1/jquery-ui.min.css",
"node_modules/jquery-ui-dist/jquery-ui.min.js": "https://cdn.jsdelivr.net/npm/jquery-ui-dist@1.13.1/jquery-ui.min.js",
"node_modules/jquery/dist/jquery.min.js": "https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js",
+ "node_modules/luxon/build/global/luxon.min.js": "https://cdn.jsdelivr.net/npm/luxon@2.3.1/build/global/luxon.min.js",
"node_modules/mathjax/es5/tex-chtml.js": "https://cdn.jsdelivr.net/npm/mathjax@3.2.0/es5/tex-chtml.js",
"node_modules/sortablejs/Sortable.min.js": "https://cdn.jsdelivr.net/npm/sortablejs@1.14.0/Sortable.min.js"
}
diff --git a/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm b/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm
index 1051d2cf3f..095b56bb5d 100644
--- a/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm
+++ b/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm
@@ -610,8 +610,6 @@ sub FieldHTML {
my $choose = ($properties{type} eq 'choose') && ($properties{override} ne 'none');
# FIXME: allow one selector to set multiple fields
- # my $globalValue = $globalRecord->{$field};
- # my $userValue = $userRecord->{$field};
my ($globalValue, $userValue) = ('', '');
my $blankfield = '';
if ($field =~ /:/) {
@@ -649,12 +647,8 @@ sub FieldHTML {
: $blankfield; # this allows for a label if value is 0
if ($field =~ /_date/) {
- $globalValue = $self->formatDateTime($globalValue, '', '%m/%d/%Y at %I:%M%P')
- if defined $globalValue && $globalValue ne '';
- # this is still fragile, but the check for blank (as opposed to 0) $userValue seems to prevent errors when
- # no user has been assigned.
- $userValue = $self->formatDateTime($userValue, '', '%m/%d/%Y at %I:%M%P')
- if defined $userValue && $userValue =~ /\S/ && $userValue ne '';
+ $globalValue = $self->formatDateTime($globalValue, '', 'datetime_format_short', $r->ce->{language})
+ if $forUsers && defined $globalValue && $globalValue ne '';
}
if (defined($properties{convertby}) && $properties{convertby}) {
@@ -686,15 +680,16 @@ sub FieldHTML {
$inputType = CGI::div(
{ class => 'input-group input-group-sm flatpickr' },
CGI::textfield({
- name => "$recordType.$recordID.$field",
- id => "$recordType.$recordID.${field}_id",
- value => $r->param("$recordType.$recordID.$field") || ($forUsers ? $userValue : $globalValue),
- data_override => "$recordType.$recordID.$field.override_id",
- class => 'form-control form-control-sm' . ($field eq 'open_date' ? ' datepicker-group' : ''),
- data_enable_datepicker => $r->ce->{options}{useDateTimePicker},
- placeholder => x('None Specified'),
- data_input => undef,
- data_done_text => $r->maketext('Done'),
+ name => "$recordType.$recordID.$field",
+ id => "$recordType.$recordID.${field}_id",
+ value => $r->param("$recordType.$recordID.$field") || ($forUsers ? $userValue : $globalValue),
+ class => 'form-control form-control-sm' . ($field eq 'open_date' ? ' datepicker-group' : ''),
+ placeholder => $r->maketext('None Specified'),
+ data_input => undef,
+ data_done_text => $r->maketext('Done'),
+ data_locale => $r->ce->{language},
+ data_timezone => $r->ce->{siteDefaults}{timezone},
+ data_override => "$recordType.$recordID.$field.override_id",
$forUsers && $check ? (aria_labelledby => "$recordType.$recordID.$field.label") : (),
}),
CGI::a(
@@ -775,19 +770,20 @@ sub FieldHTML {
: ''
) if $forUsers;
- push @return, CGI::label(
- $forUsers && $check
- ? {
- for => "$recordType.$recordID.$field.override_id",
- id => "$recordType.$recordID.$field.label",
- class => 'form-check-label'
+ push @return,
+ CGI::label(
+ $forUsers && $check
+ ? {
+ for => "$recordType.$recordID.$field.override_id",
+ id => "$recordType.$recordID.$field.label",
+ class => 'form-check-label'
}
- : {
- for => "$recordType.$recordID.${field}_id",
- class => 'form-label'
- },
- $r->maketext($properties{name})
- );
+ : {
+ for => "$recordType.$recordID.${field}_id",
+ class => 'form-label'
+ },
+ $r->maketext($properties{name})
+ );
push @return,
$properties{help_text}
@@ -822,7 +818,8 @@ sub FieldHTML {
value => $gDisplVal,
size => $properties{size} || 5,
class => 'form-control form-control-sm',
- aria_labelledby => "$recordType.$recordID.$field.label"
+ aria_labelledby => "$recordType.$recordID.$field.label",
+ $field =~ /date/ ? (dir => 'ltr') : ()
})
: ''
) if $forUsers;
@@ -1195,21 +1192,11 @@ sub initialize {
my @names = ("open_date", "due_date", "answer_date", "reduced_scoring_date");
my %dates;
- for (@names)
- {
+ for (@names) {
$dates{$_} = $r->param("set.$setID.$_") || '';
- if (defined($undoLabels{$_}{$dates{$_}}) || !$dates{$_})
- {
+ if (defined $undoLabels{$_}{ $dates{$_} } || !$dates{$_}) {
$dates{$_} = $setRecord->$_;
}
- else
- {
- eval{ $dates{$_} = $self->parseDateTime($dates{$_}) };
- if ($@) {
- $self->addbadmessage("Badly defined time. No date changes made:
$@");
- $error = $r->param('submit_changes');
- }
- }
}
if (!$error)
@@ -1307,10 +1294,6 @@ sub initialize {
my $unlabel = $undoLabels{$field}->{$param};
$param = $unlabel if defined $unlabel;
-# $param = $undoLabels{$field}->{$param} || $param;
- if ($field =~ /_date/ ) {
- $param = $self->parseDateTime($param) unless defined $unlabel;
- }
if (defined($properties{$field}->{convertby}) && $properties{$field}->{convertby}) {
$param = $param*$properties{$field}->{convertby};
}
@@ -1414,9 +1397,6 @@ sub initialize {
$param = defined $properties{$field}->{default} ? $properties{$field}->{default} : "" unless defined $param && $param ne "";
my $unlabel = $undoLabels{$field}->{$param};
$param = $unlabel if defined $unlabel;
- if ($field =~ /_date/ ) {
- $param = $self->parseDateTime($param) unless (defined $unlabel || !$param);
- }
if ($field =~ /restricted_release/ && $param) {
$param = format_set_name_internal($param =~ s/\s*,\s*/,/gr);
$self->check_sets($db, $param);
@@ -3008,7 +2988,19 @@ sub output_JS {
rel => 'stylesheet',
href => getAssetURL($ce, 'node_modules/flatpickr/dist/plugins/confirmDate/confirmDate.css')
});
+ print CGI::script({ src => getAssetURL($ce, 'node_modules/luxon/build/global/luxon.min.js'), defer => undef }, '');
print CGI::script({ src => getAssetURL($ce, 'node_modules/flatpickr/dist/flatpickr.min.js'), defer => undef }, '');
+ if ($ce->{language} !~ /^en/) {
+ print CGI::script(
+ {
+ src => getAssetURL(
+ $ce, 'node_modules/flatpickr/dist/l10n/' . ($ce->{language} =~ s/^(..).*/$1/gr) . '.js'
+ ),
+ defer => undef
+ },
+ ''
+ );
+ }
print CGI::script(
{
src => getAssetURL($ce, 'node_modules/flatpickr/dist/plugins/confirmDate/confirmDate.js'),
diff --git a/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetList.pm b/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetList.pm
index 049c805fb8..7baf03f242 100644
--- a/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetList.pm
+++ b/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetList.pm
@@ -1326,14 +1326,15 @@ sub import_form {
CGI::div(
{ class => 'input-group input-group-sm flatpickr' },
CGI::textfield({
- id => 'import_date_shift',
- name => 'action.import.start.date',
- size => '27',
- value => $actionParams{'action.import.start.date'}[0] || '',
- class => 'form-control',
- data_enable_datepicker => $ce->{options}{useDateTimePicker},
- data_input => undef,
- data_done_text => $r->maketext('Done')
+ id => 'import_date_shift',
+ name => 'action.import.start.date',
+ size => '27',
+ value => $actionParams{'action.import.start.date'}[0] || '',
+ class => 'form-control',
+ data_input => undef,
+ data_done_text => $r->maketext('Done'),
+ data_locale => $ce->{language},
+ data_timezone => $ce->{siteDefaults}{timezone}
}),
CGI::a(
{
@@ -1376,30 +1377,27 @@ sub import_handler {
my ($self, $genericParams, $actionParams, $tableParams) = @_;
my $r = $self->r;
- my @fileNames = @{ $actionParams->{"action.import.source"} };
- my $newSetName = $actionParams->{'action.import.number'}[0] > 1
+ my ($added, $skipped) = $self->importSetsFromDef(
+ $actionParams->{"action.import.number"}[0] > 1
? '' # Cannot assign set names to multiple imports.
- : format_set_name_internal($actionParams->{'action.import.name'}[0] // '');
- my $assign = $actionParams->{"action.import.assign"}->[0];
- my $startdate = 0;
- if ($actionParams->{"action.import.start.date"}->[0]) {
- $startdate = $self->parseDateTime($actionParams->{"action.import.start.date"}->[0]);
- }
-
- my ($added, $skipped) = $self->importSetsFromDef($newSetName, $assign, $startdate, @fileNames);
+ : format_set_name_internal($actionParams->{'action.import.name'}[0]),
+ $actionParams->{'action.import.assign'}[0],
+ $actionParams->{'action.import.start.date'}[0] // 0,
+ @{ $actionParams->{'action.import.source'} }
+ );
- # make new sets visible... do we really want to do this? probably.
+ # Make new sets visible.
push @{ $self->{visibleSetIDs} }, @$added;
- push @{ $self->{allSetIDs} }, @$added;
+ push @{ $self->{allSetIDs} }, @$added;
- my $numAdded = @$added;
+ my $numAdded = @$added;
my $numSkipped = @$skipped;
return CGI::div(
{ class => 'alert alert-success p-1 mb-0' },
$r->maketext(
- "[_1] sets added, [_2] sets skipped. Skipped sets: ([_3])",
- $numAdded, $numSkipped, join(", ", @$skipped)
+ '[_1] sets added, [_2] sets skipped. Skipped sets: ([_3])',
+ $numAdded, $numSkipped, join(', ', @$skipped)
)
);
}
@@ -1565,20 +1563,20 @@ sub saveEdit_handler {
foreach my $field ($Set->NONKEYFIELDS()) {
my $param = "set.${setID}.${field}";
- if (defined $tableParams->{$param}->[0]) {
+ if (defined $tableParams->{$param}[0]) {
if ($field =~ /_date/) {
- $Set->$field($self->parseDateTime($tableParams->{$param}->[0]));
+ $Set->$field($tableParams->{$param}[0]);
} elsif ($field eq 'enable_reduced_scoring') {
- #If we are enableing reduced scoring, make sure the reduced scoring date is set and in a proper interval
- my $value = $tableParams->{$param}->[0];
- $Set->enable_reduced_scoring($value);
- if (!$Set->reduced_scoring_date) {
- $Set->reduced_scoring_date($Set->due_date -
- 60*$ce->{pg}{ansEvalDefaults}{reducedScoringPeriod});
- }
-
+ # If we are enableing reduced scoring, make sure the reduced scoring date
+ # is set and in a proper interval.
+ my $value = $tableParams->{$param}[0];
+ $Set->enable_reduced_scoring($value);
+ if (!$Set->reduced_scoring_date) {
+ $Set->reduced_scoring_date(
+ $Set->due_date - 60 * $ce->{pg}{ansEvalDefaults}{reducedScoringPeriod});
+ }
} else {
- $Set->$field($tableParams->{$param}->[0]);
+ $Set->$field($tableParams->{$param}->[0]);
}
}
}
@@ -1647,20 +1645,6 @@ sub saveEdit_handler {
# utilities
################################################################################
-# generate labels for open_date/due_date/answer_date popup menus
-sub menuLabels {
- my ($self, $hashRef) = @_;
- my %hash = %$hashRef;
-
- my %result;
- foreach my $key (keys %hash) {
- my $count = @{ $hash{$key} };
- my $displayKey = $self->formatDateTime($key) || "";
- $result{$key} = "$displayKey ($count sets)";
- }
- return %result;
-}
-
sub importSetsFromDef {
my ($self, $newSetName, $assign, $startdate, @setDefFiles) = @_;
my $r = $self->r;
@@ -2437,6 +2421,7 @@ sub fieldEditHTML {
my $headerFiles = $self->{headerFiles};
if ($access eq "readonly") {
+ return CGI::span({ dir => 'ltr' }, $value) if ($type eq 'date');
return $value;
}
@@ -2466,9 +2451,12 @@ sub fieldEditHTML {
size => $size,
class => 'form-control w-auto ' . ($fieldName =~ /\.open_date/ ? ' datepicker-group' : ''),
placeholder => $self->r->maketext("None Specified"),
- data_enable_datepicker => $self->r->ce->{options}{useDateTimePicker},
- data_input => undef,
- data_done_text => $self->r->maketext('Done')
+ data_input => undef,
+ data_done_text => $self->r->maketext('Done'),
+ data_locale => $self->r->ce->{language},
+ data_timezone => $self->r->ce->{siteDefaults}{timezone},
+ role => 'button',
+ tabindex => 0
}),
CGI::a(
{
@@ -2505,36 +2493,46 @@ sub fieldEditHTML {
sub recordEditHTML {
my ($self, $Set, %options) = @_;
- my $r = $self->r;
- my $urlpath = $r->urlpath;
- my $ce = $r->ce;
- my $db = $r->db;
- my $authz = $r->authz;
- my $user = $r->param('user');
- my $root = $ce->{webworkURLs}->{root};
- my $courseName = $urlpath->arg("courseID");
-
- my $editMode = $options{editMode};
- my $exportMode = $options{exportMode};
+ my $r = $self->r;
+ my $urlpath = $r->urlpath;
+ my $ce = $r->ce;
+ my $db = $r->db;
+ my $authz = $r->authz;
+ my $user = $r->param('user');
+ my $root = $ce->{webworkURLs}{root};
+ my $courseName = $urlpath->arg('courseID');
+
+ my $editMode = $options{editMode};
+ my $exportMode = $options{exportMode};
my $setSelected = $options{setSelected};
- my $visibleClass = $Set->visible ? "font-visible" : "font-hidden";
- my $enable_reduced_scoringClass = $Set->enable_reduced_scoring ? $r->maketext('Reduced Scoring Enabled') : $r->maketext('Reduced Scoring Disabled');
+ my $visibleClass = $Set->visible ? 'font-visible' : 'font-hidden';
+ my $enable_reduced_scoringClass =
+ $Set->enable_reduced_scoring
+ ? $r->maketext('Reduced Scoring Enabled')
+ : $r->maketext('Reduced Scoring Disabled');
- my $users = $db->countSetUsers($Set->set_id);
+ my $users = $db->countSetUsers($Set->set_id);
my $totalUsers = $self->{totalUsers};
my $problems = $db->countGlobalProblems($Set->set_id);
- my $usersAssignedToSetURL = $self->systemLink($urlpath->new(type=>'instructor_users_assigned_to_set', args=>{courseID => $courseName, setID => $Set->set_id} ));
- my $prettySetID = format_set_name_display($Set->set_id);
- my $problemListURL = $self->systemLink($urlpath->new(type=>'instructor_set_detail', args=>{courseID => $courseName, setID => $Set->set_id} ));
- my $problemSetListURL = $self->systemLink($urlpath->new(type=>'instructor_set_list', args=>{courseID => $courseName, setID => $Set->set_id})) . "&editMode=1&visible_sets=" . $Set->set_id;
+ my $usersAssignedToSetURL = $self->systemLink($urlpath->new(
+ type => 'instructor_users_assigned_to_set',
+ args => { courseID => $courseName, setID => $Set->set_id }
+ ));
+ my $prettySetID = format_set_name_display($Set->set_id);
+ my $problemListURL = $self->systemLink(
+ $urlpath->new(type => 'instructor_set_detail', args => { courseID => $courseName, setID => $Set->set_id }));
+ my $problemSetListURL = $self->systemLink(
+ $urlpath->new(type => 'instructor_set_list', args => { courseID => $courseName, setID => $Set->set_id }))
+ . '&editMode=1&visible_sets='
+ . $Set->set_id;
my $imageLink = '';
- if ($authz->hasPermissions($user, "modify_problem_sets")) {
- $imageLink = CGI::a({href => $problemSetListURL},
- CGI::i({ class => 'icon fas fa-pencil-alt', data_alt => 'edit', aria_hidden => "true" }, ""));
+ if ($authz->hasPermissions($user, 'modify_problem_sets')) {
+ $imageLink = CGI::a({ href => $problemSetListURL },
+ CGI::i({ class => 'icon fas fa-pencil-alt', data_alt => 'edit', aria_hidden => 'true' }, ''));
}
my @tableCells;
@@ -2548,34 +2546,37 @@ sub recordEditHTML {
class => 'form-check-input',
$setSelected ? (checked => undef) : (),
});
- $fakeRecord{set_id} = $editMode
- ? CGI::a({href=>$problemListURL}, "$set_id")
- : CGI::span({class=>$visibleClass}, $set_id) . " " . $imageLink;
- $fakeRecord{problems} = (FIELD_PERMS()->{problems} and not $authz->hasPermissions($user, FIELD_PERMS()->{problems}))
- ? "$problems"
- : CGI::a({href=>$problemListURL}, "$problems");
- $fakeRecord{users} = (FIELD_PERMS()->{users} and not $authz->hasPermissions($user, FIELD_PERMS()->{users}))
+ $fakeRecord{set_id} =
+ $editMode
+ ? CGI::a({ href => $problemListURL }, $set_id)
+ : CGI::span({ class => $visibleClass }, $set_id) . ' ' . $imageLink;
+ $fakeRecord{problems} =
+ (FIELD_PERMS()->{problems} and not $authz->hasPermissions($user, FIELD_PERMS()->{problems}))
+ ? $problems
+ : CGI::a({ href => $problemListURL }, "$problems");
+ $fakeRecord{users} =
+ (FIELD_PERMS()->{users} and not $authz->hasPermissions($user, FIELD_PERMS()->{users}))
? "$users/$totalUsers"
- : CGI::a({href=>$usersAssignedToSetURL}, "$users/$totalUsers");
- $fakeRecord{filename} = CGI::input({-name => "set.$set_id", -value=>"set$set_id.def", -size=>60});
+ : CGI::a({ href => $usersAssignedToSetURL }, "$users/$totalUsers");
+ $fakeRecord{filename} = CGI::input({ -name => "set.$set_id", -value => "set$set_id.def", -size => 60 });
# Select
- my $label="";
- my $label_text="";
+ my $label = '';
+ my $label_text = '';
if ($editMode) {
# No checkbox column in this case.
$label_text = CGI::a({ href => $problemListURL }, $prettySetID);
} else {
# Set ID
- my $label = "";
+ my $label = '';
if ($editMode) {
$label = CGI::a({ href => $problemListURL }, $prettySetID);
} else {
$label = CGI::span({
- class => "set-label set-id-tooltip $visibleClass",
- data_bs_toggle => 'tooltip',
+ class => "set-label set-id-tooltip $visibleClass",
+ data_bs_toggle => 'tooltip',
data_bs_placement => 'right',
- data_bs_title => $Set->description()
+ data_bs_title => $Set->description()
}, $prettySetID) . ' ' . $imageLink;
}
@@ -2599,7 +2600,7 @@ sub recordEditHTML {
push @tableCells, $label_text;
} else {
# "problem list" link
- push @tableCells, CGI::a({href=>$problemListURL}, "$problems");
+ push @tableCells, CGI::a({ href => $problemListURL }, $problems);
}
# Users link
@@ -2607,7 +2608,7 @@ sub recordEditHTML {
# column not there
} else {
# "edit users assigned to set" link
- push @tableCells, CGI::a({href=>$usersAssignedToSetURL}, "$users/$totalUsers");
+ push @tableCells, CGI::a({ href => $usersAssignedToSetURL }, "$users/$totalUsers");
}
# determine which non-key fields to show
@@ -2622,28 +2623,35 @@ sub recordEditHTML {
# Remove the enable reduced scoring box if that feature isnt enabled
if (!$ce->{pg}{ansEvalDefaults}{enableReducedScoring}) {
- @fieldsToShow = grep {!/enable_reduced_scoring|reduced_scoring_date/} @fieldsToShow;
+ @fieldsToShow = grep { !/enable_reduced_scoring|reduced_scoring_date/ } @fieldsToShow;
}
# make a hash out of this so we can test membership easily
- my %nonkeyfields; @nonkeyfields{$Set->NONKEYFIELDS} = ();
+ my %nonkeyfields;
+ @nonkeyfields{ $Set->NONKEYFIELDS } = ();
# Set Fields
- foreach my $field (@fieldsToShow) {
+ for my $field (@fieldsToShow) {
next unless exists $nonkeyfields{$field};
- my $fieldName = "set." . $set_id . "." . $field,
- my $fieldValue = $Set->$field;
- #print $field;
+ my $fieldName = 'set.' . $set_id . '.' . $field, my $fieldValue = $Set->$field;
+
my %properties = %{ FIELD_PROPERTIES()->{$field} };
- $properties{access} = "readonly" unless $editMode;
+ $properties{access} = 'readonly' unless $editMode;
- $fieldValue = $self->formatDateTime($fieldValue,'','%m/%d/%Y at %I:%M%P') if $field =~ /_date/;
+ $fieldValue = $self->formatDateTime($fieldValue, '', 'datetime_format_short', $ce->{language})
+ if !$editMode && $field =~ /_date/;
$fieldValue =~ s/ / /g unless $editMode;
- $fieldValue = ($fieldValue) ? $r->maketext("Yes") : $r->maketext("No") if $field =~ /visible/ and not $editMode;
- $fieldValue = ($fieldValue) ? $r->maketext("Yes") : $r->maketext("No") if $field =~ /enable_reduced_scoring/ and not $editMode;
- $fieldValue = ($fieldValue) ? $r->maketext("Yes") : $r->maketext("No") if $field =~ /hide_hint/ and not $editMode;
- push @tableCells, CGI::span({class=>$visibleClass}, $self->fieldEditHTML($fieldName, $fieldValue, \%properties));
+ $fieldValue = $fieldValue ? $r->maketext('Yes') : $r->maketext('No')
+ if $field =~ /visible/ and not $editMode;
+ $fieldValue = $fieldValue ? $r->maketext('Yes') : $r->maketext('No')
+ if $field =~ /enable_reduced_scoring/ and not $editMode;
+ $fieldValue = $fieldValue ? $r->maketext('Yes') : $r->maketext('No')
+ if $field =~ /hide_hint/ and not $editMode;
+
+ push @tableCells,
+ CGI::span({ class => "d-inline-block w-100 text-center $visibleClass" },
+ $self->fieldEditHTML($fieldName, $fieldValue, \%properties));
}
return CGI::Tr(CGI::td(\@tableCells));
@@ -2762,7 +2770,19 @@ sub output_JS {
rel => 'stylesheet',
href => getAssetURL($ce, 'node_modules/flatpickr/dist/plugins/confirmDate/confirmDate.css')
});
+ print CGI::script({ src => getAssetURL($ce, 'node_modules/luxon/build/global/luxon.min.js'), defer => undef }, '');
print CGI::script({ src => getAssetURL($ce, 'node_modules/flatpickr/dist/flatpickr.min.js'), defer => undef }, '');
+ if ($ce->{language} !~ /^en/) {
+ print CGI::script(
+ {
+ src => getAssetURL(
+ $ce, 'node_modules/flatpickr/dist/l10n/' . ($ce->{language} =~ s/^(..).*/$1/gr) . '.js'
+ ),
+ defer => undef
+ },
+ ''
+ );
+ }
print CGI::script(
{
src => getAssetURL($ce, 'node_modules/flatpickr/dist/plugins/confirmDate/confirmDate.js'),
diff --git a/lib/WeBWorK/ContentGenerator/Instructor/SetsAssignedToUser.pm b/lib/WeBWorK/ContentGenerator/Instructor/SetsAssignedToUser.pm
index ca8f619967..4073c63b19 100644
--- a/lib/WeBWorK/ContentGenerator/Instructor/SetsAssignedToUser.pm
+++ b/lib/WeBWorK/ContentGenerator/Instructor/SetsAssignedToUser.pm
@@ -162,9 +162,9 @@ sub body {
my $prettyDate;
if ($currentlyAssigned and $UserSet->due_date) {
- $prettyDate = $self->formatDateTime($UserSet->due_date);
+ $prettyDate = $self->formatDateTime($UserSet->due_date, '', 'datetime_format_short', $ce->{language});
} else {
- $prettyDate = $self->formatDateTime($Set->due_date);
+ $prettyDate = $self->formatDateTime($Set->due_date, '', 'datetime_format_short', $ce->{language});
}
# URL to edit user-specific set data
@@ -179,20 +179,18 @@ sub body {
print CGI::Tr(
CGI::td(
{ class => 'text-center' },
- (
- CGI::checkbox({
- type => 'checkbox',
- name => 'selected',
- checked => $currentlyAssigned,
- value => $setID,
- label => '',
- class => 'form-check-input'
- })
- )
+ CGI::checkbox({
+ type => 'checkbox',
+ name => 'selected',
+ checked => $currentlyAssigned,
+ value => $setID,
+ label => '',
+ class => 'form-check-input'
+ })
),
- CGI::td([
- $setID, $prettyDate, $currentlyAssigned ? CGI::a({ href => $url }, 'Edit user-specific set data') : '',
- ])
+ CGI::td($setID),
+ CGI::td({ class => 'text-center' }, $prettyDate),
+ CGI::td($currentlyAssigned ? CGI::a({ href => $url }, 'Edit user-specific set data') : '')
);
}
print CGI::end_table(), CGI::end_div();
diff --git a/lib/WeBWorK/ContentGenerator/Instructor/UserDetail.pm b/lib/WeBWorK/ContentGenerator/Instructor/UserDetail.pm
index ec61aff51e..9faa296c63 100644
--- a/lib/WeBWorK/ContentGenerator/Instructor/UserDetail.pm
+++ b/lib/WeBWorK/ContentGenerator/Instructor/UserDetail.pm
@@ -354,37 +354,27 @@ sub outputSetRow {
}
sub checkDates {
- my $self = shift;
- my $setRecord = shift;
- my $setID = shift;
- my $r = $self->r;
- my $ce = $r->ce;
- my %dates = ();
- my $error_undefined_override = 0;
- my $numerical_date=0;
- my $error = 0;
- foreach my $field (@{DATE_FIELDS_ORDER()}) { # check that override dates can be parsed and are not blank
- $dates{$field} = $setRecord->$field;
- if (defined $r->param("set.$setID.$field.override") &&
- $r->param("set.$setID.$field") ne ''){
- eval{ $numerical_date = $self->parseDateTime($r->param("set.$setID.$field"))};
- unless( $@ ) {
- $dates{$field}=$numerical_date;
- } else {
- $self->addbadmessage(" * Badly defined time for set $setID $field. No date changes made:
$@");
- $error = 1;
- }
- }
-
-
+ my $self = shift;
+ my $setRecord = shift;
+ my $setID = shift;
+ my $r = $self->r;
+ my $error = 0;
+
+ # For each of the dates, use the override date if set. Otherwise use the value from the global set.
+ my %dates;
+ for my $field (@{ DATE_FIELDS_ORDER() }) {
+ $dates{$field} =
+ (defined $r->param("set.$setID.$field.override") && $r->param("set.$setID.$field") ne '')
+ ? $r->param("set.$setID.$field")
+ : $setRecord->$field;
}
- return {%dates,error=>1} if $error; # no point in going on if the dates can't be parsed.
- my ($open_date, $reduced_scoring_date, $due_date, $answer_date) = map { $dates{$_} } @{DATE_FIELDS_ORDER()};
+ my ($open_date, $reduced_scoring_date, $due_date, $answer_date) = map { $dates{$_} } @{ DATE_FIELDS_ORDER() };
- unless ($answer_date && $due_date && $open_date) {
- $self->addbadmessage("set $setID has errors in its dates: answer_date |$answer_date|,
- due date |$due_date|, open_date |$open_date|");
+ unless ($answer_date && $due_date && $open_date) {
+ $self->addbadmessage("set $setID has errors in its dates: answer_date |$answer_date|, "
+ . "due date |$due_date|, open_date |$open_date|");
+ $error = 1;
}
if ($answer_date < $due_date || $answer_date < $open_date) {
@@ -397,18 +387,17 @@ sub checkDates {
$error = 1;
}
- if ($ce->{pg}{ansEvalDefaults}{enableReducedScoring} &&
- $setRecord->enable_reduced_scoring &&
- ($reduced_scoring_date < $open_date || $reduced_scoring_date > $due_date)) {
- $self->addbadmessage("The reduced scoring date should be between the open date and the due date in set $setID!");
+ if ($r->ce->{pg}{ansEvalDefaults}{enableReducedScoring}
+ && $setRecord->enable_reduced_scoring
+ && ($reduced_scoring_date < $open_date || $reduced_scoring_date > $due_date))
+ {
+ $self->addbadmessage(
+ "The reduced scoring date should be between the open date and the due date in set $setID!");
$error = 1;
-}
-
+ }
- # make sure the dates are not more than 10 years in the future
- my $curr_time = time;
- my $seconds_per_year = 31_556_926;
- my $cutoff = $curr_time + $seconds_per_year*10;
+ # Make sure the dates are not more than 10 years in the future.
+ my $cutoff = time + 31_556_926 * 10;
if ($open_date > $cutoff) {
$self->addbadmessage("Error: open date cannot be more than 10 years from now in set $setID");
$error = 1;
@@ -422,11 +411,9 @@ sub checkDates {
$error = 1;
}
+ $self->addbadmessage('No date changes were saved!') if ($error);
- if ($error) {
- $self->addbadmessage("No date changes were saved!");
- }
- return {%dates,error=>$error};
+ return { %dates, error => $error };
}
sub DBFieldTable {
@@ -479,16 +466,17 @@ sub DBFieldTable {
? CGI::div(
{ class => 'input-group input-group-sm flex-nowrap flatpickr' },
CGI::input({
- name => "$recordType.$recordID.$field",
- id => "$recordType.$recordID.${field}_id",
- type => 'text',
- value => $userValue ? $self->formatDateTime($userValue, '', '%m/%d/%Y at %I:%M%P') : '',
- data_override => "$recordType.$recordID.$field.override_id",
- placeholder => x('None Specified'),
- class => 'form-control w-auto' . ($field eq 'open_date' ? ' datepicker-group' : ''),
- data_enable_datepicker => $ce->{options}{useDateTimePicker},
- data_input => undef,
- data_done_text => $self->r->maketext('Done')
+ name => "$recordType.$recordID.$field",
+ id => "$recordType.$recordID.${field}_id",
+ type => 'text',
+ value => $userValue,
+ data_override => "$recordType.$recordID.$field.override_id",
+ placeholder => $r->maketext('None Specified'),
+ class => 'form-control w-auto' . ($field eq 'open_date' ? ' datepicker-group' : ''),
+ data_input => undef,
+ data_done_text => $r->maketext('Done'),
+ data_locale => $ce->{language},
+ data_timezone => $ce->{siteDefaults}{timezone}
}),
CGI::a(
{ class => 'btn btn-secondary btn-sm', data_toggle => undef },
@@ -496,7 +484,10 @@ sub DBFieldTable {
)
)
: '',
- $self->formatDateTime($globalValue, '', '%m/%d/%Y at %I:%M%P'),
+ CGI::span(
+ { dir => 'ltr' },
+ $self->formatDateTime($globalValue, '', 'datetime_format_short', $ce->{language})
+ )
];
}
@@ -518,7 +509,19 @@ sub output_JS {
rel => 'stylesheet',
href => getAssetURL($ce, 'node_modules/flatpickr/dist/plugins/confirmDate/confirmDate.css')
});
+ print CGI::script({ src => getAssetURL($ce, 'node_modules/luxon/build/global/luxon.min.js'), defer => undef }, '');
print CGI::script({ src => getAssetURL($ce, 'node_modules/flatpickr/dist/flatpickr.min.js'), defer => undef }, '');
+ if ($ce->{language} !~ /^en/) {
+ print CGI::script(
+ {
+ src => getAssetURL(
+ $ce, 'node_modules/flatpickr/dist/l10n/' . ($ce->{language} =~ s/^(..).*/$1/gr) . '.js'
+ ),
+ defer => undef
+ },
+ ''
+ );
+ }
print CGI::script(
{
src => getAssetURL($ce, 'node_modules/flatpickr/dist/plugins/confirmDate/confirmDate.js'),
diff --git a/lib/WeBWorK/ContentGenerator/Instructor/UsersAssignedToSet.pm b/lib/WeBWorK/ContentGenerator/Instructor/UsersAssignedToSet.pm
index 25647f4316..edd40296fc 100644
--- a/lib/WeBWorK/ContentGenerator/Instructor/UsersAssignedToSet.pm
+++ b/lib/WeBWorK/ContentGenerator/Instructor/UsersAssignedToSet.pm
@@ -172,7 +172,7 @@ sub body {
my $userSetRecord = $db->getUserSet($user, $setID);
my $prettyName = $userRecord->last_name . ', ' . $userRecord->first_name;
my $dueDate = $userSetRecord->due_date if ref($userSetRecord);
- my $prettyDate = $dueDate ? $self->formatDateTime($dueDate) : '';
+ my $prettyDate = $dueDate ? $self->formatDateTime($dueDate, '', 'datetime_format_short', $ce->{language}) : '';
print CGI::Tr(
CGI::td(
{ class => 'text-center' },
diff --git a/lib/WeBWorK/Localize.pm b/lib/WeBWorK/Localize.pm
index 74e0f7ae65..1f3811ac24 100644
--- a/lib/WeBWorK/Localize.pm
+++ b/lib/WeBWorK/Localize.pm
@@ -55,7 +55,7 @@ sub getLangHandle {
sub plural {
my ($handle, $num, @forms) = @_;
- return "" if @forms == 0;
+ return '' if @forms == 0;
return $forms[2] if @forms > 2 and $num == 0;
# Normal case:
diff --git a/lib/WeBWorK/Utils.pm b/lib/WeBWorK/Utils.pm
index 58fabef7bd..cdce8bc630 100644
--- a/lib/WeBWorK/Utils.pm
+++ b/lib/WeBWorK/Utils.pm
@@ -48,7 +48,7 @@ use open IO => ':encoding(UTF-8)';
use constant MKDIR_ATTEMPTS => 10;
# "standard" WeBWorK date/time format (for set definition files):
-# %m/%d/%y at %I:%M%P
+# %m/%d/%y at %I:%M%P %Z
# where:
# %m = month number, starting with 01
# %d = numeric day of the month, with leading zeros (eg 01..31)
@@ -56,6 +56,7 @@ use constant MKDIR_ATTEMPTS => 10;
# %I = hour, 12 hour clock, leading 0's)
# %M = minute, leading 0's
# %P = am or pm (Yes %p and %P are backwards :)
+# %Z = timezone name
use constant DATE_FORMAT => "%m/%d/%Y at %I:%M%P %Z";
use constant JITAR_MASK => [hex 'FF000000', hex '00FC0000',
@@ -656,8 +657,11 @@ Formats the UNIX datetime $dateTime in the custom format provided by $format_str
If $format_string is not provided, the standard WeBWorK datetime format is used.
$dateTime is assumed to be in the server's time zone. If $display_tz is given,
the datetime is converted from the server's timezone to the timezone specified.
-The available patterns for $format_string can be found in the documentation for
-the perl DateTime package under the heading of strftime Patterns.
+If $format_string is a method of the $dt->locale instance, then format_cldr
+is used, and otherwise strftime is used. The available patterns for
+$format_string can be found in the documentation for the perl DateTime package
+under the heading of strftime Patterns. The available methods for the $dt->locale
+instance are documented at L.
$dateTime is assumed to be in the server's time zone. If $display_tz is given,
the datetime is converted from the server's timezone to the timezone specified.
If $locale is provided, the string returned will be in the format of that locale,
@@ -666,24 +670,27 @@ month names. If $locale is not provided, perl defaults to en_US.
=cut
-sub formatDateTime($;$;$;$) {
+sub formatDateTime {
my ($dateTime, $display_tz, $format_string, $locale) = @_;
- warn "Utils::formatDateTime is not a method. ", join(" ",caller(2)) if ref($dateTime); # catch bad calls to Utils::formatDateTime
- warn "not defined formatDateTime('$dateTime', '$display_tz') ",join(" ",caller(2)) unless $display_tz;
- $dateTime = $dateTime ||0; # do our best to provide default values
- $display_tz ||= "local"; # do our best to provide default vaules
+
+ warn "Utils::formatDateTime is not a method. ", join(" ", caller(2))
+ if ref($dateTime); # catch bad calls to Utils::formatDateTime
+ warn "not defined formatDateTime('$dateTime', '$display_tz') ", join(" ", caller(2)) unless $display_tz;
+
+ $dateTime = $dateTime || 0; # do our best to provide default values
+ $display_tz ||= "local"; # do our best to provide default vaules
$display_tz = verify_timezone($display_tz);
+ $format_string ||= DATE_FORMAT; # If a format is not provided, use the default WeBWorK date format
- $format_string ||= DATE_FORMAT; # If a format is not provided, use the default WeBWorK date format
- my $dt;
- if($locale) {
- $dt = DateTime->from_epoch(epoch => $dateTime, time_zone => $display_tz, locale=>$locale);
- }
- else {
- $dt = DateTime->from_epoch(epoch => $dateTime, time_zone => $display_tz);
+ my $dt = DateTime->from_epoch(epoch => $dateTime, time_zone => $display_tz, $locale ? (locale => $locale) : ());
+
+ # If $format_string is a method of $dt->locale then use call format_cldr on its return value.
+ # Otherwise assume it is a locale string meant for strftime.
+ if ($dt->locale->can($format_string)) {
+ return $dt->format_cldr($dt->locale->$format_string);
+ } else {
+ return $dt->strftime($format_string);
}
- #warn "\t\$dt = ", $dt->strftime(DATE_FORMAT), "\n";
- return $dt->strftime($format_string);
}
diff --git a/lib/WebworkWebservice/CourseActions.pm b/lib/WebworkWebservice/CourseActions.pm
index 2d34484a0d..8396879cef 100644
--- a/lib/WebworkWebservice/CourseActions.pm
+++ b/lib/WebworkWebservice/CourseActions.pm
@@ -11,7 +11,7 @@ use WebworkWebservice;
use base qw(WebworkWebservice);
use WeBWorK::DB;
use WeBWorK::DB::Utils qw(initializeUserProblem);
-use WeBWorK::Utils qw(runtime_use cryptPassword formatDateTime parseDateTime encode_utf8_base64 decode_utf8_base64);
+use WeBWorK::Utils qw(runtime_use cryptPassword encode_utf8_base64 decode_utf8_base64);
use WeBWorK::Utils::CourseManagement qw(addCourse);
use WeBWorK::Debug;
use WeBWorK::ContentGenerator::Instructor::SendMail;