<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"/>
<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>
</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"/>
<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" />
<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">
--- /dev/null
+<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>
--- /dev/null
+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(); }
+}
--- /dev/null
+
+<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>
--- /dev/null
+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');
+ }
+}
+
+
--- /dev/null
+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 {}
+
--- /dev/null
+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 {}
{ 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)
}
];
$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 {
--- /dev/null
+#!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) );
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';
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,
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,
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
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;
--- /dev/null
+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;
+
--- /dev/null
+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;
</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>
--- /dev/null
+New Patron Triggered Events Log
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+A reimplementation of the Patron Triggered Events Log interface along with
+supporting infrastructure for speedier results with large datasets.