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

Evergreen Git git at git.evergreen-ils.org
Thu Feb 25 19:32:20 EST 2016


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  431d6372122256e1f7b8f7e893f32c6b64a42181 (commit)
       via  523beea71270451e4790e1dcb42e53f5641a2dc0 (commit)
       via  adda2b57df047c3cefbb37e4bb6513fd82cd5a6e (commit)
       via  120cbd0b85d94e32307c0a143049cd1008ab96cd (commit)
       via  3784e320d305b99f2df943c09140b42c464b1361 (commit)
       via  6a9f2d238593efcf8e061bbd986477d03f8234b7 (commit)
       via  2b3b1dc5df5949dded462204b65eef84d24c5094 (commit)
       via  153f8b073e5b1d0fd559325efba496c4df511424 (commit)
       via  52aa1f6f0c6074568f32c21436a5d6240bcc5feb (commit)
       via  1d5a8f4db87a1a48efa45d764a479cf7d51b78f0 (commit)
      from  db8bd918412d9ee7fe4f8928d3d85bc24aa5120e (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 431d6372122256e1f7b8f7e893f32c6b64a42181
Author: Kathy Lussier <klussier at masslnc.org>
Date:   Thu Feb 25 19:30:05 2016 -0500

    LP#1527342 Stamping upgrade script decouple checkout history
    
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/sql/Pg/002.schema.config.sql b/Open-ILS/src/sql/Pg/002.schema.config.sql
index 7f19216..12d93f9 100644
--- a/Open-ILS/src/sql/Pg/002.schema.config.sql
+++ b/Open-ILS/src/sql/Pg/002.schema.config.sql
@@ -91,7 +91,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 ('0959', :eg_version); -- csharp/dbwells
+INSERT INTO config.upgrade_log (version, applied_to) VALUES ('0960', :eg_version); -- berick/kmlussier
 
 CREATE TABLE config.bib_source (
 	id		SERIAL	PRIMARY KEY,
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.decouple_co_history.sql b/Open-ILS/src/sql/Pg/upgrade/0960.schema.decouple_co_history.sql
similarity index 99%
rename from Open-ILS/src/sql/Pg/upgrade/XXXX.schema.decouple_co_history.sql
rename to Open-ILS/src/sql/Pg/upgrade/0960.schema.decouple_co_history.sql
index 889ab3f..5fc0fed 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.decouple_co_history.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/0960.schema.decouple_co_history.sql
@@ -4,7 +4,7 @@ BEGIN;
 -- TODO process to delete history items once the age threshold 
 -- history.circ.retention_age is reached?
 
--- SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version); 
+SELECT evergreen.upgrade_deps_block_check('0960', :eg_version); 
 
 CREATE TABLE action.usr_circ_history (
     id           BIGSERIAL PRIMARY KEY,

commit 523beea71270451e4790e1dcb42e53f5641a2dc0
Author: Bill Erickson <berickxx at gmail.com>
Date:   Thu Feb 25 13:52:29 2016 -0500

    LP#1527342 Improve display of C/O history delete warning
    
    Move the history warning to the top of the form for visbility and avoid
    displaying the success message when the warning is also displayed.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/templates/opac/myopac/prefs_settings.tt2 b/Open-ILS/src/templates/opac/myopac/prefs_settings.tt2
index edefb5c..e98f6dd 100644
--- a/Open-ILS/src/templates/opac/myopac/prefs_settings.tt2
+++ b/Open-ILS/src/templates/opac/myopac/prefs_settings.tt2
@@ -12,7 +12,18 @@
             title="[% l('Search and History Preferences') %]">
             <tbody>
 
-                [% IF ctx.updated_user_settings %]
+                [% IF ctx.confirm_history_delete %]
+                <tr><td colspan='2'>
+                  <div id='clear-history-confirm' class='renew-summary'>
+                  [% l('Disabling checkout or holds history will permanently remove all items from your history.') %]
+                  <br/>
+                  [% l('Are you sure you wish to continue?') %]
+                  </div>
+                  <a href="[% mkurl() %]">[% l('Cancel') %]</a>
+                  <input type='hidden' name='history_delete_confirmed' value='1'/>
+                  <input type="submit" value="[% l('Confirm') %]" class="opac-button"/>
+                </td></tr>
+                [% ELSIF ctx.updated_user_settings %]
                 <tr><td colspan='2'>
                     <div class='renew-summary'>
                         [% l('Account Successfully Updated') %]
@@ -123,16 +134,7 @@
 
             </tbody>
         </table>
-        [% IF ctx.confirm_history_delete %]
-        <div id='clear-history-confirm'>
-        [% l('Disabling checkout or holds history will permanently remove all items from your history.') %]
-        <br/>
-        [% l('Are you sure you wish to continue?') %]
-        </div>
-        <a href="[% mkurl() %]">[% l('Cancel') %]</a>
-        <input type='hidden' name='history_delete_confirmed' value='1'/>
-        <input type="submit" value="[% l('Confirm') %]" class="opac-button"/>
-        [% ELSE %]
+        [% IF !ctx.confirm_history_delete %]
         <input type="submit" value="[% l('Save') %]" class="opac-button"/>
         [% END %]
     </form>

commit adda2b57df047c3cefbb37e4bb6513fd82cd5a6e
Author: Bill Erickson <berickxx at gmail.com>
Date:   Wed Feb 24 10:01:06 2016 -0500

    LP#1527342 Patron CO history CSV update core type
    
    Modify circ.format.history.csv to core type of 'auch' in seed data.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

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 99410a3..b804d72 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -13139,7 +13139,7 @@ INSERT INTO config.org_unit_setting_type
 INSERT INTO action_trigger.hook (key, core_type, description, passive)
 VALUES (
     'circ.format.history.csv',
-    'circ',
+    'auch',
     oils_i18n_gettext(
         'circ.format.history.csv',
         'Produce CSV of circulation history',

commit 120cbd0b85d94e32307c0a143049cd1008ab96cd
Author: Bill Erickson <berickxx at gmail.com>
Date:   Tue Feb 23 10:42:27 2016 -0500

    LP#1527342 Patron checkout history CSV export repair
    
    When exporting circ history as CSV, export history objects, not
    (unrelated) circ objects.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
index b3c15bf..ec0bada 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
@@ -2681,9 +2681,7 @@ sub load_myopac_circ_history_export {
 
     $self->ctx->{csv} = $U->fire_object_event(
         undef, 
-        'circ.format.history.csv',
-        $e->search_action_circulation(
-            {id => [map {$_->id} @$circs]}, {substream =>1}),
+        'circ.format.history.csv', $circs,
         $self->editor->requestor->home_ou
     );
 

commit 3784e320d305b99f2df943c09140b42c464b1361
Author: Bill Erickson <berickxx at gmail.com>
Date:   Mon Dec 28 12:22:57 2015 -0500

    LP#1527342 Patron checkout history table release notes
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/docs/RELEASE_NOTES_NEXT/Circulation/user-circ-history.adoc b/docs/RELEASE_NOTES_NEXT/Circulation/user-circ-history.adoc
new file mode 100644
index 0000000..168d753
--- /dev/null
+++ b/docs/RELEASE_NOTES_NEXT/Circulation/user-circ-history.adoc
@@ -0,0 +1,54 @@
+Patron Checkout History Stored in a Dedicated Table
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Patron checkout history is now stored in separate, dedicated database 
+table instead of being derived from the main circulation data.  This
+allows us to age/anonymize circulations more aggressively, since they 
+no longer need to stick around in cases where they represent a patron's
+opt-in checkout history.
+
+This has a number of patron privacy implications.
+
+ * Minimal metadata is stored in the new patron checkout history table, 
+   so once the corresponding circulation is aged, the full set of 
+   circulation metadata is no longer linked to a patron's reading history.
+   ** It is limited to checkout date, due date, checkin date, and copy data.
+ * Staff can no longer report on a patron's reading history.  
+   ** While it is possible to build aggregate reports on reading history 
+      data, it is not possible to report on which user an entry in the
+      history table belongs to.  (The 'usr' column is hidden from the 
+      reporter).
+ * Staff can no longer retrieve a patron's reading history via API.  Only
+   the user that owns the history data can access it.
+ * Though not implemented as part of this change, it will now be possible
+   with future development to truly remove specific items from a patron's
+   checkout history.
+
+TPAC Patron History Disable Warning
++++++++++++++++++++++++++++++++++++
+
+When disabling checkout and/or holds history in the TPAC Search and
+History Preferences tab, patrons will be warned that the operation is
+irreversible when history data exists that will be deleted as part of 
+the update.
+
+Upgrade Notes
++++++++++++++
+
+Administrators should verify the CSV export of checkout history works after
+deploying this change.  If local changes were made to the CSV template,
+the template will not be updated as part of this deployment.  The stock 
+template was modified to handle gracefully NULL values for checkin_time.
+
+For example:
+
+[source,diff]
+------------------------------------------------------------------------
+-    Returned: [% date.format(helpers.format_date(circ.checkin_time), '%Y-%m-%d') %]
++    Returned: [%
++        date.format(
++            helpers.format_date(circ.checkin_time), '%Y-%m-%d') 
++            IF circ.checkin_time; 
++    %]
+------------------------------------------------------------------------
+

commit 6a9f2d238593efcf8e061bbd986477d03f8234b7
Author: Bill Erickson <berickxx at gmail.com>
Date:   Mon Dec 28 10:43:58 2015 -0500

    LP#1527342 Patron checkout history PGTAP tests
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/sql/Pg/live_t/user-circ-history.pg b/Open-ILS/src/sql/Pg/live_t/user-circ-history.pg
new file mode 100644
index 0000000..a78879b
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/live_t/user-circ-history.pg
@@ -0,0 +1,85 @@
+BEGIN;
+
+SELECT plan(5);
+
+-- at the time of writing, user '2' has zero circulations 
+-- in the default sample data set.
+\set circ_usr 2
+
+-- activate circ history tracking for a patron
+INSERT INTO actor.usr_setting (usr, name, value) VALUES 
+    (:circ_usr, 'history.circ.retention_start', '"2015-01-01"');
+
+INSERT INTO action.circulation (
+    usr, target_copy, circ_lib, circ_staff, renewal_remaining,
+    grace_period, duration, recurring_fine, max_fine, duration_rule,
+    recurring_fine_rule, max_fine_rule, due_date )
+VALUES (
+    :circ_usr, 1, 4, 1, 2, '1 day', '14 days','0.10', '10',
+   'default','default','default', now() + '14 days'::interval
+);
+
+SELECT isnt_empty(
+    'SELECT * FROM action.usr_circ_history WHERE usr = ' || :circ_usr,
+    'Confirm a history row was inserted'
+);
+
+SELECT is(
+    (SELECT checkin_time FROM action.usr_circ_history WHERE usr = :circ_usr),
+    NULL,
+    'Confirm checkin_time is NULL'
+);
+
+-- simulate a renewal
+UPDATE action.circulation 
+    SET checkin_time = NOW(), stop_fines = 'RENEW' 
+    WHERE usr = :circ_usr;
+
+-- create the renewal
+INSERT INTO action.circulation (
+    usr, target_copy, circ_lib, circ_staff, renewal_remaining,
+    grace_period, duration, recurring_fine, max_fine, duration_rule,
+    recurring_fine_rule, max_fine_rule, due_date, parent_circ )
+VALUES (
+    :circ_usr, 1, 4, 1, 2, '1 day', '14 days','0.10', '10',
+   'default','default','default', '3001-01-01',
+   (SELECT id FROM action.circulation WHERE usr = :circ_usr)
+);
+
+-- confirm due_date on the history object is updated to match the
+-- due date of the renewal circ.
+SELECT is(
+    (SELECT DATE(due_date) FROM action.usr_circ_history WHERE usr = :circ_usr),
+    '3001-01-01',
+    'Confirm due_date matches renewal due date'
+);
+
+UPDATE action.circulation SET checkin_time = NOW() 
+    WHERE usr = :circ_usr AND stop_fines IS NULL;
+
+SELECT isnt(
+    (SELECT checkin_time FROM action.usr_circ_history WHERE usr = :circ_usr),
+    NULL,
+    'Confirm checkin_time is set'
+);
+
+-- Confirm no history is created users that are not opted in.
+-- Assumes :circ_usr + 1 is a valid user id.
+INSERT INTO action.circulation (
+    usr, target_copy, circ_lib, circ_staff, renewal_remaining,
+    grace_period, duration, recurring_fine, max_fine, duration_rule,
+    recurring_fine_rule, max_fine_rule, due_date )
+VALUES (
+    :circ_usr + 1, 1, 4, 1, 2, '1 day', '14 days','0.10', '10',
+   'default','default','default', now() + '14 days'::interval
+);
+
+SELECT is_empty(
+    'SELECT * FROM action.usr_circ_history WHERE usr = ' || :circ_usr + 1,
+    'Confirm no history is created'
+);
+
+
+-- Finish the tests and clean up.
+SELECT * FROM finish();
+ROLLBACK;

commit 2b3b1dc5df5949dded462204b65eef84d24c5094
Author: Bill Erickson <berickxx at gmail.com>
Date:   Tue Dec 22 15:14:12 2015 -0500

    LP#1527342 search result history tagging
    
    Use circ history data to indicate circulated items in search results.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm
index 1e6a485..708dc90 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm
@@ -1465,8 +1465,8 @@ sub tag_circulated_records {
 
     my $query = {
         select   => { acn => [{ column => 'record', alias => 'tagme' }] }, 
-        from     => { acp => 'acn' }, 
-        where    => { id => { in => { from => ['action.usr_visible_circ_copies', $e->requestor->id] } } },
+        from     => { auch => { acp => { join => 'acn' }} }, 
+        where    => { usr => $e->requestor->id },
         distinct => 1
     };
 

commit 153f8b073e5b1d0fd559325efba496c4df511424
Author: Bill Erickson <berickxx at gmail.com>
Date:   Mon Dec 21 11:19:19 2015 -0500

    LP#1527342 Patron checkout history TPAC display
    
    Checkout history is now derived from the new action.usr_circ_history
    table.  When a patron disables circ history, all history is deleted from
    the new table.  Also, when disabling circ or holds history, the patron
    is now warned if data will be deleted or, in the case of holds, become
    inaccessible.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm
index e5b4316..281298a 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm
@@ -4215,33 +4215,47 @@ sub event_def_opt_in_settings {
 
 
 __PACKAGE__->register_method(
-    method    => "user_visible_circs",
-    api_name  => "open-ils.actor.history.circ.visible",
+    method    => "user_circ_history",
+    api_name  => "open-ils.actor.history.circ",
     stream => 1,
+    authoritative => 1,
     signature => {
-        desc   => 'Returns the set of opt-in visible circulations accompanied by circulation chain summaries',
+        desc   => 'Returns user circ history objects for the calling user',
         params => [
             { desc => 'Authentication token',  type => 'string'},
-            { desc => 'User ID.  If no user id is present, the authenticated user is assumed', type => 'number' },
             { desc => 'Options hash.  Supported fields are "limit" and "offset"', type => 'object' },
         ],
         return => {
-            desc => q/An object with 2 fields: circulation and summary.  
-                circulation is the "circ" object.   summary is the related "accs" object/,
+            desc => q/Stream of 'auch' circ history objects/,
             type => 'object',
         }
     }
 );
 
 __PACKAGE__->register_method(
-    method    => "user_visible_circs",
-    api_name  => "open-ils.actor.history.circ.visible.print",
+    method    => "user_circ_history",
+    api_name  => "open-ils.actor.history.circ.clear",
     stream => 1,
     signature => {
-        desc   => 'Returns printable output for the set of opt-in visible circulations',
+        desc   => 'Delete all user circ history entries for the calling user',
+        params => [
+            { desc => 'Authentication token',  type => 'string'},
+        ],
+        return => {
+            desc => q/1 on success, event on error/,
+            type => 'object',
+        }
+    }
+);
+
+__PACKAGE__->register_method(
+    method    => "user_circ_history",
+    api_name  => "open-ils.actor.history.circ.print",
+    stream => 1,
+    signature => {
+        desc   => q/Returns printable output for the caller's circ history objects/,
         params => [
             { desc => 'Authentication token',  type => 'string'},
-            { desc => 'User ID.  If no user id is present, the authenticated user is assumed', type => 'number' },
             { desc => 'Options hash.  Supported fields are "limit" and "offset"', type => 'object' },
         ],
         return => {
@@ -4252,11 +4266,11 @@ __PACKAGE__->register_method(
 );
 
 __PACKAGE__->register_method(
-    method    => "user_visible_circs",
-    api_name  => "open-ils.actor.history.circ.visible.email",
+    method    => "user_circ_history",
+    api_name  => "open-ils.actor.history.circ.email",
     stream => 1,
     signature => {
-        desc   => 'Emails the set of opt-in visible circulations to the requestor',
+        desc   => q/Emails the caller's circ history/,
         params => [
             { desc => 'Authentication token',  type => 'string'},
             { desc => 'User ID.  If no user id is present, the authenticated user is assumed', type => 'number' },
@@ -4268,6 +4282,68 @@ __PACKAGE__->register_method(
     }
 );
 
+sub user_circ_history {
+    my ($self, $conn, $auth, $options) = @_;
+    $options ||= {};
+
+    my $for_print = ($self->api_name =~ /print/);
+    my $for_email = ($self->api_name =~ /email/);
+    my $for_clear = ($self->api_name =~ /clear/);
+
+    # No perm check is performed.  Caller may only access his/her own
+    # circ history entries.
+    my $e = new_editor(authtoken => $auth);
+    return $e->event unless $e->checkauth;
+
+    my %limits = ();
+    if (!$for_clear) { # clear deletes all
+        $limits{offset} = $options->{offset} if defined $options->{offset};
+        $limits{limit} = $options->{limit} if defined $options->{limit};
+    }
+
+    my $circs = $e->search_action_user_circ_history([
+        {usr => $e->requestor->id},
+        {   # order newest to oldest by default
+            order_by => {auch => 'xact_start DESC'},
+            %limits
+        },
+        {substream => 1} # could be a large list
+    ]);
+
+    if ($for_print) {
+        return $U->fire_object_event(undef, 
+            'circ.format.history.print', $circs, $e->requestor->home_ou);
+    }
+
+    $e->xact_begin if $for_clear;
+    $conn->respond_complete(1) if $for_email;  # no sense in waiting
+
+    for my $circ (@$circs) {
+
+        if ($for_email) {
+            # events will be fired from action_trigger_runner
+            $U->create_events_for_hook('circ.format.history.email', 
+                $circ, $e->editor->home_ou, undef, undef, 1);
+
+        } elsif ($for_clear) {
+
+            $e->delete_action_user_circ_history($circ) 
+                or return $e->die_event;
+
+        } else {
+            $conn->respond($circ);
+        }
+    }
+
+    if ($for_clear) {
+        $e->commit;
+        return 1;
+    }
+
+    return undef;
+}
+
+
 __PACKAGE__->register_method(
     method    => "user_visible_circs",
     api_name  => "open-ils.actor.history.hold.visible",
@@ -4321,10 +4397,10 @@ __PACKAGE__->register_method(
     }
 );
 
-sub user_visible_circs {
+sub user_visible_holds {
     my($self, $conn, $auth, $user_id, $options) = @_;
 
-    my $is_hold = ($self->api_name =~ /hold/);
+    my $is_hold = 1;
     my $for_print = ($self->api_name =~ /print/);
     my $for_email = ($self->api_name =~ /email/);
     my $e = new_editor(authtoken => $auth);
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
index 30e0756..b3c15bf 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
@@ -616,18 +616,81 @@ sub load_myopac_prefs_settings {
         $settings{$key}= $val unless $$set_map{$key} eq $val;
     }
 
+    # Used by the settings update form when warning on history delete.
+    my $clear_circ_history = 0;
+    my $clear_hold_history = 0;
+
+    # true if we need to show the warning on next page load.
+    my $hist_warning_needed = 0;
+    my $hist_clear_confirmed = $self->cgi->param('history_delete_confirmed');
+
     my $now = DateTime->now->strftime('%F');
-    foreach my $key (qw/history.circ.retention_start history.hold.retention_start/) {
+    foreach my $key (
+            qw/history.circ.retention_start history.hold.retention_start/) {
+
         my $val = $self->cgi->param($key);
         if($val and $val eq 'on') {
             # Set the start time to 'now' unless a start time already exists for the user
             $settings{$key} = $now unless $$set_map{$key};
+
         } else {
-            # clear the start time if one previously existed for the user
-            $settings{$key} = undef if $$set_map{$key};
+
+            next unless $$set_map{$key}; # nothing to do
+
+            $clear_circ_history = 1 if $key =~ /circ/;
+            $clear_hold_history = 1 if $key =~ /hold/;
+
+            if (!$hist_clear_confirmed) {
+                # when clearing circ history, only warn if history data exists.
+    
+                if ($clear_circ_history) {
+
+                    if ($self->fetch_user_circ_history(0, 1)->[0]) {
+                        $hist_warning_needed = 1;
+                        next; # no history updates while confirmation pending
+                    }
+
+                } else {
+
+                    my $one_hold = $e->json_query({
+                        select => {
+                            au => [{
+                                column => 'id', 
+                                transform => 'action.usr_visible_holds', 
+                                result_field => 'id'
+                            }]
+                        },
+                        from => 'au',
+                        where => {id => $e->requestor->id},
+                        limit => 1
+                    })->[0];
+
+                    if ($one_hold) {
+                        $hist_warning_needed = 1;
+                        next; # no history updates while confirmation pending
+                    }
+                }
+            }
+
+            $settings{$key} = undef;
+
+            if ($key eq 'history.circ.retention_start') {
+                # delete existing circulation history data.
+                $U->simplereq(
+                    'open-ils.actor',
+                    'open-ils.actor.history.circ.clear',
+                    $self->editor->authtoken);
+            }
         }
     }
 
+    # Warn patrons before clearing circ/hold history
+    if ($hist_warning_needed) {
+        $self->ctx->{clear_circ_history} = $clear_circ_history;
+        $self->ctx->{clear_hold_history} = $clear_hold_history;
+        $self->ctx->{confirm_history_delete} = 1;
+    }
+
     # Send the modified settings off to be saved
     $U->simplereq(
         'open-ils.actor', 
@@ -1526,38 +1589,62 @@ sub load_myopac_circ_history {
     $ctx->{circ_history_limit} = $limit;
     $ctx->{circ_history_offset} = $offset;
 
-    my $circ_ids;
-    if ($self->cgi->param('sort') ne "") {		# Defer limitation to circ_history.tt2
-       $circ_ids = $e->json_query({
-        select => {
-            au => [{
-                column => 'id', 
-                transform => 'action.usr_visible_circs', 
-                result_field => 'id'
-            }]
+    # Defer limitation to circ_history.tt2 when sorting
+    if ($self->cgi->param('sort')) {
+        $limit = undef;
+        $offset = undef;
+    }
+
+    $ctx->{circs} = $self->fetch_user_circ_history(1, $limit, $offset);
+    return Apache2::Const::OK;
+}
+
+# if 'flesh' is set, copy data etc. is loaded and the return value is 
+# a hash of 'circ' and 'marc_xml'.  Othwerwise, it's just a list of 
+# auch objects.
+sub fetch_user_circ_history {
+    my ($self, $flesh, $limit, $offset) = @_;
+    my $e = $self->editor;
+
+    my %limits = ();
+    $limits{offset} = $offset if defined $offset;
+    $limits{limit} = $limit if defined $limit;
+
+    my %flesh_ops = (
+        flesh => 3,
+        flesh_fields => {
+            auch => ['target_copy'],
+            acp => ['call_number'],
+            acn => ['record']
         },
-        from => 'au',
-        where => {id => $e->requestor->id}  
-        });
+    );
 
-    } else {
-       $circ_ids = $e->json_query({
-        select => {
-            au => [{
-                column => 'id', 
-                transform => 'action.usr_visible_circs', 
-                result_field => 'id'
-            }]
+    $e->xact_begin;
+    my $circs = $e->search_action_user_circ_history([
+        {usr => $e->requestor->id},
+        {   # order newest to oldest by default
+            order_by => {auch => 'xact_start DESC'},
+            $flesh ? %flesh_ops : (),
+            %limits
         },
-        from => 'au',
-        where => {id => $e->requestor->id}, 
-        limit => $limit,
-        offset => $offset
+        {substream => 1}
+    ]);
+    $e->rollback;
+
+    return $circs unless $flesh;
+
+    my @circs;
+    for my $circ (@$circs) {
+        push(@circs, {
+            circ => $circ, 
+            marc_xml => ($circ->target_copy->call_number->id != -1) ? 
+                XML::LibXML->new->parse_string(
+                    $circ->target_copy->call_number->record->marc) : 
+                undef  # pre-cat copy, use the dummy title/author instead
         });
     }
 
-    $ctx->{circs} = $self->fetch_user_circs(1, [map { $_->{id} } @$circ_ids]);
-    return Apache2::Const::OK;
+    return \@circs;
 }
 
 # TODO: action.usr_visible_holds does not return cancelled holds.  Should it?
@@ -2590,22 +2677,13 @@ sub load_myopac_circ_history_export {
     my $e = $self->editor;
     my $filename = $self->cgi->param('filename') || 'circ_history.csv';
 
-    my $ids = $e->json_query({
-        select => {
-            au => [{
-                column => 'id', 
-                transform => 'action.usr_visible_circs', 
-                result_field => 'id'
-            }]
-        },
-        from => 'au',
-        where => {id => $e->requestor->id} 
-    });
+    my $circs = $self->fetch_user_circ_history;
 
     $self->ctx->{csv} = $U->fire_object_event(
         undef, 
         'circ.format.history.csv',
-        $e->search_action_circulation({id => [map {$_->{id}} @$ids]}, {substream =>1}),
+        $e->search_action_circulation(
+            {id => [map {$_->id} @$circs]}, {substream =>1}),
         $self->editor->requestor->home_ou
     );
 
diff --git a/Open-ILS/src/templates/opac/css/style.css.tt2 b/Open-ILS/src/templates/opac/css/style.css.tt2
index 2bfe26b..fe98473 100644
--- a/Open-ILS/src/templates/opac/css/style.css.tt2
+++ b/Open-ILS/src/templates/opac/css/style.css.tt2
@@ -2133,3 +2133,9 @@ label[for*=expert_]
     text-align: center;
     font-style: italic;
 }
+
+#clear-history-confirm {
+  font-weight: bold;
+  color: [% css_colors.text_badnews %]; 
+  padding: 10px;
+}
diff --git a/Open-ILS/src/templates/opac/myopac/prefs_settings.tt2 b/Open-ILS/src/templates/opac/myopac/prefs_settings.tt2
index 9f1288c..edefb5c 100644
--- a/Open-ILS/src/templates/opac/myopac/prefs_settings.tt2
+++ b/Open-ILS/src/templates/opac/myopac/prefs_settings.tt2
@@ -72,7 +72,8 @@
                     <td>
                         <input id='[% setting %]' name='[% setting %]' 
                             type="checkbox" title="[% circ_name %]"
-                            [% IF ctx.user_setting_map.$setting; %] checked='checked' [% END %]/>
+                            [% IF ctx.user_setting_map.$setting 
+                              AND !ctx.clear_circ_history; %] checked='checked' [% END %]/>
                     </td>
                 </tr>
                 [%- setting = 'history.hold.retention_start' -%]
@@ -82,7 +83,8 @@
                     <td>
                         <input id='[% setting %]' name='[% setting %]' 
                             type="checkbox" title="[% hold_name %]"
-                            [% IF ctx.user_setting_map.$setting; %] checked='checked' [% END %]/>
+                            [% IF ctx.user_setting_map.$setting
+                              AND !ctx.clear_hold_history; %] checked='checked' [% END %]/>
                     </td>
                 </tr>
                 [%- setting = 'opac.temporary_list_no_warn' -%]
@@ -121,7 +123,18 @@
 
             </tbody>
         </table>
+        [% IF ctx.confirm_history_delete %]
+        <div id='clear-history-confirm'>
+        [% l('Disabling checkout or holds history will permanently remove all items from your history.') %]
+        <br/>
+        [% l('Are you sure you wish to continue?') %]
+        </div>
+        <a href="[% mkurl() %]">[% l('Cancel') %]</a>
+        <input type='hidden' name='history_delete_confirmed' value='1'/>
+        <input type="submit" value="[% l('Confirm') %]" class="opac-button"/>
+        [% ELSE %]
         <input type="submit" value="[% l('Save') %]" class="opac-button"/>
+        [% END %]
     </form>
     [% INCLUDE "opac/parts/myopac/prefs_hints.tt2" %]
 [% END %]

commit 52aa1f6f0c6074568f32c21436a5d6240bcc5feb
Author: Bill Erickson <berickxx at gmail.com>
Date:   Wed Dec 30 11:53:52 2015 -0500

    LP#1527342 Patron co history remove on purge
    
    Delete checkout history rows when purging a user.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/sql/Pg/999.functions.global.sql b/Open-ILS/src/sql/Pg/999.functions.global.sql
index d98edc0..7e13654 100644
--- a/Open-ILS/src/sql/Pg/999.functions.global.sql
+++ b/Open-ILS/src/sql/Pg/999.functions.global.sql
@@ -431,6 +431,7 @@ BEGIN
 	UPDATE action.non_cataloged_circulation SET staff = dest_usr WHERE staff = src_usr;
 	DELETE FROM action.survey_response WHERE usr = src_usr;
 	UPDATE action.fieldset SET owner = dest_usr WHERE owner = src_usr;
+	DELETE FROM action.usr_circ_history WHERE usr = src_usr;
 
 	-- actor.*
 	DELETE FROM actor.card WHERE usr = src_usr;
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.decouple_co_history.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.decouple_co_history.sql
index 5625f0c..889ab3f 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.decouple_co_history.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.decouple_co_history.sql
@@ -333,6 +333,335 @@ BEGIN
 END;
 $func$ LANGUAGE PLPGSQL;
 
+-- delete circ history rows when a user is purged.
+CREATE OR REPLACE FUNCTION actor.usr_purge_data(
+	src_usr  IN INTEGER,
+	specified_dest_usr IN INTEGER
+) RETURNS VOID AS $$
+DECLARE
+	suffix TEXT;
+	renamable_row RECORD;
+	dest_usr INTEGER;
+BEGIN
+
+	IF specified_dest_usr IS NULL THEN
+		dest_usr := 1; -- Admin user on stock installs
+	ELSE
+		dest_usr := specified_dest_usr;
+	END IF;
+
+	-- acq.*
+	UPDATE acq.fund_allocation SET allocator = dest_usr WHERE allocator = src_usr;
+	UPDATE acq.lineitem SET creator = dest_usr WHERE creator = src_usr;
+	UPDATE acq.lineitem SET editor = dest_usr WHERE editor = src_usr;
+	UPDATE acq.lineitem SET selector = dest_usr WHERE selector = src_usr;
+	UPDATE acq.lineitem_note SET creator = dest_usr WHERE creator = src_usr;
+	UPDATE acq.lineitem_note SET editor = dest_usr WHERE editor = src_usr;
+	DELETE FROM acq.lineitem_usr_attr_definition WHERE usr = src_usr;
+
+	-- Update with a rename to avoid collisions
+	FOR renamable_row in
+		SELECT id, name
+		FROM   acq.picklist
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  acq.picklist
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = renamable_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+	UPDATE acq.picklist SET creator = dest_usr WHERE creator = src_usr;
+	UPDATE acq.picklist SET editor = dest_usr WHERE editor = src_usr;
+	UPDATE acq.po_note SET creator = dest_usr WHERE creator = src_usr;
+	UPDATE acq.po_note SET editor = dest_usr WHERE editor = src_usr;
+	UPDATE acq.purchase_order SET owner = dest_usr WHERE owner = src_usr;
+	UPDATE acq.purchase_order SET creator = dest_usr WHERE creator = src_usr;
+	UPDATE acq.purchase_order SET editor = dest_usr WHERE editor = src_usr;
+	UPDATE acq.claim_event SET creator = dest_usr WHERE creator = src_usr;
+
+	-- action.*
+	DELETE FROM action.circulation WHERE usr = src_usr;
+	UPDATE action.circulation SET circ_staff = dest_usr WHERE circ_staff = src_usr;
+	UPDATE action.circulation SET checkin_staff = dest_usr WHERE checkin_staff = src_usr;
+	UPDATE action.hold_notification SET notify_staff = dest_usr WHERE notify_staff = src_usr;
+	UPDATE action.hold_request SET fulfillment_staff = dest_usr WHERE fulfillment_staff = src_usr;
+	UPDATE action.hold_request SET requestor = dest_usr WHERE requestor = src_usr;
+	DELETE FROM action.hold_request WHERE usr = src_usr;
+	UPDATE action.in_house_use SET staff = dest_usr WHERE staff = src_usr;
+	UPDATE action.non_cat_in_house_use SET staff = dest_usr WHERE staff = src_usr;
+	DELETE FROM action.non_cataloged_circulation WHERE patron = src_usr;
+	UPDATE action.non_cataloged_circulation SET staff = dest_usr WHERE staff = src_usr;
+	DELETE FROM action.survey_response WHERE usr = src_usr;
+	UPDATE action.fieldset SET owner = dest_usr WHERE owner = src_usr;
+    DELETE FROM action.usr_circ_history WHERE usr = src_usr;
+
+	-- actor.*
+	DELETE FROM actor.card WHERE usr = src_usr;
+	DELETE FROM actor.stat_cat_entry_usr_map WHERE target_usr = src_usr;
+
+	-- The following update is intended to avoid transient violations of a foreign
+	-- key constraint, whereby actor.usr_address references itself.  It may not be
+	-- necessary, but it does no harm.
+	UPDATE actor.usr_address SET replaces = NULL
+		WHERE usr = src_usr AND replaces IS NOT NULL;
+	DELETE FROM actor.usr_address WHERE usr = src_usr;
+	DELETE FROM actor.usr_note WHERE usr = src_usr;
+	UPDATE actor.usr_note SET creator = dest_usr WHERE creator = src_usr;
+	DELETE FROM actor.usr_org_unit_opt_in WHERE usr = src_usr;
+	UPDATE actor.usr_org_unit_opt_in SET staff = dest_usr WHERE staff = src_usr;
+	DELETE FROM actor.usr_setting WHERE usr = src_usr;
+	DELETE FROM actor.usr_standing_penalty WHERE usr = src_usr;
+	UPDATE actor.usr_standing_penalty SET staff = dest_usr WHERE staff = src_usr;
+
+	-- asset.*
+	UPDATE asset.call_number SET creator = dest_usr WHERE creator = src_usr;
+	UPDATE asset.call_number SET editor = dest_usr WHERE editor = src_usr;
+	UPDATE asset.call_number_note SET creator = dest_usr WHERE creator = src_usr;
+	UPDATE asset.copy SET creator = dest_usr WHERE creator = src_usr;
+	UPDATE asset.copy SET editor = dest_usr WHERE editor = src_usr;
+	UPDATE asset.copy_note SET creator = dest_usr WHERE creator = src_usr;
+
+	-- auditor.*
+	DELETE FROM auditor.actor_usr_address_history WHERE id = src_usr;
+	DELETE FROM auditor.actor_usr_history WHERE id = src_usr;
+	UPDATE auditor.asset_call_number_history SET creator = dest_usr WHERE creator = src_usr;
+	UPDATE auditor.asset_call_number_history SET editor  = dest_usr WHERE editor  = src_usr;
+	UPDATE auditor.asset_copy_history SET creator = dest_usr WHERE creator = src_usr;
+	UPDATE auditor.asset_copy_history SET editor  = dest_usr WHERE editor  = src_usr;
+	UPDATE auditor.biblio_record_entry_history SET creator = dest_usr WHERE creator = src_usr;
+	UPDATE auditor.biblio_record_entry_history SET editor  = dest_usr WHERE editor  = src_usr;
+
+	-- biblio.*
+	UPDATE biblio.record_entry SET creator = dest_usr WHERE creator = src_usr;
+	UPDATE biblio.record_entry SET editor = dest_usr WHERE editor = src_usr;
+	UPDATE biblio.record_note SET creator = dest_usr WHERE creator = src_usr;
+	UPDATE biblio.record_note SET editor = dest_usr WHERE editor = src_usr;
+
+	-- container.*
+	-- Update buckets with a rename to avoid collisions
+	FOR renamable_row in
+		SELECT id, name
+		FROM   container.biblio_record_entry_bucket
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  container.biblio_record_entry_bucket
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = renamable_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+	FOR renamable_row in
+		SELECT id, name
+		FROM   container.call_number_bucket
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  container.call_number_bucket
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = renamable_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+	FOR renamable_row in
+		SELECT id, name
+		FROM   container.copy_bucket
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  container.copy_bucket
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = renamable_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+	FOR renamable_row in
+		SELECT id, name
+		FROM   container.user_bucket
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  container.user_bucket
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = renamable_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+	DELETE FROM container.user_bucket_item WHERE target_user = src_usr;
+
+	-- money.*
+	DELETE FROM money.billable_xact WHERE usr = src_usr;
+	DELETE FROM money.collections_tracker WHERE usr = src_usr;
+	UPDATE money.collections_tracker SET collector = dest_usr WHERE collector = src_usr;
+
+	-- permission.*
+	DELETE FROM permission.usr_grp_map WHERE usr = src_usr;
+	DELETE FROM permission.usr_object_perm_map WHERE usr = src_usr;
+	DELETE FROM permission.usr_perm_map WHERE usr = src_usr;
+	DELETE FROM permission.usr_work_ou_map WHERE usr = src_usr;
+
+	-- reporter.*
+	-- Update with a rename to avoid collisions
+	BEGIN
+		FOR renamable_row in
+			SELECT id, name
+			FROM   reporter.output_folder
+			WHERE  owner = src_usr
+		LOOP
+			suffix := ' (' || src_usr || ')';
+			LOOP
+				BEGIN
+					UPDATE  reporter.output_folder
+					SET     owner = dest_usr, name = name || suffix
+					WHERE   id = renamable_row.id;
+				EXCEPTION WHEN unique_violation THEN
+					suffix := suffix || ' ';
+					CONTINUE;
+				END;
+				EXIT;
+			END LOOP;
+		END LOOP;
+	EXCEPTION WHEN undefined_table THEN
+		-- do nothing
+	END;
+
+	BEGIN
+		UPDATE reporter.report SET owner = dest_usr WHERE owner = src_usr;
+	EXCEPTION WHEN undefined_table THEN
+		-- do nothing
+	END;
+
+	-- Update with a rename to avoid collisions
+	BEGIN
+		FOR renamable_row in
+			SELECT id, name
+			FROM   reporter.report_folder
+			WHERE  owner = src_usr
+		LOOP
+			suffix := ' (' || src_usr || ')';
+			LOOP
+				BEGIN
+					UPDATE  reporter.report_folder
+					SET     owner = dest_usr, name = name || suffix
+					WHERE   id = renamable_row.id;
+				EXCEPTION WHEN unique_violation THEN
+					suffix := suffix || ' ';
+					CONTINUE;
+				END;
+				EXIT;
+			END LOOP;
+		END LOOP;
+	EXCEPTION WHEN undefined_table THEN
+		-- do nothing
+	END;
+
+	BEGIN
+		UPDATE reporter.schedule SET runner = dest_usr WHERE runner = src_usr;
+	EXCEPTION WHEN undefined_table THEN
+		-- do nothing
+	END;
+
+	BEGIN
+		UPDATE reporter.template SET owner = dest_usr WHERE owner = src_usr;
+	EXCEPTION WHEN undefined_table THEN
+		-- do nothing
+	END;
+
+	-- Update with a rename to avoid collisions
+	BEGIN
+		FOR renamable_row in
+			SELECT id, name
+			FROM   reporter.template_folder
+			WHERE  owner = src_usr
+		LOOP
+			suffix := ' (' || src_usr || ')';
+			LOOP
+				BEGIN
+					UPDATE  reporter.template_folder
+					SET     owner = dest_usr, name = name || suffix
+					WHERE   id = renamable_row.id;
+				EXCEPTION WHEN unique_violation THEN
+					suffix := suffix || ' ';
+					CONTINUE;
+				END;
+				EXIT;
+			END LOOP;
+		END LOOP;
+	EXCEPTION WHEN undefined_table THEN
+	-- do nothing
+	END;
+
+	-- vandelay.*
+	-- Update with a rename to avoid collisions
+	FOR renamable_row in
+		SELECT id, name
+		FROM   vandelay.queue
+		WHERE  owner = src_usr
+	LOOP
+		suffix := ' (' || src_usr || ')';
+		LOOP
+			BEGIN
+				UPDATE  vandelay.queue
+				SET     owner = dest_usr, name = name || suffix
+				WHERE   id = renamable_row.id;
+			EXCEPTION WHEN unique_violation THEN
+				suffix := suffix || ' ';
+				CONTINUE;
+			END;
+			EXIT;
+		END LOOP;
+	END LOOP;
+
+    -- NULL-ify addresses last so other cleanup (e.g. circ anonymization)
+    -- can access the information before deletion.
+	UPDATE actor.usr SET
+		active = FALSE,
+		card = NULL,
+		mailing_address = NULL,
+		billing_address = NULL
+	WHERE id = src_usr;
+
+END;
+$$ LANGUAGE plpgsql;
+
+
 
 COMMIT;
 

commit 1d5a8f4db87a1a48efa45d764a479cf7d51b78f0
Author: Bill Erickson <berickxx at gmail.com>
Date:   Fri Dec 18 18:09:16 2015 -0500

    LP#1527342 Patron checkout history SQL/IDL
    
    Adds a new table action.usr_circ_history for tracking opt-in checkout
    history.  History is maintained via trigger on action.circulation.
    
    Includes updates to html/email/csv checkout history templates to
    gracefully handle NULL checkin_time values, since history starts as soon
    as an item is checked out.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml
index ff9aa87..4bf53c0 100644
--- a/Open-ILS/examples/fm_IDL.xml
+++ b/Open-ILS/examples/fm_IDL.xml
@@ -4259,7 +4259,27 @@ SELECT  usr,
 			<link field="aaasc_entries" reltype="has_many" key="xact" map="" class="aaasc"/>
 		</links>
 	</class>
-
+	<class id="auch" 
+		controller="open-ils.cstore" 
+		oils_obj:fieldmapper="action::user_circ_history" 
+		oils_persist:tablename="action.usr_circ_history" 
+		reporter:label="User Checkout History">
+		<fields oils_persist:primary="id" oils_persist:sequence="action.usr_circ_history_id_seq">
+			<field reporter:label="ID" name="id" reporter:datatype="id"/>
+			<!-- prevent reporting on the user that owns the checkout history -->
+			<field reporter:label="User" name="usr" reporter:datatype="link" suppress_controller="open-ils.reporter-store"/>
+			<field reporter:label="Circulating Item" name="target_copy" reporter:datatype="link"/>
+			<field reporter:label="Checkin Time" name="checkin_time" reporter:datatype="timestamp" />
+			<field reporter:label="Due Date" name="due_date" reporter:datatype="timestamp" />
+			<field reporter:label="Checkout Time" name="xact_start" reporter:datatype="timestamp" />
+			<field reporter:label="Source Circulation" name="source_circ" reporter:datatype="link" />
+		</fields>
+		<links>
+			<link field="usr" reltype="has_a" key="id" map="" class="au"/>
+			<link field="target_copy" reltype="has_a" key="id" map="" class="acp"/>
+			<link field="source_circ" reltype="has_a" key="id" map="" class="circ"/>
+		</links>
+	</class>
 	<class id="brt" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="booking::resource_type" oils_persist:tablename="booking.resource_type" reporter:label="Resource Type">
 		<fields oils_persist:primary="id" oils_persist:sequence="booking.resource_type_id_seq">
 			<field reporter:label="Resource Type ID" name="id" reporter:datatype="id" reporter:selector="name"/>
diff --git a/Open-ILS/src/sql/Pg/090.schema.action.sql b/Open-ILS/src/sql/Pg/090.schema.action.sql
index ca1e7f3..f203aa9 100644
--- a/Open-ILS/src/sql/Pg/090.schema.action.sql
+++ b/Open-ILS/src/sql/Pg/090.schema.action.sql
@@ -879,51 +879,6 @@ BEGIN
 END;
 $$ LANGUAGE 'plpgsql';
 
--- Return the list of circ chain heads in xact_start order that the user has chosen to "retain"
-CREATE OR REPLACE FUNCTION action.usr_visible_circs (usr_id INT) RETURNS SETOF action.circulation AS $func$
-DECLARE
-    c               action.circulation%ROWTYPE;
-    view_age        INTERVAL;
-    usr_view_age    actor.usr_setting%ROWTYPE;
-    usr_view_start  actor.usr_setting%ROWTYPE;
-BEGIN
-    SELECT * INTO usr_view_age FROM actor.usr_setting WHERE usr = usr_id AND name = 'history.circ.retention_age';
-    SELECT * INTO usr_view_start FROM actor.usr_setting WHERE usr = usr_id AND name = 'history.circ.retention_start';
-
-    IF usr_view_age.value IS NOT NULL AND usr_view_start.value IS NOT NULL THEN
-        -- User opted in and supplied a retention age
-        IF oils_json_to_text(usr_view_age.value)::INTERVAL > AGE(NOW(), oils_json_to_text(usr_view_start.value)::TIMESTAMPTZ) THEN
-            view_age := AGE(NOW(), oils_json_to_text(usr_view_start.value)::TIMESTAMPTZ);
-        ELSE
-            view_age := oils_json_to_text(usr_view_age.value)::INTERVAL;
-        END IF;
-    ELSIF usr_view_start.value IS NOT NULL THEN
-        -- User opted in
-        view_age := AGE(NOW(), oils_json_to_text(usr_view_start.value)::TIMESTAMPTZ);
-    ELSE
-        -- User did not opt in
-        RETURN;
-    END IF;
-
-    FOR c IN
-        SELECT  *
-          FROM  action.circulation
-          WHERE usr = usr_id
-                AND parent_circ IS NULL
-                AND xact_start > NOW() - view_age
-          ORDER BY xact_start DESC
-    LOOP
-        RETURN NEXT c;
-    END LOOP;
-
-    RETURN;
-END;
-$func$ LANGUAGE PLPGSQL;
-
-CREATE OR REPLACE FUNCTION action.usr_visible_circ_copies( INTEGER ) RETURNS SETOF BIGINT AS $$
-    SELECT DISTINCT(target_copy) FROM action.usr_visible_circs($1)
-$$ LANGUAGE SQL ROWS 10;
-
 CREATE OR REPLACE FUNCTION action.usr_visible_holds (usr_id INT) RETURNS SETOF action.hold_request AS $func$
 DECLARE
     h               action.hold_request%ROWTYPE;
@@ -989,8 +944,6 @@ $func$ LANGUAGE PLPGSQL;
 
 CREATE OR REPLACE FUNCTION action.purge_circulations () RETURNS INT AS $func$
 DECLARE
-    usr_keep_age    actor.usr_setting%ROWTYPE;
-    usr_keep_start  actor.usr_setting%ROWTYPE;
     org_keep_age    INTERVAL;
     org_use_last    BOOL = false;
     org_age_is_min  BOOL = false;
@@ -1052,24 +1005,7 @@ BEGIN
                 last_finished := circ_chain_tail.xact_finish;
             END IF;
 
-            -- Now get the user settings, if any, to block purging if the user wants to keep more circs
-            usr_keep_age.value := NULL;
-            SELECT * INTO usr_keep_age FROM actor.usr_setting WHERE usr = circ_chain_head.usr AND name = 'history.circ.retention_age';
-
-            usr_keep_start.value := NULL;
-            SELECT * INTO usr_keep_start FROM actor.usr_setting WHERE usr = circ_chain_head.usr AND name = 'history.circ.retention_start';
-
-            IF usr_keep_age.value IS NOT NULL AND usr_keep_start.value IS NOT NULL THEN
-                IF oils_json_to_text(usr_keep_age.value)::INTERVAL > AGE(NOW(), oils_json_to_text(usr_keep_start.value)::TIMESTAMPTZ) THEN
-                    keep_age := AGE(NOW(), oils_json_to_text(usr_keep_start.value)::TIMESTAMPTZ);
-                ELSE
-                    keep_age := oils_json_to_text(usr_keep_age.value)::INTERVAL;
-                END IF;
-            ELSIF usr_keep_start.value IS NOT NULL THEN
-                keep_age := AGE(NOW(), oils_json_to_text(usr_keep_start.value)::TIMESTAMPTZ);
-            ELSE
-                keep_age := COALESCE( org_keep_age, '2000 years'::INTERVAL );
-            END IF;
+            keep_age := COALESCE( org_keep_age, '2000 years'::INTERVAL );
 
             IF org_age_is_min THEN
                 keep_age := GREATEST( keep_age, org_keep_age );
@@ -1370,4 +1306,93 @@ $f$ LANGUAGE PLPGSQL;
 
 CREATE TRIGGER hold_copy_proximity_update_tgr BEFORE INSERT OR UPDATE ON action.hold_copy_map FOR EACH ROW EXECUTE PROCEDURE action.hold_copy_calculated_proximity_update ();
 
+CREATE TABLE action.usr_circ_history (
+    id           BIGSERIAL PRIMARY KEY,
+    usr          INTEGER NOT NULL REFERENCES actor.usr(id)
+                 DEFERRABLE INITIALLY DEFERRED,
+    xact_start   TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
+    target_copy  BIGINT NOT NULL REFERENCES asset.copy(id)
+                 DEFERRABLE INITIALLY DEFERRED,
+    due_date     TIMESTAMP WITH TIME ZONE NOT NULL,
+    checkin_time TIMESTAMP WITH TIME ZONE,
+    source_circ  BIGINT REFERENCES action.circulation(id)
+                 ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED
+);
+
+CREATE OR REPLACE FUNCTION action.maintain_usr_circ_history() 
+    RETURNS TRIGGER AS $FUNK$
+DECLARE
+    cur_circ  BIGINT;
+    first_circ BIGINT;
+BEGIN                                                                          
+
+    -- Any retention value signifies history is enabled.
+    -- This assumes that clearing these values via external 
+    -- process deletes the action.usr_circ_history rows.
+    -- TODO: replace these settings w/ a single bool setting?
+    PERFORM 1 FROM actor.usr_setting 
+        WHERE usr = NEW.usr AND value IS NOT NULL AND name IN (
+            'history.circ.retention_age', 
+            'history.circ.retention_start'
+        );
+
+    IF NOT FOUND THEN
+        RETURN NEW;
+    END IF;
+
+    IF TG_OP = 'INSERT' AND NEW.parent_circ IS NULL THEN
+        -- Starting a new circulation.  Insert the history row.
+        INSERT INTO action.usr_circ_history 
+            (usr, xact_start, target_copy, due_date, source_circ)
+        VALUES (
+            NEW.usr, 
+            NEW.xact_start, 
+            NEW.target_copy, 
+            NEW.due_date, 
+            NEW.id
+        );
+
+        RETURN NEW;
+    END IF;
+
+    -- find the first and last circs in the circ chain 
+    -- for the currently modified circ.
+    FOR cur_circ IN SELECT id FROM action.circ_chain(NEW.id) LOOP
+        IF first_circ IS NULL THEN
+            first_circ := cur_circ;
+            CONTINUE;
+        END IF;
+        -- Allow the loop to continue so that at as the loop
+        -- completes cur_circ points to the final circulation.
+    END LOOP;
+
+    IF NEW.id <> cur_circ THEN
+        -- Modifying an intermediate circ.  Ignore it.
+        RETURN NEW;
+    END IF;
+
+    -- Update the due_date/checkin_time on the history row if the current 
+    -- circ is the last circ in the chain and an update is warranted.
+
+    UPDATE action.usr_circ_history 
+        SET 
+            due_date = NEW.due_date,
+            checkin_time = NEW.checkin_time
+        WHERE 
+            source_circ = first_circ 
+            AND (
+                due_date <> NEW.due_date OR (
+                    (checkin_time IS NULL AND NEW.checkin_time IS NOT NULL) OR
+                    (checkin_time IS NOT NULL AND NEW.checkin_time IS NULL) OR
+                    (checkin_time <> NEW.checkin_time)
+                )
+            );
+    RETURN NEW;
+END;                                                                           
+$FUNK$ LANGUAGE PLPGSQL; 
+
+CREATE TRIGGER maintain_usr_circ_history_tgr 
+    AFTER INSERT OR UPDATE ON action.circulation 
+    FOR EACH ROW EXECUTE PROCEDURE action.maintain_usr_circ_history();
+
 COMMIT;
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 5b8fcce..99410a3 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -9445,7 +9445,7 @@ INSERT INTO
 
 INSERT INTO action_trigger.hook (key,core_type,description,passive) VALUES (
         'circ.format.history.email',
-        'circ', 
+        'auch', 
         oils_i18n_gettext(
             'circ.format.history.email',
             'An email has been requested for a circ history.',
@@ -9456,7 +9456,7 @@ INSERT INTO action_trigger.hook (key,core_type,description,passive) VALUES (
     )
     ,(
         'circ.format.history.print',
-        'circ', 
+        'auch', 
         oils_i18n_gettext(
             'circ.format.history.print',
             'A circ history needs to be formatted for printing.',
@@ -9523,7 +9523,11 @@ Subject: Circulation History
             Barcode: [% circ.target_copy.barcode %]
             Checked Out: [% date.format(helpers.format_date(circ.xact_start), '%Y-%m-%d') %]
             Due Date: [% date.format(helpers.format_date(circ.due_date), '%Y-%m-%d') %]
-            Returned: [% date.format(helpers.format_date(circ.checkin_time), '%Y-%m-%d') %]
+            Returned: [%
+                date.format(
+                    helpers.format_date(circ.checkin_time), '%Y-%m-%d') 
+                    IF circ.checkin_time; 
+            %]
     [% END %]
 $$
     )
@@ -9552,7 +9556,11 @@ $$
             <div>Barcode: [% circ.target_copy.barcode %]</div>
             <div>Checked Out: [% date.format(helpers.format_date(circ.xact_start), '%Y-%m-%d') %]</div>
             <div>Due Date: [% date.format(helpers.format_date(circ.due_date), '%Y-%m-%d') %]</div>
-            <div>Returned: [% date.format(helpers.format_date(circ.checkin_time), '%Y-%m-%d') %]</div>
+            <div>Returned: [%
+                date.format(
+                    helpers.format_date(circ.checkin_time), '%Y-%m-%d') 
+                    IF circ.checkin_time; -%]
+            </div>
         </li>
     [% END %]
     </ol>
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.decouple_co_history.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.decouple_co_history.sql
new file mode 100644
index 0000000..5625f0c
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.decouple_co_history.sql
@@ -0,0 +1,338 @@
+
+BEGIN;
+
+-- TODO process to delete history items once the age threshold 
+-- history.circ.retention_age is reached?
+
+-- SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version); 
+
+CREATE TABLE action.usr_circ_history (
+    id           BIGSERIAL PRIMARY KEY,
+    usr          INTEGER NOT NULL REFERENCES actor.usr(id)
+                 DEFERRABLE INITIALLY DEFERRED,
+    xact_start   TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
+    target_copy  BIGINT NOT NULL REFERENCES asset.copy(id)
+                 DEFERRABLE INITIALLY DEFERRED,
+    due_date     TIMESTAMP WITH TIME ZONE NOT NULL,
+    checkin_time TIMESTAMP WITH TIME ZONE,
+    source_circ  BIGINT REFERENCES action.circulation(id)
+                 ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED
+);
+
+CREATE OR REPLACE FUNCTION action.maintain_usr_circ_history() 
+    RETURNS TRIGGER AS $FUNK$
+DECLARE
+    cur_circ  BIGINT;
+    first_circ BIGINT;
+BEGIN                                                                          
+
+    -- Any retention value signifies history is enabled.
+    -- This assumes that clearing these values via external 
+    -- process deletes the action.usr_circ_history rows.
+    -- TODO: replace these settings w/ a single bool setting?
+    PERFORM 1 FROM actor.usr_setting 
+        WHERE usr = NEW.usr AND value IS NOT NULL AND name IN (
+            'history.circ.retention_age', 
+            'history.circ.retention_start'
+        );
+
+    IF NOT FOUND THEN
+        RETURN NEW;
+    END IF;
+
+    IF TG_OP = 'INSERT' AND NEW.parent_circ IS NULL THEN
+        -- Starting a new circulation.  Insert the history row.
+        INSERT INTO action.usr_circ_history 
+            (usr, xact_start, target_copy, due_date, source_circ)
+        VALUES (
+            NEW.usr, 
+            NEW.xact_start, 
+            NEW.target_copy, 
+            NEW.due_date, 
+            NEW.id
+        );
+
+        RETURN NEW;
+    END IF;
+
+    -- find the first and last circs in the circ chain 
+    -- for the currently modified circ.
+    FOR cur_circ IN SELECT id FROM action.circ_chain(NEW.id) LOOP
+        IF first_circ IS NULL THEN
+            first_circ := cur_circ;
+            CONTINUE;
+        END IF;
+        -- Allow the loop to continue so that at as the loop
+        -- completes cur_circ points to the final circulation.
+    END LOOP;
+
+    IF NEW.id <> cur_circ THEN
+        -- Modifying an intermediate circ.  Ignore it.
+        RETURN NEW;
+    END IF;
+
+    -- Update the due_date/checkin_time on the history row if the current 
+    -- circ is the last circ in the chain and an update is warranted.
+
+    UPDATE action.usr_circ_history 
+        SET 
+            due_date = NEW.due_date,
+            checkin_time = NEW.checkin_time
+        WHERE 
+            source_circ = first_circ 
+            AND (
+                due_date <> NEW.due_date OR (
+                    (checkin_time IS NULL AND NEW.checkin_time IS NOT NULL) OR
+                    (checkin_time IS NOT NULL AND NEW.checkin_time IS NULL) OR
+                    (checkin_time <> NEW.checkin_time)
+                )
+            );
+    RETURN NEW;
+END;                                                                           
+$FUNK$ LANGUAGE PLPGSQL; 
+
+CREATE TRIGGER maintain_usr_circ_history_tgr 
+    AFTER INSERT OR UPDATE ON action.circulation 
+    FOR EACH ROW EXECUTE PROCEDURE action.maintain_usr_circ_history();
+
+UPDATE action_trigger.hook 
+    SET core_type = 'auch' 
+    WHERE key ~ '^circ.format.history.'; 
+
+UPDATE action_trigger.event_definition SET template = 
+$$
+[%- USE date -%]
+[%- SET user = target.0.usr -%]
+To: [%- params.recipient_email || user.email %]
+From: [%- params.sender_email || default_sender %]
+Subject: Circulation History
+
+    [% FOR circ IN target %]
+            [% helpers.get_copy_bib_basics(circ.target_copy.id).title %]
+            Barcode: [% circ.target_copy.barcode %]
+            Checked Out: [% date.format(helpers.format_date(circ.xact_start), '%Y-%m-%d') %]
+            Due Date: [% date.format(helpers.format_date(circ.due_date), '%Y-%m-%d') %]
+            Returned: [%
+                date.format(
+                    helpers.format_date(circ.checkin_time), '%Y-%m-%d') 
+                    IF circ.checkin_time; 
+            %]
+    [% END %]
+$$
+WHERE id = 25 AND template = 
+$$
+[%- USE date -%]
+[%- SET user = target.0.usr -%]
+To: [%- params.recipient_email || user.email %]
+From: [%- params.sender_email || default_sender %]
+Subject: Circulation History
+
+    [% FOR circ IN target %]
+            [% helpers.get_copy_bib_basics(circ.target_copy.id).title %]
+            Barcode: [% circ.target_copy.barcode %]
+            Checked Out: [% date.format(helpers.format_date(circ.xact_start), '%Y-%m-%d') %]
+            Due Date: [% date.format(helpers.format_date(circ.due_date), '%Y-%m-%d') %]
+            Returned: [% date.format(helpers.format_date(circ.checkin_time), '%Y-%m-%d') %]
+    [% END %]
+$$;
+
+-- avoid TT undef date errors
+UPDATE action_trigger.event_definition SET template = 
+$$
+[%- USE date -%]
+<div>
+    <style> li { padding: 8px; margin 5px; }</style>
+    <div>[% date.format %]</div>
+    <br/>
+
+    [% user.family_name %], [% user.first_given_name %]
+    <ol>
+    [% FOR circ IN target %]
+        <li>
+            <div>[% helpers.get_copy_bib_basics(circ.target_copy.id).title %]</div>
+            <div>Barcode: [% circ.target_copy.barcode %]</div>
+            <div>Checked Out: [% date.format(helpers.format_date(circ.xact_start), '%Y-%m-%d') %]</div>
+            <div>Due Date: [% date.format(helpers.format_date(circ.due_date), '%Y-%m-%d') %]</div>
+            <div>Returned: [%
+                date.format(
+                    helpers.format_date(circ.checkin_time), '%Y-%m-%d') 
+                    IF circ.checkin_time; -%]
+            </div>
+        </li>
+    [% END %]
+    </ol>
+</div>
+$$
+WHERE id = 26 AND template = -- only replace template if it matches stock
+$$
+[%- USE date -%]
+<div>
+    <style> li { padding: 8px; margin 5px; }</style>
+    <div>[% date.format %]</div>
+    <br/>
+
+    [% user.family_name %], [% user.first_given_name %]
+    <ol>
+    [% FOR circ IN target %]
+        <li>
+            <div>[% helpers.get_copy_bib_basics(circ.target_copy.id).title %]</div>
+            <div>Barcode: [% circ.target_copy.barcode %]</div>
+            <div>Checked Out: [% date.format(helpers.format_date(circ.xact_start), '%Y-%m-%d') %]</div>
+            <div>Due Date: [% date.format(helpers.format_date(circ.due_date), '%Y-%m-%d') %]</div>
+            <div>Returned: [% date.format(helpers.format_date(circ.checkin_time), '%Y-%m-%d') %]</div>
+        </li>
+    [% END %]
+    </ol>
+</div>
+$$;
+
+-- NOTE: ^-- stock CSV template does not include checkin_time, so 
+-- no modifications are required.
+
+-- Create circ history rows for existing circ history data.
+DO $FUNK$
+DECLARE
+    cur_usr   INTEGER;
+    cur_circ  action.circulation%ROWTYPE;
+    last_circ action.circulation%ROWTYPE;
+    counter   INTEGER DEFAULT 1;
+BEGIN
+
+    RAISE NOTICE 
+        'Migrating circ history for % users.  This might take a while...',
+        (SELECT COUNT(DISTINCT(au.id)) FROM actor.usr au
+            JOIN actor.usr_setting aus ON (aus.usr = au.id)
+            WHERE NOT au.deleted AND 
+                aus.name ~ '^history.circ.retention_');
+
+    FOR cur_usr IN 
+        SELECT DISTINCT(au.id)
+            FROM actor.usr au 
+            JOIN actor.usr_setting aus ON (aus.usr = au.id)
+            WHERE NOT au.deleted AND 
+                aus.name ~ '^history.circ.retention_' LOOP
+
+        FOR cur_circ IN SELECT * FROM action.usr_visible_circs(cur_usr) LOOP
+
+            -- Find the last circ in the circ chain.
+            SELECT INTO last_circ * 
+                FROM action.circ_chain(cur_circ.id) 
+                ORDER BY xact_start DESC LIMIT 1;
+
+            -- Create the history row.
+            -- It's OK if last_circ = cur_circ
+            INSERT INTO action.usr_circ_history 
+                (usr, xact_start, target_copy, 
+                    due_date, checkin_time, source_circ)
+            VALUES (
+                cur_circ.usr, 
+                cur_circ.xact_start, 
+                cur_circ.target_copy, 
+                last_circ.due_date, 
+                last_circ.checkin_time,
+                cur_circ.id
+            );
+
+            -- useful for alleviating administrator anxiety.
+            IF counter % 10000 = 0 THEN
+                RAISE NOTICE 'Migrated history for % total users', counter;
+            END IF;
+
+            counter := counter + 1;
+
+        END LOOP;
+    END LOOP;
+
+END $FUNK$;
+
+DROP FUNCTION IF EXISTS action.usr_visible_circs (INTEGER);
+DROP FUNCTION IF EXISTS action.usr_visible_circ_copies (INTEGER);
+
+-- remove user retention age checks
+CREATE OR REPLACE FUNCTION action.purge_circulations () RETURNS INT AS $func$
+DECLARE
+    org_keep_age    INTERVAL;
+    org_use_last    BOOL = false;
+    org_age_is_min  BOOL = false;
+    org_keep_count  INT;
+
+    keep_age        INTERVAL;
+
+    target_acp      RECORD;
+    circ_chain_head action.circulation%ROWTYPE;
+    circ_chain_tail action.circulation%ROWTYPE;
+
+    count_purged    INT;
+    num_incomplete  INT;
+
+    last_finished   TIMESTAMP WITH TIME ZONE;
+BEGIN
+
+    count_purged := 0;
+
+    SELECT value::INTERVAL INTO org_keep_age FROM config.global_flag WHERE name = 'history.circ.retention_age' AND enabled;
+
+    SELECT value::INT INTO org_keep_count FROM config.global_flag WHERE name = 'history.circ.retention_count' AND enabled;
+    IF org_keep_count IS NULL THEN
+        RETURN count_purged; -- Gimme a count to keep, or I keep them all, forever
+    END IF;
+
+    SELECT enabled INTO org_use_last FROM config.global_flag WHERE name = 'history.circ.retention_uses_last_finished';
+    SELECT enabled INTO org_age_is_min FROM config.global_flag WHERE name = 'history.circ.retention_age_is_min';
+
+    -- First, find copies with more than keep_count non-renewal circs
+    FOR target_acp IN
+        SELECT  target_copy,
+                COUNT(*) AS total_real_circs
+          FROM  action.circulation
+          WHERE parent_circ IS NULL
+                AND xact_finish IS NOT NULL
+          GROUP BY target_copy
+          HAVING COUNT(*) > org_keep_count
+    LOOP
+        -- And, for those, select circs that are finished and older than keep_age
+        FOR circ_chain_head IN
+            -- For reference, the subquery uses a window function to order the circs newest to oldest and number them
+            -- The outer query then uses that information to skip the most recent set the library wants to keep
+            -- End result is we don't care what order they come out in, as they are all potentials for deletion.
+            SELECT ac.* FROM action.circulation ac JOIN (
+              SELECT  rank() OVER (ORDER BY xact_start DESC), ac.id
+                FROM  action.circulation ac
+                WHERE ac.target_copy = target_acp.target_copy
+                  AND ac.parent_circ IS NULL
+                ORDER BY ac.xact_start ) ranked USING (id)
+                WHERE ranked.rank > org_keep_count
+        LOOP
+
+            SELECT * INTO circ_chain_tail FROM action.circ_chain(circ_chain_head.id) ORDER BY xact_start DESC LIMIT 1;
+            SELECT COUNT(CASE WHEN xact_finish IS NULL THEN 1 ELSE NULL END), MAX(xact_finish) INTO num_incomplete, last_finished FROM action.circ_chain(circ_chain_head.id);
+            CONTINUE WHEN circ_chain_tail.xact_finish IS NULL OR num_incomplete > 0;
+
+            IF NOT org_use_last THEN
+                last_finished := circ_chain_tail.xact_finish;
+            END IF;
+
+            keep_age := COALESCE( org_keep_age, '2000 years'::INTERVAL );
+
+            IF org_age_is_min THEN
+                keep_age := GREATEST( keep_age, org_keep_age );
+            END IF;
+
+            CONTINUE WHEN AGE(NOW(), last_finished) < keep_age;
+
+            -- We've passed the purging tests, purge the circ chain starting at the end
+            -- A trigger should auto-purge the rest of the chain.
+            DELETE FROM action.circulation WHERE id = circ_chain_tail.id;
+
+            count_purged := count_purged + 1;
+
+        END LOOP;
+    END LOOP;
+
+    return count_purged;
+END;
+$func$ LANGUAGE PLPGSQL;
+
+
+COMMIT;
+

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

Summary of changes:
 Open-ILS/examples/fm_IDL.xml                       |   22 +-
 .../src/perlmods/lib/OpenILS/Application/Actor.pm  |  106 +++-
 .../lib/OpenILS/Application/Search/Biblio.pm       |    4 +-
 .../lib/OpenILS/WWW/EGCatLoader/Account.pm         |  160 ++++--
 Open-ILS/src/sql/Pg/002.schema.config.sql          |    2 +-
 Open-ILS/src/sql/Pg/090.schema.action.sql          |  155 +++--
 Open-ILS/src/sql/Pg/950.data.seed-values.sql       |   18 +-
 Open-ILS/src/sql/Pg/999.functions.global.sql       |    1 +
 Open-ILS/src/sql/Pg/live_t/user-circ-history.pg    |   85 +++
 .../Pg/upgrade/0960.schema.decouple_co_history.sql |  667 ++++++++++++++++++++
 Open-ILS/src/templates/opac/css/style.css.tt2      |    6 +
 .../src/templates/opac/myopac/prefs_settings.tt2   |   21 +-
 .../Circulation/user-circ-history.adoc             |   54 ++
 13 files changed, 1167 insertions(+), 134 deletions(-)
 create mode 100644 Open-ILS/src/sql/Pg/live_t/user-circ-history.pg
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/0960.schema.decouple_co_history.sql
 create mode 100644 docs/RELEASE_NOTES_NEXT/Circulation/user-circ-history.adoc


hooks/post-receive
-- 
Evergreen ILS


More information about the open-ils-commits mailing list