[open-ils-commits] [GIT] Evergreen ILS branch master updated. e052cad09214b8f79618e9ddd66b1159a4a2e5cf

Evergreen Git git at git.evergreen-ils.org
Fri Aug 11 15:10:59 EDT 2017


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "Evergreen ILS".

The branch, master has been updated
       via  e052cad09214b8f79618e9ddd66b1159a4a2e5cf (commit)
       via  0f0203bb230fce6b44137e8203b999a5de326b46 (commit)
       via  6cf766a634e123f0b89eece2a0096a7b6d01fb7a (commit)
       via  d012c91ef7693f94e4bc836ad408f5bf6e5bf32a (commit)
       via  0ac027598e4d80960b8090bc45242bc2ad95cb67 (commit)
       via  e6c7aa0980416931b03d163a39d8c40274bcce38 (commit)
       via  51c0123d2cd43b8a64479df0527ef648aa62ced4 (commit)
       via  3c320f1a2a439329a0f2933af53a8170c12017bf (commit)
       via  f33efdc0a21ec4e0533e2cc380d94c63ed4bea6d (commit)
       via  da6075828f9bba157b6b0fd850d4d87e9b91cd06 (commit)
      from  b45f7d99938f02f39a37ebecbed5099dcb4c84b5 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit e052cad09214b8f79618e9ddd66b1159a4a2e5cf
Author: Bill Erickson <berickxx at gmail.com>
Date:   Fri Aug 11 14:25:24 2017 -0400

    LP#1705524 Stamping org timezones SQL
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/src/sql/Pg/002.schema.config.sql b/Open-ILS/src/sql/Pg/002.schema.config.sql
index f42d9e9..3679f20 100644
--- a/Open-ILS/src/sql/Pg/002.schema.config.sql
+++ b/Open-ILS/src/sql/Pg/002.schema.config.sql
@@ -90,7 +90,7 @@ CREATE TRIGGER no_overlapping_deps
     BEFORE INSERT OR UPDATE ON config.db_patch_dependencies
     FOR EACH ROW EXECUTE PROCEDURE evergreen.array_overlap_check ('deprecates');
 
-INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1053', :eg_version); -- kmlussier/miker
+INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1054', :eg_version); -- miker/berick
 
 CREATE TABLE config.bib_source (
 	id		SERIAL	PRIMARY KEY,
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.tz_org_setting.sql b/Open-ILS/src/sql/Pg/upgrade/1054.data.tz_org_setting.sql
similarity index 97%
rename from Open-ILS/src/sql/Pg/upgrade/XXXX.data.tz_org_setting.sql
rename to Open-ILS/src/sql/Pg/upgrade/1054.data.tz_org_setting.sql
index 22b4c9a..bc33d7f 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.tz_org_setting.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/1054.data.tz_org_setting.sql
@@ -1,5 +1,7 @@
 BEGIN;
 
+SELECT evergreen.upgrade_deps_block_check('1054', :eg_version);
+
 INSERT into config.org_unit_setting_type
 ( name, grp, label, description, datatype ) VALUES
 

commit 0f0203bb230fce6b44137e8203b999a5de326b46
Author: Mike Rylander <mrylander at gmail.com>
Date:   Thu Aug 3 14:18:30 2017 -0400

    LP#1705524: Minor cleanup in prep for committing
    
    1. Provide an admin-friendly summary of changes at the top of the release notes.
    2. Change quoting of optional post-upgrade SQL echoing in the upgrade script.
    3. Fix preexisting issue with variable interpolation inside an alert modal.
    4. Protect against null or empty date fields when formatting.
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    
    5. Added if (date == 'now') check.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

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
index 4d7f24c..22b4c9a 100644
--- 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
@@ -47,31 +47,31 @@ $$ LANGUAGE PLPGSQL;
 COMMIT;
 
 \qecho The following query will adjust all historical, unaged circulations so
-\qecho that if their due date field pushed to the end of the day, it is done
-\qecho in the circulating library's time zone, and not the server time zone.
+\qecho that if their due date field is pushed to the end of the day, it is done
+\qecho in the circulating library'''s time zone, and not the server time zone.
 \qecho 
 \qecho It is safe to run this after any change to library time zones.
 \qecho 
 \qecho Running this is not required, as no code before this change has
-\qecho depended on the time string of '23:59:59'.  It is also not necessary
+\qecho depended on the time string of '''23:59:59'''.  It is also not necessary
 \qecho if all of your libraries are in the same time zone, and that time zone
-\qecho is the same as the databases configured time zone.
+\qecho is the same as the database'''s configured time zone.
 \qecho 
-\qecho DO $$
-\qecho declare
-\qecho     new_tz  text;
-\qecho     ou_id   int;
-\qecho begin
-\qecho     for ou_id in select id from actor.org_unit loop
-\qecho         for new_tz in select oils_json_to_text(value) from actor.org_unit_ancestor_setting('lib.timezone',ou_id) loop
-\qecho             if new_tz is not null then
-\qecho                 update  action.circulation
-\qecho                   set   due_date = (due_date::timestamp || ' ' || new_tz)::timestamptz
-\qecho                   where circ_lib = ou_id
-\qecho                         and substring((due_date at time zone new_tz)::time::text from 1 for 8) <> '23:59:59';
-\qecho             end if;
-\qecho         end loop;
-\qecho     end loop;
-\qecho end;
-\qecho $$;
+\qecho 'DO $$'
+\qecho 'declare'
+\qecho '    new_tz  text;'
+\qecho '    ou_id   int;'
+\qecho 'begin'
+\qecho '    for ou_id in select id from actor.org_unit loop'
+\qecho '        for new_tz in select oils_json_to_text(value) from actor.org_unit_ancestor_setting('''lib.timezone''',ou_id) loop'
+\qecho '            if new_tz is not null then'
+\qecho '                update  action.circulation'
+\qecho '                  set   due_date = (due_date::timestamp || ''' ''' || new_tz)::timestamptz'
+\qecho '                  where circ_lib = ou_id'
+\qecho '                        and substring((due_date at time zone new_tz)::time::text from 1 for 8) <> '''23:59:59''';'
+\qecho '            end if;'
+\qecho '        end loop;'
+\qecho '    end loop;'
+\qecho 'end;'
+\qecho '$$;'
 \qecho 
diff --git a/Open-ILS/src/templates/staff/circ/share/t_circ_exists_dialog.tt2 b/Open-ILS/src/templates/staff/circ/share/t_circ_exists_dialog.tt2
index d4b4aeb..552de04 100644
--- a/Open-ILS/src/templates/staff/circ/share/t_circ_exists_dialog.tt2
+++ b/Open-ILS/src/templates/staff/circ/share/t_circ_exists_dialog.tt2
@@ -8,13 +8,13 @@
       </div>
       <div class="modal-body">
         <div ng-if="sameUser">
-          [% |l("{{circDate | date:$root.egDateFormat}}") %]
+          [% |l('{{circDate | date:$root.egDateFormat}}') %]
           There is an open circulation on the requested item.  
           This item was already checked out to this user on [_1].
           [% END %]
         </div>
         <div ng-if="!sameUser">
-          [% |l("{{circDate | date:$root.egDateFormat}}") %]
+          [% |l('{{circDate | date:$root.egDateFormat}}') %]
           There is an open circulation on the requested item.  
           This copy was checked out by another patron on [_1].
           [% END %]
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 9c4fc7e..f6dc3a0 100644
--- a/Open-ILS/web/js/ui/default/staff/services/ui.js
+++ b/Open-ILS/web/js/ui/default/staff/services/ui.js
@@ -119,6 +119,11 @@ function($timeout , $parse) {
     ];
 
     return function (date, format, tz) {
+        if (!date) return '';
+
+        if (date == 'now') 
+            date = new Date().toISOString();
+
         if (format) {
             var fmt = formatMap[format] || format;
             angular.forEach(formatReplace, function (r) {
diff --git a/docs/RELEASE_NOTES_NEXT/Circulation/due_date_timezones.adoc b/docs/RELEASE_NOTES_NEXT/Circulation/due_date_timezones.adoc
index f534879..cc28100 100644
--- a/docs/RELEASE_NOTES_NEXT/Circulation/due_date_timezones.adoc
+++ b/docs/RELEASE_NOTES_NEXT/Circulation/due_date_timezones.adoc
@@ -1,5 +1,53 @@
 Honor timezone of the acting library
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Summary
++++++++
+
+* Display day-granular due dates in the circulating library's timezone.
+* Only display the date portion of the due date for day-granular circulations.
+* Display the full timestamp, in the client's timezone rather than the circulation library's, for hourly circulations.
+* Provide infrastructure for more advanced formatting of timestamps.
+* Override the built-in AngularJS date filter with an implementation that uses moment.js, providing consistency and better standards compliance.
+
+Upgrade note
+++++++++++++
+
+The following query will adjust all historical, unaged circulations so
+that if their due date field is pushed to the end of the day, it is done
+in the circulating library's time zone, and not the server time zone.
+
+It is safe to run this after any change to library time zones.
+
+Running this is not required, as no code before this change has
+depended on the time string of '23:59:59'.  It is also not necessary
+if all of your libraries are in the same time zone, and that time zone
+is the same as the database's configured time zone.
+
+[source,sql]
+----
+DO $$
+declare
+    new_tz  text;
+    ou_id   int;
+begin
+    for ou_id in select id from actor.org_unit loop
+        for new_tz in select oils_json_to_text(value) from actor.org_unit_ancestor_setting('lib.timezone',ou_id) loop
+            if new_tz is not null then
+                update  action.circulation
+                  set   due_date = (due_date::timestamp || ' ' || new_tz)::timestamptz
+                  where circ_lib = ou_id
+                        and substring((due_date at time zone new_tz)::time::text from 1 for 8) <> '23:59:59';
+            end if;
+        end loop;
+    end loop;
+end;
+$$;
+----
+
+Details
++++++++
+
 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.

commit 6cf766a634e123f0b89eece2a0096a7b6d01fb7a
Author: Bill Erickson <berickxx at gmail.com>
Date:   Thu Aug 3 11:43:56 2017 -0400

    LP#1705524 Grid value filter defaults to null
    
    Avoid use of Perl undef in JS.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

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 27344c0..1811941 100644
--- a/Open-ILS/web/js/ui/default/staff/services/grid.js
+++ b/Open-ILS/web/js/ui/default/staff/services/grid.js
@@ -1940,7 +1940,7 @@ angular.module('egGridMod',
         var list = path.split('.');
         for (var part in path) {
             if (obj[path]) obj = obj[path]
-            else return undef;
+            else return null;
         }
         return obj;
     }

commit d012c91ef7693f94e4bc836ad408f5bf6e5bf32a
Author: Bill Erickson <berickxx at gmail.com>
Date:   Thu Aug 3 11:35:31 2017 -0400

    LP#1705524 Closed dates editor handles no timezones
    
    Fix issue in closed dates editor where it failed creating new closed
    dates when an org unit had no value applied for the lib.timezone org
    unit setting.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

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 c9815d0..94e94e7 100644
--- a/Open-ILS/xul/staff_client/server/admin/closed_dates.js
+++ b/Open-ILS/xul/staff_client/server/admin/closed_dates.js
@@ -203,7 +203,11 @@ function cdGetTZ(org, callback) {
                 var data = r.recv().content();
                 if(e = openils.Event.parse(data))
                     return alert(e);
-                orgTZ[org] = data['lib.timezone'].value || OpenSRF.tz;
+                if (data['lib.timezone'] && data['lib.timezone'].value) {
+                    orgTZ[org] = data['lib.timezone'].value;
+                } else {
+                    orgTZ[org] = OpenSRF.tz;
+                }
                 if (callback) callback();
             }
         }

commit 0ac027598e4d80960b8090bc45242bc2ad95cb67
Author: Bill Erickson <berickxx at gmail.com>
Date:   Thu Aug 3 11:25:56 2017 -0400

    LP#1705524 Load workstation org timezone in startup
    
    Piggy-back on the batch org setting call made during page startup to
    load / pre-cache the timezone setting for the workstation org unit.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/web/js/ui/default/staff/services/startup.js b/Open-ILS/web/js/ui/default/staff/services/startup.js
index ea114d2..038eb2d 100644
--- a/Open-ILS/web/js/ui/default/staff/services/startup.js
+++ b/Open-ILS/web/js/ui/default/staff/services/startup.js
@@ -32,7 +32,8 @@ function($q,  $rootScope,  $location,  $window,  egIDL,  egAuth,  egEnv , egOrg
         function() {
             return egOrg.settings([
                 'webstaff.format.dates',
-                'webstaff.format.date_and_time'
+                'webstaff.format.date_and_time',
+                'lib.timezone'
             ]).then(
                 function(set) {
                     $rootScope.egDateFormat = 

commit e6c7aa0980416931b03d163a39d8c40274bcce38
Author: Mike Rylander <mrylander at gmail.com>
Date:   Wed Aug 2 16:56:20 2017 -0400

    LP#1705524: Override angular date filter
    
    Here we'll use moment.js to format all dates that want to use the angular date
    filter, for consistency and standards compliance.  The primary benefit is the
    ability to use a proper timezone (region) rather than just a simple GMT offset.
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

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 25f650b..9c4fc7e 100644
--- a/Open-ILS/web/js/ui/default/staff/services/ui.js
+++ b/Open-ILS/web/js/ui/default/staff/services/ui.js
@@ -84,11 +84,10 @@ 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) {
+// 'date' filter
+// Overriding the core angular date filter with a moment-js based one for
+// better timezone and formatting support.
+.filter('date',function() {
 
     var formatMap = {
         short  : 'l LT',
@@ -119,15 +118,31 @@ function($timeout , $parse) {
         [ /Z/g,    'ZZ'   ]
     ];
 
-    var tzcache = {'*':null};
+    return function (date, format, tz) {
+        if (format) {
+            var fmt = formatMap[format] || format;
+            angular.forEach(formatReplace, function (r) {
+                fmt = fmt.replace(r[0],r[1]);
+            });
+        }
 
-    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 = moment(date);
+        if (tz && tz !== '-') d.tz(tz);
 
-        var d;
+        return d.isValid() ? d.format(fmt) : '';
+    }
+
+})
+
+// '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',['$filter','egCore',
+             function($filter , egCore) {
+
+    var tzcache = {};
+
+    function eg_date_filter (date, fmt, ouID) {
         if (ouID) {
             if (angular.isObject(ouID)) {
                 if (angular.isFunction(ouID.id)) {
@@ -137,26 +152,16 @@ function($timeout , $parse) {
                 }
             }
     
-            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);
+            if (!tzcache[ouID]) {
+                tzcache[ouID] = '-';
+                egCore.org.settings('lib.timezone', ouID)
+                .then(function(s) {
+                    tzcache[ouID] = s['lib.timezone'] || OpenSRF.tz;
+                });
             }
-        } else {
-            d = moment(date);
         }
 
-        return d.isValid() ? d.format(fmt) : '';
+        return $filter('date')(date, fmt, tzcache[ouID]);
     }
 
     eg_date_filter.$stateful = true;

commit 51c0123d2cd43b8a64479df0527ef648aa62ced4
Author: Mike Rylander <mrylander at gmail.com>
Date:   Mon Jul 24 10:27:09 2017 -0400

    LP#1705524 Adding timezone release note
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Tina Ji <tji at sitka.bclibraries.ca>

diff --git a/docs/RELEASE_NOTES_NEXT/Circulation/due_date_timezones.adoc b/docs/RELEASE_NOTES_NEXT/Circulation/due_date_timezones.adoc
new file mode 100644
index 0000000..f534879
--- /dev/null
+++ b/docs/RELEASE_NOTES_NEXT/Circulation/due_date_timezones.adoc
@@ -0,0 +1,55 @@
+Honor timezone of the acting library
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+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.
+
+Now that due dates are globally stored in the configured timezone of the
+circulating library, the automatic adjustment to day-granular due dates needs
+to take those timezones into account.
+
+An optional SQL command is provided by the upgrade script to retroactively
+adjust existing due dates after library configuration is complete.
+
+This work, as with 1485374, was funded by SITKA, and we thank them for their
+partnership in making this happen!
+

commit 3c320f1a2a439329a0f2933af53a8170c12017bf
Author: Mike Rylander <mrylander at gmail.com>
Date:   Mon Jul 24 10:13:31 2017 -0400

    LP#1705524: Adjust day-granular due date pushing
    
    Now that due dates are globally stored in the configured timezone of the
    circulating library, the automatic adjustment to day-granular due dates needs
    to take those timezones into account.
    
    An optional SQL command is provided by the upgrade script to retroactively
    adjust existing due dates after library configuration is complete.
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Tina Ji <tji at sitka.bclibraries.ca>

diff --git a/Open-ILS/src/sql/Pg/090.schema.action.sql b/Open-ILS/src/sql/Pg/090.schema.action.sql
index 3de949e..18604b8 100644
--- a/Open-ILS/src/sql/Pg/090.schema.action.sql
+++ b/Open-ILS/src/sql/Pg/090.schema.action.sql
@@ -173,9 +173,20 @@ CREATE TRIGGER mat_summary_change_tgr AFTER UPDATE ON action.circulation FOR EAC
 CREATE TRIGGER mat_summary_remove_tgr AFTER DELETE ON action.circulation FOR EACH ROW EXECUTE PROCEDURE money.mat_summary_delete ();
 
 CREATE OR REPLACE FUNCTION action.push_circ_due_time () RETURNS TRIGGER AS $$
+DECLARE
+    proper_tz TEXT := COALESCE(
+        oils_json_to_text((
+            SELECT value
+              FROM  actor.org_unit_ancestor_setting('lib.timezone',NEW.circ_lib)
+              LIMIT 1
+        )),
+        CURRENT_SETTING('timezone')
+    );
 BEGIN
-    IF (EXTRACT(EPOCH FROM NEW.duration)::INT % EXTRACT(EPOCH FROM '1 day'::INTERVAL)::INT) = 0 THEN
-        NEW.due_date = (NEW.due_date::DATE + '1 day'::INTERVAL - '1 second'::INTERVAL)::TIMESTAMPTZ;
+
+    IF (EXTRACT(EPOCH FROM NEW.duration)::INT % EXTRACT(EPOCH FROM '1 day'::INTERVAL)::INT) = 0 -- day-granular duration
+        AND SUBSTRING((NEW.due_date AT TIME ZONE proper_tz)::TIME::TEXT FROM 1 FOR 8) <> '23:59:59' THEN -- has not yet been pushed
+        NEW.due_date = ((NEW.due_date AT TIME ZONE proper_tz)::DATE + '1 day'::INTERVAL - '1 second'::INTERVAL) || ' ' || proper_tz;
     END IF;
 
     RETURN NEW;
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
index 53b1b1a..4d7f24c 100644
--- 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
@@ -23,5 +23,55 @@ UPDATE actor.org_unit_closed SET full_day = TRUE
         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';
 
+CREATE OR REPLACE FUNCTION action.push_circ_due_time () RETURNS TRIGGER AS $$
+DECLARE
+    proper_tz TEXT := COALESCE(
+        oils_json_to_text((
+            SELECT value
+              FROM  actor.org_unit_ancestor_setting('lib.timezone',NEW.circ_lib)
+              LIMIT 1
+        )),
+        CURRENT_SETTING('timezone')
+    );
+BEGIN
+
+    IF (EXTRACT(EPOCH FROM NEW.duration)::INT % EXTRACT(EPOCH FROM '1 day'::INTERVAL)::INT) = 0 -- day-granular duration
+        AND SUBSTRING((NEW.due_date AT TIME ZONE proper_tz)::TIME::TEXT FROM 1 FOR 8) <> '23:59:59' THEN -- has not yet been pushed
+        NEW.due_date = ((NEW.due_date AT TIME ZONE proper_tz)::DATE + '1 day'::INTERVAL - '1 second'::INTERVAL) || ' ' || proper_tz;
+    END IF;
+
+    RETURN NEW;
+END;
+$$ LANGUAGE PLPGSQL;
+
 COMMIT;
 
+\qecho The following query will adjust all historical, unaged circulations so
+\qecho that if their due date field pushed to the end of the day, it is done
+\qecho in the circulating library's time zone, and not the server time zone.
+\qecho 
+\qecho It is safe to run this after any change to library time zones.
+\qecho 
+\qecho Running this is not required, as no code before this change has
+\qecho depended on the time string of '23:59:59'.  It is also not necessary
+\qecho if all of your libraries are in the same time zone, and that time zone
+\qecho is the same as the databases configured time zone.
+\qecho 
+\qecho DO $$
+\qecho declare
+\qecho     new_tz  text;
+\qecho     ou_id   int;
+\qecho begin
+\qecho     for ou_id in select id from actor.org_unit loop
+\qecho         for new_tz in select oils_json_to_text(value) from actor.org_unit_ancestor_setting('lib.timezone',ou_id) loop
+\qecho             if new_tz is not null then
+\qecho                 update  action.circulation
+\qecho                   set   due_date = (due_date::timestamp || ' ' || new_tz)::timestamptz
+\qecho                   where circ_lib = ou_id
+\qecho                         and substring((due_date at time zone new_tz)::time::text from 1 for 8) <> '23:59:59';
+\qecho             end if;
+\qecho         end loop;
+\qecho     end loop;
+\qecho end;
+\qecho $$;
+\qecho 

commit f33efdc0a21ec4e0533e2cc380d94c63ed4bea6d
Author: Mike Rylander <mrylander at gmail.com>
Date:   Thu Jul 20 17:52:38 2017 -0400

    LP#1705524: Use the new grid configuration for date format in the bills list
    
    The bills list in the patron interface predated any grid date formatting, so
    we bring it into the modern world.
    
    This also enhances the grid autoformatting for dates to support both flattened
    and dot-pathed item layout.
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Tina Ji <tji at sitka.bclibraries.ca>

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 3d4863c..840b83d 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
@@ -100,9 +100,7 @@
     path='grocery.billing_location.shortname' required hidden> </eg-grid-field>
   <eg-grid-field path='circulation.circ_lib' required hidden></eg-grid-field>
   <eg-grid-field path='circulation.duration' required hidden></eg-grid-field>
-  <eg-grid-field path='circulation.due_date' required hidden>
-    {{ circulation.due_date | egDueDate:$root.egDateAndTimeFormat:circulation.circ_lib:circulation.duration}}
-  </eg-grid-field>
+  <eg-grid-field path='circulation.due_date' dateonlyinterval="circulation.duration" datecontext="circulation.circ_lib" required hidden></eg-grid-field>
   <eg-grid-field path='circulation.*' hidden> </eg-grid-field>
   <eg-grid-field label="[% l('Checkout / Renewal Library') %]"
     path='circulation.circ_lib.shortname' required hidden> </eg-grid-field>
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 41e2d98..27344c0 100644
--- a/Open-ILS/web/js/ui/default/staff/services/grid.js
+++ b/Open-ILS/web/js/ui/default/staff/services/grid.js
@@ -1936,6 +1936,15 @@ angular.module('egGridMod',
  * Others likely to follow...
  */
 .filter('egGridValueFilter', ['$filter','egCore', function($filter,egCore) {
+    function traversePath(obj,path) {
+        var list = path.split('.');
+        for (var part in path) {
+            if (obj[path]) obj = obj[path]
+            else return undef;
+        }
+        return obj;
+    }
+
     var GVF = function(value, column, item) {
         switch(column.datatype) {
             case 'bool':
@@ -1957,10 +1966,16 @@ angular.module('egGridMod',
                     ? item[column.dateonlyinterval]()
                     : item[column.dateonlyinterval];
 
+                if (column.dateonlyinterval && !interval) // try it as a dotted path
+                    interval = traversePath(item, column.dateonlyinterval);
+
                 var context = angular.isFunction(item[column.datecontext])
                     ? item[column.datecontext]()
                     : item[column.datecontext];
 
+                if (column.datecontext && !context) // try it as a dotted path
+                    context = traversePath(item, column.datecontext);
+
                 var date_filter = column.datefilter || 'egOrgDateInContext';
 
                 return $filter(date_filter)(value, column.dateformat, context, interval);

commit da6075828f9bba157b6b0fd850d4d87e9b91cd06
Author: Mike Rylander <mrylander at gmail.com>
Date:   Wed Jun 21 14:03:29 2017 -0400

    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 <mrylander at gmail.com>
    Signed-off-by: Tina Ji <tji at sitka.bclibraries.ca>

diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml
index 43aed7d..f2abaa7 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
 			<field name="id" reporter:datatype="id" />
 			<field name="org_unit" reporter:datatype="org_unit"/>
 			<field name="reason" reporter:datatype="text"/>
+			<field name="full_day" reporter:datatype="bool"/>
+			<field name="multi_day" reporter:datatype="bool"/>
 		</fields>
 		<links>
 			<link field="org_unit" reltype="has_a" key="id" map="" class="aou"/>
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm
index 790425e..a56443d 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 b22fee4..c7caea7 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 4572dcc..a4047f7 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 cf1f7e9..199ff7a 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 90a351d..d96f83d 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 130b217..e95d94c 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 0000000..53b1b1a
--- /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 d7f2175..6940bde 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); %]
                         </td>
                         <td>
-                            [% 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); %]
                         </td>
                         <td>
                             [% 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 91ebeb0..92ae989 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 %]
                         </td>
                         [%
-                            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' : '';
                         %]
                         <td name="due_date" class='[% due_class %]'>
diff --git a/Open-ILS/src/templates/opac/myopac/main.tt2 b/Open-ILS/src/templates/opac/myopac/main.tt2
index 9dc672e..91133fc 100644
--- a/Open-ILS/src/templates/opac/myopac/main.tt2
+++ b/Open-ILS/src/templates/opac/myopac/main.tt2
@@ -72,8 +72,9 @@
                     </td>
                     <td name='myopac_circ_trans_due'>
                         [% 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 %]
                     </td>
                     <td name='myopac_circ_trans_finished'>
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 774f188..6be5346 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;
     <td>[% bib.target_copy.barcode | html %]</td>
     <td>[% copy_info.copy_location | html %]</td>
     <td>[% copy_info.copy_status | html %]</td>
-    <td>[% copy_info.due_date | html %]</td>
+    <td>[% date.format(ctx.parse_datetime(copy_info.due_date, copy_info.circ_circ_lib),DATE_FORMAT) %]</td>
 </tr>
    [%- END; # FOREACH peer
 END; # FOREACH bib
@@ -217,7 +217,7 @@ END; # FOREACH bib
             <td>[%
                 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 82b662e..1880f66 100644
--- a/Open-ILS/src/templates/staff/base_js.tt2
+++ b/Open-ILS/src/templates/staff/base_js.tt2
@@ -19,6 +19,8 @@
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/build/js/iframeResizer.min.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/build/js/ng-order-object-by.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/build/js/lovefield.min.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/build/js/moment-with-locales.min.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/build/js/moment-timezone-with-data.min.js"></script>
 
 <!-- IDL / opensrf (network) -->
 <script src="[% ctx.media_prefix %]/js/dojo/opensrf/JSON_v1.js"></script>
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 3cab22b..422830f 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 @@
     <div class="flex-cell">[% l('Check Out Date') %]</div>
     <div class="flex-cell well">{{circ.xact_start() | date:egDateAndTimeFormat}}</div>
     <div class="flex-cell">[% l('Due Date') %]</div>
-    <div class="flex-cell well">{{circ.due_date() | date:egDateAndTimeFormat}}</div>
+    <div class="flex-cell well">{{circ.due_date() | egDueDate:egDateAndTimeFormat:circ.circ_lib():circ.duration()}}</div>
     <div class="flex-cell">[% l('Stop Fines Time') %]</div>
     <div class="flex-cell well">{{circ.stop_fines_time() | date:egDateAndTimeFormat}}</div>
     <div class="flex-cell">[% l('Checkin Time') %]</div>
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 6afb87a..eb584c9 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 @@
   <eg-grid-field label="[% l('Alert Message') %]"  path='alert_message' visible></eg-grid-field>
   <eg-grid-field label="[% l('Barcode') %]"        path='barcode' visible></eg-grid-field>
   <eg-grid-field label="[% l('Call Number') %]"    path="call_number.label" visible></eg-grid-field>
-  <eg-grid-field label="[% l('Due Date') %]"       path="_circ.due_date" datatype="timestamp" visible></eg-grid-field>
+  <eg-grid-field label="[% l('Due Date') %]"       path="_circ.due_date" datecontext="_circ_lib" dateonlyinterval="_duration" datatype="timestamp" visible></eg-grid-field>
 
   <eg-grid-field label="[% l('Location') %]"       path="location.name" visible></eg-grid-field>
   <eg-grid-field label="[% l('Copy Status') %]"    path="status.name" visible></eg-grid-field>
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 4f03835..4b9abe3 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 @@
     <div class="flex-cell well">{{copy.call_number().label()}}</div>
 
     <div class="flex-cell">[% l('Due Date') %]</div>
-    <div class="flex-cell well">{{circ.due_date() | date:egDateAndTimeFormat}}</div>
+    <div class="flex-cell well">{{circ.due_date() | egDueDate:egDateAndTimeFormat:circ.circ_lib():circ.duration()}}</div>
   </div>
 
   <div class="flex-row">
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 0ccf9e4..0066cb3 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 @@
   </eg-grid-field>
 
   <eg-grid-field label="[% l('Due Date') %]"    
-    path='circ.due_date' datatype="timestamp" hidden></eg-grid-field>
+    path='circ.due_date' dateonlyinterval="duration" datecontext="circ_lib" datatype="timestamp" hidden></eg-grid-field>
 
   <eg-grid-field label="[% l('Author') %]"      
     path="author" hidden></eg-grid-field>
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 68511ad..3d4863c 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 @@
   <eg-grid-field path='grocery.*' hidden> </eg-grid-field>
   <eg-grid-field label="[% l('Billing Location') %]"
     path='grocery.billing_location.shortname' required hidden> </eg-grid-field>
+  <eg-grid-field path='circulation.circ_lib' required hidden></eg-grid-field>
+  <eg-grid-field path='circulation.duration' required hidden></eg-grid-field>
+  <eg-grid-field path='circulation.due_date' required hidden>
+    {{ circulation.due_date | egDueDate:$root.egDateAndTimeFormat:circulation.circ_lib:circulation.duration}}
+  </eg-grid-field>
   <eg-grid-field path='circulation.*' hidden> </eg-grid-field>
   <eg-grid-field label="[% l('Checkout / Renewal Library') %]"
     path='circulation.circ_lib.shortname' required hidden> </eg-grid-field>
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 57f4b45..9343c92 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"></eg-grid-field>
 
   <eg-grid-field label="[% l('Due Date') %]"    
-    path='circ.due_date' datatype="timestamp"></eg-grid-field>
+    path='circ.due_date' datecontext="circ_lib" dateonlyinterval="duration" datatype="timestamp"></eg-grid-field>
 
   <eg-grid-field label="[% l('Family Name') %]"    
     path='au.family_name'></eg-grid-field>
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 c80c4b6..4c354fd 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 @@
   <eg-grid-field label="[% l('Item Type') %]" path='item_type.name'></eg-grid-field>
   <eg-grid-field label="[% l('Checkout Library') %]" path='circ_lib.shortname'></eg-grid-field>
   <eg-grid-field label="[% l('Checkout Date') %]" path='circ_time' datatype="timestamp"></eg-grid-field>
-  <eg-grid-field label="[% l('Due Date') %]" path='duedate' datatype="timestamp"></eg-grid-field>
+  <eg-grid-field label="[% l('Due Date') %]" path='duedate' dateformat="shortDate" datatype="timestamp"></eg-grid-field>
   <eg-grid-field label="[% l('Checkout Staff') %]" path='staff.usrname'></eg-grid-field>
 </eg-grid>
 
@@ -80,7 +80,7 @@
       {{item.target_copy().barcode()}}
     </a>
   </eg-grid-field>
-  <eg-grid-field label="[% l('Due Date') %]" path='due_date' datatype="timestamp"></eg-grid-field>
+  <eg-grid-field label="[% l('Due Date') %]" path='due_date' datefilter="egDueDate" dateonlyinterval="duration" datecontext="circ_lib" datatype="timestamp"></eg-grid-field>
   <eg-grid-field label="[% l('Workstation') %]" path='workstation.name'></eg-grid-field>
   <eg-grid-field label="[% l('Checkin Workstation') %]" path='checkin_workstation.name'></eg-grid-field>
   <eg-grid-field label="[% l('Checkout / Renewal Library') %]" path='circ_lib.shortname'></eg-grid-field>
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 5f13cc7..032a726 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 @@
   <div class="col-md-2 strong-text">[% l('Total Billed') %]</div>
   <div class="col-md-2">{{xact.summary().balance_owed() | currency}}</div>
   <div class="col-md-2 strong-text">[% l('Due Date') %]</div>
-  <div class="col-md-2">{{xact.circulation().due_date() | date:$root.egDateAndTimeFormat}}</div>
+  <div class="col-md-2">{{xact.circulation().due_date() | egDueDate:$root.egDateAndTimeFormat:xact.circulation().circ_lib():xact.circulation().duration()}}</div>
 </div>
 <div class="row">
   <div class="col-md-2 strong-text">[% l('Finish') %]</div>
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 7b0fdb7..a84373a 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"></eg-grid-field>
 
   <eg-grid-field label="[% l('Due Date') %]"    
-    path='circ.due_date' datatype="timestamp"></eg-grid-field>
+    path='circ.due_date' datecontext="circ_lib" dateonlyinterval="duration" datatype="timestamp"></eg-grid-field>
 
   <eg-grid-field label="[% l('Family Name') %]"    
     path='au.family_name'></eg-grid-field>
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 808f52c..327a05e 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:
       <div>{{checkout.title}}</div>
       <div>[% l('Barcode: [_1] Due: [_2]', 
         '{{checkout.copy.barcode}}',
-        '{{checkout.circ.due_date | date:$root.egDateAndTimeFormat}}') %]</div>
+        '{{checkout.circ.due_date | egDueDate:$root.egDateAndTimeFormat:checkout.circ.circ_lib:checkout.circ.duration}}') %]</div>
     </li>
   </ol>
   <hr/>
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 91e565a..9d49e15 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:
       <div>{{checkout.title}}</div>
       <div>[% l('Barcode: [_1] Due: [_2]', 
         '{{checkout.copy.barcode}}',
-        '{{checkout.circ.due_date | date:$root.egDateAndTimeFormat}}') %]</div>
+        '{{checkout.circ.due_date | egDueDate:$root.egDateAndTimeFormat:checkout.circ.circ_lib:checkout.circ.duration}}') %]</div>
     </li>
   </ol>
   <hr/>
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 3e17647..9d4510f 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:
       <div>{{renewal.title}}</div>
       <div>[% l('Barcode: [_1] Due: [_2]', 
         '{{renewal.copy.barcode}}',
-        '{{renewal.circ.due_date | date:$root.egDateAndTimeFormat}}') %]</div>
+        '{{renewal.circ.due_date | egDueDate:$root.egDateAndTimeFormat:renewal.circ.circ_lib:renewal.circ.duration}}') %]</div>
     </li>
   </ol>
   <hr/>
diff --git a/Open-ILS/src/templates/staff/share/t_autogrid.tt2 b/Open-ILS/src/templates/staff/share/t_autogrid.tt2
index cf5ac46..0942d1c 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 @@
           <!-- otherwise, simply display the item value, which may 
                pass through datatype-specific filtering. -->
           <span ng-if="!col.template" style="padding-left:5px; padding-right:10px;">
-            {{itemFieldValue(item, col) | egGridValueFilter:col}}
+            {{itemFieldValue(item, col) | egGridValueFilter:col:item}}
           </span>
       </div>
     </div>
diff --git a/Open-ILS/web/js/ui/default/staff/Gruntfile.js b/Open-ILS/web/js/ui/default/staff/Gruntfile.js
index e594196..8a885a1 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 06a908d..2cc0256 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 8d73b9d..5628a48 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 3ec7822..f219d57 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 91c88fa..cd3a9bb 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 96c4a9e..41e2d98 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 dfa1a1a..25f650b 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 ced1577..c9815d0 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 8eb074e..0368fbc 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 @@
 
     <head>
         <title>&staff.server.admin.closed_dates.title;</title>
+
+        <!-- welp, hope nobody uses media_prefix... -->
+        <script src="/js/ui/default/staff/build/js/moment-with-locales.min.js"></script>
+        <script src="/js/ui/default/staff/build/js/moment-timezone-with-data.min.js"></script>
+
         <script type="text/javascript" djConfig="parseOnLoad: true,isDebug:false" src="/js/dojo/dojo/dojo.js"></script>
         <script type="text/javascript" djConfig="parseOnLoad: true,isDebug:false" src="/js/dojo/dojo/openils_dojo.js"></script>
         <script type='text/javascript' src='/opac/common/js/utils.js'> </script>

-----------------------------------------------------------------------

Summary of changes:
 Open-ILS/examples/fm_IDL.xml                       |    2 +
 .../perlmods/lib/OpenILS/Application/AppUtils.pm   |    2 +-
 .../lib/OpenILS/Application/Circ/CircCommon.pm     |    7 +-
 .../lib/OpenILS/Application/Storage/CDBI/actor.pm  |    2 +-
 .../perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm   |    6 +
 Open-ILS/src/sql/Pg/002.schema.config.sql          |    2 +-
 Open-ILS/src/sql/Pg/005.schema.actors.sql          |    2 +
 Open-ILS/src/sql/Pg/090.schema.action.sql          |   15 ++-
 Open-ILS/src/sql/Pg/950.data.seed-values.sql       |   12 ++
 .../sql/Pg/upgrade/1054.data.tz_org_setting.sql    |   79 +++++++++
 .../src/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 +-
 .../src/templates/opac/parts/record/copy_table.tt2 |    4 +-
 Open-ILS/src/templates/staff/base_js.tt2           |    2 +
 .../templates/staff/cat/item/t_circ_list_pane.tt2  |    2 +-
 Open-ILS/src/templates/staff/cat/item/t_list.tt2   |    2 +-
 .../templates/staff/cat/item/t_summary_pane.tt2    |    2 +-
 .../staff/circ/checkin/t_checkin_table.tt2         |    2 +-
 .../templates/staff/circ/patron/t_bills_list.tt2   |    3 +
 .../src/templates/staff/circ/patron/t_checkout.tt2 |    2 +-
 .../templates/staff/circ/patron/t_items_out.tt2    |    4 +-
 .../templates/staff/circ/patron/t_xact_details.tt2 |    2 +-
 .../src/templates/staff/circ/renew/t_renew.tt2     |    2 +-
 .../staff/circ/share/t_circ_exists_dialog.tt2      |    4 +-
 .../staff/share/print_templates/t_checkout.tt2     |    2 +-
 .../staff/share/print_templates/t_items_out.tt2    |    2 +-
 .../staff/share/print_templates/t_renew.tt2        |    2 +-
 Open-ILS/src/templates/staff/share/t_autogrid.tt2  |    2 +-
 Open-ILS/web/js/ui/default/staff/Gruntfile.js      |    6 +-
 .../js/ui/default/staff/admin/workstation/app.js   |    4 +
 Open-ILS/web/js/ui/default/staff/cat/item/app.js   |    2 +
 .../web/js/ui/default/staff/circ/services/circ.js  |    3 +
 Open-ILS/web/js/ui/default/staff/package.json      |    2 +
 Open-ILS/web/js/ui/default/staff/services/grid.js  |   84 +++++++--
 .../web/js/ui/default/staff/services/startup.js    |    3 +-
 Open-ILS/web/js/ui/default/staff/services/ui.js    |  139 +++++++++++++++
 .../xul/staff_client/server/admin/closed_dates.js  |  182 ++++++++++++++------
 .../staff_client/server/admin/closed_dates.xhtml   |    5 +
 .../Circulation/due_date_timezones.adoc            |  103 +++++++++++
 40 files changed, 605 insertions(+), 103 deletions(-)
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/1054.data.tz_org_setting.sql
 create mode 100644 docs/RELEASE_NOTES_NEXT/Circulation/due_date_timezones.adoc


hooks/post-receive
-- 
Evergreen ILS


More information about the open-ils-commits mailing list