]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/sql/Pg/version-upgrade/2.1-2.2-upgrade-db.sql
Start collecting version-upgrade scripts in one spot
[working/Evergreen.git] / Open-ILS / src / sql / Pg / version-upgrade / 2.1-2.2-upgrade-db.sql
1 --Upgrade Script for 2.1 to 2.2-alpha1
2 BEGIN;
3 INSERT INTO config.upgrade_log (version, applied_to) VALUES ('2.2-alpha1');
4 -- DROP objects that might have existed from a prior run of 0526
5 -- Yes this is ironic.
6 DROP TABLE IF EXISTS config.db_patch_dependencies;
7 ALTER TABLE config.upgrade_log DROP COLUMN applied_to;
8 DROP FUNCTION evergreen.upgrade_list_applied_deprecates(TEXT);
9 DROP FUNCTION evergreen.upgrade_list_applied_supersedes(TEXT);
10
11
12 INSERT INTO config.upgrade_log (version) VALUES ('0526'); --miker
13
14 CREATE TABLE config.db_patch_dependencies (
15   db_patch      TEXT PRIMARY KEY,
16   supersedes    TEXT[],
17   deprecates    TEXT[]
18 );
19
20 CREATE OR REPLACE FUNCTION evergreen.array_overlap_check (/* field */) RETURNS TRIGGER AS $$
21 DECLARE
22     fld     TEXT;
23     cnt     INT;
24 BEGIN
25     fld := TG_ARGV[1];
26     EXECUTE 'SELECT COUNT(*) FROM '|| TG_TABLE_SCHEMA ||'.'|| TG_TABLE_NAME ||' WHERE '|| fld ||' && ($1).'|| fld INTO cnt USING NEW;
27     IF cnt > 0 THEN
28         RAISE EXCEPTION 'Cannot insert duplicate array into field % of table %', fld, TG_TABLE_SCHEMA ||'.'|| TG_TABLE_NAME;
29     END IF;
30     RETURN NEW;
31 END;
32 $$ LANGUAGE PLPGSQL;
33
34 CREATE TRIGGER no_overlapping_sups
35     BEFORE INSERT OR UPDATE ON config.db_patch_dependencies
36     FOR EACH ROW EXECUTE PROCEDURE evergreen.array_overlap_check ('supersedes');
37
38 CREATE TRIGGER no_overlapping_deps
39     BEFORE INSERT OR UPDATE ON config.db_patch_dependencies
40     FOR EACH ROW EXECUTE PROCEDURE evergreen.array_overlap_check ('deprecates');
41
42 ALTER TABLE config.upgrade_log
43     ADD COLUMN applied_to TEXT;
44
45 -- Provide a named type for patching functions
46 CREATE TYPE evergreen.patch AS (patch TEXT);
47
48 -- List applied db patches that are deprecated by (and block the application of) my_db_patch
49 CREATE OR REPLACE FUNCTION evergreen.upgrade_list_applied_deprecates ( my_db_patch TEXT ) RETURNS SETOF evergreen.patch AS $$
50     SELECT  DISTINCT l.version
51       FROM  config.upgrade_log l
52             JOIN config.db_patch_dependencies d ON (l.version::TEXT[] && d.deprecates)
53       WHERE d.db_patch = $1
54 $$ LANGUAGE SQL;
55
56 -- List applied db patches that are superseded by (and block the application of) my_db_patch
57 CREATE OR REPLACE FUNCTION evergreen.upgrade_list_applied_supersedes ( my_db_patch TEXT ) RETURNS SETOF evergreen.patch AS $$
58     SELECT  DISTINCT l.version
59       FROM  config.upgrade_log l
60             JOIN config.db_patch_dependencies d ON (l.version::TEXT[] && d.supersedes)
61       WHERE d.db_patch = $1
62 $$ LANGUAGE SQL;
63
64 -- List applied db patches that deprecates (and block the application of) my_db_patch
65 CREATE OR REPLACE FUNCTION evergreen.upgrade_list_applied_deprecated ( my_db_patch TEXT ) RETURNS TEXT AS $$
66     SELECT  db_patch
67       FROM  config.db_patch_dependencies
68       WHERE ARRAY[$1]::TEXT[] && deprecates
69 $$ LANGUAGE SQL;
70
71 -- List applied db patches that supersedes (and block the application of) my_db_patch
72 CREATE OR REPLACE FUNCTION evergreen.upgrade_list_applied_superseded ( my_db_patch TEXT ) RETURNS TEXT AS $$
73     SELECT  db_patch
74       FROM  config.db_patch_dependencies
75       WHERE ARRAY[$1]::TEXT[] && supersedes
76 $$ LANGUAGE SQL;
77
78 -- Make sure that no deprecated or superseded db patches are currently applied
79 CREATE OR REPLACE FUNCTION evergreen.upgrade_verify_no_dep_conflicts ( my_db_patch TEXT ) RETURNS BOOL AS $$
80     SELECT  COUNT(*) = 0
81       FROM  (SELECT * FROM evergreen.upgrade_list_applied_deprecates( $1 )
82                 UNION
83              SELECT * FROM evergreen.upgrade_list_applied_supersedes( $1 )
84                 UNION
85              SELECT * FROM evergreen.upgrade_list_applied_deprecated( $1 )
86                 UNION
87              SELECT * FROM evergreen.upgrade_list_applied_superseded( $1 ))x
88 $$ LANGUAGE SQL;
89
90 -- Raise an exception if there are, in fact, dep/sup confilct
91 CREATE OR REPLACE FUNCTION evergreen.upgrade_deps_block_check ( my_db_patch TEXT, my_applied_to TEXT ) RETURNS BOOL AS $$
92 DECLARE 
93     deprecates TEXT;
94     supersedes TEXT;
95 BEGIN
96     IF NOT evergreen.upgrade_verify_no_dep_conflicts( my_db_patch ) THEN
97         SELECT  STRING_AGG(patch, ', ') INTO deprecates FROM evergreen.upgrade_list_applied_deprecates(my_db_patch);
98         SELECT  STRING_AGG(patch, ', ') INTO supersedes FROM evergreen.upgrade_list_applied_supersedes(my_db_patch);
99         RAISE EXCEPTION '
100 Upgrade script % can not be applied:
101   applied deprecated scripts %
102   applied superseded scripts %
103   deprecated by %
104   superseded by %',
105             my_db_patch,
106             ARRAY_AGG(evergreen.upgrade_list_applied_deprecates(my_db_patch)),
107             ARRAY_AGG(evergreen.upgrade_list_applied_supersedes(my_db_patch)),
108             evergreen.upgrade_list_applied_deprecated(my_db_patch),
109             evergreen.upgrade_list_applied_superseded(my_db_patch);
110     END IF;
111
112     INSERT INTO config.upgrade_log (version, applied_to) VALUES (my_db_patch, my_applied_to);
113     RETURN TRUE;
114 END;
115 $$ LANGUAGE PLPGSQL;
116
117 -- Evergreen DB patch 0536.schema.lazy_circ-barcode_lookup.sql
118 --
119 -- FIXME: insert description of change, if needed
120 --
121
122 -- check whether patch can be applied
123 INSERT INTO config.upgrade_log (version) VALUES ('0536');
124
125 INSERT INTO config.org_unit_setting_type ( name, label, description, datatype) VALUES ( 'circ.staff_client.actor_on_checkout', 'Load patron from Checkout', 'When scanning barcodes into Checkout auto-detect if a new patron barcode is scanned and auto-load the new patron.', 'bool');
126
127 CREATE TABLE config.barcode_completion (
128     id          SERIAL  PRIMARY KEY,
129     active      BOOL    NOT NULL DEFAULT true,
130     org_unit    INT     NOT NULL REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
131     prefix      TEXT,
132     suffix      TEXT,
133     length      INT     NOT NULL DEFAULT 0,
134     padding     TEXT,
135     padding_end BOOL    NOT NULL DEFAULT false,
136     asset       BOOL    NOT NULL DEFAULT true,
137     actor       BOOL    NOT NULL DEFAULT true
138 );
139
140 CREATE TYPE evergreen.barcode_set AS (type TEXT, id BIGINT, barcode TEXT);
141
142 CREATE OR REPLACE FUNCTION evergreen.get_barcodes(select_ou INT, type TEXT, in_barcode TEXT) RETURNS SETOF evergreen.barcode_set AS $$
143 DECLARE
144     cur_barcode TEXT;
145     barcode_len INT;
146     completion_len  INT;
147     asset_barcodes  TEXT[];
148     actor_barcodes  TEXT[];
149     do_asset    BOOL = false;
150     do_serial   BOOL = false;
151     do_booking  BOOL = false;
152     do_actor    BOOL = false;
153     completion_set  config.barcode_completion%ROWTYPE;
154 BEGIN
155
156     IF position('asset' in type) > 0 THEN
157         do_asset = true;
158     END IF;
159     IF position('serial' in type) > 0 THEN
160         do_serial = true;
161     END IF;
162     IF position('booking' in type) > 0 THEN
163         do_booking = true;
164     END IF;
165     IF do_asset OR do_serial OR do_booking THEN
166         asset_barcodes = asset_barcodes || in_barcode;
167     END IF;
168     IF position('actor' in type) > 0 THEN
169         do_actor = true;
170         actor_barcodes = actor_barcodes || in_barcode;
171     END IF;
172
173     barcode_len := length(in_barcode);
174
175     FOR completion_set IN
176       SELECT * FROM config.barcode_completion
177         WHERE active
178         AND org_unit IN (SELECT aou.id FROM actor.org_unit_ancestors(select_ou) aou)
179         LOOP
180         IF completion_set.prefix IS NULL THEN
181             completion_set.prefix := '';
182         END IF;
183         IF completion_set.suffix IS NULL THEN
184             completion_set.suffix := '';
185         END IF;
186         IF completion_set.length = 0 OR completion_set.padding IS NULL OR length(completion_set.padding) = 0 THEN
187             cur_barcode = completion_set.prefix || in_barcode || completion_set.suffix;
188         ELSE
189             completion_len = completion_set.length - length(completion_set.prefix) - length(completion_set.suffix);
190             IF completion_len >= barcode_len THEN
191                 IF completion_set.padding_end THEN
192                     cur_barcode = rpad(in_barcode, completion_len, completion_set.padding);
193                 ELSE
194                     cur_barcode = lpad(in_barcode, completion_len, completion_set.padding);
195                 END IF;
196                 cur_barcode = completion_set.prefix || cur_barcode || completion_set.suffix;
197             END IF;
198         END IF;
199         IF completion_set.actor THEN
200             actor_barcodes = actor_barcodes || cur_barcode;
201         END IF;
202         IF completion_set.asset THEN
203             asset_barcodes = asset_barcodes || cur_barcode;
204         END IF;
205     END LOOP;
206
207     IF do_asset AND do_serial THEN
208         RETURN QUERY SELECT 'asset'::TEXT, id, barcode FROM ONLY asset.copy WHERE barcode = ANY(asset_barcodes) AND deleted = false;
209         RETURN QUERY SELECT 'serial'::TEXT, id, barcode FROM serial.unit WHERE barcode = ANY(asset_barcodes) AND deleted = false;
210     ELSIF do_asset THEN
211         RETURN QUERY SELECT 'asset'::TEXT, id, barcode FROM asset.copy WHERE barcode = ANY(asset_barcodes) AND deleted = false;
212     ELSIF do_serial THEN
213         RETURN QUERY SELECT 'serial'::TEXT, id, barcode FROM serial.unit WHERE barcode = ANY(asset_barcodes) AND deleted = false;
214     END IF;
215     IF do_booking THEN
216         RETURN QUERY SELECT 'booking'::TEXT, id::BIGINT, barcode FROM booking.resource WHERE barcode = ANY(asset_barcodes);
217     END IF;
218     IF do_actor THEN
219         RETURN QUERY SELECT 'actor'::TEXT, c.usr::BIGINT, c.barcode FROM actor.card c JOIN actor.usr u ON c.usr = u.id WHERE c.barcode = ANY(actor_barcodes) AND c.active AND NOT u.deleted ORDER BY usr;
220     END IF;
221     RETURN;
222 END;
223 $$ LANGUAGE plpgsql;
224
225 COMMENT ON FUNCTION evergreen.get_barcodes(INT, TEXT, TEXT) IS $$
226 Given user input, find an appropriate barcode in the proper class.
227
228 Will add prefix/suffix information to do so, and return all results.
229 $$;
230
231
232
233 INSERT INTO config.upgrade_log (version) VALUES ('0537'); --miker
234
235 DROP FUNCTION evergreen.upgrade_deps_block_check(text,text);
236 DROP FUNCTION evergreen.upgrade_verify_no_dep_conflicts(text);
237 DROP FUNCTION evergreen.upgrade_list_applied_deprecated(text);
238 DROP FUNCTION evergreen.upgrade_list_applied_superseded(text);
239
240 -- List applied db patches that deprecates (and block the application of) my_db_patch
241 CREATE FUNCTION evergreen.upgrade_list_applied_deprecated ( my_db_patch TEXT ) RETURNS SETOF TEXT AS $$
242     SELECT  db_patch
243       FROM  config.db_patch_dependencies
244       WHERE ARRAY[$1]::TEXT[] && deprecates
245 $$ LANGUAGE SQL;
246
247 -- List applied db patches that supersedes (and block the application of) my_db_patch
248 CREATE FUNCTION evergreen.upgrade_list_applied_superseded ( my_db_patch TEXT ) RETURNS SETOF TEXT AS $$
249     SELECT  db_patch
250       FROM  config.db_patch_dependencies
251       WHERE ARRAY[$1]::TEXT[] && supersedes
252 $$ LANGUAGE SQL;
253
254 -- Make sure that no deprecated or superseded db patches are currently applied
255 CREATE FUNCTION evergreen.upgrade_verify_no_dep_conflicts ( my_db_patch TEXT ) RETURNS BOOL AS $$
256     SELECT  COUNT(*) = 0
257       FROM  (SELECT * FROM evergreen.upgrade_list_applied_deprecates( $1 )
258                 UNION
259              SELECT * FROM evergreen.upgrade_list_applied_supersedes( $1 )
260                 UNION
261              SELECT * FROM evergreen.upgrade_list_applied_deprecated( $1 )
262                 UNION
263              SELECT * FROM evergreen.upgrade_list_applied_superseded( $1 ))x
264 $$ LANGUAGE SQL;
265
266 -- Raise an exception if there are, in fact, dep/sup confilct
267 CREATE FUNCTION evergreen.upgrade_deps_block_check ( my_db_patch TEXT, my_applied_to TEXT ) RETURNS BOOL AS $$
268 BEGIN
269     IF NOT evergreen.upgrade_verify_no_dep_conflicts( my_db_patch ) THEN
270         RAISE EXCEPTION '
271 Upgrade script % can not be applied:
272   applied deprecated scripts %
273   applied superseded scripts %
274   deprecated by %
275   superseded by %',
276             my_db_patch,
277             ARRAY_ACCUM(evergreen.upgrade_list_applied_deprecates(my_db_patch)),
278             ARRAY_ACCUM(evergreen.upgrade_list_applied_supersedes(my_db_patch)),
279             evergreen.upgrade_list_applied_deprecated(my_db_patch),
280             evergreen.upgrade_list_applied_superseded(my_db_patch);
281     END IF;
282
283     INSERT INTO config.upgrade_log (version, applied_to) VALUES (my_db_patch, my_applied_to);
284     RETURN TRUE;
285 END;
286 $$ LANGUAGE PLPGSQL;
287
288
289 INSERT INTO config.upgrade_log (version) VALUES ('0544');
290
291 INSERT INTO config.usr_setting_type 
292 ( name, opac_visible, label, description, datatype) VALUES 
293 ( 'circ.collections.exempt',
294   FALSE, 
295   oils_i18n_gettext('circ.collections.exempt', 'Collections: Exempt', 'cust', 'description'),
296   oils_i18n_gettext('circ.collections.exempt', 'User is exempt from collections tracking/processing', 'cust', 'description'),
297   'bool'
298 );
299
300
301
302 SELECT evergreen.upgrade_deps_block_check('0545', :eg_version);
303
304 INSERT INTO permission.perm_list VALUES
305  (507, 'ABORT_TRANSIT_ON_LOST', oils_i18n_gettext(507, 'Allows a user to abort a transit on a copy with status of LOST', 'ppl', 'description')),
306  (508, 'ABORT_TRANSIT_ON_MISSING', oils_i18n_gettext(508, 'Allows a user to abort a transit on a copy with status of MISSING', 'ppl', 'description'));
307
308 --- stock Circulation Administrator group
309
310 INSERT INTO permission.grp_perm_map ( grp, perm, depth, grantable )
311     SELECT
312         4,
313         id,
314         0,
315         't'
316     FROM permission.perm_list
317     WHERE code in ('ABORT_TRANSIT_ON_LOST', 'ABORT_TRANSIT_ON_MISSING');
318
319 -- Evergreen DB patch 0546.schema.sip_statcats.sql
320
321
322 -- check whether patch can be applied
323 SELECT evergreen.upgrade_deps_block_check('0546', :eg_version);
324
325 CREATE TABLE actor.stat_cat_sip_fields (
326     field   CHAR(2) PRIMARY KEY,
327     name    TEXT    NOT NULL,
328     one_only  BOOL    NOT NULL DEFAULT FALSE
329 );
330 COMMENT ON TABLE actor.stat_cat_sip_fields IS $$
331 Actor Statistical Category SIP Fields
332
333 Contains the list of valid SIP Field identifiers for
334 Statistical Categories.
335 $$;
336 ALTER TABLE actor.stat_cat
337     ADD COLUMN sip_field   CHAR(2) REFERENCES actor.stat_cat_sip_fields(field) ON UPDATE CASCADE ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
338     ADD COLUMN sip_format  TEXT;
339
340 CREATE FUNCTION actor.stat_cat_check() RETURNS trigger AS $func$
341 DECLARE
342     sipfield actor.stat_cat_sip_fields%ROWTYPE;
343     use_count INT;
344 BEGIN
345     IF NEW.sip_field IS NOT NULL THEN
346         SELECT INTO sipfield * FROM actor.stat_cat_sip_fields WHERE field = NEW.sip_field;
347         IF sipfield.one_only THEN
348             SELECT INTO use_count count(id) FROM actor.stat_cat WHERE sip_field = NEW.sip_field AND id != NEW.id;
349             IF use_count > 0 THEN
350                 RAISE EXCEPTION 'Sip field cannot be used twice';
351             END IF;
352         END IF;
353     END IF;
354     RETURN NEW;
355 END;
356 $func$ LANGUAGE PLPGSQL;
357
358 CREATE TRIGGER actor_stat_cat_sip_update_trigger
359     BEFORE INSERT OR UPDATE ON actor.stat_cat FOR EACH ROW
360     EXECUTE PROCEDURE actor.stat_cat_check();
361
362 CREATE TABLE asset.stat_cat_sip_fields (
363     field   CHAR(2) PRIMARY KEY,
364     name    TEXT    NOT NULL,
365     one_only BOOL    NOT NULL DEFAULT FALSE
366 );
367 COMMENT ON TABLE asset.stat_cat_sip_fields IS $$
368 Asset Statistical Category SIP Fields
369
370 Contains the list of valid SIP Field identifiers for
371 Statistical Categories.
372 $$;
373
374 ALTER TABLE asset.stat_cat
375     ADD COLUMN sip_field   CHAR(2) REFERENCES asset.stat_cat_sip_fields(field) ON UPDATE CASCADE ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
376     ADD COLUMN sip_format  TEXT;
377
378 CREATE FUNCTION asset.stat_cat_check() RETURNS trigger AS $func$
379 DECLARE
380     sipfield asset.stat_cat_sip_fields%ROWTYPE;
381     use_count INT;
382 BEGIN
383     IF NEW.sip_field IS NOT NULL THEN
384         SELECT INTO sipfield * FROM asset.stat_cat_sip_fields WHERE field = NEW.sip_field;
385         IF sipfield.one_only THEN
386             SELECT INTO use_count count(id) FROM asset.stat_cat WHERE sip_field = NEW.sip_field AND id != NEW.id;
387             IF use_count > 0 THEN
388                 RAISE EXCEPTION 'Sip field cannot be used twice';
389             END IF;
390         END IF;
391     END IF;
392     RETURN NEW;
393 END;
394 $func$ LANGUAGE PLPGSQL;
395
396 CREATE TRIGGER asset_stat_cat_sip_update_trigger
397     BEFORE INSERT OR UPDATE ON asset.stat_cat FOR EACH ROW
398     EXECUTE PROCEDURE asset.stat_cat_check();
399
400
401
402 SELECT evergreen.upgrade_deps_block_check('0548', :eg_version); -- dbwells
403
404 \qecho This redoes the original part 1 of 0547 which did not apply to rel_2_1,
405 \qecho and is being added for the sake of clarity
406
407 -- delete errant inserts from 0545 (group 4 is NOT the circulation admin group)
408 DELETE FROM permission.grp_perm_map WHERE grp = 4 AND perm IN (
409         SELECT id FROM permission.perm_list
410         WHERE code in ('ABORT_TRANSIT_ON_LOST', 'ABORT_TRANSIT_ON_MISSING')
411 );
412
413 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
414         SELECT
415                 pgt.id, perm.id, aout.depth, TRUE
416         FROM
417                 permission.grp_tree pgt,
418                 permission.perm_list perm,
419                 actor.org_unit_type aout
420         WHERE
421                 pgt.name = 'Circulation Administrator' AND
422                 aout.name = 'Consortium' AND
423                 perm.code IN (
424                         'ABORT_TRANSIT_ON_LOST',
425                         'ABORT_TRANSIT_ON_MISSING'
426                 ) AND NOT EXISTS (
427                         SELECT 1
428                         FROM permission.grp_perm_map AS map
429                         WHERE
430                                 map.grp = pgt.id
431                                 AND map.perm = perm.id
432                 );
433
434 -- Evergreen DB patch XXXX.data.transit-checkin-interval.sql
435 --
436 -- New org unit setting "circ.transit.min_checkin_interval"
437 -- New TRANSIT_CHECKIN_INTERVAL_BLOCK.override permission
438 --
439
440
441 -- check whether patch can be applied
442 SELECT evergreen.upgrade_deps_block_check('0549', :eg_version);
443
444 INSERT INTO config.org_unit_setting_type ( name, label, description, datatype ) VALUES (
445     'circ.transit.min_checkin_interval',
446     oils_i18n_gettext( 
447         'circ.transit.min_checkin_interval', 
448         'Circ:  Minimum Transit Checkin Interval',
449         'coust',
450         'label'
451     ),
452     oils_i18n_gettext( 
453         'circ.transit.min_checkin_interval', 
454         'In-Transit items checked in this close to the transit start time will be prevented from checking in',
455         'coust',
456         'label'
457     ),
458     'interval'
459 );
460
461 INSERT INTO permission.perm_list ( id, code, description ) VALUES (  
462     509, 
463     'TRANSIT_CHECKIN_INTERVAL_BLOCK.override', 
464     oils_i18n_gettext(
465         509,
466         'Allows a user to override the TRANSIT_CHECKIN_INTERVAL_BLOCK event', 
467         'ppl', 
468         'description'
469     )
470 );
471
472 -- add the perm to the default circ admin group
473 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
474         SELECT
475                 pgt.id, perm.id, aout.depth, TRUE
476         FROM
477                 permission.grp_tree pgt,
478                 permission.perm_list perm,
479                 actor.org_unit_type aout
480         WHERE
481                 pgt.name = 'Circulation Administrator' AND
482                 aout.name = 'System' AND
483                 perm.code IN ( 'TRANSIT_CHECKIN_INTERVAL_BLOCK.override' );
484
485
486 -- check whether patch can be applied
487 SELECT evergreen.upgrade_deps_block_check('0550', :eg_version);
488
489 INSERT INTO config.org_unit_setting_type ( name, label, description, datatype ) VALUES (
490     'org.patron_opt_boundary',
491     oils_i18n_gettext( 
492         'org.patron_opt_boundary',
493         'Circ: Patron Opt-In Boundary',
494         'coust',
495         'label'
496     ),
497     oils_i18n_gettext( 
498         'org.patron_opt_boundary',
499         'This determines at which depth above which patrons must be opted in, and below which patrons will be assumed to be opted in.',
500         'coust',
501         'label'
502     ),
503     'integer'
504 );
505
506 INSERT INTO config.org_unit_setting_type ( name, label, description, datatype ) VALUES (
507     'org.patron_opt_default',
508     oils_i18n_gettext( 
509         'org.patron_opt_default',
510         'Circ: Patron Opt-In Default',
511         'coust',
512         'label'
513     ),
514     oils_i18n_gettext( 
515         'org.patron_opt_default',
516         'This is the default depth at which a patron is opted in; it is calculated as an org unit relative to the current workstation.',
517         'coust',
518         'label'
519     ),
520     'integer'
521 );
522
523 -- Evergreen DB patch 0562.schema.copy_active_date.sql
524 --
525 -- Active Date
526
527
528 -- check whether patch can be applied
529 SELECT evergreen.upgrade_deps_block_check('0562', :eg_version);
530
531 ALTER TABLE asset.copy
532     ADD COLUMN active_date TIMESTAMP WITH TIME ZONE;
533
534 ALTER TABLE auditor.asset_copy_history
535     ADD COLUMN active_date TIMESTAMP WITH TIME ZONE;
536
537 ALTER TABLE auditor.serial_unit_history
538     ADD COLUMN active_date TIMESTAMP WITH TIME ZONE;
539
540 ALTER TABLE config.copy_status
541     ADD COLUMN copy_active BOOL NOT NULL DEFAULT FALSE;
542
543 ALTER TABLE config.circ_matrix_weights
544     ADD COLUMN item_age NUMERIC(6,2) NOT NULL DEFAULT 0.0;
545
546 ALTER TABLE config.hold_matrix_weights
547     ADD COLUMN item_age NUMERIC(6,2) NOT NULL DEFAULT 0.0;
548
549 -- The two defaults above were to stop erroring on NOT NULL
550 -- Remove them here
551 ALTER TABLE config.circ_matrix_weights
552     ALTER COLUMN item_age DROP DEFAULT;
553
554 ALTER TABLE config.hold_matrix_weights
555     ALTER COLUMN item_age DROP DEFAULT;
556
557 ALTER TABLE config.circ_matrix_matchpoint
558     ADD COLUMN item_age INTERVAL;
559
560 ALTER TABLE config.hold_matrix_matchpoint
561     ADD COLUMN item_age INTERVAL;
562
563 CREATE OR REPLACE FUNCTION asset.acp_status_changed()
564 RETURNS TRIGGER AS $$
565 BEGIN
566     IF NEW.status <> OLD.status THEN
567         NEW.status_changed_time := now();
568         IF NEW.active_date IS NULL AND NEW.status IN (SELECT id FROM config.copy_status WHERE copy_active = true) THEN
569             NEW.active_date := now();
570         END IF;
571     END IF;
572     RETURN NEW;
573 END;
574 $$ LANGUAGE plpgsql;
575
576 CREATE OR REPLACE FUNCTION asset.acp_created()
577 RETURNS TRIGGER AS $$
578 BEGIN
579     IF NEW.active_date IS NULL AND NEW.status IN (SELECT id FROM config.copy_status WHERE copy_active = true) THEN
580         NEW.active_date := now();
581     END IF;
582     IF NEW.status_changed_time IS NULL THEN
583         NEW.status_changed_time := now();
584     END IF;
585     RETURN NEW;
586 END;
587 $$ LANGUAGE plpgsql;
588
589 CREATE TRIGGER acp_created_trig
590     BEFORE INSERT ON asset.copy
591     FOR EACH ROW EXECUTE PROCEDURE asset.acp_created();
592
593 CREATE TRIGGER sunit_created_trig
594     BEFORE INSERT ON serial.unit
595     FOR EACH ROW EXECUTE PROCEDURE asset.acp_created();
596
597 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$
598 DECLARE
599     matchpoint_id        INT;
600     user_object        actor.usr%ROWTYPE;
601     age_protect_object    config.rule_age_hold_protect%ROWTYPE;
602     standing_penalty    config.standing_penalty%ROWTYPE;
603     transit_range_ou_type    actor.org_unit_type%ROWTYPE;
604     transit_source        actor.org_unit%ROWTYPE;
605     item_object        asset.copy%ROWTYPE;
606     item_cn_object     asset.call_number%ROWTYPE;
607     ou_skip              actor.org_unit_setting%ROWTYPE;
608     result            action.matrix_test_result;
609     hold_test        config.hold_matrix_matchpoint%ROWTYPE;
610     use_active_date   TEXT;
611     age_protect_date  TIMESTAMP WITH TIME ZONE;
612     hold_count        INT;
613     hold_transit_prox    INT;
614     frozen_hold_count    INT;
615     context_org_list    INT[];
616     done            BOOL := FALSE;
617 BEGIN
618     SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
619     SELECT INTO context_org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( pickup_ou );
620
621     result.success := TRUE;
622
623     -- Fail if we couldn't find a user
624     IF user_object.id IS NULL THEN
625         result.fail_part := 'no_user';
626         result.success := FALSE;
627         done := TRUE;
628         RETURN NEXT result;
629         RETURN;
630     END IF;
631
632     SELECT INTO item_object * FROM asset.copy WHERE id = match_item;
633
634     -- Fail if we couldn't find a copy
635     IF item_object.id IS NULL THEN
636         result.fail_part := 'no_item';
637         result.success := FALSE;
638         done := TRUE;
639         RETURN NEXT result;
640         RETURN;
641     END IF;
642
643     SELECT INTO matchpoint_id action.find_hold_matrix_matchpoint(pickup_ou, request_ou, match_item, match_user, match_requestor);
644     result.matchpoint := matchpoint_id;
645
646     SELECT INTO ou_skip * FROM actor.org_unit_setting WHERE name = 'circ.holds.target_skip_me' AND org_unit = item_object.circ_lib;
647
648     -- Fail if the circ_lib for the item has circ.holds.target_skip_me set to true
649     IF ou_skip.id IS NOT NULL AND ou_skip.value = 'true' THEN
650         result.fail_part := 'circ.holds.target_skip_me';
651         result.success := FALSE;
652         done := TRUE;
653         RETURN NEXT result;
654         RETURN;
655     END IF;
656
657     -- Fail if user is barred
658     IF user_object.barred IS TRUE THEN
659         result.fail_part := 'actor.usr.barred';
660         result.success := FALSE;
661         done := TRUE;
662         RETURN NEXT result;
663         RETURN;
664     END IF;
665
666     -- Fail if we couldn't find any matchpoint (requires a default)
667     IF matchpoint_id IS NULL THEN
668         result.fail_part := 'no_matchpoint';
669         result.success := FALSE;
670         done := TRUE;
671         RETURN NEXT result;
672         RETURN;
673     END IF;
674
675     SELECT INTO hold_test * FROM config.hold_matrix_matchpoint WHERE id = matchpoint_id;
676
677     IF hold_test.holdable IS FALSE THEN
678         result.fail_part := 'config.hold_matrix_test.holdable';
679         result.success := FALSE;
680         done := TRUE;
681         RETURN NEXT result;
682     END IF;
683
684     IF hold_test.transit_range IS NOT NULL THEN
685         SELECT INTO transit_range_ou_type * FROM actor.org_unit_type WHERE id = hold_test.transit_range;
686         IF hold_test.distance_is_from_owner THEN
687             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;
688         ELSE
689             SELECT INTO transit_source * FROM actor.org_unit WHERE id = item_object.circ_lib;
690         END IF;
691
692         PERFORM * FROM actor.org_unit_descendants( transit_source.id, transit_range_ou_type.depth ) WHERE id = pickup_ou;
693
694         IF NOT FOUND THEN
695             result.fail_part := 'transit_range';
696             result.success := FALSE;
697             done := TRUE;
698             RETURN NEXT result;
699         END IF;
700     END IF;
701  
702     FOR standing_penalty IN
703         SELECT  DISTINCT csp.*
704           FROM  actor.usr_standing_penalty usp
705                 JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
706           WHERE usr = match_user
707                 AND usp.org_unit IN ( SELECT * FROM explode_array(context_org_list) )
708                 AND (usp.stop_date IS NULL or usp.stop_date > NOW())
709                 AND csp.block_list LIKE '%HOLD%' LOOP
710
711         result.fail_part := standing_penalty.name;
712         result.success := FALSE;
713         done := TRUE;
714         RETURN NEXT result;
715     END LOOP;
716
717     IF hold_test.stop_blocked_user IS TRUE THEN
718         FOR standing_penalty IN
719             SELECT  DISTINCT csp.*
720               FROM  actor.usr_standing_penalty usp
721                     JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
722               WHERE usr = match_user
723                     AND usp.org_unit IN ( SELECT * FROM explode_array(context_org_list) )
724                     AND (usp.stop_date IS NULL or usp.stop_date > NOW())
725                     AND csp.block_list LIKE '%CIRC%' LOOP
726     
727             result.fail_part := standing_penalty.name;
728             result.success := FALSE;
729             done := TRUE;
730             RETURN NEXT result;
731         END LOOP;
732     END IF;
733
734     IF hold_test.max_holds IS NOT NULL AND NOT retargetting THEN
735         SELECT    INTO hold_count COUNT(*)
736           FROM    action.hold_request
737           WHERE    usr = match_user
738             AND fulfillment_time IS NULL
739             AND cancel_time IS NULL
740             AND CASE WHEN hold_test.include_frozen_holds THEN TRUE ELSE frozen IS FALSE END;
741
742         IF hold_count >= hold_test.max_holds THEN
743             result.fail_part := 'config.hold_matrix_test.max_holds';
744             result.success := FALSE;
745             done := TRUE;
746             RETURN NEXT result;
747         END IF;
748     END IF;
749
750     IF item_object.age_protect IS NOT NULL THEN
751         SELECT INTO age_protect_object * FROM config.rule_age_hold_protect WHERE id = item_object.age_protect;
752         IF hold_test.distance_is_from_owner THEN
753             SELECT INTO use_active_date value FROM actor.org_unit_ancestor_setting('circ.holds.age_protect.active_date', item_cn_object.owning_lib);
754         ELSE
755             SELECT INTO use_active_date value FROM actor.org_unit_ancestor_setting('circ.holds.age_protect.active_date', item_object.circ_lib);
756         END IF;
757         IF use_active_date = 'true' THEN
758             age_protect_date := COALESCE(item_object.active_date, NOW());
759         ELSE
760             age_protect_date := item_object.create_date;
761         END IF;
762         IF age_protect_date + age_protect_object.age > NOW() THEN
763             IF hold_test.distance_is_from_owner THEN
764                 SELECT INTO item_cn_object * FROM asset.call_number WHERE id = item_object.call_number;
765                 SELECT INTO hold_transit_prox prox FROM actor.org_unit_proximity WHERE from_org = item_cn_object.owning_lib AND to_org = pickup_ou;
766             ELSE
767                 SELECT INTO hold_transit_prox prox FROM actor.org_unit_proximity WHERE from_org = item_object.circ_lib AND to_org = pickup_ou;
768             END IF;
769
770             IF hold_transit_prox > age_protect_object.prox THEN
771                 result.fail_part := 'config.rule_age_hold_protect.prox';
772                 result.success := FALSE;
773                 done := TRUE;
774                 RETURN NEXT result;
775             END IF;
776         END IF;
777     END IF;
778
779     IF NOT done THEN
780         RETURN NEXT result;
781     END IF;
782
783     RETURN;
784 END;
785 $func$ LANGUAGE plpgsql;
786
787 CREATE OR REPLACE FUNCTION action.find_circ_matrix_matchpoint( context_ou INT, item_object asset.copy, user_object actor.usr, renewal BOOL ) RETURNS action.found_circ_matrix_matchpoint AS $func$
788 DECLARE
789     cn_object       asset.call_number%ROWTYPE;
790     rec_descriptor  metabib.rec_descriptor%ROWTYPE;
791     cur_matchpoint  config.circ_matrix_matchpoint%ROWTYPE;
792     matchpoint      config.circ_matrix_matchpoint%ROWTYPE;
793     weights         config.circ_matrix_weights%ROWTYPE;
794     user_age        INTERVAL;
795     my_item_age     INTERVAL;
796     denominator     NUMERIC(6,2);
797     row_list        INT[];
798     result          action.found_circ_matrix_matchpoint;
799 BEGIN
800     -- Assume failure
801     result.success = false;
802
803     -- Fetch useful data
804     SELECT INTO cn_object       * FROM asset.call_number        WHERE id = item_object.call_number;
805     SELECT INTO rec_descriptor  * FROM metabib.rec_descriptor   WHERE record = cn_object.record;
806
807     -- Pre-generate this so we only calc it once
808     IF user_object.dob IS NOT NULL THEN
809         SELECT INTO user_age age(user_object.dob);
810     END IF;
811
812     -- Ditto
813     SELECT INTO my_item_age age(coalesce(item_object.active_date, now()));
814
815     -- Grab the closest set circ weight setting.
816     SELECT INTO weights cw.*
817       FROM config.weight_assoc wa
818            JOIN config.circ_matrix_weights cw ON (cw.id = wa.circ_weights)
819            JOIN actor.org_unit_ancestors_distance( context_ou ) d ON (wa.org_unit = d.id)
820       WHERE active
821       ORDER BY d.distance
822       LIMIT 1;
823
824     -- No weights? Bad admin! Defaults to handle that anyway.
825     IF weights.id IS NULL THEN
826         weights.grp                 := 11.0;
827         weights.org_unit            := 10.0;
828         weights.circ_modifier       := 5.0;
829         weights.marc_type           := 4.0;
830         weights.marc_form           := 3.0;
831         weights.marc_bib_level      := 2.0;
832         weights.marc_vr_format      := 2.0;
833         weights.copy_circ_lib       := 8.0;
834         weights.copy_owning_lib     := 8.0;
835         weights.user_home_ou        := 8.0;
836         weights.ref_flag            := 1.0;
837         weights.juvenile_flag       := 6.0;
838         weights.is_renewal          := 7.0;
839         weights.usr_age_lower_bound := 0.0;
840         weights.usr_age_upper_bound := 0.0;
841         weights.item_age            := 0.0;
842     END IF;
843
844     -- Determine the max (expected) depth (+1) of the org tree and max depth of the permisson tree
845     -- If you break your org tree with funky parenting this may be wrong
846     -- Note: This CTE is duplicated in the find_hold_matrix_matchpoint function, and it may be a good idea to split it off to a function
847     -- We use one denominator for all tree-based checks for when permission groups and org units have the same weighting
848     WITH all_distance(distance) AS (
849             SELECT depth AS distance FROM actor.org_unit_type
850         UNION
851             SELECT distance AS distance FROM permission.grp_ancestors_distance((SELECT id FROM permission.grp_tree WHERE parent IS NULL))
852         )
853     SELECT INTO denominator MAX(distance) + 1 FROM all_distance;
854
855     -- Loop over all the potential matchpoints
856     FOR cur_matchpoint IN
857         SELECT m.*
858           FROM  config.circ_matrix_matchpoint m
859                 /*LEFT*/ JOIN permission.grp_ancestors_distance( user_object.profile ) upgad ON m.grp = upgad.id
860                 /*LEFT*/ JOIN actor.org_unit_ancestors_distance( context_ou ) ctoua ON m.org_unit = ctoua.id
861                 LEFT JOIN actor.org_unit_ancestors_distance( cn_object.owning_lib ) cnoua ON m.copy_owning_lib = cnoua.id
862                 LEFT JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) iooua ON m.copy_circ_lib = iooua.id
863                 LEFT JOIN actor.org_unit_ancestors_distance( user_object.home_ou  ) uhoua ON m.user_home_ou = uhoua.id
864           WHERE m.active
865                 -- Permission Groups
866              -- AND (m.grp                      IS NULL OR upgad.id IS NOT NULL) -- Optional Permission Group?
867                 -- Org Units
868              -- AND (m.org_unit                 IS NULL OR ctoua.id IS NOT NULL) -- Optional Org Unit?
869                 AND (m.copy_owning_lib          IS NULL OR cnoua.id IS NOT NULL)
870                 AND (m.copy_circ_lib            IS NULL OR iooua.id IS NOT NULL)
871                 AND (m.user_home_ou             IS NULL OR uhoua.id IS NOT NULL)
872                 -- Circ Type
873                 AND (m.is_renewal               IS NULL OR m.is_renewal = renewal)
874                 -- Static User Checks
875                 AND (m.juvenile_flag            IS NULL OR m.juvenile_flag = user_object.juvenile)
876                 AND (m.usr_age_lower_bound      IS NULL OR (user_age IS NOT NULL AND m.usr_age_lower_bound < user_age))
877                 AND (m.usr_age_upper_bound      IS NULL OR (user_age IS NOT NULL AND m.usr_age_upper_bound > user_age))
878                 -- Static Item Checks
879                 AND (m.circ_modifier            IS NULL OR m.circ_modifier = item_object.circ_modifier)
880                 AND (m.marc_type                IS NULL OR m.marc_type = COALESCE(item_object.circ_as_type, rec_descriptor.item_type))
881                 AND (m.marc_form                IS NULL OR m.marc_form = rec_descriptor.item_form)
882                 AND (m.marc_bib_level           IS NULL OR m.marc_bib_level = rec_descriptor.bib_level)
883                 AND (m.marc_vr_format           IS NULL OR m.marc_vr_format = rec_descriptor.vr_format)
884                 AND (m.ref_flag                 IS NULL OR m.ref_flag = item_object.ref)
885                 AND (m.item_age                 IS NULL OR (my_item_age IS NOT NULL AND m.item_age > my_item_age))
886           ORDER BY
887                 -- Permission Groups
888                 CASE WHEN upgad.distance        IS NOT NULL THEN 2^(2*weights.grp - (upgad.distance/denominator)) ELSE 0.0 END +
889                 -- Org Units
890                 CASE WHEN ctoua.distance        IS NOT NULL THEN 2^(2*weights.org_unit - (ctoua.distance/denominator)) ELSE 0.0 END +
891                 CASE WHEN cnoua.distance        IS NOT NULL THEN 2^(2*weights.copy_owning_lib - (cnoua.distance/denominator)) ELSE 0.0 END +
892                 CASE WHEN iooua.distance        IS NOT NULL THEN 2^(2*weights.copy_circ_lib - (iooua.distance/denominator)) ELSE 0.0 END +
893                 CASE WHEN uhoua.distance        IS NOT NULL THEN 2^(2*weights.user_home_ou - (uhoua.distance/denominator)) ELSE 0.0 END +
894                 -- Circ Type                    -- Note: 4^x is equiv to 2^(2*x)
895                 CASE WHEN m.is_renewal          IS NOT NULL THEN 4^weights.is_renewal ELSE 0.0 END +
896                 -- Static User Checks
897                 CASE WHEN m.juvenile_flag       IS NOT NULL THEN 4^weights.juvenile_flag ELSE 0.0 END +
898                 CASE WHEN m.usr_age_lower_bound IS NOT NULL THEN 4^weights.usr_age_lower_bound ELSE 0.0 END +
899                 CASE WHEN m.usr_age_upper_bound IS NOT NULL THEN 4^weights.usr_age_upper_bound ELSE 0.0 END +
900                 -- Static Item Checks
901                 CASE WHEN m.circ_modifier       IS NOT NULL THEN 4^weights.circ_modifier ELSE 0.0 END +
902                 CASE WHEN m.marc_type           IS NOT NULL THEN 4^weights.marc_type ELSE 0.0 END +
903                 CASE WHEN m.marc_form           IS NOT NULL THEN 4^weights.marc_form ELSE 0.0 END +
904                 CASE WHEN m.marc_vr_format      IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0.0 END +
905                 CASE WHEN m.ref_flag            IS NOT NULL THEN 4^weights.ref_flag ELSE 0.0 END +
906                 -- Item age has a slight adjustment to weight based on value.
907                 -- This should ensure that a shorter age limit comes first when all else is equal.
908                 -- NOTE: This assumes that intervals will normally be in days.
909                 CASE WHEN m.item_age            IS NOT NULL THEN 4^weights.item_age - 1 + 86400/EXTRACT(EPOCH FROM m.item_age) ELSE 0.0 END DESC,
910                 -- Final sort on id, so that if two rules have the same sorting in the previous sort they have a defined order
911                 -- This prevents "we changed the table order by updating a rule, and we started getting different results"
912                 m.id LOOP
913
914         -- Record the full matching row list
915         row_list := row_list || cur_matchpoint.id;
916
917         -- No matchpoint yet?
918         IF matchpoint.id IS NULL THEN
919             -- Take the entire matchpoint as a starting point
920             matchpoint := cur_matchpoint;
921             CONTINUE; -- No need to look at this row any more.
922         END IF;
923
924         -- Incomplete matchpoint?
925         IF matchpoint.circulate IS NULL THEN
926             matchpoint.circulate := cur_matchpoint.circulate;
927         END IF;
928         IF matchpoint.duration_rule IS NULL THEN
929             matchpoint.duration_rule := cur_matchpoint.duration_rule;
930         END IF;
931         IF matchpoint.recurring_fine_rule IS NULL THEN
932             matchpoint.recurring_fine_rule := cur_matchpoint.recurring_fine_rule;
933         END IF;
934         IF matchpoint.max_fine_rule IS NULL THEN
935             matchpoint.max_fine_rule := cur_matchpoint.max_fine_rule;
936         END IF;
937         IF matchpoint.hard_due_date IS NULL THEN
938             matchpoint.hard_due_date := cur_matchpoint.hard_due_date;
939         END IF;
940         IF matchpoint.total_copy_hold_ratio IS NULL THEN
941             matchpoint.total_copy_hold_ratio := cur_matchpoint.total_copy_hold_ratio;
942         END IF;
943         IF matchpoint.available_copy_hold_ratio IS NULL THEN
944             matchpoint.available_copy_hold_ratio := cur_matchpoint.available_copy_hold_ratio;
945         END IF;
946         IF matchpoint.renewals IS NULL THEN
947             matchpoint.renewals := cur_matchpoint.renewals;
948         END IF;
949         IF matchpoint.grace_period IS NULL THEN
950             matchpoint.grace_period := cur_matchpoint.grace_period;
951         END IF;
952     END LOOP;
953
954     -- Check required fields
955     IF matchpoint.circulate             IS NOT NULL AND
956        matchpoint.duration_rule         IS NOT NULL AND
957        matchpoint.recurring_fine_rule   IS NOT NULL AND
958        matchpoint.max_fine_rule         IS NOT NULL THEN
959         -- All there? We have a completed match.
960         result.success := true;
961     END IF;
962
963     -- Include the assembled matchpoint, even if it isn't complete
964     result.matchpoint := matchpoint;
965
966     -- Include (for debugging) the full list of matching rows
967     result.buildrows := row_list;
968
969     -- Hand the result back to caller
970     RETURN result;
971 END;
972 $func$ LANGUAGE plpgsql;
973
974 CREATE OR REPLACE FUNCTION action.find_hold_matrix_matchpoint(pickup_ou integer, request_ou integer, match_item bigint, match_user integer, match_requestor integer)
975   RETURNS integer AS
976 $func$
977 DECLARE
978     requestor_object    actor.usr%ROWTYPE;
979     user_object         actor.usr%ROWTYPE;
980     item_object         asset.copy%ROWTYPE;
981     item_cn_object      asset.call_number%ROWTYPE;
982     my_item_age         INTERVAL;
983     rec_descriptor      metabib.rec_descriptor%ROWTYPE;
984     matchpoint          config.hold_matrix_matchpoint%ROWTYPE;
985     weights             config.hold_matrix_weights%ROWTYPE;
986     denominator         NUMERIC(6,2);
987 BEGIN
988     SELECT INTO user_object         * FROM actor.usr                WHERE id = match_user;
989     SELECT INTO requestor_object    * FROM actor.usr                WHERE id = match_requestor;
990     SELECT INTO item_object         * FROM asset.copy               WHERE id = match_item;
991     SELECT INTO item_cn_object      * FROM asset.call_number        WHERE id = item_object.call_number;
992     SELECT INTO rec_descriptor      * FROM metabib.rec_descriptor   WHERE record = item_cn_object.record;
993
994     SELECT INTO my_item_age age(coalesce(item_object.active_date, now()));
995
996     -- The item's owner should probably be the one determining if the item is holdable
997     -- How to decide that is debatable. Decided to default to the circ library (where the item lives)
998     -- This flag will allow for setting it to the owning library (where the call number "lives")
999     PERFORM * FROM config.internal_flag WHERE name = 'circ.holds.weight_owner_not_circ' AND enabled;
1000
1001     -- Grab the closest set circ weight setting.
1002     IF NOT FOUND THEN
1003         -- Default to circ library
1004         SELECT INTO weights hw.*
1005           FROM config.weight_assoc wa
1006                JOIN config.hold_matrix_weights hw ON (hw.id = wa.hold_weights)
1007                JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) d ON (wa.org_unit = d.id)
1008           WHERE active
1009           ORDER BY d.distance
1010           LIMIT 1;
1011     ELSE
1012         -- Flag is set, use owning library
1013         SELECT INTO weights hw.*
1014           FROM config.weight_assoc wa
1015                JOIN config.hold_matrix_weights hw ON (hw.id = wa.hold_weights)
1016                JOIN actor.org_unit_ancestors_distance( item_cn_object.owning_lib ) d ON (wa.org_unit = d.id)
1017           WHERE active
1018           ORDER BY d.distance
1019           LIMIT 1;
1020     END IF;
1021
1022     -- No weights? Bad admin! Defaults to handle that anyway.
1023     IF weights.id IS NULL THEN
1024         weights.user_home_ou    := 5.0;
1025         weights.request_ou      := 5.0;
1026         weights.pickup_ou       := 5.0;
1027         weights.item_owning_ou  := 5.0;
1028         weights.item_circ_ou    := 5.0;
1029         weights.usr_grp         := 7.0;
1030         weights.requestor_grp   := 8.0;
1031         weights.circ_modifier   := 4.0;
1032         weights.marc_type       := 3.0;
1033         weights.marc_form       := 2.0;
1034         weights.marc_bib_level  := 1.0;
1035         weights.marc_vr_format  := 1.0;
1036         weights.juvenile_flag   := 4.0;
1037         weights.ref_flag        := 0.0;
1038         weights.item_age        := 0.0;
1039     END IF;
1040
1041     -- Determine the max (expected) depth (+1) of the org tree and max depth of the permisson tree
1042     -- If you break your org tree with funky parenting this may be wrong
1043     -- Note: This CTE is duplicated in the find_circ_matrix_matchpoint function, and it may be a good idea to split it off to a function
1044     -- We use one denominator for all tree-based checks for when permission groups and org units have the same weighting
1045     WITH all_distance(distance) AS (
1046             SELECT depth AS distance FROM actor.org_unit_type
1047         UNION
1048             SELECT distance AS distance FROM permission.grp_ancestors_distance((SELECT id FROM permission.grp_tree WHERE parent IS NULL))
1049         )
1050     SELECT INTO denominator MAX(distance) + 1 FROM all_distance;
1051
1052     -- To ATTEMPT to make this work like it used to, make it reverse the user/requestor profile ids.
1053     -- This may be better implemented as part of the upgrade script?
1054     -- Set usr_grp = requestor_grp, requestor_grp = 1 or something when this flag is already set
1055     -- Then remove this flag, of course.
1056     PERFORM * FROM config.internal_flag WHERE name = 'circ.holds.usr_not_requestor' AND enabled;
1057
1058     IF FOUND THEN
1059         -- Note: This, to me, is REALLY hacky. I put it in anyway.
1060         -- If you can't tell, this is a single call swap on two variables.
1061         SELECT INTO user_object.profile, requestor_object.profile
1062                     requestor_object.profile, user_object.profile;
1063     END IF;
1064
1065     -- Select the winning matchpoint into the matchpoint variable for returning
1066     SELECT INTO matchpoint m.*
1067       FROM  config.hold_matrix_matchpoint m
1068             /*LEFT*/ JOIN permission.grp_ancestors_distance( requestor_object.profile ) rpgad ON m.requestor_grp = rpgad.id
1069             LEFT JOIN permission.grp_ancestors_distance( user_object.profile ) upgad ON m.usr_grp = upgad.id
1070             LEFT JOIN actor.org_unit_ancestors_distance( pickup_ou ) puoua ON m.pickup_ou = puoua.id
1071             LEFT JOIN actor.org_unit_ancestors_distance( request_ou ) rqoua ON m.request_ou = rqoua.id
1072             LEFT JOIN actor.org_unit_ancestors_distance( item_cn_object.owning_lib ) cnoua ON m.item_owning_ou = cnoua.id
1073             LEFT JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) iooua ON m.item_circ_ou = iooua.id
1074             LEFT JOIN actor.org_unit_ancestors_distance( user_object.home_ou  ) uhoua ON m.user_home_ou = uhoua.id
1075       WHERE m.active
1076             -- Permission Groups
1077          -- AND (m.requestor_grp        IS NULL OR upgad.id IS NOT NULL) -- Optional Requestor Group?
1078             AND (m.usr_grp              IS NULL OR upgad.id IS NOT NULL)
1079             -- Org Units
1080             AND (m.pickup_ou            IS NULL OR (puoua.id IS NOT NULL AND (puoua.distance = 0 OR NOT m.strict_ou_match)))
1081             AND (m.request_ou           IS NULL OR (rqoua.id IS NOT NULL AND (rqoua.distance = 0 OR NOT m.strict_ou_match)))
1082             AND (m.item_owning_ou       IS NULL OR (cnoua.id IS NOT NULL AND (cnoua.distance = 0 OR NOT m.strict_ou_match)))
1083             AND (m.item_circ_ou         IS NULL OR (iooua.id IS NOT NULL AND (iooua.distance = 0 OR NOT m.strict_ou_match)))
1084             AND (m.user_home_ou         IS NULL OR (uhoua.id IS NOT NULL AND (uhoua.distance = 0 OR NOT m.strict_ou_match)))
1085             -- Static User Checks
1086             AND (m.juvenile_flag        IS NULL OR m.juvenile_flag = user_object.juvenile)
1087             -- Static Item Checks
1088             AND (m.circ_modifier        IS NULL OR m.circ_modifier = item_object.circ_modifier)
1089             AND (m.marc_type            IS NULL OR m.marc_type = COALESCE(item_object.circ_as_type, rec_descriptor.item_type))
1090             AND (m.marc_form            IS NULL OR m.marc_form = rec_descriptor.item_form)
1091             AND (m.marc_bib_level       IS NULL OR m.marc_bib_level = rec_descriptor.bib_level)
1092             AND (m.marc_vr_format       IS NULL OR m.marc_vr_format = rec_descriptor.vr_format)
1093             AND (m.ref_flag             IS NULL OR m.ref_flag = item_object.ref)
1094             AND (m.item_age             IS NULL OR (my_item_age IS NOT NULL AND m.item_age > my_item_age))
1095       ORDER BY
1096             -- Permission Groups
1097             CASE WHEN rpgad.distance    IS NOT NULL THEN 2^(2*weights.requestor_grp - (rpgad.distance/denominator)) ELSE 0.0 END +
1098             CASE WHEN upgad.distance    IS NOT NULL THEN 2^(2*weights.usr_grp - (upgad.distance/denominator)) ELSE 0.0 END +
1099             -- Org Units
1100             CASE WHEN puoua.distance    IS NOT NULL THEN 2^(2*weights.pickup_ou - (puoua.distance/denominator)) ELSE 0.0 END +
1101             CASE WHEN rqoua.distance    IS NOT NULL THEN 2^(2*weights.request_ou - (rqoua.distance/denominator)) ELSE 0.0 END +
1102             CASE WHEN cnoua.distance    IS NOT NULL THEN 2^(2*weights.item_owning_ou - (cnoua.distance/denominator)) ELSE 0.0 END +
1103             CASE WHEN iooua.distance    IS NOT NULL THEN 2^(2*weights.item_circ_ou - (iooua.distance/denominator)) ELSE 0.0 END +
1104             CASE WHEN uhoua.distance    IS NOT NULL THEN 2^(2*weights.user_home_ou - (uhoua.distance/denominator)) ELSE 0.0 END +
1105             -- Static User Checks       -- Note: 4^x is equiv to 2^(2*x)
1106             CASE WHEN m.juvenile_flag   IS NOT NULL THEN 4^weights.juvenile_flag ELSE 0.0 END +
1107             -- Static Item Checks
1108             CASE WHEN m.circ_modifier   IS NOT NULL THEN 4^weights.circ_modifier ELSE 0.0 END +
1109             CASE WHEN m.marc_type       IS NOT NULL THEN 4^weights.marc_type ELSE 0.0 END +
1110             CASE WHEN m.marc_form       IS NOT NULL THEN 4^weights.marc_form ELSE 0.0 END +
1111             CASE WHEN m.marc_vr_format  IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0.0 END +
1112             CASE WHEN m.ref_flag        IS NOT NULL THEN 4^weights.ref_flag ELSE 0.0 END +
1113             -- Item age has a slight adjustment to weight based on value.
1114             -- This should ensure that a shorter age limit comes first when all else is equal.
1115             -- NOTE: This assumes that intervals will normally be in days.
1116             CASE WHEN m.item_age            IS NOT NULL THEN 4^weights.item_age - 86400/EXTRACT(EPOCH FROM m.item_age) ELSE 0.0 END DESC,
1117             -- Final sort on id, so that if two rules have the same sorting in the previous sort they have a defined order
1118             -- This prevents "we changed the table order by updating a rule, and we started getting different results"
1119             m.id;
1120
1121     -- Return just the ID for now
1122     RETURN matchpoint.id;
1123 END;
1124 $func$ LANGUAGE 'plpgsql';
1125
1126 DROP INDEX IF EXISTS config.ccmm_once_per_paramset;
1127
1128 DROP INDEX IF EXISTS config.chmm_once_per_paramset;
1129
1130 CREATE UNIQUE INDEX ccmm_once_per_paramset ON config.circ_matrix_matchpoint (org_unit, grp, COALESCE(circ_modifier, ''), COALESCE(marc_type, ''), COALESCE(marc_form, ''), COALESCE(marc_bib_level,''), COALESCE(marc_vr_format, ''), COALESCE(copy_circ_lib::TEXT, ''), COALESCE(copy_owning_lib::TEXT, ''), COALESCE(user_home_ou::TEXT, ''), COALESCE(ref_flag::TEXT, ''), COALESCE(juvenile_flag::TEXT, ''), COALESCE(is_renewal::TEXT, ''), COALESCE(usr_age_lower_bound::TEXT, ''), COALESCE(usr_age_upper_bound::TEXT, ''), COALESCE(item_age::TEXT, '')) WHERE active;
1131
1132 CREATE UNIQUE INDEX chmm_once_per_paramset ON config.hold_matrix_matchpoint (COALESCE(user_home_ou::TEXT, ''), COALESCE(request_ou::TEXT, ''), COALESCE(pickup_ou::TEXT, ''), COALESCE(item_owning_ou::TEXT, ''), COALESCE(item_circ_ou::TEXT, ''), COALESCE(usr_grp::TEXT, ''), COALESCE(requestor_grp::TEXT, ''), COALESCE(circ_modifier, ''), COALESCE(marc_type, ''), COALESCE(marc_form, ''), COALESCE(marc_bib_level, ''), COALESCE(marc_vr_format, ''), COALESCE(juvenile_flag::TEXT, ''), COALESCE(ref_flag::TEXT, ''), COALESCE(item_age::TEXT, '')) WHERE active;
1133
1134 UPDATE config.copy_status SET copy_active = true WHERE id IN (0, 1, 7, 8, 10, 12, 15);
1135
1136 INSERT into config.org_unit_setting_type
1137 ( name, label, description, datatype ) VALUES
1138 ( 'circ.holds.age_protect.active_date', 'Holds: Use Active Date for Age Protection', 'When calculating age protection rules use the active date instead of the creation date.', 'bool');
1139
1140 -- Assume create date when item is in status we would update active date for anyway
1141 UPDATE asset.copy SET active_date = create_date WHERE status IN (SELECT id FROM config.copy_status WHERE copy_active = true);
1142
1143 -- Assume create date for any item with circs
1144 UPDATE asset.copy SET active_date = create_date WHERE id IN (SELECT id FROM extend_reporter.full_circ_count WHERE circ_count > 0);
1145
1146 -- Assume create date for status change time while we are at it. Because being created WAS a change in status.
1147 UPDATE asset.copy SET status_changed_time = create_date WHERE status_changed_time IS NULL;
1148
1149 -- Evergreen DB patch 0564.data.delete_empty_volume.sql
1150 --
1151 -- New org setting cat.volume.delete_on_empty
1152 --
1153
1154 -- check whether patch can be applied
1155 SELECT evergreen.upgrade_deps_block_check('0564', :eg_version);
1156
1157 INSERT INTO config.org_unit_setting_type ( name, label, description, datatype ) 
1158     VALUES ( 
1159         'cat.volume.delete_on_empty',
1160         oils_i18n_gettext('cat.volume.delete_on_empty', 'Cat: Delete volume with last copy', 'coust', 'label'),
1161         oils_i18n_gettext('cat.volume.delete_on_empty', 'Automatically delete a volume when the last linked copy is deleted', 'coust', 'description'),
1162         'bool'
1163     );
1164
1165
1166 -- Evergreen DB patch 0565.schema.action-trigger.event_definition.hold-cancel-no-target-notification.sql
1167 --
1168 -- New action trigger event definition: Hold Cancelled (No Target) Email Notification
1169 --
1170
1171 -- check whether patch can be applied
1172 SELECT evergreen.upgrade_deps_block_check('0565', :eg_version);
1173
1174 INSERT INTO action_trigger.event_definition (id, active, owner, name, hook, validator, reactor, delay, delay_field, group_field, template)
1175     VALUES (38, FALSE, 1, 
1176         'Hold Cancelled (No Target) Email Notification', 
1177         'hold_request.cancel.expire_no_target', 
1178         'HoldIsCancelled', 'SendEmail', '30 minutes', 'cancel_time', 'usr',
1179 $$
1180 [%- USE date -%]
1181 [%- user = target.0.usr -%]
1182 To: [%- params.recipient_email || user.email %]
1183 From: [%- params.sender_email || default_sender %]
1184 Subject: Hold Request Cancelled
1185
1186 Dear [% user.family_name %], [% user.first_given_name %]
1187 The following holds were cancelled because no items were found to fullfil the hold.
1188
1189 [% FOR hold IN target %]
1190     Title: [% hold.bib_rec.bib_record.simple_record.title %]
1191     Author: [% hold.bib_rec.bib_record.simple_record.author %]
1192     Library: [% hold.pickup_lib.name %]
1193     Request Date: [% date.format(helpers.format_date(hold.rrequest_time), '%Y-%m-%d') %]
1194 [% END %]
1195
1196 $$);
1197
1198 INSERT INTO action_trigger.environment (event_def, path) VALUES
1199     (38, 'usr'),
1200     (38, 'pickup_lib'),
1201     (38, 'bib_rec.bib_record.simple_record');
1202
1203 -- Evergreen DB patch XXXX.data.ou_setting_generate_overdue_on_lost.sql.sql
1204
1205 -- check whether patch can be applied
1206 SELECT evergreen.upgrade_deps_block_check('0567', :eg_version);
1207
1208 INSERT INTO config.org_unit_setting_type ( name, label, description, datatype ) VALUES (
1209     'circ.lost.generate_overdue_on_checkin',
1210     oils_i18n_gettext( 
1211         'circ.lost.generate_overdue_on_checkin',
1212         'Circ:  Lost Checkin Generates New Overdues',
1213         'coust',
1214         'label'
1215     ),
1216     oils_i18n_gettext( 
1217         'circ.lost.generate_overdue_on_checkin',
1218         'Enabling this setting causes retroactive creation of not-yet-existing overdue fines on lost item checkin, up to the point of checkin time (or max fines is reached).  This is different than "restore overdue on lost", because it only creates new overdue fines.  Use both settings together to get the full complement of overdue fines for a lost item',
1219         'coust',
1220         'label'
1221     ),
1222     'bool'
1223 );
1224
1225 -- Evergreen DB patch 0572.vandelay-record-matching-and-quality.sql
1226 --
1227
1228
1229 -- check whether patch can be applied
1230 SELECT evergreen.upgrade_deps_block_check('0572', :eg_version);
1231
1232 CREATE OR REPLACE FUNCTION evergreen.array_remove_item_by_value(inp ANYARRAY, el ANYELEMENT) RETURNS anyarray AS $$ SELECT ARRAY_ACCUM(x.e) FROM UNNEST( $1 ) x(e) WHERE x.e <> $2; $$ LANGUAGE SQL;
1233
1234 CREATE TABLE vandelay.match_set (
1235     id      SERIAL  PRIMARY KEY,
1236     name    TEXT        NOT NULL,
1237     owner   INT     NOT NULL REFERENCES actor.org_unit (id) ON DELETE CASCADE,
1238     mtype   TEXT        NOT NULL DEFAULT 'biblio', -- 'biblio','authority','mfhd'?, others?
1239     CONSTRAINT name_once_per_owner_mtype UNIQUE (name, owner, mtype)
1240 );
1241
1242 -- Table to define match points, either FF via SVF or tag+subfield
1243 CREATE TABLE vandelay.match_set_point (
1244     id          SERIAL  PRIMARY KEY,
1245     match_set   INT     REFERENCES vandelay.match_set (id) ON DELETE CASCADE,
1246     parent      INT     REFERENCES vandelay.match_set_point (id),
1247     bool_op     TEXT    CHECK (bool_op IS NULL OR (bool_op IN ('AND','OR','NOT'))),
1248     svf         TEXT    REFERENCES config.record_attr_definition (name),
1249     tag         TEXT,
1250     subfield    TEXT,
1251     negate      BOOL    DEFAULT FALSE,
1252     quality     INT     NOT NULL DEFAULT 1, -- higher is better
1253     CONSTRAINT vmsp_need_a_subfield_with_a_tag CHECK ((tag IS NOT NULL AND subfield IS NOT NULL) OR tag IS NULL),
1254     CONSTRAINT vmsp_need_a_tag_or_a_ff_or_a_bo CHECK (
1255         (tag IS NOT NULL AND svf IS NULL AND bool_op IS NULL) OR
1256         (tag IS NULL AND svf IS NOT NULL AND bool_op IS NULL) OR
1257         (tag IS NULL AND svf IS NULL AND bool_op IS NOT NULL)
1258     )
1259 );
1260
1261 CREATE TABLE vandelay.match_set_quality (
1262     id          SERIAL  PRIMARY KEY,
1263     match_set   INT     NOT NULL REFERENCES vandelay.match_set (id) ON DELETE CASCADE,
1264     svf         TEXT    REFERENCES config.record_attr_definition,
1265     tag         TEXT,
1266     subfield    TEXT,
1267     value       TEXT    NOT NULL,
1268     quality     INT     NOT NULL DEFAULT 1, -- higher is better
1269     CONSTRAINT vmsq_need_a_subfield_with_a_tag CHECK ((tag IS NOT NULL AND subfield IS NOT NULL) OR tag IS NULL),
1270     CONSTRAINT vmsq_need_a_tag_or_a_ff CHECK ((tag IS NOT NULL AND svf IS NULL) OR (tag IS NULL AND svf IS NOT NULL))
1271 );
1272 CREATE UNIQUE INDEX vmsq_def_once_per_set ON vandelay.match_set_quality (match_set, COALESCE(tag,''), COALESCE(subfield,''), COALESCE(svf,''), value);
1273
1274
1275 -- ALTER TABLEs...
1276 ALTER TABLE vandelay.queue ADD COLUMN match_set INT REFERENCES vandelay.match_set (id) ON UPDATE CASCADE ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED;
1277 ALTER TABLE vandelay.queued_record ADD COLUMN quality INT NOT NULL DEFAULT 0;
1278 ALTER TABLE vandelay.bib_attr_definition DROP COLUMN ident;
1279
1280 CREATE TABLE vandelay.import_error (
1281     code        TEXT    PRIMARY KEY,
1282     description TEXT    NOT NULL -- i18n
1283 );
1284
1285 ALTER TABLE vandelay.queued_bib_record
1286     ADD COLUMN import_error TEXT REFERENCES vandelay.import_error (code) ON DELETE SET NULL ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
1287     ADD COLUMN error_detail TEXT;
1288
1289 ALTER TABLE vandelay.bib_match
1290     DROP COLUMN field_type,
1291     DROP COLUMN matched_attr,
1292     ADD COLUMN quality INT NOT NULL DEFAULT 1,
1293     ADD COLUMN match_score INT NOT NULL DEFAULT 0;
1294
1295 ALTER TABLE vandelay.import_item
1296     ADD COLUMN import_error TEXT REFERENCES vandelay.import_error (code) ON DELETE SET NULL ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
1297     ADD COLUMN error_detail TEXT,
1298     ADD COLUMN imported_as BIGINT REFERENCES asset.copy (id) DEFERRABLE INITIALLY DEFERRED,
1299     ADD COLUMN import_time TIMESTAMP WITH TIME ZONE;
1300
1301 ALTER TABLE vandelay.merge_profile ADD COLUMN lwm_ratio NUMERIC;
1302
1303 CREATE OR REPLACE FUNCTION vandelay.marc21_record_type( marc TEXT ) RETURNS config.marc21_rec_type_map AS $func$
1304 DECLARE
1305     ldr         TEXT;
1306     tval        TEXT;
1307     tval_rec    RECORD;
1308     bval        TEXT;
1309     bval_rec    RECORD;
1310     retval      config.marc21_rec_type_map%ROWTYPE;
1311 BEGIN
1312     ldr := oils_xpath_string( '//*[local-name()="leader"]', marc );
1313
1314     IF ldr IS NULL OR ldr = '' THEN
1315         SELECT * INTO retval FROM config.marc21_rec_type_map WHERE code = 'BKS';
1316         RETURN retval;
1317     END IF;
1318
1319     SELECT * INTO tval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'Type' LIMIT 1; -- They're all the same
1320     SELECT * INTO bval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'BLvl' LIMIT 1; -- They're all the same
1321
1322
1323     tval := SUBSTRING( ldr, tval_rec.start_pos + 1, tval_rec.length );
1324     bval := SUBSTRING( ldr, bval_rec.start_pos + 1, bval_rec.length );
1325
1326     -- RAISE NOTICE 'type %, blvl %, ldr %', tval, bval, ldr;
1327
1328     SELECT * INTO retval FROM config.marc21_rec_type_map WHERE type_val LIKE '%' || tval || '%' AND blvl_val LIKE '%' || bval || '%';
1329
1330
1331     IF retval.code IS NULL THEN
1332         SELECT * INTO retval FROM config.marc21_rec_type_map WHERE code = 'BKS';
1333     END IF;
1334
1335     RETURN retval;
1336 END;
1337 $func$ LANGUAGE PLPGSQL;
1338
1339 CREATE OR REPLACE FUNCTION vandelay.marc21_extract_fixed_field( marc TEXT, ff TEXT ) RETURNS TEXT AS $func$
1340 DECLARE
1341     rtype       TEXT;
1342     ff_pos      RECORD;
1343     tag_data    RECORD;
1344     val         TEXT;
1345 BEGIN
1346     rtype := (vandelay.marc21_record_type( marc )).code;
1347     FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE fixed_field = ff AND rec_type = rtype ORDER BY tag DESC LOOP
1348         IF ff_pos.tag = 'ldr' THEN
1349             val := oils_xpath_string('//*[local-name()="leader"]', marc);
1350             IF val IS NOT NULL THEN
1351                 val := SUBSTRING( val, ff_pos.start_pos + 1, ff_pos.length );
1352                 RETURN val;
1353             END IF;
1354         ELSE
1355             FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(ff_pos.tag) || '"]/text()', marc ) ) x(value) LOOP
1356                 val := SUBSTRING( tag_data.value, ff_pos.start_pos + 1, ff_pos.length );
1357                 RETURN val;
1358             END LOOP;
1359         END IF;
1360         val := REPEAT( ff_pos.default_val, ff_pos.length );
1361         RETURN val;
1362     END LOOP;
1363
1364     RETURN NULL;
1365 END;
1366 $func$ LANGUAGE PLPGSQL;
1367
1368 CREATE OR REPLACE FUNCTION vandelay.marc21_extract_all_fixed_fields( marc TEXT ) RETURNS SETOF biblio.record_ff_map AS $func$
1369 DECLARE
1370     tag_data    TEXT;
1371     rtype       TEXT;
1372     ff_pos      RECORD;
1373     output      biblio.record_ff_map%ROWTYPE;
1374 BEGIN
1375     rtype := (vandelay.marc21_record_type( marc )).code;
1376
1377     FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE rec_type = rtype ORDER BY tag DESC LOOP
1378         output.ff_name  := ff_pos.fixed_field;
1379         output.ff_value := NULL;
1380
1381         IF ff_pos.tag = 'ldr' THEN
1382             output.ff_value := oils_xpath_string('//*[local-name()="leader"]', marc);
1383             IF output.ff_value IS NOT NULL THEN
1384                 output.ff_value := SUBSTRING( output.ff_value, ff_pos.start_pos + 1, ff_pos.length );
1385                 RETURN NEXT output;
1386                 output.ff_value := NULL;
1387             END IF;
1388         ELSE
1389             FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(ff_pos.tag) || '"]/text()', marc ) ) x(value) LOOP
1390                 output.ff_value := SUBSTRING( tag_data, ff_pos.start_pos + 1, ff_pos.length );
1391                 IF output.ff_value IS NULL THEN output.ff_value := REPEAT( ff_pos.default_val, ff_pos.length ); END IF;
1392                 RETURN NEXT output;
1393                 output.ff_value := NULL;
1394             END LOOP;
1395         END IF;
1396     
1397     END LOOP;
1398
1399     RETURN;
1400 END;
1401 $func$ LANGUAGE PLPGSQL;
1402
1403 CREATE OR REPLACE FUNCTION vandelay.marc21_physical_characteristics( marc TEXT) RETURNS SETOF biblio.marc21_physical_characteristics AS $func$
1404 DECLARE
1405     rowid   INT := 0;
1406     _007    TEXT;
1407     ptype   config.marc21_physical_characteristic_type_map%ROWTYPE;
1408     psf     config.marc21_physical_characteristic_subfield_map%ROWTYPE;
1409     pval    config.marc21_physical_characteristic_value_map%ROWTYPE;
1410     retval  biblio.marc21_physical_characteristics%ROWTYPE;
1411 BEGIN
1412
1413     _007 := oils_xpath_string( '//*[@tag="007"]', marc );
1414
1415     IF _007 IS NOT NULL AND _007 <> '' THEN
1416         SELECT * INTO ptype FROM config.marc21_physical_characteristic_type_map WHERE ptype_key = SUBSTRING( _007, 1, 1 );
1417
1418         IF ptype.ptype_key IS NOT NULL THEN
1419             FOR psf IN SELECT * FROM config.marc21_physical_characteristic_subfield_map WHERE ptype_key = ptype.ptype_key LOOP
1420                 SELECT * INTO pval FROM config.marc21_physical_characteristic_value_map WHERE ptype_subfield = psf.id AND value = SUBSTRING( _007, psf.start_pos + 1, psf.length );
1421
1422                 IF pval.id IS NOT NULL THEN
1423                     rowid := rowid + 1;
1424                     retval.id := rowid;
1425                     retval.ptype := ptype.ptype_key;
1426                     retval.subfield := psf.id;
1427                     retval.value := pval.id;
1428                     RETURN NEXT retval;
1429                 END IF;
1430
1431             END LOOP;
1432         END IF;
1433     END IF;
1434
1435     RETURN;
1436 END;
1437 $func$ LANGUAGE PLPGSQL;
1438
1439 CREATE TYPE vandelay.flat_marc AS ( tag CHAR(3), ind1 TEXT, ind2 TEXT, subfield TEXT, value TEXT );
1440 CREATE OR REPLACE FUNCTION vandelay.flay_marc ( TEXT ) RETURNS SETOF vandelay.flat_marc AS $func$
1441
1442 use MARC::Record;
1443 use MARC::File::XML (BinaryEncoding => 'UTF-8');
1444 use MARC::Charset;
1445 use strict;
1446
1447 MARC::Charset->assume_unicode(1);
1448
1449 my $xml = shift;
1450 my $r = MARC::Record->new_from_xml( $xml );
1451
1452 return_next( { tag => 'LDR', value => $r->leader } );
1453
1454 for my $f ( $r->fields ) {
1455     if ($f->is_control_field) {
1456         return_next({ tag => $f->tag, value => $f->data });
1457     } else {
1458         for my $s ($f->subfields) {
1459             return_next({
1460                 tag      => $f->tag,
1461                 ind1     => $f->indicator(1),
1462                 ind2     => $f->indicator(2),
1463                 subfield => $s->[0],
1464                 value    => $s->[1]
1465             });
1466
1467             if ( $f->tag eq '245' and $s->[0] eq 'a' ) {
1468                 my $trim = $f->indicator(2) || 0;
1469                 return_next({
1470                     tag      => 'tnf',
1471                     ind1     => $f->indicator(1),
1472                     ind2     => $f->indicator(2),
1473                     subfield => 'a',
1474                     value    => substr( $s->[1], $trim )
1475                 });
1476             }
1477         }
1478     }
1479 }
1480
1481 return undef;
1482
1483 $func$ LANGUAGE PLPERLU;
1484
1485 CREATE OR REPLACE FUNCTION vandelay.flatten_marc ( marc TEXT ) RETURNS SETOF vandelay.flat_marc AS $func$
1486 DECLARE
1487     output  vandelay.flat_marc%ROWTYPE;
1488     field   RECORD;
1489 BEGIN
1490     FOR field IN SELECT * FROM vandelay.flay_marc( marc ) LOOP
1491         output.ind1 := field.ind1;
1492         output.ind2 := field.ind2;
1493         output.tag := field.tag;
1494         output.subfield := field.subfield;
1495         IF field.subfield IS NOT NULL AND field.tag NOT IN ('020','022','024') THEN -- exclude standard numbers and control fields
1496             output.value := naco_normalize(field.value, field.subfield);
1497         ELSE
1498             output.value := field.value;
1499         END IF;
1500
1501         CONTINUE WHEN output.value IS NULL;
1502
1503         RETURN NEXT output;
1504     END LOOP;
1505 END;
1506 $func$ LANGUAGE PLPGSQL;
1507
1508 CREATE OR REPLACE FUNCTION vandelay.extract_rec_attrs ( xml TEXT, attr_defs TEXT[]) RETURNS hstore AS $_$
1509 DECLARE
1510     transformed_xml TEXT;
1511     prev_xfrm       TEXT;
1512     normalizer      RECORD;
1513     xfrm            config.xml_transform%ROWTYPE;
1514     attr_value      TEXT;
1515     new_attrs       HSTORE := ''::HSTORE;
1516     attr_def        config.record_attr_definition%ROWTYPE;
1517 BEGIN
1518
1519     FOR attr_def IN SELECT * FROM config.record_attr_definition WHERE name IN (SELECT * FROM UNNEST(attr_defs)) ORDER BY format LOOP
1520
1521         IF attr_def.tag IS NOT NULL THEN -- tag (and optional subfield list) selection
1522             SELECT  ARRAY_TO_STRING(ARRAY_ACCUM(x.value), COALESCE(attr_def.joiner,' ')) INTO attr_value
1523               FROM  vandelay.flatten_marc(xml) AS x
1524               WHERE x.tag LIKE attr_def.tag
1525                     AND CASE
1526                         WHEN attr_def.sf_list IS NOT NULL
1527                             THEN POSITION(x.subfield IN attr_def.sf_list) > 0
1528                         ELSE TRUE
1529                         END
1530               GROUP BY x.tag
1531               ORDER BY x.tag
1532               LIMIT 1;
1533
1534         ELSIF attr_def.fixed_field IS NOT NULL THEN -- a named fixed field, see config.marc21_ff_pos_map.fixed_field
1535             attr_value := vandelay.marc21_extract_fixed_field(xml, attr_def.fixed_field);
1536
1537         ELSIF attr_def.xpath IS NOT NULL THEN -- and xpath expression
1538
1539             SELECT INTO xfrm * FROM config.xml_transform WHERE name = attr_def.format;
1540
1541             -- See if we can skip the XSLT ... it's expensive
1542             IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
1543                 -- Can't skip the transform
1544                 IF xfrm.xslt <> '---' THEN
1545                     transformed_xml := oils_xslt_process(xml,xfrm.xslt);
1546                 ELSE
1547                     transformed_xml := xml;
1548                 END IF;
1549
1550                 prev_xfrm := xfrm.name;
1551             END IF;
1552
1553             IF xfrm.name IS NULL THEN
1554                 -- just grab the marcxml (empty) transform
1555                 SELECT INTO xfrm * FROM config.xml_transform WHERE xslt = '---' LIMIT 1;
1556                 prev_xfrm := xfrm.name;
1557             END IF;
1558
1559             attr_value := oils_xpath_string(attr_def.xpath, transformed_xml, COALESCE(attr_def.joiner,' '), ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]);
1560
1561         ELSIF attr_def.phys_char_sf IS NOT NULL THEN -- a named Physical Characteristic, see config.marc21_physical_characteristic_*_map
1562             SELECT  m.value::TEXT INTO attr_value
1563               FROM  vandelay.marc21_physical_characteristics(xml) v
1564                     JOIN config.marc21_physical_characteristic_value_map m ON (m.id = v.value)
1565               WHERE v.subfield = attr_def.phys_char_sf
1566               LIMIT 1; -- Just in case ...
1567
1568         END IF;
1569
1570         -- apply index normalizers to attr_value
1571         FOR normalizer IN
1572             SELECT  n.func AS func,
1573                     n.param_count AS param_count,
1574                     m.params AS params
1575               FROM  config.index_normalizer n
1576                     JOIN config.record_attr_index_norm_map m ON (m.norm = n.id)
1577               WHERE attr = attr_def.name
1578               ORDER BY m.pos LOOP
1579                 EXECUTE 'SELECT ' || normalizer.func || '(' ||
1580                     quote_literal( attr_value ) ||
1581                     CASE
1582                         WHEN normalizer.param_count > 0
1583                             THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
1584                             ELSE ''
1585                         END ||
1586                     ')' INTO attr_value;
1587
1588         END LOOP;
1589
1590         -- Add the new value to the hstore
1591         new_attrs := new_attrs || hstore( attr_def.name, attr_value );
1592
1593     END LOOP;
1594
1595     RETURN new_attrs;
1596 END;
1597 $_$ LANGUAGE PLPGSQL;
1598
1599 CREATE OR REPLACE FUNCTION vandelay.extract_rec_attrs ( xml TEXT ) RETURNS hstore AS $_$
1600     SELECT vandelay.extract_rec_attrs( $1, (SELECT ARRAY_ACCUM(name) FROM config.record_attr_definition));
1601 $_$ LANGUAGE SQL;
1602
1603 -- Everything between this comment and the beginning of the definition of
1604 -- vandelay.match_bib_record() is strictly in service of that function.
1605 CREATE TYPE vandelay.match_set_test_result AS (record BIGINT, quality INTEGER);
1606
1607 CREATE OR REPLACE FUNCTION vandelay.match_set_test_marcxml(
1608     match_set_id INTEGER, record_xml TEXT
1609 ) RETURNS SETOF vandelay.match_set_test_result AS $$
1610 DECLARE
1611     tags_rstore HSTORE;
1612     svf_rstore  HSTORE;
1613     coal        TEXT;
1614     joins       TEXT;
1615     query_      TEXT;
1616     wq          TEXT;
1617     qvalue      INTEGER;
1618     rec         RECORD;
1619 BEGIN
1620     tags_rstore := vandelay.flatten_marc_hstore(record_xml);
1621     svf_rstore := vandelay.extract_rec_attrs(record_xml);
1622
1623     CREATE TEMPORARY TABLE _vandelay_tmp_qrows (q INTEGER);
1624     CREATE TEMPORARY TABLE _vandelay_tmp_jrows (j TEXT);
1625
1626     -- generate the where clause and return that directly (into wq), and as
1627     -- a side-effect, populate the _vandelay_tmp_[qj]rows tables.
1628     wq := vandelay.get_expr_from_match_set(match_set_id);
1629
1630     query_ := 'SELECT bre.id AS record, ';
1631
1632     -- qrows table is for the quality bits we add to the SELECT clause
1633     SELECT ARRAY_TO_STRING(
1634         ARRAY_ACCUM('COALESCE(n' || q::TEXT || '.quality, 0)'), ' + '
1635     ) INTO coal FROM _vandelay_tmp_qrows;
1636
1637     -- our query string so far is the SELECT clause and the inital FROM.
1638     -- no JOINs yet nor the WHERE clause
1639     query_ := query_ || coal || ' AS quality ' || E'\n' ||
1640         'FROM biblio.record_entry bre ';
1641
1642     -- jrows table is for the joins we must make (and the real text conditions)
1643     SELECT ARRAY_TO_STRING(ARRAY_ACCUM(j), E'\n') INTO joins
1644         FROM _vandelay_tmp_jrows;
1645
1646     -- add those joins and the where clause to our query.
1647     query_ := query_ || joins || E'\n' || 'WHERE ' || wq || ' AND not bre.deleted';
1648
1649     -- this will return rows of record,quality
1650     FOR rec IN EXECUTE query_ USING tags_rstore, svf_rstore LOOP
1651         RETURN NEXT rec;
1652     END LOOP;
1653
1654     DROP TABLE _vandelay_tmp_qrows;
1655     DROP TABLE _vandelay_tmp_jrows;
1656     RETURN;
1657 END;
1658
1659 $$ LANGUAGE PLPGSQL;
1660
1661 CREATE OR REPLACE FUNCTION vandelay.flatten_marc_hstore(
1662     record_xml TEXT
1663 ) RETURNS HSTORE AS $$
1664 BEGIN
1665     RETURN (SELECT
1666         HSTORE(
1667             ARRAY_ACCUM(tag || (COALESCE(subfield, ''))),
1668             ARRAY_ACCUM(value)
1669         )
1670         FROM (
1671             SELECT tag, subfield, ARRAY_ACCUM(value)::TEXT AS value
1672                 FROM vandelay.flatten_marc(record_xml)
1673                 GROUP BY tag, subfield ORDER BY tag, subfield
1674         ) subquery
1675     );
1676 END;
1677 $$ LANGUAGE PLPGSQL;
1678
1679 CREATE OR REPLACE FUNCTION vandelay.get_expr_from_match_set(
1680     match_set_id INTEGER
1681 ) RETURNS TEXT AS $$
1682 DECLARE
1683     root    vandelay.match_set_point;
1684 BEGIN
1685     SELECT * INTO root FROM vandelay.match_set_point
1686         WHERE parent IS NULL AND match_set = match_set_id;
1687
1688     RETURN vandelay.get_expr_from_match_set_point(root);
1689 END;
1690 $$  LANGUAGE PLPGSQL;
1691
1692 CREATE OR REPLACE FUNCTION vandelay.get_expr_from_match_set_point(
1693     node vandelay.match_set_point
1694 ) RETURNS TEXT AS $$
1695 DECLARE
1696     q           TEXT;
1697     i           INTEGER;
1698     this_op     TEXT;
1699     children    INTEGER[];
1700     child       vandelay.match_set_point;
1701 BEGIN
1702     SELECT ARRAY_ACCUM(id) INTO children FROM vandelay.match_set_point
1703         WHERE parent = node.id;
1704
1705     IF ARRAY_LENGTH(children, 1) > 0 THEN
1706         this_op := vandelay._get_expr_render_one(node);
1707         q := '(';
1708         i := 1;
1709         WHILE children[i] IS NOT NULL LOOP
1710             SELECT * INTO child FROM vandelay.match_set_point
1711                 WHERE id = children[i];
1712             IF i > 1 THEN
1713                 q := q || ' ' || this_op || ' ';
1714             END IF;
1715             i := i + 1;
1716             q := q || vandelay.get_expr_from_match_set_point(child);
1717         END LOOP;
1718         q := q || ')';
1719         RETURN q;
1720     ELSIF node.bool_op IS NULL THEN
1721         PERFORM vandelay._get_expr_push_qrow(node);
1722         PERFORM vandelay._get_expr_push_jrow(node);
1723         RETURN vandelay._get_expr_render_one(node);
1724     ELSE
1725         RETURN '';
1726     END IF;
1727 END;
1728 $$  LANGUAGE PLPGSQL;
1729
1730 CREATE OR REPLACE FUNCTION vandelay._get_expr_push_qrow(
1731     node vandelay.match_set_point
1732 ) RETURNS VOID AS $$
1733 DECLARE
1734 BEGIN
1735     INSERT INTO _vandelay_tmp_qrows (q) VALUES (node.id);
1736 END;
1737 $$ LANGUAGE PLPGSQL;
1738
1739 CREATE OR REPLACE FUNCTION vandelay._get_expr_push_jrow(
1740     node vandelay.match_set_point
1741 ) RETURNS VOID AS $$
1742 DECLARE
1743     jrow        TEXT;
1744     my_alias    TEXT;
1745     op          TEXT;
1746     tagkey      TEXT;
1747 BEGIN
1748     IF node.negate THEN
1749         op := '<>';
1750     ELSE
1751         op := '=';
1752     END IF;
1753
1754     IF node.tag IS NOT NULL THEN
1755         tagkey := node.tag;
1756         IF node.subfield IS NOT NULL THEN
1757             tagkey := tagkey || node.subfield;
1758         END IF;
1759     END IF;
1760
1761     my_alias := 'n' || node.id::TEXT;
1762
1763     jrow := 'LEFT JOIN (SELECT *, ' || node.quality ||
1764         ' AS quality FROM metabib.';
1765     IF node.tag IS NOT NULL THEN
1766         jrow := jrow || 'full_rec) ' || my_alias || ' ON (' ||
1767             my_alias || '.record = bre.id AND ' || my_alias || '.tag = ''' ||
1768             node.tag || '''';
1769         IF node.subfield IS NOT NULL THEN
1770             jrow := jrow || ' AND ' || my_alias || '.subfield = ''' ||
1771                 node.subfield || '''';
1772         END IF;
1773         jrow := jrow || ' AND (' || my_alias || '.value ' || op ||
1774             ' ANY(($1->''' || tagkey || ''')::TEXT[])))';
1775     ELSE    -- svf
1776         jrow := jrow || 'record_attr) ' || my_alias || ' ON (' ||
1777             my_alias || '.id = bre.id AND (' ||
1778             my_alias || '.attrs->''' || node.svf ||
1779             ''' ' || op || ' $2->''' || node.svf || '''))';
1780     END IF;
1781     INSERT INTO _vandelay_tmp_jrows (j) VALUES (jrow);
1782 END;
1783 $$ LANGUAGE PLPGSQL;
1784
1785 CREATE OR REPLACE FUNCTION vandelay._get_expr_render_one(
1786     node vandelay.match_set_point
1787 ) RETURNS TEXT AS $$
1788 DECLARE
1789     s           TEXT;
1790 BEGIN
1791     IF node.bool_op IS NOT NULL THEN
1792         RETURN node.bool_op;
1793     ELSE
1794         RETURN '(n' || node.id::TEXT || '.id IS NOT NULL)';
1795     END IF;
1796 END;
1797 $$ LANGUAGE PLPGSQL;
1798
1799 CREATE OR REPLACE FUNCTION vandelay.match_bib_record() RETURNS TRIGGER AS $func$
1800 DECLARE
1801     incoming_existing_id    TEXT;
1802     test_result             vandelay.match_set_test_result%ROWTYPE;
1803     tmp_rec                 BIGINT;
1804     match_set               INT;
1805 BEGIN
1806     IF TG_OP IN ('INSERT','UPDATE') AND NEW.imported_as IS NOT NULL THEN
1807         RETURN NEW;
1808     END IF;
1809
1810     DELETE FROM vandelay.bib_match WHERE queued_record = NEW.id;
1811
1812     SELECT q.match_set INTO match_set FROM vandelay.bib_queue q WHERE q.id = NEW.queue;
1813
1814     IF match_set IS NOT NULL THEN
1815         NEW.quality := vandelay.measure_record_quality( NEW.marc, match_set );
1816     END IF;
1817
1818     -- Perfect matches on 901$c exit early with a match with high quality.
1819     incoming_existing_id :=
1820         oils_xpath_string('//*[@tag="901"]/*[@code="c"][1]', NEW.marc);
1821
1822     IF incoming_existing_id IS NOT NULL AND incoming_existing_id != '' THEN
1823         SELECT id INTO tmp_rec FROM biblio.record_entry WHERE id = incoming_existing_id::bigint;
1824         IF tmp_rec IS NOT NULL THEN
1825             INSERT INTO vandelay.bib_match (queued_record, eg_record, match_score, quality) 
1826                 SELECT
1827                     NEW.id, 
1828                     b.id,
1829                     9999,
1830                     -- note: no match_set means quality==0
1831                     vandelay.measure_record_quality( b.marc, match_set )
1832                 FROM biblio.record_entry b
1833                 WHERE id = incoming_existing_id::bigint;
1834         END IF;
1835     END IF;
1836
1837     IF match_set IS NULL THEN
1838         RETURN NEW;
1839     END IF;
1840
1841     FOR test_result IN SELECT * FROM
1842         vandelay.match_set_test_marcxml(match_set, NEW.marc) LOOP
1843
1844         INSERT INTO vandelay.bib_match ( queued_record, eg_record, match_score, quality )
1845             SELECT  
1846                 NEW.id,
1847                 test_result.record,
1848                 test_result.quality,
1849                 vandelay.measure_record_quality( b.marc, match_set )
1850                 FROM  biblio.record_entry b
1851                 WHERE id = test_result.record;
1852
1853     END LOOP;
1854
1855     RETURN NEW;
1856 END;
1857 $func$ LANGUAGE PLPGSQL;
1858
1859 CREATE OR REPLACE FUNCTION vandelay.measure_record_quality ( xml TEXT, match_set_id INT ) RETURNS INT AS $_$
1860 DECLARE
1861     out_q   INT := 0;
1862     rvalue  TEXT;
1863     test    vandelay.match_set_quality%ROWTYPE;
1864 BEGIN
1865
1866     FOR test IN SELECT * FROM vandelay.match_set_quality WHERE match_set = match_set_id LOOP
1867         IF test.tag IS NOT NULL THEN
1868             FOR rvalue IN SELECT value FROM vandelay.flatten_marc( xml ) WHERE tag = test.tag AND subfield = test.subfield LOOP
1869                 IF test.value = rvalue THEN
1870                     out_q := out_q + test.quality;
1871                 END IF;
1872             END LOOP;
1873         ELSE
1874             IF test.value = vandelay.extract_rec_attrs(xml, ARRAY[test.svf]) -> test.svf THEN
1875                 out_q := out_q + test.quality;
1876             END IF;
1877         END IF;
1878     END LOOP;
1879
1880     RETURN out_q;
1881 END;
1882 $_$ LANGUAGE PLPGSQL;
1883
1884
1885 CREATE OR REPLACE FUNCTION vandelay.overlay_bib_record ( import_id BIGINT, eg_id BIGINT, merge_profile_id INT ) RETURNS BOOL AS $$
1886 DECLARE
1887     merge_profile   vandelay.merge_profile%ROWTYPE;
1888     dyn_profile     vandelay.compile_profile%ROWTYPE;
1889     editor_string   TEXT;
1890     editor_id       INT;
1891     source_marc     TEXT;
1892     target_marc     TEXT;
1893     eg_marc         TEXT;
1894     v_marc          TEXT;
1895     replace_rule    TEXT;
1896 BEGIN
1897
1898     SELECT  q.marc INTO v_marc
1899       FROM  vandelay.queued_record q
1900             JOIN vandelay.bib_match m ON (m.queued_record = q.id AND q.id = import_id)
1901       LIMIT 1;
1902
1903     IF v_marc IS NULL THEN
1904         -- RAISE NOTICE 'no marc for vandelay or bib record';
1905         RETURN FALSE;
1906     END IF;
1907
1908     IF vandelay.template_overlay_bib_record( v_marc, eg_id, merge_profile_id) THEN
1909         UPDATE  vandelay.queued_bib_record
1910           SET   imported_as = eg_id,
1911                 import_time = NOW()
1912           WHERE id = import_id;
1913
1914         editor_string := (oils_xpath('//*[@tag="905"]/*[@code="u"]/text()',v_marc))[1];
1915
1916         IF editor_string IS NOT NULL AND editor_string <> '' THEN
1917             SELECT usr INTO editor_id FROM actor.card WHERE barcode = editor_string;
1918
1919             IF editor_id IS NULL THEN
1920                 SELECT id INTO editor_id FROM actor.usr WHERE usrname = editor_string;
1921             END IF;
1922
1923             IF editor_id IS NOT NULL THEN
1924                 UPDATE biblio.record_entry SET editor = editor_id WHERE id = eg_id;
1925             END IF;
1926         END IF;
1927
1928         RETURN TRUE;
1929     END IF;
1930
1931     -- RAISE NOTICE 'update of biblio.record_entry failed';
1932
1933     RETURN FALSE;
1934
1935 END;
1936 $$ LANGUAGE PLPGSQL;
1937
1938
1939 CREATE OR REPLACE FUNCTION vandelay.auto_overlay_bib_record_with_best ( import_id BIGINT, merge_profile_id INT, lwm_ratio_value_p NUMERIC ) RETURNS BOOL AS $$
1940 DECLARE
1941     eg_id           BIGINT;
1942     lwm_ratio_value NUMERIC;
1943 BEGIN
1944
1945     lwm_ratio_value := COALESCE(lwm_ratio_value_p, 0.0);
1946
1947     PERFORM * FROM vandelay.queued_bib_record WHERE import_time IS NOT NULL AND id = import_id;
1948
1949     IF FOUND THEN
1950         -- RAISE NOTICE 'already imported, cannot auto-overlay'
1951         RETURN FALSE;
1952     END IF;
1953
1954     SELECT  m.eg_record INTO eg_id
1955       FROM  vandelay.bib_match m
1956             JOIN vandelay.queued_bib_record qr ON (m.queued_record = qr.id)
1957             JOIN vandelay.bib_queue q ON (qr.queue = q.id)
1958             JOIN biblio.record_entry r ON (r.id = m.eg_record)
1959       WHERE m.queued_record = import_id
1960             AND qr.quality::NUMERIC / COALESCE(NULLIF(m.quality,0),1)::NUMERIC >= lwm_ratio_value
1961       ORDER BY  m.match_score DESC, -- required match score
1962                 qr.quality::NUMERIC / COALESCE(NULLIF(m.quality,0),1)::NUMERIC DESC, -- quality tie breaker
1963                 m.id -- when in doubt, use the first match
1964       LIMIT 1;
1965
1966     IF eg_id IS NULL THEN
1967         -- RAISE NOTICE 'incoming record is not of high enough quality';
1968         RETURN FALSE;
1969     END IF;
1970
1971     RETURN vandelay.overlay_bib_record( import_id, eg_id, merge_profile_id );
1972 END;
1973 $$ LANGUAGE PLPGSQL;
1974
1975 CREATE OR REPLACE FUNCTION vandelay.auto_overlay_bib_record_with_best ( import_id BIGINT, merge_profile_id INT, lwm_ratio_value_p NUMERIC ) RETURNS BOOL AS $$
1976 DECLARE
1977     eg_id           BIGINT;
1978     lwm_ratio_value NUMERIC;
1979 BEGIN
1980
1981     lwm_ratio_value := COALESCE(lwm_ratio_value_p, 0.0);
1982
1983     PERFORM * FROM vandelay.queued_bib_record WHERE import_time IS NOT NULL AND id = import_id;
1984
1985     IF FOUND THEN
1986         -- RAISE NOTICE 'already imported, cannot auto-overlay'
1987         RETURN FALSE;
1988     END IF;
1989
1990     SELECT  m.eg_record INTO eg_id
1991       FROM  vandelay.bib_match m
1992             JOIN vandelay.queued_bib_record qr ON (m.queued_record = qr.id)
1993             JOIN vandelay.bib_queue q ON (qr.queue = q.id)
1994             JOIN biblio.record_entry r ON (r.id = m.eg_record)
1995       WHERE m.queued_record = import_id
1996             AND qr.quality::NUMERIC / COALESCE(NULLIF(m.quality,0),1)::NUMERIC >= lwm_ratio_value
1997       ORDER BY  m.match_score DESC, -- required match score
1998                 qr.quality::NUMERIC / COALESCE(NULLIF(m.quality,0),1)::NUMERIC DESC, -- quality tie breaker
1999                 m.id -- when in doubt, use the first match
2000       LIMIT 1;
2001
2002     IF eg_id IS NULL THEN
2003         -- RAISE NOTICE 'incoming record is not of high enough quality';
2004         RETURN FALSE;
2005     END IF;
2006
2007     RETURN vandelay.overlay_bib_record( import_id, eg_id, merge_profile_id );
2008 END;
2009 $$ LANGUAGE PLPGSQL;
2010
2011
2012 CREATE OR REPLACE FUNCTION vandelay.auto_overlay_bib_queue_with_best ( queue_id BIGINT, merge_profile_id INT, lwm_ratio_value NUMERIC ) RETURNS SETOF BIGINT AS $$
2013 DECLARE
2014     queued_record   vandelay.queued_bib_record%ROWTYPE;
2015 BEGIN
2016
2017     FOR queued_record IN SELECT * FROM vandelay.queued_bib_record WHERE queue = queue_id AND import_time IS NULL LOOP
2018
2019         IF vandelay.auto_overlay_bib_record_with_best( queued_record.id, merge_profile_id, lwm_ratio_value ) THEN
2020             RETURN NEXT queued_record.id;
2021         END IF;
2022
2023     END LOOP;
2024
2025     RETURN;
2026     
2027 END;
2028 $$ LANGUAGE PLPGSQL;
2029
2030 CREATE OR REPLACE FUNCTION vandelay.auto_overlay_bib_queue_with_best ( import_id BIGINT, merge_profile_id INT ) RETURNS SETOF BIGINT AS $$
2031     SELECT vandelay.auto_overlay_bib_queue_with_best( $1, $2, p.lwm_ratio ) FROM vandelay.merge_profile p WHERE id = $2;
2032 $$ LANGUAGE SQL;
2033
2034 CREATE OR REPLACE FUNCTION vandelay.ingest_bib_marc ( ) RETURNS TRIGGER AS $$
2035 DECLARE
2036     value   TEXT;
2037     atype   TEXT;
2038     adef    RECORD;
2039 BEGIN
2040     IF TG_OP IN ('INSERT','UPDATE') AND NEW.imported_as IS NOT NULL THEN
2041         RETURN NEW;
2042     END IF;
2043
2044     FOR adef IN SELECT * FROM vandelay.bib_attr_definition LOOP
2045
2046         SELECT extract_marc_field('vandelay.queued_bib_record', id, adef.xpath, adef.remove) INTO value FROM vandelay.queued_bib_record WHERE id = NEW.id;
2047         IF (value IS NOT NULL AND value <> '') THEN
2048             INSERT INTO vandelay.queued_bib_record_attr (record, field, attr_value) VALUES (NEW.id, adef.id, value);
2049         END IF;
2050
2051     END LOOP;
2052
2053     RETURN NULL;
2054 END;
2055 $$ LANGUAGE PLPGSQL;
2056
2057 CREATE OR REPLACE FUNCTION vandelay.ingest_bib_items ( ) RETURNS TRIGGER AS $func$
2058 DECLARE
2059     attr_def    BIGINT;
2060     item_data   vandelay.import_item%ROWTYPE;
2061 BEGIN
2062
2063     IF TG_OP IN ('INSERT','UPDATE') AND NEW.imported_as IS NOT NULL THEN
2064         RETURN NEW;
2065     END IF;
2066
2067     SELECT item_attr_def INTO attr_def FROM vandelay.bib_queue WHERE id = NEW.queue;
2068
2069     FOR item_data IN SELECT * FROM vandelay.ingest_items( NEW.id::BIGINT, attr_def ) LOOP
2070         INSERT INTO vandelay.import_item (
2071             record,
2072             definition,
2073             owning_lib,
2074             circ_lib,
2075             call_number,
2076             copy_number,
2077             status,
2078             location,
2079             circulate,
2080             deposit,
2081             deposit_amount,
2082             ref,
2083             holdable,
2084             price,
2085             barcode,
2086             circ_modifier,
2087             circ_as_type,
2088             alert_message,
2089             pub_note,
2090             priv_note,
2091             opac_visible
2092         ) VALUES (
2093             NEW.id,
2094             item_data.definition,
2095             item_data.owning_lib,
2096             item_data.circ_lib,
2097             item_data.call_number,
2098             item_data.copy_number,
2099             item_data.status,
2100             item_data.location,
2101             item_data.circulate,
2102             item_data.deposit,
2103             item_data.deposit_amount,
2104             item_data.ref,
2105             item_data.holdable,
2106             item_data.price,
2107             item_data.barcode,
2108             item_data.circ_modifier,
2109             item_data.circ_as_type,
2110             item_data.alert_message,
2111             item_data.pub_note,
2112             item_data.priv_note,
2113             item_data.opac_visible
2114         );
2115     END LOOP;
2116
2117     RETURN NULL;
2118 END;
2119 $func$ LANGUAGE PLPGSQL;
2120
2121 CREATE OR REPLACE FUNCTION vandelay.cleanup_bib_marc ( ) RETURNS TRIGGER AS $$
2122 BEGIN
2123     IF TG_OP IN ('INSERT','UPDATE') AND NEW.imported_as IS NOT NULL THEN
2124         RETURN NEW;
2125     END IF;
2126
2127     DELETE FROM vandelay.queued_bib_record_attr WHERE record = OLD.id;
2128     DELETE FROM vandelay.import_item WHERE record = OLD.id;
2129
2130     IF TG_OP = 'UPDATE' THEN
2131         RETURN NEW;
2132     END IF;
2133     RETURN OLD;
2134 END;
2135 $$ LANGUAGE PLPGSQL;
2136
2137 -- ALTER TABLEs...
2138
2139 DROP TRIGGER zz_match_bibs_trigger ON vandelay.queued_bib_record;
2140 CREATE TRIGGER zz_match_bibs_trigger
2141     BEFORE INSERT OR UPDATE ON vandelay.queued_bib_record
2142     FOR EACH ROW EXECUTE PROCEDURE vandelay.match_bib_record();
2143
2144 CREATE OR REPLACE FUNCTION vandelay.ingest_authority_marc ( ) RETURNS TRIGGER AS $$
2145 DECLARE
2146     value   TEXT;
2147     atype   TEXT;
2148     adef    RECORD;
2149 BEGIN
2150     IF TG_OP IN ('INSERT','UPDATE') AND NEW.imported_as IS NOT NULL THEN
2151         RETURN NEW;
2152     END IF;
2153
2154     FOR adef IN SELECT * FROM vandelay.authority_attr_definition LOOP
2155
2156         SELECT extract_marc_field('vandelay.queued_authority_record', id, adef.xpath, adef.remove) INTO value FROM vandelay.queued_authority_record WHERE id = NEW.id;
2157         IF (value IS NOT NULL AND value <> '') THEN
2158             INSERT INTO vandelay.queued_authority_record_attr (record, field, attr_value) VALUES (NEW.id, adef.id, value);
2159         END IF;
2160
2161     END LOOP;
2162
2163     RETURN NULL;
2164 END;
2165 $$ LANGUAGE PLPGSQL;
2166
2167 ALTER TABLE vandelay.authority_attr_definition DROP COLUMN ident;
2168 ALTER TABLE vandelay.queued_authority_record
2169     ADD COLUMN import_error TEXT REFERENCES vandelay.import_error (code) ON DELETE SET NULL ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
2170     ADD COLUMN error_detail TEXT;
2171
2172 ALTER TABLE vandelay.authority_match DROP COLUMN matched_attr;
2173
2174 CREATE OR REPLACE FUNCTION vandelay.cleanup_authority_marc ( ) RETURNS TRIGGER AS $$
2175 BEGIN
2176     IF TG_OP IN ('INSERT','UPDATE') AND NEW.imported_as IS NOT NULL THEN
2177         RETURN NEW;
2178     END IF;
2179
2180     DELETE FROM vandelay.queued_authority_record_attr WHERE record = OLD.id;
2181     IF TG_OP = 'UPDATE' THEN
2182         RETURN NEW;
2183     END IF;
2184     RETURN OLD;
2185 END;
2186 $$ LANGUAGE PLPGSQL;
2187
2188 CREATE OR REPLACE FUNCTION authority.flatten_marc ( rid BIGINT ) RETURNS SETOF authority.full_rec AS $func$
2189 DECLARE
2190         auth    authority.record_entry%ROWTYPE;
2191         output  authority.full_rec%ROWTYPE;
2192         field   RECORD;
2193 BEGIN
2194         SELECT INTO auth * FROM authority.record_entry WHERE id = rid;
2195
2196         FOR field IN SELECT * FROM vandelay.flatten_marc( auth.marc ) LOOP
2197                 output.record := rid;
2198                 output.ind1 := field.ind1;
2199                 output.ind2 := field.ind2;
2200                 output.tag := field.tag;
2201                 output.subfield := field.subfield;
2202                 output.value := field.value;
2203
2204                 RETURN NEXT output;
2205         END LOOP;
2206 END;
2207 $func$ LANGUAGE PLPGSQL;
2208
2209 CREATE OR REPLACE FUNCTION biblio.flatten_marc ( rid BIGINT ) RETURNS SETOF metabib.full_rec AS $func$
2210 DECLARE
2211         bib     biblio.record_entry%ROWTYPE;
2212         output  metabib.full_rec%ROWTYPE;
2213         field   RECORD;
2214 BEGIN
2215         SELECT INTO bib * FROM biblio.record_entry WHERE id = rid;
2216
2217         FOR field IN SELECT * FROM vandelay.flatten_marc( bib.marc ) LOOP
2218                 output.record := rid;
2219                 output.ind1 := field.ind1;
2220                 output.ind2 := field.ind2;
2221                 output.tag := field.tag;
2222                 output.subfield := field.subfield;
2223                 output.value := field.value;
2224
2225                 RETURN NEXT output;
2226         END LOOP;
2227 END;
2228 $func$ LANGUAGE PLPGSQL;
2229
2230 -----------------------------------------------
2231 -- Seed data for import errors
2232 -----------------------------------------------
2233
2234 INSERT INTO vandelay.import_error ( code, description ) VALUES ( 'general.unknown', oils_i18n_gettext('general.unknown', 'Import or Overlay failed', 'vie', 'description') );
2235 INSERT INTO vandelay.import_error ( code, description ) VALUES ( 'import.item.duplicate.barcode', oils_i18n_gettext('import.item.duplicate.barcode', 'Import failed due to barcode collision', 'vie', 'description') );
2236 INSERT INTO vandelay.import_error ( code, description ) VALUES ( 'import.item.invalid.circ_modifier', oils_i18n_gettext('import.item.invalid.circ_modifier', 'Import failed due to invalid circulation modifier', 'vie', 'description') );
2237 INSERT INTO vandelay.import_error ( code, description ) VALUES ( 'import.item.invalid.location', oils_i18n_gettext('import.item.invalid.location', 'Import failed due to invalid copy location', 'vie', 'description') );
2238 INSERT INTO vandelay.import_error ( code, description ) VALUES ( 'import.duplicate.sysid', oils_i18n_gettext('import.duplicate.sysid', 'Import failed due to system id collision', 'vie', 'description') );
2239 INSERT INTO vandelay.import_error ( code, description ) VALUES ( 'import.duplicate.tcn', oils_i18n_gettext('import.duplicate.sysid', 'Import failed due to system id collision', 'vie', 'description') );
2240 INSERT INTO vandelay.import_error ( code, description ) VALUES ( 'overlay.missing.sysid', oils_i18n_gettext('overlay.missing.sysid', 'Overlay failed due to missing system id', 'vie', 'description') );
2241 INSERT INTO vandelay.import_error ( code, description ) VALUES ( 'import.auth.duplicate.acn', oils_i18n_gettext('import.auth.duplicate.acn', 'Import failed due to Accession Number collision', 'vie', 'description') );
2242 INSERT INTO vandelay.import_error ( code, description ) VALUES ( 'import.xml.malformed', oils_i18n_gettext('import.xml.malformed', 'Malformed record cause Import failure', 'vie', 'description') );
2243 INSERT INTO vandelay.import_error ( code, description ) VALUES ( 'overlay.xml.malformed', oils_i18n_gettext('overlay.xml.malformed', 'Malformed record cause Overlay failure', 'vie', 'description') );
2244 INSERT INTO vandelay.import_error ( code, description ) VALUES ( 'overlay.record.quality', oils_i18n_gettext('overlay.record.quality', 'New record had insufficient quality', 'vie', 'description') );
2245
2246
2247 ----------------------------------------------------------------
2248 -- Seed data for queued record/item exports
2249 ----------------------------------------------------------------
2250
2251 INSERT INTO action_trigger.hook (key,core_type,description,passive) VALUES (
2252         'vandelay.queued_bib_record.print',
2253         'vqbr', 
2254         oils_i18n_gettext(
2255             'vandelay.queued_bib_record.print',
2256             'Print output has been requested for records in an Importer Bib Queue.',
2257             'ath',
2258             'description'
2259         ), 
2260         FALSE
2261     )
2262     ,(
2263         'vandelay.queued_bib_record.csv',
2264         'vqbr', 
2265         oils_i18n_gettext(
2266             'vandelay.queued_bib_record.csv',
2267             'CSV output has been requested for records in an Importer Bib Queue.',
2268             'ath',
2269             'description'
2270         ), 
2271         FALSE
2272     )
2273     ,(
2274         'vandelay.queued_bib_record.email',
2275         'vqbr', 
2276         oils_i18n_gettext(
2277             'vandelay.queued_bib_record.email',
2278             'An email has been requested for records in an Importer Bib Queue.',
2279             'ath',
2280             'description'
2281         ), 
2282         FALSE
2283     )
2284     ,(
2285         'vandelay.queued_auth_record.print',
2286         'vqar', 
2287         oils_i18n_gettext(
2288             'vandelay.queued_auth_record.print',
2289             'Print output has been requested for records in an Importer Authority Queue.',
2290             'ath',
2291             'description'
2292         ), 
2293         FALSE
2294     )
2295     ,(
2296         'vandelay.queued_auth_record.csv',
2297         'vqar', 
2298         oils_i18n_gettext(
2299             'vandelay.queued_auth_record.csv',
2300             'CSV output has been requested for records in an Importer Authority Queue.',
2301             'ath',
2302             'description'
2303         ), 
2304         FALSE
2305     )
2306     ,(
2307         'vandelay.queued_auth_record.email',
2308         'vqar', 
2309         oils_i18n_gettext(
2310             'vandelay.queued_auth_record.email',
2311             'An email has been requested for records in an Importer Authority Queue.',
2312             'ath',
2313             'description'
2314         ), 
2315         FALSE
2316     )
2317     ,(
2318         'vandelay.import_items.print',
2319         'vii', 
2320         oils_i18n_gettext(
2321             'vandelay.import_items.print',
2322             'Print output has been requested for Import Items from records in an Importer Bib Queue.',
2323             'ath',
2324             'description'
2325         ), 
2326         FALSE
2327     )
2328     ,(
2329         'vandelay.import_items.csv',
2330         'vii', 
2331         oils_i18n_gettext(
2332             'vandelay.import_items.csv',
2333             'CSV output has been requested for Import Items from records in an Importer Bib Queue.',
2334             'ath',
2335             'description'
2336         ), 
2337         FALSE
2338     )
2339     ,(
2340         'vandelay.import_items.email',
2341         'vii', 
2342         oils_i18n_gettext(
2343             'vandelay.import_items.email',
2344             'An email has been requested for Import Items from records in an Importer Bib Queue.',
2345             'ath',
2346             'description'
2347         ), 
2348         FALSE
2349     )
2350 ;
2351
2352 INSERT INTO action_trigger.event_definition (
2353         id,
2354         active,
2355         owner,
2356         name,
2357         hook,
2358         validator,
2359         reactor,
2360         group_field,
2361         granularity,
2362         template
2363     ) VALUES (
2364         39,
2365         TRUE,
2366         1,
2367         'Print Output for Queued Bib Records',
2368         'vandelay.queued_bib_record.print',
2369         'NOOP_True',
2370         'ProcessTemplate',
2371         'queue.owner',
2372         'print-on-demand',
2373 $$
2374 [%- USE date -%]
2375 <pre>
2376 Queue ID: [% target.0.queue.id %]
2377 Queue Name: [% target.0.queue.name %]
2378 Queue Type: [% target.0.queue.queue_type %]
2379 Complete? [% target.0.queue.complete %]
2380
2381     [% FOR vqbr IN target %]
2382 =-=-=
2383  Title of work    | [% helpers.get_queued_bib_attr('title',vqbr.attributes) %]
2384  Author of work   | [% helpers.get_queued_bib_attr('author',vqbr.attributes) %]
2385  Language of work | [% helpers.get_queued_bib_attr('language',vqbr.attributes) %]
2386  Pagination       | [% helpers.get_queued_bib_attr('pagination',vqbr.attributes) %]
2387  ISBN             | [% helpers.get_queued_bib_attr('isbn',vqbr.attributes) %]
2388  ISSN             | [% helpers.get_queued_bib_attr('issn',vqbr.attributes) %]
2389  Price            | [% helpers.get_queued_bib_attr('price',vqbr.attributes) %]
2390  Accession Number | [% helpers.get_queued_bib_attr('rec_identifier',vqbr.attributes) %]
2391  TCN Value        | [% helpers.get_queued_bib_attr('eg_tcn',vqbr.attributes) %]
2392  TCN Source       | [% helpers.get_queued_bib_attr('eg_tcn_source',vqbr.attributes) %]
2393  Internal ID      | [% helpers.get_queued_bib_attr('eg_identifier',vqbr.attributes) %]
2394  Publisher        | [% helpers.get_queued_bib_attr('publisher',vqbr.attributes) %]
2395  Publication Date | [% helpers.get_queued_bib_attr('pubdate',vqbr.attributes) %]
2396  Edition          | [% helpers.get_queued_bib_attr('edition',vqbr.attributes) %]
2397  Item Barcode     | [% helpers.get_queued_bib_attr('item_barcode',vqbr.attributes) %]
2398
2399     [% END %]
2400 </pre>
2401 $$
2402     )
2403 ;
2404
2405 INSERT INTO action_trigger.environment ( event_def, path) VALUES (
2406     39, 'attributes')
2407     ,( 39, 'queue')
2408 ;
2409
2410 INSERT INTO action_trigger.event_definition (
2411         id,
2412         active,
2413         owner,
2414         name,
2415         hook,
2416         validator,
2417         reactor,
2418         group_field,
2419         granularity,
2420         template
2421     ) VALUES (
2422         40,
2423         TRUE,
2424         1,
2425         'CSV Output for Queued Bib Records',
2426         'vandelay.queued_bib_record.csv',
2427         'NOOP_True',
2428         'ProcessTemplate',
2429         'queue.owner',
2430         'print-on-demand',
2431 $$
2432 [%- USE date -%]
2433 "Title of work","Author of work","Language of work","Pagination","ISBN","ISSN","Price","Accession Number","TCN Value","TCN Source","Internal ID","Publisher","Publication Date","Edition","Item Barcode"
2434 [% FOR vqbr IN target %]"[% helpers.get_queued_bib_attr('title',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('author',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('language',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('pagination',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('isbn',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('issn',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('price',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('rec_identifier',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('eg_tcn',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('eg_tcn_source',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('eg_identifier',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('publisher',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('pubdate',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('edition',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('item_barcode',vqbr.attributes) | replace('"', '""') %]"
2435 [% END %]
2436 $$
2437     )
2438 ;
2439
2440 INSERT INTO action_trigger.environment ( event_def, path) VALUES (
2441     40, 'attributes')
2442     ,( 40, 'queue')
2443 ;
2444
2445 INSERT INTO action_trigger.event_definition (
2446         id,
2447         active,
2448         owner,
2449         name,
2450         hook,
2451         validator,
2452         reactor,
2453         group_field,
2454         granularity,
2455         template
2456     ) VALUES (
2457         41,
2458         TRUE,
2459         1,
2460         'Email Output for Queued Bib Records',
2461         'vandelay.queued_bib_record.email',
2462         'NOOP_True',
2463         'SendEmail',
2464         'queue.owner',
2465         NULL,
2466 $$
2467 [%- USE date -%]
2468 [%- SET user = target.0.queue.owner -%]
2469 To: [%- params.recipient_email || user.email || 'root@localhost' %]
2470 From: [%- params.sender_email || default_sender %]
2471 Subject: Bibs from Import Queue
2472
2473 Queue ID: [% target.0.queue.id %]
2474 Queue Name: [% target.0.queue.name %]
2475 Queue Type: [% target.0.queue.queue_type %]
2476 Complete? [% target.0.queue.complete %]
2477
2478     [% FOR vqbr IN target %]
2479 =-=-=
2480  Title of work    | [% helpers.get_queued_bib_attr('title',vqbr.attributes) %]
2481  Author of work   | [% helpers.get_queued_bib_attr('author',vqbr.attributes) %]
2482  Language of work | [% helpers.get_queued_bib_attr('language',vqbr.attributes) %]
2483  Pagination       | [% helpers.get_queued_bib_attr('pagination',vqbr.attributes) %]
2484  ISBN             | [% helpers.get_queued_bib_attr('isbn',vqbr.attributes) %]
2485  ISSN             | [% helpers.get_queued_bib_attr('issn',vqbr.attributes) %]
2486  Price            | [% helpers.get_queued_bib_attr('price',vqbr.attributes) %]
2487  Accession Number | [% helpers.get_queued_bib_attr('rec_identifier',vqbr.attributes) %]
2488  TCN Value        | [% helpers.get_queued_bib_attr('eg_tcn',vqbr.attributes) %]
2489  TCN Source       | [% helpers.get_queued_bib_attr('eg_tcn_source',vqbr.attributes) %]
2490  Internal ID      | [% helpers.get_queued_bib_attr('eg_identifier',vqbr.attributes) %]
2491  Publisher        | [% helpers.get_queued_bib_attr('publisher',vqbr.attributes) %]
2492  Publication Date | [% helpers.get_queued_bib_attr('pubdate',vqbr.attributes) %]
2493  Edition          | [% helpers.get_queued_bib_attr('edition',vqbr.attributes) %]
2494  Item Barcode     | [% helpers.get_queued_bib_attr('item_barcode',vqbr.attributes) %]
2495
2496     [% END %]
2497
2498 $$
2499     )
2500 ;
2501
2502 INSERT INTO action_trigger.environment ( event_def, path) VALUES (
2503     41, 'attributes')
2504     ,( 41, 'queue')
2505     ,( 41, 'queue.owner')
2506 ;
2507
2508 INSERT INTO action_trigger.event_definition (
2509         id,
2510         active,
2511         owner,
2512         name,
2513         hook,
2514         validator,
2515         reactor,
2516         group_field,
2517         granularity,
2518         template
2519     ) VALUES (
2520         42,
2521         TRUE,
2522         1,
2523         'Print Output for Queued Authority Records',
2524         'vandelay.queued_auth_record.print',
2525         'NOOP_True',
2526         'ProcessTemplate',
2527         'queue.owner',
2528         'print-on-demand',
2529 $$
2530 [%- USE date -%]
2531 <pre>
2532 Queue ID: [% target.0.queue.id %]
2533 Queue Name: [% target.0.queue.name %]
2534 Queue Type: [% target.0.queue.queue_type %]
2535 Complete? [% target.0.queue.complete %]
2536
2537     [% FOR vqar IN target %]
2538 =-=-=
2539  Record Identifier | [% helpers.get_queued_auth_attr('rec_identifier',vqar.attributes) %]
2540
2541     [% END %]
2542 </pre>
2543 $$
2544     )
2545 ;
2546
2547 INSERT INTO action_trigger.environment ( event_def, path) VALUES (
2548     42, 'attributes')
2549     ,( 42, 'queue')
2550 ;
2551
2552 INSERT INTO action_trigger.event_definition (
2553         id,
2554         active,
2555         owner,
2556         name,
2557         hook,
2558         validator,
2559         reactor,
2560         group_field,
2561         granularity,
2562         template
2563     ) VALUES (
2564         43,
2565         TRUE,
2566         1,
2567         'CSV Output for Queued Authority Records',
2568         'vandelay.queued_auth_record.csv',
2569         'NOOP_True',
2570         'ProcessTemplate',
2571         'queue.owner',
2572         'print-on-demand',
2573 $$
2574 [%- USE date -%]
2575 "Record Identifier"
2576 [% FOR vqar IN target %]"[% helpers.get_queued_auth_attr('rec_identifier',vqar.attributes) | replace('"', '""') %]"
2577 [% END %]
2578 $$
2579     )
2580 ;
2581
2582 INSERT INTO action_trigger.environment ( event_def, path) VALUES (
2583     43, 'attributes')
2584     ,( 43, 'queue')
2585 ;
2586
2587 INSERT INTO action_trigger.event_definition (
2588         id,
2589         active,
2590         owner,
2591         name,
2592         hook,
2593         validator,
2594         reactor,
2595         group_field,
2596         granularity,
2597         template
2598     ) VALUES (
2599         44,
2600         TRUE,
2601         1,
2602         'Email Output for Queued Authority Records',
2603         'vandelay.queued_auth_record.email',
2604         'NOOP_True',
2605         'SendEmail',
2606         'queue.owner',
2607         NULL,
2608 $$
2609 [%- USE date -%]
2610 [%- SET user = target.0.queue.owner -%]
2611 To: [%- params.recipient_email || user.email || 'root@localhost' %]
2612 From: [%- params.sender_email || default_sender %]
2613 Subject: Authorities from Import Queue
2614
2615 Queue ID: [% target.0.queue.id %]
2616 Queue Name: [% target.0.queue.name %]
2617 Queue Type: [% target.0.queue.queue_type %]
2618 Complete? [% target.0.queue.complete %]
2619
2620     [% FOR vqar IN target %]
2621 =-=-=
2622  Record Identifier | [% helpers.get_queued_auth_attr('rec_identifier',vqar.attributes) %]
2623
2624     [% END %]
2625
2626 $$
2627     )
2628 ;
2629
2630 INSERT INTO action_trigger.environment ( event_def, path) VALUES (
2631     44, 'attributes')
2632     ,( 44, 'queue')
2633     ,( 44, 'queue.owner')
2634 ;
2635
2636 INSERT INTO action_trigger.event_definition (
2637         id,
2638         active,
2639         owner,
2640         name,
2641         hook,
2642         validator,
2643         reactor,
2644         group_field,
2645         granularity,
2646         template
2647     ) VALUES (
2648         45,
2649         TRUE,
2650         1,
2651         'Print Output for Import Items from Queued Bib Records',
2652         'vandelay.import_items.print',
2653         'NOOP_True',
2654         'ProcessTemplate',
2655         'record.queue.owner',
2656         'print-on-demand',
2657 $$
2658 [%- USE date -%]
2659 <pre>
2660 Queue ID: [% target.0.record.queue.id %]
2661 Queue Name: [% target.0.record.queue.name %]
2662 Queue Type: [% target.0.record.queue.queue_type %]
2663 Complete? [% target.0.record.queue.complete %]
2664
2665     [% FOR vii IN target %]
2666 =-=-=
2667  Import Item ID         | [% vii.id %]
2668  Title of work          | [% helpers.get_queued_bib_attr('title',vii.record.attributes) %]
2669  ISBN                   | [% helpers.get_queued_bib_attr('isbn',vii.record.attributes) %]
2670  Attribute Definition   | [% vii.definition %]
2671  Import Error           | [% vii.import_error %]
2672  Import Error Detail    | [% vii.error_detail %]
2673  Owning Library         | [% vii.owning_lib %]
2674  Circulating Library    | [% vii.circ_lib %]
2675  Call Number            | [% vii.call_number %]
2676  Copy Number            | [% vii.copy_number %]
2677  Status                 | [% vii.status.name %]
2678  Shelving Location      | [% vii.location.name %]
2679  Circulate              | [% vii.circulate %]
2680  Deposit                | [% vii.deposit %]
2681  Deposit Amount         | [% vii.deposit_amount %]
2682  Reference              | [% vii.ref %]
2683  Holdable               | [% vii.holdable %]
2684  Price                  | [% vii.price %]
2685  Barcode                | [% vii.barcode %]
2686  Circulation Modifier   | [% vii.circ_modifier %]
2687  Circulate As MARC Type | [% vii.circ_as_type %]
2688  Alert Message          | [% vii.alert_message %]
2689  Public Note            | [% vii.pub_note %]
2690  Private Note           | [% vii.priv_note %]
2691  OPAC Visible           | [% vii.opac_visible %]
2692
2693     [% END %]
2694 </pre>
2695 $$
2696     )
2697 ;
2698
2699 INSERT INTO action_trigger.environment ( event_def, path) VALUES (
2700     45, 'record')
2701     ,( 45, 'record.attributes')
2702     ,( 45, 'record.queue')
2703     ,( 45, 'record.queue.owner')
2704 ;
2705
2706 INSERT INTO action_trigger.event_definition (
2707         id,
2708         active,
2709         owner,
2710         name,
2711         hook,
2712         validator,
2713         reactor,
2714         group_field,
2715         granularity,
2716         template
2717     ) VALUES (
2718         46,
2719         TRUE,
2720         1,
2721         'CSV Output for Import Items from Queued Bib Records',
2722         'vandelay.import_items.csv',
2723         'NOOP_True',
2724         'ProcessTemplate',
2725         'record.queue.owner',
2726         'print-on-demand',
2727 $$
2728 [%- USE date -%]
2729 "Import Item ID","Title of work","ISBN","Attribute Definition","Import Error","Import Error Detail","Owning Library","Circulating Library","Call Number","Copy Number","Status","Shelving Location","Circulate","Deposit","Deposit Amount","Reference","Holdable","Price","Barcode","Circulation Modifier","Circulate As MARC Type","Alert Message","Public Note","Private Note","OPAC Visible"
2730 [% FOR vii IN target %]"[% vii.id | replace('"', '""') %]","[% helpers.get_queued_bib_attr('title',vii.record.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('isbn',vii.record.attributes) | replace('"', '""') %]","[% vii.definition | replace('"', '""') %]","[% vii.import_error | replace('"', '""') %]","[% vii.error_detail | replace('"', '""') %]","[% vii.owning_lib | replace('"', '""') %]","[% vii.circ_lib | replace('"', '""') %]","[% vii.call_number | replace('"', '""') %]","[% vii.copy_number | replace('"', '""') %]","[% vii.status.name | replace('"', '""') %]","[% vii.location.name | replace('"', '""') %]","[% vii.circulate | replace('"', '""') %]","[% vii.deposit | replace('"', '""') %]","[% vii.deposit_amount | replace('"', '""') %]","[% vii.ref | replace('"', '""') %]","[% vii.holdable | replace('"', '""') %]","[% vii.price | replace('"', '""') %]","[% vii.barcode | replace('"', '""') %]","[% vii.circ_modifier | replace('"', '""') %]","[% vii.circ_as_type | replace('"', '""') %]","[% vii.alert_message | replace('"', '""') %]","[% vii.pub_note | replace('"', '""') %]","[% vii.priv_note | replace('"', '""') %]","[% vii.opac_visible | replace('"', '""') %]"
2731 [% END %]
2732 $$
2733     )
2734 ;
2735
2736 INSERT INTO action_trigger.environment ( event_def, path) VALUES (
2737     46, 'record')
2738     ,( 46, 'record.attributes')
2739     ,( 46, 'record.queue')
2740     ,( 46, 'record.queue.owner')
2741 ;
2742
2743 INSERT INTO action_trigger.event_definition (
2744         id,
2745         active,
2746         owner,
2747         name,
2748         hook,
2749         validator,
2750         reactor,
2751         group_field,
2752         granularity,
2753         template
2754     ) VALUES (
2755         47,
2756         TRUE,
2757         1,
2758         'Email Output for Import Items from Queued Bib Records',
2759         'vandelay.import_items.email',
2760         'NOOP_True',
2761         'SendEmail',
2762         'record.queue.owner',
2763         NULL,
2764 $$
2765 [%- USE date -%]
2766 [%- SET user = target.0.record.queue.owner -%]
2767 To: [%- params.recipient_email || user.email || 'root@localhost' %]
2768 From: [%- params.sender_email || default_sender %]
2769 Subject: Import Items from Import Queue
2770
2771 Queue ID: [% target.0.record.queue.id %]
2772 Queue Name: [% target.0.record.queue.name %]
2773 Queue Type: [% target.0.record.queue.queue_type %]
2774 Complete? [% target.0.record.queue.complete %]
2775
2776     [% FOR vii IN target %]
2777 =-=-=
2778  Import Item ID         | [% vii.id %]
2779  Title of work          | [% helpers.get_queued_bib_attr('title',vii.record.attributes) %]
2780  ISBN                   | [% helpers.get_queued_bib_attr('isbn',vii.record.attributes) %]
2781  Attribute Definition   | [% vii.definition %]
2782  Import Error           | [% vii.import_error %]
2783  Import Error Detail    | [% vii.error_detail %]
2784  Owning Library         | [% vii.owning_lib %]
2785  Circulating Library    | [% vii.circ_lib %]
2786  Call Number            | [% vii.call_number %]
2787  Copy Number            | [% vii.copy_number %]
2788  Status                 | [% vii.status.name %]
2789  Shelving Location      | [% vii.location.name %]
2790  Circulate              | [% vii.circulate %]
2791  Deposit                | [% vii.deposit %]
2792  Deposit Amount         | [% vii.deposit_amount %]
2793  Reference              | [% vii.ref %]
2794  Holdable               | [% vii.holdable %]
2795  Price                  | [% vii.price %]
2796  Barcode                | [% vii.barcode %]
2797  Circulation Modifier   | [% vii.circ_modifier %]
2798  Circulate As MARC Type | [% vii.circ_as_type %]
2799  Alert Message          | [% vii.alert_message %]
2800  Public Note            | [% vii.pub_note %]
2801  Private Note           | [% vii.priv_note %]
2802  OPAC Visible           | [% vii.opac_visible %]
2803
2804     [% END %]
2805 $$
2806     )
2807 ;
2808
2809 INSERT INTO action_trigger.environment ( event_def, path) VALUES (
2810     47, 'record')
2811     ,( 47, 'record.attributes')
2812     ,( 47, 'record.queue')
2813     ,( 47, 'record.queue.owner')
2814 ;
2815
2816
2817
2818 SELECT evergreen.upgrade_deps_block_check('0574', :eg_version);
2819
2820 UPDATE action_trigger.event_definition SET template =
2821 $$
2822 [%- USE date -%]
2823 <style>
2824     table { border-collapse: collapse; }
2825     td { padding: 5px; border-bottom: 1px solid #888; }
2826     th { font-weight: bold; }
2827 </style>
2828 [%
2829     # Sort the holds into copy-location buckets
2830     # In the main print loop, sort each bucket by callnumber before printing
2831     SET holds_list = [];
2832     SET loc_data = [];
2833     SET current_location = target.0.current_copy.location.id;
2834     FOR hold IN target;
2835         IF current_location != hold.current_copy.location.id;
2836             SET current_location = hold.current_copy.location.id;
2837             holds_list.push(loc_data);
2838             SET loc_data = [];
2839         END;
2840         SET hold_data = {
2841             'hold' => hold,
2842             'callnumber' => hold.current_copy.call_number.label
2843         };
2844         loc_data.push(hold_data);
2845     END;
2846     holds_list.push(loc_data)
2847 %]
2848 <table>
2849     <thead>
2850         <tr>
2851             <th>Title</th>
2852             <th>Author</th>
2853             <th>Shelving Location</th>
2854             <th>Call Number</th>
2855             <th>Barcode/Part</th>
2856             <th>Patron</th>
2857         </tr>
2858     </thead>
2859     <tbody>
2860     [% FOR loc_data IN holds_list  %]
2861         [% FOR hold_data IN loc_data.sort('callnumber') %]
2862             [%
2863                 SET hold = hold_data.hold;
2864                 SET copy_data = helpers.get_copy_bib_basics(hold.current_copy.id);
2865             %]
2866             <tr>
2867                 <td>[% copy_data.title | truncate %]</td>
2868                 <td>[% copy_data.author | truncate %]</td>
2869                 <td>[% hold.current_copy.location.name %]</td>
2870                 <td>[% hold.current_copy.call_number.label %]</td>
2871                 <td>[% hold.current_copy.barcode %]
2872                     [% FOR part IN hold.current_copy.parts %]
2873                        [% part.part.label %]
2874                     [% END %]
2875                 </td>
2876                 <td>[% hold.usr.card.barcode %]</td>
2877             </tr>
2878         [% END %]
2879     [% END %]
2880     <tbody>
2881 </table>
2882 $$
2883     WHERE id = 35;
2884
2885 INSERT INTO action_trigger.environment (
2886         event_def,
2887         path
2888     ) VALUES
2889         (35, 'current_copy.parts'),
2890         (35, 'current_copy.parts.part')
2891 ;
2892
2893
2894 -- Evergreen DB patch XXXX.schema.authority-control-sets.sql
2895 --
2896 -- Schema upgrade to add Authority Control Set functionality
2897 --
2898
2899
2900 -- check whether patch can be applied
2901 SELECT evergreen.upgrade_deps_block_check('0575', :eg_version);
2902
2903 CREATE TABLE authority.control_set (
2904     id          SERIAL  PRIMARY KEY,
2905     name        TEXT    NOT NULL UNIQUE, -- i18n
2906     description TEXT                     -- i18n
2907 );
2908
2909 CREATE TABLE authority.control_set_authority_field (
2910     id          SERIAL  PRIMARY KEY,
2911     main_entry  INT     REFERENCES authority.control_set_authority_field (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
2912     control_set INT     NOT NULL REFERENCES authority.control_set (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
2913     tag         CHAR(3) NOT NULL,
2914     sf_list     TEXT    NOT NULL,
2915     name        TEXT    NOT NULL, -- i18n
2916     description TEXT              -- i18n
2917 );
2918
2919 CREATE TABLE authority.control_set_bib_field (
2920     id              SERIAL  PRIMARY KEY,
2921     authority_field INT     NOT NULL REFERENCES authority.control_set_authority_field (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
2922     tag             CHAR(3) NOT NULL
2923 );
2924
2925 CREATE TABLE authority.thesaurus (
2926     code        TEXT    PRIMARY KEY,     -- MARC21 thesaurus code
2927     control_set INT     NOT NULL REFERENCES authority.control_set (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
2928     name        TEXT    NOT NULL UNIQUE, -- i18n
2929     description TEXT                     -- i18n
2930 );
2931
2932 CREATE TABLE authority.browse_axis (
2933     code        TEXT    PRIMARY KEY,
2934     name        TEXT    UNIQUE NOT NULL, -- i18n
2935     sorter      TEXT    REFERENCES config.record_attr_definition (name) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
2936     description TEXT
2937 );
2938
2939 CREATE TABLE authority.browse_axis_authority_field_map (
2940     id          SERIAL  PRIMARY KEY,
2941     axis        TEXT    NOT NULL REFERENCES authority.browse_axis (code) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
2942     field       INT     NOT NULL REFERENCES authority.control_set_authority_field (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED
2943 );
2944
2945 ALTER TABLE authority.record_entry ADD COLUMN control_set INT REFERENCES authority.control_set (id) ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED;
2946 ALTER TABLE authority.rec_descriptor DROP COLUMN char_encoding, ADD COLUMN encoding_level TEXT, ADD COLUMN thesaurus TEXT;
2947
2948 CREATE INDEX authority_full_rec_value_index ON authority.full_rec (value);
2949 CREATE OR REPLACE RULE protect_authority_rec_delete AS ON DELETE TO authority.record_entry DO INSTEAD (UPDATE authority.record_entry SET deleted = TRUE WHERE OLD.id = authority.record_entry.id; DELETE FROM authority.full_rec WHERE record = OLD.id);
2950  
2951 CREATE OR REPLACE FUNCTION authority.normalize_heading( marcxml TEXT, no_thesaurus BOOL ) RETURNS TEXT AS $func$
2952 DECLARE
2953     acsaf           authority.control_set_authority_field%ROWTYPE;
2954     tag_used        TEXT;
2955     sf              TEXT;
2956     thes_code       TEXT;
2957     cset            INT;
2958     heading_text    TEXT;
2959     tmp_text        TEXT;
2960 BEGIN
2961     thes_code := vandelay.marc21_extract_fixed_field(marcxml,'Subj');
2962     IF thes_code IS NULL THEN
2963         thes_code := '|';
2964     END IF;
2965
2966     SELECT control_set INTO cset FROM authority.thesaurus WHERE code = thes_code;
2967     IF NOT FOUND THEN
2968         cset = 1;
2969     END IF;
2970
2971     heading_text := '';
2972     FOR acsaf IN SELECT * FROM authority.control_set_authority_field WHERE control_set = cset AND main_entry IS NULL LOOP
2973         tag_used := acsaf.tag;
2974         FOR sf IN SELECT * FROM regexp_split_to_table(acsaf.sf_list,'') LOOP
2975             tmp_text := oils_xpath_string('//*[@tag="'||tag_used||'"]/*[@code="'||sf||'"]', marcxml);
2976             IF tmp_text IS NOT NULL AND tmp_text <> '' THEN
2977                 heading_text := heading_text || E'\u2021' || sf || ' ' || tmp_text;
2978             END IF;
2979         END LOOP;
2980         EXIT WHEN heading_text <> '';
2981     END LOOP;
2982  
2983     IF thes_code = 'z' THEN
2984         thes_code := oils_xpath_string('//*[@tag="040"]/*[@code="f"][1]', marcxml);
2985     END IF;
2986
2987     IF heading_text <> '' THEN
2988         IF no_thesaurus IS TRUE THEN
2989             heading_text := tag_used || ' ' || public.naco_normalize(heading_text);
2990         ELSE
2991             heading_text := tag_used || '_' || thes_code || ' ' || public.naco_normalize(heading_text);
2992         END IF;
2993     ELSE
2994         heading_text := 'NOHEADING_' || thes_code || ' ' || MD5(marcxml);
2995     END IF;
2996
2997     RETURN heading_text;
2998 END;
2999 $func$ LANGUAGE PLPGSQL IMMUTABLE;
3000
3001 CREATE OR REPLACE FUNCTION authority.simple_normalize_heading( marcxml TEXT ) RETURNS TEXT AS $func$
3002     SELECT authority.normalize_heading($1, TRUE);
3003 $func$ LANGUAGE SQL IMMUTABLE;
3004
3005 CREATE OR REPLACE FUNCTION authority.normalize_heading( marcxml TEXT ) RETURNS TEXT AS $func$
3006     SELECT authority.normalize_heading($1, FALSE);
3007 $func$ LANGUAGE SQL IMMUTABLE;
3008
3009 CREATE OR REPLACE VIEW authority.tracing_links AS
3010     SELECT  main.record AS record,
3011             main.id AS main_id,
3012             main.tag AS main_tag,
3013             oils_xpath_string('//*[@tag="'||main.tag||'"]/*[local-name()="subfield"]', are.marc) AS main_value,
3014             substr(link.value,1,1) AS relationship,
3015             substr(link.value,2,1) AS use_restriction,
3016             substr(link.value,3,1) AS deprecation,
3017             substr(link.value,4,1) AS display_restriction,
3018             link.id AS link_id,
3019             link.tag AS link_tag,
3020             oils_xpath_string('//*[@tag="'||link.tag||'"]/*[local-name()="subfield"]', are.marc) AS link_value,
3021             authority.normalize_heading(are.marc) AS normalized_main_value
3022       FROM  authority.full_rec main
3023             JOIN authority.record_entry are ON (main.record = are.id)
3024             JOIN authority.control_set_authority_field main_entry
3025                 ON (main_entry.tag = main.tag
3026                     AND main_entry.main_entry IS NULL
3027                     AND main.subfield = 'a' )
3028             JOIN authority.control_set_authority_field sub_entry
3029                 ON (main_entry.id = sub_entry.main_entry)
3030             JOIN authority.full_rec link
3031                 ON (link.record = main.record
3032                     AND link.tag = sub_entry.tag
3033                     AND link.subfield = 'w' );
3034  
3035 CREATE OR REPLACE FUNCTION authority.generate_overlay_template (source_xml TEXT) RETURNS TEXT AS $f$
3036 DECLARE
3037     cset                INT;
3038     main_entry          authority.control_set_authority_field%ROWTYPE;
3039     bib_field           authority.control_set_bib_field%ROWTYPE;
3040     auth_id             INT DEFAULT oils_xpath_string('//*[@tag="901"]/*[local-name()="subfield" and @code="c"]', source_xml)::INT;
3041     replace_data        XML[] DEFAULT '{}'::XML[];
3042     replace_rules       TEXT[] DEFAULT '{}'::TEXT[];
3043     auth_field          XML[];
3044 BEGIN
3045     IF auth_id IS NULL THEN
3046         RETURN NULL;
3047     END IF;
3048
3049     -- Default to the LoC controll set
3050     SELECT COALESCE(control_set,1) INTO cset FROM authority.record_entry WHERE id = auth_id;
3051
3052     FOR main_entry IN SELECT * FROM authority.control_set_authority_field WHERE control_set = cset LOOP
3053         auth_field := XPATH('//*[@tag="'||main_entry.tag||'"][1]',source_xml::XML);
3054         IF ARRAY_LENGTH(auth_field,1) > 0 THEN
3055             FOR bib_field IN SELECT * FROM authority.control_set_bib_field WHERE authority_field = main_entry.id LOOP
3056                 replace_data := replace_data || XMLELEMENT( name datafield, XMLATTRIBUTES(bib_field.tag AS tag), XPATH('//*[local-name()="subfield"]',auth_field[1])::XML[]);
3057                 replace_rules := replace_rules || ( bib_field.tag || main_entry.sf_list || E'[0~\\)' || auth_id || '$]' );
3058             END LOOP;
3059             EXIT;
3060         END IF;
3061     END LOOP;
3062  
3063     RETURN XMLELEMENT(
3064         name record,
3065         XMLATTRIBUTES('http://www.loc.gov/MARC21/slim' AS xmlns),
3066         XMLELEMENT( name leader, '00881nam a2200193   4500'),
3067         replace_data,
3068         XMLELEMENT(
3069             name datafield,
3070             XMLATTRIBUTES( '905' AS tag, ' ' AS ind1, ' ' AS ind2),
3071             XMLELEMENT(
3072                 name subfield,
3073                 XMLATTRIBUTES('r' AS code),
3074                 ARRAY_TO_STRING(replace_rules,',')
3075             )
3076         )
3077     )::TEXT;
3078 END;
3079 $f$ STABLE LANGUAGE PLPGSQL;
3080  
3081 CREATE OR REPLACE FUNCTION authority.generate_overlay_template ( BIGINT ) RETURNS TEXT AS $func$
3082     SELECT authority.generate_overlay_template( marc ) FROM authority.record_entry WHERE id = $1;
3083 $func$ LANGUAGE SQL;
3084  
3085 CREATE OR REPLACE FUNCTION vandelay.add_field ( target_xml TEXT, source_xml TEXT, field TEXT, force_add INT ) RETURNS TEXT AS $_$
3086
3087     use MARC::Record;
3088     use MARC::File::XML (BinaryEncoding => 'UTF-8');
3089     use MARC::Charset;
3090     use strict;
3091
3092     MARC::Charset->assume_unicode(1);
3093
3094     my $target_xml = shift;
3095     my $source_xml = shift;
3096     my $field_spec = shift;
3097     my $force_add = shift || 0;
3098
3099     my $target_r = MARC::Record->new_from_xml( $target_xml );
3100     my $source_r = MARC::Record->new_from_xml( $source_xml );
3101
3102     return $target_xml unless ($target_r && $source_r);
3103
3104     my @field_list = split(',', $field_spec);
3105
3106     my %fields;
3107     for my $f (@field_list) {
3108         $f =~ s/^\s*//; $f =~ s/\s*$//;
3109         if ($f =~ /^(.{3})(\w*)(?:\[([^]]*)\])?$/) {
3110             my $field = $1;
3111             $field =~ s/\s+//;
3112             my $sf = $2;
3113             $sf =~ s/\s+//;
3114             my $match = $3;
3115             $match =~ s/^\s*//; $match =~ s/\s*$//;
3116             $fields{$field} = { sf => [ split('', $sf) ] };
3117             if ($match) {
3118                 my ($msf,$mre) = split('~', $match);
3119                 if (length($msf) > 0 and length($mre) > 0) {
3120                     $msf =~ s/^\s*//; $msf =~ s/\s*$//;
3121                     $mre =~ s/^\s*//; $mre =~ s/\s*$//;
3122                     $fields{$field}{match} = { sf => $msf, re => qr/$mre/ };
3123                 }
3124             }
3125         }
3126     }
3127
3128     for my $f ( keys %fields) {
3129         if ( @{$fields{$f}{sf}} ) {
3130             for my $from_field ($source_r->field( $f )) {
3131                 my @tos = $target_r->field( $f );
3132                 if (!@tos) {
3133                     next if (exists($fields{$f}{match}) and !$force_add);
3134                     my @new_fields = map { $_->clone } $source_r->field( $f );
3135                     $target_r->insert_fields_ordered( @new_fields );
3136                 } else {
3137                     for my $to_field (@tos) {
3138                         if (exists($fields{$f}{match})) {
3139                             next unless (grep { $_ =~ $fields{$f}{match}{re} } $to_field->subfield($fields{$f}{match}{sf}));
3140                         }
3141                         my @new_sf = map { ($_ => $from_field->subfield($_)) } grep { defined($from_field->subfield($_)) } @{$fields{$f}{sf}};
3142                         $to_field->add_subfields( @new_sf );
3143                     }
3144                 }
3145             }
3146         } else {
3147             my @new_fields = map { $_->clone } $source_r->field( $f );
3148             $target_r->insert_fields_ordered( @new_fields );
3149         }
3150     }
3151
3152     $target_xml = $target_r->as_xml_record;
3153     $target_xml =~ s/^<\?.+?\?>$//mo;
3154     $target_xml =~ s/\n//sgo;
3155     $target_xml =~ s/>\s+</></sgo;
3156
3157     return $target_xml;
3158
3159 $_$ LANGUAGE PLPERLU;
3160
3161
3162 CREATE INDEX by_heading ON authority.record_entry (authority.simple_normalize_heading(marc)) WHERE deleted IS FALSE or deleted = FALSE;
3163
3164 INSERT INTO config.metabib_field ( id, field_class, name, label, format, xpath, search_field, facet_field) VALUES
3165     (28, 'identifier', 'authority_id', oils_i18n_gettext(28, 'Authority Record ID', 'cmf', 'label'), 'marcxml', '//marc:datafield/marc:subfield[@code="0"]', FALSE, TRUE);
3166  
3167 INSERT INTO config.marc21_rec_type_map (code, type_val, blvl_val) VALUES ('AUT','z',' ');
3168 INSERT INTO config.marc21_rec_type_map (code, type_val, blvl_val) VALUES ('MFHD','uvxy',' ');
3169  
3170 INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('ELvl', 'ldr', 'AUT', 17, 1, ' ');
3171 INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('Subj', '008', 'AUT', 11, 1, '|');
3172 INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('RecStat', 'ldr', 'AUT', 5, 1, 'n');
3173  
3174 INSERT INTO config.metabib_field_index_norm_map (field,norm,pos)
3175     SELECT  m.id,
3176             i.id,
3177             -1
3178       FROM  config.metabib_field m,
3179             config.index_normalizer i
3180       WHERE i.func = 'remove_paren_substring'
3181             AND m.id IN (28);
3182
3183 SELECT SETVAL('authority.control_set_id_seq'::TEXT, 100);
3184 SELECT SETVAL('authority.control_set_authority_field_id_seq'::TEXT, 1000);
3185 SELECT SETVAL('authority.control_set_bib_field_id_seq'::TEXT, 1000);
3186
3187 INSERT INTO authority.control_set (id, name, description) VALUES (
3188     1,
3189     oils_i18n_gettext('1','LoC','acs','name'),
3190     oils_i18n_gettext('1','Library of Congress standard authority record control semantics','acs','description')
3191 );
3192
3193 INSERT INTO authority.control_set_authority_field (id, control_set, main_entry, tag, sf_list, name) VALUES
3194
3195 -- Main entries
3196     (1, 1, NULL, '100', 'abcdefklmnopqrstvxyz', oils_i18n_gettext('1','Heading -- Personal Name','acsaf','name')),
3197     (2, 1, NULL, '110', 'abcdefgklmnoprstvxyz', oils_i18n_gettext('2','Heading -- Corporate Name','acsaf','name')),
3198     (3, 1, NULL, '111', 'acdefgklnpqstvxyz', oils_i18n_gettext('3','Heading -- Meeting Name','acsaf','name')),
3199     (4, 1, NULL, '130', 'adfgklmnoprstvxyz', oils_i18n_gettext('4','Heading -- Uniform Title','acsaf','name')),
3200     (5, 1, NULL, '150', 'abvxyz', oils_i18n_gettext('5','Heading -- Topical Term','acsaf','name')),
3201     (6, 1, NULL, '151', 'avxyz', oils_i18n_gettext('6','Heading -- Geographic Name','acsaf','name')),
3202     (7, 1, NULL, '155', 'avxyz', oils_i18n_gettext('7','Heading -- Genre/Form Term','acsaf','name')),
3203     (8, 1, NULL, '180', 'vxyz', oils_i18n_gettext('8','Heading -- General Subdivision','acsaf','name')),
3204     (9, 1, NULL, '181', 'vxyz', oils_i18n_gettext('9','Heading -- Geographic Subdivision','acsaf','name')),
3205     (10, 1, NULL, '182', 'vxyz', oils_i18n_gettext('10','Heading -- Chronological Subdivision','acsaf','name')),
3206     (11, 1, NULL, '185', 'vxyz', oils_i18n_gettext('11','Heading -- Form Subdivision','acsaf','name')),
3207     (12, 1, NULL, '148', 'avxyz', oils_i18n_gettext('12','Heading -- Chronological Term','acsaf','name')),
3208
3209 -- See Also From tracings
3210     (21, 1, 1, '500', 'abcdefiklmnopqrstvwxyz4', oils_i18n_gettext('21','See Also From Tracing -- Personal Name','acsaf','name')),
3211     (22, 1, 2, '510', 'abcdefgiklmnoprstvwxyz4', oils_i18n_gettext('22','See Also From Tracing -- Corporate Name','acsaf','name')),
3212     (23, 1, 3, '511', 'acdefgiklnpqstvwxyz4', oils_i18n_gettext('23','See Also From Tracing -- Meeting Name','acsaf','name')),
3213     (24, 1, 4, '530', 'adfgiklmnoprstvwxyz4', oils_i18n_gettext('24','See Also From Tracing -- Uniform Title','acsaf','name')),
3214     (25, 1, 5, '550', 'abivwxyz4', oils_i18n_gettext('25','See Also From Tracing -- Topical Term','acsaf','name')),
3215     (26, 1, 6, '551', 'aivwxyz4', oils_i18n_gettext('26','See Also From Tracing -- Geographic Name','acsaf','name')),
3216     (27, 1, 7, '555', 'aivwxyz4', oils_i18n_gettext('27','See Also From Tracing -- Genre/Form Term','acsaf','name')),
3217     (28, 1, 8, '580', 'ivwxyz4', oils_i18n_gettext('28','See Also From Tracing -- General Subdivision','acsaf','name')),
3218     (29, 1, 9, '581', 'ivwxyz4', oils_i18n_gettext('29','See Also From Tracing -- Geographic Subdivision','acsaf','name')),
3219     (30, 1, 10, '582', 'ivwxyz4', oils_i18n_gettext('30','See Also From Tracing -- Chronological Subdivision','acsaf','name')),
3220     (31, 1, 11, '585', 'ivwxyz4', oils_i18n_gettext('31','See Also From Tracing -- Form Subdivision','acsaf','name')),
3221     (32, 1, 12, '548', 'aivwxyz4', oils_i18n_gettext('32','See Also From Tracing -- Chronological Term','acsaf','name')),
3222
3223 -- Linking entries
3224     (41, 1, 1, '700', 'abcdefghjklmnopqrstvwxyz25', oils_i18n_gettext('41','Established Heading Linking Entry -- Personal Name','acsaf','name')),
3225     (42, 1, 2, '710', 'abcdefghklmnoprstvwxyz25', oils_i18n_gettext('42','Established Heading Linking Entry -- Corporate Name','acsaf','name')),
3226     (43, 1, 3, '711', 'acdefghklnpqstvwxyz25', oils_i18n_gettext('43','Established Heading Linking Entry -- Meeting Name','acsaf','name')),
3227     (44, 1, 4, '730', 'adfghklmnoprstvwxyz25', oils_i18n_gettext('44','Established Heading Linking Entry -- Uniform Title','acsaf','name')),
3228     (45, 1, 5, '750', 'abvwxyz25', oils_i18n_gettext('45','Established Heading Linking Entry -- Topical Term','acsaf','name')),
3229     (46, 1, 6, '751', 'avwxyz25', oils_i18n_gettext('46','Established Heading Linking Entry -- Geographic Name','acsaf','name')),
3230     (47, 1, 7, '755', 'avwxyz25', oils_i18n_gettext('47','Established Heading Linking Entry -- Genre/Form Term','acsaf','name')),
3231     (48, 1, 8, '780', 'vwxyz25', oils_i18n_gettext('48','Subdivision Linking Entry -- General Subdivision','acsaf','name')),
3232     (49, 1, 9, '781', 'vwxyz25', oils_i18n_gettext('49','Subdivision Linking Entry -- Geographic Subdivision','acsaf','name')),
3233     (50, 1, 10, '782', 'vwxyz25', oils_i18n_gettext('50','Subdivision Linking Entry -- Chronological Subdivision','acsaf','name')),
3234     (51, 1, 11, '785', 'vwxyz25', oils_i18n_gettext('51','Subdivision Linking Entry -- Form Subdivision','acsaf','name')),
3235     (52, 1, 12, '748', 'avwxyz25', oils_i18n_gettext('52','Established Heading Linking Entry -- Chronological Term','acsaf','name')),
3236
3237 -- See From tracings
3238     (61, 1, 1, '400', 'abcdefiklmnopqrstvwxyz4', oils_i18n_gettext('61','See Also Tracing -- Personal Name','acsaf','name')),
3239     (62, 1, 2, '410', 'abcdefgiklmnoprstvwxyz4', oils_i18n_gettext('62','See Also Tracing -- Corporate Name','acsaf','name')),
3240     (63, 1, 3, '411', 'acdefgiklnpqstvwxyz4', oils_i18n_gettext('63','See Also Tracing -- Meeting Name','acsaf','name')),
3241     (64, 1, 4, '430', 'adfgiklmnoprstvwxyz4', oils_i18n_gettext('64','See Also Tracing -- Uniform Title','acsaf','name')),
3242     (65, 1, 5, '450', 'abivwxyz4', oils_i18n_gettext('65','See Also Tracing -- Topical Term','acsaf','name')),
3243     (66, 1, 6, '451', 'aivwxyz4', oils_i18n_gettext('66','See Also Tracing -- Geographic Name','acsaf','name')),
3244     (67, 1, 7, '455', 'aivwxyz4', oils_i18n_gettext('67','See Also Tracing -- Genre/Form Term','acsaf','name')),
3245     (68, 1, 8, '480', 'ivwxyz4', oils_i18n_gettext('68','See Also Tracing -- General Subdivision','acsaf','name')),
3246     (69, 1, 9, '481', 'ivwxyz4', oils_i18n_gettext('69','See Also Tracing -- Geographic Subdivision','acsaf','name')),
3247     (70, 1, 10, '482', 'ivwxyz4', oils_i18n_gettext('70','See Also Tracing -- Chronological Subdivision','acsaf','name')),
3248     (71, 1, 11, '485', 'ivwxyz4', oils_i18n_gettext('71','See Also Tracing -- Form Subdivision','acsaf','name')),
3249     (72, 1, 12, '448', 'aivwxyz4', oils_i18n_gettext('72','See Also Tracing -- Chronological Term','acsaf','name'));
3250
3251 INSERT INTO authority.browse_axis (code,name,description,sorter) VALUES
3252     ('title','Title','Title axis','titlesort'),
3253     ('author','Author','Author axis','titlesort'),
3254     ('subject','Subject','Subject axis','titlesort'),
3255     ('topic','Topic','Topic Subject axis','titlesort');
3256
3257 INSERT INTO authority.browse_axis_authority_field_map (axis,field) VALUES
3258     ('author',  1 ),
3259     ('author',  2 ),
3260     ('author',  3 ),
3261     ('title',   4 ),
3262     ('topic',   5 ),
3263     ('subject', 5 ),
3264     ('subject', 6 ),
3265     ('subject', 7 ),
3266     ('subject', 12);
3267
3268 INSERT INTO authority.control_set_bib_field (tag, authority_field) 
3269     SELECT '100', id FROM authority.control_set_authority_field WHERE tag IN ('100')
3270         UNION
3271     SELECT '600', id FROM authority.control_set_authority_field WHERE tag IN ('100','180','181','182','185')
3272         UNION
3273     SELECT '700', id FROM authority.control_set_authority_field WHERE tag IN ('100')
3274         UNION
3275     SELECT '800', id FROM authority.control_set_authority_field WHERE tag IN ('100')
3276         UNION
3277
3278     SELECT '110', id FROM authority.control_set_authority_field WHERE tag IN ('110')
3279         UNION
3280     SELECT '610', id FROM authority.control_set_authority_field WHERE tag IN ('110')
3281         UNION
3282     SELECT '710', id FROM authority.control_set_authority_field WHERE tag IN ('110')
3283         UNION
3284     SELECT '810', id FROM authority.control_set_authority_field WHERE tag IN ('110')
3285         UNION
3286
3287     SELECT '111', id FROM authority.control_set_authority_field WHERE tag IN ('111')
3288         UNION
3289     SELECT '611', id FROM authority.control_set_authority_field WHERE tag IN ('111')
3290         UNION
3291     SELECT '711', id FROM authority.control_set_authority_field WHERE tag IN ('111')
3292         UNION
3293     SELECT '811', id FROM authority.control_set_authority_field WHERE tag IN ('111')
3294         UNION
3295
3296     SELECT '130', id FROM authority.control_set_authority_field WHERE tag IN ('130')
3297         UNION
3298     SELECT '240', id FROM authority.control_set_authority_field WHERE tag IN ('130')
3299         UNION
3300     SELECT '630', id FROM authority.control_set_authority_field WHERE tag IN ('130')
3301         UNION
3302     SELECT '730', id FROM authority.control_set_authority_field WHERE tag IN ('130')
3303         UNION
3304     SELECT '830', id FROM authority.control_set_authority_field WHERE tag IN ('130')
3305         UNION
3306
3307     SELECT '648', id FROM authority.control_set_authority_field WHERE tag IN ('148')
3308         UNION
3309
3310     SELECT '650', id FROM authority.control_set_authority_field WHERE tag IN ('150','180','181','182','185')
3311         UNION
3312     SELECT '651', id FROM authority.control_set_authority_field WHERE tag IN ('151','180','181','182','185')
3313         UNION
3314     SELECT '655', id FROM authority.control_set_authority_field WHERE tag IN ('155','180','181','182','185')
3315 ;
3316
3317 INSERT INTO authority.thesaurus (code, name, control_set) VALUES
3318     ('a', oils_i18n_gettext('a','Library of Congress Subject Headings','at','name'), 1),
3319     ('b', oils_i18n_gettext('b',$$LC subject headings for children's literature$$,'at','name'), 1), -- silly vim '
3320     ('c', oils_i18n_gettext('c','Medical Subject Headings','at','name'), 1),
3321     ('d', oils_i18n_gettext('d','National Agricultural Library subject authority file','at','name'), 1),
3322     ('k', oils_i18n_gettext('k','Canadian Subject Headings','at','name'), 1),
3323     ('n', oils_i18n_gettext('n','Not applicable','at','name'), 1),
3324     ('r', oils_i18n_gettext('r','Art and Architecture Thesaurus','at','name'), 1),
3325     ('s', oils_i18n_gettext('s','Sears List of Subject Headings','at','name'), 1),
3326     ('v', oils_i18n_gettext('v','Repertoire de vedettes-matiere','at','name'), 1),
3327     ('z', oils_i18n_gettext('z','Other','at','name'), 1),
3328     ('|', oils_i18n_gettext('|','No attempt to code','at','name'), 1);
3329  
3330 CREATE OR REPLACE FUNCTION authority.map_thesaurus_to_control_set () RETURNS TRIGGER AS $func$
3331 BEGIN
3332     IF NEW.control_set IS NULL THEN
3333         SELECT  control_set INTO NEW.control_set
3334           FROM  authority.thesaurus
3335           WHERE vandelay.marc21_extract_fixed_field(NEW.marc,'Subj') = code;
3336     END IF;
3337
3338     RETURN NEW;
3339 END;
3340 $func$ LANGUAGE PLPGSQL;
3341
3342 CREATE TRIGGER map_thesaurus_to_control_set BEFORE INSERT OR UPDATE ON authority.record_entry FOR EACH ROW EXECUTE PROCEDURE authority.map_thesaurus_to_control_set ();
3343
3344 CREATE OR REPLACE FUNCTION authority.reingest_authority_rec_descriptor( auth_id BIGINT ) RETURNS VOID AS $func$
3345 BEGIN
3346     DELETE FROM authority.rec_descriptor WHERE record = auth_id;
3347     INSERT INTO authority.rec_descriptor (record, record_status, encoding_level, thesaurus)
3348         SELECT  auth_id,
3349                 vandelay.marc21_extract_fixed_field(marc,'RecStat'),
3350                 vandelay.marc21_extract_fixed_field(marc,'ELvl'),
3351                 vandelay.marc21_extract_fixed_field(marc,'Subj')
3352           FROM  authority.record_entry
3353           WHERE id = auth_id;
3354      RETURN;
3355  END;
3356  $func$ LANGUAGE PLPGSQL;
3357
3358 CREATE OR REPLACE FUNCTION authority.indexing_ingest_or_delete () RETURNS TRIGGER AS $func$
3359 BEGIN
3360
3361     IF NEW.deleted IS TRUE THEN -- If this authority is deleted
3362         DELETE FROM authority.bib_linking WHERE authority = NEW.id; -- Avoid updating fields in bibs that are no longer visible
3363         DELETE FROM authority.full_rec WHERE record = NEW.id; -- Avoid validating fields against deleted authority records
3364           -- Should remove matching $0 from controlled fields at the same time?
3365         RETURN NEW; -- and we're done
3366     END IF;
3367
3368     IF TG_OP = 'UPDATE' THEN -- re-ingest?
3369         PERFORM * FROM config.internal_flag WHERE name = 'ingest.reingest.force_on_same_marc' AND enabled;
3370
3371         IF NOT FOUND AND OLD.marc = NEW.marc THEN -- don't do anything if the MARC didn't change
3372             RETURN NEW;
3373         END IF;
3374         -- Propagate these updates to any linked bib records
3375         PERFORM authority.propagate_changes(NEW.id) FROM authority.record_entry WHERE id = NEW.id;
3376     END IF;
3377
3378     -- Flatten and insert the afr data
3379     PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_authority_full_rec' AND enabled;
3380     IF NOT FOUND THEN
3381         PERFORM authority.reingest_authority_full_rec(NEW.id);
3382         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_authority_rec_descriptor' AND enabled;
3383         IF NOT FOUND THEN
3384             PERFORM authority.reingest_authority_rec_descriptor(NEW.id);
3385         END IF;
3386     END IF;
3387
3388     RETURN NEW;
3389 END;
3390 $func$ LANGUAGE PLPGSQL;
3391
3392
3393 -- Evergreen DB patch 0577.schema.vandelay-item-import-copy-loc-ancestors.sql
3394 --
3395 -- Ingest items copy location inheritance
3396 --
3397
3398 -- check whether patch can be applied
3399 SELECT evergreen.upgrade_deps_block_check('0577', :eg_version); -- berick
3400
3401 CREATE OR REPLACE FUNCTION vandelay.ingest_items ( import_id BIGINT, attr_def_id BIGINT ) RETURNS SETOF vandelay.import_item AS $$
3402 DECLARE
3403
3404     owning_lib      TEXT;
3405     circ_lib        TEXT;
3406     call_number     TEXT;
3407     copy_number     TEXT;
3408     status          TEXT;
3409     location        TEXT;
3410     circulate       TEXT;
3411     deposit         TEXT;
3412     deposit_amount  TEXT;
3413     ref             TEXT;
3414     holdable        TEXT;
3415     price           TEXT;
3416     barcode         TEXT;
3417     circ_modifier   TEXT;
3418     circ_as_type    TEXT;
3419     alert_message   TEXT;
3420     opac_visible    TEXT;
3421     pub_note        TEXT;
3422     priv_note       TEXT;
3423
3424     attr_def        RECORD;
3425     tmp_attr_set    RECORD;
3426     attr_set        vandelay.import_item%ROWTYPE;
3427
3428     xpath           TEXT;
3429
3430 BEGIN
3431
3432     SELECT * INTO attr_def FROM vandelay.import_item_attr_definition WHERE id = attr_def_id;
3433
3434     IF FOUND THEN
3435
3436         attr_set.definition := attr_def.id;
3437
3438         -- Build the combined XPath
3439
3440         owning_lib :=
3441             CASE
3442                 WHEN attr_def.owning_lib IS NULL THEN 'null()'
3443                 WHEN LENGTH( attr_def.owning_lib ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.owning_lib || '"]'
3444                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.owning_lib
3445             END;
3446
3447         circ_lib :=
3448             CASE
3449                 WHEN attr_def.circ_lib IS NULL THEN 'null()'
3450                 WHEN LENGTH( attr_def.circ_lib ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.circ_lib || '"]'
3451                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.circ_lib
3452             END;
3453
3454         call_number :=
3455             CASE
3456                 WHEN attr_def.call_number IS NULL THEN 'null()'
3457                 WHEN LENGTH( attr_def.call_number ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.call_number || '"]'
3458                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.call_number
3459             END;
3460
3461         copy_number :=
3462             CASE
3463                 WHEN attr_def.copy_number IS NULL THEN 'null()'
3464                 WHEN LENGTH( attr_def.copy_number ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.copy_number || '"]'
3465                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.copy_number
3466             END;
3467
3468         status :=
3469             CASE
3470                 WHEN attr_def.status IS NULL THEN 'null()'
3471                 WHEN LENGTH( attr_def.status ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.status || '"]'
3472                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.status
3473             END;
3474
3475         location :=
3476             CASE
3477                 WHEN attr_def.location IS NULL THEN 'null()'
3478                 WHEN LENGTH( attr_def.location ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.location || '"]'
3479                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.location
3480             END;
3481
3482         circulate :=
3483             CASE
3484                 WHEN attr_def.circulate IS NULL THEN 'null()'
3485                 WHEN LENGTH( attr_def.circulate ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.circulate || '"]'
3486                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.circulate
3487             END;
3488
3489         deposit :=
3490             CASE
3491                 WHEN attr_def.deposit IS NULL THEN 'null()'
3492                 WHEN LENGTH( attr_def.deposit ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.deposit || '"]'
3493                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.deposit
3494             END;
3495
3496         deposit_amount :=
3497             CASE
3498                 WHEN attr_def.deposit_amount IS NULL THEN 'null()'
3499                 WHEN LENGTH( attr_def.deposit_amount ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.deposit_amount || '"]'
3500                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.deposit_amount
3501             END;
3502
3503         ref :=
3504             CASE
3505                 WHEN attr_def.ref IS NULL THEN 'null()'
3506                 WHEN LENGTH( attr_def.ref ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.ref || '"]'
3507                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.ref
3508             END;
3509
3510         holdable :=
3511             CASE
3512                 WHEN attr_def.holdable IS NULL THEN 'null()'
3513                 WHEN LENGTH( attr_def.holdable ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.holdable || '"]'
3514                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.holdable
3515             END;
3516
3517         price :=
3518             CASE
3519                 WHEN attr_def.price IS NULL THEN 'null()'
3520                 WHEN LENGTH( attr_def.price ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.price || '"]'
3521                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.price
3522             END;
3523
3524         barcode :=
3525             CASE
3526                 WHEN attr_def.barcode IS NULL THEN 'null()'
3527                 WHEN LENGTH( attr_def.barcode ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.barcode || '"]'
3528                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.barcode
3529             END;
3530
3531         circ_modifier :=
3532             CASE
3533                 WHEN attr_def.circ_modifier IS NULL THEN 'null()'
3534                 WHEN LENGTH( attr_def.circ_modifier ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.circ_modifier || '"]'
3535                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.circ_modifier
3536             END;
3537
3538         circ_as_type :=
3539             CASE
3540                 WHEN attr_def.circ_as_type IS NULL THEN 'null()'
3541                 WHEN LENGTH( attr_def.circ_as_type ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.circ_as_type || '"]'
3542                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.circ_as_type
3543             END;
3544
3545         alert_message :=
3546             CASE
3547                 WHEN attr_def.alert_message IS NULL THEN 'null()'
3548                 WHEN LENGTH( attr_def.alert_message ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.alert_message || '"]'
3549                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.alert_message
3550             END;
3551
3552         opac_visible :=
3553             CASE
3554                 WHEN attr_def.opac_visible IS NULL THEN 'null()'
3555                 WHEN LENGTH( attr_def.opac_visible ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.opac_visible || '"]'
3556                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.opac_visible
3557             END;
3558
3559         pub_note :=
3560             CASE
3561                 WHEN attr_def.pub_note IS NULL THEN 'null()'
3562                 WHEN LENGTH( attr_def.pub_note ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.pub_note || '"]'
3563                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.pub_note
3564             END;
3565         priv_note :=
3566             CASE
3567                 WHEN attr_def.priv_note IS NULL THEN 'null()'
3568                 WHEN LENGTH( attr_def.priv_note ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.priv_note || '"]'
3569                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.priv_note
3570             END;
3571
3572
3573         xpath :=
3574             owning_lib      || '|' ||
3575             circ_lib        || '|' ||
3576             call_number     || '|' ||
3577             copy_number     || '|' ||
3578             status          || '|' ||
3579             location        || '|' ||
3580             circulate       || '|' ||
3581             deposit         || '|' ||
3582             deposit_amount  || '|' ||
3583             ref             || '|' ||
3584             holdable        || '|' ||
3585             price           || '|' ||
3586             barcode         || '|' ||
3587             circ_modifier   || '|' ||
3588             circ_as_type    || '|' ||
3589             alert_message   || '|' ||
3590             pub_note        || '|' ||
3591             priv_note       || '|' ||
3592             opac_visible;
3593
3594         -- RAISE NOTICE 'XPath: %', xpath;
3595
3596         FOR tmp_attr_set IN
3597                 SELECT  *
3598                   FROM  oils_xpath_table( 'id', 'marc', 'vandelay.queued_bib_record', xpath, 'id = ' || import_id )
3599                             AS t( id INT, ol TEXT, clib TEXT, cn TEXT, cnum TEXT, cs TEXT, cl TEXT, circ TEXT,
3600                                   dep TEXT, dep_amount TEXT, r TEXT, hold TEXT, pr TEXT, bc TEXT, circ_mod TEXT,
3601                                   circ_as TEXT, amessage TEXT, note TEXT, pnote TEXT, opac_vis TEXT )
3602         LOOP
3603
3604             tmp_attr_set.pr = REGEXP_REPLACE(tmp_attr_set.pr, E'[^0-9\\.]', '', 'g');
3605             tmp_attr_set.dep_amount = REGEXP_REPLACE(tmp_attr_set.dep_amount, E'[^0-9\\.]', '', 'g');
3606
3607             tmp_attr_set.pr := NULLIF( tmp_attr_set.pr, '' );
3608             tmp_attr_set.dep_amount := NULLIF( tmp_attr_set.dep_amount, '' );
3609
3610             SELECT id INTO attr_set.owning_lib FROM actor.org_unit WHERE shortname = UPPER(tmp_attr_set.ol); -- INT
3611             SELECT id INTO attr_set.circ_lib FROM actor.org_unit WHERE shortname = UPPER(tmp_attr_set.clib); -- INT
3612             SELECT id INTO attr_set.status FROM config.copy_status WHERE LOWER(name) = LOWER(tmp_attr_set.cs); -- INT
3613
3614
3615             -- search up the org unit tree for a matching copy location
3616
3617             WITH RECURSIVE anscestor_depth AS (
3618                 SELECT  ou.id,
3619                     out.depth AS depth,
3620                     ou.parent_ou
3621                 FROM  actor.org_unit ou
3622                     JOIN actor.org_unit_type out ON (out.id = ou.ou_type)
3623                 WHERE ou.id = COALESCE(attr_set.owning_lib, attr_set.circ_lib)
3624                     UNION ALL
3625                 SELECT  ou.id,
3626                     out.depth,
3627                     ou.parent_ou
3628                 FROM  actor.org_unit ou
3629                     JOIN actor.org_unit_type out ON (out.id = ou.ou_type)
3630                     JOIN anscestor_depth ot ON (ot.parent_ou = ou.id)
3631             ) SELECT  cpl.id INTO attr_set.location
3632                 FROM  anscestor_depth a
3633                     JOIN asset.copy_location cpl ON (cpl.owning_lib = a.id)
3634                 WHERE LOWER(cpl.name) = LOWER(tmp_attr_set.cl)
3635                 ORDER BY a.depth DESC
3636                 LIMIT 1; 
3637
3638             attr_set.circulate      :=
3639                 LOWER( SUBSTRING( tmp_attr_set.circ, 1, 1)) IN ('t','y','1')
3640                 OR LOWER(tmp_attr_set.circ) = 'circulating'; -- BOOL
3641
3642             attr_set.deposit        :=
3643                 LOWER( SUBSTRING( tmp_attr_set.dep, 1, 1 ) ) IN ('t','y','1')
3644                 OR LOWER(tmp_attr_set.dep) = 'deposit'; -- BOOL
3645
3646             attr_set.holdable       :=
3647                 LOWER( SUBSTRING( tmp_attr_set.hold, 1, 1 ) ) IN ('t','y','1')
3648                 OR LOWER(tmp_attr_set.hold) = 'holdable'; -- BOOL
3649
3650             attr_set.opac_visible   :=
3651                 LOWER( SUBSTRING( tmp_attr_set.opac_vis, 1, 1 ) ) IN ('t','y','1')
3652                 OR LOWER(tmp_attr_set.opac_vis) = 'visible'; -- BOOL
3653
3654             attr_set.ref            :=
3655                 LOWER( SUBSTRING( tmp_attr_set.r, 1, 1 ) ) IN ('t','y','1')
3656                 OR LOWER(tmp_attr_set.r) = 'reference'; -- BOOL
3657
3658             attr_set.copy_number    := tmp_attr_set.cnum::INT; -- INT,
3659             attr_set.deposit_amount := tmp_attr_set.dep_amount::NUMERIC(6,2); -- NUMERIC(6,2),
3660             attr_set.price          := tmp_attr_set.pr::NUMERIC(8,2); -- NUMERIC(8,2),
3661
3662             attr_set.call_number    := tmp_attr_set.cn; -- TEXT
3663             attr_set.barcode        := tmp_attr_set.bc; -- TEXT,
3664             attr_set.circ_modifier  := tmp_attr_set.circ_mod; -- TEXT,
3665             attr_set.circ_as_type   := tmp_attr_set.circ_as; -- TEXT,
3666             attr_set.alert_message  := tmp_attr_set.amessage; -- TEXT,
3667             attr_set.pub_note       := tmp_attr_set.note; -- TEXT,
3668             attr_set.priv_note      := tmp_attr_set.pnote; -- TEXT,
3669             attr_set.alert_message  := tmp_attr_set.amessage; -- TEXT,
3670
3671             RETURN NEXT attr_set;
3672
3673         END LOOP;
3674
3675     END IF;
3676
3677     RETURN;
3678
3679 END;
3680 $$ LANGUAGE PLPGSQL;
3681
3682
3683 -- Evergreen DB patch XXXX.data.org-setting-ui.circ.billing.uncheck_bills_and_unfocus_payment_box.sql
3684 --
3685 -- New org setting ui.circ.billing.uncheck_bills_and_unfocus_payment_box
3686 --
3687
3688 -- check whether patch can be applied
3689 SELECT evergreen.upgrade_deps_block_check('0584', :eg_version);
3690
3691 INSERT INTO config.org_unit_setting_type ( name, label, description, datatype ) 
3692     VALUES ( 
3693         'ui.circ.billing.uncheck_bills_and_unfocus_payment_box',
3694         oils_i18n_gettext(
3695             'ui.circ.billing.uncheck_bills_and_unfocus_payment_box',
3696             'GUI: Uncheck bills by default in the patron billing interface',
3697             'coust',
3698             'label'
3699         ),
3700         oils_i18n_gettext(
3701             'ui.circ.billing.uncheck_bills_and_unfocus_payment_box',
3702             'Uncheck bills by default in the patron billing interface,'
3703             || ' and focus on the Uncheck All button instead of the'
3704             || ' Payment Received field.',
3705             'coust',
3706             'description'
3707         ),
3708         'bool'
3709     );
3710
3711
3712 -- check whether patch can be applied
3713 SELECT evergreen.upgrade_deps_block_check('0585', :eg_version);
3714
3715 INSERT into config.org_unit_setting_type
3716 ( name, label, description, datatype ) VALUES
3717 ( 'circ.checkout_fills_related_hold_exact_match_only',
3718     'Checkout Fills Related Hold On Valid Copy Only',
3719     'When filling related holds on checkout only match on items that are valid for opportunistic capture for the hold. Without this set a Title or Volume hold could match when the item is not holdable. With this set only holdable items will match.',
3720     'bool');
3721
3722
3723 -- check whether patch can be applied
3724 SELECT evergreen.upgrade_deps_block_check('0586', :eg_version);
3725
3726 INSERT INTO permission.perm_list (id, code, description) VALUES (
3727     511,
3728     'PERSISTENT_LOGIN',
3729     oils_i18n_gettext(
3730         511,
3731         'Allows a user to authenticate and get a long-lived session (length configured in opensrf.xml)',
3732         'ppl',
3733         'description'
3734     )
3735 );
3736
3737 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
3738     SELECT
3739         pgt.id, perm.id, aout.depth, FALSE
3740     FROM
3741         permission.grp_tree pgt,
3742         permission.perm_list perm,
3743         actor.org_unit_type aout
3744     WHERE
3745         pgt.name = 'Users' AND
3746         aout.name = 'Consortium' AND
3747         perm.code = 'PERSISTENT_LOGIN';
3748
3749 \qecho 
3750 \qecho If this transaction succeeded, your users (staff and patrons) now have
3751 \qecho the PERSISTENT_LOGIN permission by default.
3752 \qecho 
3753
3754
3755 -- Evergreen DB patch XXXX.data.org-setting-circ.offline.skip_foo_if_newer_status_changed_time.sql
3756 --
3757 -- New org setting circ.offline.skip_checkout_if_newer_status_changed_time
3758 -- New org setting circ.offline.skip_renew_if_newer_status_changed_time
3759 -- New org setting circ.offline.skip_checkin_if_newer_status_changed_time
3760 --
3761
3762 -- check whether patch can be applied
3763 SELECT evergreen.upgrade_deps_block_check('0593', :eg_version);
3764
3765 INSERT INTO config.org_unit_setting_type ( name, label, description, datatype ) 
3766     VALUES ( 
3767         'circ.offline.skip_checkout_if_newer_status_changed_time',
3768         oils_i18n_gettext(
3769             'circ.offline.skip_checkout_if_newer_status_changed_time',
3770             'Offline: Skip offline checkout if newer item Status Changed Time.',
3771             'coust',
3772             'label'
3773         ),
3774         oils_i18n_gettext(
3775             'circ.offline.skip_checkout_if_newer_status_changed_time',
3776             'Skip offline checkout transaction (raise exception when'
3777             || ' processing) if item Status Changed Time is newer than the'
3778             || ' recorded transaction time.  WARNING: The Reshelving to'
3779             || ' Available status rollover will trigger this.',
3780             'coust',
3781             'description'
3782         ),
3783         'bool'
3784     ),( 
3785         'circ.offline.skip_renew_if_newer_status_changed_time',
3786         oils_i18n_gettext(
3787             'circ.offline.skip_renew_if_newer_status_changed_time',
3788             'Offline: Skip offline renewal if newer item Status Changed Time.',
3789             'coust',
3790             'label'
3791         ),
3792         oils_i18n_gettext(
3793             'circ.offline.skip_renew_if_newer_status_changed_time',
3794             'Skip offline renewal transaction (raise exception when'
3795             || ' processing) if item Status Changed Time is newer than the'
3796             || ' recorded transaction time.  WARNING: The Reshelving to'
3797             || ' Available status rollover will trigger this.',
3798             'coust',
3799             'description'
3800         ),
3801         'bool'
3802     ),( 
3803         'circ.offline.skip_checkin_if_newer_status_changed_time',
3804         oils_i18n_gettext(
3805             'circ.offline.skip_checkin_if_newer_status_changed_time',
3806             'Offline: Skip offline checkin if newer item Status Changed Time.',
3807             'coust',
3808             'label'
3809         ),
3810         oils_i18n_gettext(
3811             'circ.offline.skip_checkin_if_newer_status_changed_time',
3812             'Skip offline checkin transaction (raise exception when'
3813             || ' processing) if item Status Changed Time is newer than the'
3814             || ' recorded transaction time.  WARNING: The Reshelving to'
3815             || ' Available status rollover will trigger this.',
3816             'coust',
3817             'description'
3818         ),
3819         'bool'
3820     );
3821
3822 -- Evergreen DB patch YYYY.schema.acp_status_date_changed.sql
3823 --
3824 -- Change trigger which updates copy status_changed_time to ignore the
3825 -- Reshelving->Available status rollover
3826
3827 -- FIXME: 0039.schema.acp_status_date_changed.sql defines this the first time
3828 -- around, but along with the column itself, etc.  And it gets modified with
3829 -- 0562.schema.copy_active_date.sql.  Not sure how to use the supercedes /
3830 -- deprecate stuff for upgrade scripts, if it's even applicable when a given
3831 -- upgrade script is doing so much.
3832
3833 -- check whether patch can be applied
3834 SELECT evergreen.upgrade_deps_block_check('0594', :eg_version);
3835
3836 CREATE OR REPLACE FUNCTION asset.acp_status_changed()
3837 RETURNS TRIGGER AS $$
3838 BEGIN
3839         IF NEW.status <> OLD.status AND NOT (NEW.status = 0 AND OLD.status = 7) THEN
3840         NEW.status_changed_time := now();
3841         IF NEW.active_date IS NULL AND NEW.status IN (SELECT id FROM config.copy_status WHERE copy_active = true) THEN
3842             NEW.active_date := now();
3843         END IF;
3844     END IF;
3845     RETURN NEW;
3846 END;
3847 $$ LANGUAGE plpgsql;
3848
3849 -- Evergreen DB patch 0595.data.org-setting-ui.patron_search.result_cap.sql
3850 --
3851 -- New org setting ui.patron_search.result_cap
3852 --
3853
3854 -- check whether patch can be applied
3855 SELECT evergreen.upgrade_deps_block_check('0595', :eg_version);
3856
3857 INSERT INTO config.org_unit_setting_type ( name, label, description, datatype )
3858     VALUES (
3859         'ui.patron_search.result_cap',
3860         oils_i18n_gettext(
3861             'ui.patron_search.result_cap',
3862             'GUI: Cap results in Patron Search at this number.',
3863             'coust',
3864             'label'
3865         ),
3866         oils_i18n_gettext(
3867             'ui.patron_search.result_cap',
3868             'So for example, if you search for John Doe, normally you would get'
3869             || ' at most 50 results.  This setting allows you to raise or lower'
3870             || ' that limit.',
3871             'coust',
3872             'description'
3873         ),
3874         'integer'
3875     );
3876
3877 -- Evergreen DB patch 0596.schema.vandelay-item-import-error-detail.sql
3878
3879 -- check whether patch can be applied
3880 SELECT evergreen.upgrade_deps_block_check('0596', :eg_version);
3881
3882 INSERT INTO vandelay.import_error ( code, description ) VALUES ( 
3883     'import.item.invalid.status', oils_i18n_gettext('import.item.invalid.status', 'Invalid value for "status"', 'vie', 'description') );
3884 INSERT INTO vandelay.import_error ( code, description ) VALUES ( 
3885     'import.item.invalid.price', oils_i18n_gettext('import.item.invalid.price', 'Invalid value for "price"', 'vie', 'description') );
3886 INSERT INTO vandelay.import_error ( code, description ) VALUES ( 
3887     'import.item.invalid.deposit_amount', oils_i18n_gettext('import.item.invalid.deposit_amount', 'Invalid value for "deposit_amount"', 'vie', 'description') );
3888 INSERT INTO vandelay.import_error ( code, description ) VALUES ( 
3889     'import.item.invalid.owning_lib', oils_i18n_gettext('import.item.invalid.owning_lib', 'Invalid value for "owning_lib"', 'vie', 'description') );
3890 INSERT INTO vandelay.import_error ( code, description ) VALUES ( 
3891     'import.item.invalid.circ_lib', oils_i18n_gettext('import.item.invalid.circ_lib', 'Invalid value for "circ_lib"', 'vie', 'description') );
3892 INSERT INTO vandelay.import_error ( code, description ) VALUES ( 
3893     'import.item.invalid.copy_number', oils_i18n_gettext('import.item.invalid.copy_number', 'Invalid value for "copy_number"', 'vie', 'description') );
3894 INSERT INTO vandelay.import_error ( code, description ) VALUES ( 
3895     'import.item.invalid.circ_as_type', oils_i18n_gettext('import.item.invalid.circ_as_type', 'Invalid value for "circ_as_type"', 'vie', 'description') );
3896
3897 CREATE OR REPLACE FUNCTION vandelay.ingest_items ( import_id BIGINT, attr_def_id BIGINT ) RETURNS SETOF vandelay.import_item AS $$
3898 DECLARE
3899
3900     owning_lib      TEXT;
3901     circ_lib        TEXT;
3902     call_number     TEXT;
3903     copy_number     TEXT;
3904     status          TEXT;
3905     location        TEXT;
3906     circulate       TEXT;
3907     deposit         TEXT;
3908     deposit_amount  TEXT;
3909     ref             TEXT;
3910     holdable        TEXT;
3911     price           TEXT;
3912     barcode         TEXT;
3913     circ_modifier   TEXT;
3914     circ_as_type    TEXT;
3915     alert_message   TEXT;
3916     opac_visible    TEXT;
3917     pub_note        TEXT;
3918     priv_note       TEXT;
3919
3920     attr_def        RECORD;
3921     tmp_attr_set    RECORD;
3922     attr_set        vandelay.import_item%ROWTYPE;
3923
3924     xpath           TEXT;
3925     tmp_str         TEXT;
3926
3927 BEGIN
3928
3929     SELECT * INTO attr_def FROM vandelay.import_item_attr_definition WHERE id = attr_def_id;
3930
3931     IF FOUND THEN
3932
3933         attr_set.definition := attr_def.id;
3934
3935         -- Build the combined XPath
3936
3937         owning_lib :=
3938             CASE
3939                 WHEN attr_def.owning_lib IS NULL THEN 'null()'
3940                 WHEN LENGTH( attr_def.owning_lib ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.owning_lib || '"]'
3941                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.owning_lib
3942             END;
3943
3944         circ_lib :=
3945             CASE
3946                 WHEN attr_def.circ_lib IS NULL THEN 'null()'
3947                 WHEN LENGTH( attr_def.circ_lib ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.circ_lib || '"]'
3948                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.circ_lib
3949             END;
3950
3951         call_number :=
3952             CASE
3953                 WHEN attr_def.call_number IS NULL THEN 'null()'
3954                 WHEN LENGTH( attr_def.call_number ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.call_number || '"]'
3955                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.call_number
3956             END;
3957
3958         copy_number :=
3959             CASE
3960                 WHEN attr_def.copy_number IS NULL THEN 'null()'
3961                 WHEN LENGTH( attr_def.copy_number ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.copy_number || '"]'
3962                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.copy_number
3963             END;
3964
3965         status :=
3966             CASE
3967                 WHEN attr_def.status IS NULL THEN 'null()'
3968                 WHEN LENGTH( attr_def.status ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.status || '"]'
3969                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.status
3970             END;
3971
3972         location :=
3973             CASE
3974                 WHEN attr_def.location IS NULL THEN 'null()'
3975                 WHEN LENGTH( attr_def.location ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.location || '"]'
3976                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.location
3977             END;
3978
3979         circulate :=
3980             CASE
3981                 WHEN attr_def.circulate IS NULL THEN 'null()'
3982                 WHEN LENGTH( attr_def.circulate ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.circulate || '"]'
3983                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.circulate
3984             END;
3985
3986         deposit :=
3987             CASE
3988                 WHEN attr_def.deposit IS NULL THEN 'null()'
3989                 WHEN LENGTH( attr_def.deposit ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.deposit || '"]'
3990                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.deposit
3991             END;
3992
3993         deposit_amount :=
3994             CASE
3995                 WHEN attr_def.deposit_amount IS NULL THEN 'null()'
3996                 WHEN LENGTH( attr_def.deposit_amount ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.deposit_amount || '"]'
3997                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.deposit_amount
3998             END;
3999
4000         ref :=
4001             CASE
4002                 WHEN attr_def.ref IS NULL THEN 'null()'
4003                 WHEN LENGTH( attr_def.ref ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.ref || '"]'
4004                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.ref
4005             END;
4006
4007         holdable :=
4008             CASE
4009                 WHEN attr_def.holdable IS NULL THEN 'null()'
4010                 WHEN LENGTH( attr_def.holdable ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.holdable || '"]'
4011                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.holdable
4012             END;
4013
4014         price :=
4015             CASE
4016                 WHEN attr_def.price IS NULL THEN 'null()'
4017                 WHEN LENGTH( attr_def.price ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.price || '"]'
4018                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.price
4019             END;
4020
4021         barcode :=
4022             CASE
4023                 WHEN attr_def.barcode IS NULL THEN 'null()'
4024                 WHEN LENGTH( attr_def.barcode ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.barcode || '"]'
4025                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.barcode
4026             END;
4027
4028         circ_modifier :=
4029             CASE
4030                 WHEN attr_def.circ_modifier IS NULL THEN 'null()'
4031                 WHEN LENGTH( attr_def.circ_modifier ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.circ_modifier || '"]'
4032                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.circ_modifier
4033             END;
4034
4035         circ_as_type :=
4036             CASE
4037                 WHEN attr_def.circ_as_type IS NULL THEN 'null()'
4038                 WHEN LENGTH( attr_def.circ_as_type ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.circ_as_type || '"]'
4039                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.circ_as_type
4040             END;
4041
4042         alert_message :=
4043             CASE
4044                 WHEN attr_def.alert_message IS NULL THEN 'null()'
4045                 WHEN LENGTH( attr_def.alert_message ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.alert_message || '"]'
4046                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.alert_message
4047             END;
4048
4049         opac_visible :=
4050             CASE
4051                 WHEN attr_def.opac_visible IS NULL THEN 'null()'
4052                 WHEN LENGTH( attr_def.opac_visible ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.opac_visible || '"]'
4053                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.opac_visible
4054             END;
4055
4056         pub_note :=
4057             CASE
4058                 WHEN attr_def.pub_note IS NULL THEN 'null()'
4059                 WHEN LENGTH( attr_def.pub_note ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.pub_note || '"]'
4060                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.pub_note
4061             END;
4062         priv_note :=
4063             CASE
4064                 WHEN attr_def.priv_note IS NULL THEN 'null()'
4065                 WHEN LENGTH( attr_def.priv_note ) = 1 THEN '//*[@tag="' || attr_def.tag || '"]/*[@code="' || attr_def.priv_note || '"]'
4066                 ELSE '//*[@tag="' || attr_def.tag || '"]/*' || attr_def.priv_note
4067             END;
4068
4069
4070         xpath :=
4071             owning_lib      || '|' ||
4072             circ_lib        || '|' ||
4073             call_number     || '|' ||
4074             copy_number     || '|' ||
4075             status          || '|' ||
4076             location        || '|' ||
4077             circulate       || '|' ||
4078             deposit         || '|' ||
4079             deposit_amount  || '|' ||
4080             ref             || '|' ||
4081             holdable        || '|' ||
4082             price           || '|' ||
4083             barcode         || '|' ||
4084             circ_modifier   || '|' ||
4085             circ_as_type    || '|' ||
4086             alert_message   || '|' ||
4087             pub_note        || '|' ||
4088             priv_note       || '|' ||
4089             opac_visible;
4090
4091         FOR tmp_attr_set IN
4092                 SELECT  *
4093                   FROM  oils_xpath_table( 'id', 'marc', 'vandelay.queued_bib_record', xpath, 'id = ' || import_id )
4094                             AS t( id INT, ol TEXT, clib TEXT, cn TEXT, cnum TEXT, cs TEXT, cl TEXT, circ TEXT,
4095                                   dep TEXT, dep_amount TEXT, r TEXT, hold TEXT, pr TEXT, bc TEXT, circ_mod TEXT,
4096                                   circ_as TEXT, amessage TEXT, note TEXT, pnote TEXT, opac_vis TEXT )
4097         LOOP
4098
4099             attr_set.import_error := NULL;
4100             attr_set.error_detail := NULL;
4101             attr_set.deposit_amount := NULL;
4102             attr_set.copy_number := NULL;
4103             attr_set.price := NULL;
4104
4105             IF tmp_attr_set.pr != '' THEN
4106                 tmp_str = REGEXP_REPLACE(tmp_attr_set.pr, E'[^0-9\\.]', '', 'g');
4107                 IF tmp_str = '' THEN 
4108                     attr_set.import_error := 'import.item.invalid.price';
4109                     attr_set.error_detail := tmp_attr_set.pr; -- original value
4110                     RETURN NEXT attr_set; CONTINUE; 
4111                 END IF;
4112                 attr_set.price := tmp_str::NUMERIC(8,2); 
4113             END IF;
4114
4115             IF tmp_attr_set.dep_amount != '' THEN
4116                 tmp_str = REGEXP_REPLACE(tmp_attr_set.dep_amount, E'[^0-9\\.]', '', 'g');
4117                 IF tmp_str = '' THEN 
4118                     attr_set.import_error := 'import.item.invalid.deposit_amount';
4119                     attr_set.error_detail := tmp_attr_set.dep_amount; 
4120                     RETURN NEXT attr_set; CONTINUE; 
4121                 END IF;
4122                 attr_set.deposit_amount := tmp_str::NUMERIC(8,2); 
4123             END IF;
4124
4125             IF tmp_attr_set.cnum != '' THEN
4126                 tmp_str = REGEXP_REPLACE(tmp_attr_set.cnum, E'[^0-9]', '', 'g');
4127                 IF tmp_str = '' THEN 
4128                     attr_set.import_error := 'import.item.invalid.copy_number';
4129                     attr_set.error_detail := tmp_attr_set.cnum; 
4130                     RETURN NEXT attr_set; CONTINUE; 
4131                 END IF;
4132                 attr_set.copy_number := tmp_str::INT; 
4133             END IF;
4134
4135             IF tmp_attr_set.ol != '' THEN
4136                 SELECT id INTO attr_set.owning_lib FROM actor.org_unit WHERE shortname = UPPER(tmp_attr_set.ol); -- INT
4137                 IF NOT FOUND THEN
4138                     attr_set.import_error := 'import.item.invalid.owning_lib';
4139                     attr_set.error_detail := tmp_attr_set.ol;
4140                     RETURN NEXT attr_set; CONTINUE; 
4141                 END IF;
4142             END IF;
4143
4144             IF tmp_attr_set.clib != '' THEN
4145                 SELECT id INTO attr_set.circ_lib FROM actor.org_unit WHERE shortname = UPPER(tmp_attr_set.clib); -- INT
4146                 IF NOT FOUND THEN
4147                     attr_set.import_error := 'import.item.invalid.circ_lib';
4148                     attr_set.error_detail := tmp_attr_set.clib;
4149                     RETURN NEXT attr_set; CONTINUE; 
4150                 END IF;
4151             END IF;
4152
4153             IF tmp_attr_set.cs != '' THEN
4154                 SELECT id INTO attr_set.status FROM config.copy_status WHERE LOWER(name) = LOWER(tmp_attr_set.cs); -- INT
4155                 IF NOT FOUND THEN
4156                     attr_set.import_error := 'import.item.invalid.status';
4157                     attr_set.error_detail := tmp_attr_set.cs;
4158                     RETURN NEXT attr_set; CONTINUE; 
4159                 END IF;
4160             END IF;
4161
4162             IF tmp_attr_set.circ_mod != '' THEN
4163                 SELECT code INTO attr_set.circ_modifier FROM config.circ_modifier WHERE code = tmp_attr_set.circ_mod;
4164                 IF NOT FOUND THEN
4165                     attr_set.import_error := 'import.item.invalid.circ_modifier';
4166                     attr_set.error_detail := tmp_attr_set.circ_mod;
4167                     RETURN NEXT attr_set; CONTINUE; 
4168                 END IF;
4169             END IF;
4170
4171             IF tmp_attr_set.circ_as != '' THEN
4172                 SELECT code INTO attr_set.circ_as_type FROM config.coded_value_map WHERE ctype = 'item_type' AND code = tmp_attr_set.circ_as;
4173                 IF NOT FOUND THEN
4174                     attr_set.import_error := 'import.item.invalid.circ_as_type';
4175                     attr_set.error_detail := tmp_attr_set.circ_as;
4176                     RETURN NEXT attr_set; CONTINUE; 
4177                 END IF;
4178             END IF;
4179
4180             IF tmp_attr_set.cl != '' THEN
4181
4182                 -- search up the org unit tree for a matching copy location
4183                 WITH RECURSIVE anscestor_depth AS (
4184                     SELECT  ou.id,
4185                         out.depth AS depth,
4186                         ou.parent_ou
4187                     FROM  actor.org_unit ou
4188                         JOIN actor.org_unit_type out ON (out.id = ou.ou_type)
4189                     WHERE ou.id = COALESCE(attr_set.owning_lib, attr_set.circ_lib)
4190                         UNION ALL
4191                     SELECT  ou.id,
4192                         out.depth,
4193                         ou.parent_ou
4194                     FROM  actor.org_unit ou
4195                         JOIN actor.org_unit_type out ON (out.id = ou.ou_type)
4196                         JOIN anscestor_depth ot ON (ot.parent_ou = ou.id)
4197                 ) SELECT  cpl.id INTO attr_set.location
4198                     FROM  anscestor_depth a
4199                         JOIN asset.copy_location cpl ON (cpl.owning_lib = a.id)
4200                     WHERE LOWER(cpl.name) = LOWER(tmp_attr_set.cl)
4201                     ORDER BY a.depth DESC
4202                     LIMIT 1; 
4203
4204                 IF NOT FOUND THEN
4205                     attr_set.import_error := 'import.item.invalid.location';
4206                     attr_set.error_detail := tmp_attr_set.cs;
4207                     RETURN NEXT attr_set; CONTINUE; 
4208                 END IF;
4209             END IF;
4210
4211             attr_set.circulate      :=
4212                 LOWER( SUBSTRING( tmp_attr_set.circ, 1, 1)) IN ('t','y','1')
4213                 OR LOWER(tmp_attr_set.circ) = 'circulating'; -- BOOL
4214
4215             attr_set.deposit        :=
4216                 LOWER( SUBSTRING( tmp_attr_set.dep, 1, 1 ) ) IN ('t','y','1')
4217                 OR LOWER(tmp_attr_set.dep) = 'deposit'; -- BOOL
4218
4219             attr_set.holdable       :=
4220                 LOWER( SUBSTRING( tmp_attr_set.hold, 1, 1 ) ) IN ('t','y','1')
4221                 OR LOWER(tmp_attr_set.hold) = 'holdable'; -- BOOL
4222
4223             attr_set.opac_visible   :=
4224                 LOWER( SUBSTRING( tmp_attr_set.opac_vis, 1, 1 ) ) IN ('t','y','1')
4225                 OR LOWER(tmp_attr_set.opac_vis) = 'visible'; -- BOOL
4226
4227             attr_set.ref            :=
4228                 LOWER( SUBSTRING( tmp_attr_set.r, 1, 1 ) ) IN ('t','y','1')
4229                 OR LOWER(tmp_attr_set.r) = 'reference'; -- BOOL
4230
4231             attr_set.call_number    := tmp_attr_set.cn; -- TEXT
4232             attr_set.barcode        := tmp_attr_set.bc; -- TEXT,
4233             attr_set.alert_message  := tmp_attr_set.amessage; -- TEXT,
4234             attr_set.pub_note       := tmp_attr_set.note; -- TEXT,
4235             attr_set.priv_note      := tmp_attr_set.pnote; -- TEXT,
4236             attr_set.alert_message  := tmp_attr_set.amessage; -- TEXT,
4237
4238             RETURN NEXT attr_set;
4239
4240         END LOOP;
4241
4242     END IF;
4243
4244     RETURN;
4245
4246 END;
4247 $$ LANGUAGE PLPGSQL;
4248
4249 CREATE OR REPLACE FUNCTION vandelay.ingest_bib_items ( ) RETURNS TRIGGER AS $func$
4250 DECLARE
4251     attr_def    BIGINT;
4252     item_data   vandelay.import_item%ROWTYPE;
4253 BEGIN
4254
4255     IF TG_OP IN ('INSERT','UPDATE') AND NEW.imported_as IS NOT NULL THEN
4256         RETURN NEW;
4257     END IF;
4258
4259     SELECT item_attr_def INTO attr_def FROM vandelay.bib_queue WHERE id = NEW.queue;
4260
4261     FOR item_data IN SELECT * FROM vandelay.ingest_items( NEW.id::BIGINT, attr_def ) LOOP
4262         INSERT INTO vandelay.import_item (
4263             record,
4264             definition,
4265             owning_lib,
4266             circ_lib,
4267             call_number,
4268             copy_number,
4269             status,
4270             location,
4271             circulate,
4272             deposit,
4273             deposit_amount,
4274             ref,
4275             holdable,
4276             price,
4277             barcode,
4278             circ_modifier,
4279             circ_as_type,
4280             alert_message,
4281             pub_note,
4282             priv_note,
4283             opac_visible,
4284             import_error,
4285             error_detail
4286         ) VALUES (
4287             NEW.id,
4288             item_data.definition,
4289             item_data.owning_lib,
4290             item_data.circ_lib,
4291             item_data.call_number,
4292             item_data.copy_number,
4293             item_data.status,
4294             item_data.location,
4295             item_data.circulate,
4296             item_data.deposit,
4297             item_data.deposit_amount,
4298             item_data.ref,
4299             item_data.holdable,
4300             item_data.price,
4301             item_data.barcode,
4302             item_data.circ_modifier,
4303             item_data.circ_as_type,
4304             item_data.alert_message,
4305             item_data.pub_note,
4306             item_data.priv_note,
4307             item_data.opac_visible,
4308             item_data.import_error,
4309             item_data.error_detail
4310         );
4311     END LOOP;
4312
4313     RETURN NULL;
4314 END;
4315 $func$ LANGUAGE PLPGSQL;
4316
4317 -- Evergreen DB patch XXXX.schema.vandelay.bib_match_isxn_caseless.sql
4318
4319
4320 -- check whether patch can be applied
4321 SELECT evergreen.upgrade_deps_block_check('0597', :eg_version);
4322
4323 CREATE INDEX metabib_full_rec_isxn_caseless_idx
4324     ON metabib.real_full_rec (LOWER(value))
4325     WHERE tag IN ('020', '022', '024');
4326
4327
4328 CREATE OR REPLACE FUNCTION vandelay.flatten_marc_hstore(
4329     record_xml TEXT
4330 ) RETURNS HSTORE AS $$
4331 BEGIN
4332     RETURN (SELECT
4333         HSTORE(
4334             ARRAY_ACCUM(tag || (COALESCE(subfield, ''))),
4335             ARRAY_ACCUM(value)
4336         )
4337         FROM (
4338             SELECT
4339                 tag, subfield,
4340                 CASE WHEN tag IN ('020', '022', '024') THEN  -- caseless
4341                     ARRAY_ACCUM(LOWER(value))::TEXT
4342                 ELSE
4343                     ARRAY_ACCUM(value)::TEXT
4344                 END AS value
4345                 FROM vandelay.flatten_marc(record_xml)
4346                 GROUP BY tag, subfield ORDER BY tag, subfield
4347         ) subquery
4348     );
4349 END;
4350 $$ LANGUAGE PLPGSQL;
4351
4352 CREATE OR REPLACE FUNCTION vandelay._get_expr_push_jrow(
4353     node vandelay.match_set_point
4354 ) RETURNS VOID AS $$
4355 DECLARE
4356     jrow        TEXT;
4357     my_alias    TEXT;
4358     op          TEXT;
4359     tagkey      TEXT;
4360     caseless    BOOL;
4361 BEGIN
4362     -- remember $1 is tags_rstore, and $2 is svf_rstore
4363
4364     IF node.negate THEN
4365         op := '<>';
4366     ELSE
4367         op := '=';
4368     END IF;
4369
4370     caseless := FALSE;
4371
4372     IF node.tag IS NOT NULL THEN
4373         caseless := (node.tag IN ('020', '022', '024'));
4374         tagkey := node.tag;
4375         IF node.subfield IS NOT NULL THEN
4376             tagkey := tagkey || node.subfield;
4377         END IF;
4378     END IF;
4379
4380     my_alias := 'n' || node.id::TEXT;
4381
4382     jrow := 'LEFT JOIN (SELECT *, ' || node.quality ||
4383         ' AS quality FROM metabib.';
4384     IF node.tag IS NOT NULL THEN
4385         jrow := jrow || 'full_rec) ' || my_alias || ' ON (' ||
4386             my_alias || '.record = bre.id AND ' || my_alias || '.tag = ''' ||
4387             node.tag || '''';
4388         IF node.subfield IS NOT NULL THEN
4389             jrow := jrow || ' AND ' || my_alias || '.subfield = ''' ||
4390                 node.subfield || '''';
4391         END IF;
4392         jrow := jrow || ' AND (';
4393
4394         IF caseless THEN
4395             jrow := jrow || 'LOWER(' || my_alias || '.value) ' || op;
4396         ELSE
4397             jrow := jrow || my_alias || '.value ' || op;
4398         END IF;
4399
4400         jrow := jrow || ' ANY(($1->''' || tagkey || ''')::TEXT[])))';
4401     ELSE    -- svf
4402         jrow := jrow || 'record_attr) ' || my_alias || ' ON (' ||
4403             my_alias || '.id = bre.id AND (' ||
4404             my_alias || '.attrs->''' || node.svf ||
4405             ''' ' || op || ' $2->''' || node.svf || '''))';
4406     END IF;
4407     INSERT INTO _vandelay_tmp_jrows (j) VALUES (jrow);
4408 END;
4409 $$ LANGUAGE PLPGSQL;
4410
4411 -- Evergreen DB patch 0598.schema.vandelay_one_match_per.sql
4412 --
4413
4414
4415 -- check whether patch can be applied
4416 SELECT evergreen.upgrade_deps_block_check('0598', :eg_version);
4417
4418 CREATE OR REPLACE FUNCTION vandelay.match_set_test_marcxml(
4419     match_set_id INTEGER, record_xml TEXT
4420 ) RETURNS SETOF vandelay.match_set_test_result AS $$
4421 DECLARE
4422     tags_rstore HSTORE;
4423     svf_rstore  HSTORE;
4424     coal        TEXT;
4425     joins       TEXT;
4426     query_      TEXT;
4427     wq          TEXT;
4428     qvalue      INTEGER;
4429     rec         RECORD;
4430 BEGIN
4431     tags_rstore := vandelay.flatten_marc_hstore(record_xml);
4432     svf_rstore := vandelay.extract_rec_attrs(record_xml);
4433
4434     CREATE TEMPORARY TABLE _vandelay_tmp_qrows (q INTEGER);
4435     CREATE TEMPORARY TABLE _vandelay_tmp_jrows (j TEXT);
4436
4437     -- generate the where clause and return that directly (into wq), and as
4438     -- a side-effect, populate the _vandelay_tmp_[qj]rows tables.
4439     wq := vandelay.get_expr_from_match_set(match_set_id);
4440
4441     query_ := 'SELECT DISTINCT(bre.id) AS record, ';
4442
4443     -- qrows table is for the quality bits we add to the SELECT clause
4444     SELECT ARRAY_TO_STRING(
4445         ARRAY_ACCUM('COALESCE(n' || q::TEXT || '.quality, 0)'), ' + '
4446     ) INTO coal FROM _vandelay_tmp_qrows;
4447
4448     -- our query string so far is the SELECT clause and the inital FROM.
4449     -- no JOINs yet nor the WHERE clause
4450     query_ := query_ || coal || ' AS quality ' || E'\n' ||
4451         'FROM biblio.record_entry bre ';
4452
4453     -- jrows table is for the joins we must make (and the real text conditions)
4454     SELECT ARRAY_TO_STRING(ARRAY_ACCUM(j), E'\n') INTO joins
4455         FROM _vandelay_tmp_jrows;
4456
4457     -- add those joins and the where clause to our query.
4458     query_ := query_ || joins || E'\n' || 'WHERE ' || wq || ' AND not bre.deleted';
4459
4460     -- this will return rows of record,quality
4461     FOR rec IN EXECUTE query_ USING tags_rstore, svf_rstore LOOP
4462         RETURN NEXT rec;
4463     END LOOP;
4464
4465     DROP TABLE _vandelay_tmp_qrows;
4466     DROP TABLE _vandelay_tmp_jrows;
4467     RETURN;
4468 END;
4469
4470 $$ LANGUAGE PLPGSQL;
4471
4472 -- Evergreen DB patch 0606.schema.czs_use_perm_column.sql
4473 --
4474 -- This adds a column to config.z3950_source called use_perm.
4475 -- The idea is that if a permission is set for a given source,
4476 -- then staff will need the referenced permission to use that
4477 -- source.
4478 --
4479
4480 -- check whether patch can be applied
4481 SELECT evergreen.upgrade_deps_block_check('0606', :eg_version);
4482
4483 ALTER TABLE config.z3950_source 
4484     ADD COLUMN use_perm INT REFERENCES permission.perm_list (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED;
4485
4486 COMMENT ON COLUMN config.z3950_source.use_perm IS $$
4487 If set, this permission is required for the source to be listed in the staff
4488 client Z39.50 interface.  Similar to permission.grp_tree.application_perm.
4489 $$;
4490
4491 -- Evergreen DB patch 0608.data.vandelay-export-error-match-info.sql
4492 --
4493 --
4494
4495
4496 -- check whether patch can be applied
4497 SELECT evergreen.upgrade_deps_block_check('0608', :eg_version);
4498
4499 -- Add vqbr.import_error, vqbr.error_detail, and vqbr.matches.size to queue print output
4500
4501 UPDATE action_trigger.event_definition SET template = $$
4502 [%- USE date -%]
4503 <pre>
4504 Queue ID: [% target.0.queue.id %]
4505 Queue Name: [% target.0.queue.name %]
4506 Queue Type: [% target.0.queue.queue_type %]
4507 Complete? [% target.0.queue.complete %]
4508
4509     [% FOR vqbr IN target %]
4510 =-=-=
4511  Title of work    | [% helpers.get_queued_bib_attr('title',vqbr.attributes) %]
4512  Author of work   | [% helpers.get_queued_bib_attr('author',vqbr.attributes) %]
4513  Language of work | [% helpers.get_queued_bib_attr('language',vqbr.attributes) %]
4514  Pagination       | [% helpers.get_queued_bib_attr('pagination',vqbr.attributes) %]
4515  ISBN             | [% helpers.get_queued_bib_attr('isbn',vqbr.attributes) %]
4516  ISSN             | [% helpers.get_queued_bib_attr('issn',vqbr.attributes) %]
4517  Price            | [% helpers.get_queued_bib_attr('price',vqbr.attributes) %]
4518  Accession Number | [% helpers.get_queued_bib_attr('rec_identifier',vqbr.attributes) %]
4519  TCN Value        | [% helpers.get_queued_bib_attr('eg_tcn',vqbr.attributes) %]
4520  TCN Source       | [% helpers.get_queued_bib_attr('eg_tcn_source',vqbr.attributes) %]
4521  Internal ID      | [% helpers.get_queued_bib_attr('eg_identifier',vqbr.attributes) %]
4522  Publisher        | [% helpers.get_queued_bib_attr('publisher',vqbr.attributes) %]
4523  Publication Date | [% helpers.get_queued_bib_attr('pubdate',vqbr.attributes) %]
4524  Edition          | [% helpers.get_queued_bib_attr('edition',vqbr.attributes) %]
4525  Item Barcode     | [% helpers.get_queued_bib_attr('item_barcode',vqbr.attributes) %]
4526  Import Error     | [% vqbr.import_error %]
4527  Error Detail     | [% vqbr.error_detail %]
4528  Match Count      | [% vqbr.matches.size %]
4529
4530     [% END %]
4531 </pre>
4532 $$
4533 WHERE id = 39;
4534
4535
4536 -- Do the same for the CVS version
4537
4538 UPDATE action_trigger.event_definition SET template = $$
4539 [%- USE date -%]
4540 "Title of work","Author of work","Language of work","Pagination","ISBN","ISSN","Price","Accession Number","TCN Value","TCN Source","Internal ID","Publisher","Publication Date","Edition","Item Barcode","Import Error","Error Detail","Match Count"
4541 [% FOR vqbr IN target %]"[% helpers.get_queued_bib_attr('title',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('author',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('language',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('pagination',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('isbn',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('issn',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('price',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('rec_identifier',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('eg_tcn',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('eg_tcn_source',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('eg_identifier',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('publisher',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('pubdate',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('edition',vqbr.attributes) | replace('"', '""') %]","[% helpers.get_queued_bib_attr('item_barcode',vqbr.attributes) | replace('"', '""') %]","[% vqbr.import_error | replace('"', '""') %]","[% vqbr.error_detail | replace('"', '""') %]","[% vqbr.matches.size %]"
4542 [% END %]
4543 $$
4544 WHERE id = 40;
4545
4546 -- Add matches to the env for both
4547 INSERT INTO action_trigger.environment (event_def, path) VALUES (39, 'matches');
4548 INSERT INTO action_trigger.environment (event_def, path) VALUES (40, 'matches');
4549
4550
4551 -- Evergreen DB patch XXXX.data.acq-copy-creator-from-receiver.sql
4552
4553 -- check whether patch can be applied
4554 SELECT evergreen.upgrade_deps_block_check('0609', :eg_version);
4555
4556 ALTER TABLE acq.lineitem_detail 
4557     ADD COLUMN receiver INT REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED;
4558
4559
4560 -- Evergreen DB patch XXXX.data.acq-copy-creator-from-receiver.sql
4561
4562 -- check whether patch can be applied
4563 SELECT evergreen.upgrade_deps_block_check('0610', :eg_version);
4564
4565 INSERT INTO config.org_unit_setting_type ( name, label, description, datatype ) VALUES (
4566     'acq.copy_creator_uses_receiver',
4567     oils_i18n_gettext( 
4568         'acq.copy_creator_uses_receiver',
4569         'Acq: Set copy creator as receiver',
4570         'coust',
4571         'label'
4572     ),
4573     oils_i18n_gettext( 
4574         'acq.copy_creator_uses_receiver',
4575         'When receiving a copy in acquisitions, set the copy "creator" to be the staff that received the copy',
4576         'coust',
4577         'label'
4578     ),
4579     'bool'
4580 );
4581
4582 -- Evergreen DB patch 0611.data.magic_macros.sql
4583
4584 -- check whether patch can be applied
4585 SELECT evergreen.upgrade_deps_block_check('0611', :eg_version);
4586
4587 INSERT into config.org_unit_setting_type
4588 ( name, label, description, datatype ) VALUES
4589 (
4590         'circ.staff_client.receipt.header_text',
4591         oils_i18n_gettext(
4592             'circ.staff_client.receipt.header_text',
4593             'Receipt Template: Content of header_text include',
4594             'coust',
4595             'label'
4596         ),
4597         oils_i18n_gettext(
4598             'circ.staff_client.receipt.header_text',
4599             'Text/HTML/Macros to be inserted into receipt templates in place of %INCLUDE(header_text)%',
4600             'coust',
4601             'description'
4602         ),
4603         'string'
4604     )
4605 ,(
4606         'circ.staff_client.receipt.footer_text',
4607         oils_i18n_gettext(
4608             'circ.staff_client.receipt.footer_text',
4609             'Receipt Template: Content of footer_text include',
4610             'coust',
4611             'label'
4612         ),
4613         oils_i18n_gettext(
4614             'circ.staff_client.receipt.footer_text',
4615             'Text/HTML/Macros to be inserted into receipt templates in place of %INCLUDE(footer_text)%',
4616             'coust',
4617             'description'
4618         ),
4619         'string'
4620     )
4621 ,(
4622         'circ.staff_client.receipt.notice_text',
4623         oils_i18n_gettext(
4624             'circ.staff_client.receipt.notice_text',
4625             'Receipt Template: Content of notice_text include',
4626             'coust',
4627             'label'
4628         ),
4629         oils_i18n_gettext(
4630             'circ.staff_client.receipt.notice_text',
4631             'Text/HTML/Macros to be inserted into receipt templates in place of %INCLUDE(notice_text)%',
4632             'coust',
4633             'description'
4634         ),
4635         'string'
4636     )
4637 ,(
4638         'circ.staff_client.receipt.alert_text',
4639         oils_i18n_gettext(
4640             'circ.staff_client.receipt.alert_text',
4641             'Receipt Template: Content of alert_text include',
4642             'coust',
4643             'label'
4644         ),
4645         oils_i18n_gettext(
4646             'circ.staff_client.receipt.alert_text',
4647             'Text/HTML/Macros to be inserted into receipt templates in place of %INCLUDE(alert_text)%',
4648             'coust',
4649             'description'
4650         ),
4651         'string'
4652     )
4653 ,(
4654         'circ.staff_client.receipt.event_text',
4655         oils_i18n_gettext(
4656             'circ.staff_client.receipt.event_text',
4657             'Receipt Template: Content of event_text include',
4658             'coust',
4659             'label'
4660         ),
4661         oils_i18n_gettext(
4662             'circ.staff_client.receipt.event_text',
4663             'Text/HTML/Macros to be inserted into receipt templates in place of %INCLUDE(event_text)%',
4664             'coust',
4665             'description'
4666         ),
4667         'string'
4668     );
4669
4670 -- Evergreen DB patch 0612.schema.authority_overlay_protection.sql
4671 --
4672
4673
4674 -- check whether patch can be applied
4675 SELECT evergreen.upgrade_deps_block_check('0612', :eg_version);
4676
4677 -- FIXME: add/check SQL statements to perform the upgrade
4678
4679 -- Function to generate an ephemeral overlay template from an authority record
4680 CREATE OR REPLACE FUNCTION authority.generate_overlay_template (source_xml TEXT) RETURNS TEXT AS $f$
4681 DECLARE
4682     cset                INT;
4683     main_entry          authority.control_set_authority_field%ROWTYPE;
4684     bib_field           authority.control_set_bib_field%ROWTYPE;
4685     auth_id             INT DEFAULT oils_xpath_string('//*[@tag="901"]/*[local-name()="subfield" and @code="c"]', source_xml)::INT;
4686     replace_data        XML[] DEFAULT '{}'::XML[];
4687     replace_rules       TEXT[] DEFAULT '{}'::TEXT[];
4688     auth_field          XML[];
4689 BEGIN
4690     IF auth_id IS NULL THEN
4691         RETURN NULL;
4692     END IF;
4693
4694     -- Default to the LoC controll set
4695     SELECT control_set INTO cset FROM authority.record_entry WHERE id = auth_id;
4696
4697     -- if none, make a best guess
4698     IF cset IS NULL THEN
4699         SELECT  control_set INTO cset
4700           FROM  authority.control_set_authority_field
4701           WHERE tag IN (
4702                     SELECT  UNNEST(XPATH('//*[starts-with(@tag,"1")]/@tag',marc::XML)::TEXT[])
4703                       FROM  authority.record_entry
4704                       WHERE id = auth_id
4705                 )
4706           LIMIT 1;
4707     END IF;
4708
4709     -- if STILL none, no-op change
4710     IF cset IS NULL THEN
4711         RETURN XMLELEMENT(
4712             name record,
4713             XMLATTRIBUTES('http://www.loc.gov/MARC21/slim' AS xmlns),
4714             XMLELEMENT( name leader, '00881nam a2200193   4500'),
4715             XMLELEMENT(
4716                 name datafield,
4717                 XMLATTRIBUTES( '905' AS tag, ' ' AS ind1, ' ' AS ind2),
4718                 XMLELEMENT(
4719                     name subfield,
4720                     XMLATTRIBUTES('d' AS code),
4721                     '901c'
4722                 )
4723             )
4724         )::TEXT;
4725     END IF;
4726
4727     FOR main_entry IN SELECT * FROM authority.control_set_authority_field WHERE control_set = cset LOOP
4728         auth_field := XPATH('//*[@tag="'||main_entry.tag||'"][1]',source_xml::XML);
4729         IF ARRAY_LENGTH(auth_field,1) > 0 THEN
4730             FOR bib_field IN SELECT * FROM authority.control_set_bib_field WHERE authority_field = main_entry.id LOOP
4731                 replace_data := replace_data || XMLELEMENT( name datafield, XMLATTRIBUTES(bib_field.tag AS tag), XPATH('//*[local-name()="subfield"]',auth_field[1])::XML[]);
4732                 replace_rules := replace_rules || ( bib_field.tag || main_entry.sf_list || E'[0~\\)' || auth_id || '$]' );
4733             END LOOP;
4734             EXIT;
4735         END IF;
4736     END LOOP;
4737
4738     RETURN XMLELEMENT(
4739         name record,
4740         XMLATTRIBUTES('http://www.loc.gov/MARC21/slim' AS xmlns),
4741         XMLELEMENT( name leader, '00881nam a2200193   4500'),
4742         replace_data,
4743         XMLELEMENT(
4744             name datafield,
4745             XMLATTRIBUTES( '905' AS tag, ' ' AS ind1, ' ' AS ind2),
4746             XMLELEMENT(
4747                 name subfield,
4748                 XMLATTRIBUTES('r' AS code),
4749                 ARRAY_TO_STRING(replace_rules,',')
4750             )
4751         )
4752     )::TEXT;
4753 END;
4754 $f$ STABLE LANGUAGE PLPGSQL;
4755
4756
4757
4758 -- Evergreen DB patch 0613.schema.vandelay_isxn_normalization.sql
4759 --
4760
4761
4762 -- check whether patch can be applied
4763 SELECT evergreen.upgrade_deps_block_check('0613', :eg_version);
4764
4765 CREATE OR REPLACE FUNCTION vandelay.flatten_marc_hstore(
4766     record_xml TEXT
4767 ) RETURNS HSTORE AS $func$
4768 BEGIN
4769     RETURN (SELECT
4770         HSTORE(
4771             ARRAY_ACCUM(tag || (COALESCE(subfield, ''))),
4772             ARRAY_ACCUM(value)
4773         )
4774         FROM (
4775             SELECT  tag, subfield, ARRAY_ACCUM(value)::TEXT AS value
4776               FROM  (SELECT tag,
4777                             subfield,
4778                             CASE WHEN tag = '020' THEN -- caseless -- isbn
4779                                 LOWER((REGEXP_MATCHES(value,$$^(\S{10,17})$$))[1] || '%')
4780                             WHEN tag = '022' THEN -- caseless -- issn
4781                                 LOWER((REGEXP_MATCHES(value,$$^(\S{4}[- ]?\S{4})$$))[1] || '%')
4782                             WHEN tag = '024' THEN -- caseless -- upc (other)
4783                                 LOWER(value || '%')
4784                             ELSE
4785                                 value
4786                             END AS value
4787                       FROM  vandelay.flatten_marc(record_xml)) x
4788                 GROUP BY tag, subfield ORDER BY tag, subfield
4789         ) subquery
4790     );
4791 END;
4792 $func$ LANGUAGE PLPGSQL;
4793
4794 CREATE OR REPLACE FUNCTION vandelay._get_expr_push_jrow(
4795     node vandelay.match_set_point
4796 ) RETURNS VOID AS $$
4797 DECLARE
4798     jrow        TEXT;
4799     my_alias    TEXT;
4800     op          TEXT;
4801     tagkey      TEXT;
4802     caseless    BOOL;
4803 BEGIN
4804     -- remember $1 is tags_rstore, and $2 is svf_rstore
4805
4806     caseless := FALSE;
4807
4808     IF node.tag IS NOT NULL THEN
4809         caseless := (node.tag IN ('020', '022', '024'));
4810         tagkey := node.tag;
4811         IF node.subfield IS NOT NULL THEN
4812             tagkey := tagkey || node.subfield;
4813         END IF;
4814     END IF;
4815
4816     IF node.negate THEN
4817         IF caseless THEN
4818             op := 'NOT LIKE';
4819         ELSE
4820             op := '<>';
4821         END IF;
4822     ELSE
4823         IF caseless THEN
4824             op := 'LIKE';
4825         ELSE
4826             op := '=';
4827         END IF;
4828     END IF;
4829
4830     my_alias := 'n' || node.id::TEXT;
4831
4832     jrow := 'LEFT JOIN (SELECT *, ' || node.quality ||
4833         ' AS quality FROM metabib.';
4834     IF node.tag IS NOT NULL THEN
4835         jrow := jrow || 'full_rec) ' || my_alias || ' ON (' ||
4836             my_alias || '.record = bre.id AND ' || my_alias || '.tag = ''' ||
4837             node.tag || '''';
4838         IF node.subfield IS NOT NULL THEN
4839             jrow := jrow || ' AND ' || my_alias || '.subfield = ''' ||
4840                 node.subfield || '''';
4841         END IF;
4842         jrow := jrow || ' AND (';
4843
4844         IF caseless THEN
4845             jrow := jrow || 'LOWER(' || my_alias || '.value) ' || op;
4846         ELSE
4847             jrow := jrow || my_alias || '.value ' || op;
4848         END IF;
4849
4850         jrow := jrow || ' ANY(($1->''' || tagkey || ''')::TEXT[])))';
4851     ELSE    -- svf
4852         jrow := jrow || 'record_attr) ' || my_alias || ' ON (' ||
4853             my_alias || '.id = bre.id AND (' ||
4854             my_alias || '.attrs->''' || node.svf ||
4855             ''' ' || op || ' $2->''' || node.svf || '''))';
4856     END IF;
4857     INSERT INTO _vandelay_tmp_jrows (j) VALUES (jrow);
4858 END;
4859 $$ LANGUAGE PLPGSQL;
4860
4861
4862
4863 -- Evergreen DB patch XXXX.schema.generic-mapping-index-normalizer.sql
4864 --
4865
4866 -- check whether patch can be applied
4867 SELECT evergreen.upgrade_deps_block_check('0615', :eg_version);
4868
4869 -- evergreen.generic_map_normalizer 
4870
4871 CREATE OR REPLACE FUNCTION evergreen.generic_map_normalizer ( TEXT, TEXT ) RETURNS TEXT AS $f$
4872 my $string = shift;
4873 my %map;
4874
4875 my $default = $string;
4876
4877 $_ = shift;
4878 while (/^\s*?(.*?)\s*?=>\s*?(\S+)\s*/) {
4879     if ($1 eq '') {
4880         $default = $2;
4881     } else {
4882         $map{$2} = [split(/\s*,\s*/, $1)];
4883     }
4884     $_ = $';
4885 }
4886
4887 for my $key ( keys %map ) {
4888     return $key if (grep { $_ eq $string } @{ $map{$key} });
4889 }
4890
4891 return $default;
4892
4893 $f$ LANGUAGE PLPERLU;
4894
4895 -- evergreen.generic_map_normalizer 
4896
4897 INSERT INTO config.index_normalizer (name, description, func, param_count) VALUES (
4898     'Generic Mapping Normalizer', 
4899     'Map values or sets of values to new values',
4900     'generic_map_normalizer', 
4901     1
4902 );
4903
4904
4905 SELECT evergreen.upgrade_deps_block_check('0616', :eg_version);
4906
4907 CREATE OR REPLACE FUNCTION actor.org_unit_prox_update () RETURNS TRIGGER as $$
4908 BEGIN
4909
4910
4911 IF TG_OP = 'DELETE' THEN
4912
4913     DELETE FROM actor.org_unit_proximity WHERE (from_org = OLD.id or to_org= OLD.id);
4914
4915 END IF;
4916
4917 IF TG_OP = 'UPDATE' THEN
4918
4919     IF NEW.parent_ou <> OLD.parent_ou THEN
4920
4921         DELETE FROM actor.org_unit_proximity WHERE (from_org = OLD.id or to_org= OLD.id);
4922             INSERT INTO actor.org_unit_proximity (from_org, to_org, prox)
4923             SELECT  l.id, r.id, actor.org_unit_proximity(l.id,r.id)
4924                 FROM  actor.org_unit l, actor.org_unit r
4925                 WHERE (l.id = NEW.id or r.id = NEW.id);
4926
4927     END IF;
4928
4929 END IF;
4930
4931 IF TG_OP = 'INSERT' THEN
4932
4933      INSERT INTO actor.org_unit_proximity (from_org, to_org, prox)
4934      SELECT  l.id, r.id, actor.org_unit_proximity(l.id,r.id)
4935          FROM  actor.org_unit l, actor.org_unit r
4936          WHERE (l.id = NEW.id or r.id = NEW.id);
4937
4938 END IF;
4939
4940 RETURN null;
4941
4942 END;
4943 $$ LANGUAGE plpgsql;
4944
4945
4946 CREATE TRIGGER proximity_update_tgr AFTER INSERT OR UPDATE OR DELETE ON actor.org_unit FOR EACH ROW EXECUTE PROCEDURE actor.org_unit_prox_update ();
4947
4948
4949 SELECT evergreen.upgrade_deps_block_check('0617', :eg_version);
4950
4951 -- add notify columns to booking.reservation
4952 ALTER TABLE booking.reservation
4953   ADD COLUMN email_notify BOOLEAN NOT NULL DEFAULT FALSE;
4954
4955 -- create the hook and validator
4956 INSERT INTO action_trigger.hook (key, core_type, description, passive)
4957   VALUES ('reservation.available', 'bresv', 'A reservation is available for pickup', false);
4958 INSERT INTO action_trigger.validator (module, description)
4959   VALUES ('ReservationIsAvailable','Checked that a reserved resource is available for checkout');
4960
4961 -- create org unit setting to toggle checkbox display
4962 INSERT INTO config.org_unit_setting_type (name, label, description, datatype)
4963   VALUES ('booking.allow_email_notify', 'booking.allow_email_notify', 'Permit email notification when a reservation is ready for pickup.', 'bool');
4964
4965
4966 SELECT evergreen.upgrade_deps_block_check('0618', :eg_version);
4967
4968 UPDATE config.org_unit_setting_type SET description = E'The Regular Expression for validation on the day_phone field in patron registration. Note: The first capture group will be used for the "last 4 digits of phone number" feature, if enabled. Ex: "[2-9]\\d{2}-\\d{3}-(\\d{4})( x\\d+)?" will ignore the extension on a NANP number.' WHERE name = 'ui.patron.edit.au.day_phone.regex';
4969
4970 UPDATE config.org_unit_setting_type SET description = 'The Regular Expression for validation on phone fields in patron registration. Applies to all phone fields without their own setting. NOTE: See description of the day_phone regex for important information about capture groups with it.' WHERE name = 'ui.patron.edit.phone.regex';
4971
4972 UPDATE config.org_unit_setting_type SET description = oils_i18n_gettext('patron.password.use_phone', 'By default, use the last 4 alphanumeric characters of the patrons phone number as the default password when creating new users.  The exact characters used may be configured via the "GUI: Regex for day_phone field on patron registration" setting.', 'coust', 'description') WHERE name = 'patron.password.use_phone';
4973
4974 -- Evergreen DB patch 0619.schema.au_last_update_time.sql
4975
4976 -- check whether patch can be applied
4977 SELECT evergreen.upgrade_deps_block_check('0619', :eg_version);
4978
4979 -- Add new column last_update_time to actor.usr, with trigger to maintain it
4980 -- Add corresponding new column to auditor.actor_usr_history
4981
4982 ALTER TABLE actor.usr
4983         ADD COLUMN last_update_time TIMESTAMPTZ;
4984
4985 ALTER TABLE auditor.actor_usr_history
4986         ADD COLUMN last_update_time TIMESTAMPTZ;
4987
4988 CREATE OR REPLACE FUNCTION actor.au_updated()
4989 RETURNS TRIGGER AS $$
4990 BEGIN
4991     NEW.last_update_time := now();
4992         RETURN NEW;
4993 END;
4994 $$ LANGUAGE plpgsql;
4995
4996 CREATE TRIGGER au_update_trig
4997         BEFORE INSERT OR UPDATE ON actor.usr
4998         FOR EACH ROW EXECUTE PROCEDURE actor.au_updated();
4999
5000 -- Evergreen DB patch XXXX.data.opac_payment_history_age_limit.sql
5001
5002
5003 SELECT evergreen.upgrade_deps_block_check('0621', :eg_version);
5004
5005 INSERT into config.org_unit_setting_type (name, label, description, datatype)
5006 VALUES (
5007     'opac.payment_history_age_limit',
5008     oils_i18n_gettext('opac.payment_history_age_limit',
5009         'OPAC: Payment History Age Limit', 'coust', 'label'),
5010     oils_i18n_gettext('opac.payment_history_age_limit',
5011         'The OPAC should not display payments by patrons that are older than any interval defined here.', 'coust', 'label'),
5012     'interval'
5013 );
5014
5015 -- Updates config.org_unit_setting_type to remove the old tag prefixes for once 
5016 -- groups have been added.
5017 --
5018
5019 SELECT evergreen.upgrade_deps_block_check('0622', :eg_version);
5020
5021 INSERT INTO config.settings_group (name, label) VALUES
5022 ('sys', oils_i18n_gettext('config.settings_group.system', 'System', 'coust', 'label')),
5023 ('gui', oils_i18n_gettext('config.settings_group.gui', 'GUI', 'coust', 'label')),
5024 ('lib', oils_i18n_gettext('config.settings_group.lib', 'Library', 'coust', 'label')),
5025 ('sec', oils_i18n_gettext('config.settings_group.sec', 'Security', 'coust', 'label')),
5026 ('cat', oils_i18n_gettext('config.settings_group.cat', 'Cataloging', 'coust', 'label')),
5027 ('holds', oils_i18n_gettext('config.settings_group.holds', 'Holds', 'coust', 'label')),
5028 ('circ', oils_i18n_gettext('config.settings_group.circulation', 'Circulation', 'coust', 'label')),
5029 ('self', oils_i18n_gettext('config.settings_group.self', 'Self Check', 'coust', 'label')),
5030 ('opac', oils_i18n_gettext('config.settings_group.opac', 'OPAC', 'coust', 'label')),
5031 ('prog', oils_i18n_gettext('config.settings_group.program', 'Program', 'coust', 'label')),
5032 ('glob', oils_i18n_gettext('config.settings_group.global', 'Global', 'coust', 'label')),
5033 ('finance', oils_i18n_gettext('config.settings_group.finances', 'Finanaces', 'coust', 'label')),
5034 ('credit', oils_i18n_gettext('config.settings_group.ccp', 'Credit Card Processing', 'coust', 'label')),
5035 ('serial', oils_i18n_gettext('config.settings_group.serial', 'Serials', 'coust', 'label')),
5036 ('recall', oils_i18n_gettext('config.settings_group.recall', 'Recalls', 'coust', 'label')),
5037 ('booking', oils_i18n_gettext('config.settings_group.booking', 'Booking', 'coust', 'label')),
5038 ('offline', oils_i18n_gettext('config.settings_group.offline', 'Offline', 'coust', 'label')),
5039 ('receipt_template', oils_i18n_gettext('config.settings_group.receipt_template', 'Receipt Template', 'coust', 'label'));
5040
5041 UPDATE config.org_unit_setting_type SET grp = 'lib', label='Set copy creator as receiver' WHERE name = 'acq.copy_creator_uses_receiver';
5042 UPDATE config.org_unit_setting_type SET grp = 'lib' WHERE name = 'acq.default_circ_modifier';
5043 UPDATE config.org_unit_setting_type SET grp = 'lib' WHERE name = 'acq.default_copy_location';
5044 UPDATE config.org_unit_setting_type SET grp = 'finance' WHERE name = 'acq.fund.balance_limit.block';
5045 UPDATE config.org_unit_setting_type SET grp = 'finance' WHERE name = 'acq.fund.balance_limit.warn';
5046 UPDATE config.org_unit_setting_type SET grp = 'lib' WHERE name = 'acq.holds.allow_holds_from_purchase_request';
5047 UPDATE config.org_unit_setting_type SET grp = 'lib' WHERE name = 'acq.tmp_barcode_prefix';
5048 UPDATE config.org_unit_setting_type SET grp = 'lib' WHERE name = 'acq.tmp_callnumber_prefix';
5049 UPDATE config.org_unit_setting_type SET grp = 'sec' WHERE name = 'auth.opac_timeout';
5050 UPDATE config.org_unit_setting_type SET grp = 'sec' WHERE name = 'auth.persistent_login_interval';
5051 UPDATE config.org_unit_setting_type SET grp = 'sec' WHERE name = 'auth.staff_timeout';
5052 UPDATE config.org_unit_setting_type SET grp = 'booking' WHERE name = 'booking.allow_email_notify';
5053 UPDATE config.org_unit_setting_type SET grp = 'gui' WHERE name = 'cat.bib.alert_on_empty';
5054 UPDATE config.org_unit_setting_type SET grp = 'cat', label='Delete bib if all copies are deleted via Acquisitions lineitem cancellation.' WHERE name = 'cat.bib.delete_on_no_copy_via_acq_lineitem_cancel';
5055 UPDATE config.org_unit_setting_type SET grp = 'prog' WHERE name = 'cat.bib.keep_on_empty';
5056 UPDATE config.org_unit_setting_type SET grp = 'cat', label='Default Classification Scheme' WHERE name = 'cat.default_classification_scheme';
5057 UPDATE config.org_unit_setting_type SET grp = 'cat', label='Default copy status (fast add)' WHERE name = 'cat.default_copy_status_fast';
5058 UPDATE config.org_unit_setting_type SET grp = 'cat', label='Default copy status (normal)' WHERE name = 'cat.default_copy_status_normal';
5059 UPDATE config.org_unit_setting_type SET grp = 'finance' WHERE name = 'cat.default_item_price';
5060 UPDATE config.org_unit_setting_type SET grp = 'cat', label='Spine and pocket label font family' WHERE name = 'cat.label.font.family';
5061 UPDATE config.org_unit_setting_type SET grp = 'cat', label='Spine and pocket label font size' WHERE name = 'cat.label.font.size';
5062 UPDATE config.org_unit_setting_type SET grp = 'cat', label='Spine and pocket label font weight' WHERE name = 'cat.label.font.weight';
5063 UPDATE config.org_unit_setting_type SET grp = 'cat', label='Defines the control number identifier used in 003 and 035 fields.' WHERE name = 'cat.marc_control_number_identifier';
5064 UPDATE config.org_unit_setting_type SET grp = 'cat', label='Spine label maximum lines' WHERE name = 'cat.spine.line.height';
5065 UPDATE config.org_unit_setting_type SET grp = 'cat', label='Spine label left margin' WHERE name = 'cat.spine.line.margin';
5066 UPDATE config.org_unit_setting_type SET grp = 'cat', label='Spine label line width' WHERE name = 'cat.spine.line.width';
5067 UPDATE config.org_unit_setting_type SET grp = 'cat', label='Delete volume with last copy' WHERE name = 'cat.volume.delete_on_empty';
5068 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Toggle off the patron summary sidebar after first view.' WHERE name = 'circ.auto_hide_patron_summary';
5069 UPDATE config.org_unit_setting_type SET grp = 'holds', label='Block Renewal of Items Needed for Holds' WHERE name = 'circ.block_renews_for_holds';
5070 UPDATE config.org_unit_setting_type SET grp = 'booking', label='Elbow room' WHERE name = 'circ.booking_reservation.default_elbow_room';
5071 UPDATE config.org_unit_setting_type SET grp = 'finance' WHERE name = 'circ.charge_lost_on_zero';
5072 UPDATE config.org_unit_setting_type SET grp = 'finance' WHERE name = 'circ.charge_on_damaged';
5073 UPDATE config.org_unit_setting_type SET grp = 'circ' WHERE name = 'circ.checkout_auto_renew_age';
5074 UPDATE config.org_unit_setting_type SET grp = 'circ' WHERE name = 'circ.checkout_fills_related_hold';
5075 UPDATE config.org_unit_setting_type SET grp = 'circ' WHERE name = 'circ.checkout_fills_related_hold_exact_match_only';
5076 UPDATE config.org_unit_setting_type SET grp = 'lib' WHERE name = 'circ.claim_never_checked_out.mark_missing';
5077 UPDATE config.org_unit_setting_type SET grp = 'lib' WHERE name = 'circ.claim_return.copy_status';
5078 UPDATE config.org_unit_setting_type SET grp = 'lib' WHERE name = 'circ.damaged.void_ovedue';
5079 UPDATE config.org_unit_setting_type SET grp = 'finance' WHERE name = 'circ.damaged_item_processing_fee';
5080 UPDATE config.org_unit_setting_type SET grp = 'circ', label='Do not include outstanding Claims Returned circulations in lump sum tallies in Patron Display.' WHERE name = 'circ.do_not_tally_claims_returned';
5081 UPDATE config.org_unit_setting_type SET grp = 'holds', label='Hard boundary' WHERE name = 'circ.hold_boundary.hard';
5082 UPDATE config.org_unit_setting_type SET grp = 'holds', label='Soft boundary' WHERE name = 'circ.hold_boundary.soft';
5083 UPDATE config.org_unit_setting_type SET grp = 'holds', label='Expire Alert Interval' WHERE name = 'circ.hold_expire_alert_interval';
5084 UPDATE config.org_unit_setting_type SET grp = 'holds', label='Expire Interval' WHERE name = 'circ.hold_expire_interval';
5085 UPDATE config.org_unit_setting_type SET grp = 'circ' WHERE name = 'circ.hold_shelf_status_delay';
5086 UPDATE config.org_unit_setting_type SET grp = 'holds', label='Soft stalling interval' WHERE name = 'circ.hold_stalling.soft';
5087 UPDATE config.org_unit_setting_type SET grp = 'holds', label='Hard stalling interval' WHERE name = 'circ.hold_stalling_hard';
5088 UPDATE config.org_unit_setting_type SET grp = 'holds', label='Use Active Date for Age Protection' WHERE name = 'circ.holds.age_protect.active_date';
5089 UPDATE config.org_unit_setting_type SET grp = 'holds', label='Behind Desk Pickup Supported' WHERE name = 'circ.holds.behind_desk_pickup_supported';
5090 UPDATE config.org_unit_setting_type SET grp = 'holds', label='Canceled holds display age' WHERE name = 'circ.holds.canceled.display_age';
5091 UPDATE config.org_unit_setting_type SET grp = 'holds', label='Canceled holds display count' WHERE name = 'circ.holds.canceled.display_count';
5092 UPDATE config.org_unit_setting_type SET grp = 'holds', label='Clear shelf copy status' WHERE name = 'circ.holds.clear_shelf.copy_status';
5093 UPDATE config.org_unit_setting_type SET grp = 'holds', label='Bypass hold capture during clear shelf process' WHERE name = 'circ.holds.clear_shelf.no_capture_holds';
5094 UPDATE config.org_unit_setting_type SET grp = 'holds', label='Default Estimated Wait' WHERE name = 'circ.holds.default_estimated_wait_interval';
5095 UPDATE config.org_unit_setting_type SET grp = 'holds' WHERE name = 'circ.holds.default_shelf_expire_interval';
5096 UPDATE config.org_unit_setting_type SET grp = 'circ', label='Block hold request if hold recipient privileges have expired' WHERE name = 'circ.holds.expired_patron_block';
5097 UPDATE config.org_unit_setting_type SET grp = 'holds', label='Has Local Copy Alert' WHERE name = 'circ.holds.hold_has_copy_at.alert';
5098 UPDATE config.org_unit_setting_type SET grp = 'holds', label='Has Local Copy Block' WHERE name = 'circ.holds.hold_has_copy_at.block';
5099 UPDATE config.org_unit_setting_type SET grp = 'holds', label='Maximum library target attempts' WHERE name = 'circ.holds.max_org_unit_target_loops';
5100 UPDATE config.org_unit_setting_type SET grp = 'holds', label='Minimum Estimated Wait' WHERE name = 'circ.holds.min_estimated_wait_interval';
5101 UPDATE config.org_unit_setting_type SET grp = 'holds', label='Org Unit Target Weight' WHERE name = 'circ.holds.org_unit_target_weight';
5102 UPDATE config.org_unit_setting_type SET grp = 'recall', label='An array of fine amount, fine interval, and maximum fine.' WHERE name = 'circ.holds.recall_fine_rules';
5103 UPDATE config.org_unit_setting_type SET grp = 'recall', label='Truncated loan period.' WHERE name = 'circ.holds.recall_return_interval';
5104 UPDATE config.org_unit_setting_type SET grp = 'recall', label='Circulation duration that triggers a recall.' WHERE name = 'circ.holds.recall_threshold';
5105 UPDATE config.org_unit_setting_type SET grp = 'holds', label='Use weight-based hold targeting' WHERE name = 'circ.holds.target_holds_by_org_unit_weight';
5106 UPDATE config.org_unit_setting_type SET grp = 'holds' WHERE name = 'circ.holds.target_skip_me';
5107 UPDATE config.org_unit_setting_type SET grp = 'holds', label='Reset request time on un-cancel' WHERE name = 'circ.holds.uncancel.reset_request_time';
5108 UPDATE config.org_unit_setting_type SET grp = 'holds', label='FIFO' WHERE name = 'circ.holds_fifo';
5109 UPDATE config.org_unit_setting_type SET grp = 'gui' WHERE name = 'circ.item_checkout_history.max';
5110 UPDATE config.org_unit_setting_type SET grp = 'circ', label='Lost Checkin Generates New Overdues' WHERE name = 'circ.lost.generate_overdue_on_checkin';
5111 UPDATE config.org_unit_setting_type SET grp = 'circ', label='Lost items usable on checkin' WHERE name = 'circ.lost_immediately_available';
5112 UPDATE config.org_unit_setting_type SET grp = 'finance' WHERE name = 'circ.lost_materials_processing_fee';
5113 UPDATE config.org_unit_setting_type SET grp = 'circ', label='Void lost max interval' WHERE name = 'circ.max_accept_return_of_lost';
5114 UPDATE config.org_unit_setting_type SET grp = 'circ', label='Cap Max Fine at Item Price' WHERE name = 'circ.max_fine.cap_at_price';
5115 UPDATE config.org_unit_setting_type SET grp = 'circ' WHERE name = 'circ.max_patron_claim_return_count';
5116 UPDATE config.org_unit_setting_type SET grp = 'circ', label='Item Status for Missing Pieces' WHERE name = 'circ.missing_pieces.copy_status';
5117 UPDATE config.org_unit_setting_type SET grp = 'sec' WHERE name = 'circ.obscure_dob';
5118 UPDATE config.org_unit_setting_type SET grp = 'offline', label='Skip offline checkin if newer item Status Changed Time.' WHERE name = 'circ.offline.skip_checkin_if_newer_status_changed_time';
5119 UPDATE config.org_unit_setting_type SET grp = 'offline', label='Skip offline checkout if newer item Status Changed Time.' WHERE name = 'circ.offline.skip_checkout_if_newer_status_changed_time';
5120 UPDATE config.org_unit_setting_type SET grp = 'offline', label='Skip offline renewal if newer item Status Changed Time.' WHERE name = 'circ.offline.skip_renew_if_newer_status_changed_time';
5121 UPDATE config.org_unit_setting_type SET grp = 'sec', label='Offline: Patron Usernames Allowed' WHERE name = 'circ.offline.username_allowed';
5122 UPDATE config.org_unit_setting_type SET grp = 'sec', label='Maximum concurrently active self-serve password reset requests per user' WHERE name = 'circ.password_reset_request_per_user_limit';
5123 UPDATE config.org_unit_setting_type SET grp = 'circ', label='Require matching email address for password reset requests' WHERE name = 'circ.password_reset_request_requires_matching_email';
5124 UPDATE config.org_unit_setting_type SET grp = 'sec', label='Maximum concurrently active self-serve password reset requests' WHERE name = 'circ.password_reset_request_throttle';
5125 UPDATE config.org_unit_setting_type SET grp = 'sec', label='Self-serve password reset request time-to-live' WHERE name = 'circ.password_reset_request_time_to_live';
5126 UPDATE config.org_unit_setting_type SET grp = 'circ', label='Patron Registration: Cloned patrons get address copy' WHERE name = 'circ.patron_edit.clone.copy_address';
5127 UPDATE config.org_unit_setting_type SET grp = 'circ' WHERE name = 'circ.patron_invalid_address_apply_penalty';
5128 UPDATE config.org_unit_setting_type SET grp = 'lib' WHERE name = 'circ.pre_cat_copy_circ_lib';
5129 UPDATE config.org_unit_setting_type SET grp = 'lib' WHERE name = 'circ.reshelving_complete.interval';
5130 UPDATE config.org_unit_setting_type SET grp = 'circ', label='Restore overdues on lost item return' WHERE name = 'circ.restore_overdue_on_lost_return';
5131 UPDATE config.org_unit_setting_type SET grp = 'self', label='Pop-up alert for errors' WHERE name = 'circ.selfcheck.alert.popup';
5132 UPDATE config.org_unit_setting_type SET grp = 'self', label='Audio Alerts' WHERE name = 'circ.selfcheck.alert.sound';
5133 UPDATE config.org_unit_setting_type SET grp = 'self' WHERE name = 'circ.selfcheck.auto_override_checkout_events';
5134 UPDATE config.org_unit_setting_type SET grp = 'self', label='Block copy checkout status' WHERE name = 'circ.selfcheck.block_checkout_on_copy_status';
5135 UPDATE config.org_unit_setting_type SET grp = 'self', label='Patron Login Timeout (in seconds)' WHERE name = 'circ.selfcheck.patron_login_timeout';
5136 UPDATE config.org_unit_setting_type SET grp = 'self', label='Require Patron Password' WHERE name = 'circ.selfcheck.patron_password_required';
5137 UPDATE config.org_unit_setting_type SET grp = 'self', label='Require patron password' WHERE name = 'circ.selfcheck.require_patron_password';
5138 UPDATE config.org_unit_setting_type SET grp = 'self', label='Workstation Required' WHERE name = 'circ.selfcheck.workstation_required';
5139 UPDATE config.org_unit_setting_type SET grp = 'circ' WHERE name = 'circ.staff_client.actor_on_checkout';
5140 UPDATE config.org_unit_setting_type SET grp = 'prog' WHERE name = 'circ.staff_client.do_not_auto_attempt_print';
5141 UPDATE config.org_unit_setting_type SET grp = 'receipt_template', label='Content of alert_text include' WHERE name = 'circ.staff_client.receipt.alert_text';
5142 UPDATE config.org_unit_setting_type SET grp = 'receipt_template', label='Content of event_text include' WHERE name = 'circ.staff_client.receipt.event_text';
5143 UPDATE config.org_unit_setting_type SET grp = 'receipt_template', label='Content of footer_text include' WHERE name = 'circ.staff_client.receipt.footer_text';
5144 UPDATE config.org_unit_setting_type SET grp = 'receipt_template', label='Content of header_text include' WHERE name = 'circ.staff_client.receipt.header_text';
5145 UPDATE config.org_unit_setting_type SET grp = 'receipt_template', label='Content of notice_text include' WHERE name = 'circ.staff_client.receipt.notice_text';
5146 UPDATE config.org_unit_setting_type SET grp = 'circ', label='Minimum Transit Checkin Interval' WHERE name = 'circ.transit.min_checkin_interval';
5147 UPDATE config.org_unit_setting_type SET grp = 'circ', label='Patron Merge Deactivate Card' WHERE name = 'circ.user_merge.deactivate_cards';
5148 UPDATE config.org_unit_setting_type SET grp = 'circ', label='Patron Merge Address Delete' WHERE name = 'circ.user_merge.delete_addresses';
5149 UPDATE config.org_unit_setting_type SET grp = 'circ', label='Patron Merge Barcode Delete' WHERE name = 'circ.user_merge.delete_cards';
5150 UPDATE config.org_unit_setting_type SET grp = 'circ', label='Void lost item billing when returned' WHERE name = 'circ.void_lost_on_checkin';
5151 UPDATE config.org_unit_setting_type SET grp = 'circ', label='Void processing fee on lost item return' WHERE name = 'circ.void_lost_proc_fee_on_checkin';
5152 UPDATE config.org_unit_setting_type SET grp = 'finance', label='Void overdue fines when items are marked lost' WHERE name = 'circ.void_overdue_on_lost';
5153 UPDATE config.org_unit_setting_type SET grp = 'finance' WHERE name = 'credit.payments.allow';
5154 UPDATE config.org_unit_setting_type SET grp = 'credit', label='Enable AuthorizeNet payments' WHERE name = 'credit.processor.authorizenet.enabled';
5155 UPDATE config.org_unit_setting_type SET grp = 'credit', label='AuthorizeNet login' WHERE name = 'credit.processor.authorizenet.login';
5156 UPDATE config.org_unit_setting_type SET grp = 'credit', label='AuthorizeNet password' WHERE name = 'credit.processor.authorizenet.password';
5157 UPDATE config.org_unit_setting_type SET grp = 'credit', label='AuthorizeNet server' WHERE name = 'credit.processor.authorizenet.server';
5158 UPDATE config.org_unit_setting_type SET grp = 'credit', label='AuthorizeNet test mode' WHERE name = 'credit.processor.authorizenet.testmode';
5159 UPDATE config.org_unit_setting_type SET grp = 'credit', label='Name default credit processor' WHERE name = 'credit.processor.default';
5160 UPDATE config.org_unit_setting_type SET grp = 'credit', label='Enable PayflowPro payments' WHERE name = 'credit.processor.payflowpro.enabled';
5161 UPDATE config.org_unit_setting_type SET grp = 'credit', label='PayflowPro login/merchant ID' WHERE name = 'credit.processor.payflowpro.login';
5162 UPDATE config.org_unit_setting_type SET grp = 'credit', label='PayflowPro partner' WHERE name = 'credit.processor.payflowpro.partner';
5163 UPDATE config.org_unit_setting_type SET grp = 'credit', label='PayflowPro password' WHERE name = 'credit.processor.payflowpro.password';
5164 UPDATE config.org_unit_setting_type SET grp = 'credit', label='PayflowPro test mode' WHERE name = 'credit.processor.payflowpro.testmode';
5165 UPDATE config.org_unit_setting_type SET grp = 'credit', label='PayflowPro vendor' WHERE name = 'credit.processor.payflowpro.vendor';
5166 UPDATE config.org_unit_setting_type SET grp = 'credit', label='Enable PayPal payments' WHERE name = 'credit.processor.paypal.enabled';
5167 UPDATE config.org_unit_setting_type SET grp = 'credit', label='PayPal login' WHERE name = 'credit.processor.paypal.login';
5168 UPDATE config.org_unit_setting_type SET grp = 'credit', label='PayPal password' WHERE name = 'credit.processor.paypal.password';
5169 UPDATE config.org_unit_setting_type SET grp = 'credit', label='PayPal signature' WHERE name = 'credit.processor.paypal.signature';
5170 UPDATE config.org_unit_setting_type SET grp = 'credit', label='PayPal test mode' WHERE name = 'credit.processor.paypal.testmode';
5171 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Format Dates with this pattern.' WHERE name = 'format.date';
5172 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Format Times with this pattern.' WHERE name = 'format.time';
5173 UPDATE config.org_unit_setting_type SET grp = 'glob' WHERE name = 'global.default_locale';
5174 UPDATE config.org_unit_setting_type SET grp = 'lib' WHERE name = 'global.juvenile_age_threshold';
5175 UPDATE config.org_unit_setting_type SET grp = 'glob' WHERE name = 'global.password_regex';
5176 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Disable the ability to save list column configurations locally.' WHERE name = 'gui.disable_local_save_columns';
5177 UPDATE config.org_unit_setting_type SET grp = 'lib', label='Courier Code' WHERE name = 'lib.courier_code';
5178 UPDATE config.org_unit_setting_type SET grp = 'lib' WHERE name = 'notice.telephony.callfile_lines';
5179 UPDATE config.org_unit_setting_type SET grp = 'opac', label='Allow pending addresses' WHERE name = 'opac.allow_pending_address';
5180 UPDATE config.org_unit_setting_type SET grp = 'glob' WHERE name = 'opac.barcode_regex';
5181 UPDATE config.org_unit_setting_type SET grp = 'opac', label='Use fully compressed serial holdings' WHERE name = 'opac.fully_compressed_serial_holdings';
5182 UPDATE config.org_unit_setting_type SET grp = 'opac', label='Org Unit Hiding Depth' WHERE name = 'opac.org_unit_hiding.depth';
5183 UPDATE config.org_unit_setting_type SET grp = 'opac', label='Payment History Age Limit' WHERE name = 'opac.payment_history_age_limit';
5184 UPDATE config.org_unit_setting_type SET grp = 'prog' WHERE name = 'org.bounced_emails';
5185 UPDATE config.org_unit_setting_type SET grp = 'sec', label='Patron Opt-In Boundary' WHERE name = 'org.patron_opt_boundary';
5186 UPDATE config.org_unit_setting_type SET grp = 'sec', label='Patron Opt-In Default' WHERE name = 'org.patron_opt_default';
5187 UPDATE config.org_unit_setting_type SET grp = 'sec' WHERE name = 'patron.password.use_phone';
5188 UPDATE config.org_unit_setting_type SET grp = 'serial', label='Previous Issuance Copy Location' WHERE name = 'serial.prev_issuance_copy_location';
5189 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Work Log: Maximum Patrons Logged' WHERE name = 'ui.admin.patron_log.max_entries';
5190 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Work Log: Maximum Actions Logged' WHERE name = 'ui.admin.work_log.max_entries';
5191 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Horizontal layout for Volume/Copy Creator/Editor.' WHERE name = 'ui.cat.volume_copy_editor.horizontal';
5192 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Uncheck bills by default in the patron billing interface' WHERE name = 'ui.circ.billing.uncheck_bills_and_unfocus_payment_box';
5193 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Record In-House Use: Maximum # of uses allowed per entry.' WHERE name = 'ui.circ.in_house_use.entry_cap';
5194 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Record In-House Use: # of uses threshold for Are You Sure? dialog.' WHERE name = 'ui.circ.in_house_use.entry_warn';
5195 UPDATE config.org_unit_setting_type SET grp = 'gui' WHERE name = 'ui.circ.patron_summary.horizontal';
5196 UPDATE config.org_unit_setting_type SET grp = 'gui' WHERE name = 'ui.circ.show_billing_tab_on_bills';
5197 UPDATE config.org_unit_setting_type SET grp = 'circ', label='Suppress popup-dialogs during check-in.' WHERE name = 'ui.circ.suppress_checkin_popups';
5198 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Button bar' WHERE name = 'ui.general.button_bar';
5199 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Default Hotkeyset' WHERE name = 'ui.general.hotkeyset';
5200 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Idle timeout' WHERE name = 'ui.general.idle_timeout';
5201 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Default Country for New Addresses in Patron Editor' WHERE name = 'ui.patron.default_country';
5202 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Default Ident Type for Patron Registration' WHERE name = 'ui.patron.default_ident_type';
5203 UPDATE config.org_unit_setting_type SET grp = 'sec', label='Default level of patrons'' internet access' WHERE name = 'ui.patron.default_inet_access_level';
5204 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Show active field on patron registration' WHERE name = 'ui.patron.edit.au.active.show';
5205 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Suggest active field on patron registration' WHERE name = 'ui.patron.edit.au.active.suggest';
5206 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Show alert_message field on patron registration' WHERE name = 'ui.patron.edit.au.alert_message.show';
5207 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Suggest alert_message field on patron registration' WHERE name = 'ui.patron.edit.au.alert_message.suggest';
5208 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Show alias field on patron registration' WHERE name = 'ui.patron.edit.au.alias.show';
5209 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Suggest alias field on patron registration' WHERE name = 'ui.patron.edit.au.alias.suggest';
5210 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Show barred field on patron registration' WHERE name = 'ui.patron.edit.au.barred.show';
5211 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Suggest barred field on patron registration' WHERE name = 'ui.patron.edit.au.barred.suggest';
5212 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Show claims_never_checked_out_count field on patron registration' WHERE name = 'ui.patron.edit.au.claims_never_checked_out_count.show';
5213 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Suggest claims_never_checked_out_count field on patron registration' WHERE name = 'ui.patron.edit.au.claims_never_checked_out_count.suggest';
5214 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Show claims_returned_count field on patron registration' WHERE name = 'ui.patron.edit.au.claims_returned_count.show';
5215 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Suggest claims_returned_count field on patron registration' WHERE name = 'ui.patron.edit.au.claims_returned_count.suggest';
5216 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Example for day_phone field on patron registration' WHERE name = 'ui.patron.edit.au.day_phone.example';
5217 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Regex for day_phone field on patron registration' WHERE name = 'ui.patron.edit.au.day_phone.regex';
5218 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Require day_phone field on patron registration' WHERE name = 'ui.patron.edit.au.day_phone.require';
5219 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Show day_phone field on patron registration' WHERE name = 'ui.patron.edit.au.day_phone.show';
5220 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Suggest day_phone field on patron registration' WHERE name = 'ui.patron.edit.au.day_phone.suggest';
5221 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Show calendar widget for dob field on patron registration' WHERE name = 'ui.patron.edit.au.dob.calendar';
5222 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Require dob field on patron registration' WHERE name = 'ui.patron.edit.au.dob.require';
5223 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Show dob field on patron registration' WHERE name = 'ui.patron.edit.au.dob.show';
5224 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Suggest dob field on patron registration' WHERE name = 'ui.patron.edit.au.dob.suggest';
5225 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Example for email field on patron registration' WHERE name = 'ui.patron.edit.au.email.example';
5226 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Regex for email field on patron registration' WHERE name = 'ui.patron.edit.au.email.regex';
5227 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Require email field on patron registration' WHERE name = 'ui.patron.edit.au.email.require';
5228 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Show email field on patron registration' WHERE name = 'ui.patron.edit.au.email.show';
5229 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Suggest email field on patron registration' WHERE name = 'ui.patron.edit.au.email.suggest';
5230 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Example for evening_phone field on patron registration' WHERE name = 'ui.patron.edit.au.evening_phone.example';
5231 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Regex for evening_phone field on patron registration' WHERE name = 'ui.patron.edit.au.evening_phone.regex';
5232 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Require evening_phone field on patron registration' WHERE name = 'ui.patron.edit.au.evening_phone.require';
5233 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Show evening_phone field on patron registration' WHERE name = 'ui.patron.edit.au.evening_phone.show';
5234 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Suggest evening_phone field on patron registration' WHERE name = 'ui.patron.edit.au.evening_phone.suggest';
5235 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Show ident_value field on patron registration' WHERE name = 'ui.patron.edit.au.ident_value.show';
5236 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Suggest ident_value field on patron registration' WHERE name = 'ui.patron.edit.au.ident_value.suggest';
5237 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Show ident_value2 field on patron registration' WHERE name = 'ui.patron.edit.au.ident_value2.show';
5238 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Suggest ident_value2 field on patron registration' WHERE name = 'ui.patron.edit.au.ident_value2.suggest';
5239 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Show juvenile field on patron registration' WHERE name = 'ui.patron.edit.au.juvenile.show';
5240 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Suggest juvenile field on patron registration' WHERE name = 'ui.patron.edit.au.juvenile.suggest';
5241 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Show master_account field on patron registration' WHERE name = 'ui.patron.edit.au.master_account.show';
5242 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Suggest master_account field on patron registration' WHERE name = 'ui.patron.edit.au.master_account.suggest';
5243 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Example for other_phone field on patron registration' WHERE name = 'ui.patron.edit.au.other_phone.example';
5244 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Regex for other_phone field on patron registration' WHERE name = 'ui.patron.edit.au.other_phone.regex';
5245 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Require other_phone field on patron registration' WHERE name = 'ui.patron.edit.au.other_phone.require';
5246 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Show other_phone field on patron registration' WHERE name = 'ui.patron.edit.au.other_phone.show';
5247 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Suggest other_phone field on patron registration' WHERE name = 'ui.patron.edit.au.other_phone.suggest';
5248 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Show second_given_name field on patron registration' WHERE name = 'ui.patron.edit.au.second_given_name.show';
5249 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Suggest second_given_name field on patron registration' WHERE name = 'ui.patron.edit.au.second_given_name.suggest';
5250 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Show suffix field on patron registration' WHERE name = 'ui.patron.edit.au.suffix.show';
5251 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Suggest suffix field on patron registration' WHERE name = 'ui.patron.edit.au.suffix.suggest';
5252 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Require county field on patron registration' WHERE name = 'ui.patron.edit.aua.county.require';
5253 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Example for post_code field on patron registration' WHERE name = 'ui.patron.edit.aua.post_code.example';
5254 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Regex for post_code field on patron registration' WHERE name = 'ui.patron.edit.aua.post_code.regex';
5255 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Default showing suggested patron registration fields' WHERE name = 'ui.patron.edit.default_suggested';
5256 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Example for phone fields on patron registration' WHERE name = 'ui.patron.edit.phone.example';
5257 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Regex for phone fields on patron registration' WHERE name = 'ui.patron.edit.phone.regex';
5258 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Require at least one address for Patron Registration' WHERE name = 'ui.patron.registration.require_address';
5259 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Cap results in Patron Search at this number.' WHERE name = 'ui.patron_search.result_cap';
5260 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Require staff initials for entry/edit of item/patron/penalty notes/messages.' WHERE name = 'ui.staff.require_initials';
5261 UPDATE config.org_unit_setting_type SET grp = 'gui', label='Unified Volume/Item Creator/Editor' WHERE name = 'ui.unified_volume_copy_editor';
5262 UPDATE config.org_unit_setting_type SET grp = 'gui', label='URL for remote directory containing list column settings.' WHERE name = 'url.remote_column_settings';
5263
5264
5265
5266
5267 SELECT evergreen.upgrade_deps_block_check('0623', :eg_version);
5268
5269
5270 CREATE TABLE config.org_unit_setting_type_log (
5271     id              BIGSERIAL   PRIMARY KEY,
5272     date_applied    TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
5273     org             INT         REFERENCES actor.org_unit (id),
5274     original_value  TEXT,
5275     new_value       TEXT,
5276     field_name      TEXT      REFERENCES config.org_unit_setting_type (name)
5277 );
5278
5279 -- Log each change in oust to oustl, so admins can see what they messed up if someting stops working.
5280 CREATE OR REPLACE FUNCTION ous_change_log() RETURNS TRIGGER AS $ous_change_log$
5281     DECLARE
5282     original TEXT;
5283     BEGIN
5284         -- Check for which setting is being updated, and log it.
5285         SELECT INTO original value FROM actor.org_unit_setting WHERE name = NEW.name AND org_unit = NEW.org_unit;
5286                 
5287         INSERT INTO config.org_unit_setting_type_log (org,original_value,new_value,field_name) VALUES (NEW.org_unit, original, NEW.value, NEW.name);
5288         
5289         RETURN NEW;
5290     END;
5291 $ous_change_log$ LANGUAGE plpgsql;    
5292
5293 CREATE TRIGGER log_ous_change
5294     BEFORE INSERT OR UPDATE ON actor.org_unit_setting
5295     FOR EACH ROW EXECUTE PROCEDURE ous_change_log();
5296
5297 CREATE OR REPLACE FUNCTION ous_delete_log() RETURNS TRIGGER AS $ous_delete_log$
5298     DECLARE
5299     original TEXT;
5300     BEGIN
5301         -- Check for which setting is being updated, and log it.
5302         SELECT INTO original value FROM actor.org_unit_setting WHERE name = OLD.name AND org_unit = OLD.org_unit;
5303                 
5304         INSERT INTO config.org_unit_setting_type_log (org,original_value,new_value,field_name) VALUES (OLD.org_unit, original, 'null', OLD.name);
5305         
5306         RETURN OLD;
5307     END;
5308 $ous_delete_log$ LANGUAGE plpgsql;    
5309
5310 CREATE TRIGGER log_ous_del
5311     BEFORE DELETE ON actor.org_unit_setting
5312     FOR EACH ROW EXECUTE PROCEDURE ous_delete_log();
5313
5314 -- Evergreen DB patch 0625.data.opac_staff_saved_search_size.sql
5315
5316
5317 SELECT evergreen.upgrade_deps_block_check('0625', :eg_version);
5318
5319 INSERT into config.org_unit_setting_type (name, grp, label, description, datatype)
5320 VALUES (
5321     'opac.staff_saved_search.size', 'opac',
5322     oils_i18n_gettext('opac.staff_saved_search.size',
5323         'OPAC: Number of staff client saved searches to display on left side of results and record details pages', 'coust', 'label'),
5324     oils_i18n_gettext('opac.staff_saved_search.size',
5325         'If unset, the OPAC (only when wrapped in the staff client!) will default to showing you your ten most recent searches on the left side of the results and record details pages.  If you actually don''t want to see this feature at all, set this value to zero at the top of your organizational tree.', 'coust', 'description'),
5326     'integer'
5327 );
5328
5329 -- Evergreen DB patch 0626.schema.bookbag-goodies.sql
5330
5331
5332 SELECT evergreen.upgrade_deps_block_check('0626', :eg_version);
5333
5334 ALTER TABLE container.biblio_record_entry_bucket
5335     ADD COLUMN description TEXT;
5336
5337 ALTER TABLE container.call_number_bucket
5338     ADD COLUMN description TEXT;
5339
5340 ALTER TABLE container.copy_bucket
5341     ADD COLUMN description TEXT;
5342
5343 ALTER TABLE container.user_bucket
5344     ADD COLUMN description TEXT;
5345
5346 INSERT INTO action_trigger.hook (key, core_type, description, passive)
5347 VALUES (
5348     'container.biblio_record_entry_bucket.csv',
5349     'cbreb',
5350     oils_i18n_gettext(
5351         'container.biblio_record_entry_bucket.csv',
5352         'Produce a CSV file representing a bookbag',
5353         'ath',
5354         'description'
5355     ),
5356     FALSE
5357 );
5358
5359 INSERT INTO action_trigger.reactor (module, description)
5360 VALUES (
5361     'ContainerCSV',
5362     oils_i18n_gettext(
5363         'ContainerCSV',
5364         'Facilitates produce a CSV file representing a bookbag by introducing an "items" variable into the TT environment, sorted as dictated according to user params',
5365         'atr',
5366         'description'
5367     )
5368 );
5369
5370 INSERT INTO action_trigger.event_definition (
5371     id, active, owner,
5372     name, hook, reactor,
5373     validator, template
5374 ) VALUES (
5375     48, TRUE, 1,
5376     'Bookbag CSV', 'container.biblio_record_entry_bucket.csv', 'ContainerCSV',
5377     'NOOP_True',
5378 $$
5379 [%-
5380 # target is the bookbag itself. The 'items' variable does not need to be in
5381 # the environment because a special reactor will take care of filling it in.
5382
5383 FOR item IN items;
5384     bibxml = helpers.xml_doc(item.target_biblio_record_entry.marc);
5385     title = "";
5386     FOR part IN bibxml.findnodes('//*[@tag="245"]/*[@code="a" or @code="b"]');
5387         title = title _ part.textContent;
5388     END;
5389     author = bibxml.findnodes('//*[@tag="100"]/*[@code="a"]').textContent;
5390
5391     helpers.csv_datum(title) %],[% helpers.csv_datum(author) %],[% FOR note IN item.notes; helpers.csv_datum(note.note); ","; END; "\n";
5392 END -%]
5393 $$
5394 );
5395
5396 -- Evergreen DB patch 0627.data.patron-password-reset-msg.sql
5397 --
5398 -- Updates password reset template to match TPAC reset form
5399 --
5400
5401 -- check whether patch can be applied
5402 SELECT evergreen.upgrade_deps_block_check('0627', :eg_version);
5403
5404 UPDATE action_trigger.event_definition SET template = 
5405 $$
5406 [%- USE date -%]
5407 [%- user = target.usr -%]
5408 To: [%- params.recipient_email || user.email %]
5409 From: [%- params.sender_email || user.home_ou.email || default_sender %]
5410 Subject: [% user.home_ou.name %]: library account password reset request
5411
5412 You have received this message because you, or somebody else, requested a reset
5413 of your library system password. If you did not request a reset of your library
5414 system password, just ignore this message and your current password will
5415 continue to work.
5416
5417 If you did request a reset of your library system password, please perform
5418 the following steps to continue the process of resetting your password:
5419
5420 1. Open the following link in a web browser: https://[% params.hostname %]/eg/opac/password_reset/[% target.uuid %]
5421 The browser displays a password reset form.
5422
5423 2. Enter your new password in the password reset form in the browser. You must
5424 enter the password twice to ensure that you do not make a mistake. If the
5425 passwords match, you will then be able to log in to your library system account
5426 with the new password.
5427
5428 $$
5429 WHERE id = 20; -- Password reset request notification
5430
5431
5432 SELECT evergreen.upgrade_deps_block_check('0630', :eg_version);
5433
5434 INSERT into config.org_unit_setting_type (name, grp, label, description, datatype) VALUES
5435 ( 'circ.transit.suppress_hold', 'circ',
5436     oils_i18n_gettext('circ.transit.suppress_hold',
5437         'Suppress Hold Transits Group',
5438         'coust', 'label'),
5439     oils_i18n_gettext('circ.transit.suppress_hold',
5440         'If set to a non-empty value, Hold Transits will be suppressed between this OU and others with the same value. If set to an empty value, transits will not be suppressed.',
5441         'coust', 'description'),
5442     'string')
5443 ,( 'circ.transit.suppress_non_hold', 'circ',
5444     oils_i18n_gettext('circ.transit.suppress_non_hold',
5445         'Suppress Non-Hold Transits Group',
5446         'coust', 'label'),
5447     oils_i18n_gettext('circ.transit.suppress_non_hold',
5448         'If set to a non-empty value, Non-Hold Transits will be suppressed between this OU and others with the same value. If set to an empty value, transits will not be suppressed.',
5449         'coust', 'description'),
5450     'string');
5451
5452
5453 -- check whether patch can be applied
5454 SELECT evergreen.upgrade_deps_block_check('0632', :eg_version);
5455
5456 INSERT INTO config.org_unit_setting_type (name, grp, label, description, datatype) VALUES
5457 ( 'opac.username_regex', 'glob',
5458     oils_i18n_gettext('opac.username_regex',
5459         'Patron username format',
5460         'coust', 'label'),
5461     oils_i18n_gettext('opac.username_regex',
5462         'Regular expression defining the patron username format, used for patron registration and self-service username changing only',
5463         'coust', 'description'),
5464     'string')
5465 ,( 'opac.lock_usernames', 'glob',
5466     oils_i18n_gettext('opac.lock_usernames',
5467         'Lock Usernames',
5468         'coust', 'label'),
5469     oils_i18n_gettext('opac.lock_usernames',
5470         'If enabled username changing via the OPAC will be disabled',
5471         'coust', 'description'),
5472     'bool')
5473 ,( 'opac.unlimit_usernames', 'glob',
5474     oils_i18n_gettext('opac.unlimit_usernames',
5475         'Allow multiple username changes',
5476         'coust', 'label'),
5477     oils_i18n_gettext('opac.unlimit_usernames',
5478         'If enabled (and Lock Usernames is not set) patrons will be allowed to change their username when it does not look like a barcode. Otherwise username changing in the OPAC will only be allowed when the patron''s username looks like a barcode.',
5479         'coust', 'description'),
5480     'bool')
5481 ;
5482
5483 -- Evergreen DB patch 0635.data.opac.jump-to-details-setting.sql
5484 --
5485
5486
5487 -- check whether patch can be applied
5488 SELECT evergreen.upgrade_deps_block_check('0635', :eg_version);
5489
5490 INSERT INTO config.org_unit_setting_type ( name, grp, label, description, datatype )
5491     VALUES (
5492         'opac.staff.jump_to_details_on_single_hit', 
5493         'opac',
5494         oils_i18n_gettext(
5495             'opac.staff.jump_to_details_on_single_hit',
5496             'Jump to details on 1 hit (staff client)',
5497             'coust', 
5498             'label'
5499         ),
5500         oils_i18n_gettext(
5501             'opac.staff.jump_to_details_on_single_hit',
5502             'When a search yields only 1 result, jump directly to the record details page.  This setting only affects the OPAC within the staff client',
5503             'coust', 
5504             'description'
5505         ),
5506         'bool'
5507     ), (
5508         'opac.patron.jump_to_details_on_single_hit', 
5509         'opac',
5510         oils_i18n_gettext(
5511             'opac.patron.jump_to_details_on_single_hit',
5512             'Jump to details on 1 hit (public)',
5513             'coust', 
5514             'label'
5515         ),
5516         oils_i18n_gettext(
5517             'opac.patron.jump_to_details_on_single_hit',
5518             'When a search yields only 1 result, jump directly to the record details page.  This setting only affects the public OPAC',
5519             'coust', 
5520             'description'
5521         ),
5522         'bool'
5523     );
5524
5525 -- Evergreen DB patch 0636.data.grace_period_extend.sql
5526 --
5527 -- OU setting turns on grace period auto extension. By default they only do so
5528 -- when the grace period ends on a closed date, but there are two modifiers to
5529 -- change that.
5530 -- 
5531 -- The first modifier causes grace periods to extend for all closed dates that
5532 -- they intersect. This is "grace periods are only consumed by open days."
5533 -- 
5534 -- The second modifier causes a grace period that ends just before a closed
5535 -- day, with or without extension having happened, to include the closed day
5536 -- (and any following it) as well. This is mainly so that a backdate into the
5537 -- closed period following the grace period will assume the "best case" of the
5538 -- item having been returned after hours on the last day of the closed date.
5539 --
5540
5541
5542 -- check whether patch can be applied
5543 SELECT evergreen.upgrade_deps_block_check('0636', :eg_version);
5544
5545 INSERT INTO config.org_unit_setting_type(name, grp, label, description, datatype) VALUES
5546
5547 ( 'circ.grace.extend', 'circ',
5548     oils_i18n_gettext('circ.grace.extend',
5549         'Auto-Extend Grace Periods',
5550         'coust', 'label'),
5551     oils_i18n_gettext('circ.grace.extend',
5552         'When enabled grace periods will auto-extend. By default this will be only when they are a full day or more and end on a closed date, though other options can alter this.',
5553         'coust', 'description'),
5554     'bool')
5555
5556 ,( 'circ.grace.extend.all', 'circ',
5557     oils_i18n_gettext('circ.grace.extend.all',
5558         'Auto-Extending Grace Periods extend for all closed dates',
5559         'coust', 'label'),
5560     oils_i18n_gettext('circ.grace.extend.all',
5561         'If enabled and Grace Periods auto-extending is turned on grace periods will extend past all closed dates they intersect, within hard-coded limits. This basically becomes "grace periods can only be consumed by closed dates".',
5562         'coust', 'description'),
5563     'bool')
5564
5565 ,( 'circ.grace.extend.into_closed', 'circ',
5566     oils_i18n_gettext('circ.grace.extend.into_closed',
5567         'Auto-Extending Grace Periods include trailing closed dates',
5568         'coust', 'label'),
5569     oils_i18n_gettext('circ.grace.extend.into_closed',
5570          'If enabled and Grace Periods auto-extending is turned on grace periods will include closed dates that directly follow the last day of the grace period, to allow a backdate into the closed dates to assume "returned after hours on the last day of the grace period, and thus still within it" automatically.',
5571         'coust', 'description'),
5572     'bool');
5573
5574
5575 -- XXXX.schema-acs-nfi.sql
5576
5577 SELECT evergreen.upgrade_deps_block_check('0640', :eg_version);
5578
5579 -- AFTER UPDATE OR INSERT trigger for authority.record_entry
5580 CREATE OR REPLACE FUNCTION authority.indexing_ingest_or_delete () RETURNS TRIGGER AS $func$
5581 BEGIN
5582
5583     IF NEW.deleted IS TRUE THEN -- If this authority is deleted
5584         DELETE FROM authority.bib_linking WHERE authority = NEW.id; -- Avoid updating fields in bibs that are no longer visible
5585         DELETE FROM authority.full_rec WHERE record = NEW.id; -- Avoid validating fields against deleted authority records
5586         DELETE FROM authority.simple_heading WHERE record = NEW.id;
5587           -- Should remove matching $0 from controlled fields at the same time?
5588         RETURN NEW; -- and we're done
5589     END IF;
5590
5591     IF TG_OP = 'UPDATE' THEN -- re-ingest?
5592         PERFORM * FROM config.internal_flag WHERE name = 'ingest.reingest.force_on_same_marc' AND enabled;
5593
5594         IF NOT FOUND AND OLD.marc = NEW.marc THEN -- don't do anything if the MARC didn't change
5595             RETURN NEW;
5596         END IF;
5597
5598         -- Propagate these updates to any linked bib records
5599         PERFORM authority.propagate_changes(NEW.id) FROM authority.record_entry WHERE id = NEW.id;
5600
5601         DELETE FROM authority.simple_heading WHERE record = NEW.id;
5602     END IF;
5603
5604     INSERT INTO authority.simple_heading (record,atag,value,sort_value)
5605         SELECT record, atag, value, sort_value FROM authority.simple_heading_set(NEW.marc);
5606
5607     -- Flatten and insert the afr data
5608     PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_authority_full_rec' AND enabled;
5609     IF NOT FOUND THEN
5610         PERFORM authority.reingest_authority_full_rec(NEW.id);
5611         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_authority_rec_descriptor' AND enabled;
5612         IF NOT FOUND THEN
5613             PERFORM authority.reingest_authority_rec_descriptor(NEW.id);
5614         END IF;
5615     END IF;
5616
5617     RETURN NEW;
5618 END;
5619 $func$ LANGUAGE PLPGSQL;
5620
5621 ALTER TABLE authority.control_set_authority_field ADD COLUMN nfi CHAR(1);
5622
5623 -- Entries that need to respect an NFI
5624 UPDATE authority.control_set_authority_field SET nfi = '2'
5625     WHERE id IN (4,24,44,64);
5626
5627 DROP TRIGGER authority_full_rec_fti_trigger ON authority.full_rec;
5628 CREATE TRIGGER authority_full_rec_fti_trigger
5629     BEFORE UPDATE OR INSERT ON authority.full_rec
5630     FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('keyword');
5631
5632 CREATE OR REPLACE FUNCTION authority.normalize_heading( marcxml TEXT, no_thesaurus BOOL ) RETURNS TEXT AS $func$
5633 DECLARE
5634     acsaf           authority.control_set_authority_field%ROWTYPE;
5635     tag_used        TEXT;
5636     nfi_used        TEXT;
5637     sf              TEXT;
5638     thes_code       TEXT;
5639     cset            INT;
5640     heading_text    TEXT;
5641     tmp_text        TEXT;
5642     first_sf        BOOL;
5643     auth_id         INT DEFAULT oils_xpath_string('//*[@tag="901"]/*[local-name()="subfield" and @code="c"]', marcxml)::INT;
5644 BEGIN
5645     SELECT control_set INTO cset FROM authority.record_entry WHERE id = auth_id;
5646
5647     IF cset IS NULL THEN
5648         SELECT  control_set INTO cset
5649           FROM  authority.control_set_authority_field
5650           WHERE tag IN ( SELECT  UNNEST(XPATH('//*[starts-with(@tag,"1")]/@tag',marcxml::XML)::TEXT[]))
5651           LIMIT 1;
5652     END IF;
5653
5654     thes_code := vandelay.marc21_extract_fixed_field(marcxml,'Subj');
5655     IF thes_code IS NULL THEN
5656         thes_code := '|';
5657     ELSIF thes_code = 'z' THEN
5658         thes_code := COALESCE( oils_xpath_string('//*[@tag="040"]/*[@code="f"][1]', marcxml), '' );
5659     END IF;
5660
5661     heading_text := '';
5662     FOR acsaf IN SELECT * FROM authority.control_set_authority_field WHERE control_set = cset AND main_entry IS NULL LOOP
5663         tag_used := acsaf.tag;
5664         nfi_used := acsaf.nfi;
5665         first_sf := TRUE;
5666         FOR sf IN SELECT * FROM regexp_split_to_table(acsaf.sf_list,'') LOOP
5667             tmp_text := oils_xpath_string('//*[@tag="'||tag_used||'"]/*[@code="'||sf||'"]', marcxml);
5668
5669             IF first_sf AND tmp_text IS NOT NULL AND nfi_used IS NOT NULL THEN
5670
5671                 tmp_text := SUBSTRING(
5672                     tmp_text FROM
5673                     COALESCE(
5674                         NULLIF(
5675                             REGEXP_REPLACE(
5676                                 oils_xpath_string('//*[@tag="'||tag_used||'"]/@ind'||nfi_used, marcxml),
5677                                 $$\D+$$,
5678                                 '',
5679                                 'g'
5680                             ),
5681                             ''
5682                         )::INT,
5683                         0
5684                     ) + 1
5685                 );
5686
5687             END IF;
5688
5689             first_sf := FALSE;
5690
5691             IF tmp_text IS NOT NULL AND tmp_text <> '' THEN
5692                 heading_text := heading_text || E'\u2021' || sf || ' ' || tmp_text;
5693             END IF;
5694         END LOOP;
5695         EXIT WHEN heading_text <> '';
5696     END LOOP;
5697
5698     IF heading_text <> '' THEN
5699         IF no_thesaurus IS TRUE THEN
5700             heading_text := tag_used || ' ' || public.naco_normalize(heading_text);
5701         ELSE
5702             heading_text := tag_used || '_' || COALESCE(nfi_used,'-') || '_' || thes_code || ' ' || public.naco_normalize(heading_text);
5703         END IF;
5704     ELSE
5705         heading_text := 'NOHEADING_' || thes_code || ' ' || MD5(marcxml);
5706     END IF;
5707
5708     RETURN heading_text;
5709 END;
5710 $func$ LANGUAGE PLPGSQL IMMUTABLE;
5711
5712 CREATE OR REPLACE FUNCTION authority.simple_normalize_heading( marcxml TEXT ) RETURNS TEXT AS $func$
5713     SELECT authority.normalize_heading($1, TRUE);
5714 $func$ LANGUAGE SQL IMMUTABLE;
5715
5716 CREATE OR REPLACE FUNCTION authority.normalize_heading( marcxml TEXT ) RETURNS TEXT AS $func$
5717     SELECT authority.normalize_heading($1, FALSE);
5718 $func$ LANGUAGE SQL IMMUTABLE;
5719
5720
5721 CREATE TABLE authority.simple_heading (
5722     id              BIGSERIAL   PRIMARY KEY,
5723     record          BIGINT      NOT NULL REFERENCES authority.record_entry (id),
5724     atag            INT         NOT NULL REFERENCES authority.control_set_authority_field (id),
5725     value           TEXT        NOT NULL,
5726     sort_value      TEXT        NOT NULL,
5727     index_vector    tsvector    NOT NULL
5728 );
5729 CREATE TRIGGER authority_simple_heading_fti_trigger
5730     BEFORE UPDATE OR INSERT ON authority.simple_heading
5731     FOR EACH ROW EXECUTE PROCEDURE oils_tsearch2('keyword');
5732
5733 CREATE INDEX authority_simple_heading_index_vector_idx ON authority.simple_heading USING GIST (index_vector);
5734 CREATE INDEX authority_simple_heading_value_idx ON authority.simple_heading (value);
5735 CREATE INDEX authority_simple_heading_sort_value_idx ON authority.simple_heading (sort_value);
5736
5737 CREATE OR REPLACE FUNCTION authority.simple_heading_set( marcxml TEXT ) RETURNS SETOF authority.simple_heading AS $func$
5738 DECLARE
5739     res             authority.simple_heading%ROWTYPE;
5740     acsaf           authority.control_set_authority_field%ROWTYPE;
5741     tag_used        TEXT;
5742     nfi_used        TEXT;
5743     sf              TEXT;
5744     cset            INT;
5745     heading_text    TEXT;
5746     sort_text       TEXT;
5747     tmp_text        TEXT;
5748     tmp_xml         TEXT;
5749     first_sf        BOOL;
5750     auth_id         INT DEFAULT oils_xpath_string('//*[@tag="901"]/*[local-name()="subfield" and @code="c"]', marcxml)::INT;
5751 BEGIN
5752
5753     res.record := auth_id;
5754
5755     SELECT  control_set INTO cset
5756       FROM  authority.control_set_authority_field
5757       WHERE tag IN ( SELECT UNNEST(XPATH('//*[starts-with(@tag,"1")]/@tag',marcxml::XML)::TEXT[]) )
5758       LIMIT 1;
5759
5760     FOR acsaf IN SELECT * FROM authority.control_set_authority_field WHERE control_set = cset LOOP
5761
5762         res.atag := acsaf.id;
5763         tag_used := acsaf.tag;
5764         nfi_used := acsaf.nfi;
5765
5766         FOR tmp_xml IN SELECT UNNEST(XPATH('//*[@tag="'||tag_used||'"]', marcxml::XML)) LOOP
5767             heading_text := '';
5768
5769             FOR sf IN SELECT * FROM regexp_split_to_table(acsaf.sf_list,'') LOOP
5770                 heading_text := heading_text || COALESCE( ' ' || oils_xpath_string('//*[@code="'||sf||'"]',tmp_xml::TEXT), '');
5771             END LOOP;
5772
5773             heading_text := public.naco_normalize(heading_text);
5774             
5775             IF nfi_used IS NOT NULL THEN
5776
5777                 sort_text := SUBSTRING(
5778                     heading_text FROM
5779                     COALESCE(
5780                         NULLIF(
5781                             REGEXP_REPLACE(
5782                                 oils_xpath_string('//*[@tag="'||tag_used||'"]/@ind'||nfi_used, marcxml),
5783                                 $$\D+$$,
5784                                 '',
5785                                 'g'
5786                             ),
5787                             ''
5788                         )::INT,
5789                         0
5790                     ) + 1
5791                 );
5792
5793             ELSE
5794                 sort_text := heading_text;
5795             END IF;
5796
5797             IF heading_text IS NOT NULL AND heading_text <> '' THEN
5798                 res.value := heading_text;
5799                 res.sort_value := sort_text;
5800                 RETURN NEXT res;
5801             END IF;
5802
5803         END LOOP;
5804
5805     END LOOP;
5806
5807     RETURN;
5808 END;
5809 $func$ LANGUAGE PLPGSQL IMMUTABLE;
5810
5811 -- Support function used to find the pivot for alpha-heading-browse style searching
5812 CREATE OR REPLACE FUNCTION authority.simple_heading_find_pivot( a INT[], q TEXT ) RETURNS TEXT AS $$
5813 DECLARE
5814     sort_value_row  RECORD;
5815     value_row       RECORD;
5816     t_term          TEXT;
5817 BEGIN
5818
5819     t_term := public.naco_normalize(q);
5820
5821     SELECT  CASE WHEN ash.sort_value LIKE t_term || '%' THEN 1 ELSE 0 END
5822                 + CASE WHEN ash.value LIKE t_term || '%' THEN 1 ELSE 0 END AS rank,
5823             ash.sort_value
5824       INTO  sort_value_row
5825       FROM  authority.simple_heading ash
5826       WHERE ash.atag = ANY (a)
5827             AND ash.sort_value >= t_term
5828       ORDER BY rank DESC, ash.sort_value
5829       LIMIT 1;
5830
5831     SELECT  CASE WHEN ash.sort_value LIKE t_term || '%' THEN 1 ELSE 0 END
5832                 + CASE WHEN ash.value LIKE t_term || '%' THEN 1 ELSE 0 END AS rank,
5833             ash.sort_value
5834       INTO  value_row
5835       FROM  authority.simple_heading ash
5836       WHERE ash.atag = ANY (a)
5837             AND ash.value >= t_term
5838       ORDER BY rank DESC, ash.sort_value
5839       LIMIT 1;
5840
5841     IF value_row.rank > sort_value_row.rank THEN
5842         RETURN value_row.sort_value;
5843     ELSE
5844         RETURN sort_value_row.sort_value;
5845     END IF;
5846 END;
5847 $$ LANGUAGE PLPGSQL;
5848
5849
5850 CREATE OR REPLACE FUNCTION authority.simple_heading_browse_center( atag_list INT[], q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9 ) RETURNS SETOF BIGINT AS $$
5851 DECLARE
5852     pivot_sort_value    TEXT;
5853     boffset             INT DEFAULT 0;
5854     aoffset             INT DEFAULT 0;
5855     blimit              INT DEFAULT 0;
5856     alimit              INT DEFAULT 0;
5857 BEGIN
5858
5859     pivot_sort_value := authority.simple_heading_find_pivot(atag_list,q);
5860
5861     IF page = 0 THEN
5862         blimit := pagesize / 2;
5863         alimit := blimit;
5864
5865         IF pagesize % 2 <> 0 THEN
5866             alimit := alimit + 1;
5867         END IF;
5868     ELSE
5869         blimit := pagesize;
5870         alimit := blimit;
5871
5872         boffset := pagesize / 2;
5873         aoffset := boffset;
5874
5875         IF pagesize % 2 <> 0 THEN
5876             boffset := boffset + 1;
5877         END IF;
5878     END IF;
5879
5880     IF page <= 0 THEN
5881         RETURN QUERY
5882             -- "bottom" half of the browse results
5883             SELECT id FROM (
5884                 SELECT  ash.id,
5885                         row_number() over ()
5886                   FROM  authority.simple_heading ash
5887                   WHERE ash.atag = ANY (atag_list)
5888                         AND ash.sort_value < pivot_sort_value
5889                   ORDER BY ash.sort_value DESC
5890                   LIMIT blimit
5891                   OFFSET ABS(page) * pagesize - boffset
5892             ) x ORDER BY row_number DESC;
5893     END IF;
5894
5895     IF page >= 0 THEN
5896         RETURN QUERY
5897             -- "bottom" half of the browse results
5898             SELECT  ash.id
5899               FROM  authority.simple_heading ash
5900               WHERE ash.atag = ANY (atag_list)
5901                     AND ash.sort_value >= pivot_sort_value
5902               ORDER BY ash.sort_value
5903               LIMIT alimit
5904               OFFSET ABS(page) * pagesize - aoffset;
5905     END IF;
5906 END;
5907 $$ LANGUAGE PLPGSQL ROWS 10;
5908
5909 CREATE OR REPLACE FUNCTION authority.simple_heading_browse_top( atag_list INT[], q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
5910 DECLARE
5911     pivot_sort_value    TEXT;
5912 BEGIN
5913
5914     pivot_sort_value := authority.simple_heading_find_pivot(atag_list,q);
5915
5916     IF page < 0 THEN
5917         RETURN QUERY
5918             -- "bottom" half of the browse results
5919             SELECT id FROM (
5920                 SELECT  ash.id,
5921                         row_number() over ()
5922                   FROM  authority.simple_heading ash
5923                   WHERE ash.atag = ANY (atag_list)
5924                         AND ash.sort_value < pivot_sort_value
5925                   ORDER BY ash.sort_value DESC
5926                   LIMIT pagesize
5927                   OFFSET (ABS(page) - 1) * pagesize
5928             ) x ORDER BY row_number DESC;
5929     END IF;
5930
5931     IF page >= 0 THEN
5932         RETURN QUERY
5933             -- "bottom" half of the browse results
5934             SELECT  ash.id
5935               FROM  authority.simple_heading ash
5936               WHERE ash.atag = ANY (atag_list)
5937                     AND ash.sort_value >= pivot_sort_value
5938               ORDER BY ash.sort_value
5939               LIMIT pagesize
5940               OFFSET ABS(page) * pagesize ;
5941     END IF;
5942 END;
5943 $$ LANGUAGE PLPGSQL ROWS 10;
5944
5945 CREATE OR REPLACE FUNCTION authority.simple_heading_search_rank( atag_list INT[], q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
5946     SELECT  ash.id
5947       FROM  authority.simple_heading ash,
5948             public.naco_normalize($2) t(term),
5949             plainto_tsquery('keyword'::regconfig,$2) ptsq(term)
5950       WHERE ash.atag = ANY ($1)
5951             AND ash.index_vector @@ ptsq.term
5952       ORDER BY ts_rank_cd(ash.index_vector,ptsq.term,14)::numeric
5953                     + CASE WHEN ash.sort_value LIKE t.term || '%' THEN 2 ELSE 0 END
5954                     + CASE WHEN ash.value LIKE t.term || '%' THEN 1 ELSE 0 END DESC
5955       LIMIT $4
5956       OFFSET $4 * $3;
5957 $$ LANGUAGE SQL ROWS 10;
5958
5959 CREATE OR REPLACE FUNCTION authority.simple_heading_search_heading( atag_list INT[], q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
5960     SELECT  ash.id
5961       FROM  authority.simple_heading ash,
5962             public.naco_normalize($2) t(term),
5963             plainto_tsquery('keyword'::regconfig,$2) ptsq(term)
5964       WHERE ash.atag = ANY ($1)
5965             AND ash.index_vector @@ ptsq.term
5966       ORDER BY ash.sort_value
5967       LIMIT $4
5968       OFFSET $4 * $3;
5969 $$ LANGUAGE SQL ROWS 10;
5970
5971
5972 CREATE OR REPLACE FUNCTION authority.axis_authority_tags(a TEXT) RETURNS INT[] AS $$
5973     SELECT ARRAY_ACCUM(field) FROM authority.browse_axis_authority_field_map WHERE axis = $1;
5974 $$ LANGUAGE SQL;
5975
5976 CREATE OR REPLACE FUNCTION authority.axis_authority_tags_refs(a TEXT) RETURNS INT[] AS $$
5977     SELECT  ARRAY_CAT(
5978                 ARRAY[a.field],
5979                 (SELECT ARRAY_ACCUM(x.id) FROM authority.control_set_authority_field x WHERE x.main_entry = a.field)
5980             )
5981       FROM  authority.browse_axis_authority_field_map a
5982       WHERE axis = $1
5983 $$ LANGUAGE SQL;
5984
5985
5986
5987 CREATE OR REPLACE FUNCTION authority.btag_authority_tags(btag TEXT) RETURNS INT[] AS $$
5988     SELECT ARRAY_ACCUM(authority_field) FROM authority.control_set_bib_field WHERE tag = $1
5989 $$ LANGUAGE SQL;
5990
5991 CREATE OR REPLACE FUNCTION authority.btag_authority_tags_refs(btag TEXT) RETURNS INT[] AS $$
5992     SELECT  ARRAY_CAT(
5993                 ARRAY[a.authority_field],
5994                 (SELECT ARRAY_ACCUM(x.id) FROM authority.control_set_authority_field x WHERE x.main_entry = a.authority_field)
5995             )
5996       FROM  authority.control_set_bib_field a
5997       WHERE a.tag = $1
5998 $$ LANGUAGE SQL;
5999
6000
6001
6002 CREATE OR REPLACE FUNCTION authority.atag_authority_tags(atag TEXT) RETURNS INT[] AS $$
6003     SELECT ARRAY_ACCUM(id) FROM authority.control_set_authority_field WHERE tag = $1
6004 $$ LANGUAGE SQL;
6005
6006 CREATE OR REPLACE FUNCTION authority.atag_authority_tags_refs(atag TEXT) RETURNS INT[] AS $$
6007     SELECT  ARRAY_CAT(
6008                 ARRAY[a.id],
6009                 (SELECT ARRAY_ACCUM(x.id) FROM authority.control_set_authority_field x WHERE x.main_entry = a.id)
6010             )
6011       FROM  authority.control_set_authority_field a
6012       WHERE a.tag = $1
6013 $$ LANGUAGE SQL;
6014
6015
6016 CREATE OR REPLACE FUNCTION authority.axis_browse_center( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9 ) RETURNS SETOF BIGINT AS $$
6017     SELECT * FROM authority.simple_heading_browse_center(authority.axis_authority_tags($1), $2, $3, $4)
6018 $$ LANGUAGE SQL ROWS 10;
6019
6020 CREATE OR REPLACE FUNCTION authority.btag_browse_center( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9 ) RETURNS SETOF BIGINT AS $$
6021     SELECT * FROM authority.simple_heading_browse_center(authority.btag_authority_tags($1), $2, $3, $4)
6022 $$ LANGUAGE SQL ROWS 10;
6023
6024 CREATE OR REPLACE FUNCTION authority.atag_browse_center( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9 ) RETURNS SETOF BIGINT AS $$
6025     SELECT * FROM authority.simple_heading_browse_center(authority.atag_authority_tags($1), $2, $3, $4)
6026 $$ LANGUAGE SQL ROWS 10;
6027
6028 CREATE OR REPLACE FUNCTION authority.axis_browse_center_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9 ) RETURNS SETOF BIGINT AS $$
6029     SELECT * FROM authority.simple_heading_browse_center(authority.axis_authority_tags_refs($1), $2, $3, $4)
6030 $$ LANGUAGE SQL ROWS 10;
6031
6032 CREATE OR REPLACE FUNCTION authority.btag_browse_center_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9 ) RETURNS SETOF BIGINT AS $$
6033     SELECT * FROM authority.simple_heading_browse_center(authority.btag_authority_tags_refs($1), $2, $3, $4)
6034 $$ LANGUAGE SQL ROWS 10;
6035
6036 CREATE OR REPLACE FUNCTION authority.atag_browse_center_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9 ) RETURNS SETOF BIGINT AS $$
6037     SELECT * FROM authority.simple_heading_browse_center(authority.atag_authority_tags_refs($1), $2, $3, $4)
6038 $$ LANGUAGE SQL ROWS 10;
6039
6040
6041 CREATE OR REPLACE FUNCTION authority.axis_browse_top( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
6042     SELECT * FROM authority.simple_heading_browse_top(authority.axis_authority_tags($1), $2, $3, $4)
6043 $$ LANGUAGE SQL ROWS 10;
6044
6045 CREATE OR REPLACE FUNCTION authority.btag_browse_top( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
6046     SELECT * FROM authority.simple_heading_browse_top(authority.btag_authority_tags($1), $2, $3, $4)
6047 $$ LANGUAGE SQL ROWS 10;
6048
6049 CREATE OR REPLACE FUNCTION authority.atag_browse_top( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
6050     SELECT * FROM authority.simple_heading_browse_top(authority.atag_authority_tags($1), $2, $3, $4)
6051 $$ LANGUAGE SQL ROWS 10;
6052
6053 CREATE OR REPLACE FUNCTION authority.axis_browse_top_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
6054     SELECT * FROM authority.simple_heading_browse_top(authority.axis_authority_tags_refs($1), $2, $3, $4)
6055 $$ LANGUAGE SQL ROWS 10;
6056
6057 CREATE OR REPLACE FUNCTION authority.btag_browse_top_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
6058     SELECT * FROM authority.simple_heading_browse_top(authority.btag_authority_tags_refs($1), $2, $3, $4)
6059 $$ LANGUAGE SQL ROWS 10;
6060
6061 CREATE OR REPLACE FUNCTION authority.atag_browse_top_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
6062     SELECT * FROM authority.simple_heading_browse_top(authority.atag_authority_tags_refs($1), $2, $3, $4)
6063 $$ LANGUAGE SQL ROWS 10;
6064
6065
6066 CREATE OR REPLACE FUNCTION authority.axis_search_rank( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
6067     SELECT * FROM authority.simple_heading_search_rank(authority.axis_authority_tags($1), $2, $3, $4)
6068 $$ LANGUAGE SQL ROWS 10;
6069
6070 CREATE OR REPLACE FUNCTION authority.btag_search_rank( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
6071     SELECT * FROM authority.simple_heading_search_rank(authority.btag_authority_tags($1), $2, $3, $4)
6072 $$ LANGUAGE SQL ROWS 10;
6073
6074 CREATE OR REPLACE FUNCTION authority.atag_search_rank( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
6075     SELECT * FROM authority.simple_heading_search_rank(authority.atag_authority_tags($1), $2, $3, $4)
6076 $$ LANGUAGE SQL ROWS 10;
6077
6078 CREATE OR REPLACE FUNCTION authority.axis_search_rank_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
6079     SELECT * FROM authority.simple_heading_search_rank(authority.axis_authority_tags_refs($1), $2, $3, $4)
6080 $$ LANGUAGE SQL ROWS 10;
6081
6082 CREATE OR REPLACE FUNCTION authority.btag_search_rank_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
6083     SELECT * FROM authority.simple_heading_search_rank(authority.btag_authority_tags_refs($1), $2, $3, $4)
6084 $$ LANGUAGE SQL ROWS 10;
6085
6086 CREATE OR REPLACE FUNCTION authority.atag_search_rank_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
6087     SELECT * FROM authority.simple_heading_search_rank(authority.atag_authority_tags_refs($1), $2, $3, $4)
6088 $$ LANGUAGE SQL ROWS 10;
6089
6090
6091 CREATE OR REPLACE FUNCTION authority.axis_search_heading( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
6092     SELECT * FROM authority.simple_heading_search_heading(authority.axis_authority_tags($1), $2, $3, $4)
6093 $$ LANGUAGE SQL ROWS 10;
6094
6095 CREATE OR REPLACE FUNCTION authority.btag_search_heading( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
6096     SELECT * FROM authority.simple_heading_search_heading(authority.btag_authority_tags($1), $2, $3, $4)
6097 $$ LANGUAGE SQL ROWS 10;
6098
6099 CREATE OR REPLACE FUNCTION authority.atag_search_heading( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
6100     SELECT * FROM authority.simple_heading_search_heading(authority.atag_authority_tags($1), $2, $3, $4)
6101 $$ LANGUAGE SQL ROWS 10;
6102
6103 CREATE OR REPLACE FUNCTION authority.axis_search_heading_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
6104     SELECT * FROM authority.simple_heading_search_heading(authority.axis_authority_tags_refs($1), $2, $3, $4)
6105 $$ LANGUAGE SQL ROWS 10;
6106
6107 CREATE OR REPLACE FUNCTION authority.btag_search_heading_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
6108     SELECT * FROM authority.simple_heading_search_heading(authority.btag_authority_tags_refs($1), $2, $3, $4)
6109 $$ LANGUAGE SQL ROWS 10;
6110
6111 CREATE OR REPLACE FUNCTION authority.atag_search_heading_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
6112     SELECT * FROM authority.simple_heading_search_heading(authority.atag_authority_tags_refs($1), $2, $3, $4)
6113 $$ LANGUAGE SQL ROWS 10;
6114
6115
6116
6117 -- Evergreen DB patch 0641.schema.org_unit_setting_json_check.sql
6118 --
6119 --
6120
6121 -- check whether patch can be applied
6122 SELECT evergreen.upgrade_deps_block_check('0641', :eg_version);
6123
6124 ALTER TABLE actor.org_unit_setting ADD CONSTRAINT aous_must_be_json CHECK ( evergreen.is_json(value) );
6125
6126 -- Evergreen DB patch 0642.data.acq-worksheet-hold-count.sql
6127
6128 -- check whether patch can be applied
6129 SELECT evergreen.upgrade_deps_block_check('0642', :eg_version);
6130
6131 UPDATE action_trigger.event_definition SET template = 
6132 $$
6133 [%- USE date -%]
6134 [%- SET li = target; -%]
6135 <div class="wrapper">
6136     <div class="summary" style='font-size:110%; font-weight:bold;'>
6137
6138         <div>Title: [% helpers.get_li_attr("title", "", li.attributes) %]</div>
6139         <div>Author: [% helpers.get_li_attr("author", "", li.attributes) %]</div>
6140         <div class="count">Item Count: [% li.lineitem_details.size %]</div>
6141         <div class="lineid">Lineitem ID: [% li.id %]</div>
6142         <div>Open Holds: [% helpers.bre_open_hold_count(li.eg_bib_id) %]</div>
6143
6144         [% IF li.distribution_formulas.size > 0 %]
6145             [% SET forms = [] %]
6146             [% FOREACH form IN li.distribution_formulas; forms.push(form.formula.name); END %]
6147             <div>Distribution Formulas: [% forms.join(',') %]</div>
6148         [% END %]
6149
6150         [% IF li.lineitem_notes.size > 0 %]
6151             Lineitem Notes:
6152             <ul>
6153                 [%- FOR note IN li.lineitem_notes -%]
6154                     <li>
6155                     [% IF note.alert_text %]
6156                         [% note.alert_text.code -%] 
6157                         [% IF note.value -%]
6158                             : [% note.value %]
6159                         [% END %]
6160                     [% ELSE %]
6161                         [% note.value -%] 
6162                     [% END %]
6163                     </li>
6164                 [% END %]
6165             </ul>
6166         [% END %]
6167     </div>
6168     <br/>
6169     <table>
6170         <thead>
6171             <tr>
6172                 <th>Branch</th>
6173                 <th>Barcode</th>
6174                 <th>Call Number</th>
6175                 <th>Fund</th>
6176                 <th>Shelving Location</th>
6177                 <th>Recd.</th>
6178                 <th>Notes</th>
6179             </tr>
6180         </thead>
6181         <tbody>
6182         [% FOREACH detail IN li.lineitem_details.sort('owning_lib') %]
6183             [% 
6184                 IF detail.eg_copy_id;
6185                     SET copy = detail.eg_copy_id;
6186                     SET cn_label = copy.call_number.label;
6187                 ELSE; 
6188                     SET copy = detail; 
6189                     SET cn_label = detail.cn_label;
6190                 END 
6191             %]
6192             <tr>
6193                 <!-- acq.lineitem_detail.id = [%- detail.id -%] -->
6194                 <td style='padding:5px;'>[% detail.owning_lib.shortname %]</td>
6195                 <td style='padding:5px;'>[% IF copy.barcode   %]<span class="barcode"  >[% detail.barcode   %]</span>[% END %]</td>
6196                 <td style='padding:5px;'>[% IF cn_label %]<span class="cn_label" >[% cn_label  %]</span>[% END %]</td>
6197                 <td style='padding:5px;'>[% IF detail.fund %]<span class="fund">[% detail.fund.code %] ([% detail.fund.year %])</span>[% END %]</td>
6198                 <td style='padding:5px;'>[% copy.location.name %]</td>
6199                 <td style='padding:5px;'>[% IF detail.recv_time %]<span class="recv_time">[% detail.recv_time %]</span>[% END %]</td>
6200                 <td style='padding:5px;'>[% detail.note %]</td>
6201             </tr>
6202         [% END %]
6203         </tbody>
6204     </table>
6205 </div>
6206 $$
6207 WHERE id = 14;
6208
6209
6210 SELECT evergreen.upgrade_deps_block_check('0643', :eg_version);
6211
6212 DO $$
6213 DECLARE x TEXT;
6214 BEGIN
6215
6216     FOR x IN
6217         SELECT  marc
6218           FROM  authority.record_entry
6219           WHERE id > 0
6220                 AND NOT deleted
6221                 AND id NOT IN (SELECT DISTINCT record FROM authority.simple_heading)
6222     LOOP
6223         INSERT INTO authority.simple_heading (record,atag,value,sort_value)
6224             SELECT record, atag, value, sort_value FROM authority.simple_heading_set(x);
6225     END LOOP;
6226 END;
6227 $$;
6228
6229
6230
6231 SELECT evergreen.upgrade_deps_block_check('0644', :eg_version);
6232
6233 INSERT into config.org_unit_setting_type (name, grp, label, description, datatype) VALUES
6234 ( 'circ.holds.target_when_closed', 'circ',
6235     oils_i18n_gettext('circ.holds.target_when_closed',
6236         'Target copies for a hold even if copy''s circ lib is closed',
6237         'coust', 'label'),
6238     oils_i18n_gettext('circ.holds.target_when_closed',
6239         'If this setting is true at a given org unit or one of its ancestors, the hold targeter will target copies from this org unit even if the org unit is closed (according to the actor.org_unit.closed_date table).',
6240         'coust', 'description'),
6241     'bool'),
6242 ( 'circ.holds.target_when_closed_if_at_pickup_lib', 'circ',
6243     oils_i18n_gettext('circ.holds.target_when_closed_if_at_pickup_lib',
6244         'Target copies for a hold even if copy''s circ lib is closed IF the circ lib is the hold''s pickup lib',
6245         'coust', 'label'),
6246     oils_i18n_gettext('circ.holds.target_when_closed_if_at_pickup_lib',
6247         'If this setting is true at a given org unit or one of its ancestors, the hold targeter will target copies from this org unit even if the org unit is closed (according to the actor.org_unit.closed_date table) IF AND ONLY IF the copy''s circ lib is the same as the hold''s pickup lib.',
6248         'coust', 'description'),
6249     'bool')
6250 ;
6251
6252 -- Evergreen DB patch XXXX.data.hold-notification-cleanup-mod.sql
6253
6254 -- check whether patch can be applied
6255 SELECT evergreen.upgrade_deps_block_check('0647', :eg_version);
6256
6257 INSERT INTO action_trigger.cleanup ( module, description ) VALUES (
6258     'CreateHoldNotification',
6259     oils_i18n_gettext(
6260         'CreateHoldNotification',
6261         'Creates a hold_notification record for each notified hold',
6262         'atclean',
6263         'description'
6264     )
6265 );
6266
6267 UPDATE action_trigger.event_definition 
6268     SET 
6269         cleanup_success = 'CreateHoldNotification' 
6270     WHERE 
6271         id = 5 -- stock hold-ready email event_def
6272         AND cleanup_success IS NULL; -- don't clobber any existing cleanup mod
6273
6274 -- Evergreen DB patch XXXX.schema.unnest-hold-permit-upgrade-script-repair.sql
6275 --
6276 -- This patch makes no changes to the baseline schema and is 
6277 -- only meant to repair a previous upgrade script.
6278 --
6279
6280 -- check whether patch can be applied
6281 SELECT evergreen.upgrade_deps_block_check('0651', :eg_version);
6282
6283 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$
6284 DECLARE
6285     matchpoint_id        INT;
6286     user_object        actor.usr%ROWTYPE;
6287     age_protect_object    config.rule_age_hold_protect%ROWTYPE;
6288     standing_penalty    config.standing_penalty%ROWTYPE;
6289     transit_range_ou_type    actor.org_unit_type%ROWTYPE;
6290     transit_source        actor.org_unit%ROWTYPE;
6291     item_object        asset.copy%ROWTYPE;
6292     item_cn_object     asset.call_number%ROWTYPE;
6293     ou_skip              actor.org_unit_setting%ROWTYPE;
6294     result            action.matrix_test_result;
6295     hold_test        config.hold_matrix_matchpoint%ROWTYPE;
6296     use_active_date   TEXT;
6297     age_protect_date  TIMESTAMP WITH TIME ZONE;
6298     hold_count        INT;
6299     hold_transit_prox    INT;
6300     frozen_hold_count    INT;
6301     context_org_list    INT[];
6302     done            BOOL := FALSE;
6303 BEGIN
6304     SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
6305     SELECT INTO context_org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( pickup_ou );
6306
6307     result.success := TRUE;
6308
6309     -- Fail if we couldn't find a user
6310     IF user_object.id IS NULL THEN
6311         result.fail_part := 'no_user';
6312         result.success := FALSE;
6313         done := TRUE;
6314         RETURN NEXT result;
6315         RETURN;
6316     END IF;
6317
6318     SELECT INTO item_object * FROM asset.copy WHERE id = match_item;
6319
6320     -- Fail if we couldn't find a copy
6321     IF item_object.id IS NULL THEN
6322         result.fail_part := 'no_item';
6323         result.success := FALSE;
6324         done := TRUE;
6325         RETURN NEXT result;
6326         RETURN;
6327     END IF;
6328
6329     SELECT INTO matchpoint_id action.find_hold_matrix_matchpoint(pickup_ou, request_ou, match_item, match_user, match_requestor);
6330     result.matchpoint := matchpoint_id;
6331
6332     SELECT INTO ou_skip * FROM actor.org_unit_setting WHERE name = 'circ.holds.target_skip_me' AND org_unit = item_object.circ_lib;
6333
6334     -- Fail if the circ_lib for the item has circ.holds.target_skip_me set to true
6335     IF ou_skip.id IS NOT NULL AND ou_skip.value = 'true' THEN
6336         result.fail_part := 'circ.holds.target_skip_me';
6337         result.success := FALSE;
6338         done := TRUE;
6339         RETURN NEXT result;
6340         RETURN;
6341     END IF;
6342
6343     -- Fail if user is barred
6344     IF user_object.barred IS TRUE THEN
6345         result.fail_part := 'actor.usr.barred';
6346         result.success := FALSE;
6347         done := TRUE;
6348         RETURN NEXT result;
6349         RETURN;
6350     END IF;
6351
6352     -- Fail if we couldn't find any matchpoint (requires a default)
6353     IF matchpoint_id IS NULL THEN
6354         result.fail_part := 'no_matchpoint';
6355         result.success := FALSE;
6356         done := TRUE;
6357         RETURN NEXT result;
6358         RETURN;
6359     END IF;
6360
6361     SELECT INTO hold_test * FROM config.hold_matrix_matchpoint WHERE id = matchpoint_id;
6362
6363     IF hold_test.holdable IS FALSE THEN
6364         result.fail_part := 'config.hold_matrix_test.holdable';
6365         result.success := FALSE;
6366         done := TRUE;
6367         RETURN NEXT result;
6368     END IF;
6369
6370     IF hold_test.transit_range IS NOT NULL THEN
6371         SELECT INTO transit_range_ou_type * FROM actor.org_unit_type WHERE id = hold_test.transit_range;
6372         IF hold_test.distance_is_from_owner THEN
6373             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;
6374         ELSE
6375             SELECT INTO transit_source * FROM actor.org_unit WHERE id = item_object.circ_lib;
6376         END IF;
6377
6378         PERFORM * FROM actor.org_unit_descendants( transit_source.id, transit_range_ou_type.depth ) WHERE id = pickup_ou;
6379
6380         IF NOT FOUND THEN
6381             result.fail_part := 'transit_range';
6382             result.success := FALSE;
6383             done := TRUE;
6384             RETURN NEXT result;
6385         END IF;
6386     END IF;
6387  
6388     FOR standing_penalty IN
6389         SELECT  DISTINCT csp.*
6390           FROM  actor.usr_standing_penalty usp
6391                 JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
6392           WHERE usr = match_user
6393                 AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
6394                 AND (usp.stop_date IS NULL or usp.stop_date > NOW())
6395                 AND csp.block_list LIKE '%HOLD%' LOOP
6396
6397         result.fail_part := standing_penalty.name;
6398         result.success := FALSE;
6399         done := TRUE;
6400         RETURN NEXT result;
6401     END LOOP;
6402
6403     IF hold_test.stop_blocked_user IS TRUE THEN
6404         FOR standing_penalty IN
6405             SELECT  DISTINCT csp.*
6406               FROM  actor.usr_standing_penalty usp
6407                     JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
6408               WHERE usr = match_user
6409                     AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
6410                     AND (usp.stop_date IS NULL or usp.stop_date > NOW())
6411                     AND csp.block_list LIKE '%CIRC%' LOOP
6412     
6413             result.fail_part := standing_penalty.name;
6414             result.success := FALSE;
6415             done := TRUE;
6416             RETURN NEXT result;
6417         END LOOP;
6418     END IF;
6419
6420     IF hold_test.max_holds IS NOT NULL AND NOT retargetting THEN
6421         SELECT    INTO hold_count COUNT(*)
6422           FROM    action.hold_request
6423           WHERE    usr = match_user
6424             AND fulfillment_time IS NULL
6425             AND cancel_time IS NULL
6426             AND CASE WHEN hold_test.include_frozen_holds THEN TRUE ELSE frozen IS FALSE END;
6427
6428         IF hold_count >= hold_test.max_holds THEN
6429             result.fail_part := 'config.hold_matrix_test.max_holds';
6430             result.success := FALSE;
6431             done := TRUE;
6432             RETURN NEXT result;
6433         END IF;
6434     END IF;
6435
6436     IF item_object.age_protect IS NOT NULL THEN
6437         SELECT INTO age_protect_object * FROM config.rule_age_hold_protect WHERE id = item_object.age_protect;
6438         IF hold_test.distance_is_from_owner THEN
6439             SELECT INTO use_active_date value FROM actor.org_unit_ancestor_setting('circ.holds.age_protect.active_date', item_cn_object.owning_lib);
6440         ELSE
6441             SELECT INTO use_active_date value FROM actor.org_unit_ancestor_setting('circ.holds.age_protect.active_date', item_object.circ_lib);
6442         END IF;
6443         IF use_active_date = 'true' THEN
6444             age_protect_date := COALESCE(item_object.active_date, NOW());
6445         ELSE
6446             age_protect_date := item_object.create_date;
6447         END IF;
6448         IF age_protect_date + age_protect_object.age > NOW() THEN
6449             IF hold_test.distance_is_from_owner THEN
6450                 SELECT INTO item_cn_object * FROM asset.call_number WHERE id = item_object.call_number;
6451                 SELECT INTO hold_transit_prox prox FROM actor.org_unit_proximity WHERE from_org = item_cn_object.owning_lib AND to_org = pickup_ou;
6452             ELSE
6453                 SELECT INTO hold_transit_prox prox FROM actor.org_unit_proximity WHERE from_org = item_object.circ_lib AND to_org = pickup_ou;
6454             END IF;
6455
6456             IF hold_transit_prox > age_protect_object.prox THEN
6457                 result.fail_part := 'config.rule_age_hold_protect.prox';
6458                 result.success := FALSE;
6459                 done := TRUE;
6460                 RETURN NEXT result;
6461             END IF;
6462         END IF;
6463     END IF;
6464
6465     IF NOT done THEN
6466         RETURN NEXT result;
6467     END IF;
6468
6469     RETURN;
6470 END;
6471 $func$ LANGUAGE plpgsql;
6472
6473 COMMIT;