LP1207533 patron triggered events log
authorJason Etheridge <jason@EquinoxInitiative.org>
Wed, 29 Jul 2020 12:11:33 +0000 (08:11 -0400)
committerChris Sharp <csharp@georgialibraries.org>
Tue, 21 Sep 2021 20:18:23 +0000 (16:18 -0400)
* first cut at schema
* setting context_user, context_library, and context_bib on action_trigger.event
  when building the environment
* toward UI
  The original interface still exists and is used when spawned from Item Status,
  but for the patron interface, the Other -> Triggered Events / Notifications
  action will now spawn a new tab with the new interface.
* data retention
  Break the link between actor.usr and action_trigger.event when purging user data
  or aging circulations (as best as we can; some textual links may exist in
  action_trigger.event_output--i.e. overdue notices)
* release notes
* live tests

Signed-off-by: Jason Etheridge <jason@EquinoxInitiative.org>
Signed-off-by: Mike Rylander <mrylander@gmail.com>
Signed-off-by: Dawn Dale <ddale@georgialibraries.org>
Signed-off-by: Chris Sharp <csharp@georgialibraries.org>

18 files changed:
Open-ILS/examples/fm_IDL.xml
Open-ILS/src/eg2/src/app/staff/circ/patron/event-log/event-grid.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/patron/event-log/event-grid.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/patron/event-log/event-log.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/patron/event-log/event-log.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/patron/event-log/event-log.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/patron/event-log/routing.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/patron/routing.module.ts
Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Event.pm
Open-ILS/src/perlmods/live_t/32-lp1207533-triggered-events.t [new file with mode: 0644]
Open-ILS/src/sql/Pg/090.schema.action.sql
Open-ILS/src/sql/Pg/400.schema.action_trigger.sql
Open-ILS/src/sql/Pg/950.data.seed-values.sql
Open-ILS/src/sql/Pg/999.functions.global.sql
Open-ILS/src/sql/Pg/upgrade/XXXX.schema.triggered_event_log.sql [new file with mode: 0644]
Open-ILS/src/sql/Pg/upgrade/YYYY.functions.triggered_event_log.sql [new file with mode: 0644]
Open-ILS/src/templates/staff/circ/patron/index.tt2
docs/RELEASE_NOTES_NEXT/Circulation/PatronTriggeredEventsLog.adoc [new file with mode: 0644]

index c068c2c..fc20984 100644 (file)
@@ -1454,6 +1454,9 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <field reporter:label="Environment Entries" name="env" oils_persist:virtual="true"  reporter:datatype="link"/>
                        <field reporter:label="Parameters" name="params" oils_persist:virtual="true"  reporter:datatype="link"/>
                        <field reporter:label="Retention Interval" name="retention_interval" reporter:datatype="interval"/>
+                       <field reporter:label="Context User Path" name="context_usr_path" reporter:datatype="text"/>
+                       <field reporter:label="Context Library Path" name="context_library_path" reporter:datatype="text"/>
+                       <field reporter:label="Context Bib Path" name="context_bib_path" reporter:datatype="text"/>
                </fields>
                <links>
                        <link field="owner" reltype="has_a" key="id" map="" class="aou"/>
@@ -1548,12 +1551,18 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <field reporter:label="Error Output" name="error_output" reporter:datatype="text"/>
                        <field reporter:label="Asynchronous Output" name="async_output" reporter:datatype="link"/>
                        <field reporter:label="Update Process" name="update_process" reporter:datatype="int"/>
+                       <field reporter:label="Context User" name="context_user" reporter:datatype="link"/>
+                       <field reporter:label="Context Library" name="context_library" reporter:datatype="link"/>
+                       <field reporter:label="Context Bib" name="context_bib" reporter:datatype="link"/>
                </fields>
                <links>
                        <link field="event_def" reltype="has_a" key="id" map="" class="atevdef"/>
                        <link field="template_output" reltype="has_a" key="id" map="" class="ateo"/>
                        <link field="error_output" reltype="has_a" key="id" map="" class="ateo"/>
                        <link field="async_output" reltype="has_a" key="id" map="" class="ateo"/>
+                       <link field="context_user" reltype="has_a" key="id" map="" class="au"/>
+                       <link field="context_library" reltype="has_a" key="id" map="" class="aou"/>
+                       <link field="context_bib" reltype="has_a" key="id" map="" class="bre"/>
                </links>
        </class>
 
@@ -1663,6 +1672,91 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                </permacrud>
        </class>
 
+       <class id="atoul" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="action_trigger::optimized_user_log" reporter:label="Action Trigger Optimized User Log" oils_persist:readonly="true">
+               <oils_persist:source_definition><![CDATA[
+               SELECT  atevdef.hook,
+                       atevdef.name,
+                       atevdef.reactor,
+                       atev.id,
+                       atev.event_def,
+                       atev.add_time,
+                       atev.run_time,
+                       atev.start_time,
+                       atev.update_time,
+                       atev.complete_time,
+                       atev.update_process,
+                       atev.state,
+                       atev.user_data,
+                       atev.template_output,
+                       atev.error_output,
+                       atev.async_output,
+            atev.target,
+                       CASE WHEN ath.core_type = 'circ' THEN atev.target END AS target_circ,
+                       CASE WHEN ath.core_type = 'ahr' THEN atev.target END AS target_hold,
+            atev.context_user,
+                       atev.context_library,
+            atev.context_bib,
+            rssr.title,
+            rssr.author
+               FROM action_trigger.event atev
+               JOIN action_trigger.event_definition atevdef ON
+                       (atevdef.id = atev.event_def)
+               JOIN action_trigger.hook ath ON
+                       (ath.key = atevdef.hook AND ath.core_type IN ('circ','ahr'))
+        LEFT JOIN reporter.super_simple_record rssr ON
+            (atev.context_bib = rssr.id)
+               WHERE atev.add_time > NOW() - (SELECT MIN(value) FROM (
+                       SELECT value::INTERVAL FROM actor.org_unit_ancestor_setting(
+                               'circ.staff.max_visible_event_age',
+                               atev.context_library
+                       ) UNION
+                       SELECT '1000 YEARS'::INTERVAL AS value
+               ) ous)
+               ]]></oils_persist:source_definition>
+               <fields oils_persist:primary="id">
+                       <field reporter:label="Hook" name="hook" reporter:datatype="link" />
+                       <field reporter:label="Name" name="name" reporter:datatype="text" />
+                       <field reporter:label="Reactor" name="reactor" reporter:datatype="text" />
+                       <field reporter:label="Event ID" name="id" reporter:datatype="id" />
+                       <field reporter:label="Event Definition ID" name="event_def" reporter:datatype="int" />
+                       <field reporter:label="Event Add Time" name="add_time" reporter:datatype="timestamp" />
+                       <field reporter:label="Event Run Time" name="run_time" reporter:datatype="timestamp" />
+                       <field reporter:label="Event Start Time" name="start_time" reporter:datatype="timestamp" />
+                       <field reporter:label="Event Update Time" name="update_time" reporter:datatype="timestamp" />
+                       <field reporter:label="Event Complete Time" name="complete_time" reporter:datatype="timestamp" />
+                       <field reporter:label="Event Update PID" name="update_process" reporter:datatype="int" />
+                       <field reporter:label="Event State" name="state" reporter:datatype="text" />
+                       <field reporter:label="Event User Data" name="user_data" reporter:datatype="text" />
+                       <field reporter:label="Event Template Output" name="template_output" reporter:datatype="link" />
+                       <field reporter:label="Event Error Output" name="error_output" reporter:datatype="link" />
+                       <field reporter:label="Event Async Output" name="async_output" reporter:datatype="link" />
+                       <field reporter:label="Event Target Object ID" name="target" reporter:datatype="link" />
+                       <field reporter:label="Target Circulation" name="target_circ" reporter:datatype="link" />
+                       <field reporter:label="Target Hold" name="target_hold" reporter:datatype="link" />
+                       <field reporter:label="Context User" name="context_user" reporter:datatype="link" />
+                       <field reporter:label="Context Library" name="context_library" reporter:datatype="org_unit" />
+                       <field reporter:label="Context Bib" name="context_bib" reporter:datatype="link" />
+                       <field reporter:label="Title" name="title" reporter:datatype="text" />
+                       <field reporter:label="Author" name="author" reporter:datatype="text" />
+               </fields>
+               <links>
+                       <link field="hook" reltype="has_a" key="key" map="" class="ath" />
+                       <link field="template_output" reltype="has_a" key="id" map="" class="ateo" />
+                       <link field="error_output" reltype="has_a" key="id" map="" class="ateo" />
+                       <link field="async_output" reltype="has_a" key="id" map="" class="ateo" />
+                       <link field="target_circ" reltype="has_a" key="id" map="" class="circ" />
+                       <link field="target_hold" reltype="has_a" key="id" map="" class="ahr" />
+                       <link field="context_user" reltype="has_a" key="id" map="" class="au" />
+                       <link field="context_library" reltype="has_a" key="id" map="" class="aou" />
+                       <link field="context_bib" reltype="has_a" key="id" map="" class="bre" />
+               </links>
+               <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+                       <actions>
+                               <retrieve permission="VIEW_TRIGGER_EVENT" context_field="perm_lib" />
+                       </actions>
+               </permacrud>
+       </class>
+
        <class id="aws" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="actor::workstation" oils_persist:tablename="actor.workstation" reporter:label="Workstation">
                <fields oils_persist:primary="id" oils_persist:sequence="actor.workstation_id_seq">
                        <field reporter:label="Workstation ID" name="id" reporter:datatype="id"/>
@@ -10872,7 +10966,7 @@ SELECT  usr,
                        <link field="id" reltype="might_have" key="id" map="" class="circ"/>
                </links>
        </class>
-       <class id="rhrr" controller="open-ils.reporter-store open-ils.cstore" oils_obj:fieldmapper="reporter::hold_request_record" oils_persist:tablename="reporter.hold_request_record" reporter:label="Hold Request Record">
+       <class id="rhrr" controller="open-ils.reporter-store open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="reporter::hold_request_record" oils_persist:tablename="reporter.hold_request_record" reporter:label="Hold Request Record">
                <fields oils_persist:primary="id">
                        <field reporter:label="Hold ID" name="id" reporter:datatype="id" />
                        <field reporter:label="Hold Target" name="target" reporter:datatype="int" />
@@ -10883,6 +10977,13 @@ SELECT  usr,
                        <link field="id" reltype="might_have" key="id" map="" class="ahr"/>
                        <link field="bib_record" reltype="has_a" key="id" map="" class="bre"/>
                </links>
+               <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+                       <actions>
+                               <retrieve permission="VIEW_HOLD">
+                                       <context link="id" field="pickup_lib"/>
+                               </retrieve>
+                       </actions>
+               </permacrud>
        </class>
        <class id="rxbt" controller="open-ils.reporter-store" oils_obj:fieldmapper="reporter::xact_billing_totals" oils_persist:tablename="reporter.xact_billing_totals" reporter:label="Transaction Billing Totals">
                <fields oils_persist:primary="xact">
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/event-log/event-grid.component.html b/Open-ILS/src/eg2/src/app/staff/circ/patron/event-log/event-grid.component.html
new file mode 100644 (file)
index 0000000..0605678
--- /dev/null
@@ -0,0 +1,15 @@
+<eg-grid #grid [dataSource]="gridSource" idlClass="atoul"
+  [sortable]="true"
+  [filterable]="true"
+  showFields="perm_lib,state,name,reactor,run_time,target_circ.target_copy.barcode,target_hold.current_copy.barcode,title,author"
+  ignoreFields="target_circ,target_hold"
+  persistKey="event_grid">
+  <eg-grid-toolbar-action label="Reset selected events" i18n-label
+    (onClick)="act_on_events('reset',$event)" [disableOnRows]="noRowSelected">
+  </eg-grid-toolbar-action>
+  <eg-grid-toolbar-action label="Cancel selected events" i18n-label
+    (onClick)="act_on_events('cancel',$event)" [disableOnRows]="noRowSelected">
+  </eg-grid-toolbar-action>
+  <eg-grid-column *ngIf="event_type=='circ'" [sortable]="false" [filterable]="false" path="target_circ.target_copy.barcode"></eg-grid-column>
+  <eg-grid-column *ngIf="event_type=='hold'" [sortable]="false" [filterable]="false" path="target_hold.current_copy.barcode"></eg-grid-column>
+</eg-grid>
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/event-log/event-grid.component.ts b/Open-ILS/src/eg2/src/app/staff/circ/patron/event-log/event-grid.component.ts
new file mode 100644 (file)
index 0000000..385754e
--- /dev/null
@@ -0,0 +1,130 @@
+import {Component, EventEmitter, Input, Output, OnChanges, OnInit, ViewChild} from '@angular/core';
+import {Router} from '@angular/router';
+import {Observable, from, of} from 'rxjs';
+import {map, tap, switchMap, mergeMap} from 'rxjs/operators';
+import {AuthService} from '@eg/core/auth.service';
+import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
+import {FormatService} from '@eg/core/format.service';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {GridDataSource} from '@eg/share/grid/grid';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {EventService} from '@eg/core/event.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {Pager} from '@eg/share/util/pager';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {NetService} from '@eg/core/net.service';
+import {OrgService} from '@eg/core/org.service';
+import {BibRecordService, BibRecordSummary} from '@eg/share/catalog/bib-record.service';
+
+// A filterable grid of A/T events for circ or ahr hook core types
+
+@Component({
+    selector: 'eg-event-grid',
+    templateUrl: './event-grid.component.html'
+})
+
+export class EventGridComponent implements OnChanges, OnInit {
+
+    @Input() patron: number;
+    @Input() event_type: string;
+
+    gridSource: GridDataSource;
+    numRowsSelected: number;
+
+    act_on_events: (action: string, rows: IdlObject[]) => void;
+    noRowSelected: (rows: IdlObject[]) => boolean;
+
+    @ViewChild('grid', { static: true }) grid: GridComponent;
+
+    constructor(
+        private idl: IdlService,
+        private auth: AuthService,
+        private bib: BibRecordService,
+        private format: FormatService,
+        private pcrud: PcrudService,
+        private router: Router,
+        private toast: ToastService,
+        private net: NetService,
+        private evt: EventService,
+        private org: OrgService
+    ) {
+
+    }
+
+    ngOnInit() {
+        this.gridSource = new GridDataSource();
+
+        this.gridSource.getRows = (pager: Pager, sort: any[]): Observable<IdlObject> => {
+        // TODO: why is this getting called twice on page load?
+
+            const orderBy: any = {atoul: 'id'};
+            if (sort.length) {
+                orderBy.atoul = sort[0].name + ' ' + sort[0].dir;
+            }
+
+            // base query to grab everything
+            const base: Object = {};
+            base[this.idl.classes['atoul'].pkey] = {'!=' : null};
+            base['context_user'] = (this.patron ? this.patron : {'>' : 0})
+
+            // circs or holds?
+            if (this.event_type == 'circ') {
+                base['target_circ'] = { '>' : 0 }
+            } else {
+                base['target_hold'] = { '>' : 0 }
+            }
+
+            const query: any = new Array();
+            query.push(base);
+
+            // and add any filters
+            Object.keys(this.gridSource.filters).forEach(key => {
+                Object.keys(this.gridSource.filters[key]).forEach(key2 => {
+                    query.push(this.gridSource.filters[key][key2]);
+                });
+            });
+
+            return this.pcrud.search('atoul',
+                query, {
+                flesh: 3,
+                flesh_fields: {
+                    atoul: ['target_circ', 'target_hold'],
+                    circ: ['target_copy'],
+                    ahr: ['current_copy']
+                },
+                offset: pager.offset,
+                limit: pager.limit,
+                order_by: orderBy
+            });
+        };
+
+        this.act_on_events = (action: string, rows: IdlObject[]) => {
+            this.net.request(
+                'open-ils.actor',
+                'open-ils.actor.user.event.' + action + '.batch',
+                this.auth.token(), rows.map( event => event.id() )
+            ).subscribe(
+                (res) => {
+                    if (this.evt.parse(res)) {
+                        console.error('parsed error response',res);
+                    } else {
+                        console.log('success',res);
+                    }
+                },
+                (err) => {
+                    console.error('error',err);
+                },
+                () => {
+                    console.log('finis');
+                    this.grid.reload();
+                }
+            );
+        }
+
+        this.noRowSelected = (rows: IdlObject[]) => (rows.length == 0);
+    }
+
+    ngOnChanges() { this.reloadGrid(); }
+
+    reloadGrid() { this.grid.reload(); }
+}
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/event-log/event-log.component.html b/Open-ILS/src/eg2/src/app/staff/circ/patron/event-log/event-log.component.html
new file mode 100644 (file)
index 0000000..f16f69f
--- /dev/null
@@ -0,0 +1,20 @@
+
+<eg-staff-banner bannerText="Triggered Event Log (Patron Specific)" i18n-bannerText>
+</eg-staff-banner>
+
+<ul ngbNav #nav="ngbNav" class="nav-tabs">
+  <li [ngbNavItem]="1">
+    <a ngbNavLink i18n>Circulations</a>
+    <ng-template ngbNavContent>
+      <eg-event-grid #eventGrid [patron]="patronId" event_type="circ"></eg-event-grid>
+    </ng-template>
+  </li>
+  <li [ngbNavItem]="2">
+    <a ngbNavLink i18n>Holds</a>
+    <ng-template ngbNavContent>
+      <eg-event-grid #eventGrid [patron]="patronId" event_type="hold"></eg-event-grid>
+    </ng-template>
+  </li>
+</ul>
+
+<div [ngbNavOutlet]="nav" class="mt-2"></div>
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/event-log/event-log.component.ts b/Open-ILS/src/eg2/src/app/staff/circ/patron/event-log/event-log.component.ts
new file mode 100644 (file)
index 0000000..f707467
--- /dev/null
@@ -0,0 +1,30 @@
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {ActivatedRoute} from '@angular/router';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {EventGridComponent} from './event-grid.component';
+
+@Component({
+  templateUrl: 'event-log.component.html'
+})
+
+export class EventLogComponent implements OnInit {
+    patronId: number;
+
+    @ViewChild('eventGrid', { static: true }) eventGrid: EventGridComponent;
+
+    constructor(
+        private route: ActivatedRoute,
+        private net: NetService,
+        private auth: AuthService
+    ) {}
+
+    ngOnInit() {
+        // Note: if this is not supplied, the grid will show recent events
+        // across all patrons, which may be a neat feature...
+        // TODO: see if we're honoring VIEW_USER permission and patron opt-in
+        this.patronId = +this.route.snapshot.paramMap.get('patron');
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/event-log/event-log.module.ts b/Open-ILS/src/eg2/src/app/staff/circ/patron/event-log/event-log.module.ts
new file mode 100644 (file)
index 0000000..868a470
--- /dev/null
@@ -0,0 +1,19 @@
+import {NgModule} from '@angular/core';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {EventLogRoutingModule} from './routing.module';
+import {EventGridComponent} from './event-grid.component';
+import {EventLogComponent} from './event-log.component';
+
+@NgModule({
+  declarations: [
+    EventGridComponent,
+    EventLogComponent
+  ],
+  imports: [
+    StaffCommonModule,
+    EventLogRoutingModule,
+  ],
+})
+
+export class EventLogModule {}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/event-log/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/circ/patron/event-log/routing.module.ts
new file mode 100644 (file)
index 0000000..7196d31
--- /dev/null
@@ -0,0 +1,19 @@
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {EventLogComponent} from './event-log.component';
+
+const routes: Routes = [
+  { path: '',
+    component: EventLogComponent
+  },
+  { path: ':patron',
+    component: EventLogComponent
+  },
+];
+
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule]
+})
+
+export class EventLogRoutingModule {}
index c2b7432..a1b4ae6 100644 (file)
@@ -5,6 +5,10 @@ const routes: Routes = [
   { path: 'bcsearch',
     loadChildren: () =>
       import('./bcsearch/bcsearch.module').then(m => m.BcSearchModule)
+  },
+  { path: 'event-log',
+    loadChildren: () =>
+      import('./event-log/event-log.module').then(m => m.EventLogModule)
   }
 ];
 
index 1c0286b..66df9c9 100644 (file)
@@ -521,6 +521,42 @@ sub build_environment {
                 $self->_object_by_path( $self->event->event_def, undef, [qw/usr_message sending_lib/], ['owner'] );
             }
         }
+
+        if ($self->event->event_def->context_usr_path) {
+            my @usr_path = split(/\./, $self->event->event_def->context_usr_path);
+            $self->_object_by_path( $self->target, undef, [qw/context usr/], \@usr_path );
+
+            if ($self->event->event_def->context_bib_path) {
+                my @bib_path = split(/\./, $self->event->event_def->context_bib_path);
+                $self->_object_by_path( $self->target, undef, [qw/context bib/], \@bib_path );
+                if (ref $self->environment->{context}->{bib} eq 'ARRAY') {
+                    $self->environment->{context}->{bib} = $self->environment->{context}->{bib}->[0];
+                }
+                if ($self->environment->{context}->{bib}->isa('Fieldmapper::biblio::record_entry')) {
+                    $self->environment->{context}->{bib} = $self->environment->{context}->{bib}->id;
+                } elsif ($self->environment->{context}->{bib}->isa('Fieldmapper::reporter::hold_request_record')) {
+                    $self->environment->{context}->{bib} = $self->environment->{context}->{bib}->bib_record;
+                }
+            }
+
+            if ($self->event->event_def->context_library_path) {
+                my @library_path = split(/\./, $self->event->event_def->context_library_path);
+                $self->_object_by_path( $self->target, undef, [qw/context org/], \@library_path );
+            } else {
+                $self->_object_by_path( $self->event->event_def, undef, [qw/context org/], ['owner'] );
+            }
+            $self->update_state(
+                $self->event->state, {
+                    'context_user' => $self->environment->{context}->{usr}
+                        ? $self->environment->{context}->{usr}->id
+                        : undef,
+                    'context_library' => $self->environment->{context}->{org}
+                        ? $self->environment->{context}->{org}->id
+                        : undef,
+                    'context_bib' => $self->environment->{context}->{bib}
+                }
+            );
+        }
     
         $self->environment->{complete} = 1;
     } otherwise {
diff --git a/Open-ILS/src/perlmods/live_t/32-lp1207533-triggered-events.t b/Open-ILS/src/perlmods/live_t/32-lp1207533-triggered-events.t
new file mode 100644 (file)
index 0000000..a611182
--- /dev/null
@@ -0,0 +1,119 @@
+#!perl
+
+use strict; use warnings;
+use Test::More tests => 10;
+use OpenILS::Utils::TestUtils;
+use OpenILS::Utils::CStoreEditor qw/:funcs/;
+use OpenILS::Application::AppUtils;
+
+diag("Test patron triggered event log infrastructure");
+
+use constant WORKSTATION_NAME => 'BR4-test-02-simple-circ.t'; # we'll just re-use this
+use constant WORKSTATION_LIB => 7;
+use constant ITEM_BARCODE => 'CONC70000345';
+use constant ITEM_ID => 310;
+
+my $script = OpenILS::Utils::TestUtils->new();
+our $apputils = 'OpenILS::Application::AppUtils';
+
+# -----------------------------------------------------------------------------
+# 0. Let's get our auth token
+# -----------------------------------------------------------------------------
+
+$script->authenticate({
+    username => 'admin',
+    password => 'demo123',
+    type => 'staff',
+    workstation => WORKSTATION_NAME});
+my $authtoken = $script->authtoken;
+ok(
+    $authtoken,
+    'Have an authtoken associated with the workstation'
+);
+
+# -----------------------------------------------------------------------------
+# 1. Let's create an easy A/T event definition template for circs
+# -----------------------------------------------------------------------------
+
+my $e = new_editor(xact => 1);
+$e->init;
+
+my $atevdef = Fieldmapper::action_trigger::event_definition->new;
+$atevdef->active(1);
+$atevdef->owner(1);
+$atevdef->name('circ event test');
+$atevdef->hook('checkout');
+$atevdef->validator('NOOP_True');
+$atevdef->reactor('NOOP_True');
+$atevdef->delay('0');
+$atevdef->delay_field('xact_start');
+$atevdef->group_field('usr');
+$atevdef->context_usr_path('usr');
+$atevdef->context_library_path('circ_lib');
+$atevdef->context_bib_path('target_copy.call_number.record');
+
+$e->create_action_trigger_event_definition( $atevdef );
+$e->commit;
+
+my $defs = $e->search_action_trigger_event_definition({name => 'circ event test'});
+is(scalar(@$defs), 1, 'Successfully created atevdef');
+
+my $def_id = $defs->[0]->id;
+diag("def id = $def_id");
+
+# ---------------------------------------------------------------------------------
+# 3. Let's redo an earlier circulation from another test and get an event this time
+# ---------------------------------------------------------------------------------
+
+my $checkout_resp = $script->do_checkout({
+    patron => 1,
+    barcode => ITEM_BARCODE});
+is(
+    ref $checkout_resp,
+    'HASH',
+    'Checkout request returned a HASH'
+);
+is(
+    $checkout_resp->{ilsevent},
+    0,
+    'Checkout returned a SUCCESS event'
+);
+
+my $circ_id = $checkout_resp->{payload}->{circ}->id;
+
+diag("circ id = $circ_id");
+# -----------------------------------------------------------------------------
+# 4. Let's find said event
+# -----------------------------------------------------------------------------
+
+sleep 2; # race condition
+
+my $events = $e->search_action_trigger_event({event_def => $def_id, target => $circ_id});
+is(scalar(@$events), 1, 'Found event');
+
+# -----------------------------------------------------------------------------
+# 5. Let's run action_trigger_runner to flesh said event
+# -----------------------------------------------------------------------------
+
+my $command = '/openils/bin/action_trigger_runner.pl --osrf-config /openils/conf/opensrf_core.xml --run-pending --verbose';
+chomp(my $output = `$command`);
+like($output, qr/run_pending: NON-GRANULAR/, 'action_trigger_runner.pl ran correctly');
+
+# -----------------------------------------------------------------------------
+# 6. Let's re-fetch the event and see if it's fleshed
+# -----------------------------------------------------------------------------
+
+sleep 2; # race condition
+
+$events = $e->search_action_trigger_event({event_def => $def_id, target => $circ_id});
+is(scalar(@$events), 1, 'Found event');
+
+my $event = $events->[0];
+
+is($event->context_user, 1, 'context_user is correct');
+is($event->context_library, 7, 'context_library is correct');
+is($event->context_bib, 10, 'context_bib is correct');
+
+#use Data::Dumper::Perltidy;
+#diag( Dumper($event) );
index 813ab0b..711269e 100644 (file)
@@ -364,6 +364,24 @@ BEGIN
         PERFORM money.age_billings_and_payments_for_xact(OLD.id);
     END IF;
 
+    -- Break the link with the user in action_trigger.event (warning: event_output may essentially have this information)
+    UPDATE
+        action_trigger.event e
+    SET
+        context_user = NULL
+    FROM
+        action.all_circulation c
+    WHERE
+            c.id = OLD.id
+        AND e.context_user = c.usr
+        AND e.target = c.id
+        AND e.event_def IN (
+            SELECT id
+            FROM action_trigger.event_definition
+            WHERE hook in (SELECT key FROM action_trigger.hook WHERE core_type = 'circ')
+        )
+    ;
+
     RETURN OLD;
 END;
 $$ LANGUAGE 'plpgsql';
index 8a0c213..80e4f8d 100644 (file)
@@ -197,6 +197,10 @@ CREATE TABLE action_trigger.event_definition (
     template        TEXT,                 -- the TT block.  will have an 'environment' hash (or array of hashes, grouped events) built up by validator and collector(s), which can be modified.
     granularity     TEXT,   -- could specify a batch which is the only time these events should actually run
 
+    context_usr_path        TEXT, -- for optimizing action_trigger.event
+    context_library_path    TEXT, -- '''
+    context_bib_path        TEXT, -- '''
+
     message_template        TEXT,
     message_usr_path        TEXT,
     message_library_path    TEXT,
@@ -275,13 +279,18 @@ CREATE TABLE action_trigger.event (
     user_data       TEXT        CHECK (user_data IS NULL OR is_json( user_data )),
     template_output BIGINT      REFERENCES action_trigger.event_output (id),
     error_output    BIGINT      REFERENCES action_trigger.event_output (id),
-    async_output    BIGINT      REFERENCES action_trigger.event_output (id)
+    async_output    BIGINT      REFERENCES action_trigger.event_output (id),
+    context_user    INT         REFERENCES actor.usr (id),
+    context_library INT         REFERENCES actor.org_unit (id),
+    context_bib     BIGINT      REFERENCES biblio.record_entry (id)
 );
 CREATE INDEX atev_target_def_idx ON action_trigger.event (target,event_def);
 CREATE INDEX atev_def_state ON action_trigger.event (event_def,state);
 CREATE INDEX atev_template_output ON action_trigger.event (template_output);
 CREATE INDEX atev_async_output ON action_trigger.event (async_output);
 CREATE INDEX atev_error_output ON action_trigger.event (error_output);
+CREATE INDEX atev_context_user ON action_trigger.event (context_user);
+CREATE INDEX atev_context_library ON action_trigger.event (context_library);
 
 CREATE TABLE action_trigger.event_params (
     id          BIGSERIAL   PRIMARY KEY,
index 8fc21ae..3ec8dc6 100644 (file)
@@ -17553,6 +17553,30 @@ INSERT INTO action_trigger.environment (
 INSERT INTO action_trigger.event_params (event_def, param, value)
     VALUES (currval('action_trigger.event_definition_id_seq'), 'check_sms_notify', 1);
 
+UPDATE
+    action_trigger.event_definition
+SET
+    context_usr_path = 'usr',
+    context_library_path = 'circ_lib',
+    context_bib_path = 'target_copy.call_number.record'
+WHERE
+    hook IN (
+        SELECT key FROM action_trigger.hook WHERE core_type = 'circ'
+    )
+;
+
+UPDATE
+    action_trigger.event_definition
+SET
+    context_usr_path = 'usr',
+    context_library_path = 'pickup_lib',
+    context_bib_path = 'bib_rec'
+WHERE
+    hook IN (
+        SELECT key FROM action_trigger.hook WHERE core_type = 'ahr'
+    )
+;
+
 INSERT INTO config.org_unit_setting_type
 (name, grp, label, description, datatype)
 VALUES
index 4722e23..3827544 100644 (file)
@@ -422,6 +422,9 @@ BEGIN
                dest_usr := specified_dest_usr;
        END IF;
 
+    -- action_trigger.event (even doing this, event_output may--and probably does--contain PII and should have a retention/removal policy)
+    UPDATE action_trigger.event SET context_user = dest_usr WHERE context_user = src_usr;
+
        -- acq.*
        UPDATE acq.fund_allocation SET allocator = dest_usr WHERE allocator = src_usr;
        UPDATE acq.lineitem SET creator = dest_usr WHERE creator = src_usr;
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.triggered_event_log.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.triggered_event_log.sql
new file mode 100644 (file)
index 0000000..203a6b3
--- /dev/null
@@ -0,0 +1,59 @@
+BEGIN;
+
+SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+--    context_usr_path        TEXT, -- for optimizing action_trigger.event
+--    context_library_path    TEXT, -- '''
+--    context_bib_path        TEXT, -- '''
+ALTER TABLE action_trigger.event_definition ADD COLUMN context_usr_path TEXT;
+ALTER TABLE action_trigger.event_definition ADD COLUMN context_library_path TEXT;
+ALTER TABLE action_trigger.event_definition ADD COLUMN context_bib_path TEXT;
+
+--    context_user    INT         REFERENCES actor.usr (id),
+--    context_library INT         REFERENCES actor.org_unit (id),
+--    context_bib     BIGINT      REFERENCES biblio.record_entry (id)
+ALTER TABLE action_trigger.event ADD COLUMN context_user INT REFERENCES actor.usr (id);
+ALTER TABLE action_trigger.event ADD COLUMN context_library INT REFERENCES actor.org_unit (id);
+ALTER TABLE action_trigger.event ADD COLUMN context_bib BIGINT REFERENCES biblio.record_entry (id);
+CREATE INDEX atev_context_user ON action_trigger.event (context_user);
+CREATE INDEX atev_context_library ON action_trigger.event (context_library);
+
+UPDATE
+    action_trigger.event_definition
+SET
+    context_usr_path = 'usr',
+    context_library_path = 'circ_lib',
+    context_bib_path = 'target_copy.call_number.record'
+WHERE
+    hook IN (
+        SELECT key FROM action_trigger.hook WHERE core_type = 'circ'
+    )
+;
+
+UPDATE
+    action_trigger.event_definition
+SET
+    context_usr_path = 'usr',
+    context_library_path = 'pickup_lib',
+    context_bib_path = 'bib_rec'
+WHERE
+    hook IN (
+        SELECT key FROM action_trigger.hook WHERE core_type = 'ahr'
+    )
+;
+
+-- Retroactively setting context_user and context_library on existing rows in action_trigger.event:
+-- This is not done by default because it'll likely take a long time depending on the Evergreen
+-- installation.  You may want to do this out-of-band with the upgrade if you want to do this at all.
+--
+-- \pset format unaligned
+-- \t
+-- \o update_action_trigger_events_for_circs.sql
+-- SELECT 'UPDATE action_trigger.event e SET context_user = c.usr, context_library = c.circ_lib, context_bib = cn.record FROM action.circulation c, asset.copy i, asset.call_number cn WHERE c.id = e.target AND c.target_copy = i.id AND i.call_number = cn.id AND e.id = ' || e.id || ' RETURNING ' || e.id || ';' FROM action_trigger.event e, action.circulation c WHERE e.target = c.id AND e.event_def IN (SELECT id FROM action_trigger.event_definition WHERE hook in (SELECT key FROM action_trigger.hook WHERE core_type = 'circ')) ORDER BY e.id DESC;
+-- \o
+-- \o update_action_trigger_events_for_holds.sql
+-- SELECT 'UPDATE action_trigger.event e SET context_user = h.usr, context_library = h.pickup_lib, context_bib = r.bib_record FROM action.hold_request h, reporter.hold_request_record r WHERE h.id = e.target AND h.id = r.id AND e.id = ' || e.id || ' RETURNING ' || e.id || ';' FROM action_trigger.event e, action.hold_request h WHERE e.target = h.id AND e.event_def IN (SELECT id FROM action_trigger.event_definition WHERE hook in (SELECT key FROM action_trigger.hook WHERE core_type = 'ahr')) ORDER BY e.id DESC;
+-- \o
+
+COMMIT;
+
diff --git a/Open-ILS/src/sql/Pg/upgrade/YYYY.functions.triggered_event_log.sql b/Open-ILS/src/sql/Pg/upgrade/YYYY.functions.triggered_event_log.sql
new file mode 100644 (file)
index 0000000..987fe25
--- /dev/null
@@ -0,0 +1,407 @@
+BEGIN;
+
+SELECT evergreen.upgrade_deps_block_check('YYYY', :eg_version);
+
+CREATE OR REPLACE FUNCTION action.age_circ_on_delete () RETURNS TRIGGER AS $$
+DECLARE
+found char := 'N';
+BEGIN
+
+    -- If there are any renewals for this circulation, don't archive or delete
+    -- it yet.   We'll do so later, when we archive and delete the renewals.
+
+    SELECT 'Y' INTO found
+    FROM action.circulation
+    WHERE parent_circ = OLD.id
+    LIMIT 1;
+
+    IF found = 'Y' THEN
+        RETURN NULL;  -- don't delete
+       END IF;
+
+    -- Archive a copy of the old row to action.aged_circulation
+
+    INSERT INTO action.aged_circulation
+        (id,usr_post_code, usr_home_ou, usr_profile, usr_birth_year, copy_call_number, copy_location,
+        copy_owning_lib, copy_circ_lib, copy_bib_record, xact_start, xact_finish, target_copy,
+        circ_lib, circ_staff, checkin_staff, checkin_lib, renewal_remaining, grace_period, due_date,
+        stop_fines_time, checkin_time, create_time, duration, fine_interval, recurring_fine,
+        max_fine, phone_renewal, desk_renewal, opac_renewal, duration_rule, recurring_fine_rule,
+        max_fine_rule, stop_fines, workstation, checkin_workstation, checkin_scan_time, parent_circ,
+        auto_renewal, auto_renewal_remaining)
+      SELECT
+        id,usr_post_code, usr_home_ou, usr_profile, usr_birth_year, copy_call_number, copy_location,
+        copy_owning_lib, copy_circ_lib, copy_bib_record, xact_start, xact_finish, target_copy,
+        circ_lib, circ_staff, checkin_staff, checkin_lib, renewal_remaining, grace_period, due_date,
+        stop_fines_time, checkin_time, create_time, duration, fine_interval, recurring_fine,
+        max_fine, phone_renewal, desk_renewal, opac_renewal, duration_rule, recurring_fine_rule,
+        max_fine_rule, stop_fines, workstation, checkin_workstation, checkin_scan_time, parent_circ,
+        auto_renewal, auto_renewal_remaining
+        FROM action.all_circulation WHERE id = OLD.id;
+
+    -- Migrate billings and payments to aged tables
+
+    SELECT 'Y' INTO found FROM config.global_flag 
+        WHERE name = 'history.money.age_with_circs' AND enabled;
+
+    IF found = 'Y' THEN
+        PERFORM money.age_billings_and_payments_for_xact(OLD.id);
+    END IF;
+
+    -- Break the link with the user in action_trigger.event (warning: event_output may essentially have this information)
+    UPDATE
+        action_trigger.event e
+    SET
+        context_user = NULL
+    FROM
+        action.all_circulation c
+    WHERE
+            c.id = OLD.id
+        AND e.context_user = c.usr
+        AND e.target = c.id
+        AND e.event_def IN (
+            SELECT id
+            FROM action_trigger.event_definition
+            WHERE hook in (SELECT key FROM action_trigger.hook WHERE core_type = 'circ')
+        )
+    ;
+
+    RETURN OLD;
+END;
+$$ LANGUAGE 'plpgsql';
+
+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;
+
+    -- action_trigger.event (even doing this, event_output may--and probably does--contain PII and should have a retention/removal policy)
+    UPDATE action_trigger.event SET context_user = dest_usr WHERE context_user = src_usr;
+
+       -- 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;
+    UPDATE acq.invoice SET closed_by = dest_usr WHERE closed_by = 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;
+       DELETE FROM actor.usr_privacy_waiver WHERE 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;
+
+    UPDATE vandelay.session_tracker SET usr = dest_usr WHERE usr = src_usr;
+
+    -- 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;
index b6a72bd..c2a548a 100644 (file)
@@ -184,7 +184,7 @@ angular.module('egCoreMod').run(['egStrings', function(s) {
             </a>
           </li>
           <li>
-            <a href="./circ/patron/{{patron().id()}}/triggered_events">
+            <a href="/eg2/staff/circ/patron/event-log/{{patron().id()}}" target="_blank">
               [% l('Triggered Events / Notifications') %]
             </a>
           </li>
diff --git a/docs/RELEASE_NOTES_NEXT/Circulation/PatronTriggeredEventsLog.adoc b/docs/RELEASE_NOTES_NEXT/Circulation/PatronTriggeredEventsLog.adoc
new file mode 100644 (file)
index 0000000..7932fb7
--- /dev/null
@@ -0,0 +1,5 @@
+New Patron Triggered Events Log
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+A reimplementation of the Patron Triggered Events Log interface along with
+supporting infrastructure for speedier results with large datasets.