From da6075828f9bba157b6b0fd850d4d87e9b91cd06 Mon Sep 17 00:00:00 2001 From: Mike Rylander Date: Wed, 21 Jun 2017 14:03:29 -0400 Subject: [PATCH] LP#1705524: Honor timezone of the acting library where appropriate This is a followup to the work done in bug 1485374, where we added the ability for the client to specify a timezone in which timestamps should be interpreted in business logic and the database. Most specifically, this work focuses on circulation due dates and the closed date editor. Due dates, where displayed using stock templates (including receipt templates) and used for fine calculation, are now manipulated in the library's configured timezone. This is controlled by the new 'lib.timezone' YAOUS, loaded from the server when required. Additionally, closings are recorded in the library's timezone so that so that due date calculation is more accurate. The closed date editor is also taught how to display closings in the closed library's timezone. Closed date entries also explicitly record if they are a full day closing, or a multi-day closing. This significantly simplifies the editor, and may be useful in other contexts. To accomplish this, we use the moment.js library and the moment-timezone addon. This is necessary because the stock AngularJS date filter does not understand locale-aware timezone values, which are required to support DST. A simple mapper translates the differences in format values from AngularJS date to moment.js. Of special note are a set of new filters used for formatting timestamps under certain circumstances. The new egOrgDateInContext, egOrgDate, and egDueDate filters provide the functionality, and autogrid is enhanced to make use of these where applicable. egGrid and egGridField are also taught to accept default and field-specific options for applying date filters. These filters may be useful in other or related contexts. The egDueDate filter, used for all existing displays of due date via Angular code, intentionally interprets timestamps in two different ways WRT timezone, based on the circulation duration. If the duration is day-granular (that is, the number of seconds in the duration is divisible by 86,400, or 24 hours worth of seconds) then the date is interpreted as being in the circulation library's timezone. If it is an hourly loan (any duration that does not meet the day-granular criterium) then it is instead displayed in the client's timezone, just as all other timestamps currently are, because of the work in 1485374. The OPAC is adjusted to always display the due date in the circulating library's timezone. Because the OPAC displays only the date portion of the due date field, this difference is currently considered acceptable. If this proves to be a problem in the future, a minor adjustment can be made to match the egDueDate filter logic. This work, as with 1485374 was funded by SITKA, and we thank them for their partnership in making this happen! Signed-off-by: Mike Rylander Signed-off-by: Tina Ji --- Open-ILS/examples/fm_IDL.xml | 2 + .../lib/OpenILS/Application/AppUtils.pm | 2 +- .../OpenILS/Application/Circ/CircCommon.pm | 7 +- .../OpenILS/Application/Storage/CDBI/actor.pm | 2 +- .../lib/OpenILS/WWW/EGCatLoader/Util.pm | 6 + Open-ILS/src/sql/Pg/005.schema.actors.sql | 2 + Open-ILS/src/sql/Pg/950.data.seed-values.sql | 12 ++ .../Pg/upgrade/XXXX.data.tz_org_setting.sql | 27 +++ .../templates/opac/myopac/circ_history.tt2 | 2 +- Open-ILS/src/templates/opac/myopac/circs.tt2 | 2 +- Open-ILS/src/templates/opac/myopac/main.tt2 | 3 +- .../opac/parts/record/copy_table.tt2 | 4 +- Open-ILS/src/templates/staff/base_js.tt2 | 2 + .../staff/cat/item/t_circ_list_pane.tt2 | 2 +- .../src/templates/staff/cat/item/t_list.tt2 | 2 +- .../staff/cat/item/t_summary_pane.tt2 | 2 +- .../staff/circ/checkin/t_checkin_table.tt2 | 2 +- .../staff/circ/patron/t_bills_list.tt2 | 5 + .../staff/circ/patron/t_checkout.tt2 | 2 +- .../staff/circ/patron/t_items_out.tt2 | 4 +- .../staff/circ/patron/t_xact_details.tt2 | 2 +- .../templates/staff/circ/renew/t_renew.tt2 | 2 +- .../share/print_templates/t_checkout.tt2 | 2 +- .../share/print_templates/t_items_out.tt2 | 2 +- .../staff/share/print_templates/t_renew.tt2 | 2 +- .../src/templates/staff/share/t_autogrid.tt2 | 2 +- Open-ILS/web/js/ui/default/staff/Gruntfile.js | 6 +- .../ui/default/staff/admin/workstation/app.js | 4 + .../web/js/ui/default/staff/cat/item/app.js | 2 + .../js/ui/default/staff/circ/services/circ.js | 3 + Open-ILS/web/js/ui/default/staff/package.json | 2 + .../web/js/ui/default/staff/services/grid.js | 69 +++++-- .../web/js/ui/default/staff/services/ui.js | 129 +++++++++++++ .../staff_client/server/admin/closed_dates.js | 178 ++++++++++++------ .../server/admin/closed_dates.xhtml | 5 + 35 files changed, 405 insertions(+), 97 deletions(-) create mode 100644 Open-ILS/src/sql/Pg/upgrade/XXXX.data.tz_org_setting.sql diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml index 43aed7d9b9..f2abaa798f 100644 --- a/Open-ILS/examples/fm_IDL.xml +++ b/Open-ILS/examples/fm_IDL.xml @@ -3203,6 +3203,8 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + + diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm index 790425edf1..a56443da60 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm @@ -2062,7 +2062,7 @@ sub basic_opac_copy_query { {column => 'id', alias => 'call_number'}, {column => 'owning_lib', alias => 'call_number_owning_lib'} ], - circ => ['due_date'], + circ => ['due_date',{column => 'circ_lib', alias => 'circ_circ_lib'}], acnp => [ {column => 'label', alias => 'call_number_prefix_label'}, {column => 'id', alias => 'call_number_prefix'} diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/CircCommon.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/CircCommon.pm index b22fee458f..c7caea75a3 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/CircCommon.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/CircCommon.pm @@ -576,6 +576,9 @@ sub generate_fines { $c->$circ_lib_method, 'circ.fines.truncate_to_max_fine'); $truncate_to_max_fine = $U->is_true($truncate_to_max_fine); + my $tz = $U->ou_ancestor_setting_value( + $c->$circ_lib_method, 'lib.timezone') || 'local'; + my ($latest_billing_ts, $latest_amount) = ('',0); for (my $bill = 1; $bill <= $pending_fine_count; $bill++) { @@ -592,8 +595,8 @@ sub generate_fines { last; } - # XXX Use org time zone (or default to 'local') once we have the ou setting built for that - my $billing_ts = DateTime->from_epoch( epoch => $last_fine, time_zone => 'local' ); + # Use org time zone (or default to 'local') + my $billing_ts = DateTime->from_epoch( epoch => $last_fine, time_zone => $tz ); my $current_bill_count = $bill; while ( $current_bill_count ) { $billing_ts->add( seconds_to_interval_hash( $fine_interval ) ); diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/actor.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/actor.pm index 4572dcc869..a4047f794e 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/actor.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/actor.pm @@ -100,7 +100,7 @@ use base qw/actor/; __PACKAGE__->table( 'actor_org_unit_closed' ); __PACKAGE__->columns( Primary => qw/id/); -__PACKAGE__->columns( Essential => qw/org_unit close_start close_end reason/); +__PACKAGE__->columns( Essential => qw/org_unit close_start close_end reason full_day multi_day/); #------------------------------------------------------------------------------- diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm index cf1f7e9869..199ff7a900 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm @@ -240,6 +240,7 @@ sub init_ro_object_cache { # turns an ISO date into something TT can understand $locale_subs->{parse_datetime} = sub { my $date = shift; + my $context_org = shift; # optional, for setting timezone via YAOUS # Calling parse_datetime() with empty $date will lead to Internal Server Error return '' if (!defined($date) or $date eq ''); @@ -259,6 +260,11 @@ sub init_ro_object_cache { my $cleansed_date = cleanse_ISO8601($date); $date = DateTime::Format::ISO8601->new->parse_datetime($cleansed_date); + if ($context_org) { + $context_org = $context_org->id if ref($context_org); + my $tz = $locale_subs->{get_org_setting}->($context_org,'lib.timezone'); + $date->set_time_zone($tz) if ($tz); + } return sprintf( "%0.2d:%0.2d:%0.2d %0.2d-%0.2d-%0.4d", $date->hour, diff --git a/Open-ILS/src/sql/Pg/005.schema.actors.sql b/Open-ILS/src/sql/Pg/005.schema.actors.sql index 90a351ddd0..d96f83dd50 100644 --- a/Open-ILS/src/sql/Pg/005.schema.actors.sql +++ b/Open-ILS/src/sql/Pg/005.schema.actors.sql @@ -483,6 +483,8 @@ CREATE TABLE actor.org_unit_closed ( org_unit INT NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED, close_start TIMESTAMP WITH TIME ZONE NOT NULL, close_end TIMESTAMP WITH TIME ZONE NOT NULL, + full_day BOOLEAN NOT NULL DEFAULT FALSE, + multi_day BOOLEAN NOT NULL DEFAULT FALSE, reason TEXT ); diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql index 130b217272..e95d94caea 100644 --- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql +++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql @@ -17113,3 +17113,15 @@ VALUES ( ); INSERT INTO config.copy_tag_type (code, label, owner) VALUES ('bookplate', 'Digital Bookplate', 1); + +INSERT into config.org_unit_setting_type +( name, grp, label, description, datatype ) VALUES + +( 'lib.timezone', 'lib', + oils_i18n_gettext('lib.timezone', + 'Library time zone', + 'coust', 'label'), + oils_i18n_gettext('lib.timezone', + 'Define the time zone in which a library physically resides', + 'coust', 'description'), + 'string'); diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.tz_org_setting.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.tz_org_setting.sql new file mode 100644 index 0000000000..53b1b1a376 --- /dev/null +++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.tz_org_setting.sql @@ -0,0 +1,27 @@ +BEGIN; + +INSERT into config.org_unit_setting_type +( name, grp, label, description, datatype ) VALUES + +( 'lib.timezone', 'lib', + oils_i18n_gettext('lib.timezone', + 'Library time zone', + 'coust', 'label'), + oils_i18n_gettext('lib.timezone', + 'Define the time zone in which a library physically resides', + 'coust', 'description'), + 'string'); + +ALTER TABLE actor.org_unit_closed ADD COLUMN full_day BOOLEAN DEFAULT FALSE; +ALTER TABLE actor.org_unit_closed ADD COLUMN multi_day BOOLEAN DEFAULT FALSE; + +UPDATE actor.org_unit_closed SET multi_day = TRUE + WHERE close_start::DATE <> close_end::DATE; + +UPDATE actor.org_unit_closed SET full_day = TRUE + WHERE close_start::DATE = close_end::DATE + AND SUBSTRING(close_start::time::text FROM 1 FOR 8) = '00:00:00' + AND SUBSTRING(close_end::time::text FROM 1 FOR 8) = '23:59:59'; + +COMMIT; + diff --git a/Open-ILS/src/templates/opac/myopac/circ_history.tt2 b/Open-ILS/src/templates/opac/myopac/circ_history.tt2 index d7f2175c52..6940bdeeff 100644 --- a/Open-ILS/src/templates/opac/myopac/circ_history.tt2 +++ b/Open-ILS/src/templates/opac/myopac/circ_history.tt2 @@ -187,7 +187,7 @@ [% date.format(ctx.parse_datetime(circ.circ.xact_start),DATE_FORMAT); %] - [% date.format(ctx.parse_datetime(circ.circ.due_date),DATE_FORMAT); %] + [% date.format(ctx.parse_datetime(circ.circ.due_date, circ.circ.circ_lib),DATE_FORMAT); %] [% IF circ.circ.checkin_time; diff --git a/Open-ILS/src/templates/opac/myopac/circs.tt2 b/Open-ILS/src/templates/opac/myopac/circs.tt2 index 91ebeb09f4..92ae989f9c 100644 --- a/Open-ILS/src/templates/opac/myopac/circs.tt2 +++ b/Open-ILS/src/templates/opac/myopac/circs.tt2 @@ -167,7 +167,7 @@ [% circ.circ.renewal_remaining %] [% - due_date = ctx.parse_datetime(circ.circ.due_date); + due_date = ctx.parse_datetime(circ.circ.due_date, circ.circ.circ_lib); due_class = (date.now > date.format(due_date, '%s')) ? 'error' : ''; %] diff --git a/Open-ILS/src/templates/opac/myopac/main.tt2 b/Open-ILS/src/templates/opac/myopac/main.tt2 index 9dc672e1d9..91133fc68d 100644 --- a/Open-ILS/src/templates/opac/myopac/main.tt2 +++ b/Open-ILS/src/templates/opac/myopac/main.tt2 @@ -72,8 +72,9 @@ [% ts = f.xact.circulation.due_date || f.xact.reservation.end_time || 0; + due_org = f.xact.circulation.circ_lib || f.xact.reservation.pickup_lib; IF ts; - date.format(ctx.parse_datetime(ts), DATE_FORMAT); + date.format(ctx.parse_datetime(ts, due_org), DATE_FORMAT); END %] diff --git a/Open-ILS/src/templates/opac/parts/record/copy_table.tt2 b/Open-ILS/src/templates/opac/parts/record/copy_table.tt2 index 774f188201..6be5346bed 100644 --- a/Open-ILS/src/templates/opac/parts/record/copy_table.tt2 +++ b/Open-ILS/src/templates/opac/parts/record/copy_table.tt2 @@ -74,7 +74,7 @@ IF has_copies; [% bib.target_copy.barcode | html %] [% copy_info.copy_location | html %] [% copy_info.copy_status | html %] - [% copy_info.due_date | html %] + [% date.format(ctx.parse_datetime(copy_info.due_date, copy_info.circ_circ_lib),DATE_FORMAT) %] [%- END; # FOREACH peer END; # FOREACH bib @@ -217,7 +217,7 @@ END; # FOREACH bib [% IF copy_info.due_date; date.format( - ctx.parse_datetime(copy_info.due_date), + ctx.parse_datetime(copy_info.due_date, copy_info.circ_circ_lib), DATE_FORMAT ); ELSE; diff --git a/Open-ILS/src/templates/staff/base_js.tt2 b/Open-ILS/src/templates/staff/base_js.tt2 index 82b662e432..1880f66e0a 100644 --- a/Open-ILS/src/templates/staff/base_js.tt2 +++ b/Open-ILS/src/templates/staff/base_js.tt2 @@ -19,6 +19,8 @@ + + diff --git a/Open-ILS/src/templates/staff/cat/item/t_circ_list_pane.tt2 b/Open-ILS/src/templates/staff/cat/item/t_circ_list_pane.tt2 index 3cab22b938..422830f9c4 100644 --- a/Open-ILS/src/templates/staff/cat/item/t_circ_list_pane.tt2 +++ b/Open-ILS/src/templates/staff/cat/item/t_circ_list_pane.tt2 @@ -39,7 +39,7 @@
[% l('Check Out Date') %]
{{circ.xact_start() | date:egDateAndTimeFormat}}
[% l('Due Date') %]
-
{{circ.due_date() | date:egDateAndTimeFormat}}
+
{{circ.due_date() | egDueDate:egDateAndTimeFormat:circ.circ_lib():circ.duration()}}
[% l('Stop Fines Time') %]
{{circ.stop_fines_time() | date:egDateAndTimeFormat}}
[% l('Checkin Time') %]
diff --git a/Open-ILS/src/templates/staff/cat/item/t_list.tt2 b/Open-ILS/src/templates/staff/cat/item/t_list.tt2 index 6afb87ade5..eb584c95e5 100644 --- a/Open-ILS/src/templates/staff/cat/item/t_list.tt2 +++ b/Open-ILS/src/templates/staff/cat/item/t_list.tt2 @@ -64,7 +64,7 @@ - + diff --git a/Open-ILS/src/templates/staff/cat/item/t_summary_pane.tt2 b/Open-ILS/src/templates/staff/cat/item/t_summary_pane.tt2 index 4f03835efa..4b9abe3f3f 100644 --- a/Open-ILS/src/templates/staff/cat/item/t_summary_pane.tt2 +++ b/Open-ILS/src/templates/staff/cat/item/t_summary_pane.tt2 @@ -34,7 +34,7 @@
{{copy.call_number().label()}}
[% l('Due Date') %]
-
{{circ.due_date() | date:egDateAndTimeFormat}}
+
{{circ.due_date() | egDueDate:egDateAndTimeFormat:circ.circ_lib():circ.duration()}}
diff --git a/Open-ILS/src/templates/staff/circ/checkin/t_checkin_table.tt2 b/Open-ILS/src/templates/staff/circ/checkin/t_checkin_table.tt2 index 0ccf9e4bbe..0066cb3217 100644 --- a/Open-ILS/src/templates/staff/circ/checkin/t_checkin_table.tt2 +++ b/Open-ILS/src/templates/staff/circ/checkin/t_checkin_table.tt2 @@ -70,7 +70,7 @@ + path='circ.due_date' dateonlyinterval="duration" datecontext="circ_lib" datatype="timestamp" hidden> diff --git a/Open-ILS/src/templates/staff/circ/patron/t_bills_list.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_bills_list.tt2 index 68511ad704..3d4863cd88 100644 --- a/Open-ILS/src/templates/staff/circ/patron/t_bills_list.tt2 +++ b/Open-ILS/src/templates/staff/circ/patron/t_bills_list.tt2 @@ -98,6 +98,11 @@ + + + diff --git a/Open-ILS/src/templates/staff/circ/patron/t_checkout.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_checkout.tt2 index 57f4b453ba..9343c92e6c 100644 --- a/Open-ILS/src/templates/staff/circ/patron/t_checkout.tt2 +++ b/Open-ILS/src/templates/staff/circ/patron/t_checkout.tt2 @@ -88,7 +88,7 @@ path="acn.label"> + path='circ.due_date' datecontext="circ_lib" dateonlyinterval="duration" datatype="timestamp"> diff --git a/Open-ILS/src/templates/staff/circ/patron/t_items_out.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_items_out.tt2 index c80c4b6b3f..4c354fd40d 100644 --- a/Open-ILS/src/templates/staff/circ/patron/t_items_out.tt2 +++ b/Open-ILS/src/templates/staff/circ/patron/t_items_out.tt2 @@ -38,7 +38,7 @@ - + @@ -80,7 +80,7 @@ {{item.target_copy().barcode()}} - + diff --git a/Open-ILS/src/templates/staff/circ/patron/t_xact_details.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_xact_details.tt2 index 5f13cc7c5c..032a726d92 100644 --- a/Open-ILS/src/templates/staff/circ/patron/t_xact_details.tt2 +++ b/Open-ILS/src/templates/staff/circ/patron/t_xact_details.tt2 @@ -25,7 +25,7 @@
[% l('Total Billed') %]
{{xact.summary().balance_owed() | currency}}
[% l('Due Date') %]
-
{{xact.circulation().due_date() | date:$root.egDateAndTimeFormat}}
+
{{xact.circulation().due_date() | egDueDate:$root.egDateAndTimeFormat:xact.circulation().circ_lib():xact.circulation().duration()}}
[% l('Finish') %]
diff --git a/Open-ILS/src/templates/staff/circ/renew/t_renew.tt2 b/Open-ILS/src/templates/staff/circ/renew/t_renew.tt2 index 7b0fdb791f..a84373ac5b 100644 --- a/Open-ILS/src/templates/staff/circ/renew/t_renew.tt2 +++ b/Open-ILS/src/templates/staff/circ/renew/t_renew.tt2 @@ -91,7 +91,7 @@ path="acn.label"> + path='circ.due_date' datecontext="circ_lib" dateonlyinterval="duration" datatype="timestamp"> diff --git a/Open-ILS/src/templates/staff/share/print_templates/t_checkout.tt2 b/Open-ILS/src/templates/staff/share/print_templates/t_checkout.tt2 index 808f52c212..327a05ea57 100644 --- a/Open-ILS/src/templates/staff/share/print_templates/t_checkout.tt2 +++ b/Open-ILS/src/templates/staff/share/print_templates/t_checkout.tt2 @@ -18,7 +18,7 @@ Template for printing checkout receipts; fields available include:
{{checkout.title}}
[% l('Barcode: [_1] Due: [_2]', '{{checkout.copy.barcode}}', - '{{checkout.circ.due_date | date:$root.egDateAndTimeFormat}}') %]
+ '{{checkout.circ.due_date | egDueDate:$root.egDateAndTimeFormat:checkout.circ.circ_lib:checkout.circ.duration}}') %]

diff --git a/Open-ILS/src/templates/staff/share/print_templates/t_items_out.tt2 b/Open-ILS/src/templates/staff/share/print_templates/t_items_out.tt2 index 91e565ac8f..9d49e15c73 100644 --- a/Open-ILS/src/templates/staff/share/print_templates/t_items_out.tt2 +++ b/Open-ILS/src/templates/staff/share/print_templates/t_items_out.tt2 @@ -18,7 +18,7 @@ Fields include:
{{checkout.title}}
[% l('Barcode: [_1] Due: [_2]', '{{checkout.copy.barcode}}', - '{{checkout.circ.due_date | date:$root.egDateAndTimeFormat}}') %]
+ '{{checkout.circ.due_date | egDueDate:$root.egDateAndTimeFormat:checkout.circ.circ_lib:checkout.circ.duration}}') %]
diff --git a/Open-ILS/src/templates/staff/share/print_templates/t_renew.tt2 b/Open-ILS/src/templates/staff/share/print_templates/t_renew.tt2 index 3e17647100..9d4510fda8 100644 --- a/Open-ILS/src/templates/staff/share/print_templates/t_renew.tt2 +++ b/Open-ILS/src/templates/staff/share/print_templates/t_renew.tt2 @@ -17,7 +17,7 @@ Template for printing a renewal receipt. Fields include:
{{renewal.title}}
[% l('Barcode: [_1] Due: [_2]', '{{renewal.copy.barcode}}', - '{{renewal.circ.due_date | date:$root.egDateAndTimeFormat}}') %]
+ '{{renewal.circ.due_date | egDueDate:$root.egDateAndTimeFormat:renewal.circ.circ_lib:renewal.circ.duration}}') %]
diff --git a/Open-ILS/src/templates/staff/share/t_autogrid.tt2 b/Open-ILS/src/templates/staff/share/t_autogrid.tt2 index cf5ac460a2..0942d1cf3e 100644 --- a/Open-ILS/src/templates/staff/share/t_autogrid.tt2 +++ b/Open-ILS/src/templates/staff/share/t_autogrid.tt2 @@ -345,7 +345,7 @@ - {{itemFieldValue(item, col) | egGridValueFilter:col}} + {{itemFieldValue(item, col) | egGridValueFilter:col:item}} diff --git a/Open-ILS/web/js/ui/default/staff/Gruntfile.js b/Open-ILS/web/js/ui/default/staff/Gruntfile.js index e5941960fa..8a885a1f1e 100644 --- a/Open-ILS/web/js/ui/default/staff/Gruntfile.js +++ b/Open-ILS/web/js/ui/default/staff/Gruntfile.js @@ -38,7 +38,9 @@ module.exports = function(grunt) { 'node_modules/iframe-resizer/js/iframeResizer.contentWindow.min.js', 'node_modules/angular-order-object-by/src/ng-order-object-by.js', 'node_modules/lovefield/dist/lovefield.min.js', - 'node_modules/lovefield/dist/lovefield.min.js.map' + 'node_modules/lovefield/dist/lovefield.min.js.map', + 'node_modules/moment/min/moment-with-locales.min.js', + 'node_modules/moment-timezone/builds/moment-timezone-with-data.min.js' ] }] }, @@ -145,6 +147,8 @@ module.exports = function(grunt) { 'build/js/angular-tree-control.js', 'build/js/ngToast.min.js', 'build/js/lovefield.min.js', + 'bulid/js/moment-with-locales.min.js', + 'build/js/moment-timezone-with-data.min.js', // NOTE: OpenSRF must be installed // XXX: Should not be hard-coded '/openils/lib/javascript/JSON_v1.js', diff --git a/Open-ILS/web/js/ui/default/staff/admin/workstation/app.js b/Open-ILS/web/js/ui/default/staff/admin/workstation/app.js index 06a908d3c3..2cc025623f 100644 --- a/Open-ILS/web/js/ui/default/staff/admin/workstation/app.js +++ b/Open-ILS/web/js/ui/default/staff/admin/workstation/app.js @@ -449,6 +449,8 @@ function($scope , $q , egCore , ngToast) { checkins : [ { due_date : new Date().toISOString(), + circ_lib : 1, + duration : '7 days', target_copy : seed_copy, copy_barcode : seed_copy.barcode, call_number : seed_copy.call_number, @@ -460,6 +462,8 @@ function($scope , $q , egCore , ngToast) { { circ : { due_date : new Date().toISOString(), + circ_lib : 1, + duration : '7 days' }, copy : seed_copy, title : seed_record.title, diff --git a/Open-ILS/web/js/ui/default/staff/cat/item/app.js b/Open-ILS/web/js/ui/default/staff/cat/item/app.js index 8d73b9d456..5628a48be7 100644 --- a/Open-ILS/web/js/ui/default/staff/cat/item/app.js +++ b/Open-ILS/web/js/ui/default/staff/cat/item/app.js @@ -179,6 +179,8 @@ function(egCore , egCirc , $uibModal , $q , $timeout , $window , egConfirmDialog if (copyData.circ) { flatCopy._circ = egCore.idl.toHash(copyData.circ, true); flatCopy._circ_summary = egCore.idl.toHash(copyData.circ_summary, true); + flatCopy._circ_lib = copyData.circ.circ_lib(); + flatCopy._duration = copyData.circ.duration(); } flatCopy.index = service.index++; service.copies.unshift(flatCopy); diff --git a/Open-ILS/web/js/ui/default/staff/circ/services/circ.js b/Open-ILS/web/js/ui/default/staff/circ/services/circ.js index 3ec7822ff8..f219d57bb0 100644 --- a/Open-ILS/web/js/ui/default/staff/circ/services/circ.js +++ b/Open-ILS/web/js/ui/default/staff/circ/services/circ.js @@ -272,6 +272,9 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog, data.isbn = final_resp.evt[0].isbn; data.route_to = final_resp.evt[0].route_to; + if (payload.circ) data.duration = payload.circ.duration(); + if (payload.circ) data.circ_lib = payload.circ.circ_lib(); + // for checkin, the mbts lives on the main circ if (payload.circ && payload.circ.billable_transaction()) data.mbts = payload.circ.billable_transaction().summary(); diff --git a/Open-ILS/web/js/ui/default/staff/package.json b/Open-ILS/web/js/ui/default/staff/package.json index 91c88fa37a..cd3a9bb41a 100644 --- a/Open-ILS/web/js/ui/default/staff/package.json +++ b/Open-ILS/web/js/ui/default/staff/package.json @@ -17,6 +17,8 @@ "angular-tree-control": "~0.2.28", "angular-order-object-by": "rxfork/ngOrderObjectBy#npm", "lovefield": "*", + "moment": "*", + "moment-timezone": "*", "bootstrap": "~3.3.6", "grunt": "~0.4.4", "grunt-cli": "^0.1.13", diff --git a/Open-ILS/web/js/ui/default/staff/services/grid.js b/Open-ILS/web/js/ui/default/staff/services/grid.js index 96c4a9eda9..41e2d98663 100644 --- a/Open-ILS/web/js/ui/default/staff/services/grid.js +++ b/Open-ILS/web/js/ui/default/staff/services/grid.js @@ -64,6 +64,9 @@ angular.module('egGridMod', menuLabel : '@', dateformat : '@', // optional: passed down to egGridValueFilter + datecontext: '@', // optional: passed down to egGridValueFilter to choose TZ + datefilter: '@', // optional: passed down to egGridValueFilter to choose specialized date filters + dateonlyinterval: '@', // optional: passed down to egGridValueFilter to choose a "better" format // Hash of control functions. // @@ -183,7 +186,10 @@ angular.module('egGridMod', defaultToHidden : (features.indexOf('-display') > -1), defaultToNoSort : (features.indexOf('-sort') > -1), defaultToNoMultiSort : (features.indexOf('-multisort') > -1), - defaultDateFormat : $scope.dateformat + defaultDateFormat : $scope.dateformat, + defaultDateContext : $scope.datecontext, + defaultDateFilter : $scope.datefilter, + defaultDateOnlyInterval : $scope.dateonlyinterval }); $scope.canMultiSelect = (features.indexOf('-multiselect') == -1); @@ -989,7 +995,7 @@ angular.module('egGridMod', // bare value var val = grid.dataProvider.itemFieldValue(item, col); // filtered value (dates, etc.) - val = $filter('egGridValueFilter')(val, col); + val = $filter('egGridValueFilter')(val, col, item); csvStr += grid.csvDatum(val); csvStr += ','; } @@ -1093,6 +1099,9 @@ angular.module('egGridMod', flex : '@', // optional; default flex width align : '@', // optional; default alignment, left/center/right dateformat : '@', // optional: passed down to egGridValueFilter + datecontext: '@', // optional: passed down to egGridValueFilter to choose TZ + datefilter: '@', // optional: passed down to egGridValueFilter to choose specialized date filters + dateonlyinterval: '@', // optional: passed down to egGridValueFilter to choose a "better" format // if a field is part of an IDL object, but we are unable to // determine the class, because it's nested within a hash @@ -1179,6 +1188,7 @@ angular.module('egGridMod', cols.defaultToNoSort = args.defaultToNoSort; cols.defaultToNoMultiSort = args.defaultToNoMultiSort; cols.defaultDateFormat = args.defaultDateFormat; + cols.defaultDateContext = args.defaultDateContext; // resets column width, visibility, and sort behavior // Visibility resets to the visibility settings defined in the @@ -1385,6 +1395,9 @@ angular.module('egGridMod', multisortable : colSpec.multisortable, nonmultisortable : colSpec.nonmultisortable, dateformat : colSpec.dateformat, + datecontext : colSpec.datecontext, + datefilter : colSpec.datefilter, + dateonlyinterval : colSpec.dateonlyinterval, parentIdlClass : colSpec.parentIdlClass }; } @@ -1428,6 +1441,18 @@ angular.module('egGridMod', column.dateformat = cols.defaultDateFormat; } + if (cols.defaultDateOnlyInterval && ! column.dateonlyinterval) { + column.dateonlyinterval = cols.defaultDateOnlyInterval; + } + + if (cols.defaultDateContext && ! column.datecontext) { + column.datecontext = cols.defaultDateContext; + } + + if (cols.defaultDateFilter && ! column.datefilter) { + column.datefilter = cols.defaultDateFilter; + } + cols.columns.push(column); // Track which columns are visible by default in case we @@ -1910,12 +1935,12 @@ angular.module('egGridMod', * value. (Though we could manually translate instead..) * Others likely to follow... */ -.filter('egGridValueFilter', ['$filter', function($filter) { - return function(value, column) { - switch(column.datatype) { - case 'bool': +.filter('egGridValueFilter', ['$filter','egCore', function($filter,egCore) { + var GVF = function(value, column, item) { + switch(column.datatype) { + case 'bool': switch(value) { - // Browser will translate true/false for us + // Browser will translate true/false for us case 't' : case '1' : // legacy case true: @@ -1927,16 +1952,26 @@ angular.module('egGridMod', // value may be null, '', etc. default : return ''; } - case 'timestamp': - // canned angular date filter FTW - if (!column.dateformat) - column.dateformat = 'shortDate'; - return $filter('date')(value, column.dateformat); - case 'money': + case 'timestamp': + var interval = angular.isFunction(item[column.dateonlyinterval]) + ? item[column.dateonlyinterval]() + : item[column.dateonlyinterval]; + + var context = angular.isFunction(item[column.datecontext]) + ? item[column.datecontext]() + : item[column.datecontext]; + + var date_filter = column.datefilter || 'egOrgDateInContext'; + + return $filter(date_filter)(value, column.dateformat, context, interval); + case 'money': return $filter('currency')(value); - default: - return value; - } - } + default: + return value; + } + }; + + GVF.$stateful = true; + return GVF; }]); diff --git a/Open-ILS/web/js/ui/default/staff/services/ui.js b/Open-ILS/web/js/ui/default/staff/services/ui.js index dfa1a1ab9a..25f650b023 100644 --- a/Open-ILS/web/js/ui/default/staff/services/ui.js +++ b/Open-ILS/web/js/ui/default/staff/services/ui.js @@ -84,6 +84,135 @@ function($timeout , $parse) { }; }) +// 'egOrgDate' filter +// Uses moment.js and moment-timezone.js to put dates into the most appropriate +// timezone for a given (optional) org unit based on its lib.timezone setting +.filter('egOrgDate',['egCore', + function(egCore) { + + var formatMap = { + short : 'l LT', + medium : 'lll', + long : 'LLL', + full : 'LLLL', + + shortDate : 'l', + mediumDate : 'll', + longDate : 'LL', + fullDate : 'LL', + + shortTime : 'LT', + mediumTime : 'LTS' + }; + + var formatReplace = [ + [ /yyyy/g, 'YYYY' ], + [ /yy/g, 'YY' ], + [ /y/g, 'Y' ], + [ /ww/g, 'WW' ], + [ /w/g, 'W' ], + [ /dd/g, 'DD' ], + [ /d/g, 'D' ], + [ /sss/g, 'SSS' ], + [ /EEEE/g, 'dddd' ], + [ /EEE/g, 'ddd' ], + [ /Z/g, 'ZZ' ] + ]; + + var tzcache = {'*':null}; + + function eg_date_filter (date, format, ouID) { + var fmt = formatMap[format] || format; + angular.forEach(formatReplace, function (r) { + fmt = fmt.replace(r[0],r[1]); + }); + + var d; + if (ouID) { + if (angular.isObject(ouID)) { + if (angular.isFunction(ouID.id)) { + ouID = ouID.id(); + } else { + ouID = ouID.id; + } + } + + if (tzcache[ouID] && tzcache[ouID] !== '-') { + d = moment(date).tz(tzcache[ouID]); + } else { + + if (!tzcache[ouID]) { + tzcache[ouID] = '-'; + + egCore.org.settings('lib.timezone', ouID) + .then(function(s) { + tzcache[ouID] = s['lib.timezone'] || OpenSRF.tz; + }); + } + + d = moment(date); + } + } else { + d = moment(date); + } + + return d.isValid() ? d.format(fmt) : ''; + } + + eg_date_filter.$stateful = true; + + return eg_date_filter; +}]) + +// 'egOrgDateInContext' filter +// Uses the egOrgDate filter to make time and date location aware, and further +// modifies the format if one of [short, medium, long, full] to show only the +// date if the optional interval parameter is day-granular. This is +// particularly useful for due dates on circulations. +.filter('egOrgDateInContext',['$filter','egCore', + function($filter , egCore) { + + function eg_context_date_filter (date, format, orgID, interval) { + var fmt = format; + if (!fmt) fmt = 'shortDate'; + + // if this is a simple, one-word format, and it doesn't say "Date" in it... + if (['short','medium','long','full'].filter(function(x){return fmt == x}).length > 0 && interval) { + var secs = egCore.date.intervalToSeconds(interval); + if (secs !== null && secs % 86400 == 0) fmt += 'Date'; + } + + return $filter('egOrgDate')(date, fmt, orgID); + } + + eg_context_date_filter.$stateful = true; + + return eg_context_date_filter; +}]) + +// 'egDueDate' filter +// Uses the egOrgDateInContext filter to make time and date location aware, but +// only if the supplied interval is day-granular. This is as wrapper for +// egOrgDateInContext to be used for circulation due date /only/. +.filter('egDueDate',['$filter','egCore', + function($filter , egCore) { + + function eg_context_due_date_filter (date, format, orgID, interval) { + if (interval) { + var secs = egCore.date.intervalToSeconds(interval); + if (secs === null || secs % 86400 != 0) { + orgID = null; + interval = null; + } + } + return $filter('egOrgDateInContext')(date, format, orgID, interval); + } + + eg_context_due_date_filter.$stateful = true; + + return eg_context_due_date_filter; +}]) + /** * Progress Dialog. * diff --git a/Open-ILS/xul/staff_client/server/admin/closed_dates.js b/Open-ILS/xul/staff_client/server/admin/closed_dates.js index ced15776b1..c9815d007d 100644 --- a/Open-ILS/xul/staff_client/server/admin/closed_dates.js +++ b/Open-ILS/xul/staff_client/server/admin/closed_dates.js @@ -1,3 +1,8 @@ +dojo.require('fieldmapper.AutoIDL'); +dojo.require('fieldmapper.Fieldmapper'); +dojo.require('fieldmapper.OrgUtils'); +dojo.require('openils.Event'); + var myPackageDir = 'open_ils_staff_client'; var IAMXUL = true; var g = {}; var FETCH_CLOSED_DATES = 'open-ils.actor:open-ils.actor.org_unit.closed.retrieve.all'; var FETCH_CLOSED_DATE = 'open-ils.actor:open-ils.actor.org_unit.closed.retrieve'; @@ -10,6 +15,7 @@ var cdAllMultiDayTemplate; var cdTbody; var cdDateCache = {}; +var orgTZ = {}; var selectedStart; var selectedEnd; @@ -157,24 +163,51 @@ function cdBuild(r) { removeChildren(cdTbody); for( var d = 0; d < dates.length; d++ ) { var date = dates[d]; - var row = cdBuildRow( date ); - cdTbody.appendChild(row); + // super-closure! + (function (date) { + cdGetTZ(date.org_unit(), function () { + var row = cdBuildRow( date ); + cdTbody.appendChild(row); + }) + })(date); } } -function cdDateToHours(date) { - var date_obj = new Date(Date.parse(date)); - var hrs = date_obj.getHours(); - var mins = date_obj.getMinutes(); +function cdDateToHours(date, org) { + var date_obj = moment(date).tz(orgTZ[org]); + var hrs = date_obj.hours(); + var mins = date_obj.minutes(); // wee, strftime if (hrs < 10) hrs = '0' + hrs; if (mins < 10) mins = '0' + mins; return hrs + ':' + mins; } -function cdDateToDate(date) { - var date_obj = new Date(Date.parse(date)); - return date_obj.toLocaleDateString(); +function cdDateToDate(date, org) { + var date_obj = moment(date).tz(orgTZ[org]); + return date_obj.format('YYYY-MM-DD'); +} + +function cdGetTZ(org, callback) { + if (orgTZ[org]) { + if (callback) callback(); + return; + } + + fieldmapper.standardRequest( + [ 'open-ils.actor', + 'open-ils.actor.ou_setting.ancestor_default.batch'], + { async: true, + params: [org, ['lib.timezone'], SESSION], + oncomplete: function(r) { + var data = r.recv().content(); + if(e = openils.Event.parse(data)) + return alert(e); + orgTZ[org] = data['lib.timezone'].value || OpenSRF.tz; + if (callback) callback(); + } + } + ); } @@ -183,17 +216,17 @@ function cdBuildRow( date ) { cdDateCache[date.id()] = date; - var sh = cdDateToHours(date.close_start()); - var sd = cdDateToDate(date.close_start()); - var eh = cdDateToHours(date.close_end()); - var ed = cdDateToDate(date.close_end()); + var sh = cdDateToHours(date.close_start(), date.org_unit()); + var sd = cdDateToDate(date.close_start(), date.org_unit()); + var eh = cdDateToHours(date.close_end(), date.org_unit()); + var ed = cdDateToDate(date.close_end(), date.org_unit()); var row; var flesh = false; - if( sh == '00:00' && eh == '23:59' ) { + if( isTrue(date.full_day()) ) { - if( sd == ed ) { + if( !isTrue(date.multi_day()) ) { row = cdAllDayTemplate.cloneNode(true); $n(row, 'start_date').appendChild(text(sd)); @@ -220,10 +253,10 @@ function cdBuildRow( date ) { } function cdEditFleshRow(row, date) { - $n(row, 'start_time').appendChild(text(cdDateToHours(date.close_start()))); - $n(row, 'start_date').appendChild(text(cdDateToDate(date.close_start()))); - $n(row, 'end_time').appendChild(text(cdDateToHours(date.close_end()))); - $n(row, 'end_date').appendChild(text(cdDateToDate(date.close_end()))); + $n(row, 'start_time').appendChild(text(cdDateToHours(date.close_start(), date.org_unit()))); + $n(row, 'start_date').appendChild(text(cdDateToDate(date.close_start(), date.org_unit()))); + $n(row, 'end_time').appendChild(text(cdDateToHours(date.close_end(), date.org_unit()))); + $n(row, 'end_date').appendChild(text(cdDateToDate(date.close_end(), date.org_unit()))); } @@ -267,10 +300,28 @@ function cdVerifyTime(t) { return t && t.match(/\d{2}:\d{2}:\d{2}/); } -function cdDateStrToDate( str ) { +function cdDateStrToDate( str, org, callback ) { + if (!org) org = cdCurrentOrg(); - var date = new Date(); - var data = str.split(/ /); + if (callback) { // async mode + if (!orgTZ[org]) { // fetch then call again + return cdGetTZ(org, function () { + cdDateStrToDate( str, org, callback ); + }); + } else { + var d = cdDateStrToDate( str, org ); + return callback(d); + } + } + + var date; + if (orgTZ[org]) { + date = moment(new Date()).tz(orgTZ[org]); + } else { + date = moment(new Date()); + } + + var data = str.replace(/\s+/, 'T').split(/T/); var year = data[0]; var time = data[1]; @@ -284,15 +335,15 @@ function cdDateStrToDate( str ) { /* seed the date with day = 1, which is a valid day for any month. this prevents automatic date correction by the date code for days that fall outside of the current or target month */ - date.setDate(1); + date.date(1); - date.setFullYear(new Number(yeardata[0])); - date.setMonth(new Number(yeardata[1]) - 1); - date.setDate(new Number(yeardata[2])); + date.year(new Number(yeardata[0])); + date.month(new Number(yeardata[1]) - 1); + date.date(new Number(yeardata[2])); - date.setHours(new Number(timedata[0])); - date.setMinutes(new Number(timedata[1])); - date.setSeconds(new Number(timedata[2])); + date.hour(new Number(timedata[0])); + date.minute(new Number(timedata[1])); + date.second(new Number(timedata[2])); return date; } @@ -301,20 +352,25 @@ function cdNew() { var start; var end; + var full_day = 0; + var multi_day = 0; if( ! $('cd_edit_allday_row').className.match(/hide_me/) ) { var date = $('cd_edit_allday_start_date').value; - start = cdDateStrToDate(date + ' 00:00:00'); - end = cdDateStrToDate(date + ' 23:59:59'); + start = cdDateStrToDate(date + 'T00:00:00'); + end = cdDateStrToDate(date + 'T23:59:59'); + full_day = 1; } else if( ! $('cd_edit_allmultiday_row').className.match(/hide_me/) ) { var sdate = $('cd_edit_allmultiday_start_date').value; var edate = $('cd_edit_allmultiday_end_date').value; - start = cdDateStrToDate(sdate + ' 00:00:00'); - end = cdDateStrToDate(edate + ' 23:59:59'); + start = cdDateStrToDate(sdate + 'T00:00:00'); + end = cdDateStrToDate(edate + 'T23:59:59'); + full_day = 1; + multi_day = 1; } else { @@ -338,30 +394,30 @@ function cdNew() { etime += ':00'; } - start = cdDateStrToDate(sdate + ' ' + stime); - end = cdDateStrToDate(edate + ' ' + etime); + start = cdDateStrToDate(sdate + 'T' + stime); + end = cdDateStrToDate(edate + 'T' + etime); } - if (end.getTime() < start.getTime()) { + if (end.unix() < start.unix()) { alertId('cd_invalid_date_span'); return; } - cdCreate(start, end, $('cd_edit_note').value); + cdCreate(start, end, $('cd_edit_note').value, full_day, multi_day); } -function cdCreate(start, end, note) { +function cdCreate(start, end, note, full_day, multi_day) { if( $('cd_apply_all').checked ) { var list = cdGetOrgList(); for( var o = 0; o < list.length; o++ ) { var id = list[o].id(); - cdCreateOne( id, start, end, note, (id == cdCurrentOrg()) ); + cdCreateOne( id, start, end, note, full_day, multi_day, (id == cdCurrentOrg()) ); } } else { - cdCreateOne( cdCurrentOrg(), start, end, note, true ); + cdCreateOne( cdCurrentOrg(), start, end, note, full_day, multi_day, true ); } } @@ -386,25 +442,33 @@ function cdGetOrgList(org) { return list; } - -function cdCreateOne( org, start, end, note, refresh ) { +function cdCreateOne( org, start, end, note, full_day, multi_day, refresh ) { var date = new aoucd(); - date.close_start(start.toISOString()); - date.close_end(end.toISOString()); - date.org_unit(org); - date.reason(note); + // force TZ normalization + cdDateStrToDate(start.format('YYYY-MM-DD HH:mm:ss'), org, function (s) { + start = s; + cdDateStrToDate(end.format('YYYY-MM-DD HH:mm:ss'), org, function (e) { + end = e; + + date.close_start(start.toISOString()); + date.close_end(end.toISOString()); + date.org_unit(org); + date.reason(note); + date.full_day(full_day); + date.multi_day(multi_day); + + var req = new Request(CREATE_CLOSED_DATE, SESSION, date); + req.callback( + function(r) { + var res = r.getResultObject(); + if( checkILSEvent(res) ) alertILSEvent(res); + if(refresh) cdDrawRange(selectedStart, selectedEnd, true); + } + ); + req.send(); + }); + }); - var req = new Request(CREATE_CLOSED_DATE, SESSION, date); - req.callback( - function(r) { - var res = r.getResultObject(); - if( checkILSEvent(res) ) alertILSEvent(res); - if(refresh) cdDrawRange(selectedStart, selectedEnd, true); - } - ); - req.send(); } - - diff --git a/Open-ILS/xul/staff_client/server/admin/closed_dates.xhtml b/Open-ILS/xul/staff_client/server/admin/closed_dates.xhtml index 8eb074e4c1..0368fbc0eb 100644 --- a/Open-ILS/xul/staff_client/server/admin/closed_dates.xhtml +++ b/Open-ILS/xul/staff_client/server/admin/closed_dates.xhtml @@ -10,6 +10,11 @@ &staff.server.admin.closed_dates.title; + + + + + -- 2.43.2