LP#1207533: item-oriented Triggered Event Log
authorMike Rylander <mrylander@gmail.com>
Mon, 22 Mar 2021 20:00:55 +0000 (16:00 -0400)
committerChris Sharp <csharp@georgialibraries.org>
Tue, 21 Sep 2021 20:18:57 +0000 (16:18 -0400)
Building on the previous commit from Jason Etheridge, this adds an
Angular reimplementation of the item-oriented TEL variant.  In addition,
it separates the grid settings persist key and creates the YAOUSen
needed to save those grid settings.

Because these interfaces make use of the flattener service to drive grid
construction and data retrieval, staff will have to have the appropriate
VIEW_USER permission to be able to see patron related data in the
item-oriented grid.

Funding for these interfaces comes from PaILS, for the patron-oriented
interface, and from ECDI, for the item-oriented one.

Signed-off-by: Mike Rylander <mrylander@gmail.com>
Signed-off-by: Dawn Dale <ddale@georgialibraries.org>
Signed-off-by: Chris Sharp <csharp@georgialibraries.org>
21 files changed:
Open-ILS/examples/fm_IDL.xml
Open-ILS/src/eg2/src/app/staff/circ/item/event-log/event-grid.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/item/event-log/event-grid.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/item/event-log/event-log.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/item/event-log/event-log.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/item/event-log/event-log.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/item/event-log/routing.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/item/routing.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/patron/event-log/event-grid.component.html
Open-ILS/src/eg2/src/app/staff/circ/patron/event-log/event-grid.component.ts
Open-ILS/src/eg2/src/app/staff/circ/routing.module.ts
Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Event.pm
Open-ILS/src/sql/Pg/040.schema.asset.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/upgrade/ZZZZ.schema.item_triggered_event_log.sql [new file with mode: 0644]
Open-ILS/src/templates/staff/cat/item/t_view.tt2
Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
Open-ILS/web/js/ui/default/staff/cat/item/app.js
Open-ILS/web/js/ui/default/staff/circ/patron/items_out.js
docs/RELEASE_NOTES_NEXT/Circulation/ItemTriggeredEventsLog.adoc [new file with mode: 0644]

index fc20984..0f0dff8 100644 (file)
@@ -1457,6 +1457,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <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"/>
+                       <field reporter:label="Context Item Path" name="context_item_path" reporter:datatype="text"/>
                </fields>
                <links>
                        <link field="owner" reltype="has_a" key="id" map="" class="aou"/>
@@ -1554,6 +1555,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <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"/>
+                       <field reporter:label="Context Item" name="context_item" reporter:datatype="link"/>
                </fields>
                <links>
                        <link field="event_def" reltype="has_a" key="id" map="" class="atevdef"/>
@@ -1563,6 +1565,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <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"/>
+                       <link field="context_item" reltype="has_a" key="id" map="" class="acp"/>
                </links>
        </class>
 
@@ -1696,6 +1699,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
             atev.context_user,
                        atev.context_library,
             atev.context_bib,
+            atev.context_item,
             rssr.title,
             rssr.author
                FROM action_trigger.event atev
@@ -1736,6 +1740,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <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="Context Item" name="context_item" reporter:datatype="link" />
                        <field reporter:label="Title" name="title" reporter:datatype="text" />
                        <field reporter:label="Author" name="author" reporter:datatype="text" />
                </fields>
@@ -1749,10 +1754,11 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <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" />
+                       <link field="context_item" reltype="has_a" key="id" map="" class="acp" />
                </links>
                <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
                        <actions>
-                               <retrieve permission="VIEW_TRIGGER_EVENT" context_field="perm_lib" />
+                               <retrieve permission="VIEW_TRIGGER_EVENT" context_field="context_library" />
                        </actions>
                </permacrud>
        </class>
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/item/event-log/event-grid.component.html b/Open-ILS/src/eg2/src/app/staff/circ/item/event-log/event-grid.component.html
new file mode 100644 (file)
index 0000000..80f6227
--- /dev/null
@@ -0,0 +1,14 @@
+<eg-grid #grid [dataSource]="gridSource" idlClass="atoul"
+  [sortable]="true"
+  [filterable]="true"
+  showFields="context_library,state,name,reactor,run_time,context_user.card.barcode,title,author"
+  ignoreFields="target_circ,target_hold"
+  persistKey="item.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 [sortable]="false" [filterable]="false" path="context_user.card.barcode" label="Patron Barcode" i18n-label></eg-grid-column>
+</eg-grid>
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/item/event-log/event-grid.component.ts b/Open-ILS/src/eg2/src/app/staff/circ/item/event-log/event-grid.component.ts
new file mode 100644 (file)
index 0000000..063d2cd
--- /dev/null
@@ -0,0 +1,129 @@
+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-item-event-grid',
+    templateUrl: './event-grid.component.html'
+})
+
+export class ItemEventGridComponent implements OnChanges, OnInit {
+
+    @Input() item: 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_item'] = (this.item ? this.item : {'>' : 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: ['context_user','context_item'],
+                    au: ['card']
+                },
+                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/item/event-log/event-log.component.html b/Open-ILS/src/eg2/src/app/staff/circ/item/event-log/event-log.component.html
new file mode 100644 (file)
index 0000000..8b3c707
--- /dev/null
@@ -0,0 +1,20 @@
+
+<eg-staff-banner bannerText="Triggered Event Log (Item 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-item-event-grid #itemEventGrid [item]="itemId" event_type="circ"></eg-item-event-grid>
+    </ng-template>
+  </li>
+  <li [ngbNavItem]="2">
+    <a ngbNavLink i18n>Holds</a>
+    <ng-template ngbNavContent>
+      <eg-item-event-grid #itemEventGrid [item]="itemId" event_type="hold"></eg-item-event-grid>
+    </ng-template>
+  </li>
+</ul>
+
+<div [ngbNavOutlet]="nav" class="mt-2"></div>
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/item/event-log/event-log.component.ts b/Open-ILS/src/eg2/src/app/staff/circ/item/event-log/event-log.component.ts
new file mode 100644 (file)
index 0000000..2916ba8
--- /dev/null
@@ -0,0 +1,29 @@
+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 {ItemEventGridComponent} from './event-grid.component';
+
+@Component({
+  templateUrl: 'event-log.component.html'
+})
+
+export class ItemEventLogComponent implements OnInit {
+    itemId: number;
+
+    @ViewChild('itemEventGrid', { static: true }) itemEventGrid: ItemEventGridComponent;
+
+    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 items, which may be a neat feature...
+        this.itemId = +this.route.snapshot.paramMap.get('item');
+    }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/item/event-log/event-log.module.ts b/Open-ILS/src/eg2/src/app/staff/circ/item/event-log/event-log.module.ts
new file mode 100644 (file)
index 0000000..67e1d59
--- /dev/null
@@ -0,0 +1,19 @@
+import {NgModule} from '@angular/core';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {ItemEventLogRoutingModule} from './routing.module';
+import {ItemEventGridComponent} from './event-grid.component';
+import {ItemEventLogComponent} from './event-log.component';
+
+@NgModule({
+  declarations: [
+    ItemEventGridComponent,
+    ItemEventLogComponent
+  ],
+  imports: [
+    StaffCommonModule,
+    ItemEventLogRoutingModule,
+  ],
+})
+
+export class ItemEventLogModule {}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/item/event-log/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/circ/item/event-log/routing.module.ts
new file mode 100644 (file)
index 0000000..fdb2ccf
--- /dev/null
@@ -0,0 +1,19 @@
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {ItemEventLogComponent} from './event-log.component';
+
+const routes: Routes = [
+  { path: '',
+    component: ItemEventLogComponent
+  },
+  { path: ':item',
+    component: ItemEventLogComponent
+  },
+];
+
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule]
+})
+
+export class ItemEventLogRoutingModule {}
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/item/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/circ/item/routing.module.ts
new file mode 100644 (file)
index 0000000..e8045a9
--- /dev/null
@@ -0,0 +1,16 @@
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+
+const routes: Routes = [
+  { path: 'event-log',
+    loadChildren: () =>
+      import('./event-log/event-log.module').then(m => m.ItemEventLogModule)
+  }
+];
+
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule]
+})
+
+export class CircItemRoutingModule {}
index 0605678..f6e404e 100644 (file)
@@ -1,15 +1,14 @@
 <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"
+  showFields="context_library,state,name,reactor,run_time,context_item.barcode,title,author"
   ignoreFields="target_circ,target_hold"
-  persistKey="event_grid">
+  persistKey="patron.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-column [sortable]="false" [filterable]="false" path="context_user.card.barcode" label="Patron Barcode" i18n-label></eg-grid-column>
 </eg-grid>
index 385754e..083b1a4 100644 (file)
@@ -88,9 +88,8 @@ export class EventGridComponent implements OnChanges, OnInit {
                 query, {
                 flesh: 3,
                 flesh_fields: {
-                    atoul: ['target_circ', 'target_hold'],
-                    circ: ['target_copy'],
-                    ahr: ['current_copy']
+                    atoul: ['context_item', 'context_user'],
+                    au: ['card']
                 },
                 offset: pager.offset,
                 limit: pager.limit,
index fae4330..49cd79a 100644 (file)
@@ -5,6 +5,10 @@ const routes: Routes = [
   { path: 'patron',
     loadChildren: () =>
       import('./patron/routing.module').then(m => m.CircPatronRoutingModule)
+  },
+  { path: 'item',
+    loadChildren: () =>
+      import('./item/routing.module').then(m => m.CircItemRoutingModule)
   }
 ];
 
index 66df9c9..419bd7f 100644 (file)
@@ -525,38 +525,47 @@ sub build_environment {
         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_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->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'] );
+            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;
             }
-            $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}
-                }
-            );
         }
+
+        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'] );
+        }
+
+        if ($self->event->event_def->context_item_path) {
+            my @item_path = split(/\./, $self->event->event_def->context_item_path);
+            $self->_object_by_path( $self->target, undef, [qw/context item/], \@item_path );
+        }
+
+        $self->update_state(
+            $self->event->state, {
+                'context_item' => $self->environment->{context}->{item}
+                    ? $self->environment->{context}->{item}->id
+                    : undef,
+                '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 {
index f145530..2002113 100644 (file)
@@ -952,9 +952,11 @@ DECLARE
     copy_id BIGINT;
 BEGIN
     EXECUTE 'SELECT ($1).' || quote_ident(TG_ARGV[0]) INTO copy_id USING NEW;
-    PERFORM * FROM asset.copy WHERE id = copy_id;
-    IF NOT FOUND THEN
-        RAISE EXCEPTION 'Key (%.%=%) does not exist in asset.copy', TG_TABLE_SCHEMA, TG_TABLE_NAME, copy_id;
+    IF copy_id IS NOT NULL THEN
+        PERFORM * FROM asset.copy WHERE id = copy_id;
+        IF NOT FOUND THEN
+            RAISE EXCEPTION 'Key (%.%=%) does not exist in asset.copy', TG_TABLE_SCHEMA, TG_TABLE_NAME, copy_id;
+        END IF;
     END IF;
     RETURN NULL;
 END;
index 80e4f8d..3a0816a 100644 (file)
@@ -200,6 +200,7 @@ CREATE TABLE action_trigger.event_definition (
     context_usr_path        TEXT, -- for optimizing action_trigger.event
     context_library_path    TEXT, -- '''
     context_bib_path        TEXT, -- '''
+    context_item_path       TEXT, -- '''
 
     message_template        TEXT,
     message_usr_path        TEXT,
@@ -282,7 +283,8 @@ CREATE TABLE action_trigger.event (
     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)
+    context_bib     BIGINT      REFERENCES biblio.record_entry (id),
+    context_item    BIGINT
 );
 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);
@@ -291,6 +293,11 @@ 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 INDEX atev_context_item ON action_trigger.event (context_item);
+
+CREATE TRIGGER action_trigger_event_context_item_fkey_trig
+  AFTER INSERT OR UPDATE ON action_trigger.event
+  FOR EACH ROW EXECUTE PROCEDURE evergreen.fake_fkey_tgr('context_item');
 
 CREATE TABLE action_trigger.event_params (
     id          BIGSERIAL   PRIMARY KEY,
index 3ec8dc6..bb3edfc 100644 (file)
@@ -17558,7 +17558,8 @@ UPDATE
 SET
     context_usr_path = 'usr',
     context_library_path = 'circ_lib',
-    context_bib_path = 'target_copy.call_number.record'
+    context_bib_path = 'target_copy.call_number.record',
+    context_item_path = 'target_copy'
 WHERE
     hook IN (
         SELECT key FROM action_trigger.hook WHERE core_type = 'circ'
@@ -17570,7 +17571,8 @@ UPDATE
 SET
     context_usr_path = 'usr',
     context_library_path = 'pickup_lib',
-    context_bib_path = 'bib_rec'
+    context_bib_path = 'bib_rec',
+    context_item_path = 'current_copy'
 WHERE
     hook IN (
         SELECT key FROM action_trigger.hook WHERE core_type = 'ahr'
@@ -21754,7 +21756,22 @@ VALUES (
         'eg.staff.catalog.results.show_more',
         'Show more details in Angular staff catalog',
         'cwst', 'label'
-    )
+);
+
+INSERT INTO config.workstation_setting_type
+    (name, grp, datatype, label)
+VALUES (
+    'eg.grid.item.event_grid', 'gui', 'object',
+    oils_i18n_gettext(
+    'eg.grid.item.event_grid',
+    'Grid Config: item.event_grid',
+    'cwst', 'label')
+), (
+    'eg.grid.patron.event_grid', 'gui', 'object',
+    oils_i18n_gettext(
+    'eg.grid.patron.event_grid',
+    'Grid Config: patron.event_grid',
+    'cwst', 'label')
 );
 
 INSERT INTO config.org_unit_setting_type
diff --git a/Open-ILS/src/sql/Pg/upgrade/ZZZZ.schema.item_triggered_event_log.sql b/Open-ILS/src/sql/Pg/upgrade/ZZZZ.schema.item_triggered_event_log.sql
new file mode 100644 (file)
index 0000000..57882a3
--- /dev/null
@@ -0,0 +1,85 @@
+BEGIN;
+
+SELECT evergreen.upgrade_deps_block_check('ZZZZ', :eg_version);
+
+INSERT INTO config.workstation_setting_type
+    (name, grp, datatype, label)
+VALUES (
+    'eg.grid.item.event_grid', 'gui', 'object',
+    oils_i18n_gettext(
+    'eg.grid.item.event_grid',
+    'Grid Config: item.event_grid',
+    'cwst', 'label')
+), (
+    'eg.grid.patron.event_grid', 'gui', 'object',
+    oils_i18n_gettext(
+    'eg.grid.patron.event_grid',
+    'Grid Config: patron.event_grid',
+    'cwst', 'label')
+);
+
+DROP TRIGGER IF EXISTS action_trigger_event_context_item_trig ON action_trigger.event;
+
+-- Create a NULLABLE version of the fake-copy-fkey trigger function.
+CREATE OR REPLACE FUNCTION evergreen.fake_fkey_tgr () RETURNS TRIGGER AS $F$
+DECLARE
+    copy_id BIGINT;
+BEGIN
+    EXECUTE 'SELECT ($1).' || quote_ident(TG_ARGV[0]) INTO copy_id USING NEW;
+    IF copy_id IS NOT NULL THEN
+        PERFORM * FROM asset.copy WHERE id = copy_id;
+        IF NOT FOUND THEN
+            RAISE EXCEPTION 'Key (%.%=%) does not exist in asset.copy', TG_TABLE_SCHEMA, TG_TABLE_NAME, copy_id;
+        END IF;
+    END IF;
+    RETURN NULL;
+END;
+$F$ LANGUAGE PLPGSQL;
+
+
+--    context_item_path        TEXT, -- for optimizing action_trigger.event
+ALTER TABLE action_trigger.event_definition ADD COLUMN context_item_path TEXT;
+
+--    context_item     BIGINT      REFERENCES asset.copy (id)
+ALTER TABLE action_trigger.event ADD COLUMN context_item BIGINT;
+CREATE INDEX atev_context_item ON action_trigger.event (context_item);
+
+UPDATE
+    action_trigger.event_definition
+SET
+    context_item_path = 'target_copy'
+WHERE
+    hook IN (
+        SELECT key FROM action_trigger.hook WHERE core_type = 'circ'
+    )
+;
+
+UPDATE
+    action_trigger.event_definition
+SET
+    context_item_path = 'current_copy'
+WHERE
+    hook IN (
+        SELECT key FROM action_trigger.hook WHERE core_type = 'ahr'
+    )
+;
+
+-- Retroactively setting context_item 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_item = c.target_copy FROM action.circulation cWHERE c.id = e.target 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_item = h.current_copy FROM action.hold_request h WHERE h.id = e.target 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;
+
+CREATE TRIGGER action_trigger_event_context_item_trig
+  AFTER INSERT OR UPDATE ON action_trigger.event
+  FOR EACH ROW EXECUTE PROCEDURE evergreen.fake_fkey_tgr('context_item');
+
index bf9eaa4..47b369a 100644 (file)
@@ -21,7 +21,7 @@
     <a href="./cat/item/{{copy.id()}}/cat">[% l('Cataloging Info') %]</a>
   </li>
   <li ng-class="{active : tab == 'triggered_events'}">
-    <a href="./cat/item/{{copy.id()}}/triggered_events">[% l('Triggered Events') %]</a>
+    <a href="/eg2/staff/circ/item/event-log/{{copy.id()}}" target="_blank">[% l('Triggered Events') %]</a>
   </li>
   <li ng-class="{active : tab == 'course'}" ng-if="has_course_perms && courseModulesOptIn">
     <a href="./cat/item/{{copy.id()}}/course">[% l('Course Info') %]</a>
index 5789dba..ecd0936 100644 (file)
@@ -1658,8 +1658,7 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
         angular.forEach(
             gatherSelectedHoldingsIds(),
             function (cid) {
-                var url = egCore.env.basePath +
-                          'cat/item/' + cid + '/triggered_events';
+                var url = '/eg2/staff/circ/item/event-log/' + cid;
                 $timeout(function() { $window.open(url, '_blank') });
             }
         );
index 7e4c618..8e21e25 100644 (file)
@@ -161,7 +161,7 @@ function($scope , $q , $window , $location , $timeout , egCore , egNet , egGridD
     }
 
     $scope.show_triggered_events = function() {
-        $location.path('/cat/item/' + $scope.args.copyId + '/triggered_events');
+        window.open('/eg2/staff/circ/item/event-log/' + $scope.args.copyId, '_blank');
     }
 
     $scope.show_item_holds = function() {
@@ -469,7 +469,7 @@ function($scope , $q , $window , $location , $timeout , egCore , egNet , egGridD
     $scope.context.show_triggered_events = function() {
         var item = copyGrid.selectedItems()[0];
         if (item) 
-            $location.path('/cat/item/' + item.id + '/triggered_events');
+            window.open('/eg2/staff/circ/item/event-log/' + item.id, '_blank');
     }
 
     function gatherSelectedRecordIds () {
@@ -613,7 +613,7 @@ function($scope , $q , $window , $location , $timeout , egCore , egNet , egGridD
     $scope.selectedHoldingsItemStatusTgrEvt= function() {
         var item = copyGrid.selectedItems()[0];
         if (item)
-            $location.path('/cat/item/' + item.id + '/triggered_events');
+            window.open('/eg2/staff/circ/item/event-log/' + item.id, '_blank');
     }
 
     $scope.selectedHoldingsItemStatusHolds= function() {
@@ -1320,7 +1320,7 @@ console.debug($scope.copy_alert_count);
     }
 
     $scope.context.show_triggered_events = function() {
-        $location.path('/cat/item/' + copyId + '/triggered_events');
+        window.open('/eg2/staff/circ/item/event-log/' + copyId, '_blank');
     }
 
     loadCopy().then(loadTabData);
index 16c04c9..db0489d 100644 (file)
@@ -476,11 +476,10 @@ function($scope , $q , $routeParams , $timeout , egCore , egUser , patronSvc ,
     $scope.show_triggered_events = function(items) {
         var focus = items.length == 1;
         angular.forEach(items, function(item) {
-            var url = egCore.env.basePath +
-                      '/cat/item/' +
-                      item.target_copy().id() +
-                      '/triggered_events';
+            var url = '/eg2/staff/circ/item/event-log/' +
+                      item.target_copy().id();
             $timeout(function() { var x = $window.open(url, '_blank'); if (focus) x.focus() });
+
         });
     }
 
diff --git a/docs/RELEASE_NOTES_NEXT/Circulation/ItemTriggeredEventsLog.adoc b/docs/RELEASE_NOTES_NEXT/Circulation/ItemTriggeredEventsLog.adoc
new file mode 100644 (file)
index 0000000..e95fa47
--- /dev/null
@@ -0,0 +1,5 @@
+New Item Triggered Events Log
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+A reimplementation of the Item Triggered Events Log interface, building
+on the Patron Triggered Events Log Angular reimplemenation.