]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/sql/Pg/version-upgrade/3.5.1-3.6-rc-upgrade-db.sql
move 3.6-beta2 update to 3.6-rc
[Evergreen.git] / Open-ILS / src / sql / Pg / version-upgrade / 3.5.1-3.6-rc-upgrade-db.sql
1 --Upgrade Script for 3.5.1 to 3.6-rc
2 \set eg_version '''3.6-rc'''
3 BEGIN;
4 INSERT INTO config.upgrade_log (version, applied_to) VALUES ('3.6-rc', :eg_version);
5
6 SELECT evergreen.upgrade_deps_block_check('1205', :eg_version);
7
8 INSERT INTO config.print_template 
9     (id, name, locale, active, owner, label, template) 
10 VALUES (
11     3, 'booking_capture', 'en-US', TRUE,
12     (SELECT id FROM actor.org_unit WHERE parent_ou IS NULL),
13     oils_i18n_gettext(3, 'Booking capture slip', 'cpt', 'label'),
14 $TEMPLATE$
15 [%-
16     USE date;
17     SET data = template_data;
18     # template_data is data returned from open-ils.booking.resources.capture_for_reservation.
19 -%]
20 <div>
21   [% IF data.transit;
22        dest_ou = helpers.get_org_unit(data.transit.dest);
23   %]
24   <div>This item need to be routed to <strong>[% dest_ou.shortname %]</strong></div>
25   [% ELSE %]
26   <div>This item need to be routed to <strong>RESERVATION SHELF:</strong></div>
27   [% END %]
28   <div>Barcode: [% data.reservation.current_resource.barcode %]</div>
29   <div>Title: [% data.reservation.current_resource.type.name %]</div>
30   <div>Note: [% data.reservation.note %]</div>
31   <br/>
32   <p><strong>Reserved for patron</strong> [% data.reservation.usr.family_name %], [% data.reservation.usr.first_given_name %] [% data.reservation.usr.second_given_name %]
33   <br/>Barcode: [% data.reservation.usr.card.barcode %]</p>
34   <p>Request time: [% date.format(helpers.format_date(data.reservation.request_time, client_timezone), '%x %r', locale) %]
35   <br/>Reserved from:
36     [% date.format(helpers.format_date(data.reservation.start_time, client_timezone), '%x %r', locale) %]
37     - [% date.format(helpers.format_date(data.reservation.end_time, client_timezone), '%x %r', locale) %]</p>
38   <p>Slip date: [% date.format(helpers.current_date(client_timezone), '%x %r', locale) %]<br/>
39   Printed by [% data.staff.family_name %], [% data.staff.first_given_name %] [% data.staff.second_given_name %]
40     at [% data.workstation %]</p>
41 </div>
42 <br/>
43
44 $TEMPLATE$
45 );
46
47 INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
48 VALUES (
49     'eg.grid.booking.captured', 'gui', 'object',
50     oils_i18n_gettext(
51         'booking.manage',
52         'Grid Config: Booking Captured Reservations',
53         'cwst', 'label')
54 );
55
56
57
58 SELECT evergreen.upgrade_deps_block_check('1210', :eg_version); -- csharp/rhamby/sandbergja/gmcharlt
59
60 ALTER TABLE action.in_house_use ADD COLUMN workstation INT REFERENCES actor.workstation (id) DEFERRABLE INITIALLY DEFERRED;
61 ALTER TABLE action.non_cat_in_house_use ADD COLUMN workstation INT REFERENCES actor.workstation (id) DEFERRABLE INITIALLY DEFERRED;
62
63 CREATE INDEX action_in_house_use_ws_idx ON action.in_house_use ( workstation );
64 CREATE INDEX non_cat_in_house_use_ws_idx ON action.non_cat_in_house_use ( workstation );
65
66
67
68 SELECT evergreen.upgrade_deps_block_check('1212', :eg_version); -- berick/sandbergja/gmcharlt
69
70 DELETE FROM actor.org_unit_setting
71     WHERE name = 'ui.staff.angular_catalog.enabled';
72
73 DELETE FROM config.org_unit_setting_type_log 
74     WHERE field_name = 'ui.staff.angular_catalog.enabled';
75
76 DELETE FROM config.org_unit_setting_type
77     WHERE name = 'ui.staff.angular_catalog.enabled';
78
79 -- activate the stock hold-for-bib server print template
80 UPDATE config.print_template SET active = TRUE WHERE name = 'holds_for_bib';
81
82
83 SELECT evergreen.upgrade_deps_block_check('1213', :eg_version);
84
85 CREATE OR REPLACE FUNCTION actor.change_password (user_id INT, new_pw TEXT, pw_type TEXT DEFAULT 'main')
86 RETURNS VOID AS $$
87 DECLARE
88     new_salt TEXT;
89 BEGIN
90     SELECT actor.create_salt(pw_type) INTO new_salt;
91
92     IF pw_type = 'main' THEN
93         -- Only 'main' passwords are required to have
94         -- the extra layer of MD5 hashing.
95         PERFORM actor.set_passwd(
96             user_id, pw_type, md5(new_salt || md5(new_pw)), new_salt
97         );
98
99     ELSE
100         PERFORM actor.set_passwd(user_id, pw_type, new_pw, new_salt);
101     END IF;
102 END;
103 $$ LANGUAGE 'plpgsql';
104
105 COMMENT ON FUNCTION actor.change_password(INT,TEXT,TEXT) IS $$
106 Allows setting a salted password for a user by passing actor.usr id and the text of the password.
107 $$;
108
109
110 SELECT evergreen.upgrade_deps_block_check('1214', :eg_version);
111
112 CREATE OR REPLACE FUNCTION asset.merge_record_assets( target_record BIGINT, source_record BIGINT ) RETURNS INT AS $func$
113 DECLARE
114     moved_objects INT := 0;
115     source_cn     asset.call_number%ROWTYPE;
116     target_cn     asset.call_number%ROWTYPE;
117     metarec       metabib.metarecord%ROWTYPE;
118     hold          action.hold_request%ROWTYPE;
119     ser_rec       serial.record_entry%ROWTYPE;
120     ser_sub       serial.subscription%ROWTYPE;
121     acq_lineitem  acq.lineitem%ROWTYPE;
122     acq_request   acq.user_request%ROWTYPE;
123     booking       booking.resource_type%ROWTYPE;
124     source_part   biblio.monograph_part%ROWTYPE;
125     target_part   biblio.monograph_part%ROWTYPE;
126     multi_home    biblio.peer_bib_copy_map%ROWTYPE;
127     uri_count     INT := 0;
128     counter       INT := 0;
129     uri_datafield TEXT;
130     uri_text      TEXT := '';
131 BEGIN
132
133     -- move any 856 entries on records that have at least one MARC-mapped URI entry
134     SELECT  INTO uri_count COUNT(*)
135       FROM  asset.uri_call_number_map m
136             JOIN asset.call_number cn ON (m.call_number = cn.id)
137       WHERE cn.record = source_record;
138
139     IF uri_count > 0 THEN
140         
141         -- This returns more nodes than you might expect:
142         -- 7 instead of 1 for an 856 with $u $y $9
143         SELECT  COUNT(*) INTO counter
144           FROM  oils_xpath_table(
145                     'id',
146                     'marc',
147                     'biblio.record_entry',
148                     '//*[@tag="856"]',
149                     'id=' || source_record
150                 ) as t(i int,c text);
151     
152         FOR i IN 1 .. counter LOOP
153             SELECT  '<datafield xmlns="http://www.loc.gov/MARC21/slim"' || 
154                         ' tag="856"' ||
155                         ' ind1="' || FIRST(ind1) || '"'  ||
156                         ' ind2="' || FIRST(ind2) || '">' ||
157                         STRING_AGG(
158                             '<subfield code="' || subfield || '">' ||
159                             regexp_replace(
160                                 regexp_replace(
161                                     regexp_replace(data,'&','&amp;','g'),
162                                     '>', '&gt;', 'g'
163                                 ),
164                                 '<', '&lt;', 'g'
165                             ) || '</subfield>', ''
166                         ) || '</datafield>' INTO uri_datafield
167               FROM  oils_xpath_table(
168                         'id',
169                         'marc',
170                         'biblio.record_entry',
171                         '//*[@tag="856"][position()=' || i || ']/@ind1|' ||
172                         '//*[@tag="856"][position()=' || i || ']/@ind2|' ||
173                         '//*[@tag="856"][position()=' || i || ']/*/@code|' ||
174                         '//*[@tag="856"][position()=' || i || ']/*[@code]',
175                         'id=' || source_record
176                     ) as t(id int,ind1 text, ind2 text,subfield text,data text);
177
178             -- As most of the results will be NULL, protect against NULLifying
179             -- the valid content that we do generate
180             uri_text := uri_text || COALESCE(uri_datafield, '');
181         END LOOP;
182
183         IF uri_text <> '' THEN
184             UPDATE  biblio.record_entry
185               SET   marc = regexp_replace(marc,'(</[^>]*record>)', uri_text || E'\\1')
186               WHERE id = target_record;
187         END IF;
188
189     END IF;
190
191         -- Find and move metarecords to the target record
192         SELECT  INTO metarec *
193           FROM  metabib.metarecord
194           WHERE master_record = source_record;
195
196         IF FOUND THEN
197                 UPDATE  metabib.metarecord
198                   SET   master_record = target_record,
199                         mods = NULL
200                   WHERE id = metarec.id;
201
202                 moved_objects := moved_objects + 1;
203         END IF;
204
205         -- Find call numbers attached to the source ...
206         FOR source_cn IN SELECT * FROM asset.call_number WHERE record = source_record LOOP
207
208                 SELECT  INTO target_cn *
209                   FROM  asset.call_number
210                   WHERE label = source_cn.label
211             AND prefix = source_cn.prefix
212             AND suffix = source_cn.suffix
213                         AND owning_lib = source_cn.owning_lib
214                         AND record = target_record
215                         AND NOT deleted;
216
217                 -- ... and if there's a conflicting one on the target ...
218                 IF FOUND THEN
219
220                         -- ... move the copies to that, and ...
221                         UPDATE  asset.copy
222                           SET   call_number = target_cn.id
223                           WHERE call_number = source_cn.id;
224
225                         -- ... move V holds to the move-target call number
226                         FOR hold IN SELECT * FROM action.hold_request WHERE target = source_cn.id AND hold_type = 'V' LOOP
227                 
228                                 UPDATE  action.hold_request
229                                   SET   target = target_cn.id
230                                   WHERE id = hold.id;
231                 
232                                 moved_objects := moved_objects + 1;
233                         END LOOP;
234         
235             UPDATE asset.call_number SET deleted = TRUE WHERE id = source_cn.id;
236
237                 -- ... if not ...
238                 ELSE
239                         -- ... just move the call number to the target record
240                         UPDATE  asset.call_number
241                           SET   record = target_record
242                           WHERE id = source_cn.id;
243                 END IF;
244
245                 moved_objects := moved_objects + 1;
246         END LOOP;
247
248         -- Find T holds targeting the source record ...
249         FOR hold IN SELECT * FROM action.hold_request WHERE target = source_record AND hold_type = 'T' LOOP
250
251                 -- ... and move them to the target record
252                 UPDATE  action.hold_request
253                   SET   target = target_record
254                   WHERE id = hold.id;
255
256                 moved_objects := moved_objects + 1;
257         END LOOP;
258
259         -- Find serial records targeting the source record ...
260         FOR ser_rec IN SELECT * FROM serial.record_entry WHERE record = source_record LOOP
261                 -- ... and move them to the target record
262                 UPDATE  serial.record_entry
263                   SET   record = target_record
264                   WHERE id = ser_rec.id;
265
266                 moved_objects := moved_objects + 1;
267         END LOOP;
268
269         -- Find serial subscriptions targeting the source record ...
270         FOR ser_sub IN SELECT * FROM serial.subscription WHERE record_entry = source_record LOOP
271                 -- ... and move them to the target record
272                 UPDATE  serial.subscription
273                   SET   record_entry = target_record
274                   WHERE id = ser_sub.id;
275
276                 moved_objects := moved_objects + 1;
277         END LOOP;
278
279         -- Find booking resource types targeting the source record ...
280         FOR booking IN SELECT * FROM booking.resource_type WHERE record = source_record LOOP
281                 -- ... and move them to the target record
282                 UPDATE  booking.resource_type
283                   SET   record = target_record
284                   WHERE id = booking.id;
285
286                 moved_objects := moved_objects + 1;
287         END LOOP;
288
289         -- Find acq lineitems targeting the source record ...
290         FOR acq_lineitem IN SELECT * FROM acq.lineitem WHERE eg_bib_id = source_record LOOP
291                 -- ... and move them to the target record
292                 UPDATE  acq.lineitem
293                   SET   eg_bib_id = target_record
294                   WHERE id = acq_lineitem.id;
295
296                 moved_objects := moved_objects + 1;
297         END LOOP;
298
299         -- Find acq user purchase requests targeting the source record ...
300         FOR acq_request IN SELECT * FROM acq.user_request WHERE eg_bib = source_record LOOP
301                 -- ... and move them to the target record
302                 UPDATE  acq.user_request
303                   SET   eg_bib = target_record
304                   WHERE id = acq_request.id;
305
306                 moved_objects := moved_objects + 1;
307         END LOOP;
308
309         -- Find parts attached to the source ...
310         FOR source_part IN SELECT * FROM biblio.monograph_part WHERE record = source_record LOOP
311
312                 SELECT  INTO target_part *
313                   FROM  biblio.monograph_part
314                   WHERE label = source_part.label
315                         AND record = target_record;
316
317                 -- ... and if there's a conflicting one on the target ...
318                 IF FOUND THEN
319
320                         -- ... move the copy-part maps to that, and ...
321                         UPDATE  asset.copy_part_map
322                           SET   part = target_part.id
323                           WHERE part = source_part.id;
324
325                         -- ... move P holds to the move-target part
326                         FOR hold IN SELECT * FROM action.hold_request WHERE target = source_part.id AND hold_type = 'P' LOOP
327                 
328                                 UPDATE  action.hold_request
329                                   SET   target = target_part.id
330                                   WHERE id = hold.id;
331                 
332                                 moved_objects := moved_objects + 1;
333                         END LOOP;
334
335                 -- ... if not ...
336                 ELSE
337                         -- ... just move the part to the target record
338                         UPDATE  biblio.monograph_part
339                           SET   record = target_record
340                           WHERE id = source_part.id;
341                 END IF;
342
343                 moved_objects := moved_objects + 1;
344         END LOOP;
345
346         -- Find multi_home items attached to the source ...
347         FOR multi_home IN SELECT * FROM biblio.peer_bib_copy_map WHERE peer_record = source_record LOOP
348                 -- ... and move them to the target record
349                 UPDATE  biblio.peer_bib_copy_map
350                   SET   peer_record = target_record
351                   WHERE id = multi_home.id;
352
353                 moved_objects := moved_objects + 1;
354         END LOOP;
355
356         -- And delete mappings where the item's home bib was merged with the peer bib
357         DELETE FROM biblio.peer_bib_copy_map WHERE peer_record = (
358                 SELECT (SELECT record FROM asset.call_number WHERE id = call_number)
359                 FROM asset.copy WHERE id = target_copy
360         );
361
362     -- Apply merge tracking
363     UPDATE biblio.record_entry 
364         SET merge_date = NOW() WHERE id = target_record;
365
366     UPDATE biblio.record_entry
367         SET merge_date = NOW(), merged_to = target_record
368         WHERE id = source_record;
369
370     -- replace book bag entries of source_record with target_record
371     UPDATE container.biblio_record_entry_bucket_item
372         SET target_biblio_record_entry = target_record
373         WHERE bucket IN (SELECT id FROM container.biblio_record_entry_bucket WHERE btype = 'bookbag')
374         AND target_biblio_record_entry = source_record;
375
376     -- Finally, "delete" the source record
377     UPDATE biblio.record_entry SET active = FALSE WHERE id = source_record;
378     DELETE FROM biblio.record_entry WHERE id = source_record;
379
380         -- That's all, folks!
381         RETURN moved_objects;
382 END;
383 $func$ LANGUAGE plpgsql;
384
385
386 SELECT evergreen.upgrade_deps_block_check('1215', :eg_version);
387
388 INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
389 VALUES (
390     'eg.orgselect.cat.catalog.wide_holds', 'gui', 'integer',
391     oils_i18n_gettext(
392         'eg.orgselect.cat.catalog.wide_holds',
393         'Default org unit for catalog holds org unit selector',
394         'cwst', 'label'
395     )
396 );
397
398
399
400
401 SELECT evergreen.upgrade_deps_block_check('1216', :eg_version);
402
403 INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
404 VALUES (
405     'eg.orgselect.patron.search', 'gui', 'integer',
406     oils_i18n_gettext(
407         'eg.orgselect.patron.search',
408         'Default org unit for patron search',
409         'cwst', 'label'
410     )
411 );
412
413
414
415
416 SELECT evergreen.upgrade_deps_block_check('1217', :eg_version);
417
418 INSERT INTO config.workstation_setting_type
419     (name, grp, datatype, label)
420 VALUES (
421     'eg.acq.search.default.lineitems', 'gui', 'object',
422     oils_i18n_gettext(
423     'eg.acq.search.default.lineitems',
424     'Acquisitions Default Search: Lineitems',
425     'cwst', 'label')
426 ), (
427     'eg.acq.search.default.purchaseorders', 'gui', 'object',
428     oils_i18n_gettext(
429     'eg.acq.search.default.purchaseorders',
430     'Acquisitions Default Search: Purchase Orders',
431     'cwst', 'label')
432 ), (
433     'eg.acq.search.default.invoices', 'gui', 'object',
434     oils_i18n_gettext(
435     'eg.acq.search.default.invoices',
436     'Acquisitions Default Search: Invoices',
437     'cwst', 'label')
438 ), (
439     'eg.acq.search.default.selectionlists', 'gui', 'object',
440     oils_i18n_gettext(
441     'eg.acq.search.default.selectionlists',
442     'Acquisitions Default Search: Selection Lists',
443     'cwst', 'label')
444 );
445
446 INSERT INTO config.workstation_setting_type
447     (name, grp, datatype, label)
448 VALUES (
449     'eg.acq.search.lineitems.run_immediately', 'gui', 'bool',
450     oils_i18n_gettext(
451     'eg.acq.search.lineitems.run_immediately',
452     'Acquisitions Search: Immediately Search Lineitems',
453     'cwst', 'label')
454 ), (
455     'eg.acq.search.purchaseorders.run_immediately', 'gui', 'bool',
456     oils_i18n_gettext(
457     'eg.acq.search.purchaseorders.run_immediately',
458     'Acquisitions Search: Immediately Search Purchase Orders',
459     'cwst', 'label')
460 ), (
461     'eg.acq.search.invoices.run_immediately', 'gui', 'bool',
462     oils_i18n_gettext(
463     'eg.acq.search.invoices.run_immediately',
464     'Acquisitions Search: Immediately Search Invoices',
465     'cwst', 'label')
466 ), (
467     'eg.acq.search.selectionlists.run_immediately', 'gui', 'bool',
468     oils_i18n_gettext(
469     'eg.acq.search.selectionlists.run_immediately',
470     'Acquisitions Search: Immediately Search Selection Lists',
471     'cwst', 'label')
472 );
473
474 INSERT INTO config.workstation_setting_type
475     (name, grp, datatype, label)
476 VALUES (
477     'eg.grid.acq.search.lineitems', 'gui', 'object',
478     oils_i18n_gettext(
479     'eg.grid.acq.search.lineitems',
480     'Grid Config: acq.search.lineitems',
481     'cwst', 'label')
482 ), (
483     'eg.grid.acq.search.purchaseorders', 'gui', 'object',
484     oils_i18n_gettext(
485     'eg.grid.acq.search.purchaseorders',
486     'Grid Config: acq.search.purchaseorders',
487     'cwst', 'label')
488 ), (
489     'eg.grid.acq.search.selectionlists', 'gui', 'object',
490     oils_i18n_gettext(
491     'eg.grid.acq.search.selectionlists',
492     'Grid Config: acq.search.selectionlists',
493     'cwst', 'label')
494 ), (
495     'eg.grid.acq.search.invoices', 'gui', 'object',
496     oils_i18n_gettext(
497     'eg.grid.acq.search.invoices',
498     'Grid Config: acq.search.invoices',
499     'cwst', 'label')
500 );
501
502
503 SELECT evergreen.upgrade_deps_block_check('1218', :eg_version);
504
505 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
506         SELECT
507                 pgt.id, perm.id, aout.depth, TRUE
508         FROM
509                 permission.grp_tree pgt,
510                 permission.perm_list perm,
511                 actor.org_unit_type aout
512         WHERE
513                 pgt.name = 'Acquisitions Administrator' AND
514                 aout.name = 'Consortium' AND
515                 perm.code IN (
516                     'VIEW_FUND',
517                     'VIEW_FUNDING_SOURCE',
518                     'VIEW_FUND_ALLOCATION',
519                     'VIEW_PICKLIST',
520                     'VIEW_PROVIDER',
521                     'VIEW_PURCHASE_ORDER',
522                     'VIEW_INVOICE',
523                     'CREATE_PICKLIST',
524                     'ACQ_ADD_LINEITEM_IDENTIFIER',
525                     'ACQ_SET_LINEITEM_IDENTIFIER',
526                     'MANAGE_FUND',
527                     'CREATE_INVOICE',
528                     'CREATE_PURCHASE_ORDER',
529                     'IMPORT_ACQ_LINEITEM_BIB_RECORD',
530                     'IMPORT_ACQ_LINEITEM_BIB_RECORD_UPLOAD',
531                     'MANAGE_CLAIM',
532                     'MANAGE_PROVIDER',
533                     'MANAGE_FUNDING_SOURCE',
534                     'RECEIVE_PURCHASE_ORDER',
535                     'ADMIN_ACQ_LINEITEM_ALERT_TEXT',
536                     'UPDATE_FUNDING_SOURCE',
537                     'UPDATE_PROVIDER',
538                     'VIEW_IMPORT_MATCH_SET',
539                     'VIEW_MERGE_PROFILE',
540                     'IMPORT_MARC'
541                 );
542
543
544 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
545         SELECT
546                 pgt.id, perm.id, aout.depth, FALSE
547         FROM
548                 permission.grp_tree pgt,
549                 permission.perm_list perm,
550                 actor.org_unit_type aout
551         WHERE
552                 pgt.name = 'Acquisitions' AND
553                 aout.name = 'Consortium' AND
554                 perm.code IN (
555                     'ACQ_ADD_LINEITEM_IDENTIFIER',
556                     'ACQ_SET_LINEITEM_IDENTIFIER',
557                     'ADMIN_ACQ_FUND',
558                     'ADMIN_FUND',
559                     'ACQ_INVOICE-REOPEN',
560                     'ADMIN_ACQ_DISTRIB_FORMULA',
561                     'ADMIN_INVOICE',
562                     'IMPORT_ACQ_LINEITEM_BIB_RECORD_UPLOAD',
563                     'VIEW_IMPORT_MATCH_SET',
564                     'VIEW_MERGE_PROFILE'
565                 );
566
567
568 SELECT evergreen.upgrade_deps_block_check('1219', :eg_version);
569
570 CREATE VIEW acq.li_state_label AS
571   SELECT *
572   FROM (VALUES
573           ('new', 'New'),
574           ('selector-ready', 'Selector-Ready'),
575           ('order-ready', 'Order-Ready'),
576           ('approved', 'Approved'),
577           ('pending-order', 'Pending-Order'),
578           ('on-order', 'On-Order'),
579           ('received', 'Received'),
580           ('cancelled', 'Cancelled')
581        ) AS t (id,label);
582
583 CREATE VIEW acq.po_state_label AS
584   SELECT *
585   FROM (VALUES
586           ('new', 'New'),
587           ('pending', 'Pending'), 
588           ('on-order', 'On-Order'),
589           ('received', 'Received'),
590           ('cancelled', 'Cancelled')
591        ) AS t (id,label);
592
593
594 SELECT evergreen.upgrade_deps_block_check('1220', :eg_version);
595
596 INSERT into config.org_unit_setting_type
597 ( name, grp, label, description, datatype, fm_class ) VALUES
598 ( 'circ.holds.calculated_age_proximity', 'circ',
599     oils_i18n_gettext('circ.holds.calculated_age_proximity',
600         'Use calculated proximity for age-protection check',
601         'coust', 'label'),
602     oils_i18n_gettext('circ.holds.calculated_age_proximity',
603         'When checking whether a copy is viable for a hold based on transit distance, use calculated proximity with adjustments rather than baseline Org Unit proximity.',
604         'coust', 'description'),
605     'bool', null);
606
607
608
609 SELECT evergreen.upgrade_deps_block_check('1221', :eg_version);
610
611 CREATE OR REPLACE FUNCTION action.copy_calculated_proximity(
612     pickup  INT,
613     request INT,
614     vacp_cl  INT,
615     vacp_cm  TEXT,
616     vacn_ol  INT,
617     vacl_ol  INT
618 ) RETURNS NUMERIC AS $f$
619 DECLARE
620     baseline_prox   NUMERIC;
621     aoupa           actor.org_unit_proximity_adjustment%ROWTYPE;
622 BEGIN
623
624     -- First, gather the baseline proximity of "here" to pickup lib
625     SELECT prox INTO baseline_prox FROM actor.org_unit_proximity WHERE from_org = vacp_cl AND to_org = pickup;
626
627     -- Find any absolute adjustments, and set the baseline prox to that
628     SELECT  adj.* INTO aoupa
629       FROM  actor.org_unit_proximity_adjustment adj
630             LEFT JOIN actor.org_unit_ancestors_distance(vacp_cl) acp_cl ON (acp_cl.id = adj.item_circ_lib)
631             LEFT JOIN actor.org_unit_ancestors_distance(vacn_ol) acn_ol ON (acn_ol.id = adj.item_owning_lib)
632             LEFT JOIN actor.org_unit_ancestors_distance(vacl_ol) acl_ol ON (acl_ol.id = adj.copy_location)
633             LEFT JOIN actor.org_unit_ancestors_distance(pickup) ahr_pl ON (ahr_pl.id = adj.hold_pickup_lib)
634             LEFT JOIN actor.org_unit_ancestors_distance(request) ahr_rl ON (ahr_rl.id = adj.hold_request_lib)
635       WHERE (adj.circ_mod IS NULL OR adj.circ_mod = vacp_cm) AND
636             (adj.item_circ_lib IS NULL OR adj.item_circ_lib = acp_cl.id) AND
637             (adj.item_owning_lib IS NULL OR adj.item_owning_lib = acn_ol.id) AND
638             (adj.copy_location IS NULL OR adj.copy_location = acl_ol.id) AND
639             (adj.hold_pickup_lib IS NULL OR adj.hold_pickup_lib = ahr_pl.id) AND
640             (adj.hold_request_lib IS NULL OR adj.hold_request_lib = ahr_rl.id) AND
641             absolute_adjustment AND
642             COALESCE(acp_cl.id, acn_ol.id, acl_ol.id, ahr_pl.id, ahr_rl.id) IS NOT NULL
643       ORDER BY
644             COALESCE(acp_cl.distance,999)
645                 + COALESCE(acn_ol.distance,999)
646                 + COALESCE(acl_ol.distance,999)
647                 + COALESCE(ahr_pl.distance,999)
648                 + COALESCE(ahr_rl.distance,999),
649             adj.pos
650       LIMIT 1;
651
652     IF FOUND THEN
653         baseline_prox := aoupa.prox_adjustment;
654     END IF;
655
656     -- Now find any relative adjustments, and change the baseline prox based on them
657     FOR aoupa IN
658         SELECT  adj.*
659           FROM  actor.org_unit_proximity_adjustment adj
660                 LEFT JOIN actor.org_unit_ancestors_distance(vacp_cl) acp_cl ON (acp_cl.id = adj.item_circ_lib)
661                 LEFT JOIN actor.org_unit_ancestors_distance(vacn_ol) acn_ol ON (acn_ol.id = adj.item_owning_lib)
662                 LEFT JOIN actor.org_unit_ancestors_distance(vacl_ol) acl_ol ON (acn_ol.id = adj.copy_location)
663                 LEFT JOIN actor.org_unit_ancestors_distance(pickup) ahr_pl ON (ahr_pl.id = adj.hold_pickup_lib)
664                 LEFT JOIN actor.org_unit_ancestors_distance(request) ahr_rl ON (ahr_rl.id = adj.hold_request_lib)
665           WHERE (adj.circ_mod IS NULL OR adj.circ_mod = vacp_cm) AND
666                 (adj.item_circ_lib IS NULL OR adj.item_circ_lib = acp_cl.id) AND
667                 (adj.item_owning_lib IS NULL OR adj.item_owning_lib = acn_ol.id) AND
668                 (adj.copy_location IS NULL OR adj.copy_location = acl_ol.id) AND
669                 (adj.hold_pickup_lib IS NULL OR adj.hold_pickup_lib = ahr_pl.id) AND
670                 (adj.hold_request_lib IS NULL OR adj.hold_request_lib = ahr_rl.id) AND
671                 NOT absolute_adjustment AND
672                 COALESCE(acp_cl.id, acn_ol.id, acl_ol.id, ahr_pl.id, ahr_rl.id) IS NOT NULL
673     LOOP
674         baseline_prox := baseline_prox + aoupa.prox_adjustment;
675     END LOOP;
676
677     RETURN baseline_prox;
678 END;
679 $f$ LANGUAGE PLPGSQL;
680
681 CREATE OR REPLACE FUNCTION action.hold_copy_calculated_proximity(
682     ahr_id INT,
683     acp_id BIGINT,
684     copy_context_ou INT DEFAULT NULL
685     -- TODO maybe? hold_context_ou INT DEFAULT NULL.  This would optionally
686     -- support an "ahprox" measurement: adjust prox between copy circ lib and
687     -- hold request lib, but I'm unsure whether to use this theoretical
688     -- argument only in the baseline calculation or later in the other
689     -- queries in this function.
690 ) RETURNS NUMERIC AS $f$
691 DECLARE
692     ahr  action.hold_request%ROWTYPE;
693     acp  asset.copy%ROWTYPE;
694     acn  asset.call_number%ROWTYPE;
695     acl  asset.copy_location%ROWTYPE;
696
697     prox NUMERIC;
698 BEGIN
699
700     SELECT * INTO ahr FROM action.hold_request WHERE id = ahr_id;
701     SELECT * INTO acp FROM asset.copy WHERE id = acp_id;
702     SELECT * INTO acn FROM asset.call_number WHERE id = acp.call_number;
703     SELECT * INTO acl FROM asset.copy_location WHERE id = acp.location;
704
705     IF copy_context_ou IS NULL THEN
706         copy_context_ou := acp.circ_lib;
707     END IF;
708
709     SELECT action.copy_calculated_proximity(
710         ahr.pickup_lib,
711         ahr.request_lib,
712         copy_context_ou,
713         acp.circ_modifier,
714         acn.owning_lib,
715         acl.owning_lib
716     ) INTO prox;
717
718     RETURN prox;
719 END;
720 $f$ LANGUAGE PLPGSQL;
721
722 CREATE OR REPLACE FUNCTION action.hold_request_permit_test( pickup_ou INT, request_ou INT, match_item BIGINT, match_user INT, match_requestor INT, retargetting BOOL ) RETURNS SETOF action.matrix_test_result AS $func$
723 DECLARE
724     matchpoint_id        INT;
725     user_object        actor.usr%ROWTYPE;
726     age_protect_object    config.rule_age_hold_protect%ROWTYPE;
727     standing_penalty    config.standing_penalty%ROWTYPE;
728     transit_range_ou_type    actor.org_unit_type%ROWTYPE;
729     transit_source        actor.org_unit%ROWTYPE;
730     item_object        asset.copy%ROWTYPE;
731     item_cn_object     asset.call_number%ROWTYPE;
732     item_status_object  config.copy_status%ROWTYPE;
733     item_location_object    asset.copy_location%ROWTYPE;
734     ou_skip              actor.org_unit_setting%ROWTYPE;
735     calc_age_prox        actor.org_unit_setting%ROWTYPE;
736     result            action.matrix_test_result;
737     hold_test        config.hold_matrix_matchpoint%ROWTYPE;
738     use_active_date   TEXT;
739     prox_ou           INT;
740     age_protect_date  TIMESTAMP WITH TIME ZONE;
741     hold_count        INT;
742     hold_transit_prox    NUMERIC;
743     frozen_hold_count    INT;
744     context_org_list    INT[];
745     done            BOOL := FALSE;
746     hold_penalty TEXT;
747     v_pickup_ou ALIAS FOR pickup_ou;
748     v_request_ou ALIAS FOR request_ou;
749     item_prox INT;
750     pickup_prox INT;
751 BEGIN
752     SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
753     SELECT INTO context_org_list ARRAY_AGG(id) FROM actor.org_unit_full_path( v_pickup_ou );
754
755     result.success := TRUE;
756
757     -- The HOLD penalty block only applies to new holds.
758     -- The CAPTURE penalty block applies to existing holds.
759     hold_penalty := 'HOLD';
760     IF retargetting THEN
761         hold_penalty := 'CAPTURE';
762     END IF;
763
764     -- Fail if we couldn't find a user
765     IF user_object.id IS NULL THEN
766         result.fail_part := 'no_user';
767         result.success := FALSE;
768         done := TRUE;
769         RETURN NEXT result;
770         RETURN;
771     END IF;
772
773     SELECT INTO item_object * FROM asset.copy WHERE id = match_item;
774
775     -- Fail if we couldn't find a copy
776     IF item_object.id IS NULL THEN
777         result.fail_part := 'no_item';
778         result.success := FALSE;
779         done := TRUE;
780         RETURN NEXT result;
781         RETURN;
782     END IF;
783
784     SELECT INTO matchpoint_id action.find_hold_matrix_matchpoint(v_pickup_ou, v_request_ou, match_item, match_user, match_requestor);
785     result.matchpoint := matchpoint_id;
786
787     SELECT INTO ou_skip * FROM actor.org_unit_setting WHERE name = 'circ.holds.target_skip_me' AND org_unit = item_object.circ_lib;
788
789     -- Fail if the circ_lib for the item has circ.holds.target_skip_me set to true
790     IF ou_skip.id IS NOT NULL AND ou_skip.value = 'true' THEN
791         result.fail_part := 'circ.holds.target_skip_me';
792         result.success := FALSE;
793         done := TRUE;
794         RETURN NEXT result;
795         RETURN;
796     END IF;
797
798     -- Fail if user is barred
799     IF user_object.barred IS TRUE THEN
800         result.fail_part := 'actor.usr.barred';
801         result.success := FALSE;
802         done := TRUE;
803         RETURN NEXT result;
804         RETURN;
805     END IF;
806
807     SELECT INTO item_cn_object * FROM asset.call_number WHERE id = item_object.call_number;
808     SELECT INTO item_status_object * FROM config.copy_status WHERE id = item_object.status;
809     SELECT INTO item_location_object * FROM asset.copy_location WHERE id = item_object.location;
810
811     -- Fail if we couldn't find any matchpoint (requires a default)
812     IF matchpoint_id IS NULL THEN
813         result.fail_part := 'no_matchpoint';
814         result.success := FALSE;
815         done := TRUE;
816         RETURN NEXT result;
817         RETURN;
818     END IF;
819
820     SELECT INTO hold_test * FROM config.hold_matrix_matchpoint WHERE id = matchpoint_id;
821
822     IF hold_test.holdable IS FALSE THEN
823         result.fail_part := 'config.hold_matrix_test.holdable';
824         result.success := FALSE;
825         done := TRUE;
826         RETURN NEXT result;
827     END IF;
828
829     IF item_object.holdable IS FALSE THEN
830         result.fail_part := 'item.holdable';
831         result.success := FALSE;
832         done := TRUE;
833         RETURN NEXT result;
834     END IF;
835
836     IF item_status_object.holdable IS FALSE THEN
837         result.fail_part := 'status.holdable';
838         result.success := FALSE;
839         done := TRUE;
840         RETURN NEXT result;
841     END IF;
842
843     IF item_location_object.holdable IS FALSE THEN
844         result.fail_part := 'location.holdable';
845         result.success := FALSE;
846         done := TRUE;
847         RETURN NEXT result;
848     END IF;
849
850     IF hold_test.transit_range IS NOT NULL THEN
851         SELECT INTO transit_range_ou_type * FROM actor.org_unit_type WHERE id = hold_test.transit_range;
852         IF hold_test.distance_is_from_owner THEN
853             SELECT INTO transit_source ou.* FROM actor.org_unit ou JOIN asset.call_number cn ON (cn.owning_lib = ou.id) WHERE cn.id = item_object.call_number;
854         ELSE
855             SELECT INTO transit_source * FROM actor.org_unit WHERE id = item_object.circ_lib;
856         END IF;
857
858         PERFORM * FROM actor.org_unit_descendants( transit_source.id, transit_range_ou_type.depth ) WHERE id = v_pickup_ou;
859
860         IF NOT FOUND THEN
861             result.fail_part := 'transit_range';
862             result.success := FALSE;
863             done := TRUE;
864             RETURN NEXT result;
865         END IF;
866     END IF;
867
868     -- Proximity of user's home_ou to the pickup_lib to see if penalty should be ignored.
869     SELECT INTO pickup_prox prox FROM actor.org_unit_proximity WHERE from_org = user_object.home_ou AND to_org = v_pickup_ou;
870     -- Proximity of user's home_ou to the items' lib to see if penalty should be ignored.
871     IF hold_test.distance_is_from_owner THEN
872         SELECT INTO item_prox prox FROM actor.org_unit_proximity WHERE from_org = user_object.home_ou AND to_org = item_cn_object.owning_lib;
873     ELSE
874         SELECT INTO item_prox prox FROM actor.org_unit_proximity WHERE from_org = user_object.home_ou AND to_org = item_object.circ_lib;
875     END IF;
876
877     FOR standing_penalty IN
878         SELECT  DISTINCT csp.*
879           FROM  actor.usr_standing_penalty usp
880                 JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
881           WHERE usr = match_user
882                 AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
883                 AND (usp.stop_date IS NULL or usp.stop_date > NOW())
884                 AND (csp.ignore_proximity IS NULL OR csp.ignore_proximity < item_prox
885                      OR csp.ignore_proximity < pickup_prox)
886                 AND csp.block_list LIKE '%' || hold_penalty || '%' LOOP
887
888         result.fail_part := standing_penalty.name;
889         result.success := FALSE;
890         done := TRUE;
891         RETURN NEXT result;
892     END LOOP;
893
894     IF hold_test.stop_blocked_user IS TRUE THEN
895         FOR standing_penalty IN
896             SELECT  DISTINCT csp.*
897               FROM  actor.usr_standing_penalty usp
898                     JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
899               WHERE usr = match_user
900                     AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
901                     AND (usp.stop_date IS NULL or usp.stop_date > NOW())
902                     AND csp.block_list LIKE '%CIRC%' LOOP
903
904             result.fail_part := standing_penalty.name;
905             result.success := FALSE;
906             done := TRUE;
907             RETURN NEXT result;
908         END LOOP;
909     END IF;
910
911     IF hold_test.max_holds IS NOT NULL AND NOT retargetting THEN
912         SELECT    INTO hold_count COUNT(*)
913           FROM    action.hold_request
914           WHERE    usr = match_user
915             AND fulfillment_time IS NULL
916             AND cancel_time IS NULL
917             AND CASE WHEN hold_test.include_frozen_holds THEN TRUE ELSE frozen IS FALSE END;
918
919         IF hold_count >= hold_test.max_holds THEN
920             result.fail_part := 'config.hold_matrix_test.max_holds';
921             result.success := FALSE;
922             done := TRUE;
923             RETURN NEXT result;
924         END IF;
925     END IF;
926
927     IF item_object.age_protect IS NOT NULL THEN
928         SELECT INTO age_protect_object * FROM config.rule_age_hold_protect WHERE id = item_object.age_protect;
929         IF hold_test.distance_is_from_owner THEN
930             SELECT INTO use_active_date value FROM actor.org_unit_ancestor_setting('circ.holds.age_protect.active_date', item_cn_object.owning_lib);
931         ELSE
932             SELECT INTO use_active_date value FROM actor.org_unit_ancestor_setting('circ.holds.age_protect.active_date', item_object.circ_lib);
933         END IF;
934         IF use_active_date = 'true' THEN
935             age_protect_date := COALESCE(item_object.active_date, NOW());
936         ELSE
937             age_protect_date := item_object.create_date;
938         END IF;
939         IF age_protect_date + age_protect_object.age > NOW() THEN
940             SELECT INTO calc_age_prox * FROM actor.org_unit_setting WHERE name = 'circ.holds.calculated_age_proximity' AND org_unit = item_object.circ_lib;
941             IF hold_test.distance_is_from_owner THEN
942                 prox_ou := item_cn_object.owning_lib;
943             ELSE
944                 prox_ou := item_object.circ_lib;
945             END IF;
946             IF calc_age_prox.id IS NOT NULL AND calc_age_prox.value = 'true' THEN
947                 SELECT INTO hold_transit_prox action.copy_calculated_proximity(
948                     v_pickup_ou,
949                     v_request_ou,
950                     prox_ou,
951                     item_object.circ_modifier,
952                     item_cn_object.owning_lib,
953                     item_location_object.owning_lib
954                 );
955             ELSE
956                 SELECT INTO hold_transit_prox prox::NUMERIC FROM actor.org_unit_proximity WHERE from_org = prox_ou AND to_org = v_pickup_ou;
957             END IF;
958
959             IF hold_transit_prox > age_protect_object.prox::NUMERIC THEN
960                 result.fail_part := 'config.rule_age_hold_protect.prox';
961                 result.success := FALSE;
962                 done := TRUE;
963                 RETURN NEXT result;
964             END IF;
965         END IF;
966     END IF;
967
968     IF NOT done THEN
969         RETURN NEXT result;
970     END IF;
971
972     RETURN;
973 END;
974 $func$ LANGUAGE plpgsql;
975
976
977   
978 SELECT evergreen.upgrade_deps_block_check('1222', :eg_version);
979
980 INSERT INTO action_trigger.reactor (module, description) VALUES (
981     'CallHTTP', 'Push event information out to an external system via HTTP'
982 );
983
984 INSERT INTO action_trigger.hook (key, core_type, description, passive) VALUES (
985     'bre.edit', 'bre', 'A bib record was edited', FALSE
986 );
987
988
989
990 SELECT evergreen.upgrade_deps_block_check('1223', :eg_version);
991
992 -- First, normalize the au.create[d] and au.update[d] hooks.  The code and seed data differ.
993
994 INSERT INTO action_trigger.hook (key, core_type, description, passive)
995     VALUES ('au.created', 'au', 'A user was created', 't') ON CONFLICT DO NOTHING;
996 INSERT INTO action_trigger.hook (key, core_type, description, passive)
997     VALUES ('au.updated', 'au', 'A user was updated', 't') ON CONFLICT DO NOTHING;
998
999
1000 UPDATE action_trigger.event_definition SET hook = 'au.created' WHERE hook = 'au.create';
1001 UPDATE action_trigger.event_definition SET hook = 'au.updated' WHERE hook = 'au.update';
1002
1003 DELETE FROM action_trigger.hook WHERE key = 'au.create';
1004 DELETE FROM action_trigger.hook WHERE key = 'au.update';
1005
1006 -- Now the entirely new ones...
1007 INSERT INTO action_trigger.hook (key, core_type, description, passive)
1008     VALUES ('au.renewed', 'au', 'A user was renewed by having their expire date changed', 't');
1009
1010 INSERT INTO action_trigger.hook (key, core_type, description, passive)
1011     VALUES ('au.barcode_changed', 'au', 'A card was updated or created for an existing user', 't');
1012
1013
1014 SELECT evergreen.upgrade_deps_block_check('1224', :eg_version);
1015
1016 INSERT INTO config.coded_value_map (id,ctype,code,opac_visible,is_simple,value,search_label) VALUES
1017 (1736,'icon_format','preloadedaudio',TRUE,FALSE,
1018     oils_i18n_gettext(1736, 'Preloaded Audio', 'ccvm', 'value'),
1019     oils_i18n_gettext(1736, 'Preloaded Audio', 'ccvm', 'search_label')),
1020 (1737,'search_format','preloadedaudio',TRUE,FALSE,
1021     oils_i18n_gettext(1737, 'Preloaded Audio', 'ccvm', 'value'),
1022     oils_i18n_gettext(1737, 'Preloaded Audio', 'ccvm', 'search_label'))
1023 ;
1024
1025 INSERT INTO config.composite_attr_entry_definition (coded_value, definition) VALUES
1026 ((SELECT id from config.coded_value_map where ctype = 'search_format' AND code = 'preloadedaudio'),'{"0":{"_attr":"item_type","_val":"i"},"1":{"_attr":"item_form","_val":"q"}}'),
1027 ((SELECT id from config.coded_value_map where ctype = 'icon_format' AND code = 'preloadedaudio'),'{"0":{"_attr":"item_type","_val":"i"},"1":{"_attr":"item_form","_val":"q"}}');
1028
1029
1030
1031 SELECT evergreen.upgrade_deps_block_check('1225', :eg_version);
1032
1033 ALTER TABLE acq.provider ADD COLUMN primary_contact INT;
1034 ALTER TABLE acq.provider ADD CONSTRAINT acq_provider_primary_contact_fkey FOREIGN KEY (primary_contact) REFERENCES acq.provider_contact (id) ON DELETE SET NULL ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED;
1035
1036
1037 SELECT evergreen.upgrade_deps_block_check('1226', :eg_version);
1038
1039 INSERT INTO config.workstation_setting_type
1040     (name, grp, datatype, label)
1041 VALUES (
1042     'eg.grid.acq.provider.addresses', 'gui', 'object',
1043     oils_i18n_gettext(
1044     'eg.grid.acq.provider.addresses',
1045     'Grid Config: acq.provider.addresses',
1046     'cwst', 'label')
1047 ), (
1048     'eg.grid.acq.provider.attributes', 'gui', 'object',
1049     oils_i18n_gettext(
1050     'eg.grid.acq.provider.attributes',
1051     'Grid Config: acq.provider.attributes',
1052     'cwst', 'label')
1053 ), (
1054     'eg.grid.acq.provider.contact.addresses', 'gui', 'object',
1055     oils_i18n_gettext(
1056     'eg.grid.acq.provider.contact.addresses',
1057     'Grid Config: acq.provider.contact.addresses',
1058     'cwst', 'label')
1059 ), (
1060     'eg.grid.acq.provider.contacts', 'gui', 'object',
1061     oils_i18n_gettext(
1062     'eg.grid.acq.provider.contacts',
1063     'Grid Config: acq.provider.contacts',
1064     'cwst', 'label')
1065 ), (
1066     'eg.grid.acq.provider.edi_accounts', 'gui', 'object',
1067     oils_i18n_gettext(
1068     'eg.grid.acq.provider.edi_accounts',
1069     'Grid Config: acq.provider.edi_accounts',
1070     'cwst', 'label')
1071 ), (
1072     'eg.grid.acq.provider.edi_messages', 'gui', 'object',
1073     oils_i18n_gettext(
1074     'eg.grid.acq.provider.edi_messages',
1075     'Grid Config: acq.provider.edi_messages',
1076     'cwst', 'label')
1077 ), (
1078     'eg.grid.acq.provider.holdings', 'gui', 'object',
1079     oils_i18n_gettext(
1080     'eg.grid.acq.provider.holdings',
1081     'Grid Config: acq.provider.holdings',
1082     'cwst', 'label')
1083 ), (
1084     'eg.grid.acq.provider.invoices', 'gui', 'object',
1085     oils_i18n_gettext(
1086     'eg.grid.acq.provider.invoices',
1087     'Grid Config: acq.provider.invoices',
1088     'cwst', 'label')
1089 ), (
1090     'eg.grid.acq.provider.purchaseorders', 'gui', 'object',
1091     oils_i18n_gettext(
1092     'eg.grid.acq.provider.purchaseorders',
1093     'Grid Config: acq.provider.purchaseorders',
1094     'cwst', 'label')
1095 ), (
1096     'eg.grid.acq.provider.search.results', 'gui', 'object',
1097     oils_i18n_gettext(
1098     'eg.grid.acq.provider.search.results',
1099     'Grid Config: acq.provider.search.results',
1100     'cwst', 'label')
1101 );
1102
1103
1104 SELECT evergreen.upgrade_deps_block_check('1227', :eg_version);
1105
1106 INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
1107 VALUES (
1108     'eg.grid.cat.authority.browse', 'gui', 'object',
1109     oils_i18n_gettext(
1110     'eg.grid.cat.authority.browse',
1111     'Grid Config: eg.grid.cat.authority.browse',
1112     'cwst', 'label')
1113 ), (
1114     'eg.grid.cat.authority.manage.bibs', 'gui', 'object',
1115     oils_i18n_gettext(
1116     'eg.grid.cat.authority.manage.bibs',
1117     'Grid Config: eg.grid.cat.authority.manage.bibs',
1118     'cwst', 'label')
1119 );
1120
1121
1122 SELECT evergreen.upgrade_deps_block_check('1228', :eg_version);
1123
1124 CREATE OR REPLACE FUNCTION actor.org_unit_full_path ( INT ) RETURNS SETOF actor.org_unit AS $$
1125     SELECT  aou.*
1126       FROM  actor.org_unit AS aou
1127             JOIN (
1128                 (SELECT au.id, t.depth FROM actor.org_unit_ancestors($1) AS au JOIN actor.org_unit_type t ON (au.ou_type = t.id))
1129                     UNION
1130                 (SELECT au.id, t.depth FROM actor.org_unit_descendants($1) AS au JOIN actor.org_unit_type t ON (au.ou_type = t.id))
1131             ) AS ad ON (aou.id=ad.id)
1132       ORDER BY ad.depth;
1133 $$ LANGUAGE SQL STABLE;
1134
1135
1136
1137 SELECT evergreen.upgrade_deps_block_check('1229', :eg_version);
1138
1139
1140 INSERT into action_trigger.hook (key, core_type, description) VALUES (
1141     'au.email.test', 'au', 'A test email has been requested for this user'
1142 ),
1143 (
1144     'au.sms_text.test', 'au', 'A test SMS has been requested for this user'
1145 );
1146
1147 INSERT INTO action_trigger.event_definition (active, owner, name, hook, validator, reactor, delay, template)
1148 VALUES (
1149     't', 1, 'Send Test Email', 'au.email.test', 'NOOP_True', 'SendEmail', '00:01:00', 
1150 $$
1151 [%- USE date -%]
1152 [%- user = target -%]
1153 [%- lib = target.home_ou -%]
1154 To: [%- user.email %]
1155 From: [%- helpers.get_org_setting(target.home_ou.id, 'org.bounced_emails') || lib.email || params.sender_email || default_sender %]
1156 Date: [%- date.format(date.now, '%a, %d %b %Y %T -0000', gmt => 1) %]
1157 Reply-To: [%- lib.email || params.sender_email || default_sender %]
1158 Subject: Email Test Notification
1159 Auto-Submitted: auto-generated
1160
1161 Dear [% user.first_given_name %] [% user.family_name %],
1162
1163 This is a test of the email associated with your account at [%- lib.name -%]. If you are receiving this message, your email information is correct.
1164
1165 Sincerely,
1166 [% lib.name %]
1167
1168 Contact your library for more information:
1169
1170 [% lib.name %]
1171 [%- SET addr = lib.mailing_address -%]
1172 [%- IF !addr -%] [%- SET addr = lib.billing_address -%] [%- END %]
1173 [% addr.street1 %] [% addr.street2 %]
1174 [% addr.city %], [% addr.state %]
1175 [% addr.post_code %]
1176 [% lib.phone %]
1177 $$);
1178
1179 INSERT INTO action_trigger.environment (event_def, path)
1180 VALUES (currval('action_trigger.event_definition_id_seq'), 'home_ou'),
1181        (currval('action_trigger.event_definition_id_seq'), 'home_ou.mailing_address'),
1182        (currval('action_trigger.event_definition_id_seq'), 'home_ou.billing_address');
1183
1184 INSERT INTO action_trigger.event_definition (active, owner, name, hook, validator, reactor, delay, template)
1185 VALUES (
1186     't', 1, 'Send Test SMS', 'au.sms_text.test', 'NOOP_True', 'SendSMS', '00:01:00', 
1187 $$
1188 [%- USE date -%]
1189 [%- user = target -%]
1190 [%- lib = user.home_ou -%]
1191 [%- sms_number = helpers.get_user_setting(target.id, 'opac.default_sms_notify') -%]
1192 [%- sms_carrier = helpers.get_user_setting(target.id, 'opac.default_sms_carrier') -%]
1193 From: [%- helpers.get_org_setting(target.home_ou.id, 'org.bounced_emails') || lib.email || params.sender_email || default_sender %]
1194 To: [%- helpers.get_sms_gateway_email(sms_carrier,sms_number) %]
1195 Subject: Test Text Message
1196
1197 This is a test confirming your mobile number for [% lib.name %] is correct.
1198
1199 Sincerely,
1200 [% lib.name %]
1201
1202 Contact your library for more information:
1203
1204 [% lib.name %]
1205 [%- SET addr = lib.mailing_address -%]
1206 [%- IF !addr -%] [%- SET addr = lib.billing_address -%] [%- END %]
1207 [% addr.street1 %] [% addr.street2 %]
1208 [% addr.city %], [% addr.state %]
1209 [% addr.post_code %]
1210 [% lib.phone %]
1211 $$);
1212
1213 INSERT INTO action_trigger.environment (event_def, path)
1214 VALUES (currval('action_trigger.event_definition_id_seq'), 'home_ou'),
1215        (currval('action_trigger.event_definition_id_seq'), 'home_ou.mailing_address'),
1216        (currval('action_trigger.event_definition_id_seq'), 'home_ou.billing_address');
1217
1218
1219
1220 SELECT evergreen.upgrade_deps_block_check('1230', :eg_version);
1221
1222 INSERT INTO permission.perm_list
1223     ( id, code, description )
1224 VALUES (
1225     623, 'UPDATE_ORG_UNIT_SETTING.opac.matomo', oils_i18n_gettext(623,
1226     'Allows a user to configure Matomo Analytics org unit settings', 'ppl', 'description')
1227 );
1228
1229 INSERT into config.org_unit_setting_type
1230     ( name, grp, label, description, datatype, update_perm )
1231 VALUES (
1232     'opac.analytics.matomo_id', 'opac',
1233     oils_i18n_gettext(
1234     'opac.analytics.matomo_id',
1235     'Matomo Site ID',
1236     'coust', 'label'),
1237     oils_i18n_gettext('opac.analytics.matomo_id',
1238     'The Site ID for your Evergreen catalog. You can find the Site ID in the tracking code you got from Matomo.',
1239     'coust', 'description'),
1240     'string', 623
1241 ), (
1242     'opac.analytics.matomo_url', 'opac',
1243     oils_i18n_gettext('opac.analytics.matomo_url',
1244     'Matomo URL',
1245     'coust', 'label'),
1246     oils_i18n_gettext('opac.analytics.matomo_url',
1247     'The URL for your the Matomo software. Be sure to include the trailing slash, e.g. https://my-evergreen.matomo.cloud/',
1248     'coust', 'description'),
1249     'string', 623
1250 );
1251
1252
1253
1254 SELECT evergreen.upgrade_deps_block_check('1231', :eg_version);
1255
1256 INSERT into config.org_unit_setting_type
1257 ( name, grp, label, description, datatype, fm_class ) VALUES
1258 ( 'opac.email_record.allow_without_login', 'opac',
1259     oils_i18n_gettext('opac.email_record.allow_without_login',
1260         'Allow record emailing without login',
1261         'coust', 'label'),
1262     oils_i18n_gettext('opac.email_record.allow_without_login',
1263         'Instead of forcing a patron to log in in order to email the details of a record, just challenge them with a simple catpcha.',
1264         'coust', 'description'),
1265     'bool', null)
1266 ;
1267
1268 CREATE TABLE action_trigger.event_def_group (
1269     id      SERIAL  PRIMARY KEY,
1270     owner   INT     NOT NULL REFERENCES actor.org_unit (id)
1271                         ON DELETE RESTRICT ON UPDATE CASCADE
1272                         DEFERRABLE INITIALLY DEFERRED,
1273     hook    TEXT    NOT NULL REFERENCES action_trigger.hook (key)
1274                         ON DELETE RESTRICT ON UPDATE CASCADE
1275                         DEFERRABLE INITIALLY DEFERRED,
1276     active  BOOL    NOT NULL DEFAULT TRUE,
1277     name    TEXT    NOT NULL
1278 );
1279 SELECT SETVAL('action_trigger.event_def_group_id_seq'::TEXT, 100, TRUE);
1280
1281 CREATE TABLE action_trigger.event_def_group_member (
1282     id          SERIAL  PRIMARY KEY,
1283     grp         INT     NOT NULL REFERENCES action_trigger.event_def_group (id)
1284                             ON DELETE CASCADE ON UPDATE CASCADE
1285                             DEFERRABLE INITIALLY DEFERRED,
1286     event_def   INT     NOT NULL REFERENCES action_trigger.event_definition (id)
1287                             ON DELETE RESTRICT ON UPDATE CASCADE
1288                             DEFERRABLE INITIALLY DEFERRED,
1289     sortable    BOOL    NOT NULL DEFAULT TRUE,
1290     holdings    BOOL    NOT NULL DEFAULT FALSE,
1291     external    BOOL    NOT NULL DEFAULT FALSE,
1292     name        TEXT    NOT NULL
1293 );
1294
1295 INSERT INTO action_trigger.event_def_group (id, owner, hook, name)
1296     VALUES (1, 1, 'biblio.format.record_entry.print','Print Record(s)');
1297
1298 INSERT INTO action_trigger.event_def_group (id, owner, hook, name)
1299     VALUES (2,1,'biblio.format.record_entry.email','Email Record(s)');
1300
1301 DO $block$
1302 BEGIN
1303   PERFORM * FROM action_trigger.event_definition WHERE hook = 'biblio.format.record_entry.email' AND owner = 1 AND active AND template =
1304 $$
1305 [%- USE date -%]
1306 [%- SET user = target.0.owner -%]
1307 To: [%- params.recipient_email || user.email %]
1308 From: [%- params.sender_email || default_sender %]
1309 Date: [%- date.format(date.now, '%a, %d %b %Y %T -0000', gmt => 1) %]
1310 Subject: Bibliographic Records
1311 Auto-Submitted: auto-generated
1312
1313 [% FOR cbreb IN target %]
1314 [% FOR item IN cbreb.items;
1315     bre_id = item.target_biblio_record_entry;
1316
1317     bibxml = helpers.unapi_bre(bre_id, {flesh => '{mra}'});
1318     title = '';
1319     FOR part IN bibxml.findnodes('//*[@tag="245"]/*[@code="a" or @code="b"]');
1320         title = title _ part.textContent;
1321     END;
1322
1323     author = bibxml.findnodes('//*[@tag="100"]/*[@code="a"]').textContent;
1324     item_type = bibxml.findnodes('//*[local-name()="attributes"]/*[local-name()="field"][@name="item_type"]').getAttribute('coded-value');
1325     publisher = bibxml.findnodes('//*[@tag="260"]/*[@code="b"]').textContent;
1326     pubdate = bibxml.findnodes('//*[@tag="260"]/*[@code="c"]').textContent;
1327     isbn = bibxml.findnodes('//*[@tag="020"]/*[@code="a"]').textContent;
1328     issn = bibxml.findnodes('//*[@tag="022"]/*[@code="a"]').textContent;
1329     upc = bibxml.findnodes('//*[@tag="024"]/*[@code="a"]').textContent;
1330 %]
1331
1332 [% loop.count %]/[% loop.size %].  Bib ID# [% bre_id %]
1333 [% IF isbn %]ISBN: [% isbn _ "\n" %][% END -%]
1334 [% IF issn %]ISSN: [% issn _ "\n" %][% END -%]
1335 [% IF upc  %]UPC:  [% upc _ "\n" %] [% END -%]
1336 Title: [% title %]
1337 Author: [% author %]
1338 Publication Info: [% publisher %] [% pubdate %]
1339 Item Type: [% item_type %]
1340
1341 [% END %]
1342 [% END %]
1343 $$;
1344
1345   IF FOUND THEN -- update
1346
1347     INSERT INTO action_trigger.event_def_group_member (grp, name, event_def)
1348         SELECT 2, 'Brief', id FROM action_trigger.event_definition WHERE hook = 'biblio.format.record_entry.email';
1349
1350     INSERT INTO action_trigger.event_def_group_member (grp, name, holdings, event_def)
1351         SELECT 2, 'Full', TRUE, id FROM action_trigger.event_definition WHERE hook = 'biblio.format.record_entry.email';
1352
1353     UPDATE action_trigger.event_definition SET template = $$
1354 [%- USE date -%]
1355 [%- SET user = target.0.owner -%]
1356 To: [%- params.recipient_email || user_data.0.email || user.email %]
1357 From: [%- params.sender_email || default_sender %]
1358 Date: [%- date.format(date.now, '%a, %d %b %Y %T -0000', gmt => 1) %]
1359 Subject: [%- user_data.0.subject || 'Bibliographic Records' %]
1360 Auto-Submitted: auto-generated
1361
1362 [%- FOR cbreb IN target;
1363
1364     flesh_list = '{mra';
1365     IF user_data.0.type == 'full';
1366         flesh_list = flesh_list _ ',holdings_xml,acp';
1367         IF params.holdings_limit;
1368             flimit = 'acn=>' _ params.holdings_limit _ ',acp=>' _ params.holdings_limit;
1369         END;
1370     END;
1371     flesh_list = flesh_list _ '}';
1372
1373     item_list = helpers.sort_bucket_unapi_bre(cbreb.items,{flesh => flesh_list, site => user_data.0.context_org, flesh_limit => flimit}, user_data.0.sort_by, user_data.0.sort_dir);
1374
1375 FOR item IN item_list -%]
1376
1377 [% loop.count %]/[% loop.size %].  Bib ID# [% item.id %]
1378 [% IF item.isbn %]ISBN: [% item.isbn _ "\n" %][% END -%]
1379 [% IF item.issn %]ISSN: [% item.issn _ "\n" %][% END -%]
1380 [% IF item.upc  %]UPC:  [% item.upc _ "\n" %][% END -%]
1381 Title: [% item.title %]
1382 [% IF item.author %]Author: [% item.author _ "\n" %][% END -%]
1383 Publication Info: [% item.publisher %] [% item.pubdate %]
1384 Item Type: [% item.item_type %]
1385 [% IF user_data.0.type == 'full' && item.holdings.size == 0 %]
1386  * No items for this record at the selected location
1387 [%- END %]
1388 [% FOR cp IN item.holdings -%]
1389  * Library: [% cp.circ_lib %]
1390    Location: [% cp.location %]
1391    Call Number: [% cp.prefix _ ' ' _ cp.callnumber _ ' ' _ cp.suffix %]
1392 [% IF cp.parts %]   Parts: [% cp.parts _ "\n" %][% END -%]
1393    Status: [% cp.status_label %]
1394    Barcode: [% cp.barcode %]
1395  
1396 [% END -%]
1397 [%- END -%]
1398 [%- END -%]
1399 $$ WHERE hook = 'biblio.format.record_entry.email' AND owner = 1 AND active;
1400
1401   ELSE -- insert full and add existing brief
1402
1403     INSERT INTO action_trigger.event_def_group_member (grp, name, event_def)
1404         SELECT 2, 'Brief', id FROM action_trigger.event_definition WHERE hook = 'biblio.format.record_entry.email' AND active;
1405
1406     INSERT INTO action_trigger.event_definition (
1407         active,
1408         owner,
1409         name,
1410         hook,
1411         validator,
1412         reactor,
1413         cleanup_success,
1414         cleanup_failure,
1415         group_field,
1416         granularity,
1417         delay,
1418         template
1419     ) SELECT
1420         TRUE,
1421         owner,
1422         'biblio.record_entry.email.full',
1423         'biblio.format.record_entry.email',
1424         'NOOP_True',
1425         'SendEmail',
1426         'DeleteTempBiblioBucket',
1427         'DeleteTempBiblioBucket',
1428         'owner',
1429         NULL,
1430         '00:00:00',
1431         $$
1432 [%- USE date -%]
1433 [%- SET user = target.0.owner -%]
1434 To: [%- params.recipient_email || user_data.0.email || user.email %]
1435 From: [%- params.sender_email || default_sender %]
1436 Date: [%- date.format(date.now, '%a, %d %b %Y %T -0000', gmt => 1) %]
1437 Subject: [%- user_data.0.subject || 'Bibliographic Records' %]
1438 Auto-Submitted: auto-generated
1439
1440 [%- FOR cbreb IN target;
1441
1442     flesh_list = '{mra';
1443     IF user_data.0.type == 'full';
1444         flesh_list = flesh_list _ ',holdings_xml,acp';
1445         IF params.holdings_limit;
1446             flimit = 'acn=>' _ params.holdings_limit _ ',acp=>' _ params.holdings_limit;
1447         END;
1448     END;
1449     flesh_list = flesh_list _ '}';
1450
1451     item_list = helpers.sort_bucket_unapi_bre(cbreb.items,{flesh => flesh_list, site => user_data.0.context_org, flesh_limit => flimit}, user_data.0.sort_by, user_data.0.sort_dir);
1452
1453 FOR item IN item_list -%]
1454
1455 [% loop.count %]/[% loop.size %].  Bib ID# [% item.id %]
1456 [% IF item.isbn %]ISBN: [% item.isbn _ "\n" %][% END -%]
1457 [% IF item.issn %]ISSN: [% item.issn _ "\n" %][% END -%]
1458 [% IF item.upc  %]UPC:  [% item.upc _ "\n" %][% END -%]
1459 Title: [% item.title %]
1460 [% IF item.author %]Author: [% item.author _ "\n" %][% END -%]
1461 Publication Info: [% item.publisher %] [% item.pubdate %]
1462 Item Type: [% item.item_type %]
1463 [% IF user_data.0.type == 'full' && item.holdings.size == 0 %]
1464  * No items for this record at the selected location
1465 [%- END %]
1466 [% FOR cp IN item.holdings -%]
1467  * Library: [% cp.circ_lib %]
1468    Location: [% cp.location %]
1469    Call Number: [% cp.prefix _ ' ' _ cp.callnumber _ ' ' _ cp.suffix %]
1470 [% IF cp.parts %]   Parts: [% cp.parts _ "\n" %][% END -%]
1471    Status: [% cp.status_label %]
1472    Barcode: [% cp.barcode %]
1473  
1474 [% END -%]
1475 [%- END -%]
1476 [%- END -%]
1477 $$ FROM action_trigger.event_definition WHERE hook = 'biblio.format.record_entry.email' AND active;
1478
1479     INSERT INTO action_trigger.event_def_group_member (grp, name, holdings, event_def)
1480         SELECT 2, 'Full', TRUE, id FROM action_trigger.event_definition WHERE name = 'biblio.record_entry.email.full' and active;
1481
1482   END IF;
1483 END
1484 $block$;
1485
1486 DO $block$
1487 BEGIN
1488   PERFORM * FROM action_trigger.event_definition WHERE hook = 'biblio.format.record_entry.print' AND owner = 1 AND active AND template =
1489 $$
1490 <div>
1491     <style> li { padding: 8px; margin 5px; }</style>
1492     <ol>
1493     [% FOR cbreb IN target %]
1494     [% FOR item IN cbreb.items;
1495         bre_id = item.target_biblio_record_entry;
1496
1497         bibxml = helpers.unapi_bre(bre_id, {flesh => '{mra}'});
1498         title = '';
1499         FOR part IN bibxml.findnodes('//*[@tag="245"]/*[@code="a" or @code="b"]');
1500             title = title _ part.textContent;
1501         END;
1502
1503         author = bibxml.findnodes('//*[@tag="100"]/*[@code="a"]').textContent;
1504         item_type = bibxml.findnodes('//*[local-name()="attributes"]/*[local-name()="field"][@name="item_type"]').getAttribute('coded-value');
1505         publisher = bibxml.findnodes('//*[@tag="260"]/*[@code="b"]').textContent;
1506         pubdate = bibxml.findnodes('//*[@tag="260"]/*[@code="c"]').textContent;
1507         isbn = bibxml.findnodes('//*[@tag="020"]/*[@code="a"]').textContent;
1508         %]
1509
1510         <li>
1511             Bib ID# [% bre_id %] ISBN: [% isbn %]<br />
1512             Title: [% title %]<br />
1513             Author: [% author %]<br />
1514             Publication Info: [% publisher %] [% pubdate %]<br/>
1515             Item Type: [% item_type %]
1516         </li>
1517     [% END %]
1518     [% END %]
1519     </ol>
1520 </div>
1521 $$;
1522
1523   IF FOUND THEN -- update
1524
1525     INSERT INTO action_trigger.event_def_group_member (grp, name, event_def)
1526         SELECT 1, 'Brief', id FROM action_trigger.event_definition WHERE hook = 'biblio.format.record_entry.print';
1527
1528     INSERT INTO action_trigger.event_def_group_member (grp, name, holdings, event_def)
1529         SELECT 1, 'Full', TRUE, id FROM action_trigger.event_definition WHERE hook = 'biblio.format.record_entry.print';
1530
1531     UPDATE action_trigger.event_definition SET template = $$
1532 <div>
1533     <style> li { padding: 8px; margin 5px; }</style>
1534     <ol>
1535     [% FOR cbreb IN target;
1536
1537     flesh_list = '{mra';
1538     IF user_data.0.type == 'full';
1539         flesh_list = flesh_list _ ',holdings_xml,acp';
1540         IF params.holdings_limit;
1541             flimit = 'acn=>' _ params.holdings_limit _ ',acp=>' _ params.holdings_limit;
1542         END;
1543     END;
1544     flesh_list = flesh_list _ '}';
1545
1546     item_list = helpers.sort_bucket_unapi_bre(cbreb.items,{flesh => flesh_list, site => user_data.0.context_org, flesh_limit => flimit}, user_data.0.sort_by, user_data.0.sort_dir);
1547     FOR item IN item_list %]
1548         <li>
1549             Bib ID# [% item.id %]<br />
1550             [% IF item.isbn %]ISBN: [% item.isbn %]<br />[% END %]
1551             [% IF item.issn %]ISSN: [% item.issn %]<br />[% END %]
1552             [% IF item.upc  %]UPC:  [% item.upc %]<br />[% END %]
1553             Title: [% item.title %]<br />
1554 [% IF item.author %]            Author: [% item.author %]<br />[% END -%]
1555             Publication Info: [% item.publisher %] [% item.pubdate %]<br/>
1556             Item Type: [% item.item_type %]
1557             <ul>
1558             [% IF user_data.0.type == 'full' && item.holdings.size == 0 %]
1559                 <li>No items for this record at the selected location</li>
1560             [% END %]
1561             [% FOR cp IN item.holdings -%]
1562                 <li>
1563                     Library: [% cp.circ_lib %]<br/>
1564                     Location: [% cp.location %]<br/>
1565                     Call Number: [% cp.prefix _ ' ' _ cp.callnumber _ ' ' _ cp.suffix %]<br/>
1566                     [% IF cp.parts %]Parts: [% cp.parts %]<br/>[% END %]
1567                     Status: [% cp.status_label %]<br/>
1568                     Barcode: [% cp.barcode %]
1569                 </li>
1570             [% END %]
1571             </ul>
1572         </li>
1573     [% END %]
1574     [% END %]
1575     </ol>
1576 </div>
1577 $$ WHERE hook = 'biblio.format.record_entry.print' AND owner = 1 AND active;
1578
1579     ELSE -- insert full and add brief
1580
1581     INSERT INTO action_trigger.event_def_group_member (grp, name, event_def)
1582         SELECT 1, 'Brief', id FROM action_trigger.event_definition WHERE hook = 'biblio.format.record_entry.print' AND active;
1583
1584     INSERT INTO action_trigger.event_definition (
1585         active,
1586         owner,
1587         name,
1588         hook,
1589         validator,
1590         reactor,
1591         cleanup_success,
1592         cleanup_failure,
1593         group_field,
1594         granularity,
1595         delay,
1596         template
1597     ) SELECT
1598         TRUE,
1599         owner,
1600         'biblio.record_entry.print.full',
1601         'biblio.format.record_entry.print',
1602         'NOOP_True',
1603         'ProcessTemplate',
1604         'DeleteTempBiblioBucket',
1605         'DeleteTempBiblioBucket',
1606         'owner',
1607         'print-on-demand',
1608         '00:00:00',
1609         $$
1610 <div>
1611     <style> li { padding: 8px; margin 5px; }</style>
1612     <ol>
1613     [% FOR cbreb IN target;
1614
1615     flesh_list = '{mra';
1616     IF user_data.0.type == 'full';
1617         flesh_list = flesh_list _ ',holdings_xml,acp';
1618         IF params.holdings_limit;
1619             flimit = 'acn=>' _ params.holdings_limit _ ',acp=>' _ params.holdings_limit;
1620         END;
1621     END;
1622     flesh_list = flesh_list _ '}';
1623
1624     item_list = helpers.sort_bucket_unapi_bre(cbreb.items,{flesh => flesh_list, site => user_data.0.context_org, flesh_limit => flimit}, user_data.0.sort_by, user_data.0.sort_dir);
1625     FOR item IN item_list %]
1626         <li>
1627             Bib ID# [% item.id %]<br />
1628             [% IF item.isbn %]ISBN: [% item.isbn %]<br />[% END %]
1629             [% IF item.issn %]ISSN: [% item.issn %]<br />[% END %]
1630             [% IF item.upc  %]UPC:  [% item.upc %]<br />[% END %]
1631             Title: [% item.title %]<br />
1632 [% IF item.author %]            Author: [% item.author %]<br />[% END -%]
1633             Publication Info: [% item.publisher %] [% item.pubdate %]<br/>
1634             Item Type: [% item.item_type %]
1635             <ul>
1636             [% IF user_data.0.type == 'full' && item.holdings.size == 0 %]
1637                 <li>No items for this record at the selected location</li>
1638             [% END %]
1639             [% FOR cp IN item.holdings -%]
1640                 <li>
1641                     Library: [% cp.circ_lib %]<br/>
1642                     Location: [% cp.location %]<br/>
1643                     Call Number: [% cp.prefix _ ' ' _ cp.callnumber _ ' ' _ cp.suffix %]<br/>
1644                     [% IF cp.parts %]Parts: [% cp.parts %]<br/>[% END %]
1645                     Status: [% cp.status_label %]<br/>
1646                     Barcode: [% cp.barcode %]
1647                 </li>
1648             [% END %]
1649             </ul>
1650         </li>
1651     [% END %]
1652     [% END %]
1653     </ol>
1654 </div>
1655 $$ FROM action_trigger.event_definition WHERE hook = 'biblio.format.record_entry.print' AND active;
1656
1657     INSERT INTO action_trigger.event_def_group_member (grp, name, holdings, event_def)
1658         SELECT 1, 'Full', TRUE, id FROM action_trigger.event_definition WHERE name = 'biblio.record_entry.print.full' and active;
1659
1660   END IF;
1661 END
1662 $block$;
1663
1664
1665
1666 SELECT evergreen.upgrade_deps_block_check('1232', :eg_version);
1667
1668 CREATE TABLE asset.course_module_course (
1669     id              SERIAL PRIMARY KEY,
1670     name            TEXT NOT NULL,
1671     course_number   TEXT NOT NULL,
1672     section_number  TEXT,
1673     owning_lib      INT REFERENCES actor.org_unit (id),
1674     is_archived        BOOLEAN NOT NULL DEFAULT false
1675 );
1676
1677 CREATE TABLE asset.course_module_role (
1678     id              SERIAL  PRIMARY KEY,
1679     name            TEXT    UNIQUE NOT NULL,
1680     is_public       BOOLEAN NOT NULL DEFAULT false
1681 );
1682
1683 CREATE TABLE asset.course_module_course_users (
1684     id              SERIAL PRIMARY KEY,
1685     course          INT NOT NULL REFERENCES asset.course_module_course (id),
1686     usr             INT NOT NULL REFERENCES actor.usr (id),
1687     usr_role        INT REFERENCES asset.course_module_role (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED
1688 );
1689
1690 CREATE TABLE asset.course_module_course_materials (
1691     id              SERIAL PRIMARY KEY,
1692     course          INT NOT NULL REFERENCES asset.course_module_course (id),
1693     item            INT REFERENCES asset.copy (id),
1694     relationship    TEXT,
1695     record          INT REFERENCES biblio.record_entry (id),
1696     temporary_record       BOOLEAN,
1697     original_location      INT REFERENCES asset.copy_location,
1698     original_status        INT REFERENCES config.copy_status,
1699     original_circ_modifier TEXT, --REFERENCES config.circ_modifier
1700     original_callnumber    INT REFERENCES asset.call_number,
1701     unique (course, item, record)
1702 );
1703
1704 CREATE TABLE asset.course_module_term (
1705     id              SERIAL  PRIMARY KEY,
1706     name            TEXT    UNIQUE NOT NULL,
1707     owning_lib      INT REFERENCES actor.org_unit (id),
1708     start_date      TIMESTAMP WITH TIME ZONE,
1709     end_date        TIMESTAMP WITH TIME ZONE
1710 );
1711
1712 INSERT INTO asset.course_module_role (id, name, is_public) VALUES
1713 (1, oils_i18n_gettext(1, 'Instructor', 'acmr', 'name'), true),
1714 (2, oils_i18n_gettext(2, 'Teaching assistant', 'acmr', 'name'), true),
1715 (3, oils_i18n_gettext(2, 'Student', 'acmr', 'name'), false);
1716
1717 SELECT SETVAL('asset.course_module_role_id_seq'::TEXT, 100);
1718
1719 CREATE TABLE asset.course_module_term_course_map (
1720     id              BIGSERIAL  PRIMARY KEY,
1721     term            INT     NOT NULL REFERENCES asset.course_module_term (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
1722     course          INT     NOT NULL REFERENCES asset.course_module_course (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED
1723 );
1724
1725 INSERT INTO permission.perm_list(id, code, description)
1726     VALUES (
1727         624,
1728         'MANAGE_RESERVES',
1729         oils_i18n_gettext(
1730             624,
1731             'Allows user to manage Courses, Course Materials, and associate Users with Courses.',
1732             'ppl',
1733             'description'
1734         )
1735     );
1736
1737 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
1738     SELECT
1739         pgt.id, perm.id, aout.depth, TRUE
1740     FROM
1741         permission.grp_tree pgt,
1742         permission.perm_list perm,
1743         actor.org_unit_type aout
1744     WHERE
1745         pgt.name = 'Circulation Administrator' AND
1746         aout.name = 'Consortium' AND
1747         perm.code = 'MANAGE_RESERVES'
1748 ;
1749
1750 INSERT INTO config.org_unit_setting_type 
1751     (grp, name, datatype, label, description, fm_class)
1752 VALUES (
1753     'circ',
1754     'circ.course_materials_opt_in', 'bool',
1755     oils_i18n_gettext(
1756         'circ.course_materials_opt_in',
1757         'Opt Org Unit into the Course Materials Module',
1758         'coust',
1759         'label'
1760     ),
1761     oils_i18n_gettext(
1762         'circ.course_materials_opt_in',
1763         'If enabled, the Org Unit will utilize Course Material functionality.',
1764         'coust',
1765         'description'
1766     ), null
1767 ), (
1768     'circ',
1769     'circ.course_materials_browse_by_instructor', 'bool',
1770     oils_i18n_gettext(
1771         'circ.course_materials_browse_by_instructor',
1772         'Allow users to browse Courses by Instructor',
1773         'coust',
1774         'label'
1775     ),
1776     oils_i18n_gettext(
1777         'circ.course_materials_browse_by_instructor',
1778         'If enabled, the Org Unit will allow OPAC users to browse Courses by instructor name.',
1779         'coust',
1780         'description'
1781     ), null
1782 ), (
1783     'circ',
1784     'circ.course_materials_brief_record_bib_source', 'link',
1785     oils_i18n_gettext(
1786         'circ.course_materials_brief_record_bib_source',
1787         'Bib source for brief records created in the course materials module',
1788         'coust', 'label'
1789     ),
1790     oils_i18n_gettext(
1791         'circ.course_materials_brief_record_bib_source',
1792         'The course materials module will use this bib source for any new brief bibliographic records made inside that module. For best results, use a transcendant bib source.',
1793         'coust', 'description'
1794     ), 'cbs'
1795
1796 );
1797
1798 INSERT INTO config.bib_source (quality, source, transcendant) VALUES
1799     (1, oils_i18n_gettext(4, 'Course materials module', 'cbs', 'source'), TRUE);
1800
1801 INSERT INTO actor.org_unit_setting (org_unit, name, value)
1802     SELECT 1, 'circ.course_materials_brief_record_bib_source', id
1803     FROM config.bib_source
1804     WHERE source='Course materials module';
1805
1806
1807 SELECT evergreen.upgrade_deps_block_check('1233', :eg_version);
1808
1809 INSERT into config.workstation_setting_type (name, grp, datatype, label)
1810 VALUES (
1811     'eg.grid.hopeless.wide_holds', 'gui', 'object',
1812     oils_i18n_gettext(
1813         'eg.grid.hopeless.wide_holds',
1814         'Grid Config: hopeless.wide_holds',
1815         'cwst', 'label'
1816     )
1817 );
1818
1819
1820
1821 SELECT evergreen.upgrade_deps_block_check('1234', :eg_version);
1822
1823 ALTER TABLE config.copy_status ADD COLUMN hopeless_prone BOOL NOT NULL DEFAULT FALSE; -- 002.schema.config.sql
1824 ALTER TABLE action.hold_request ADD COLUMN hopeless_date TIMESTAMP WITH TIME ZONE; -- 090.schema.action.sql
1825
1826 INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1235', :eg_version);
1827
1828 CREATE TABLE action.curbside (
1829     id          SERIAL      PRIMARY KEY,
1830     patron      INT         NOT NULL REFERENCES actor.usr (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
1831     org         INT         NOT NULL REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
1832     slot        TIMESTAMPTZ,
1833     staged      TIMESTAMPTZ,
1834     stage_staff     INT     REFERENCES actor.usr (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
1835     arrival     TIMESTAMPTZ,
1836     delivered   TIMESTAMPTZ,
1837     delivery_staff  INT     REFERENCES actor.usr (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
1838     notes       TEXT
1839 );
1840
1841 INSERT INTO config.org_unit_setting_type (name, label, grp, description, datatype)
1842 VALUES (
1843     'circ.curbside',
1844     'Enable curbside pickup functionality at library.',
1845     'circ',
1846     'When set to TRUE, enable staff and public interfaces to schedule curbside pickup of holds that become available for pickup.',
1847     'bool'
1848 );
1849
1850 INSERT INTO config.org_unit_setting_type (name, label, grp, description, datatype)
1851 VALUES (
1852     'circ.curbside.granularity',
1853     'Time interval between curbside appointments',
1854     'circ',
1855     'Time interval between curbside appointments',
1856     'interval'
1857 );
1858
1859 INSERT INTO config.org_unit_setting_type (name, label, grp, description, datatype)
1860 VALUES (
1861     'circ.curbside.max_concurrent',
1862     'Maximum number of patrons that may select a particular curbside pickup time',
1863     'circ',
1864     'Maximum number of patrons that may select a particular curbside pickup time',
1865     'integer'
1866 );
1867
1868 INSERT INTO config.org_unit_setting_type (name, label, grp, description, datatype)
1869 VALUES (
1870     'circ.curbside.disable_patron_input',
1871     'Disable patron modification of curbside appointments in public catalog',
1872     'circ',
1873     'When set to TRUE, patrons cannot use the My Account interface to select curbside pickup times',
1874     'bool'
1875 );
1876
1877 INSERT INTO actor.org_unit_setting (org_unit, name, value)
1878     SELECT id, 'circ.curbside', 'false' FROM actor.org_unit WHERE parent_ou IS NULL
1879         UNION
1880     SELECT id, 'circ.curbside.max_concurrent', '10' FROM actor.org_unit WHERE parent_ou IS NULL
1881         UNION
1882     SELECT id, 'circ.curbside.granularity', '"15 minutes"' FROM actor.org_unit WHERE parent_ou IS NULL
1883 ;
1884
1885 INSERT INTO action_trigger.hook (key, core_type, description, passive)
1886 VALUES (
1887     'hold.offer_curbside',
1888     'ahr',
1889     oils_i18n_gettext(
1890         'hold.offer_curbside',
1891         'Hook used to trigger the notification of an offer of curbside pickup',
1892         'ath',
1893         'description'
1894     ),
1895     FALSE
1896 );
1897
1898 INSERT INTO action_trigger.hook (key, core_type, description, passive)
1899 VALUES (
1900     'hold.confirm_curbside',
1901     'acsp',
1902     oils_i18n_gettext(
1903         'hold.confirm_curbside',
1904         'Hook used to trigger the notification of the creation or update of a curbside pickup appointment with an arrival URL',
1905         'ath',
1906         'description'
1907     ),
1908     FALSE
1909 );
1910
1911 INSERT INTO action_trigger.reactor (module, description) VALUES (
1912     'CurbsideSlot', 'Create a curbside pickup appointment slot when necessary'
1913 );
1914
1915 INSERT INTO action_trigger.validator (module, description) VALUES (
1916     'Curbside', 'Confirm that curbside pickup is enabled for the hold pickup library'
1917 );
1918
1919 ------------------- Disabled example A/T defintions ------------------------------
1920
1921 -- Create a "dummy" slot when applicable, and trigger the "offer curbside" events
1922 INSERT INTO action_trigger.event_definition (
1923     active,
1924     owner,
1925     name,
1926     hook,
1927     validator,
1928     reactor,
1929     delay
1930 ) VALUES (
1931     'f',
1932     1,
1933     'Trigger curbside offer events and create a placeholder for the patron, where applicable',
1934     'hold.available',
1935     'Curbside',
1936     'CurbsideSlot',
1937     '00:30:00'
1938 );
1939
1940 -- Email offer
1941 INSERT INTO action_trigger.event_definition (
1942     active,
1943     owner,
1944     name,
1945     hook,
1946     validator,
1947     reactor,
1948     delay,
1949     delay_field,
1950     group_field,
1951     template
1952 ) VALUES (
1953     'f',
1954     1,
1955     'Curbside offer Email notification, triggered by CurbsideSlot reactor on a definition attached to the hold.available hook',
1956     'hold.offer_curbside',
1957     'Curbside',
1958     'SendEmail',
1959     '00:00:00',
1960     'shelf_time',
1961     'usr',
1962 $$
1963 [%- USE date -%]
1964 [%- user = target.0.usr -%]
1965 To: [%- params.recipient_email || user.email %]
1966 From: [%- params.sender_email || default_sender %]
1967 Date: [%- date.format(date.now, '%a, %d %b %Y %T -0000', gmt => 1) %]
1968 Subject: Curbside Pickup
1969 Auto-Submitted: auto-generated
1970
1971 [% target.0.pickup_lib.name %] is now offering curbside delivery
1972 service.  Please call [% target.0.pickup_lib.phone %] or visit the
1973 link below to schedule a pickup time.
1974
1975 https://example.org/eg/opac/myopac/holds_curbside
1976
1977 Stay safe! Wash your hands!
1978 $$);
1979
1980 INSERT INTO action_trigger.environment (
1981     event_def,
1982     path
1983 ) VALUES (
1984     currval('action_trigger.event_definition_id_seq'),
1985     'pickup_lib'
1986 ), (
1987     currval('action_trigger.event_definition_id_seq'),
1988     'usr'
1989 );
1990
1991 INSERT INTO action_trigger.event_params (event_def, param, value)
1992     VALUES (currval('action_trigger.event_definition_id_seq'), 'check_email_notify', 1);
1993
1994 -- SMS offer
1995 INSERT INTO action_trigger.event_definition (
1996     active,
1997     owner,
1998     name,
1999     hook,
2000     validator,
2001     reactor,
2002     delay,
2003     delay_field,
2004     group_field,
2005     template
2006 ) VALUES (
2007     false,
2008     1,
2009     'Curbside offer SMS notification, triggered by CurbsideSlot reactor on a definition attached to the hold.available hook',
2010     'hold.offer_curbside',
2011     'Curbside',
2012     'SendSMS',
2013     '00:00:00',
2014     'shelf_time',
2015     'sms_notify',
2016     $$[%- USE date -%]
2017 [%- user = target.0.usr -%]
2018 From: [%- params.sender_email || default_sender %]
2019 Date: [%- date.format(date.now, '%a, %d %b %Y %T -0000', gmt => 1) %]
2020 To: [%- params.recipient_email || helpers.get_sms_gateway_email(target.0.sms_carrier,target.0.sms_notify) %]
2021 Subject: Curbside Pickup
2022 Auto-Submitted: auto-generated
2023
2024 [% target.0.pickup_lib.name %] offers curbside pickup.
2025 Call [% target.0.pickup_lib.phone %] or visit https://example.org/eg/opac/myopac/holds_curbside
2026 $$
2027 );
2028
2029 INSERT INTO action_trigger.environment (
2030     event_def,
2031     path
2032 ) VALUES (
2033     currval('action_trigger.event_definition_id_seq'),
2034     'pickup_lib'
2035 ), (
2036     currval('action_trigger.event_definition_id_seq'),
2037     'usr'
2038 );
2039
2040 INSERT INTO action_trigger.event_params (event_def, param, value)
2041     VALUES (currval('action_trigger.event_definition_id_seq'), 'check_sms_notify', 1);
2042
2043 -- Email confirmation
2044 INSERT INTO action_trigger.event_definition (
2045     active,
2046     owner,
2047     name,
2048     hook,
2049     validator,
2050     reactor,
2051     delay,
2052     template
2053 ) VALUES (
2054     'f',
2055     1,
2056     'Curbside confirmation Email notification',
2057     'hold.confirm_curbside',
2058     'Curbside',
2059     'SendEmail',
2060     '00:00:00',
2061 $$
2062 [%- USE date -%]
2063 [%- user = target.patron -%]
2064 To: [%- params.recipient_email || user.email %]
2065 From: [%- params.sender_email || default_sender %]
2066 Date: [%- date.format(date.now, '%a, %d %b %Y %T -0000', gmt => 1) %]
2067 Subject: Curbside Pickup Confirmed
2068 Auto-Submitted: auto-generated
2069
2070 This email is to confirm that you have scheduled a curbside item
2071 pickup at [% target.org.name %] for [% date.format(helpers.format_date(target.slot), '%a, %d %b %Y %T') %].
2072
2073 You can cancel or change to your appointment, add vehicle description
2074 notes, and alert staff to your arrival by going to the link below.
2075
2076 When you arrive, please call [% target.org.phone %] or visit the
2077 link below to let us know you are here.
2078
2079 https://example.org/eg/opac/myopac/holds_curbside
2080
2081 Stay safe! Wash your hands!
2082 $$);
2083
2084 INSERT INTO action_trigger.environment (
2085     event_def,
2086     path
2087 ) VALUES (
2088     currval('action_trigger.event_definition_id_seq'),
2089     'org'
2090 ), (
2091     currval('action_trigger.event_definition_id_seq'),
2092     'patron'
2093 );
2094
2095 -- We do /not/ add this by default, treating curbside request as implicit opt-in
2096 /*
2097 INSERT INTO action_trigger.event_params (event_def, param, value)
2098     VALUES (currval('action_trigger.event_definition_id_seq'), 'check_email_notify', 1);
2099 */
2100
2101 -- SMS confirmation
2102 INSERT INTO action_trigger.event_definition (
2103     active,
2104     owner,
2105     name,
2106     hook,
2107     validator,
2108     reactor,
2109     delay,
2110     template
2111 ) VALUES (
2112     false,
2113     1,
2114     'Curbside confirmation SMS notification',
2115     'hold.confirm_curbside',
2116     'Curbside',
2117     'SendSMS',
2118     '00:00:00',
2119     $$[%- USE date -%]
2120 [%- user = target.patron -%]
2121 From: [%- params.sender_email || default_sender %]
2122 Date: [%- date.format(date.now, '%a, %d %b %Y %T -0000', gmt => 1) %]
2123 To: [%- params.recipient_email || helpers.get_sms_gateway_email(helpers.get_user_setting(user.id, 'opac.default_sms_carrier'), helpers.get_user_setting(user.id, 'opac.default_sms_notify')) %]
2124 Subject: Curbside Pickup Confirmed
2125 Auto-Submitted: auto-generated
2126
2127 Location: [% target.org.name %]
2128 Time: [% date.format(helpers.format_date(target.slot), '%a, %d %b %Y %T') %]
2129 Make changes at https://example.org/eg/opac/myopac/holds_curbside
2130 $$
2131 );
2132
2133 INSERT INTO action_trigger.environment (
2134     event_def,
2135     path
2136 ) VALUES (
2137     currval('action_trigger.event_definition_id_seq'),
2138     'org'
2139 ), (
2140     currval('action_trigger.event_definition_id_seq'),
2141     'patron'
2142 );
2143
2144 -- We do /not/ add this by default, treating curbside request as implicit opt-in
2145 /*
2146 INSERT INTO action_trigger.event_params (event_def, param, value)
2147     VALUES (currval('action_trigger.event_definition_id_seq'), 'check_sms_notify', 1);
2148 */
2149
2150
2151 COMMIT;
2152
2153 -- Update auditor tables to catch changes to source tables.
2154 --   Can be removed/skipped if there were no schema changes.
2155 SELECT auditor.update_auditors();