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;