From c7d538ce950054df3ed2d55a82306c34135cc865 Mon Sep 17 00:00:00 2001 From: Jason Etheridge Date: Wed, 29 Jul 2020 08:11:33 -0400 Subject: [PATCH] LP1207533 patron triggered events log * 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 Signed-off-by: Mike Rylander Signed-off-by: Dawn Dale Signed-off-by: Chris Sharp --- Open-ILS/examples/fm_IDL.xml | 103 ++++- .../event-log/event-grid.component.html | 15 + .../patron/event-log/event-grid.component.ts | 130 ++++++ .../patron/event-log/event-log.component.html | 20 + .../patron/event-log/event-log.component.ts | 30 ++ .../circ/patron/event-log/event-log.module.ts | 19 + .../circ/patron/event-log/routing.module.ts | 19 + .../app/staff/circ/patron/routing.module.ts | 4 + .../lib/OpenILS/Application/Trigger/Event.pm | 36 ++ .../live_t/32-lp1207533-triggered-events.t | 119 +++++ Open-ILS/src/sql/Pg/090.schema.action.sql | 18 + .../src/sql/Pg/400.schema.action_trigger.sql | 11 +- Open-ILS/src/sql/Pg/950.data.seed-values.sql | 24 ++ Open-ILS/src/sql/Pg/999.functions.global.sql | 3 + .../XXXX.schema.triggered_event_log.sql | 59 +++ .../YYYY.functions.triggered_event_log.sql | 407 ++++++++++++++++++ .../src/templates/staff/circ/patron/index.tt2 | 2 +- .../Circulation/PatronTriggeredEventsLog.adoc | 5 + 18 files changed, 1021 insertions(+), 3 deletions(-) create mode 100644 Open-ILS/src/eg2/src/app/staff/circ/patron/event-log/event-grid.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/circ/patron/event-log/event-grid.component.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/circ/patron/event-log/event-log.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/circ/patron/event-log/event-log.component.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/circ/patron/event-log/event-log.module.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/circ/patron/event-log/routing.module.ts create mode 100644 Open-ILS/src/perlmods/live_t/32-lp1207533-triggered-events.t create mode 100644 Open-ILS/src/sql/Pg/upgrade/XXXX.schema.triggered_event_log.sql create mode 100644 Open-ILS/src/sql/Pg/upgrade/YYYY.functions.triggered_event_log.sql create mode 100644 docs/RELEASE_NOTES_NEXT/Circulation/PatronTriggeredEventsLog.adoc diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml index c068c2cad6..fc20984373 100644 --- a/Open-ILS/examples/fm_IDL.xml +++ b/Open-ILS/examples/fm_IDL.xml @@ -1454,6 +1454,9 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + + + @@ -1548,12 +1551,18 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + + + + + + @@ -1663,6 +1672,91 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + + 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) + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -10872,7 +10966,7 @@ SELECT usr, - + @@ -10883,6 +10977,13 @@ SELECT usr, + + + + + + + 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 index 0000000000..0605678258 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/circ/patron/event-log/event-grid.component.html @@ -0,0 +1,15 @@ + + + + + + + + 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 index 0000000000..385754e2f1 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/circ/patron/event-log/event-grid.component.ts @@ -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 => { + // 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 index 0000000000..f16f69f675 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/circ/patron/event-log/event-log.component.html @@ -0,0 +1,20 @@ + + + + + + +
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 index 0000000000..f707467402 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/circ/patron/event-log/event-log.component.ts @@ -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 index 0000000000..868a470b23 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/circ/patron/event-log/event-log.module.ts @@ -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 index 0000000000..7196d314e5 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/circ/patron/event-log/routing.module.ts @@ -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 {} diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/circ/patron/routing.module.ts index c2b7432e99..a1b4ae680c 100644 --- a/Open-ILS/src/eg2/src/app/staff/circ/patron/routing.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/circ/patron/routing.module.ts @@ -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) } ]; diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Event.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Event.pm index 1c0286bd82..66df9c9785 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Event.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Event.pm @@ -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 index 0000000000..a611182e71 --- /dev/null +++ b/Open-ILS/src/perlmods/live_t/32-lp1207533-triggered-events.t @@ -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) ); diff --git a/Open-ILS/src/sql/Pg/090.schema.action.sql b/Open-ILS/src/sql/Pg/090.schema.action.sql index 813ab0ba98..711269e245 100644 --- a/Open-ILS/src/sql/Pg/090.schema.action.sql +++ b/Open-ILS/src/sql/Pg/090.schema.action.sql @@ -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'; diff --git a/Open-ILS/src/sql/Pg/400.schema.action_trigger.sql b/Open-ILS/src/sql/Pg/400.schema.action_trigger.sql index 8a0c2138f1..80e4f8de89 100644 --- a/Open-ILS/src/sql/Pg/400.schema.action_trigger.sql +++ b/Open-ILS/src/sql/Pg/400.schema.action_trigger.sql @@ -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, diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql index 8fc21aea95..3ec8dc6128 100644 --- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql +++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql @@ -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 diff --git a/Open-ILS/src/sql/Pg/999.functions.global.sql b/Open-ILS/src/sql/Pg/999.functions.global.sql index 4722e23fd6..38275442e5 100644 --- a/Open-ILS/src/sql/Pg/999.functions.global.sql +++ b/Open-ILS/src/sql/Pg/999.functions.global.sql @@ -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 index 0000000000..203a6b3fd5 --- /dev/null +++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.triggered_event_log.sql @@ -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 index 0000000000..987fe255d4 --- /dev/null +++ b/Open-ILS/src/sql/Pg/upgrade/YYYY.functions.triggered_event_log.sql @@ -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; diff --git a/Open-ILS/src/templates/staff/circ/patron/index.tt2 b/Open-ILS/src/templates/staff/circ/patron/index.tt2 index b6a72bd8d4..c2a548a847 100644 --- a/Open-ILS/src/templates/staff/circ/patron/index.tt2 +++ b/Open-ILS/src/templates/staff/circ/patron/index.tt2 @@ -184,7 +184,7 @@ angular.module('egCoreMod').run(['egStrings', function(s) {
  • - + [% l('Triggered Events / Notifications') %]
  • diff --git a/docs/RELEASE_NOTES_NEXT/Circulation/PatronTriggeredEventsLog.adoc b/docs/RELEASE_NOTES_NEXT/Circulation/PatronTriggeredEventsLog.adoc new file mode 100644 index 0000000000..7932fb75f4 --- /dev/null +++ b/docs/RELEASE_NOTES_NEXT/Circulation/PatronTriggeredEventsLog.adoc @@ -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. -- 2.43.2