]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/sql/Pg/version-upgrade/2.0-2.1-upgrade-db.sql
Avoid updating deleted call numbers for 2.0 -> 2.1 upgrade
[working/Evergreen.git] / Open-ILS / src / sql / Pg / version-upgrade / 2.0-2.1-upgrade-db.sql
1 -- 0498
2 -- Rather than polluting the public schema with general Evergreen
3 -- functions, carve out a dedicated schema.  It might already exist,
4 -- so do it before the transaction starts
5 CREATE SCHEMA evergreen;
6
7 BEGIN;
8
9 -- 0425
10 ALTER TABLE permission.grp_tree
11         ADD COLUMN hold_priority INT NOT NULL DEFAULT 0;
12
13 -- 0430 and friends
14 ALTER TABLE config.hold_matrix_matchpoint
15     ADD COLUMN strict_ou_match BOOL NOT NULL DEFAULT FALSE,
16     ADD COLUMN marc_bib_level text,
17     DROP CONSTRAINT hous_once_per_grp_loc_mod_marc,
18     DROP CONSTRAINT hold_matrix_matchpoint_marc_form_fkey,
19     DROP CONSTRAINT hold_matrix_matchpoint_marc_type_fkey,
20     DROP CONSTRAINT hold_matrix_matchpoint_marc_vr_format_fkey;
21
22
23
24 -- Replace all uses of PostgreSQL's built-in LOWER() function with
25 -- a more locale-savvy PLPERLU evergreen.lowercase() function
26 CREATE OR REPLACE FUNCTION evergreen.lowercase( TEXT ) RETURNS TEXT AS $$
27     return lc(shift);
28 $$ LANGUAGE PLPERLU STRICT IMMUTABLE;
29
30 -- 0500
31 CREATE OR REPLACE FUNCTION evergreen.change_db_setting(setting_name TEXT, settings TEXT[]) RETURNS VOID AS $$
32 BEGIN
33 EXECUTE 'ALTER DATABASE ' || quote_ident(current_database()) || ' SET ' || quote_ident(setting_name) || ' = ' || array_to_string(settings, ',');
34 END;
35
36 $$ LANGUAGE plpgsql;
37
38 -- 0501
39 SELECT evergreen.change_db_setting('search_path', ARRAY['evergreen','public','pg_catalog']);
40
41 -- Fix function breakage due to short search path
42 CREATE OR REPLACE FUNCTION evergreen.force_unicode_normal_form(string TEXT, form TEXT) RETURNS TEXT AS $func$
43 use Unicode::Normalize 'normalize';
44 return normalize($_[1],$_[0]); # reverse the params
45 $func$ LANGUAGE PLPERLU;
46
47 CREATE OR REPLACE FUNCTION evergreen.facet_force_nfc() RETURNS TRIGGER AS $$
48 BEGIN
49     NEW.value := evergreen.force_unicode_normal_form(NEW.value,'NFC');
50     RETURN NEW;
51 END;
52 $$ LANGUAGE PLPGSQL;
53
54 CREATE OR REPLACE FUNCTION evergreen.xml_escape(str TEXT) RETURNS text AS $$
55     SELECT REPLACE(REPLACE(REPLACE($1,
56        '&', '&'),
57        '<', '&lt;'),
58        '>', '&gt;');
59 $$ LANGUAGE SQL IMMUTABLE;
60
61 CREATE OR REPLACE FUNCTION evergreen.maintain_901 () RETURNS TRIGGER AS $func$
62 DECLARE
63     use_id_for_tcn BOOLEAN;
64 BEGIN
65     -- Remove any existing 901 fields before we insert the authoritative one
66     NEW.marc := REGEXP_REPLACE(NEW.marc, E'<datafield[^>]*?tag="901".+?</datafield>', '', 'g');
67
68     IF TG_TABLE_SCHEMA = 'biblio' THEN
69         -- Set TCN value to record ID?
70         SELECT enabled FROM config.global_flag INTO use_id_for_tcn
71             WHERE name = 'cat.bib.use_id_for_tcn';
72
73         IF use_id_for_tcn = 't' THEN
74             NEW.tcn_value := NEW.id;
75         END IF;
76
77         NEW.marc := REGEXP_REPLACE(
78             NEW.marc,
79             E'(</(?:[^:]*?:)?record>)',
80             E'<datafield tag="901" ind1=" " ind2=" ">' ||
81                 '<subfield code="a">' || evergreen.xml_escape(NEW.tcn_value) || E'</subfield>' ||
82                 '<subfield code="b">' || evergreen.xml_escape(NEW.tcn_source) || E'</subfield>' ||
83                 '<subfield code="c">' || NEW.id || E'</subfield>' ||
84                 '<subfield code="t">' || TG_TABLE_SCHEMA || E'</subfield>' ||
85                 CASE WHEN NEW.owner IS NOT NULL THEN '<subfield code="o">' || NEW.owner || E'</subfield>' ELSE '' END ||
86                 CASE WHEN NEW.share_depth IS NOT NULL THEN '<subfield code="d">' || NEW.share_depth || E'</subfield>' ELSE '' END ||
87              E'</datafield>\\1'
88         );
89     ELSIF TG_TABLE_SCHEMA = 'authority' THEN
90         NEW.marc := REGEXP_REPLACE(
91             NEW.marc,
92             E'(</(?:[^:]*?:)?record>)',
93             E'<datafield tag="901" ind1=" " ind2=" ">' ||
94                 '<subfield code="c">' || NEW.id || E'</subfield>' ||
95                 '<subfield code="t">' || TG_TABLE_SCHEMA || E'</subfield>' ||
96              E'</datafield>\\1'
97         );
98     ELSIF TG_TABLE_SCHEMA = 'serial' THEN
99         NEW.marc := REGEXP_REPLACE(
100             NEW.marc,
101             E'(</(?:[^:]*?:)?record>)',
102             E'<datafield tag="901" ind1=" " ind2=" ">' ||
103                 '<subfield code="c">' || NEW.id || E'</subfield>' ||
104                 '<subfield code="t">' || TG_TABLE_SCHEMA || E'</subfield>' ||
105                 '<subfield code="o">' || NEW.owning_lib || E'</subfield>' ||
106                 CASE WHEN NEW.record IS NOT NULL THEN '<subfield code="r">' || NEW.record || E'</subfield>' ELSE '' END ||
107              E'</datafield>\\1'
108         );
109     ELSE
110         NEW.marc := REGEXP_REPLACE(
111             NEW.marc,
112             E'(</(?:[^:]*?:)?record>)',
113             E'<datafield tag="901" ind1=" " ind2=" ">' ||
114                 '<subfield code="c">' || NEW.id || E'</subfield>' ||
115                 '<subfield code="t">' || TG_TABLE_SCHEMA || E'</subfield>' ||
116              E'</datafield>\\1'
117         );
118     END IF;
119
120     RETURN NEW;
121 END;
122 $func$ LANGUAGE PLPGSQL;
123
124 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;
125
126 CREATE OR REPLACE FUNCTION evergreen.lpad_number_substrings( TEXT, TEXT, INT ) RETURNS TEXT AS $$
127     my $string = shift;
128     my $pad = shift;
129     my $len = shift;
130     my $find = $len - 1;
131
132     while ($string =~ /(?:^|\D)(\d{1,$find})(?:$|\D)/) {
133         my $padded = $1;
134         $padded = $pad x ($len - length($padded)) . $padded;
135         $string =~ s/$1/$padded/sg;
136     }
137
138     return $string;
139 $$ LANGUAGE PLPERLU;
140
141 -- 0477
142 ALTER TABLE config.hard_due_date DROP CONSTRAINT hard_due_date_name_check;
143
144 -- 0478
145 CREATE OR REPLACE FUNCTION public.naco_normalize( TEXT, TEXT ) RETURNS TEXT AS $func$
146
147     use strict;
148     use Unicode::Normalize;
149     use Encode;
150
151     my $str = decode_utf8(shift);
152     my $sf = shift;
153
154     # Apply NACO normalization to input string; based on
155     # http://www.loc.gov/catdir/pcc/naco/SCA_PccNormalization_Final_revised.pdf
156     #
157     # Note that unlike a strict reading of the NACO normalization rules,
158     # output is returned as lowercase instead of uppercase for compatibility
159     # with previous versions of the Evergreen naco_normalize routine.
160
161     # Convert to upper-case first; even though final output will be lowercase, doing this will
162     # ensure that the German eszett (ß) and certain ligatures (ff, fi, ffl, etc.) will be handled correctly.
163     # If there are any bugs in Perl's implementation of upcasing, they will be passed through here.
164     $str = uc $str;
165
166     # remove non-filing strings
167     $str =~ s/\x{0098}.*?\x{009C}//g;
168
169     $str = NFKD($str);
170
171     # additional substitutions - 3.6.
172     $str =~ s/\x{00C6}/AE/g;
173     $str =~ s/\x{00DE}/TH/g;
174     $str =~ s/\x{0152}/OE/g;
175     $str =~ tr/\x{0110}\x{00D0}\x{00D8}\x{0141}\x{2113}\x{02BB}\x{02BC}]['/DDOLl/d;
176
177     # transformations based on Unicode category codes
178     $str =~ s/[\p{Cc}\p{Cf}\p{Co}\p{Cs}\p{Lm}\p{Mc}\p{Me}\p{Mn}]//g;
179
180         if ($sf && $sf =~ /^a/o) {
181                 my $commapos = index($str, ',');
182                 if ($commapos > -1) {
183                         if ($commapos != length($str) - 1) {
184                 $str =~ s/,/\x07/; # preserve first comma
185                         }
186                 }
187         }
188
189     # since we've stripped out the control characters, we can now
190     # use a few as placeholders temporarily
191     $str =~ tr/+&@\x{266D}\x{266F}#/\x01\x02\x03\x04\x05\x06/;
192     $str =~ s/[\p{Pc}\p{Pd}\p{Pe}\p{Pf}\p{Pi}\p{Po}\p{Ps}\p{Sk}\p{Sm}\p{So}\p{Zl}\p{Zp}\p{Zs}]/ /g;
193     $str =~ tr/\x01\x02\x03\x04\x05\x06\x07/+&@\x{266D}\x{266F}#,/;
194
195     # decimal digits
196     $str =~ tr/\x{0660}-\x{0669}\x{06F0}-\x{06F9}\x{07C0}-\x{07C9}\x{0966}-\x{096F}\x{09E6}-\x{09EF}\x{0A66}-\x{0A6F}\x{0AE6}-\x{0AEF}\x{0B66}-\x{0B6F}\x{0BE6}-\x{0BEF}\x{0C66}-\x{0C6F}\x{0CE6}-\x{0CEF}\x{0D66}-\x{0D6F}\x{0E50}-\x{0E59}\x{0ED0}-\x{0ED9}\x{0F20}-\x{0F29}\x{1040}-\x{1049}\x{1090}-\x{1099}\x{17E0}-\x{17E9}\x{1810}-\x{1819}\x{1946}-\x{194F}\x{19D0}-\x{19D9}\x{1A80}-\x{1A89}\x{1A90}-\x{1A99}\x{1B50}-\x{1B59}\x{1BB0}-\x{1BB9}\x{1C40}-\x{1C49}\x{1C50}-\x{1C59}\x{A620}-\x{A629}\x{A8D0}-\x{A8D9}\x{A900}-\x{A909}\x{A9D0}-\x{A9D9}\x{AA50}-\x{AA59}\x{ABF0}-\x{ABF9}\x{FF10}-\x{FF19}/0-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-90-9/;
197
198     # intentionally skipping step 8 of the NACO algorithm; if the string
199     # gets normalized away, that's fine.
200
201     # leading and trailing spaces
202     $str =~ s/\s+/ /g;
203     $str =~ s/^\s+//;
204     $str =~ s/\s+$//g;
205
206     return lc $str;
207 $func$ LANGUAGE 'plperlu' STRICT IMMUTABLE;
208
209 -- 0479
210 CREATE OR REPLACE FUNCTION permission.grp_ancestors_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$
211     WITH RECURSIVE grp_ancestors_distance(id, distance) AS (
212             SELECT $1, 0
213         UNION
214             SELECT pgt.parent, gad.distance+1
215             FROM permission.grp_tree pgt JOIN grp_ancestors_distance gad ON pgt.id = gad.id
216             WHERE pgt.parent IS NOT NULL
217     )
218     SELECT * FROM grp_ancestors_distance;
219 $$ LANGUAGE SQL STABLE;
220
221 CREATE OR REPLACE FUNCTION permission.grp_descendants_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$
222     WITH RECURSIVE grp_descendants_distance(id, distance) AS (
223             SELECT $1, 0
224         UNION
225             SELECT pgt.id, gdd.distance+1
226             FROM permission.grp_tree pgt JOIN grp_descendants_distance gdd ON pgt.parent = gdd.id
227     )
228     SELECT * FROM grp_descendants_distance;
229 $$ LANGUAGE SQL STABLE;
230
231 CREATE OR REPLACE FUNCTION actor.org_unit_ancestors_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$
232     WITH RECURSIVE org_unit_ancestors_distance(id, distance) AS (
233             SELECT $1, 0
234         UNION
235             SELECT ou.parent_ou, ouad.distance+1
236             FROM actor.org_unit ou JOIN org_unit_ancestors_distance ouad ON ou.id = ouad.id
237             WHERE ou.parent_ou IS NOT NULL
238     )
239     SELECT * FROM org_unit_ancestors_distance;
240 $$ LANGUAGE SQL STABLE;
241
242 CREATE OR REPLACE FUNCTION actor.org_unit_descendants_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$
243     WITH RECURSIVE org_unit_descendants_distance(id, distance) AS (
244             SELECT $1, 0
245         UNION
246             SELECT ou.id, oudd.distance+1
247             FROM actor.org_unit ou JOIN org_unit_descendants_distance oudd ON ou.parent_ou = oudd.id
248     )
249     SELECT * FROM org_unit_descendants_distance;
250 $$ LANGUAGE SQL STABLE;
251
252 CREATE TABLE config.circ_matrix_weights (
253     id                      SERIAL  PRIMARY KEY,
254     name                    TEXT    NOT NULL UNIQUE,
255     org_unit                NUMERIC(6,2)   NOT NULL,
256     grp                     NUMERIC(6,2)   NOT NULL,
257     circ_modifier           NUMERIC(6,2)   NOT NULL,
258     marc_type               NUMERIC(6,2)   NOT NULL,
259     marc_form               NUMERIC(6,2)   NOT NULL,
260     marc_vr_format          NUMERIC(6,2)   NOT NULL,
261     copy_circ_lib           NUMERIC(6,2)   NOT NULL,
262     copy_owning_lib         NUMERIC(6,2)   NOT NULL,
263     user_home_ou            NUMERIC(6,2)   NOT NULL,
264     ref_flag                NUMERIC(6,2)   NOT NULL,
265     juvenile_flag           NUMERIC(6,2)   NOT NULL,
266     is_renewal              NUMERIC(6,2)   NOT NULL,
267     usr_age_lower_bound     NUMERIC(6,2)   NOT NULL,
268     usr_age_upper_bound     NUMERIC(6,2)   NOT NULL
269 );
270
271 CREATE TABLE config.hold_matrix_weights (
272     id                      SERIAL  PRIMARY KEY,
273     name                    TEXT    NOT NULL UNIQUE,
274     user_home_ou            NUMERIC(6,2)   NOT NULL,
275     request_ou              NUMERIC(6,2)   NOT NULL,
276     pickup_ou               NUMERIC(6,2)   NOT NULL,
277     item_owning_ou          NUMERIC(6,2)   NOT NULL,
278     item_circ_ou            NUMERIC(6,2)   NOT NULL,
279     usr_grp                 NUMERIC(6,2)   NOT NULL,
280     requestor_grp           NUMERIC(6,2)   NOT NULL,
281     circ_modifier           NUMERIC(6,2)   NOT NULL,
282     marc_type               NUMERIC(6,2)   NOT NULL,
283     marc_form               NUMERIC(6,2)   NOT NULL,
284     marc_vr_format          NUMERIC(6,2)   NOT NULL,
285     juvenile_flag           NUMERIC(6,2)   NOT NULL,
286     ref_flag                NUMERIC(6,2)   NOT NULL
287 );
288
289 CREATE TABLE config.weight_assoc (
290     id                      SERIAL  PRIMARY KEY,
291     active                  BOOL    NOT NULL,
292     org_unit                INT     NOT NULL REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
293     circ_weights            INT     REFERENCES config.circ_matrix_weights (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
294     hold_weights            INT     REFERENCES config.hold_matrix_weights (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED
295 );
296 CREATE UNIQUE INDEX cwa_one_active_per_ou ON config.weight_assoc (org_unit) WHERE active;
297
298 INSERT INTO config.circ_matrix_weights(name, org_unit, grp, circ_modifier, marc_type, marc_form, marc_vr_format, copy_circ_lib, copy_owning_lib, user_home_ou, ref_flag, juvenile_flag, is_renewal, usr_age_upper_bound, usr_age_lower_bound) VALUES 
299     ('Default', 10.0, 11.0, 5.0, 4.0, 3.0, 2.0, 8.0, 8.0, 8.0, 1.0, 6.0, 7.0, 0.0, 0.0),
300     ('Org_Unit_First', 11.0, 10.0, 5.0, 4.0, 3.0, 2.0, 8.0, 8.0, 8.0, 1.0, 6.0, 7.0, 0.0, 0.0),
301     ('Item_Owner_First', 8.0, 8.0, 5.0, 4.0, 3.0, 2.0, 10.0, 11.0, 8.0, 1.0, 6.0, 7.0, 0.0, 0.0),
302     ('All_Equal', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
303
304 INSERT INTO config.hold_matrix_weights(name, user_home_ou, request_ou, pickup_ou, item_owning_ou, item_circ_ou, usr_grp, requestor_grp, circ_modifier, marc_type, marc_form, marc_vr_format, juvenile_flag, ref_flag) VALUES
305     ('Default', 5.0, 5.0, 5.0, 5.0, 5.0, 7.0, 8.0, 4.0, 3.0, 2.0, 1.0, 4.0, 0.0),
306     ('Item_Owner_First', 5.0, 5.0, 5.0, 8.0, 7.0, 5.0, 5.0, 4.0, 3.0, 2.0, 1.0, 4.0, 0.0),
307     ('User_Before_Requestor', 5.0, 5.0, 5.0, 5.0, 5.0, 8.0, 7.0, 4.0, 3.0, 2.0, 1.0, 4.0, 0.0),
308     ('All_Equal', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
309
310 INSERT INTO config.weight_assoc(active, org_unit, circ_weights, hold_weights) VALUES
311     (true, 1, 1, 1);
312
313 -- 0480
314 CREATE OR REPLACE FUNCTION actor.usr_purge_data(
315         src_usr  IN INTEGER,
316         specified_dest_usr IN INTEGER
317 ) RETURNS VOID AS $$
318 DECLARE
319         suffix TEXT;
320         renamable_row RECORD;
321         dest_usr INTEGER;
322 BEGIN
323
324         IF specified_dest_usr IS NULL THEN
325                 dest_usr := 1; -- Admin user on stock installs
326         ELSE
327                 dest_usr := specified_dest_usr;
328         END IF;
329
330         UPDATE actor.usr SET
331                 active = FALSE,
332                 card = NULL,
333                 mailing_address = NULL,
334                 billing_address = NULL
335         WHERE id = src_usr;
336
337         -- acq.*
338         UPDATE acq.fund_allocation SET allocator = dest_usr WHERE allocator = src_usr;
339         UPDATE acq.lineitem SET creator = dest_usr WHERE creator = src_usr;
340         UPDATE acq.lineitem SET editor = dest_usr WHERE editor = src_usr;
341         UPDATE acq.lineitem SET selector = dest_usr WHERE selector = src_usr;
342         UPDATE acq.lineitem_note SET creator = dest_usr WHERE creator = src_usr;
343         UPDATE acq.lineitem_note SET editor = dest_usr WHERE editor = src_usr;
344         DELETE FROM acq.lineitem_usr_attr_definition WHERE usr = src_usr;
345
346         -- Update with a rename to avoid collisions
347         FOR renamable_row in
348                 SELECT id, name
349                 FROM   acq.picklist
350                 WHERE  owner = src_usr
351         LOOP
352                 suffix := ' (' || src_usr || ')';
353                 LOOP
354                         BEGIN
355                                 UPDATE  acq.picklist
356                                 SET     owner = dest_usr, name = name || suffix
357                                 WHERE   id = renamable_row.id;
358                         EXCEPTION WHEN unique_violation THEN
359                                 suffix := suffix || ' ';
360                                 CONTINUE;
361                         END;
362                         EXIT;
363                 END LOOP;
364         END LOOP;
365
366         UPDATE acq.picklist SET creator = dest_usr WHERE creator = src_usr;
367         UPDATE acq.picklist SET editor = dest_usr WHERE editor = src_usr;
368         UPDATE acq.po_note SET creator = dest_usr WHERE creator = src_usr;
369         UPDATE acq.po_note SET editor = dest_usr WHERE editor = src_usr;
370         UPDATE acq.purchase_order SET owner = dest_usr WHERE owner = src_usr;
371         UPDATE acq.purchase_order SET creator = dest_usr WHERE creator = src_usr;
372         UPDATE acq.purchase_order SET editor = dest_usr WHERE editor = src_usr;
373         UPDATE acq.claim_event SET creator = dest_usr WHERE creator = src_usr;
374
375         -- action.*
376         DELETE FROM action.circulation WHERE usr = src_usr;
377         UPDATE action.circulation SET circ_staff = dest_usr WHERE circ_staff = src_usr;
378         UPDATE action.circulation SET checkin_staff = dest_usr WHERE checkin_staff = src_usr;
379         UPDATE action.hold_notification SET notify_staff = dest_usr WHERE notify_staff = src_usr;
380         UPDATE action.hold_request SET fulfillment_staff = dest_usr WHERE fulfillment_staff = src_usr;
381         UPDATE action.hold_request SET requestor = dest_usr WHERE requestor = src_usr;
382         DELETE FROM action.hold_request WHERE usr = src_usr;
383         UPDATE action.in_house_use SET staff = dest_usr WHERE staff = src_usr;
384         UPDATE action.non_cat_in_house_use SET staff = dest_usr WHERE staff = src_usr;
385         DELETE FROM action.non_cataloged_circulation WHERE patron = src_usr;
386         UPDATE action.non_cataloged_circulation SET staff = dest_usr WHERE staff = src_usr;
387         DELETE FROM action.survey_response WHERE usr = src_usr;
388         UPDATE action.fieldset SET owner = dest_usr WHERE owner = src_usr;
389
390         -- actor.*
391         DELETE FROM actor.card WHERE usr = src_usr;
392         DELETE FROM actor.stat_cat_entry_usr_map WHERE target_usr = src_usr;
393
394         -- The following update is intended to avoid transient violations of a foreign
395         -- key constraint, whereby actor.usr_address references itself.  It may not be
396         -- necessary, but it does no harm.
397         UPDATE actor.usr_address SET replaces = NULL
398                 WHERE usr = src_usr AND replaces IS NOT NULL;
399         DELETE FROM actor.usr_address WHERE usr = src_usr;
400         DELETE FROM actor.usr_note WHERE usr = src_usr;
401         UPDATE actor.usr_note SET creator = dest_usr WHERE creator = src_usr;
402         DELETE FROM actor.usr_org_unit_opt_in WHERE usr = src_usr;
403         UPDATE actor.usr_org_unit_opt_in SET staff = dest_usr WHERE staff = src_usr;
404         DELETE FROM actor.usr_setting WHERE usr = src_usr;
405         DELETE FROM actor.usr_standing_penalty WHERE usr = src_usr;
406         UPDATE actor.usr_standing_penalty SET staff = dest_usr WHERE staff = src_usr;
407
408         -- asset.*
409         UPDATE asset.call_number SET creator = dest_usr WHERE creator = src_usr;
410         UPDATE asset.call_number SET editor = dest_usr WHERE editor = src_usr;
411         UPDATE asset.call_number_note SET creator = dest_usr WHERE creator = src_usr;
412         UPDATE asset.copy SET creator = dest_usr WHERE creator = src_usr;
413         UPDATE asset.copy SET editor = dest_usr WHERE editor = src_usr;
414         UPDATE asset.copy_note SET creator = dest_usr WHERE creator = src_usr;
415
416         -- auditor.*
417         DELETE FROM auditor.actor_usr_address_history WHERE id = src_usr;
418         DELETE FROM auditor.actor_usr_history WHERE id = src_usr;
419         UPDATE auditor.asset_call_number_history SET creator = dest_usr WHERE creator = src_usr;
420         UPDATE auditor.asset_call_number_history SET editor  = dest_usr WHERE editor  = src_usr;
421         UPDATE auditor.asset_copy_history SET creator = dest_usr WHERE creator = src_usr;
422         UPDATE auditor.asset_copy_history SET editor  = dest_usr WHERE editor  = src_usr;
423         UPDATE auditor.biblio_record_entry_history SET creator = dest_usr WHERE creator = src_usr;
424         UPDATE auditor.biblio_record_entry_history SET editor  = dest_usr WHERE editor  = src_usr;
425
426         -- biblio.*
427         UPDATE biblio.record_entry SET creator = dest_usr WHERE creator = src_usr;
428         UPDATE biblio.record_entry SET editor = dest_usr WHERE editor = src_usr;
429         UPDATE biblio.record_note SET creator = dest_usr WHERE creator = src_usr;
430         UPDATE biblio.record_note SET editor = dest_usr WHERE editor = src_usr;
431
432         -- container.*
433         -- Update buckets with a rename to avoid collisions
434         FOR renamable_row in
435                 SELECT id, name
436                 FROM   container.biblio_record_entry_bucket
437                 WHERE  owner = src_usr
438         LOOP
439                 suffix := ' (' || src_usr || ')';
440                 LOOP
441                         BEGIN
442                                 UPDATE  container.biblio_record_entry_bucket
443                                 SET     owner = dest_usr, name = name || suffix
444                                 WHERE   id = renamable_row.id;
445                         EXCEPTION WHEN unique_violation THEN
446                                 suffix := suffix || ' ';
447                                 CONTINUE;
448                         END;
449                         EXIT;
450                 END LOOP;
451         END LOOP;
452
453         FOR renamable_row in
454                 SELECT id, name
455                 FROM   container.call_number_bucket
456                 WHERE  owner = src_usr
457         LOOP
458                 suffix := ' (' || src_usr || ')';
459                 LOOP
460                         BEGIN
461                                 UPDATE  container.call_number_bucket
462                                 SET     owner = dest_usr, name = name || suffix
463                                 WHERE   id = renamable_row.id;
464                         EXCEPTION WHEN unique_violation THEN
465                                 suffix := suffix || ' ';
466                                 CONTINUE;
467                         END;
468                         EXIT;
469                 END LOOP;
470         END LOOP;
471
472         FOR renamable_row in
473                 SELECT id, name
474                 FROM   container.copy_bucket
475                 WHERE  owner = src_usr
476         LOOP
477                 suffix := ' (' || src_usr || ')';
478                 LOOP
479                         BEGIN
480                                 UPDATE  container.copy_bucket
481                                 SET     owner = dest_usr, name = name || suffix
482                                 WHERE   id = renamable_row.id;
483                         EXCEPTION WHEN unique_violation THEN
484                                 suffix := suffix || ' ';
485                                 CONTINUE;
486                         END;
487                         EXIT;
488                 END LOOP;
489         END LOOP;
490
491         FOR renamable_row in
492                 SELECT id, name
493                 FROM   container.user_bucket
494                 WHERE  owner = src_usr
495         LOOP
496                 suffix := ' (' || src_usr || ')';
497                 LOOP
498                         BEGIN
499                                 UPDATE  container.user_bucket
500                                 SET     owner = dest_usr, name = name || suffix
501                                 WHERE   id = renamable_row.id;
502                         EXCEPTION WHEN unique_violation THEN
503                                 suffix := suffix || ' ';
504                                 CONTINUE;
505                         END;
506                         EXIT;
507                 END LOOP;
508         END LOOP;
509
510         DELETE FROM container.user_bucket_item WHERE target_user = src_usr;
511
512         -- money.*
513         DELETE FROM money.billable_xact WHERE usr = src_usr;
514         DELETE FROM money.collections_tracker WHERE usr = src_usr;
515         UPDATE money.collections_tracker SET collector = dest_usr WHERE collector = src_usr;
516
517         -- permission.*
518         DELETE FROM permission.usr_grp_map WHERE usr = src_usr;
519         DELETE FROM permission.usr_object_perm_map WHERE usr = src_usr;
520         DELETE FROM permission.usr_perm_map WHERE usr = src_usr;
521         DELETE FROM permission.usr_work_ou_map WHERE usr = src_usr;
522
523         -- reporter.*
524         -- Update with a rename to avoid collisions
525         BEGIN
526                 FOR renamable_row in
527                         SELECT id, name
528                         FROM   reporter.output_folder
529                         WHERE  owner = src_usr
530                 LOOP
531                         suffix := ' (' || src_usr || ')';
532                         LOOP
533                                 BEGIN
534                                         UPDATE  reporter.output_folder
535                                         SET     owner = dest_usr, name = name || suffix
536                                         WHERE   id = renamable_row.id;
537                                 EXCEPTION WHEN unique_violation THEN
538                                         suffix := suffix || ' ';
539                                         CONTINUE;
540                                 END;
541                                 EXIT;
542                         END LOOP;
543                 END LOOP;
544         EXCEPTION WHEN undefined_table THEN
545                 -- do nothing
546         END;
547
548         BEGIN
549                 UPDATE reporter.report SET owner = dest_usr WHERE owner = src_usr;
550         EXCEPTION WHEN undefined_table THEN
551                 -- do nothing
552         END;
553
554         -- Update with a rename to avoid collisions
555         BEGIN
556                 FOR renamable_row in
557                         SELECT id, name
558                         FROM   reporter.report_folder
559                         WHERE  owner = src_usr
560                 LOOP
561                         suffix := ' (' || src_usr || ')';
562                         LOOP
563                                 BEGIN
564                                         UPDATE  reporter.report_folder
565                                         SET     owner = dest_usr, name = name || suffix
566                                         WHERE   id = renamable_row.id;
567                                 EXCEPTION WHEN unique_violation THEN
568                                         suffix := suffix || ' ';
569                                         CONTINUE;
570                                 END;
571                                 EXIT;
572                         END LOOP;
573                 END LOOP;
574         EXCEPTION WHEN undefined_table THEN
575                 -- do nothing
576         END;
577
578         BEGIN
579                 UPDATE reporter.schedule SET runner = dest_usr WHERE runner = src_usr;
580         EXCEPTION WHEN undefined_table THEN
581                 -- do nothing
582         END;
583
584         BEGIN
585                 UPDATE reporter.template SET owner = dest_usr WHERE owner = src_usr;
586         EXCEPTION WHEN undefined_table THEN
587                 -- do nothing
588         END;
589
590         -- Update with a rename to avoid collisions
591         BEGIN
592                 FOR renamable_row in
593                         SELECT id, name
594                         FROM   reporter.template_folder
595                         WHERE  owner = src_usr
596                 LOOP
597                         suffix := ' (' || src_usr || ')';
598                         LOOP
599                                 BEGIN
600                                         UPDATE  reporter.template_folder
601                                         SET     owner = dest_usr, name = name || suffix
602                                         WHERE   id = renamable_row.id;
603                                 EXCEPTION WHEN unique_violation THEN
604                                         suffix := suffix || ' ';
605                                         CONTINUE;
606                                 END;
607                                 EXIT;
608                         END LOOP;
609                 END LOOP;
610         EXCEPTION WHEN undefined_table THEN
611         -- do nothing
612         END;
613
614         -- vandelay.*
615         -- Update with a rename to avoid collisions
616         FOR renamable_row in
617                 SELECT id, name
618                 FROM   vandelay.queue
619                 WHERE  owner = src_usr
620         LOOP
621                 suffix := ' (' || src_usr || ')';
622                 LOOP
623                         BEGIN
624                                 UPDATE  vandelay.queue
625                                 SET     owner = dest_usr, name = name || suffix
626                                 WHERE   id = renamable_row.id;
627                         EXCEPTION WHEN unique_violation THEN
628                                 suffix := suffix || ' ';
629                                 CONTINUE;
630                         END;
631                         EXIT;
632                 END LOOP;
633         END LOOP;
634
635 END;
636 $$ LANGUAGE plpgsql;
637
638 -- 0482, 0487, and parts of others
639 -- Circ matchpoint table changes
640 ALTER TABLE config.circ_matrix_matchpoint
641     ALTER COLUMN circulate DROP NOT NULL, -- Fallthrough enable
642     ALTER COLUMN circulate DROP DEFAULT, -- Stop defaulting to true to enable default to fallthrough
643     ALTER COLUMN duration_rule DROP NOT NULL, -- Fallthrough enable
644     ALTER COLUMN recurring_fine_rule DROP NOT NULL, -- Fallthrough enable
645     ALTER COLUMN max_fine_rule DROP NOT NULL, -- Fallthrough enable
646     ADD COLUMN renewals INT, -- Renewals override
647     ADD COLUMN user_home_ou INT REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
648     ADD COLUMN grace_period INTERVAL,
649     ADD COLUMN marc_bib_level text,
650     DROP CONSTRAINT ep_once_per_grp_loc_mod_marc,
651     DROP CONSTRAINT circ_matrix_matchpoint_marc_form_fkey,
652     DROP CONSTRAINT circ_matrix_matchpoint_marc_type_fkey,
653     DROP CONSTRAINT circ_matrix_matchpoint_marc_vr_format_fkey;
654
655 -- Clean up tables before making normalized index
656
657 CREATE OR REPLACE FUNCTION action.cleanup_matrix_matchpoints() RETURNS void AS $func$
658 DECLARE
659     temp_row    RECORD;
660 BEGIN
661     -- Circ Matrix
662     FOR temp_row IN
663         SELECT org_unit, grp, circ_modifier, marc_type, marc_form, marc_vr_format, copy_circ_lib, copy_owning_lib, user_home_ou, ref_flag, juvenile_flag, is_renewal, usr_age_lower_bound, usr_age_upper_bound, COUNT(id) as rowcount, MIN(id) as firstrow
664         FROM config.circ_matrix_matchpoint
665         WHERE active
666         GROUP BY org_unit, grp, circ_modifier, marc_type, marc_form, marc_vr_format, copy_circ_lib, copy_owning_lib, user_home_ou, ref_flag, juvenile_flag, is_renewal, usr_age_lower_bound, usr_age_upper_bound
667         HAVING COUNT(id) > 1 LOOP
668
669         UPDATE config.circ_matrix_matchpoint SET active=false
670             WHERE id > temp_row.firstrow
671                 AND org_unit = temp_row.org_unit
672                 AND grp = temp_row.grp
673                 AND circ_modifier       IS NOT DISTINCT FROM temp_row.circ_modifier
674                 AND marc_type           IS NOT DISTINCT FROM temp_row.marc_type
675                 AND marc_form           IS NOT DISTINCT FROM temp_row.marc_form
676                 AND marc_vr_format      IS NOT DISTINCT FROM temp_row.marc_vr_format
677                 AND copy_circ_lib       IS NOT DISTINCT FROM temp_row.copy_circ_lib
678                 AND copy_owning_lib     IS NOT DISTINCT FROM temp_row.copy_owning_lib
679                 AND user_home_ou        IS NOT DISTINCT FROM temp_row.user_home_ou
680                 AND ref_flag            IS NOT DISTINCT FROM temp_row.ref_flag
681                 AND juvenile_flag       IS NOT DISTINCT FROM temp_row.juvenile_flag
682                 AND is_renewal          IS NOT DISTINCT FROM temp_row.is_renewal
683                 AND usr_age_lower_bound IS NOT DISTINCT FROM temp_row.usr_age_lower_bound
684                 AND usr_age_upper_bound IS NOT DISTINCT FROM temp_row.usr_age_upper_bound;
685     END LOOP;
686
687     -- Hold Matrix
688     FOR temp_row IN
689         SELECT user_home_ou, request_ou, pickup_ou, item_owning_ou, item_circ_ou, usr_grp, requestor_grp, circ_modifier, marc_type, marc_form, marc_vr_format, juvenile_flag, ref_flag, COUNT(id) as rowcount, MIN(id) as firstrow
690         FROM config.hold_matrix_matchpoint
691         WHERE active
692         GROUP BY user_home_ou, request_ou, pickup_ou, item_owning_ou, item_circ_ou, usr_grp, requestor_grp, circ_modifier, marc_type, marc_form, marc_vr_format, juvenile_flag, ref_flag
693         HAVING COUNT(id) > 1 LOOP
694
695         UPDATE config.hold_matrix_matchpoint SET active=false
696             WHERE id > temp_row.firstrow
697                 AND user_home_ou        IS NOT DISTINCT FROM temp_row.user_home_ou
698                 AND request_ou          IS NOT DISTINCT FROM temp_row.request_ou
699                 AND pickup_ou           IS NOT DISTINCT FROM temp_row.pickup_ou
700                 AND item_owning_ou      IS NOT DISTINCT FROM temp_row.item_owning_ou
701                 AND item_circ_ou        IS NOT DISTINCT FROM temp_row.item_circ_ou
702                 AND usr_grp             IS NOT DISTINCT FROM temp_row.usr_grp
703                 AND requestor_grp       IS NOT DISTINCT FROM temp_row.requestor_grp
704                 AND circ_modifier       IS NOT DISTINCT FROM temp_row.circ_modifier
705                 AND marc_type           IS NOT DISTINCT FROM temp_row.marc_type
706                 AND marc_form           IS NOT DISTINCT FROM temp_row.marc_form
707                 AND marc_vr_format      IS NOT DISTINCT FROM temp_row.marc_vr_format
708                 AND juvenile_flag       IS NOT DISTINCT FROM temp_row.juvenile_flag
709                 AND ref_flag            IS NOT DISTINCT FROM temp_row.ref_flag;
710     END LOOP;
711 END;
712 $func$ LANGUAGE plpgsql;
713
714 SELECT action.cleanup_matrix_matchpoints();
715
716 DROP FUNCTION IF EXISTS action.cleanup_matrix_matchpoints();
717
718 -- Create Normalized indexes
719
720 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_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, '')) WHERE active;
721
722 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_vr_format, ''), COALESCE(juvenile_flag::TEXT, ''), COALESCE(ref_flag::TEXT, '')) WHERE active;
723
724 -- 0484
725 DROP FUNCTION asset.metarecord_copy_count ( INT, BIGINT, BOOL );
726 DROP FUNCTION asset.record_copy_count ( INT, BIGINT, BOOL );
727
728 DROP FUNCTION asset.opac_ou_record_copy_count (INT, BIGINT);
729 CREATE OR REPLACE FUNCTION asset.opac_ou_record_copy_count (org INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
730 DECLARE
731     ans RECORD;
732     trans INT;
733 BEGIN
734     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
735
736     FOR ans IN SELECT u.id, t.depth FROM actor.org_unit_ancestors(org) AS u JOIN actor.org_unit_type t ON (u.ou_type = t.id) LOOP
737         RETURN QUERY
738         SELECT  ans.depth,
739                 ans.id,
740                 COUNT( av.id ),
741                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
742                 COUNT( av.id ),
743                 trans
744           FROM  
745                 actor.org_unit_descendants(ans.id) d
746                 JOIN asset.opac_visible_copies av ON (av.record = rid AND av.circ_lib = d.id)
747                 JOIN asset.copy cp ON (cp.id = av.copy_id)
748           GROUP BY 1,2,6;
749
750         IF NOT FOUND THEN
751             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
752         END IF;
753
754     END LOOP;
755
756     RETURN;
757 END;
758 $f$ LANGUAGE PLPGSQL;
759
760 DROP FUNCTION asset.opac_lasso_record_copy_count (INT, BIGINT);
761 CREATE OR REPLACE FUNCTION asset.opac_lasso_record_copy_count (i_lasso INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
762 DECLARE
763     ans RECORD;
764     trans INT;
765 BEGIN
766     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
767
768     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
769         RETURN QUERY
770         SELECT  -1,
771                 ans.id,
772                 COUNT( av.id ),
773                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
774                 COUNT( av.id ),
775                 trans
776           FROM  
777                 actor.org_unit_descendants(ans.id) d
778                 JOIN asset.opac_visible_copies av ON (av.record = rid AND av.circ_lib = d.id)
779                 JOIN asset.copy cp ON (cp.id = av.copy_id)
780           GROUP BY 1,2,6;
781
782         IF NOT FOUND THEN
783             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
784         END IF;
785
786     END LOOP;
787
788     RETURN;
789 END;
790 $f$ LANGUAGE PLPGSQL;
791
792 DROP FUNCTION asset.staff_ou_record_copy_count (INT, BIGINT);
793
794 DROP FUNCTION asset.staff_lasso_record_copy_count (INT, BIGINT);
795
796 CREATE OR REPLACE FUNCTION asset.record_copy_count ( place INT, rid BIGINT, staff BOOL) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
797 BEGIN
798     IF staff IS TRUE THEN
799         IF place > 0 THEN
800             RETURN QUERY SELECT * FROM asset.staff_ou_record_copy_count( place, rid );
801         ELSE
802             RETURN QUERY SELECT * FROM asset.staff_lasso_record_copy_count( -place, rid );
803         END IF;
804     ELSE
805         IF place > 0 THEN
806             RETURN QUERY SELECT * FROM asset.opac_ou_record_copy_count( place, rid );
807         ELSE
808             RETURN QUERY SELECT * FROM asset.opac_lasso_record_copy_count( -place, rid );
809         END IF;
810     END IF;
811
812     RETURN;
813 END;
814 $f$ LANGUAGE PLPGSQL;
815
816 DROP FUNCTION asset.opac_ou_metarecord_copy_count (INT, BIGINT);
817 CREATE OR REPLACE FUNCTION asset.opac_ou_metarecord_copy_count (org INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
818 DECLARE
819     ans RECORD;
820     trans INT;
821 BEGIN
822     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
823
824     FOR ans IN SELECT u.id, t.depth FROM actor.org_unit_ancestors(org) AS u JOIN actor.org_unit_type t ON (u.ou_type = t.id) LOOP
825         RETURN QUERY
826         SELECT  ans.depth,
827                 ans.id,
828                 COUNT( av.id ),
829                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
830                 COUNT( av.id ),
831                 trans
832           FROM
833                 actor.org_unit_descendants(ans.id) d
834                 JOIN asset.opac_visible_copies av ON (av.record = rid AND av.circ_lib = d.id)
835                 JOIN asset.copy cp ON (cp.id = av.copy_id)
836                 JOIN metabib.metarecord_source_map m ON (m.source = av.record)
837           GROUP BY 1,2,6;
838
839         IF NOT FOUND THEN
840             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
841         END IF;
842
843     END LOOP;
844
845     RETURN;
846 END;
847 $f$ LANGUAGE PLPGSQL;
848
849 DROP FUNCTION asset.opac_lasso_metarecord_copy_count (INT, BIGINT);
850 CREATE OR REPLACE FUNCTION asset.opac_lasso_metarecord_copy_count (i_lasso INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
851 DECLARE
852     ans RECORD;
853     trans INT;
854 BEGIN
855     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
856
857     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
858         RETURN QUERY
859         SELECT  -1,
860                 ans.id,
861                 COUNT( av.id ),
862                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
863                 COUNT( av.id ),
864                 trans
865           FROM
866                 actor.org_unit_descendants(ans.id) d
867                 JOIN asset.opac_visible_copies av ON (av.record = rid AND av.circ_lib = d.id)
868                 JOIN asset.copy cp ON (cp.id = av.copy_id)
869                 JOIN metabib.metarecord_source_map m ON (m.source = av.record)
870           GROUP BY 1,2,6;
871
872         IF NOT FOUND THEN
873             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
874         END IF;
875
876     END LOOP;
877
878     RETURN;
879 END;
880 $f$ LANGUAGE PLPGSQL;
881
882 DROP FUNCTION asset.staff_lasso_metarecord_copy_count (INT, BIGINT);
883
884 CREATE OR REPLACE FUNCTION asset.metarecord_copy_count ( place INT, rid BIGINT, staff BOOL) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
885 BEGIN
886     IF staff IS TRUE THEN
887         IF place > 0 THEN
888             RETURN QUERY SELECT * FROM asset.staff_ou_metarecord_copy_count( place, rid );
889         ELSE
890             RETURN QUERY SELECT * FROM asset.staff_lasso_metarecord_copy_count( -place, rid );
891         END IF;
892     ELSE
893         IF place > 0 THEN
894             RETURN QUERY SELECT * FROM asset.opac_ou_metarecord_copy_count( place, rid );
895         ELSE
896             RETURN QUERY SELECT * FROM asset.opac_lasso_metarecord_copy_count( -place, rid );
897         END IF;
898     END IF;
899
900     RETURN;
901 END;
902 $f$ LANGUAGE PLPGSQL;
903
904 -- 0485
905 CREATE OR REPLACE VIEW reporter.simple_record AS
906 SELECT  r.id,
907         s.metarecord,
908         r.fingerprint,
909         r.quality,
910         r.tcn_source,
911         r.tcn_value,
912         title.value AS title,
913         uniform_title.value AS uniform_title,
914         author.value AS author,
915         publisher.value AS publisher,
916         SUBSTRING(pubdate.value FROM $$\d+$$) AS pubdate,
917         series_title.value AS series_title,
918         series_statement.value AS series_statement,
919         summary.value AS summary,
920         ARRAY_ACCUM( DISTINCT REPLACE(SUBSTRING(isbn.value FROM $$^\S+$$), '-', '') ) AS isbn,
921         ARRAY_ACCUM( DISTINCT REGEXP_REPLACE(issn.value, E'^\\S*(\\d{4})[-\\s](\\d{3,4}x?)', E'\\1 \\2') ) AS issn,
922         ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '650' AND subfield = 'a' AND record = r.id)) AS topic_subject,
923         ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '651' AND subfield = 'a' AND record = r.id)) AS geographic_subject,
924         ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '655' AND subfield = 'a' AND record = r.id)) AS genre,
925         ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '600' AND subfield = 'a' AND record = r.id)) AS name_subject,
926         ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '610' AND subfield = 'a' AND record = r.id)) AS corporate_subject,
927         ARRAY((SELECT value FROM metabib.full_rec WHERE tag = '856' AND subfield IN ('3','y','u') AND record = r.id ORDER BY CASE WHEN subfield IN ('3','y') THEN 0 ELSE 1 END)) AS external_uri
928   FROM  biblio.record_entry r
929         JOIN metabib.metarecord_source_map s ON (s.source = r.id)
930         LEFT JOIN metabib.full_rec uniform_title ON (r.id = uniform_title.record AND uniform_title.tag = '240' AND uniform_title.subfield = 'a')
931         LEFT JOIN metabib.full_rec title ON (r.id = title.record AND title.tag = '245' AND title.subfield = 'a')
932         LEFT JOIN metabib.full_rec author ON (r.id = author.record AND author.tag = '100' AND author.subfield = 'a')
933         LEFT JOIN metabib.full_rec publisher ON (r.id = publisher.record AND publisher.tag = '260' AND publisher.subfield = 'b')
934         LEFT JOIN metabib.full_rec pubdate ON (r.id = pubdate.record AND pubdate.tag = '260' AND pubdate.subfield = 'c')
935         LEFT JOIN metabib.full_rec isbn ON (r.id = isbn.record AND isbn.tag IN ('024', '020') AND isbn.subfield IN ('a','z'))
936         LEFT JOIN metabib.full_rec issn ON (r.id = issn.record AND issn.tag = '022' AND issn.subfield = 'a')
937         LEFT JOIN metabib.full_rec series_title ON (r.id = series_title.record AND series_title.tag IN ('830','440') AND series_title.subfield = 'a')
938         LEFT JOIN metabib.full_rec series_statement ON (r.id = series_statement.record AND series_statement.tag = '490' AND series_statement.subfield = 'a')
939         LEFT JOIN metabib.full_rec summary ON (r.id = summary.record AND summary.tag = '520' AND summary.subfield = 'a')
940   GROUP BY 1,2,3,4,5,6,7,8,9,10,11,12,13,14;
941
942 CREATE OR REPLACE VIEW reporter.old_super_simple_record AS
943 SELECT  r.id,
944     r.fingerprint,
945     r.quality,
946     r.tcn_source,
947     r.tcn_value,
948     FIRST(title.value) AS title,
949     FIRST(author.value) AS author,
950     ARRAY_TO_STRING(ARRAY_ACCUM( DISTINCT publisher.value), ', ') AS publisher,
951     ARRAY_TO_STRING(ARRAY_ACCUM( DISTINCT SUBSTRING(pubdate.value FROM $$\d+$$) ), ', ') AS pubdate,
952     ARRAY_ACCUM( DISTINCT REPLACE(SUBSTRING(isbn.value FROM $$^\S+$$), '-', '') ) AS isbn,
953     ARRAY_ACCUM( DISTINCT REGEXP_REPLACE(issn.value, E'^\\S*(\\d{4})[-\\s](\\d{3,4}x?)', E'\\1 \\2') ) AS issn
954   FROM  biblio.record_entry r
955     LEFT JOIN metabib.full_rec title ON (r.id = title.record AND title.tag = '245' AND title.subfield = 'a')
956     LEFT JOIN metabib.full_rec author ON (r.id = author.record AND author.tag IN ('100','110','111') AND author.subfield = 'a')
957     LEFT JOIN metabib.full_rec publisher ON (r.id = publisher.record AND publisher.tag = '260' AND publisher.subfield = 'b')
958     LEFT JOIN metabib.full_rec pubdate ON (r.id = pubdate.record AND pubdate.tag = '260' AND pubdate.subfield = 'c')
959     LEFT JOIN metabib.full_rec isbn ON (r.id = isbn.record AND isbn.tag IN ('024', '020') AND isbn.subfield IN ('a','z'))
960     LEFT JOIN metabib.full_rec issn ON (r.id = issn.record AND issn.tag = '022' AND issn.subfield = 'a')
961   GROUP BY 1,2,3,4,5;
962
963 -- 0486
964 ALTER TABLE money.credit_card_payment ADD COLUMN cc_order_number TEXT;
965
966 -- Changing return types requires explicit dropping of old versions
967 DROP FUNCTION action.find_circ_matrix_matchpoint( context_ou INT, match_item BIGINT, match_user INT, renewal BOOL );
968 DROP FUNCTION action.item_user_circ_test( circ_ou INT, match_item BIGINT, match_user INT, renewal BOOL );
969 DROP FUNCTION action.item_user_circ_test( INT, BIGINT, INT );
970 DROP FUNCTION action.item_user_renew_test( INT, BIGINT, INT );
971
972 -- New return types
973 CREATE TYPE action.found_circ_matrix_matchpoint AS ( success BOOL, matchpoint config.circ_matrix_matchpoint, buildrows INT[] );
974
975 -- Helper function - For manual calling, it can be easier to pass in IDs instead of objects
976 CREATE OR REPLACE FUNCTION action.find_circ_matrix_matchpoint( context_ou INT, match_item BIGINT, match_user INT, renewal BOOL ) RETURNS SETOF action.found_circ_matrix_matchpoint AS $func$
977 DECLARE
978     item_object asset.copy%ROWTYPE;
979     user_object actor.usr%ROWTYPE;
980 BEGIN
981     SELECT INTO item_object * FROM asset.copy   WHERE id = match_item;
982     SELECT INTO user_object * FROM actor.usr    WHERE id = match_user;
983
984     RETURN QUERY SELECT * FROM action.find_circ_matrix_matchpoint( context_ou, item_object, user_object, renewal );
985 END;
986 $func$ LANGUAGE plpgsql;
987
988 CREATE TYPE action.circ_matrix_test_result AS ( success BOOL, fail_part TEXT, buildrows INT[], matchpoint INT, circulate BOOL, duration_rule INT, recurring_fine_rule INT, max_fine_rule INT, hard_due_date INT, renewals INT, grace_period INTERVAL );
989
990 CREATE OR REPLACE FUNCTION action.item_user_circ_test( circ_ou INT, match_item BIGINT, match_user INT, renewal BOOL ) RETURNS SETOF action.circ_matrix_test_result AS $func$
991 DECLARE
992     user_object             actor.usr%ROWTYPE;
993     standing_penalty        config.standing_penalty%ROWTYPE;
994     item_object             asset.copy%ROWTYPE;
995     item_status_object      config.copy_status%ROWTYPE;
996     item_location_object    asset.copy_location%ROWTYPE;
997     result                  action.circ_matrix_test_result;
998     circ_test               action.found_circ_matrix_matchpoint;
999     circ_matchpoint         config.circ_matrix_matchpoint%ROWTYPE;
1000     out_by_circ_mod         config.circ_matrix_circ_mod_test%ROWTYPE;
1001     circ_mod_map            config.circ_matrix_circ_mod_test_map%ROWTYPE;
1002     hold_ratio              action.hold_stats%ROWTYPE;
1003     penalty_type            TEXT;
1004     items_out               INT;
1005     context_org_list        INT[];
1006     done                    BOOL := FALSE;
1007 BEGIN
1008     -- Assume success unless we hit a failure condition
1009     result.success := TRUE;
1010
1011     -- Fail if the user is BARRED
1012     SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
1013
1014     -- Fail if we couldn't find the user 
1015     IF user_object.id IS NULL THEN
1016         result.fail_part := 'no_user';
1017         result.success := FALSE;
1018         done := TRUE;
1019         RETURN NEXT result;
1020         RETURN;
1021     END IF;
1022
1023     SELECT INTO item_object * FROM asset.copy WHERE id = match_item;
1024
1025     -- Fail if we couldn't find the item 
1026     IF item_object.id IS NULL THEN
1027         result.fail_part := 'no_item';
1028         result.success := FALSE;
1029         done := TRUE;
1030         RETURN NEXT result;
1031         RETURN;
1032     END IF;
1033
1034     IF user_object.barred IS TRUE THEN
1035         result.fail_part := 'actor.usr.barred';
1036         result.success := FALSE;
1037         done := TRUE;
1038         RETURN NEXT result;
1039     END IF;
1040
1041     -- Fail if the item can't circulate
1042     IF item_object.circulate IS FALSE THEN
1043         result.fail_part := 'asset.copy.circulate';
1044         result.success := FALSE;
1045         done := TRUE;
1046         RETURN NEXT result;
1047     END IF;
1048
1049     -- Fail if the item isn't in a circulateable status on a non-renewal
1050     IF NOT renewal AND item_object.status NOT IN ( 0, 7, 8 ) THEN 
1051         result.fail_part := 'asset.copy.status';
1052         result.success := FALSE;
1053         done := TRUE;
1054         RETURN NEXT result;
1055     ELSIF renewal AND item_object.status <> 1 THEN
1056         result.fail_part := 'asset.copy.status';
1057         result.success := FALSE;
1058         done := TRUE;
1059         RETURN NEXT result;
1060     END IF;
1061
1062     -- Fail if the item can't circulate because of the shelving location
1063     SELECT INTO item_location_object * FROM asset.copy_location WHERE id = item_object.location;
1064     IF item_location_object.circulate IS FALSE THEN
1065         result.fail_part := 'asset.copy_location.circulate';
1066         result.success := FALSE;
1067         done := TRUE;
1068         RETURN NEXT result;
1069     END IF;
1070
1071     SELECT INTO circ_test * FROM action.find_circ_matrix_matchpoint(circ_ou, item_object, user_object, renewal);
1072
1073     circ_matchpoint             := circ_test.matchpoint;
1074     result.matchpoint           := circ_matchpoint.id;
1075     result.circulate            := circ_matchpoint.circulate;
1076     result.duration_rule        := circ_matchpoint.duration_rule;
1077     result.recurring_fine_rule  := circ_matchpoint.recurring_fine_rule;
1078     result.max_fine_rule        := circ_matchpoint.max_fine_rule;
1079     result.hard_due_date        := circ_matchpoint.hard_due_date;
1080     result.renewals             := circ_matchpoint.renewals;
1081     result.buildrows            := circ_test.buildrows;
1082
1083     -- Fail if we couldn't find a matchpoint
1084     IF circ_test.success = false THEN
1085         result.fail_part := 'no_matchpoint';
1086         result.success := FALSE;
1087         done := TRUE;
1088         RETURN NEXT result;
1089         RETURN; -- All tests after this point require a matchpoint. No sense in running on an incomplete or missing one.
1090     END IF;
1091
1092     -- Apparently....use the circ matchpoint org unit to determine what org units are valid.
1093     SELECT INTO context_org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( circ_matchpoint.org_unit );
1094
1095     IF renewal THEN
1096         penalty_type = '%RENEW%';
1097     ELSE
1098         penalty_type = '%CIRC%';
1099     END IF;
1100
1101     FOR standing_penalty IN
1102         SELECT  DISTINCT csp.*
1103           FROM  actor.usr_standing_penalty usp
1104                 JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
1105           WHERE usr = match_user
1106                 AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
1107                 AND (usp.stop_date IS NULL or usp.stop_date > NOW())
1108                 AND csp.block_list LIKE penalty_type LOOP
1109
1110         result.fail_part := standing_penalty.name;
1111         result.success := FALSE;
1112         done := TRUE;
1113         RETURN NEXT result;
1114     END LOOP;
1115
1116     -- Fail if the test is set to hard non-circulating
1117     IF circ_matchpoint.circulate IS FALSE THEN
1118         result.fail_part := 'config.circ_matrix_test.circulate';
1119         result.success := FALSE;
1120         done := TRUE;
1121         RETURN NEXT result;
1122     END IF;
1123
1124     -- Fail if the total copy-hold ratio is too low
1125     IF circ_matchpoint.total_copy_hold_ratio IS NOT NULL THEN
1126         SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
1127         IF hold_ratio.total_copy_ratio IS NOT NULL AND hold_ratio.total_copy_ratio < circ_matchpoint.total_copy_hold_ratio THEN
1128             result.fail_part := 'config.circ_matrix_test.total_copy_hold_ratio';
1129             result.success := FALSE;
1130             done := TRUE;
1131             RETURN NEXT result;
1132         END IF;
1133     END IF;
1134
1135     -- Fail if the available copy-hold ratio is too low
1136     IF circ_matchpoint.available_copy_hold_ratio IS NOT NULL THEN
1137         IF hold_ratio.hold_count IS NULL THEN
1138             SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
1139         END IF;
1140         IF hold_ratio.available_copy_ratio IS NOT NULL AND hold_ratio.available_copy_ratio < circ_matchpoint.available_copy_hold_ratio THEN
1141             result.fail_part := 'config.circ_matrix_test.available_copy_hold_ratio';
1142             result.success := FALSE;
1143             done := TRUE;
1144             RETURN NEXT result;
1145         END IF;
1146     END IF;
1147
1148     -- Fail if the user has too many items with specific circ_modifiers checked out
1149     FOR out_by_circ_mod IN SELECT * FROM config.circ_matrix_circ_mod_test WHERE matchpoint = circ_matchpoint.id LOOP
1150         SELECT  INTO items_out COUNT(*)
1151           FROM  action.circulation circ
1152             JOIN asset.copy cp ON (cp.id = circ.target_copy)
1153           WHERE circ.usr = match_user
1154                AND circ.circ_lib IN ( SELECT * FROM unnest(context_org_list) )
1155             AND circ.checkin_time IS NULL
1156             AND (circ.stop_fines IN ('MAXFINES','LONGOVERDUE') OR circ.stop_fines IS NULL)
1157             AND cp.circ_modifier IN (SELECT circ_mod FROM config.circ_matrix_circ_mod_test_map WHERE circ_mod_test = out_by_circ_mod.id);
1158         IF items_out >= out_by_circ_mod.items_out THEN
1159             result.fail_part := 'config.circ_matrix_circ_mod_test';
1160             result.success := FALSE;
1161             done := TRUE;
1162             RETURN NEXT result;
1163         END IF;
1164     END LOOP;
1165
1166     -- If we passed everything, return the successful matchpoint id
1167     IF NOT done THEN
1168         RETURN NEXT result;
1169     END IF;
1170
1171     RETURN;
1172 END;
1173 $func$ LANGUAGE plpgsql;
1174
1175 CREATE OR REPLACE FUNCTION action.item_user_circ_test( INT, BIGINT, INT ) RETURNS SETOF action.circ_matrix_test_result AS $func$
1176     SELECT * FROM action.item_user_circ_test( $1, $2, $3, FALSE );
1177 $func$ LANGUAGE SQL;
1178
1179 CREATE OR REPLACE FUNCTION action.item_user_renew_test( INT, BIGINT, INT ) RETURNS SETOF action.circ_matrix_test_result AS $func$
1180     SELECT * FROM action.item_user_circ_test( $1, $2, $3, TRUE );
1181 $func$ LANGUAGE SQL;
1182
1183 -- 0490
1184 CREATE OR REPLACE FUNCTION asset.staff_ou_record_copy_count (org INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
1185 DECLARE         
1186     ans RECORD; 
1187     trans INT;
1188 BEGIN           
1189     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
1190
1191     FOR ans IN SELECT u.id, t.depth FROM actor.org_unit_ancestors(org) AS u JOIN actor.org_unit_type t ON (u.ou_type = t.id) LOOP
1192         RETURN QUERY
1193         SELECT  ans.depth,
1194                 ans.id,
1195                 COUNT( cp.id ),
1196                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
1197                 COUNT( cp.id ),
1198                 trans
1199           FROM
1200                 actor.org_unit_descendants(ans.id) d
1201                 JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
1202                 JOIN asset.call_number cn ON (cn.record = rid AND cn.id = cp.call_number AND NOT cn.deleted)
1203           GROUP BY 1,2,6;
1204
1205         IF NOT FOUND THEN
1206             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
1207         END IF;
1208
1209     END LOOP;
1210
1211     RETURN;
1212 END;
1213 $f$ LANGUAGE PLPGSQL;
1214
1215 CREATE OR REPLACE FUNCTION asset.staff_lasso_record_copy_count (i_lasso INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
1216 DECLARE
1217     ans RECORD;
1218     trans INT;
1219 BEGIN
1220     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
1221
1222     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
1223         RETURN QUERY
1224         SELECT  -1,
1225                 ans.id,
1226                 COUNT( cp.id ),
1227                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
1228                 COUNT( cp.id ),
1229                 trans
1230           FROM
1231                 actor.org_unit_descendants(ans.id) d
1232                 JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
1233                 JOIN asset.call_number cn ON (cn.record = rid AND cn.id = cp.call_number AND NOT cn.deleted)
1234           GROUP BY 1,2,6;
1235
1236         IF NOT FOUND THEN
1237             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
1238         END IF;
1239
1240     END LOOP;
1241
1242     RETURN;
1243 END;
1244 $f$ LANGUAGE PLPGSQL;
1245
1246 DROP FUNCTION asset.staff_ou_metarecord_copy_count (INT, BIGINT);
1247 CREATE OR REPLACE FUNCTION asset.staff_ou_metarecord_copy_count (org INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
1248 DECLARE         
1249     ans RECORD; 
1250     trans INT;
1251 BEGIN
1252     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
1253
1254     FOR ans IN SELECT u.id, t.depth FROM actor.org_unit_ancestors(org) AS u JOIN actor.org_unit_type t ON (u.ou_type = t.id) LOOP
1255         RETURN QUERY
1256         SELECT  ans.depth,
1257                 ans.id,
1258                 COUNT( cp.id ),
1259                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
1260                 COUNT( cp.id ),
1261                 trans
1262           FROM
1263                 actor.org_unit_descendants(ans.id) d
1264                 JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
1265                 JOIN asset.call_number cn ON (cn.record = rid AND cn.id = cp.call_number AND NOT cn.deleted)
1266                 JOIN metabib.metarecord_source_map m ON (m.source = cn.record)
1267           GROUP BY 1,2,6;
1268
1269         IF NOT FOUND THEN
1270             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
1271         END IF;
1272
1273     END LOOP;
1274
1275     RETURN;
1276 END;
1277 $f$ LANGUAGE PLPGSQL;
1278
1279 CREATE OR REPLACE FUNCTION asset.staff_lasso_metarecord_copy_count (i_lasso INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
1280 DECLARE
1281     ans RECORD;
1282     trans INT;
1283 BEGIN
1284     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
1285
1286     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
1287         RETURN QUERY
1288         SELECT  -1,
1289                 ans.id,
1290                 COUNT( cp.id ),
1291                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
1292                 COUNT( cp.id ),
1293                 trans
1294           FROM
1295                 actor.org_unit_descendants(ans.id) d
1296                 JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
1297                 JOIN asset.call_number cn ON (cn.record = rid AND cn.id = cp.call_number AND NOT cn.deleted)
1298                 JOIN metabib.metarecord_source_map m ON (m.source = cn.record)
1299           GROUP BY 1,2,6;
1300
1301         IF NOT FOUND THEN
1302             RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
1303         END IF;
1304
1305     END LOOP;
1306
1307     RETURN;
1308 END;
1309 $f$ LANGUAGE PLPGSQL;
1310
1311
1312 -- 0493
1313 UPDATE config.org_unit_setting_type
1314     SET description = 'Amount of time before a hold expires at which point the patron should be alerted. Examples: "5 days", "1 hour"'
1315     WHERE label = 'Holds: Expire Alert Interval';
1316
1317 UPDATE config.org_unit_setting_type
1318     SET description = 'When predicting the amount of time a patron will be waiting for a hold to be fulfilled, this is the default estimated length of time to assume an item will be checked out. Examples: "3 weeks", "7 days"'
1319     WHERE label = 'Holds: Default Estimated Wait';
1320
1321 UPDATE config.org_unit_setting_type
1322     SET description = 'When predicting the amount of time a patron will be waiting for a hold to be fulfilled, this is the minimum estimated length of time to assume an item will be checked out. Examples: "1 week", "5 days"'
1323     WHERE label = 'Holds: Minimum Estimated Wait';
1324
1325 UPDATE config.org_unit_setting_type
1326     SET description = 'The purpose is to provide an interval of time after an item goes into the on-holds-shelf status before it appears to patrons that it is actually on the holds shelf.  This gives staff time to process the item before it shows as ready-for-pickup. Examples: "5 days", "1 hour"'
1327     WHERE label = 'Hold Shelf Status Delay';
1328
1329 -- 0494
1330 UPDATE config.metabib_field
1331     SET xpath = $$//mods32:mods/mods32:subject$$
1332     WHERE field_class = 'subject' AND name = 'complete';
1333
1334 UPDATE config.metabib_field
1335     SET xpath = $$//marc:datafield[@tag='099']$$
1336     WHERE field_class = 'identifier' AND name = 'bibcn';
1337
1338 -- 0495
1339 CREATE TABLE config.record_attr_definition (
1340     name        TEXT    PRIMARY KEY,
1341     label       TEXT    NOT NULL, -- I18N
1342     description TEXT,
1343     filter      BOOL    NOT NULL DEFAULT TRUE,  -- becomes QP filter if true
1344     sorter      BOOL    NOT NULL DEFAULT FALSE, -- becomes QP sort() axis if true
1345
1346 -- For pre-extracted fields. Takes the first occurance, uses naive subfield ordering
1347     tag         TEXT, -- LIKE format
1348     sf_list     TEXT, -- pile-o-values, like 'abcd' for a and b and c and d
1349
1350 -- This is used for both tag/sf and xpath entries
1351     joiner      TEXT,
1352
1353 -- For xpath-extracted attrs
1354     xpath       TEXT,
1355     format      TEXT    REFERENCES config.xml_transform (name) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
1356     start_pos   INT,
1357     string_len  INT,
1358
1359 -- For fixed fields
1360     fixed_field TEXT, -- should exist in config.marc21_ff_pos_map.fixed_field
1361
1362 -- For phys-char fields
1363     phys_char_sf    INT REFERENCES config.marc21_physical_characteristic_subfield_map (id)
1364 );
1365
1366 CREATE TABLE config.record_attr_index_norm_map (
1367     id      SERIAL  PRIMARY KEY,
1368     attr    TEXT    NOT NULL REFERENCES config.record_attr_definition (name) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
1369     norm    INT     NOT NULL REFERENCES config.index_normalizer (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
1370     params  TEXT,
1371     pos     INT     NOT NULL DEFAULT 0
1372 );
1373
1374 CREATE TABLE config.coded_value_map (
1375     id          SERIAL  PRIMARY KEY,
1376     ctype       TEXT    NOT NULL REFERENCES config.record_attr_definition (name) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
1377     code        TEXT    NOT NULL,
1378     value       TEXT    NOT NULL,
1379     description TEXT
1380 );
1381
1382 -- record attributes
1383 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('alph','Alph','Alph');
1384 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('audience','Audn','Audn');
1385 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('bib_level','BLvl','BLvl');
1386 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('biog','Biog','Biog');
1387 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('conf','Conf','Conf');
1388 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('control_type','Ctrl','Ctrl');
1389 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('ctry','Ctry','Ctry');
1390 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('date1','Date1','Date1');
1391 INSERT INTO config.record_attr_definition (name,label,fixed_field,sorter,filter) values ('pubdate','Pub Date','Date1',TRUE,FALSE);
1392 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('date2','Date2','Date2');
1393 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('cat_form','Desc','Desc');
1394 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('pub_status','DtSt','DtSt');
1395 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('enc_level','ELvl','ELvl');
1396 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('fest','Fest','Fest');
1397 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('item_form','Form','Form');
1398 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('gpub','GPub','GPub');
1399 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('ills','Ills','Ills');
1400 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('indx','Indx','Indx');
1401 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('item_lang','Lang','Lang');
1402 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('lit_form','LitF','LitF');
1403 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('mrec','MRec','MRec');
1404 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('ff_sl','S/L','S/L');
1405 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('type_mat','TMat','TMat');
1406 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('item_type','Type','Type');
1407 INSERT INTO config.record_attr_definition (name,label,phys_char_sf) values ('vr_format','Videorecording format',72);
1408 INSERT INTO config.record_attr_definition (name,label,sorter,filter,tag) values ('titlesort','Title',TRUE,FALSE,'tnf');
1409 INSERT INTO config.record_attr_definition (name,label,sorter,filter,tag) values ('authorsort','Author',TRUE,FALSE,'1%');
1410
1411 INSERT INTO config.upgrade_log (version) VALUES ('0624'); -- miker/tsbere
1412 -- Cont was typod as Conf. Update the old entries.
1413 UPDATE config.marc21_ff_pos_map SET fixed_field = 'Cont' WHERE fixed_field = 'Conf' AND length > 1;
1414 -- Conf thus didn't exist. Add it.
1415 INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('Conf', '006', 'BKS', 11, 1, ' ');
1416 INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('Conf', '006', 'SER', 11, 1, ' ');
1417 INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('Conf', '008', 'BKS', 29, 1, ' ');
1418 INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('Conf', '008', 'SER', 29, 1, ' ');
1419
1420 INSERT INTO config.coded_value_map (ctype,code,value,description)
1421     SELECT 'item_lang' AS ctype, code, value, NULL FROM config.language_map
1422         UNION
1423     SELECT 'bib_level' AS ctype, code, value, NULL FROM config.bib_level_map
1424         UNION
1425     SELECT 'item_form' AS ctype, code, value, NULL FROM config.item_form_map
1426         UNION
1427     SELECT 'item_type' AS ctype, code, value, NULL FROM config.item_type_map
1428         UNION
1429     SELECT 'lit_form' AS ctype, code, value, description FROM config.lit_form_map
1430         UNION
1431     SELECT 'audience' AS ctype, code, value, description FROM config.audience_map
1432         UNION
1433     SELECT 'vr_format' AS ctype, code, value, NULL FROM config.videorecording_format_map;
1434
1435 ALTER TABLE config.i18n_locale DROP CONSTRAINT i18n_locale_marc_code_fkey;
1436
1437 DROP TABLE config.language_map;
1438 DROP TABLE config.bib_level_map;
1439 DROP TABLE config.item_form_map;
1440 DROP TABLE config.item_type_map;
1441 DROP TABLE config.lit_form_map;
1442 DROP TABLE config.audience_map;
1443 DROP TABLE config.videorecording_format_map;
1444
1445 UPDATE config.i18n_core SET fq_field = 'ccvm.value', identity_value = ccvm.id FROM config.coded_value_map AS ccvm WHERE fq_field = 'clm.value' AND ccvm.ctype = 'item_lang' AND identity_value = ccvm.code;
1446 UPDATE config.i18n_core SET fq_field = 'ccvm.value', identity_value = ccvm.id FROM config.coded_value_map AS ccvm WHERE fq_field = 'cblvl.value' AND ccvm.ctype = 'bib_level' AND identity_value = ccvm.code;
1447 UPDATE config.i18n_core SET fq_field = 'ccvm.value', identity_value = ccvm.id FROM config.coded_value_map AS ccvm WHERE fq_field = 'cifm.value' AND ccvm.ctype = 'item_form' AND identity_value = ccvm.code;
1448 UPDATE config.i18n_core SET fq_field = 'ccvm.value', identity_value = ccvm.id FROM config.coded_value_map AS ccvm WHERE fq_field = 'citm.value' AND ccvm.ctype = 'item_type' AND identity_value = ccvm.code;
1449 UPDATE config.i18n_core SET fq_field = 'ccvm.value', identity_value = ccvm.id FROM config.coded_value_map AS ccvm WHERE fq_field = 'clfm.value' AND ccvm.ctype = 'lit_form' AND identity_value = ccvm.code;
1450 UPDATE config.i18n_core SET fq_field = 'ccvm.value', identity_value = ccvm.id FROM config.coded_value_map AS ccvm WHERE fq_field = 'cam.value' AND ccvm.ctype = 'audience' AND identity_value = ccvm.code;
1451 UPDATE config.i18n_core SET fq_field = 'ccvm.value', identity_value = ccvm.id FROM config.coded_value_map AS ccvm WHERE fq_field = 'cvrfm.value' AND ccvm.ctype = 'vr_format' AND identity_value = ccvm.code;
1452
1453 UPDATE config.i18n_core SET fq_field = 'ccvm.description', identity_value = ccvm.id FROM config.coded_value_map AS ccvm WHERE fq_field = 'clfm.description' AND ccvm.ctype = 'lit_form' AND identity_value = ccvm.code;
1454 UPDATE config.i18n_core SET fq_field = 'ccvm.description', identity_value = ccvm.id FROM config.coded_value_map AS ccvm WHERE fq_field = 'cam.description' AND ccvm.ctype = 'audience' AND identity_value = ccvm.code;
1455
1456 CREATE VIEW config.language_map AS SELECT code, value FROM config.coded_value_map WHERE ctype = 'item_lang';
1457 CREATE VIEW config.bib_level_map AS SELECT code, value FROM config.coded_value_map WHERE ctype = 'bib_level';
1458 CREATE VIEW config.item_form_map AS SELECT code, value FROM config.coded_value_map WHERE ctype = 'item_form';
1459 CREATE VIEW config.item_type_map AS SELECT code, value FROM config.coded_value_map WHERE ctype = 'item_type';
1460 CREATE VIEW config.lit_form_map AS SELECT code, value, description FROM config.coded_value_map WHERE ctype = 'lit_form';
1461 CREATE VIEW config.audience_map AS SELECT code, value, description FROM config.coded_value_map WHERE ctype = 'audience';
1462 CREATE VIEW config.videorecording_format_map AS SELECT code, value FROM config.coded_value_map WHERE ctype = 'vr_format';
1463
1464 CREATE TABLE metabib.record_attr (
1465        id              BIGINT  PRIMARY KEY REFERENCES biblio.record_entry (id) ON DELETE CASCADE,
1466        attrs   HSTORE  NOT NULL DEFAULT ''::HSTORE
1467 );
1468 CREATE INDEX metabib_svf_attrs_idx ON metabib.record_attr USING GIST (attrs);
1469 CREATE INDEX metabib_svf_date1_idx ON metabib.record_attr ( (attrs->'date1') );
1470 CREATE INDEX metabib_svf_dates_idx ON metabib.record_attr ( (attrs->'date1'), (attrs->'date2') );
1471
1472 INSERT INTO metabib.record_attr (id,attrs)
1473     SELECT DISTINCT ON (mrd.record) mrd.record, hstore(mrd) - '{id,record}'::TEXT[] FROM metabib.rec_descriptor mrd;
1474
1475 -- Back-compat view ... we're moving to an HSTORE world
1476 CREATE TYPE metabib.rec_desc_type AS (
1477     item_type       TEXT,
1478     item_form       TEXT,
1479     bib_level       TEXT,
1480     control_type    TEXT,
1481     char_encoding   TEXT,
1482     enc_level       TEXT,
1483     audience        TEXT,
1484     lit_form        TEXT,
1485     type_mat        TEXT,
1486     cat_form        TEXT,
1487     pub_status      TEXT,
1488     item_lang       TEXT,
1489     vr_format       TEXT,
1490     date1           TEXT,
1491     date2           TEXT
1492 );
1493
1494 DROP TABLE metabib.rec_descriptor CASCADE;
1495
1496 CREATE VIEW metabib.rec_descriptor AS
1497     SELECT  id,
1498             id AS record,
1499             (populate_record(NULL::metabib.rec_desc_type, attrs)).*
1500       FROM  metabib.record_attr;
1501
1502 CREATE OR REPLACE FUNCTION vandelay.marc21_record_type( marc TEXT ) RETURNS config.marc21_rec_type_map AS $func$
1503 DECLARE
1504     ldr         TEXT;
1505     tval        TEXT;
1506     tval_rec    RECORD;
1507     bval        TEXT;
1508     bval_rec    RECORD;
1509     retval      config.marc21_rec_type_map%ROWTYPE;
1510 BEGIN
1511     ldr := oils_xpath_string( '//*[local-name()="leader"]', marc );
1512
1513     IF ldr IS NULL OR ldr = '' THEN
1514         SELECT * INTO retval FROM config.marc21_rec_type_map WHERE code = 'BKS';
1515         RETURN retval;
1516     END IF;
1517
1518     SELECT * INTO tval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'Type' LIMIT 1; -- They're all the same
1519     SELECT * INTO bval_rec FROM config.marc21_ff_pos_map WHERE fixed_field = 'BLvl' LIMIT 1; -- They're all the same
1520
1521
1522     tval := SUBSTRING( ldr, tval_rec.start_pos + 1, tval_rec.length );
1523     bval := SUBSTRING( ldr, bval_rec.start_pos + 1, bval_rec.length );
1524
1525     -- RAISE NOTICE 'type %, blvl %, ldr %', tval, bval, ldr;
1526
1527     SELECT * INTO retval FROM config.marc21_rec_type_map WHERE type_val LIKE '%' || tval || '%' AND blvl_val LIKE '%' || bval || '%';
1528
1529
1530     IF retval.code IS NULL THEN
1531         SELECT * INTO retval FROM config.marc21_rec_type_map WHERE code = 'BKS';
1532     END IF;
1533
1534     RETURN retval;
1535 END;
1536 $func$ LANGUAGE PLPGSQL;
1537
1538 CREATE OR REPLACE FUNCTION biblio.marc21_record_type( rid BIGINT ) RETURNS config.marc21_rec_type_map AS $func$
1539     SELECT * FROM vandelay.marc21_record_type( (SELECT marc FROM biblio.record_entry WHERE id = $1) );
1540 $func$ LANGUAGE SQL;
1541
1542 CREATE OR REPLACE FUNCTION vandelay.marc21_extract_fixed_field( marc TEXT, ff TEXT ) RETURNS TEXT AS $func$
1543 DECLARE
1544     rtype       TEXT;
1545     ff_pos      RECORD;
1546     tag_data    RECORD;
1547     val         TEXT;
1548 BEGIN
1549     rtype := (vandelay.marc21_record_type( marc )).code;
1550     FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE fixed_field = ff AND rec_type = rtype ORDER BY tag DESC LOOP
1551         FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(ff_pos.tag) || '"]/text()', marc ) ) x(value) LOOP
1552             val := SUBSTRING( tag_data.value, ff_pos.start_pos + 1, ff_pos.length );
1553             RETURN val;
1554         END LOOP;
1555         val := REPEAT( ff_pos.default_val, ff_pos.length );
1556         RETURN val;
1557     END LOOP;
1558
1559     RETURN NULL;
1560 END;
1561 $func$ LANGUAGE PLPGSQL;
1562
1563 CREATE OR REPLACE FUNCTION biblio.marc21_extract_fixed_field( rid BIGINT, ff TEXT ) RETURNS TEXT AS $func$
1564     SELECT * FROM vandelay.marc21_extract_fixed_field( (SELECT marc FROM biblio.record_entry WHERE id = $1), $2 );
1565 $func$ LANGUAGE SQL;
1566
1567 CREATE TYPE biblio.record_ff_map AS (record BIGINT, ff_name TEXT, ff_value TEXT);
1568 CREATE OR REPLACE FUNCTION vandelay.marc21_extract_all_fixed_fields( marc TEXT ) RETURNS SETOF biblio.record_ff_map AS $func$
1569 DECLARE
1570     tag_data    TEXT;
1571     rtype       TEXT;
1572     ff_pos      RECORD;
1573     output      biblio.record_ff_map%ROWTYPE;
1574 BEGIN
1575     rtype := (vandelay.marc21_record_type( marc )).code;
1576
1577     FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE rec_type = rtype ORDER BY tag DESC LOOP
1578         output.ff_name  := ff_pos.fixed_field;
1579         output.ff_value := NULL;
1580
1581         FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(tag) || '"]/text()', marc ) ) x(value) LOOP
1582             output.ff_value := SUBSTRING( tag_data.value, ff_pos.start_pos + 1, ff_pos.length );
1583             IF output.ff_value IS NULL THEN output.ff_value := REPEAT( ff_pos.default_val, ff_pos.length ); END IF;
1584             RETURN NEXT output;
1585             output.ff_value := NULL;
1586         END LOOP;
1587
1588     END LOOP;
1589
1590     RETURN;
1591 END;
1592 $func$ LANGUAGE PLPGSQL;
1593
1594 CREATE OR REPLACE FUNCTION biblio.marc21_extract_all_fixed_fields( rid BIGINT ) RETURNS SETOF biblio.record_ff_map AS $func$
1595     SELECT $1 AS record, ff_name, ff_value FROM vandelay.marc21_extract_all_fixed_fields( (SELECT marc FROM biblio.record_entry WHERE id = $1) );
1596 $func$ LANGUAGE SQL;
1597
1598 CREATE OR REPLACE FUNCTION vandelay.marc21_physical_characteristics( marc TEXT) RETURNS SETOF biblio.marc21_physical_characteristics AS $func$
1599 DECLARE
1600     rowid   INT := 0;
1601     _007    TEXT;
1602     ptype   config.marc21_physical_characteristic_type_map%ROWTYPE;
1603     psf     config.marc21_physical_characteristic_subfield_map%ROWTYPE;
1604     pval    config.marc21_physical_characteristic_value_map%ROWTYPE;
1605     retval  biblio.marc21_physical_characteristics%ROWTYPE;
1606 BEGIN
1607
1608     _007 := oils_xpath_string( '//*[@tag="007"]', marc );
1609
1610     IF _007 IS NOT NULL AND _007 <> '' THEN
1611         SELECT * INTO ptype FROM config.marc21_physical_characteristic_type_map WHERE ptype_key = SUBSTRING( _007, 1, 1 );
1612
1613         IF ptype.ptype_key IS NOT NULL THEN
1614             FOR psf IN SELECT * FROM config.marc21_physical_characteristic_subfield_map WHERE ptype_key = ptype.ptype_key LOOP
1615                 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 );
1616
1617                 IF pval.id IS NOT NULL THEN
1618                     rowid := rowid + 1;
1619                     retval.id := rowid;
1620                     retval.ptype := ptype.ptype_key;
1621                     retval.subfield := psf.id;
1622                     retval.value := pval.id;
1623                     RETURN NEXT retval;
1624                 END IF;
1625
1626             END LOOP;
1627         END IF;
1628     END IF;
1629
1630     RETURN;
1631 END;
1632 $func$ LANGUAGE PLPGSQL;
1633
1634 CREATE OR REPLACE FUNCTION biblio.marc21_physical_characteristics( rid BIGINT ) RETURNS SETOF biblio.marc21_physical_characteristics AS $func$
1635     SELECT id, $1 AS record, ptype, subfield, value FROM vandelay.marc21_physical_characteristics( (SELECT marc FROM biblio.record_entry WHERE id = $1) );
1636 $func$ LANGUAGE SQL;
1637
1638 CREATE OR REPLACE FUNCTION biblio.indexing_ingest_or_delete () RETURNS TRIGGER AS $func$
1639 DECLARE
1640     transformed_xml TEXT;
1641     prev_xfrm       TEXT;
1642     normalizer      RECORD;
1643     xfrm            config.xml_transform%ROWTYPE;
1644     attr_value      TEXT;
1645     new_attrs       HSTORE := ''::HSTORE;
1646     attr_def        config.record_attr_definition%ROWTYPE;
1647 BEGIN
1648
1649     IF NEW.deleted IS TRUE THEN -- If this bib is deleted
1650         DELETE FROM metabib.metarecord_source_map WHERE source = NEW.id; -- Rid ourselves of the search-estimate-killing linkage
1651         DELETE FROM metabib.record_attr WHERE id = NEW.id; -- Kill the attrs hash, useless on deleted records
1652         DELETE FROM authority.bib_linking WHERE bib = NEW.id; -- Avoid updating fields in bibs that are no longer visible
1653         RETURN NEW; -- and we're done
1654     END IF;
1655
1656     IF TG_OP = 'UPDATE' THEN -- re-ingest?
1657         PERFORM * FROM config.internal_flag WHERE name = 'ingest.reingest.force_on_same_marc' AND enabled;
1658
1659         IF NOT FOUND AND OLD.marc = NEW.marc THEN -- don't do anything if the MARC didn't change
1660             RETURN NEW;
1661         END IF;
1662     END IF;
1663
1664     -- Record authority linking
1665     PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_authority_linking' AND enabled;
1666     IF NOT FOUND THEN
1667         PERFORM biblio.map_authority_linking( NEW.id, NEW.marc );
1668     END IF;
1669
1670     -- Flatten and insert the mfr data
1671     PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_full_rec' AND enabled;
1672     IF NOT FOUND THEN
1673         PERFORM metabib.reingest_metabib_full_rec(NEW.id);
1674
1675         -- Now we pull out attribute data, which is dependent on the mfr for all but XPath-based fields
1676         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_rec_descriptor' AND enabled;
1677         IF NOT FOUND THEN
1678             FOR attr_def IN SELECT * FROM config.record_attr_definition ORDER BY format LOOP
1679
1680                 IF attr_def.tag IS NOT NULL THEN -- tag (and optional subfield list) selection
1681                     SELECT  ARRAY_TO_STRING(ARRAY_ACCUM(value), COALESCE(attr_def.joiner,' ')) INTO attr_value
1682                       FROM  (SELECT * FROM metabib.full_rec ORDER BY tag, subfield) AS x
1683                       WHERE record = NEW.id
1684                             AND tag LIKE attr_def.tag
1685                             AND CASE
1686                                 WHEN attr_def.sf_list IS NOT NULL
1687                                     THEN POSITION(subfield IN attr_def.sf_list) > 0
1688                                 ELSE TRUE
1689                                 END
1690                       GROUP BY tag
1691                       ORDER BY tag
1692                       LIMIT 1;
1693
1694                 ELSIF attr_def.fixed_field IS NOT NULL THEN -- a named fixed field, see config.marc21_ff_pos_map.fixed_field
1695                     attr_value := biblio.marc21_extract_fixed_field(NEW.id, attr_def.fixed_field);
1696
1697                 ELSIF attr_def.xpath IS NOT NULL THEN -- and xpath expression
1698
1699                     SELECT INTO xfrm * FROM config.xml_transform WHERE name = attr_def.format;
1700
1701                     -- See if we can skip the XSLT ... it's expensive
1702                     IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
1703                         -- Can't skip the transform
1704                         IF xfrm.xslt <> '---' THEN
1705                             transformed_xml := oils_xslt_process(NEW.marc,xfrm.xslt);
1706                         ELSE
1707                             transformed_xml := NEW.marc;
1708                         END IF;
1709
1710                         prev_xfrm := xfrm.name;
1711                     END IF;
1712
1713                     IF xfrm.name IS NULL THEN
1714                         -- just grab the marcxml (empty) transform
1715                         SELECT INTO xfrm * FROM config.xml_transform WHERE xslt = '---' LIMIT 1;
1716                         prev_xfrm := xfrm.name;
1717                     END IF;
1718
1719                     attr_value := oils_xpath_string(attr_def.xpath, transformed_xml, COALESCE(attr_def.joiner,' '), ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]);
1720
1721                 ELSIF attr_def.phys_char_sf IS NOT NULL THEN -- a named Physical Characteristic, see config.marc21_physical_characteristic_*_map
1722                     SELECT  value::TEXT INTO attr_value
1723                       FROM  biblio.marc21_physical_characteristics(NEW.id)
1724                       WHERE subfield = attr_def.phys_char_sf
1725                       LIMIT 1; -- Just in case ...
1726
1727                 END IF;
1728
1729                 -- apply index normalizers to attr_value
1730                 FOR normalizer IN
1731                     SELECT  n.func AS func,
1732                             n.param_count AS param_count,
1733                             m.params AS params
1734                       FROM  config.index_normalizer n
1735                             JOIN config.record_attr_index_norm_map m ON (m.norm = n.id)
1736                       WHERE attr = attr_def.name
1737                       ORDER BY m.pos LOOP
1738                         EXECUTE 'SELECT ' || normalizer.func || '(' ||
1739                             quote_literal( attr_value ) ||
1740                             CASE
1741                                 WHEN normalizer.param_count > 0
1742                                     THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
1743                                     ELSE ''
1744                                 END ||
1745                             ')' INTO attr_value;
1746
1747                 END LOOP;
1748
1749                 -- Add the new value to the hstore
1750                 new_attrs := new_attrs || hstore( attr_def.name, attr_value );
1751
1752             END LOOP;
1753
1754             IF TG_OP = 'INSERT' OR OLD.deleted THEN -- initial insert OR revivication
1755                 INSERT INTO metabib.record_attr (id, attrs) VALUES (NEW.id, new_attrs);
1756             ELSE
1757                 UPDATE metabib.record_attr SET attrs = attrs || new_attrs WHERE id = NEW.id;
1758             END IF;
1759
1760         END IF;
1761     END IF;
1762
1763     -- Gather and insert the field entry data
1764     PERFORM metabib.reingest_metabib_field_entries(NEW.id);
1765
1766     -- Located URI magic
1767     IF TG_OP = 'INSERT' THEN
1768         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_located_uri' AND enabled;
1769         IF NOT FOUND THEN
1770             PERFORM biblio.extract_located_uris( NEW.id, NEW.marc, NEW.editor );
1771         END IF;
1772     ELSE
1773         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_located_uri' AND enabled;
1774         IF NOT FOUND THEN
1775             PERFORM biblio.extract_located_uris( NEW.id, NEW.marc, NEW.editor );
1776         END IF;
1777     END IF;
1778
1779     -- (re)map metarecord-bib linking
1780     IF TG_OP = 'INSERT' THEN -- if not deleted and performing an insert, check for the flag
1781         PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_insert' AND enabled;
1782         IF NOT FOUND THEN
1783             PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
1784         END IF;
1785     ELSE -- we're doing an update, and we're not deleted, remap
1786         PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_update' AND enabled;
1787         IF NOT FOUND THEN
1788             PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
1789         END IF;
1790     END IF;
1791
1792     RETURN NEW;
1793 END;
1794 $func$ LANGUAGE PLPGSQL;
1795
1796 DROP FUNCTION metabib.reingest_metabib_rec_descriptor( bib_id BIGINT );
1797
1798 CREATE OR REPLACE FUNCTION public.approximate_date( TEXT, TEXT ) RETURNS TEXT AS $func$
1799         SELECT REGEXP_REPLACE( $1, E'\\D', $2, 'g' );
1800 $func$ LANGUAGE SQL STRICT IMMUTABLE;
1801
1802 CREATE OR REPLACE FUNCTION public.approximate_low_date( TEXT ) RETURNS TEXT AS $func$
1803         SELECT approximate_date( $1, '0');
1804 $func$ LANGUAGE SQL STRICT IMMUTABLE;
1805
1806 CREATE OR REPLACE FUNCTION public.approximate_high_date( TEXT ) RETURNS TEXT AS $func$
1807         SELECT approximate_date( $1, '9');
1808 $func$ LANGUAGE SQL STRICT IMMUTABLE;
1809
1810 CREATE OR REPLACE FUNCTION public.integer_or_null( TEXT ) RETURNS TEXT AS $func$
1811         SELECT CASE WHEN $1 ~ E'^\\d+$' THEN $1 ELSE NULL END
1812 $func$ LANGUAGE SQL STRICT IMMUTABLE;
1813
1814 CREATE OR REPLACE FUNCTION public.content_or_null( TEXT ) RETURNS TEXT AS $func$
1815         SELECT CASE WHEN $1 ~ E'^\\s*$' THEN NULL ELSE $1 END
1816 $func$ LANGUAGE SQL STRICT IMMUTABLE;
1817
1818 CREATE OR REPLACE FUNCTION public.force_to_isbn13( TEXT ) RETURNS TEXT AS $func$
1819     use Business::ISBN;
1820     use strict;
1821     use warnings;
1822
1823     # Find the first ISBN, force it to ISBN13 and return it
1824
1825     my $input = shift;
1826
1827     foreach my $word (split(/\s/, $input)) {
1828         my $isbn = Business::ISBN->new($word);
1829
1830         # First check the checksum; if it is not valid, fix it and add the original
1831         # bad-checksum ISBN to the output
1832         if ($isbn && $isbn->is_valid_checksum() == Business::ISBN::BAD_CHECKSUM) {
1833             $isbn->fix_checksum();
1834         }
1835
1836         # If we now have a valid ISBN, force it to ISBN13 and return it
1837         return $isbn->as_isbn13->isbn if ($isbn && $isbn->is_valid());
1838     }
1839     return undef;
1840 $func$ LANGUAGE PLPERLU;
1841
1842 COMMENT ON FUNCTION public.force_to_isbn13(TEXT) IS $$
1843 /*
1844  * Copyright (C) 2011 Equinox Software
1845  * Mike Rylander <mrylander@gmail.com>
1846  *
1847  * Inspired by translate_isbn1013
1848  *
1849  * The force_to_isbn13 function takes an input ISBN and returns the ISBN13
1850  * version without hypens and with a repaired checksum if the checksum was bad
1851  */
1852 $$;
1853
1854 -- 0496
1855 UPDATE config.metabib_field
1856     SET xpath = $$//marc:datafield[@tag='024' and @ind1='1']/marc:subfield[@code='a' or @code='z']$$
1857     WHERE field_class = 'identifier' AND name = 'upc';
1858
1859 UPDATE config.metabib_field
1860     SET xpath = $$//marc:datafield[@tag='024' and @ind1='2']/marc:subfield[@code='a' or @code='z']$$
1861     WHERE field_class = 'identifier' AND name = 'ismn';
1862
1863 UPDATE config.metabib_field
1864     SET xpath = $$//marc:datafield[@tag='024' and @ind1='3']/marc:subfield[@code='a' or @code='z']$$
1865     WHERE field_class = 'identifier' AND name = 'ean';
1866
1867 UPDATE config.metabib_field
1868     SET xpath = $$//marc:datafield[@tag='024' and @ind1='0']/marc:subfield[@code='a' or @code='z']$$
1869     WHERE field_class = 'identifier' AND name = 'isrc';
1870
1871 UPDATE config.metabib_field
1872     SET xpath = $$//marc:datafield[@tag='024' and @ind1='4']/marc:subfield[@code='a' or @code='z']$$
1873     WHERE field_class = 'identifier' AND name = 'sici';
1874
1875 -- 0497
1876 INSERT into config.org_unit_setting_type
1877 ( name, label, description, datatype ) VALUES
1878
1879 ( 'ui.patron.edit.au.active.show',
1880     oils_i18n_gettext('ui.patron.edit.au.active.show', 'GUI: Show active field on patron registration', 'coust', 'label'),
1881     oils_i18n_gettext('ui.patron.edit.au.active.show', 'The active field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
1882     'bool'),
1883 ( 'ui.patron.edit.au.active.suggest',
1884     oils_i18n_gettext('ui.patron.edit.au.active.suggest', 'GUI: Suggest active field on patron registration', 'coust', 'label'),
1885     oils_i18n_gettext('ui.patron.edit.au.active.suggest', 'The active field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
1886     'bool'),
1887 ( 'ui.patron.edit.au.alert_message.show',
1888     oils_i18n_gettext('ui.patron.edit.au.alert_message.show', 'GUI: Show alert_message field on patron registration', 'coust', 'label'),
1889     oils_i18n_gettext('ui.patron.edit.au.alert_message.show', 'The alert_message field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
1890     'bool'),
1891 ( 'ui.patron.edit.au.alert_message.suggest',
1892     oils_i18n_gettext('ui.patron.edit.au.alert_message.suggest', 'GUI: Suggest alert_message field on patron registration', 'coust', 'label'),
1893     oils_i18n_gettext('ui.patron.edit.au.alert_message.suggest', 'The alert_message field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
1894     'bool'),
1895 ( 'ui.patron.edit.au.alias.show',
1896     oils_i18n_gettext('ui.patron.edit.au.alias.show', 'GUI: Show alias field on patron registration', 'coust', 'label'),
1897     oils_i18n_gettext('ui.patron.edit.au.alias.show', 'The alias field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
1898     'bool'),
1899 ( 'ui.patron.edit.au.alias.suggest',
1900     oils_i18n_gettext('ui.patron.edit.au.alias.suggest', 'GUI: Suggest alias field on patron registration', 'coust', 'label'),
1901     oils_i18n_gettext('ui.patron.edit.au.alias.suggest', 'The alias field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
1902     'bool'),
1903 ( 'ui.patron.edit.au.barred.show',
1904     oils_i18n_gettext('ui.patron.edit.au.barred.show', 'GUI: Show barred field on patron registration', 'coust', 'label'),
1905     oils_i18n_gettext('ui.patron.edit.au.barred.show', 'The barred field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
1906     'bool'),
1907 ( 'ui.patron.edit.au.barred.suggest',
1908     oils_i18n_gettext('ui.patron.edit.au.barred.suggest', 'GUI: Suggest barred field on patron registration', 'coust', 'label'),
1909     oils_i18n_gettext('ui.patron.edit.au.barred.suggest', 'The barred field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
1910     'bool'),
1911 ( 'ui.patron.edit.au.claims_never_checked_out_count.show',
1912     oils_i18n_gettext('ui.patron.edit.au.claims_never_checked_out_count.show', 'GUI: Show claims_never_checked_out_count field on patron registration', 'coust', 'label'),
1913     oils_i18n_gettext('ui.patron.edit.au.claims_never_checked_out_count.show', 'The claims_never_checked_out_count field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
1914     'bool'),
1915 ( 'ui.patron.edit.au.claims_never_checked_out_count.suggest',
1916     oils_i18n_gettext('ui.patron.edit.au.claims_never_checked_out_count.suggest', 'GUI: Suggest claims_never_checked_out_count field on patron registration', 'coust', 'label'),
1917     oils_i18n_gettext('ui.patron.edit.au.claims_never_checked_out_count.suggest', 'The claims_never_checked_out_count field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
1918     'bool'),
1919 ( 'ui.patron.edit.au.claims_returned_count.show',
1920     oils_i18n_gettext('ui.patron.edit.au.claims_returned_count.show', 'GUI: Show claims_returned_count field on patron registration', 'coust', 'label'),
1921     oils_i18n_gettext('ui.patron.edit.au.claims_returned_count.show', 'The claims_returned_count field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
1922     'bool'),
1923 ( 'ui.patron.edit.au.claims_returned_count.suggest',
1924     oils_i18n_gettext('ui.patron.edit.au.claims_returned_count.suggest', 'GUI: Suggest claims_returned_count field on patron registration', 'coust', 'label'),
1925     oils_i18n_gettext('ui.patron.edit.au.claims_returned_count.suggest', 'The claims_returned_count field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
1926     'bool'),
1927 ( 'ui.patron.edit.au.day_phone.example',
1928     oils_i18n_gettext('ui.patron.edit.au.day_phone.example', 'GUI: Example for day_phone field on patron registration', 'coust', 'label'),
1929     oils_i18n_gettext('ui.patron.edit.au.day_phone.example', 'The Example for validation on the day_phone field in patron registration.', 'coust', 'description'),
1930     'string'),
1931 ( 'ui.patron.edit.au.day_phone.regex',
1932     oils_i18n_gettext('ui.patron.edit.au.day_phone.regex', 'GUI: Regex for day_phone field on patron registration', 'coust', 'label'),
1933     oils_i18n_gettext('ui.patron.edit.au.day_phone.regex', 'The Regular Expression for validation on the day_phone field in patron registration.', 'coust', 'description'),
1934     'string'),
1935 ( 'ui.patron.edit.au.day_phone.require',
1936     oils_i18n_gettext('ui.patron.edit.au.day_phone.require', 'GUI: Require day_phone field on patron registration', 'coust', 'label'),
1937     oils_i18n_gettext('ui.patron.edit.au.day_phone.require', 'The day_phone field will be required on the patron registration screen.', 'coust', 'description'),
1938     'bool'),
1939 ( 'ui.patron.edit.au.day_phone.show',
1940     oils_i18n_gettext('ui.patron.edit.au.day_phone.show', 'GUI: Show day_phone field on patron registration', 'coust', 'label'),
1941     oils_i18n_gettext('ui.patron.edit.au.day_phone.show', 'The day_phone field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
1942     'bool'),
1943 ( 'ui.patron.edit.au.day_phone.suggest',
1944     oils_i18n_gettext('ui.patron.edit.au.day_phone.suggest', 'GUI: Suggest day_phone field on patron registration', 'coust', 'label'),
1945     oils_i18n_gettext('ui.patron.edit.au.day_phone.suggest', 'The day_phone field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
1946     'bool'),
1947 ( 'ui.patron.edit.au.dob.calendar',
1948     oils_i18n_gettext('ui.patron.edit.au.dob.calendar', 'GUI: Show calendar widget for dob field on patron registration', 'coust', 'label'),
1949     oils_i18n_gettext('ui.patron.edit.au.dob.calendar', 'If set the calendar widget will appear when editing the dob field on the patron registration form.', 'coust', 'description'),
1950     'bool'),
1951 ( 'ui.patron.edit.au.dob.require',
1952     oils_i18n_gettext('ui.patron.edit.au.dob.require', 'GUI: Require dob field on patron registration', 'coust', 'label'),
1953     oils_i18n_gettext('ui.patron.edit.au.dob.require', 'The dob field will be required on the patron registration screen.', 'coust', 'description'),
1954     'bool'),
1955 ( 'ui.patron.edit.au.dob.show',
1956     oils_i18n_gettext('ui.patron.edit.au.dob.show', 'GUI: Show dob field on patron registration', 'coust', 'label'),
1957     oils_i18n_gettext('ui.patron.edit.au.dob.show', 'The dob field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
1958     'bool'),
1959 ( 'ui.patron.edit.au.dob.suggest',
1960     oils_i18n_gettext('ui.patron.edit.au.dob.suggest', 'GUI: Suggest dob field on patron registration', 'coust', 'label'),
1961     oils_i18n_gettext('ui.patron.edit.au.dob.suggest', 'The dob field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
1962     'bool'),
1963 ( 'ui.patron.edit.au.email.example',
1964     oils_i18n_gettext('ui.patron.edit.au.email.example', 'GUI: Example for email field on patron registration', 'coust', 'label'),
1965     oils_i18n_gettext('ui.patron.edit.au.email.example', 'The Example for validation on the email field in patron registration.', 'coust', 'description'),
1966     'string'),
1967 ( 'ui.patron.edit.au.email.regex',
1968     oils_i18n_gettext('ui.patron.edit.au.email.regex', 'GUI: Regex for email field on patron registration', 'coust', 'label'),
1969     oils_i18n_gettext('ui.patron.edit.au.email.regex', 'The Regular Expression for validation on the email field in patron registration.', 'coust', 'description'),
1970     'string'),
1971 ( 'ui.patron.edit.au.email.require',
1972     oils_i18n_gettext('ui.patron.edit.au.email.require', 'GUI: Require email field on patron registration', 'coust', 'label'),
1973     oils_i18n_gettext('ui.patron.edit.au.email.require', 'The email field will be required on the patron registration screen.', 'coust', 'description'),
1974     'bool'),
1975 ( 'ui.patron.edit.au.email.show',
1976     oils_i18n_gettext('ui.patron.edit.au.email.show', 'GUI: Show email field on patron registration', 'coust', 'label'),
1977     oils_i18n_gettext('ui.patron.edit.au.email.show', 'The email field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
1978     'bool'),
1979 ( 'ui.patron.edit.au.email.suggest',
1980     oils_i18n_gettext('ui.patron.edit.au.email.suggest', 'GUI: Suggest email field on patron registration', 'coust', 'label'),
1981     oils_i18n_gettext('ui.patron.edit.au.email.suggest', 'The email field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
1982     'bool'),
1983 ( 'ui.patron.edit.au.evening_phone.example',
1984     oils_i18n_gettext('ui.patron.edit.au.evening_phone.example', 'GUI: Example for evening_phone field on patron registration', 'coust', 'label'),
1985     oils_i18n_gettext('ui.patron.edit.au.evening_phone.example', 'The Example for validation on the evening_phone field in patron registration.', 'coust', 'description'),
1986     'string'),
1987 ( 'ui.patron.edit.au.evening_phone.regex',
1988     oils_i18n_gettext('ui.patron.edit.au.evening_phone.regex', 'GUI: Regex for evening_phone field on patron registration', 'coust', 'label'),
1989     oils_i18n_gettext('ui.patron.edit.au.evening_phone.regex', 'The Regular Expression for validation on the evening_phone field in patron registration.', 'coust', 'description'),
1990     'string'),
1991 ( 'ui.patron.edit.au.evening_phone.require',
1992     oils_i18n_gettext('ui.patron.edit.au.evening_phone.require', 'GUI: Require evening_phone field on patron registration', 'coust', 'label'),
1993     oils_i18n_gettext('ui.patron.edit.au.evening_phone.require', 'The evening_phone field will be required on the patron registration screen.', 'coust', 'description'),
1994     'bool'),
1995 ( 'ui.patron.edit.au.evening_phone.show',
1996     oils_i18n_gettext('ui.patron.edit.au.evening_phone.show', 'GUI: Show evening_phone field on patron registration', 'coust', 'label'),
1997     oils_i18n_gettext('ui.patron.edit.au.evening_phone.show', 'The evening_phone field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
1998     'bool'),
1999 ( 'ui.patron.edit.au.evening_phone.suggest',
2000     oils_i18n_gettext('ui.patron.edit.au.evening_phone.suggest', 'GUI: Suggest evening_phone field on patron registration', 'coust', 'label'),
2001     oils_i18n_gettext('ui.patron.edit.au.evening_phone.suggest', 'The evening_phone field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
2002     'bool'),
2003 ( 'ui.patron.edit.au.ident_value.show',
2004     oils_i18n_gettext('ui.patron.edit.au.ident_value.show', 'GUI: Show ident_value field on patron registration', 'coust', 'label'),
2005     oils_i18n_gettext('ui.patron.edit.au.ident_value.show', 'The ident_value field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
2006     'bool'),
2007 ( 'ui.patron.edit.au.ident_value.suggest',
2008     oils_i18n_gettext('ui.patron.edit.au.ident_value.suggest', 'GUI: Suggest ident_value field on patron registration', 'coust', 'label'),
2009     oils_i18n_gettext('ui.patron.edit.au.ident_value.suggest', 'The ident_value field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
2010     'bool'),
2011 ( 'ui.patron.edit.au.ident_value2.show',
2012     oils_i18n_gettext('ui.patron.edit.au.ident_value2.show', 'GUI: Show ident_value2 field on patron registration', 'coust', 'label'),
2013     oils_i18n_gettext('ui.patron.edit.au.ident_value2.show', 'The ident_value2 field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
2014     'bool'),
2015 ( 'ui.patron.edit.au.ident_value2.suggest',
2016     oils_i18n_gettext('ui.patron.edit.au.ident_value2.suggest', 'GUI: Suggest ident_value2 field on patron registration', 'coust', 'label'),
2017     oils_i18n_gettext('ui.patron.edit.au.ident_value2.suggest', 'The ident_value2 field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
2018     'bool'),
2019 ( 'ui.patron.edit.au.juvenile.show',
2020     oils_i18n_gettext('ui.patron.edit.au.juvenile.show', 'GUI: Show juvenile field on patron registration', 'coust', 'label'),
2021     oils_i18n_gettext('ui.patron.edit.au.juvenile.show', 'The juvenile field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
2022     'bool'),
2023 ( 'ui.patron.edit.au.juvenile.suggest',
2024     oils_i18n_gettext('ui.patron.edit.au.juvenile.suggest', 'GUI: Suggest juvenile field on patron registration', 'coust', 'label'),
2025     oils_i18n_gettext('ui.patron.edit.au.juvenile.suggest', 'The juvenile field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
2026     'bool'),
2027 ( 'ui.patron.edit.au.master_account.show',
2028     oils_i18n_gettext('ui.patron.edit.au.master_account.show', 'GUI: Show master_account field on patron registration', 'coust', 'label'),
2029     oils_i18n_gettext('ui.patron.edit.au.master_account.show', 'The master_account field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
2030     'bool'),
2031 ( 'ui.patron.edit.au.master_account.suggest',
2032     oils_i18n_gettext('ui.patron.edit.au.master_account.suggest', 'GUI: Suggest master_account field on patron registration', 'coust', 'label'),
2033     oils_i18n_gettext('ui.patron.edit.au.master_account.suggest', 'The master_account field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
2034     'bool'),
2035 ( 'ui.patron.edit.au.other_phone.example',
2036     oils_i18n_gettext('ui.patron.edit.au.other_phone.example', 'GUI: Example for other_phone field on patron registration', 'coust', 'label'),
2037     oils_i18n_gettext('ui.patron.edit.au.other_phone.example', 'The Example for validation on the other_phone field in patron registration.', 'coust', 'description'),
2038     'string'),
2039 ( 'ui.patron.edit.au.other_phone.regex',
2040     oils_i18n_gettext('ui.patron.edit.au.other_phone.regex', 'GUI: Regex for other_phone field on patron registration', 'coust', 'label'),
2041     oils_i18n_gettext('ui.patron.edit.au.other_phone.regex', 'The Regular Expression for validation on the other_phone field in patron registration.', 'coust', 'description'),
2042     'string'),
2043 ( 'ui.patron.edit.au.other_phone.require',
2044     oils_i18n_gettext('ui.patron.edit.au.other_phone.require', 'GUI: Require other_phone field on patron registration', 'coust', 'label'),
2045     oils_i18n_gettext('ui.patron.edit.au.other_phone.require', 'The other_phone field will be required on the patron registration screen.', 'coust', 'description'),
2046     'bool'),
2047 ( 'ui.patron.edit.au.other_phone.show',
2048     oils_i18n_gettext('ui.patron.edit.au.other_phone.show', 'GUI: Show other_phone field on patron registration', 'coust', 'label'),
2049     oils_i18n_gettext('ui.patron.edit.au.other_phone.show', 'The other_phone field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
2050     'bool'),
2051 ( 'ui.patron.edit.au.other_phone.suggest',
2052     oils_i18n_gettext('ui.patron.edit.au.other_phone.suggest', 'GUI: Suggest other_phone field on patron registration', 'coust', 'label'),
2053     oils_i18n_gettext('ui.patron.edit.au.other_phone.suggest', 'The other_phone field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
2054     'bool'),
2055 ( 'ui.patron.edit.au.second_given_name.show',
2056     oils_i18n_gettext('ui.patron.edit.au.second_given_name.show', 'GUI: Show second_given_name field on patron registration', 'coust', 'label'),
2057     oils_i18n_gettext('ui.patron.edit.au.second_given_name.show', 'The second_given_name field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
2058     'bool'),
2059 ( 'ui.patron.edit.au.second_given_name.suggest',
2060     oils_i18n_gettext('ui.patron.edit.au.second_given_name.suggest', 'GUI: Suggest second_given_name field on patron registration', 'coust', 'label'),
2061     oils_i18n_gettext('ui.patron.edit.au.second_given_name.suggest', 'The second_given_name field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
2062     'bool'),
2063 ( 'ui.patron.edit.au.suffix.show',
2064     oils_i18n_gettext('ui.patron.edit.au.suffix.show', 'GUI: Show suffix field on patron registration', 'coust', 'label'),
2065     oils_i18n_gettext('ui.patron.edit.au.suffix.show', 'The suffix field will be shown on the patron registration screen. Showing a field makes it appear with required fields even when not required. If the field is required this setting is ignored.', 'coust', 'description'),
2066     'bool'),
2067 ( 'ui.patron.edit.au.suffix.suggest',
2068     oils_i18n_gettext('ui.patron.edit.au.suffix.suggest', 'GUI: Suggest suffix field on patron registration', 'coust', 'label'),
2069     oils_i18n_gettext('ui.patron.edit.au.suffix.suggest', 'The suffix field will be suggested on the patron registration screen. Suggesting a field makes it appear when suggested fields are shown. If the field is shown or required this setting is ignored.', 'coust', 'description'),
2070     'bool'),
2071 ( 'ui.patron.edit.aua.county.require',
2072     oils_i18n_gettext('ui.patron.edit.aua.county.require', 'GUI: Require county field on patron registration', 'coust', 'label'),
2073     oils_i18n_gettext('ui.patron.edit.aua.county.require', 'The county field will be required on the patron registration screen.', 'coust', 'description'),
2074     'bool'),
2075 ( 'ui.patron.edit.aua.post_code.example',
2076     oils_i18n_gettext('ui.patron.edit.aua.post_code.example', 'GUI: Example for post_code field on patron registration', 'coust', 'label'),
2077     oils_i18n_gettext('ui.patron.edit.aua.post_code.example', 'The Example for validation on the post_code field in patron registration.', 'coust', 'description'),
2078     'string'),
2079 ( 'ui.patron.edit.aua.post_code.regex',
2080     oils_i18n_gettext('ui.patron.edit.aua.post_code.regex', 'GUI: Regex for post_code field on patron registration', 'coust', 'label'),
2081     oils_i18n_gettext('ui.patron.edit.aua.post_code.regex', 'The Regular Expression for validation on the post_code field in patron registration.', 'coust', 'description'),
2082     'string'),
2083 ( 'ui.patron.edit.default_suggested',
2084     oils_i18n_gettext('ui.patron.edit.default_suggested', 'GUI: Default showing suggested patron registration fields', 'coust', 'label'),
2085     oils_i18n_gettext('ui.patron.edit.default_suggested', 'Instead of All fields, show just suggested fields in patron registration by default.', 'coust', 'description'),
2086     'bool'),
2087 ( 'ui.patron.edit.phone.example',
2088     oils_i18n_gettext('ui.patron.edit.phone.example', 'GUI: Example for phone fields on patron registration', 'coust', 'label'),
2089     oils_i18n_gettext('ui.patron.edit.phone.example', 'The Example for validation on phone fields in patron registration. Applies to all phone fields without their own setting.', 'coust', 'description'),
2090     'string'),
2091 ( 'ui.patron.edit.phone.regex',
2092     oils_i18n_gettext('ui.patron.edit.phone.regex', 'GUI: Regex for phone fields on patron registration', 'coust', 'label'),
2093     oils_i18n_gettext('ui.patron.edit.phone.regex', 'The Regular Expression for validation on phone fields in patron registration. Applies to all phone fields without their own setting.', 'coust', 'description'),
2094     'string');
2095
2096 -- update actor.usr_address indexes
2097 DROP INDEX IF EXISTS actor.actor_usr_addr_street1_idx;
2098 DROP INDEX IF EXISTS actor.actor_usr_addr_street2_idx;
2099 DROP INDEX IF EXISTS actor.actor_usr_addr_city_idx;
2100 DROP INDEX IF EXISTS actor.actor_usr_addr_state_idx; 
2101 DROP INDEX IF EXISTS actor.actor_usr_addr_post_code_idx;
2102
2103 CREATE INDEX actor_usr_addr_street1_idx ON actor.usr_address (evergreen.lowercase(street1));
2104 CREATE INDEX actor_usr_addr_street2_idx ON actor.usr_address (evergreen.lowercase(street2));
2105 CREATE INDEX actor_usr_addr_city_idx ON actor.usr_address (evergreen.lowercase(city));
2106 CREATE INDEX actor_usr_addr_state_idx ON actor.usr_address (evergreen.lowercase(state));
2107 CREATE INDEX actor_usr_addr_post_code_idx ON actor.usr_address (evergreen.lowercase(post_code));
2108
2109 -- update actor.usr indexes
2110 DROP INDEX IF EXISTS actor.actor_usr_first_given_name_idx;
2111 DROP INDEX IF EXISTS actor.actor_usr_second_given_name_idx;
2112 DROP INDEX IF EXISTS actor.actor_usr_family_name_idx;
2113 DROP INDEX IF EXISTS actor.actor_usr_email_idx;
2114 DROP INDEX IF EXISTS actor.actor_usr_day_phone_idx;
2115 DROP INDEX IF EXISTS actor.actor_usr_evening_phone_idx;
2116 DROP INDEX IF EXISTS actor.actor_usr_other_phone_idx;
2117 DROP INDEX IF EXISTS actor.actor_usr_ident_value_idx;
2118 DROP INDEX IF EXISTS actor.actor_usr_ident_value2_idx;
2119
2120 CREATE INDEX actor_usr_first_given_name_idx ON actor.usr (evergreen.lowercase(first_given_name));
2121 CREATE INDEX actor_usr_second_given_name_idx ON actor.usr (evergreen.lowercase(second_given_name));
2122 CREATE INDEX actor_usr_family_name_idx ON actor.usr (evergreen.lowercase(family_name));
2123 CREATE INDEX actor_usr_email_idx ON actor.usr (evergreen.lowercase(email));
2124 CREATE INDEX actor_usr_day_phone_idx ON actor.usr (evergreen.lowercase(day_phone));
2125 CREATE INDEX actor_usr_evening_phone_idx ON actor.usr (evergreen.lowercase(evening_phone));
2126 CREATE INDEX actor_usr_other_phone_idx ON actor.usr (evergreen.lowercase(other_phone));
2127 CREATE INDEX actor_usr_ident_value_idx ON actor.usr (evergreen.lowercase(ident_value));
2128 CREATE INDEX actor_usr_ident_value2_idx ON actor.usr (evergreen.lowercase(ident_value2));
2129
2130 -- update actor.card indexes
2131 DROP INDEX IF EXISTS actor.actor_card_barcode_evergreen_lowercase_idx;
2132 CREATE INDEX actor_card_barcode_evergreen_lowercase_idx ON actor.card (evergreen.lowercase(barcode));
2133
2134 CREATE OR REPLACE FUNCTION vandelay.match_bib_record ( ) RETURNS TRIGGER AS $func$
2135 DECLARE
2136     attr        RECORD;
2137     attr_def    RECORD;
2138     eg_rec      RECORD;
2139     id_value    TEXT;
2140     exact_id    BIGINT;
2141 BEGIN
2142
2143     DELETE FROM vandelay.bib_match WHERE queued_record = NEW.id;
2144
2145     SELECT * INTO attr_def FROM vandelay.bib_attr_definition WHERE xpath = '//*[@tag="901"]/*[@code="c"]' ORDER BY id LIMIT 1;
2146
2147     IF attr_def IS NOT NULL AND attr_def.id IS NOT NULL THEN
2148         id_value := extract_marc_field('vandelay.queued_bib_record', NEW.id, attr_def.xpath, attr_def.remove);
2149     
2150         IF id_value IS NOT NULL AND id_value <> '' AND id_value ~ $r$^\d+$$r$ THEN
2151             SELECT id INTO exact_id FROM biblio.record_entry WHERE id = id_value::BIGINT AND NOT deleted;
2152             SELECT * INTO attr FROM vandelay.queued_bib_record_attr WHERE record = NEW.id and field = attr_def.id LIMIT 1;
2153             IF exact_id IS NOT NULL THEN
2154                 INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('id', attr.id, NEW.id, exact_id);
2155             END IF;
2156         END IF;
2157     END IF;
2158
2159     IF exact_id IS NULL THEN
2160         FOR attr IN SELECT a.* FROM vandelay.queued_bib_record_attr a JOIN vandelay.bib_attr_definition d ON (d.id = a.field) WHERE record = NEW.id AND d.ident IS TRUE LOOP
2161     
2162                 -- All numbers? check for an id match
2163                 IF (attr.attr_value ~ $r$^\d+$$r$) THEN
2164                 FOR eg_rec IN SELECT * FROM biblio.record_entry WHERE id = attr.attr_value::BIGINT AND deleted IS FALSE LOOP
2165                         INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('id', attr.id, NEW.id, eg_rec.id);
2166                         END LOOP;
2167                 END IF;
2168     
2169                 -- Looks like an ISBN? check for an isbn match
2170                 IF (attr.attr_value ~* $r$^[0-9x]+$$r$ AND character_length(attr.attr_value) IN (10,13)) THEN
2171                 FOR eg_rec IN EXECUTE $$SELECT * FROM metabib.full_rec fr WHERE fr.value LIKE evergreen.lowercase('$$ || attr.attr_value || $$%') AND fr.tag = '020' AND fr.subfield = 'a'$$ LOOP
2172                                 PERFORM id FROM biblio.record_entry WHERE id = eg_rec.record AND deleted IS FALSE;
2173                                 IF FOUND THEN
2174                                 INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('isbn', attr.id, NEW.id, eg_rec.record);
2175                                 END IF;
2176                         END LOOP;
2177     
2178                         -- subcheck for isbn-as-tcn
2179                     FOR eg_rec IN SELECT * FROM biblio.record_entry WHERE tcn_value = 'i' || attr.attr_value AND deleted IS FALSE LOOP
2180                             INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('tcn_value', attr.id, NEW.id, eg_rec.id);
2181                 END LOOP;
2182                 END IF;
2183     
2184                 -- check for an OCLC tcn_value match
2185                 IF (attr.attr_value ~ $r$^o\d+$$r$) THEN
2186                     FOR eg_rec IN SELECT * FROM biblio.record_entry WHERE tcn_value = regexp_replace(attr.attr_value,'^o','ocm') AND deleted IS FALSE LOOP
2187                             INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('tcn_value', attr.id, NEW.id, eg_rec.id);
2188                 END LOOP;
2189                 END IF;
2190     
2191                 -- check for a direct tcn_value match
2192             FOR eg_rec IN SELECT * FROM biblio.record_entry WHERE tcn_value = attr.attr_value AND deleted IS FALSE LOOP
2193                 INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('tcn_value', attr.id, NEW.id, eg_rec.id);
2194             END LOOP;
2195     
2196                 -- check for a direct item barcode match
2197             FOR eg_rec IN
2198                     SELECT  DISTINCT b.*
2199                       FROM  biblio.record_entry b
2200                             JOIN asset.call_number cn ON (cn.record = b.id)
2201                             JOIN asset.copy cp ON (cp.call_number = cn.id)
2202                       WHERE cp.barcode = attr.attr_value AND cp.deleted IS FALSE
2203             LOOP
2204                 INSERT INTO vandelay.bib_match (field_type, matched_attr, queued_record, eg_record) VALUES ('id', attr.id, NEW.id, eg_rec.id);
2205             END LOOP;
2206     
2207         END LOOP;
2208     END IF;
2209
2210     RETURN NULL;
2211 END;
2212 $func$ LANGUAGE PLPGSQL;
2213
2214
2215 -- 0499
2216 CREATE OR REPLACE FUNCTION asset.label_normalizer_generic(TEXT) RETURNS TEXT AS $func$
2217     # Created after looking at the Koha C4::ClassSortRoutine::Generic module,
2218     # thus could probably be considered a derived work, although nothing was
2219     # directly copied - but to err on the safe side of providing attribution:
2220     # Copyright (C) 2007 LibLime
2221     # Copyright (C) 2011 Equinox Software, Inc (Steve Callendar)
2222     # Licensed under the GPL v2 or later
2223
2224     use strict;
2225     use warnings;
2226
2227     # Converts the callnumber to uppercase
2228     # Strips spaces from start and end of the call number
2229     # Converts anything other than letters, digits, and periods into spaces
2230     # Collapses multiple spaces into a single underscore
2231     my $callnum = uc(shift);
2232     $callnum =~ s/^\s//g;
2233     $callnum =~ s/\s$//g;
2234     # NOTE: this previously used underscores, but this caused sorting issues
2235     # for the "before" half of page 0 on CN browse, sorting CNs containing a
2236     # decimal before "whole number" CNs
2237     $callnum =~ s/[^A-Z0-9_.]/ /g;
2238     $callnum =~ s/ {2,}/ /g;
2239
2240     return $callnum;
2241 $func$ LANGUAGE PLPERLU;
2242
2243
2244
2245 -- 0501
2246 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('language','Language (2.0 compat version)','Lang');
2247 UPDATE metabib.record_attr SET attrs = attrs || hstore('language',(attrs->'item_lang'));
2248
2249 -- 0502
2250 -- Dewey fields
2251 UPDATE asset.call_number_class
2252     SET field = '080ab,082ab,092abef'
2253     WHERE id = 2
2254 ;
2255
2256 -- LC fields
2257 UPDATE asset.call_number_class
2258     SET field = '050ab,055ab,090abef'
2259     WHERE id = 3
2260 ;
2261
2262 -- FAIR WARNING:
2263 -- Using a tool such as pgadmin to run this script may fail
2264 -- If it does, try psql command line.
2265
2266 -- Change this to FALSE to disable updating existing circs
2267 -- Otherwise will use the fine interval for the grace period
2268 \set CircGrace TRUE
2269
2270 -- 0503
2271 -- New Columns
2272
2273 ALTER TABLE config.rule_recurring_fine
2274     ADD COLUMN grace_period INTERVAL NOT NULL DEFAULT '1 day';
2275
2276 ALTER TABLE action.circulation
2277     ADD COLUMN grace_period INTERVAL NOT NULL DEFAULT '0 seconds';
2278
2279 ALTER TABLE action.aged_circulation
2280     ADD COLUMN grace_period INTERVAL NOT NULL DEFAULT '0 seconds';
2281
2282 -- Remove defaults needed to stop null complaints
2283
2284 ALTER TABLE action.circulation
2285     ALTER COLUMN grace_period DROP DEFAULT;
2286
2287 ALTER TABLE action.aged_circulation
2288     ALTER COLUMN grace_period DROP DEFAULT;
2289
2290 -- Drop Views
2291
2292 DROP VIEW action.all_circulation;
2293 DROP VIEW action.open_circulation;
2294 DROP VIEW action.billable_circulations;
2295
2296 -- Replace Views
2297
2298 CREATE OR REPLACE VIEW action.all_circulation AS
2299     SELECT  id,usr_post_code, usr_home_ou, usr_profile, usr_birth_year, copy_call_number, copy_location,
2300         copy_owning_lib, copy_circ_lib, copy_bib_record, xact_start, xact_finish, target_copy,
2301         circ_lib, circ_staff, checkin_staff, checkin_lib, renewal_remaining, grace_period, due_date,
2302         stop_fines_time, checkin_time, create_time, duration, fine_interval, recurring_fine,
2303         max_fine, phone_renewal, desk_renewal, opac_renewal, duration_rule, recurring_fine_rule,
2304         max_fine_rule, stop_fines, workstation, checkin_workstation, checkin_scan_time, parent_circ
2305       FROM  action.aged_circulation
2306             UNION ALL
2307     SELECT  DISTINCT circ.id,COALESCE(a.post_code,b.post_code) AS usr_post_code, p.home_ou AS usr_home_ou, p.profile AS usr_profile, EXTRACT(YEAR FROM p.dob)::INT AS usr_birth_year,
2308         cp.call_number AS copy_call_number, cp.location AS copy_location, cn.owning_lib AS copy_owning_lib, cp.circ_lib AS copy_circ_lib,
2309         cn.record AS copy_bib_record, circ.xact_start, circ.xact_finish, circ.target_copy, circ.circ_lib, circ.circ_staff, circ.checkin_staff,
2310         circ.checkin_lib, circ.renewal_remaining, circ.grace_period, circ.due_date, circ.stop_fines_time, circ.checkin_time, circ.create_time, circ.duration,
2311         circ.fine_interval, circ.recurring_fine, circ.max_fine, circ.phone_renewal, circ.desk_renewal, circ.opac_renewal, circ.duration_rule,
2312         circ.recurring_fine_rule, circ.max_fine_rule, circ.stop_fines, circ.workstation, circ.checkin_workstation, circ.checkin_scan_time,
2313         circ.parent_circ
2314       FROM  action.circulation circ
2315         JOIN asset.copy cp ON (circ.target_copy = cp.id)
2316         JOIN asset.call_number cn ON (cp.call_number = cn.id)
2317         JOIN actor.usr p ON (circ.usr = p.id)
2318         LEFT JOIN actor.usr_address a ON (p.mailing_address = a.id)
2319         LEFT JOIN actor.usr_address b ON (p.billing_address = a.id);
2320
2321 CREATE OR REPLACE VIEW action.open_circulation AS
2322         SELECT  *
2323           FROM  action.circulation
2324           WHERE checkin_time IS NULL
2325           ORDER BY due_date;
2326                 
2327
2328 CREATE OR REPLACE VIEW action.billable_circulations AS
2329         SELECT  *
2330           FROM  action.circulation
2331           WHERE xact_finish IS NULL;
2332
2333 -- Drop Functions that rely on types
2334
2335 DROP FUNCTION action.item_user_circ_test(INT, BIGINT, INT, BOOL);
2336 DROP FUNCTION action.item_user_circ_test(INT, BIGINT, INT);
2337 DROP FUNCTION action.item_user_renew_test(INT, BIGINT, INT);
2338
2339 CREATE OR REPLACE FUNCTION action.item_user_circ_test( circ_ou INT, match_item BIGINT, match_user INT, renewal BOOL ) RETURNS SETOF action.circ_matrix_test_result AS $func$
2340 DECLARE
2341     user_object             actor.usr%ROWTYPE;
2342     standing_penalty        config.standing_penalty%ROWTYPE;
2343     item_object             asset.copy%ROWTYPE;
2344     item_status_object      config.copy_status%ROWTYPE;
2345     item_location_object    asset.copy_location%ROWTYPE;
2346     result                  action.circ_matrix_test_result;
2347     circ_test               action.found_circ_matrix_matchpoint;
2348     circ_matchpoint         config.circ_matrix_matchpoint%ROWTYPE;
2349     out_by_circ_mod         config.circ_matrix_circ_mod_test%ROWTYPE;
2350     circ_mod_map            config.circ_matrix_circ_mod_test_map%ROWTYPE;
2351     hold_ratio              action.hold_stats%ROWTYPE;
2352     penalty_type            TEXT;
2353     items_out               INT;
2354     context_org_list        INT[];
2355     done                    BOOL := FALSE;
2356 BEGIN
2357     -- Assume success unless we hit a failure condition
2358     result.success := TRUE;
2359
2360     -- Fail if the user is BARRED
2361     SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
2362
2363     -- Fail if we couldn't find the user 
2364     IF user_object.id IS NULL THEN
2365         result.fail_part := 'no_user';
2366         result.success := FALSE;
2367         done := TRUE;
2368         RETURN NEXT result;
2369         RETURN;
2370     END IF;
2371
2372     SELECT INTO item_object * FROM asset.copy WHERE id = match_item;
2373
2374     -- Fail if we couldn't find the item 
2375     IF item_object.id IS NULL THEN
2376         result.fail_part := 'no_item';
2377         result.success := FALSE;
2378         done := TRUE;
2379         RETURN NEXT result;
2380         RETURN;
2381     END IF;
2382
2383     IF user_object.barred IS TRUE THEN
2384         result.fail_part := 'actor.usr.barred';
2385         result.success := FALSE;
2386         done := TRUE;
2387         RETURN NEXT result;
2388     END IF;
2389
2390     -- Fail if the item can't circulate
2391     IF item_object.circulate IS FALSE THEN
2392         result.fail_part := 'asset.copy.circulate';
2393         result.success := FALSE;
2394         done := TRUE;
2395         RETURN NEXT result;
2396     END IF;
2397
2398     -- Fail if the item isn't in a circulateable status on a non-renewal
2399     IF NOT renewal AND item_object.status NOT IN ( 0, 7, 8 ) THEN 
2400         result.fail_part := 'asset.copy.status';
2401         result.success := FALSE;
2402         done := TRUE;
2403         RETURN NEXT result;
2404     ELSIF renewal AND item_object.status <> 1 THEN
2405         result.fail_part := 'asset.copy.status';
2406         result.success := FALSE;
2407         done := TRUE;
2408         RETURN NEXT result;
2409     END IF;
2410
2411     -- Fail if the item can't circulate because of the shelving location
2412     SELECT INTO item_location_object * FROM asset.copy_location WHERE id = item_object.location;
2413     IF item_location_object.circulate IS FALSE THEN
2414         result.fail_part := 'asset.copy_location.circulate';
2415         result.success := FALSE;
2416         done := TRUE;
2417         RETURN NEXT result;
2418     END IF;
2419
2420     SELECT INTO circ_test * FROM action.find_circ_matrix_matchpoint(circ_ou, item_object, user_object, renewal);
2421
2422     circ_matchpoint             := circ_test.matchpoint;
2423     result.matchpoint           := circ_matchpoint.id;
2424     result.circulate            := circ_matchpoint.circulate;
2425     result.duration_rule        := circ_matchpoint.duration_rule;
2426     result.recurring_fine_rule  := circ_matchpoint.recurring_fine_rule;
2427     result.max_fine_rule        := circ_matchpoint.max_fine_rule;
2428     result.hard_due_date        := circ_matchpoint.hard_due_date;
2429     result.renewals             := circ_matchpoint.renewals;
2430     result.grace_period         := circ_matchpoint.grace_period;
2431     result.buildrows            := circ_test.buildrows;
2432
2433     -- Fail if we couldn't find a matchpoint
2434     IF circ_test.success = false THEN
2435         result.fail_part := 'no_matchpoint';
2436         result.success := FALSE;
2437         done := TRUE;
2438         RETURN NEXT result;
2439         RETURN; -- All tests after this point require a matchpoint. No sense in running on an incomplete or missing one.
2440     END IF;
2441
2442     -- Apparently....use the circ matchpoint org unit to determine what org units are valid.
2443     SELECT INTO context_org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( circ_matchpoint.org_unit );
2444
2445     IF renewal THEN
2446         penalty_type = '%RENEW%';
2447     ELSE
2448         penalty_type = '%CIRC%';
2449     END IF;
2450
2451     FOR standing_penalty IN
2452         SELECT  DISTINCT csp.*
2453           FROM  actor.usr_standing_penalty usp
2454                 JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
2455           WHERE usr = match_user
2456                 AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
2457                 AND (usp.stop_date IS NULL or usp.stop_date > NOW())
2458                 AND csp.block_list LIKE penalty_type LOOP
2459
2460         result.fail_part := standing_penalty.name;
2461         result.success := FALSE;
2462         done := TRUE;
2463         RETURN NEXT result;
2464     END LOOP;
2465
2466     -- Fail if the test is set to hard non-circulating
2467     IF circ_matchpoint.circulate IS FALSE THEN
2468         result.fail_part := 'config.circ_matrix_test.circulate';
2469         result.success := FALSE;
2470         done := TRUE;
2471         RETURN NEXT result;
2472     END IF;
2473
2474     -- Fail if the total copy-hold ratio is too low
2475     IF circ_matchpoint.total_copy_hold_ratio IS NOT NULL THEN
2476         SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
2477         IF hold_ratio.total_copy_ratio IS NOT NULL AND hold_ratio.total_copy_ratio < circ_matchpoint.total_copy_hold_ratio THEN
2478             result.fail_part := 'config.circ_matrix_test.total_copy_hold_ratio';
2479             result.success := FALSE;
2480             done := TRUE;
2481             RETURN NEXT result;
2482         END IF;
2483     END IF;
2484
2485     -- Fail if the available copy-hold ratio is too low
2486     IF circ_matchpoint.available_copy_hold_ratio IS NOT NULL THEN
2487         IF hold_ratio.hold_count IS NULL THEN
2488             SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
2489         END IF;
2490         IF hold_ratio.available_copy_ratio IS NOT NULL AND hold_ratio.available_copy_ratio < circ_matchpoint.available_copy_hold_ratio THEN
2491             result.fail_part := 'config.circ_matrix_test.available_copy_hold_ratio';
2492             result.success := FALSE;
2493             done := TRUE;
2494             RETURN NEXT result;
2495         END IF;
2496     END IF;
2497
2498     -- Fail if the user has too many items with specific circ_modifiers checked out
2499     FOR out_by_circ_mod IN SELECT * FROM config.circ_matrix_circ_mod_test WHERE matchpoint = circ_matchpoint.id LOOP
2500         SELECT  INTO items_out COUNT(*)
2501           FROM  action.circulation circ
2502             JOIN asset.copy cp ON (cp.id = circ.target_copy)
2503           WHERE circ.usr = match_user
2504                AND circ.circ_lib IN ( SELECT * FROM unnest(context_org_list) )
2505             AND circ.checkin_time IS NULL
2506             AND (circ.stop_fines IN ('MAXFINES','LONGOVERDUE') OR circ.stop_fines IS NULL)
2507             AND cp.circ_modifier IN (SELECT circ_mod FROM config.circ_matrix_circ_mod_test_map WHERE circ_mod_test = out_by_circ_mod.id);
2508         IF items_out >= out_by_circ_mod.items_out THEN
2509             result.fail_part := 'config.circ_matrix_circ_mod_test';
2510             result.success := FALSE;
2511             done := TRUE;
2512             RETURN NEXT result;
2513         END IF;
2514     END LOOP;
2515
2516     -- If we passed everything, return the successful matchpoint id
2517     IF NOT done THEN
2518         RETURN NEXT result;
2519     END IF;
2520
2521     RETURN;
2522 END;
2523 $func$ LANGUAGE plpgsql;
2524
2525 CREATE OR REPLACE FUNCTION action.item_user_circ_test( INT, BIGINT, INT ) RETURNS SETOF action.circ_matrix_test_result AS $func$
2526     SELECT * FROM action.item_user_circ_test( $1, $2, $3, FALSE );
2527 $func$ LANGUAGE SQL;
2528
2529 CREATE OR REPLACE FUNCTION action.item_user_renew_test( INT, BIGINT, INT ) RETURNS SETOF action.circ_matrix_test_result AS $func$
2530     SELECT * FROM action.item_user_circ_test( $1, $2, $3, TRUE );
2531 $func$ LANGUAGE SQL;
2532
2533 -- Update recurring fine rules
2534 UPDATE config.rule_recurring_fine SET grace_period=recurrence_interval;
2535
2536 -- Update Circulation Data
2537 -- Only update if we were told to and the circ hasn't been checked in
2538 UPDATE action.circulation SET grace_period=fine_interval WHERE :CircGrace AND (checkin_time IS NULL);
2539
2540 -- 0504
2541 CREATE TABLE biblio.monograph_part (
2542     id              SERIAL  PRIMARY KEY,
2543     record          BIGINT  NOT NULL REFERENCES biblio.record_entry (id),
2544     label           TEXT    NOT NULL,
2545     label_sortkey   TEXT    NOT NULL,
2546     CONSTRAINT record_label_unique UNIQUE (record,label)
2547 );
2548
2549 CREATE OR REPLACE FUNCTION biblio.normalize_biblio_monograph_part_sortkey () RETURNS TRIGGER AS $$
2550 BEGIN
2551     NEW.label_sortkey := REGEXP_REPLACE(
2552         evergreen.lpad_number_substrings(
2553             naco_normalize(NEW.label),
2554             '0',
2555             10
2556         ),
2557         E'\\s+',
2558         '',
2559         'g'
2560     );
2561     RETURN NEW;
2562 END;
2563 $$ LANGUAGE PLPGSQL;
2564
2565 CREATE TRIGGER norm_sort_label BEFORE INSERT OR UPDATE ON biblio.monograph_part FOR EACH ROW EXECUTE PROCEDURE biblio.normalize_biblio_monograph_part_sortkey();
2566
2567 CREATE TABLE asset.copy_part_map (
2568     id          SERIAL  PRIMARY KEY,
2569     target_copy BIGINT  NOT NULL, -- points o asset.copy
2570     part        INT     NOT NULL REFERENCES biblio.monograph_part (id) ON DELETE CASCADE
2571 );
2572 CREATE UNIQUE INDEX copy_part_map_cp_part_idx ON asset.copy_part_map (target_copy, part);
2573
2574 CREATE TABLE asset.call_number_prefix (
2575        id                      SERIAL   PRIMARY KEY,
2576        owning_lib          INT                 NOT NULL REFERENCES actor.org_unit (id),
2577        label               TEXT                NOT NULL, -- i18n
2578        label_sortkey   TEXT
2579 );
2580
2581 CREATE OR REPLACE FUNCTION asset.normalize_affix_sortkey () RETURNS TRIGGER AS $$
2582 BEGIN
2583     NEW.label_sortkey := REGEXP_REPLACE(
2584         evergreen.lpad_number_substrings(
2585             naco_normalize(NEW.label),
2586             '0',
2587             10
2588         ),
2589         E'\\s+',
2590         '',
2591         'g'
2592     );
2593     RETURN NEW;
2594 END;
2595 $$ LANGUAGE PLPGSQL;
2596
2597 CREATE TRIGGER prefix_normalize_tgr BEFORE INSERT OR UPDATE ON asset.call_number_prefix FOR EACH ROW EXECUTE PROCEDURE asset.normalize_affix_sortkey();
2598 CREATE UNIQUE INDEX asset_call_number_prefix_once_per_lib ON asset.call_number_prefix (label, owning_lib);
2599 CREATE INDEX asset_call_number_prefix_sortkey_idx ON asset.call_number_prefix (label_sortkey);
2600
2601 CREATE TABLE asset.call_number_suffix (
2602        id                      SERIAL   PRIMARY KEY,
2603        owning_lib          INT                 NOT NULL REFERENCES actor.org_unit (id),
2604        label               TEXT                NOT NULL, -- i18n
2605        label_sortkey   TEXT
2606 );
2607 CREATE TRIGGER suffix_normalize_tgr BEFORE INSERT OR UPDATE ON asset.call_number_suffix FOR EACH ROW EXECUTE PROCEDURE asset.normalize_affix_sortkey();
2608 CREATE UNIQUE INDEX asset_call_number_suffix_once_per_lib ON asset.call_number_suffix (label, owning_lib);
2609 CREATE INDEX asset_call_number_suffix_sortkey_idx ON asset.call_number_suffix (label_sortkey);
2610
2611 INSERT INTO asset.call_number_suffix (id, owning_lib, label) VALUES (-1, 1, '');
2612 INSERT INTO asset.call_number_prefix (id, owning_lib, label) VALUES (-1, 1, '');
2613
2614 DROP INDEX IF EXISTS asset.asset_call_number_label_once_per_lib;
2615
2616 ALTER TABLE asset.call_number
2617     ADD COLUMN prefix INT NOT NULL DEFAULT -1 REFERENCES asset.call_number_prefix(id) DEFERRABLE INITIALLY DEFERRED,
2618     ADD COLUMN suffix INT NOT NULL DEFAULT -1 REFERENCES asset.call_number_suffix(id) DEFERRABLE INITIALLY DEFERRED;
2619
2620 ALTER TABLE auditor.asset_call_number_history
2621     ADD COLUMN prefix INT NOT NULL DEFAULT -1,
2622     ADD COLUMN suffix INT NOT NULL DEFAULT -1;
2623
2624 CREATE UNIQUE INDEX asset_call_number_label_once_per_lib ON asset.call_number (record, owning_lib, label, prefix, suffix) WHERE deleted = FALSE OR deleted IS FALSE;
2625
2626 INSERT INTO config.org_unit_setting_type ( name, label, description, datatype ) VALUES (
2627     'ui.cat.volume_copy_editor.horizontal',
2628     oils_i18n_gettext(
2629         'ui.cat.volume_copy_editor.horizontal',
2630         'GUI: Horizontal layout for Volume/Copy Creator/Editor.',
2631         'coust', 'label'),
2632     oils_i18n_gettext(
2633         'ui.cat.volume_copy_editor.horizontal',
2634         'The main entry point for this interface is in Holdings Maintenance, Actions for Selected Rows, Edit Item Attributes / Call Numbers / Replace Barcodes.  This setting changes the top and bottom panes for that interface into left and right panes.',
2635         'coust', 'description'),
2636     'bool'
2637 );
2638
2639
2640
2641 -- 0506
2642 ALTER FUNCTION actor.org_unit_descendants( INT, INT ) ROWS 1;
2643 ALTER FUNCTION actor.org_unit_descendants( INT ) ROWS 1;
2644 ALTER FUNCTION actor.org_unit_descendants_distance( INT )  ROWS 1;
2645 ALTER FUNCTION actor.org_unit_ancestors( INT )  ROWS 1;
2646 ALTER FUNCTION actor.org_unit_ancestors_distance( INT )  ROWS 1;
2647 ALTER FUNCTION actor.org_unit_full_path ( INT )  ROWS 2;
2648 ALTER FUNCTION actor.org_unit_full_path ( INT, INT ) ROWS 2;
2649 ALTER FUNCTION actor.org_unit_combined_ancestors ( INT, INT ) ROWS 1;
2650 ALTER FUNCTION actor.org_unit_common_ancestors ( INT, INT ) ROWS 1;
2651 ALTER FUNCTION actor.org_unit_ancestor_setting( TEXT, INT ) ROWS 1;
2652 ALTER FUNCTION permission.grp_ancestors ( INT ) ROWS 1;
2653 ALTER FUNCTION permission.grp_ancestors_distance( INT ) ROWS 1;
2654 ALTER FUNCTION permission.grp_descendants_distance( INT ) ROWS 1;
2655 ALTER FUNCTION permission.usr_perms ( INT ) ROWS 10;
2656 ALTER FUNCTION permission.usr_has_perm_at_nd ( INT, TEXT) ROWS 1;
2657 ALTER FUNCTION permission.usr_has_perm_at_all_nd ( INT, TEXT ) ROWS 1;
2658 ALTER FUNCTION permission.usr_has_perm_at ( INT, TEXT ) ROWS 1;
2659 ALTER FUNCTION permission.usr_has_perm_at_all ( INT, TEXT ) ROWS 1;
2660
2661
2662 DROP TRIGGER IF EXISTS facet_force_nfc_tgr ON metabib.facet_entry;
2663 CREATE TRIGGER facet_force_nfc_tgr
2664     BEFORE UPDATE OR INSERT ON metabib.facet_entry
2665     FOR EACH ROW EXECUTE PROCEDURE evergreen.facet_force_nfc();
2666
2667 DROP FUNCTION IF EXISTS public.force_unicode_normal_form (TEXT,TEXT);
2668 DROP FUNCTION IF EXISTS public.facet_force_nfc ();
2669
2670 DROP TRIGGER b_maintain_901 ON biblio.record_entry;
2671 DROP TRIGGER b_maintain_901 ON authority.record_entry;
2672 DROP TRIGGER b_maintain_901 ON serial.record_entry;
2673
2674 CREATE TRIGGER b_maintain_901 BEFORE INSERT OR UPDATE ON biblio.record_entry FOR EACH ROW EXECUTE PROCEDURE evergreen.maintain_901();
2675 CREATE TRIGGER b_maintain_901 BEFORE INSERT OR UPDATE ON authority.record_entry FOR EACH ROW EXECUTE PROCEDURE evergreen.maintain_901();
2676 CREATE TRIGGER b_maintain_901 BEFORE INSERT OR UPDATE ON serial.record_entry FOR EACH ROW EXECUTE PROCEDURE evergreen.maintain_901();
2677
2678 DROP FUNCTION IF EXISTS public.maintain_901 ();
2679
2680 ------ Backporting note: 2.1+ only beyond here --------
2681
2682 CREATE SCHEMA unapi;
2683
2684 CREATE TABLE unapi.bre_output_layout (
2685     name                TEXT    PRIMARY KEY,
2686     transform           TEXT    REFERENCES config.xml_transform (name) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
2687     mime_type           TEXT    NOT NULL,
2688     feed_top            TEXT    NOT NULL,
2689     holdings_element    TEXT,
2690     title_element       TEXT,
2691     description_element TEXT,
2692     creator_element     TEXT,
2693     update_ts_element   TEXT
2694 );
2695
2696 INSERT INTO unapi.bre_output_layout
2697     (name,           transform, mime_type,              holdings_element, feed_top,         title_element, description_element, creator_element, update_ts_element)
2698         VALUES
2699     ('holdings_xml', NULL,      'application/xml',      NULL,             'hxml',           NULL,          NULL,                NULL,            NULL),
2700     ('marcxml',      'marcxml', 'application/marc+xml', 'record',         'collection',     NULL,          NULL,                NULL,            NULL),
2701     ('mods32',       'mods32',  'application/mods+xml', 'mods',           'modsCollection', NULL,          NULL,                NULL,            NULL)
2702 ;
2703
2704 -- Dummy functions, so we can create the real ones out of order
2705 CREATE OR REPLACE FUNCTION unapi.aou    ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2706 CREATE OR REPLACE FUNCTION unapi.acnp   ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2707 CREATE OR REPLACE FUNCTION unapi.acns   ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2708 CREATE OR REPLACE FUNCTION unapi.acn    ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2709 CREATE OR REPLACE FUNCTION unapi.ssub   ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2710 CREATE OR REPLACE FUNCTION unapi.sdist  ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2711 CREATE OR REPLACE FUNCTION unapi.sstr   ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2712 CREATE OR REPLACE FUNCTION unapi.sitem  ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2713 CREATE OR REPLACE FUNCTION unapi.sunit  ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2714 CREATE OR REPLACE FUNCTION unapi.sisum  ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2715 CREATE OR REPLACE FUNCTION unapi.sbsum  ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2716 CREATE OR REPLACE FUNCTION unapi.sssum  ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2717 CREATE OR REPLACE FUNCTION unapi.siss   ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2718 CREATE OR REPLACE FUNCTION unapi.auri   ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2719 CREATE OR REPLACE FUNCTION unapi.acp    ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2720 CREATE OR REPLACE FUNCTION unapi.acpn   ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2721 CREATE OR REPLACE FUNCTION unapi.acl    ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2722 CREATE OR REPLACE FUNCTION unapi.ccs    ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2723 CREATE OR REPLACE FUNCTION unapi.ascecm ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2724 CREATE OR REPLACE FUNCTION unapi.bre    ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2725 CREATE OR REPLACE FUNCTION unapi.bmp    ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2726
2727 CREATE OR REPLACE FUNCTION unapi.holdings_xml ( bid BIGINT, ouid INT, org TEXT, depth INT DEFAULT NULL, includes TEXT[] DEFAULT NULL::TEXT[], slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2728 CREATE OR REPLACE FUNCTION unapi.biblio_record_entry_feed ( id_list BIGINT[], format TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE, title TEXT DEFAULT NULL, description TEXT DEFAULT NULL, creator TEXT DEFAULT NULL, update_ts TEXT DEFAULT NULL, unapi_url TEXT DEFAULT NULL, header_xml XML DEFAULT NULL ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL;
2729
2730 CREATE OR REPLACE FUNCTION unapi.memoize (classname TEXT, obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
2731 DECLARE
2732     key     TEXT;
2733     output  XML;
2734 BEGIN
2735     key :=
2736         'id'        || COALESCE(obj_id::TEXT,'') ||
2737         'format'    || COALESCE(format::TEXT,'') ||
2738         'ename'     || COALESCE(ename::TEXT,'') ||
2739         'includes'  || COALESCE(includes::TEXT,'{}'::TEXT[]::TEXT) ||
2740         'org'       || COALESCE(org::TEXT,'') ||
2741         'depth'     || COALESCE(depth::TEXT,'') ||
2742         'slimit'    || COALESCE(slimit::TEXT,'') ||
2743         'soffset'   || COALESCE(soffset::TEXT,'') ||
2744         'include_xmlns'   || COALESCE(include_xmlns::TEXT,'');
2745     -- RAISE NOTICE 'memoize key: %', key;
2746
2747     key := MD5(key);
2748     -- RAISE NOTICE 'memoize hash: %', key;
2749
2750     -- XXX cache logic ... memcached? table?
2751
2752     EXECUTE $$SELECT unapi.$$ || classname || $$( $1, $2, $3, $4, $5, $6, $7, $8, $9);$$ INTO output USING obj_id, format, ename, includes, org, depth, slimit, soffset, include_xmlns;
2753     RETURN output;
2754 END;
2755 $F$ LANGUAGE PLPGSQL;
2756
2757 CREATE OR REPLACE FUNCTION unapi.biblio_record_entry_feed ( id_list BIGINT[], format TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE, title TEXT DEFAULT NULL, description TEXT DEFAULT NULL, creator TEXT DEFAULT NULL, update_ts TEXT DEFAULT NULL, unapi_url TEXT DEFAULT NULL, header_xml XML DEFAULT NULL ) RETURNS XML AS $F$
2758 DECLARE
2759     layout          unapi.bre_output_layout%ROWTYPE;
2760     transform       config.xml_transform%ROWTYPE;
2761     item_format     TEXT;
2762     tmp_xml         TEXT;
2763     xmlns_uri       TEXT := 'http://open-ils.org/spec/feed-xml/v1';
2764     ouid            INT;
2765     element_list    TEXT[];
2766 BEGIN
2767
2768     SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
2769     SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
2770
2771     IF layout.name IS NULL THEN
2772         RETURN NULL::XML;
2773     END IF;
2774
2775     SELECT * INTO transform FROM config.xml_transform WHERE name = layout.transform;
2776     xmlns_uri := COALESCE(transform.namespace_uri,xmlns_uri);
2777
2778     -- Gather the bib xml
2779     SELECT XMLAGG( unapi.bre(i, format, '', includes, org, depth, slimit, soffset, include_xmlns)) INTO tmp_xml FROM UNNEST( id_list ) i;
2780
2781     IF layout.title_element IS NOT NULL THEN
2782         EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.title_element ||', CASE WHEN $4 THEN XMLATTRIBUTES( $1 AS xmlns) ELSE NULL END, $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, title, include_xmlns;
2783     END IF;
2784
2785     IF layout.description_element IS NOT NULL THEN
2786         EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.description_element ||', CASE WHEN $4 THEN XMLATTRIBUTES( $1 AS xmlns) ELSE NULL END, $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, description, include_xmlns;
2787     END IF;
2788
2789     IF layout.creator_element IS NOT NULL THEN
2790         EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.creator_element ||', CASE WHEN $4 THEN XMLATTRIBUTES( $1 AS xmlns) ELSE NULL END, $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, creator, include_xmlns;
2791     END IF;
2792
2793     IF layout.update_ts_element IS NOT NULL THEN
2794         EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.update_ts_element ||', CASE WHEN $4 THEN XMLATTRIBUTES( $1 AS xmlns) ELSE NULL END, $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, update_ts, include_xmlns;
2795     END IF;
2796
2797     IF unapi_url IS NOT NULL THEN
2798         EXECUTE $$SELECT XMLCONCAT( XMLELEMENT( name link, XMLATTRIBUTES( 'http://www.w3.org/1999/xhtml' AS xmlns, 'unapi-server' AS rel, $1 AS href, 'unapi' AS title)), $2)$$ INTO tmp_xml USING unapi_url, tmp_xml::XML;
2799     END IF;
2800
2801     IF header_xml IS NOT NULL THEN tmp_xml := XMLCONCAT(header_xml,tmp_xml::XML); END IF;
2802
2803     element_list := regexp_split_to_array(layout.feed_top,E'\\.');
2804     FOR i IN REVERSE ARRAY_UPPER(element_list, 1) .. 1 LOOP
2805         EXECUTE 'SELECT XMLELEMENT( name '|| quote_ident(element_list[i]) ||', CASE WHEN $4 THEN XMLATTRIBUTES( $1 AS xmlns) ELSE NULL END, $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, include_xmlns;
2806     END LOOP;
2807
2808     RETURN tmp_xml::XML;
2809 END;
2810 $F$ LANGUAGE PLPGSQL;
2811
2812 CREATE OR REPLACE FUNCTION unapi.bre ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
2813 DECLARE
2814     me      biblio.record_entry%ROWTYPE;
2815     layout  unapi.bre_output_layout%ROWTYPE;
2816     xfrm    config.xml_transform%ROWTYPE;
2817     ouid    INT;
2818     tmp_xml TEXT;
2819     top_el  TEXT;
2820     output  XML;
2821     hxml    XML;
2822 BEGIN
2823
2824     SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
2825
2826     IF ouid IS NULL THEN
2827         RETURN NULL::XML;
2828     END IF;
2829
2830     IF format = 'holdings_xml' THEN -- the special case
2831         output := unapi.holdings_xml( obj_id, ouid, org, depth, includes, slimit, soffset, include_xmlns);
2832         RETURN output;
2833     END IF;
2834
2835     SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
2836
2837     IF layout.name IS NULL THEN
2838         RETURN NULL::XML;
2839     END IF;
2840
2841     SELECT * INTO xfrm FROM config.xml_transform WHERE name = layout.transform;
2842
2843     SELECT * INTO me FROM biblio.record_entry WHERE id = obj_id;
2844
2845     -- grab hodlings if we need them
2846     IF ('holdings_xml' = ANY (includes)) THEN 
2847         hxml := unapi.holdings_xml(obj_id, ouid, org, depth, evergreen.array_remove_item_by_value(includes,'holdings_xml'), slimit, soffset, include_xmlns);
2848     ELSE
2849         hxml := NULL::XML;
2850     END IF;
2851
2852
2853     -- generate our item node
2854
2855
2856     IF format = 'marcxml' THEN
2857         tmp_xml := me.marc;
2858         IF tmp_xml !~ E'<marc:' THEN -- If we're not using the prefixed namespace in this record, then remove all declarations of it
2859            tmp_xml := REGEXP_REPLACE(tmp_xml, ' xmlns:marc="http://www.loc.gov/MARC21/slim"', '', 'g');
2860         END IF; 
2861     ELSE
2862         tmp_xml := oils_xslt_process(me.marc, xfrm.xslt)::XML;
2863     END IF;
2864
2865     top_el := REGEXP_REPLACE(tmp_xml, E'^.*?<((?:\\S+:)?' || layout.holdings_element || ').*$', E'\\1');
2866
2867     IF hxml IS NOT NULL THEN -- XXX how do we configure the holdings position?
2868         tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', hxml || '</' || top_el || E'>\\1');
2869     END IF;
2870
2871     IF ('bre.unapi' = ANY (includes)) THEN 
2872         output := REGEXP_REPLACE(
2873             tmp_xml,
2874             '</' || top_el || '>(.*?)',
2875             XMLELEMENT(
2876                 name abbr,
2877                 XMLATTRIBUTES(
2878                     'http://www.w3.org/1999/xhtml' AS xmlns,
2879                     'unapi-id' AS class,
2880                     'tag:open-ils.org:U2@bre/' || obj_id || '/' || org AS title
2881                 )
2882             )::TEXT || '</' || top_el || E'>\\1'
2883         );
2884     ELSE
2885         output := tmp_xml;
2886     END IF;
2887
2888     RETURN output;
2889 END;
2890 $F$ LANGUAGE PLPGSQL;
2891
2892 CREATE OR REPLACE FUNCTION unapi.holdings_xml (bid BIGINT, ouid INT, org TEXT, depth INT DEFAULT NULL, includes TEXT[] DEFAULT NULL::TEXT[], slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE) RETURNS XML AS $F$
2893      SELECT  XMLELEMENT(
2894                  name holdings,
2895                  XMLATTRIBUTES(
2896                     CASE WHEN $8 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
2897                     CASE WHEN ('bre' = ANY ('{acn,auri}'::TEXT[] || $5)) THEN 'tag:open-ils.org:U2@bre/' || $1 || '/' || $3 ELSE NULL END AS id
2898                  ),
2899                  XMLELEMENT(
2900                      name counts,
2901                      (SELECT  XMLAGG(XMLELEMENT::XML) FROM (
2902                          SELECT  XMLELEMENT(
2903                                      name count,
2904                                      XMLATTRIBUTES('public' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
2905                                  )::text
2906                            FROM  asset.opac_ou_record_copy_count($2,  $1)
2907                                      UNION
2908                          SELECT  XMLELEMENT(
2909                                      name count,
2910                                      XMLATTRIBUTES('staff' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
2911                                  )::text
2912                            FROM  asset.staff_ou_record_copy_count($2, $1)
2913                                      ORDER BY 1
2914                      )x)
2915                  ),
2916                  CASE 
2917                      WHEN ('bmp' = ANY ($5)) THEN
2918                         XMLELEMENT( name monograph_parts,
2919                             XMLAGG((SELECT unapi.bmp( id, 'xml', 'monograph_part', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($5,'bre'), 'holdings_xml'), $3, $4, $6, $7, FALSE) FROM biblio.monograph_part WHERE record = $1))
2920                         )
2921                      ELSE NULL
2922                  END,
2923                  CASE WHEN ('acn' = ANY ('{acn,auri}'::TEXT[] || $5)) THEN 
2924                      XMLELEMENT(
2925                          name volumes,
2926                          (SELECT XMLAGG(acn) FROM (
2927                             SELECT  unapi.acn(acn.id,'xml','volume', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value('{acn,auri}'::TEXT[] || $5,'holdings_xml'),'bre'), $3, $4, $6, $7, FALSE)
2928                               FROM  asset.call_number acn
2929                               WHERE acn.record = $1
2930                                     AND EXISTS (
2931                                         SELECT  1
2932                                           FROM  asset.copy acp
2933                                                 JOIN actor.org_unit_descendants(
2934                                                     $2,
2935                                                     (COALESCE(
2936                                                         $4,
2937                                                         (SELECT aout.depth
2938                                                           FROM  actor.org_unit_type aout
2939                                                                 JOIN actor.org_unit aou ON (aou.ou_type = aout.id AND aou.id = $2)
2940                                                         )
2941                                                     ))
2942                                                 ) aoud ON (acp.circ_lib = aoud.id)
2943                                           LIMIT 1
2944                                     )
2945                               ORDER BY label_sortkey
2946                               LIMIT $6
2947                               OFFSET $7
2948                          )x)
2949                      )
2950                  ELSE NULL END,
2951                  CASE WHEN ('ssub' = ANY ('{acn,auri}'::TEXT[] || $5)) THEN 
2952                      XMLELEMENT(
2953                          name subscriptions,
2954                          (SELECT XMLAGG(ssub) FROM (
2955                             SELECT  unapi.ssub(id,'xml','subscription','{}'::TEXT[], $3, $4, $6, $7, FALSE)
2956                               FROM  serial.subscription
2957                               WHERE record_entry = $1
2958                         )x)
2959                      )
2960                  ELSE NULL END
2961              );
2962 $F$ LANGUAGE SQL;
2963
2964 CREATE OR REPLACE FUNCTION unapi.ssub ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
2965         SELECT  XMLELEMENT(
2966                     name subscription,
2967                     XMLATTRIBUTES(
2968                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
2969                         'tag:open-ils.org:U2@ssub/' || id AS id,
2970                         start_date AS start, end_date AS end, expected_date_offset
2971                     ),
2972                     unapi.aou( owning_lib, $2, 'owning_lib', evergreen.array_remove_item_by_value($4,'ssub'), $5, $6, $7, $8),
2973                     XMLELEMENT( name distributions,
2974                         CASE 
2975                             WHEN ('sdist' = ANY ($4)) THEN
2976                                 XMLAGG((SELECT unapi.sdist( id, 'xml', 'distribution', evergreen.array_remove_item_by_value($4,'ssub'), $5, $6, $7, $8, FALSE) FROM serial.distribution WHERE subscription = ssub.id))
2977                             ELSE NULL
2978                         END
2979                     )
2980                 )
2981           FROM  serial.subscription ssub
2982           WHERE id = $1
2983           GROUP BY id, start_date, end_date, expected_date_offset, owning_lib;
2984 $F$ LANGUAGE SQL;
2985
2986 CREATE OR REPLACE FUNCTION unapi.sdist ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
2987         SELECT  XMLELEMENT(
2988                     name distribution,
2989                     XMLATTRIBUTES(
2990                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
2991                         'tag:open-ils.org:U2@sdist/' || id AS id,
2992                         'tag:open-ils.org:U2@acn/' || receive_call_number AS receive_call_number,
2993                         'tag:open-ils.org:U2@acn/' || bind_call_number AS bind_call_number,
2994                         unit_label_prefix, label, unit_label_suffix, summary_method
2995                     ),
2996                     unapi.aou( holding_lib, $2, 'holding_lib', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8),
2997                     CASE WHEN subscription IS NOT NULL AND ('ssub' = ANY ($4)) THEN unapi.ssub( subscription, 'xml', 'subscription', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE) ELSE NULL END,
2998                     XMLELEMENT( name streams,
2999                         CASE 
3000                             WHEN ('sstr' = ANY ($4)) THEN
3001                                 XMLAGG((SELECT unapi.sstr( id, 'xml', 'stream', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE) FROM serial.stream WHERE distribution = sdist.id))
3002                             ELSE NULL
3003                         END
3004                     ),
3005                     XMLELEMENT( name summaries,
3006                         CASE 
3007                             WHEN ('ssum' = ANY ($4)) THEN
3008                                 XMLAGG((SELECT unapi.sbsum( id, 'xml', 'serial_summary', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE) FROM serial.basic_summary WHERE distribution = sdist.id))
3009                             ELSE NULL
3010                         END,
3011                         CASE 
3012                             WHEN ('ssum' = ANY ($4)) THEN
3013                                 XMLAGG((SELECT unapi.sisum( id, 'xml', 'serial_summary', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE) FROM serial.index_summary WHERE distribution = sdist.id))
3014                             ELSE NULL
3015                         END,
3016                         CASE 
3017                             WHEN ('ssum' = ANY ($4)) THEN
3018                                 XMLAGG((SELECT unapi.sssum( id, 'xml', 'serial_summary', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE) FROM serial.supplement_summary WHERE distribution = sdist.id))
3019                             ELSE NULL
3020                         END
3021                     )
3022                 )
3023           FROM  serial.distribution sdist
3024           WHERE id = $1
3025           GROUP BY id, label, unit_label_prefix, unit_label_suffix, holding_lib, summary_method, subscription, receive_call_number, bind_call_number;
3026 $F$ LANGUAGE SQL;
3027
3028 CREATE OR REPLACE FUNCTION unapi.sstr ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3029     SELECT  XMLELEMENT(
3030                 name stream,
3031                 XMLATTRIBUTES(
3032                     CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3033                     'tag:open-ils.org:U2@sstr/' || id AS id,
3034                     routing_label
3035                 ),
3036                 CASE WHEN distribution IS NOT NULL AND ('sdist' = ANY ($4)) THEN unapi.sssum( distribution, 'xml', 'distribtion', evergreen.array_remove_item_by_value($4,'sstr'), $5, $6, $7, $8, FALSE) ELSE NULL END,
3037                 XMLELEMENT( name items,
3038                     CASE 
3039                         WHEN ('sitem' = ANY ($4)) THEN
3040                             XMLAGG((SELECT unapi.sitem( id, 'xml', 'serial_item', evergreen.array_remove_item_by_value($4,'sstr'), $5, $6, $7, $8, FALSE) FROM serial.item WHERE stream = sstr.id))
3041                         ELSE NULL
3042                     END
3043                 )
3044             )
3045       FROM  serial.stream sstr
3046       WHERE id = $1
3047       GROUP BY id, routing_label, distribution;
3048 $F$ LANGUAGE SQL;
3049
3050 CREATE OR REPLACE FUNCTION unapi.siss ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3051     SELECT  XMLELEMENT(
3052                 name issuance,
3053                 XMLATTRIBUTES(
3054                     CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3055                     'tag:open-ils.org:U2@siss/' || id AS id,
3056                     create_date, edit_date, label, date_published,
3057                     holding_code, holding_type, holding_link_id
3058                 ),
3059                 CASE WHEN subscription IS NOT NULL AND ('ssub' = ANY ($4)) THEN unapi.ssub( subscription, 'xml', 'subscription', evergreen.array_remove_item_by_value($4,'siss'), $5, $6, $7, $8, FALSE) ELSE NULL END,
3060                 XMLELEMENT( name items,
3061                     CASE 
3062                         WHEN ('sitem' = ANY ($4)) THEN
3063                             XMLAGG((SELECT unapi.sitem( id, 'xml', 'serial_item', evergreen.array_remove_item_by_value($4,'siss'), $5, $6, $7, $8, FALSE) FROM serial.item WHERE issuance = sstr.id))
3064                         ELSE NULL
3065                     END
3066                 )
3067             )
3068       FROM  serial.issuance sstr
3069       WHERE id = $1
3070       GROUP BY id, create_date, edit_date, label, date_published, holding_code, holding_type, holding_link_id, subscription;
3071 $F$ LANGUAGE SQL;
3072
3073 CREATE OR REPLACE FUNCTION unapi.sitem ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3074         SELECT  XMLELEMENT(
3075                     name serial_item,
3076                     XMLATTRIBUTES(
3077                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3078                         'tag:open-ils.org:U2@sitem/' || id AS id,
3079                         'tag:open-ils.org:U2@siss/' || issuance AS issuance,
3080                         date_expected, date_received
3081                     ),
3082                     CASE WHEN issuance IS NOT NULL AND ('siss' = ANY ($4)) THEN unapi.siss( issuance, $2, 'issuance', evergreen.array_remove_item_by_value($4,'sitem'), $5, $6, $7, $8, FALSE) ELSE NULL END,
3083                     CASE WHEN stream IS NOT NULL AND ('sstr' = ANY ($4)) THEN unapi.sstr( stream, $2, 'stream', evergreen.array_remove_item_by_value($4,'sitem'), $5, $6, $7, $8, FALSE) ELSE NULL END,
3084                     CASE WHEN unit IS NOT NULL AND ('sunit' = ANY ($4)) THEN unapi.sunit( stream, $2, 'serial_unit', evergreen.array_remove_item_by_value($4,'sitem'), $5, $6, $7, $8, FALSE) ELSE NULL END,
3085                     CASE WHEN uri IS NOT NULL AND ('auri' = ANY ($4)) THEN unapi.auri( uri, $2, 'uri', evergreen.array_remove_item_by_value($4,'sitem'), $5, $6, $7, $8, FALSE) ELSE NULL END
3086 --                    XMLELEMENT( name notes,
3087 --                        CASE 
3088 --                            WHEN ('acpn' = ANY ($4)) THEN
3089 --                                XMLAGG((SELECT unapi.acpn( id, 'xml', 'copy_note', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8) FROM asset.copy_note WHERE owning_copy = cp.id AND pub))
3090 --                            ELSE NULL
3091 --                        END
3092 --                    )
3093                 )
3094           FROM  serial.item sitem
3095           WHERE id = $1;
3096 $F$ LANGUAGE SQL;
3097
3098
3099 CREATE OR REPLACE FUNCTION unapi.sssum ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3100     SELECT  XMLELEMENT(
3101                 name serial_summary,
3102                 XMLATTRIBUTES(
3103                     CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3104                     'tag:open-ils.org:U2@sbsum/' || id AS id,
3105                     'sssum' AS type, generated_coverage, textual_holdings, show_generated
3106                 ),
3107                 CASE WHEN ('sdist' = ANY ($4)) THEN unapi.sdist( distribution, 'xml', 'distribtion', evergreen.array_remove_item_by_value($4,'ssum'), $5, $6, $7, $8, FALSE) ELSE NULL END
3108             )
3109       FROM  serial.supplement_summary ssum
3110       WHERE id = $1
3111       GROUP BY id, generated_coverage, textual_holdings, distribution, show_generated;
3112 $F$ LANGUAGE SQL;
3113
3114 CREATE OR REPLACE FUNCTION unapi.sbsum ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3115     SELECT  XMLELEMENT(
3116                 name serial_summary,
3117                 XMLATTRIBUTES(
3118                     CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3119                     'tag:open-ils.org:U2@sbsum/' || id AS id,
3120                     'sbsum' AS type, generated_coverage, textual_holdings, show_generated
3121                 ),
3122                 CASE WHEN ('sdist' = ANY ($4)) THEN unapi.sdist( distribution, 'xml', 'distribtion', evergreen.array_remove_item_by_value($4,'ssum'), $5, $6, $7, $8, FALSE) ELSE NULL END
3123             )
3124       FROM  serial.basic_summary ssum
3125       WHERE id = $1
3126       GROUP BY id, generated_coverage, textual_holdings, distribution, show_generated;
3127 $F$ LANGUAGE SQL;
3128
3129 CREATE OR REPLACE FUNCTION unapi.sisum ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3130     SELECT  XMLELEMENT(
3131                 name serial_summary,
3132                 XMLATTRIBUTES(
3133                     CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3134                     'tag:open-ils.org:U2@sbsum/' || id AS id,
3135                     'sisum' AS type, generated_coverage, textual_holdings, show_generated
3136                 ),
3137                 CASE WHEN ('sdist' = ANY ($4)) THEN unapi.sdist( distribution, 'xml', 'distribtion', evergreen.array_remove_item_by_value($4,'ssum'), $5, $6, $7, $8, FALSE) ELSE NULL END
3138             )
3139       FROM  serial.index_summary ssum
3140       WHERE id = $1
3141       GROUP BY id, generated_coverage, textual_holdings, distribution, show_generated;
3142 $F$ LANGUAGE SQL;
3143
3144
3145 CREATE OR REPLACE FUNCTION unapi.aou ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3146 DECLARE
3147     output XML;
3148 BEGIN
3149     IF ename = 'circlib' THEN
3150         SELECT  XMLELEMENT(
3151                     name circlib,
3152                     XMLATTRIBUTES(
3153                         'http://open-ils.org/spec/actors/v1' AS xmlns,
3154                         id AS ident
3155                     ),
3156                     name
3157                 ) INTO output
3158           FROM  actor.org_unit aou
3159           WHERE id = obj_id;
3160     ELSE
3161         EXECUTE $$SELECT  XMLELEMENT(
3162                     name $$ || ename || $$,
3163                     XMLATTRIBUTES(
3164                         'http://open-ils.org/spec/actors/v1' AS xmlns,
3165                         'tag:open-ils.org:U2@aou/' || id AS id,
3166                         shortname, name, opac_visible
3167                     )
3168                 )
3169           FROM  actor.org_unit aou
3170          WHERE id = $1 $$ INTO output USING obj_id;
3171     END IF;
3172
3173     RETURN output;
3174
3175 END;
3176 $F$ LANGUAGE PLPGSQL;
3177
3178 CREATE OR REPLACE FUNCTION unapi.acl ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3179     SELECT  XMLELEMENT(
3180                 name location,
3181                 XMLATTRIBUTES(
3182                     CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3183                     id AS ident
3184                 ),
3185                 name
3186             )
3187       FROM  asset.copy_location
3188       WHERE id = $1;
3189 $F$ LANGUAGE SQL;
3190
3191 CREATE OR REPLACE FUNCTION unapi.ccs ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3192     SELECT  XMLELEMENT(
3193                 name status,
3194                 XMLATTRIBUTES(
3195                     CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3196                     id AS ident
3197                 ),
3198                 name
3199             )
3200       FROM  config.copy_status
3201       WHERE id = $1;
3202 $F$ LANGUAGE SQL;
3203
3204 CREATE OR REPLACE FUNCTION unapi.acpn ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3205         SELECT  XMLELEMENT(
3206                     name copy_note,
3207                     XMLATTRIBUTES(
3208                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3209                         create_date AS date,
3210                         title
3211                     ),
3212                     value
3213                 )
3214           FROM  asset.copy_note
3215           WHERE id = $1;
3216 $F$ LANGUAGE SQL;
3217
3218 CREATE OR REPLACE FUNCTION unapi.ascecm ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3219         SELECT  XMLELEMENT(
3220                     name statcat,
3221                     XMLATTRIBUTES(
3222                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3223                         sc.name,
3224                         sc.opac_visible
3225                     ),
3226                     asce.value
3227                 )
3228           FROM  asset.stat_cat_entry asce
3229                 JOIN asset.stat_cat sc ON (sc.id = asce.stat_cat)
3230           WHERE asce.id = $1;
3231 $F$ LANGUAGE SQL;
3232
3233 CREATE OR REPLACE FUNCTION unapi.bmp ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3234         SELECT  XMLELEMENT(
3235                     name monograph_part,
3236                     XMLATTRIBUTES(
3237                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3238                         'tag:open-ils.org:U2@bmp/' || id AS id,
3239                         id AS ident,
3240                         label,
3241                         label_sortkey,
3242                         'tag:open-ils.org:U2@bre/' || record AS record
3243                     ),
3244                     CASE 
3245                         WHEN ('acp' = ANY ($4)) THEN
3246                             XMLELEMENT( name copies,
3247                                 (SELECT XMLAGG(acp) FROM (
3248                                     SELECT  unapi.acp( cp.id, 'xml', 'copy', evergreen.array_remove_item_by_value($4,'bmp'), $5, $6, $7, $8, FALSE)
3249                                       FROM  asset.copy cp
3250                                             JOIN asset.copy_part_map cpm ON (cpm.target_copy = cp.id)
3251                                       WHERE cpm.part = $1
3252                                       ORDER BY COALESCE(cp.copy_number,0), cp.barcode
3253                                       LIMIT $7
3254                                       OFFSET $8
3255                                 )x)
3256                             )
3257                         ELSE NULL
3258                     END,
3259                     CASE WHEN ('bre' = ANY ($4)) THEN unapi.bre( record, 'marcxml', 'record', evergreen.array_remove_item_by_value($4,'bmp'), $5, $6, $7, $8, FALSE) ELSE NULL END
3260                 )
3261           FROM  biblio.monograph_part
3262           WHERE id = $1
3263           GROUP BY id, label, label_sortkey, record;
3264 $F$ LANGUAGE SQL;
3265
3266 CREATE OR REPLACE FUNCTION unapi.acp ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3267         SELECT  XMLELEMENT(
3268                     name copy,
3269                     XMLATTRIBUTES(
3270                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3271                         'tag:open-ils.org:U2@acp/' || id AS id,
3272                         create_date, edit_date, copy_number, circulate, deposit,
3273                         ref, holdable, deleted, deposit_amount, price, barcode,
3274                         circ_modifier, circ_as_type, opac_visible
3275                     ),
3276                     unapi.ccs( status, $2, 'status', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE),
3277                     unapi.acl( location, $2, 'location', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE),
3278                     unapi.aou( circ_lib, $2, 'circ_lib', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8),
3279                     unapi.aou( circ_lib, $2, 'circlib', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8),
3280                     CASE WHEN ('acn' = ANY ($4)) THEN unapi.acn( call_number, $2, 'call_number', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE) ELSE NULL END,
3281                     XMLELEMENT( name copy_notes,
3282                         CASE 
3283                             WHEN ('acpn' = ANY ($4)) THEN
3284                                 XMLAGG((SELECT unapi.acpn( id, 'xml', 'copy_note', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE) FROM asset.copy_note WHERE owning_copy = cp.id AND pub))
3285                             ELSE NULL
3286                         END
3287                     ),
3288                     XMLELEMENT( name statcats,
3289                         CASE 
3290                             WHEN ('ascecm' = ANY ($4)) THEN
3291                                 XMLAGG((SELECT unapi.ascecm( stat_cat_entry, 'xml', 'statcat', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE) FROM asset.stat_cat_entry_copy_map WHERE owning_copy = cp.id))
3292                             ELSE NULL
3293                         END
3294                     ),
3295                     CASE 
3296                         WHEN ('bmp' = ANY ($4)) THEN
3297                             XMLELEMENT( name monograph_parts,
3298                                 XMLAGG((SELECT unapi.bmp( part, 'xml', 'monograph_part', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE) FROM asset.copy_part_map WHERE target_copy = cp.id))
3299                             )
3300                         ELSE NULL
3301                     END
3302                 )
3303           FROM  asset.copy cp
3304           WHERE id = $1
3305           GROUP BY id, status, location, circ_lib, call_number, create_date, edit_date, copy_number, circulate, deposit, ref, holdable, deleted, deposit_amount, price, barcode, circ_modifier, circ_as_type, opac_visible;
3306 $F$ LANGUAGE SQL;
3307
3308 CREATE OR REPLACE FUNCTION unapi.sunit ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3309         SELECT  XMLELEMENT(
3310                     name serial_unit,
3311                     XMLATTRIBUTES(
3312                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3313                         'tag:open-ils.org:U2@acp/' || id AS id,
3314                         create_date, edit_date, copy_number, circulate, deposit,
3315                         ref, holdable, deleted, deposit_amount, price, barcode,
3316                         circ_modifier, circ_as_type, opac_visible, status_changed_time,
3317                         floating, mint_condition, detailed_contents, sort_key, summary_contents, cost 
3318                     ),
3319                     unapi.ccs( status, $2, 'status', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($4,'acp'),'sunit'), $5, $6, $7, $8, FALSE),
3320                     unapi.acl( location, $2, 'location', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($4,'acp'),'sunit'), $5, $6, $7, $8, FALSE),
3321                     unapi.aou( circ_lib, $2, 'circ_lib', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($4,'acp'),'sunit'), $5, $6, $7, $8),
3322                     unapi.aou( circ_lib, $2, 'circlib', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($4,'acp'),'sunit'), $5, $6, $7, $8),
3323                     CASE WHEN ('acn' = ANY ($4)) THEN unapi.acn( call_number, $2, 'call_number', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE) ELSE NULL END,
3324                     XMLELEMENT( name copy_notes,
3325                         CASE 
3326                             WHEN ('acpn' = ANY ($4)) THEN
3327                                 XMLAGG((SELECT unapi.acpn( id, 'xml', 'copy_note', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($4,'acp'),'sunit'), $5, $6, $7, $8, FALSE) FROM asset.copy_note WHERE owning_copy = cp.id AND pub))
3328                             ELSE NULL
3329                         END
3330                     ),
3331                     XMLELEMENT( name statcats,
3332                         CASE 
3333                             WHEN ('ascecm' = ANY ($4)) THEN
3334                                 XMLAGG((SELECT unapi.acpn( stat_cat_entry, 'xml', 'statcat', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($4,'acp'),'sunit'), $5, $6, $7, $8, FALSE) FROM asset.stat_cat_entry_copy_map WHERE owning_copy = cp.id))
3335                             ELSE NULL
3336                         END
3337                     )
3338                 )
3339           FROM  serial.unit cp
3340           WHERE id = $1
3341           GROUP BY  id, status, location, circ_lib, call_number, create_date, edit_date, copy_number, circulate, floating, mint_condition,
3342                     deposit, ref, holdable, deleted, deposit_amount, price, barcode, circ_modifier, circ_as_type, opac_visible, status_changed_time, detailed_contents, sort_key, summary_contents, cost;
3343 $F$ LANGUAGE SQL;
3344
3345 CREATE OR REPLACE FUNCTION unapi.acn ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3346         SELECT  XMLELEMENT(
3347                     name volume,
3348                     XMLATTRIBUTES(
3349                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3350                         'tag:open-ils.org:U2@acn/' || acn.id AS id,
3351                         o.shortname AS lib,
3352                         o.opac_visible AS opac_visible,
3353                         deleted, label, label_sortkey, label_class, record
3354                     ),
3355                     unapi.aou( owning_lib, $2, 'owning_lib', evergreen.array_remove_item_by_value($4,'acn'), $5, $6, $7, $8),
3356                     XMLELEMENT( name copies,
3357                         CASE 
3358                             WHEN ('acp' = ANY ($4)) THEN
3359                                 (SELECT XMLAGG(acp) FROM (
3360                                     SELECT  unapi.acp( cp.id, 'xml', 'copy', evergreen.array_remove_item_by_value($4,'acn'), $5, $6, $7, $8, FALSE)
3361                                       FROM  asset.copy cp
3362                                             JOIN actor.org_unit_descendants(
3363                                                 (SELECT id FROM actor.org_unit WHERE shortname = $5),
3364                                                 (COALESCE($6,(SELECT aout.depth FROM actor.org_unit_type aout JOIN actor.org_unit aou ON (aou.ou_type = aout.id AND aou.shortname = $5))))
3365                                             ) aoud ON (cp.circ_lib = aoud.id)
3366                                       WHERE cp.call_number = acn.id
3367                                       ORDER BY COALESCE(cp.copy_number,0), cp.barcode
3368                                       LIMIT $7
3369                                       OFFSET $8
3370                                 )x)
3371                             ELSE NULL
3372                         END
3373                     ),
3374                     XMLELEMENT(
3375                         name uris,
3376                         (SELECT XMLAGG(auri) FROM (SELECT unapi.auri(uri,'xml','uri', evergreen.array_remove_item_by_value($4,'acn'), $5, $6, $7, $8, FALSE) FROM asset.uri_call_number_map WHERE call_number = acn.id)x)
3377                     ),
3378                     CASE WHEN ('acnp' = ANY ($4)) THEN unapi.acnp( acn.prefix, 'marcxml', 'prefix', evergreen.array_remove_item_by_value($4,'acn'), $5, $6, $7, $8, FALSE) ELSE NULL END,
3379                     CASE WHEN ('acns' = ANY ($4)) THEN unapi.acns( acn.suffix, 'marcxml', 'suffix', evergreen.array_remove_item_by_value($4,'acn'), $5, $6, $7, $8, FALSE) ELSE NULL END,
3380                     CASE WHEN ('bre' = ANY ($4)) THEN unapi.bre( acn.record, 'marcxml', 'record', evergreen.array_remove_item_by_value($4,'acn'), $5, $6, $7, $8, FALSE) ELSE NULL END
3381                 ) AS x
3382           FROM  asset.call_number acn
3383                 JOIN actor.org_unit o ON (o.id = acn.owning_lib)
3384           WHERE acn.id = $1
3385           GROUP BY acn.id, o.shortname, o.opac_visible, deleted, label, label_sortkey, label_class, owning_lib, record, acn.prefix, acn.suffix;
3386 $F$ LANGUAGE SQL;
3387
3388 CREATE OR REPLACE FUNCTION unapi.acnp ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3389         SELECT  XMLELEMENT(
3390                     name call_number_prefix,
3391                     XMLATTRIBUTES(
3392                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3393                         id AS ident,
3394                         label,
3395                         label_sortkey
3396                     ),
3397                     unapi.aou( owning_lib, $2, 'owning_lib', evergreen.array_remove_item_by_value($4,'acnp'), $5, $6, $7, $8)
3398                 )
3399           FROM  asset.call_number_prefix
3400           WHERE id = $1;
3401 $F$ LANGUAGE SQL;
3402
3403 CREATE OR REPLACE FUNCTION unapi.acns ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3404         SELECT  XMLELEMENT(
3405                     name call_number_suffix,
3406                     XMLATTRIBUTES(
3407                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3408                         id AS ident,
3409                         label,
3410                         label_sortkey
3411                     ),
3412                     unapi.aou( owning_lib, $2, 'owning_lib', evergreen.array_remove_item_by_value($4,'acns'), $5, $6, $7, $8)
3413                 )
3414           FROM  asset.call_number_suffix
3415           WHERE id = $1;
3416 $F$ LANGUAGE SQL;
3417
3418 CREATE OR REPLACE FUNCTION unapi.auri ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3419         SELECT  XMLELEMENT(
3420                     name volume,
3421                     XMLATTRIBUTES(
3422                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3423                         'tag:open-ils.org:U2@auri/' || uri.id AS id,
3424                         use_restriction,
3425                         href,
3426                         label
3427                     ),
3428                     XMLELEMENT( name copies,
3429                         CASE 
3430                             WHEN ('acn' = ANY ($4)) THEN
3431                                 (SELECT XMLAGG(acn) FROM (SELECT unapi.acn( call_number, 'xml', 'copy', evergreen.array_remove_item_by_value($4,'auri'), $5, $6, $7, $8, FALSE) FROM asset.uri_call_number_map WHERE uri = uri.id)x)
3432                             ELSE NULL
3433                         END
3434                     )
3435                 ) AS x
3436           FROM  asset.uri uri
3437           WHERE uri.id = $1
3438           GROUP BY uri.id, use_restriction, href, label;
3439 $F$ LANGUAGE SQL;
3440
3441 DROP FUNCTION IF EXISTS public.array_remove_item_by_value(ANYARRAY,ANYELEMENT);
3442
3443 DROP FUNCTION IF EXISTS public.lpad_number_substrings(TEXT,TEXT,INT);
3444
3445
3446 -- 0511
3447 CREATE OR REPLACE FUNCTION evergreen.fake_fkey_tgr () RETURNS TRIGGER AS $F$
3448 DECLARE
3449     copy_id BIGINT;
3450 BEGIN
3451     EXECUTE 'SELECT ($1).' || quote_ident(TG_ARGV[0]) INTO copy_id USING NEW;
3452     PERFORM * FROM asset.copy WHERE id = copy_id;
3453     IF NOT FOUND THEN
3454         RAISE EXCEPTION 'Key (%.%=%) does not exist in asset.copy', TG_TABLE_SCHEMA, TG_TABLE_NAME, copy_id;
3455     END IF;
3456     RETURN NULL;
3457 END;
3458 $F$ LANGUAGE PLPGSQL;
3459
3460 DROP TRIGGER IF EXISTS action_circulation_target_copy_trig ON action.circulation;
3461 CREATE TRIGGER action_circulation_target_copy_trig AFTER INSERT OR UPDATE ON action.circulation FOR EACH ROW EXECUTE PROCEDURE evergreen.fake_fkey_tgr('target_copy');
3462
3463 -- 0512
3464 CREATE TABLE biblio.peer_type (
3465     id      SERIAL  PRIMARY KEY,
3466     name        TEXT        NOT NULL UNIQUE -- i18n
3467 );
3468
3469 CREATE TABLE biblio.peer_bib_copy_map (
3470     id      SERIAL  PRIMARY KEY,
3471     peer_type   INT     NOT NULL REFERENCES biblio.peer_type (id),
3472     peer_record BIGINT      NOT NULL REFERENCES biblio.record_entry (id),
3473     target_copy BIGINT      NOT NULL -- can't use fkey because of acp subtables
3474 );
3475 CREATE INDEX peer_bib_copy_map_record_idx ON biblio.peer_bib_copy_map (peer_record);
3476 CREATE INDEX peer_bib_copy_map_copy_idx ON biblio.peer_bib_copy_map (target_copy);
3477
3478 DROP TABLE asset.opac_visible_copies;
3479 CREATE TABLE asset.opac_visible_copies (
3480   id        BIGSERIAL primary key,
3481   copy_id   BIGINT,
3482   record    BIGINT,
3483   circ_lib  INTEGER
3484 );
3485
3486 INSERT INTO biblio.peer_type (id,name) VALUES
3487     (1,oils_i18n_gettext(1,'Bound Volume','bpt','name')),
3488     (2,oils_i18n_gettext(2,'Bilingual','bpt','name')),
3489     (3,oils_i18n_gettext(3,'Back-to-back','bpt','name')),
3490     (4,oils_i18n_gettext(4,'Set','bpt','name')),
3491     (5,oils_i18n_gettext(5,'e-Reader Preload','bpt','name')); 
3492
3493 SELECT SETVAL('biblio.peer_type_id_seq'::TEXT, 100);
3494
3495 CREATE OR REPLACE FUNCTION unapi.holdings_xml (bid BIGINT, ouid INT, org TEXT, depth INT DEFAULT NULL, includes TEXT[] DEFAULT NULL::TEXT[], slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE) RETURNS XML AS $F$
3496      SELECT  XMLELEMENT(
3497                  name holdings,
3498                  XMLATTRIBUTES(
3499                     CASE WHEN $8 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3500                     CASE WHEN ('bre' = ANY ('{acn,auri}'::TEXT[] || $5)) THEN 'tag:open-ils.org:U2@bre/' || $1 || '/' || $3 ELSE NULL END AS id
3501                  ),
3502                  XMLELEMENT(
3503                      name counts,
3504                      (SELECT  XMLAGG(XMLELEMENT::XML) FROM (
3505                          SELECT  XMLELEMENT(
3506                                      name count,
3507                                      XMLATTRIBUTES('public' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
3508                                  )::text
3509                            FROM  asset.opac_ou_record_copy_count($2,  $1)
3510                                      UNION
3511                          SELECT  XMLELEMENT(
3512                                      name count,
3513                                      XMLATTRIBUTES('staff' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
3514                                  )::text
3515                            FROM  asset.staff_ou_record_copy_count($2, $1)
3516                                      ORDER BY 1
3517                      )x)
3518                  ),
3519                  CASE
3520                      WHEN ('bmp' = ANY ($5)) THEN
3521                         XMLELEMENT( name monograph_parts,
3522                             XMLAGG((SELECT unapi.bmp( id, 'xml', 'monograph_part', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($5,'bre'), 'holdings_xml'), $3, $4, $6, $7, FALSE) FROM biblio.monograph_part WHERE record = $1))
3523                         )
3524                      ELSE NULL
3525                  END,
3526                  CASE WHEN ('acn' = ANY ('{acn,auri}'::TEXT[] || $5)) THEN
3527                      XMLELEMENT(
3528                          name volumes,
3529                          (SELECT XMLAGG(acn) FROM (
3530                             SELECT  unapi.acn(acn.id,'xml','volume',evergreen.array_remove_item_by_value(evergreen.array_remove_item_by_value('{acn,auri}'::TEXT[] || $5,'holdings_xml'),'bre'), $3, $4, $6, $7, FALSE)
3531                               FROM  asset.call_number acn
3532                               WHERE acn.record = $1
3533                                     AND EXISTS (
3534                                         SELECT  1
3535                                           FROM  asset.copy acp
3536                                                 JOIN actor.org_unit_descendants(
3537                                                     $2,
3538                                                     (COALESCE(
3539                                                         $4,
3540                                                         (SELECT aout.depth
3541                                                           FROM  actor.org_unit_type aout
3542                                                                 JOIN actor.org_unit aou ON (aou.ou_type = aout.id AND aou.id = $2)
3543                                                         )
3544                                                     ))
3545                                                 ) aoud ON (acp.circ_lib = aoud.id)
3546                                           LIMIT 1
3547                                     )
3548                               ORDER BY label_sortkey
3549                               LIMIT $6
3550                               OFFSET $7
3551                          )x)
3552                      )
3553                  ELSE NULL END,
3554                  CASE WHEN ('ssub' = ANY ('{acn,auri}'::TEXT[] || $5)) THEN
3555                      XMLELEMENT(
3556                          name subscriptions,
3557                          (SELECT XMLAGG(ssub) FROM (
3558                             SELECT  unapi.ssub(id,'xml','subscription','{}'::TEXT[], $3, $4, $6, $7, FALSE)
3559                               FROM  serial.subscription
3560                               WHERE record_entry = $1
3561                         )x)
3562                      )
3563                  ELSE NULL END,
3564                  CASE WHEN ('acp' = ANY ($5)) THEN
3565                      XMLELEMENT(
3566                          name foreign_copies,
3567                          (SELECT XMLAGG(acp) FROM (
3568                             SELECT  unapi.acp(p.target_copy,'xml','copy','{}'::TEXT[], $3, $4, $6, $7, FALSE)
3569                               FROM  biblio.peer_bib_copy_map p
3570                                     JOIN asset.copy c ON (p.target_copy = c.id)
3571                               WHERE NOT c.deleted AND peer_record = $1
3572                         )x)
3573                      )
3574                  ELSE NULL END
3575              );
3576 $F$ LANGUAGE SQL;
3577
3578 CREATE OR REPLACE FUNCTION unapi.acp ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
3579         SELECT  XMLELEMENT(
3580                     name copy,
3581                     XMLATTRIBUTES(
3582                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
3583                         'tag:open-ils.org:U2@acp/' || id AS id,
3584                         create_date, edit_date, copy_number, circulate, deposit,
3585                         ref, holdable, deleted, deposit_amount, price, barcode,
3586                         circ_modifier, circ_as_type, opac_visible
3587                     ),
3588                     unapi.ccs( status, $2, 'status', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE),
3589                     unapi.acl( location, $2, 'location', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE),
3590                     unapi.aou( circ_lib, $2, 'circ_lib', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8),
3591                     unapi.aou( circ_lib, $2, 'circlib', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8),
3592                     CASE WHEN ('acn' = ANY ($4)) THEN unapi.acn( call_number, $2, 'call_number', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE) ELSE NULL END,
3593                     XMLELEMENT( name copy_notes,
3594                         CASE
3595                             WHEN ('acpn' = ANY ($4)) THEN
3596                                 XMLAGG((SELECT unapi.acpn( id, 'xml', 'copy_note', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE) FROM asset.copy_note WHERE owning_copy = cp.id AND pub))
3597                             ELSE NULL
3598                         END
3599                     ),
3600                     XMLELEMENT( name statcats,
3601                         CASE
3602                             WHEN ('ascecm' = ANY ($4)) THEN
3603                                 XMLAGG((SELECT unapi.ascecm( stat_cat_entry, 'xml', 'statcat', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE) FROM asset.stat_cat_entry_copy_map WHERE owning_copy = cp.id))
3604                             ELSE NULL
3605                         END
3606                     ),
3607                     XMLELEMENT( name foreign_records,
3608                         CASE
3609                             WHEN ('bre' = ANY ($4)) THEN
3610                                 XMLAGG((SELECT unapi.bre(peer_record,'marcxml','record','{}'::TEXT[], $5, $6, $7, $8, FALSE) FROM biblio.peer_bib_copy_map WHERE target_copy = cp.id))
3611                             ELSE NULL
3612                         END
3613
3614                     ),
3615                     CASE
3616                         WHEN ('bmp' = ANY ($4)) THEN
3617                             XMLELEMENT( name monograph_parts,
3618                                 XMLAGG((SELECT unapi.bmp( part, 'xml', 'monograph_part', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE) FROM asset.copy_part_map WHERE target_copy = cp.id))
3619                             )
3620                         ELSE NULL
3621                     END
3622                 )
3623           FROM  asset.copy cp
3624           WHERE id = $1
3625           GROUP BY id, status, location, circ_lib, call_number, create_date, edit_date, copy_number, circulate, deposit, ref, holdable, deleted, deposit_amount, price, barcode, circ_modifier, circ_as_type, opac_visible;
3626 $F$ LANGUAGE SQL;
3627
3628 CREATE OR REPLACE FUNCTION asset.refresh_opac_visible_copies_mat_view () RETURNS VOID AS $$
3629
3630     TRUNCATE TABLE asset.opac_visible_copies;
3631
3632     INSERT INTO asset.opac_visible_copies (copy_id, circ_lib, record)
3633     SELECT  cp.id, cp.circ_lib, cn.record
3634     FROM  asset.copy cp
3635         JOIN asset.call_number cn ON (cn.id = cp.call_number)
3636         JOIN actor.org_unit a ON (cp.circ_lib = a.id)
3637         JOIN asset.copy_location cl ON (cp.location = cl.id)
3638         JOIN config.copy_status cs ON (cp.status = cs.id)
3639         JOIN biblio.record_entry b ON (cn.record = b.id)
3640     WHERE NOT cp.deleted
3641         AND NOT cn.deleted
3642         AND NOT b.deleted
3643         AND cs.opac_visible
3644         AND cl.opac_visible
3645         AND cp.opac_visible
3646         AND a.opac_visible
3647             UNION
3648     SELECT  cp.id, cp.circ_lib, pbcm.peer_record AS record
3649     FROM  asset.copy cp
3650         JOIN biblio.peer_bib_copy_map pbcm ON (pbcm.target_copy = cp.id)
3651         JOIN actor.org_unit a ON (cp.circ_lib = a.id)
3652         JOIN asset.copy_location cl ON (cp.location = cl.id)
3653         JOIN config.copy_status cs ON (cp.status = cs.id)
3654     WHERE NOT cp.deleted
3655         AND cs.opac_visible
3656         AND cl.opac_visible
3657         AND cp.opac_visible
3658         AND a.opac_visible;
3659
3660 $$ LANGUAGE SQL;
3661 COMMENT ON FUNCTION asset.refresh_opac_visible_copies_mat_view() IS $$
3662 Rebuild the copy OPAC visibility cache.  Useful during migrations.
3663 $$;
3664
3665 SELECT asset.refresh_opac_visible_copies_mat_view();
3666 CREATE INDEX opac_visible_copies_idx1 on asset.opac_visible_copies (record, circ_lib);
3667 CREATE INDEX opac_visible_copies_copy_id_idx on asset.opac_visible_copies (copy_id);
3668 CREATE UNIQUE INDEX opac_visible_copies_once_per_record_idx on asset.opac_visible_copies (copy_id, record);
3669  
3670 CREATE OR REPLACE FUNCTION asset.cache_copy_visibility () RETURNS TRIGGER as $func$
3671 DECLARE
3672     add_query       TEXT;
3673     remove_query    TEXT;
3674     do_add          BOOLEAN := false;
3675     do_remove       BOOLEAN := false;
3676 BEGIN
3677     add_query := $$
3678             INSERT INTO asset.opac_visible_copies (copy_id, circ_lib, record)
3679               SELECT id, circ_lib, record FROM (
3680                 SELECT  cp.id, cp.circ_lib, cn.record, cn.id AS call_number
3681                   FROM  asset.copy cp
3682                         JOIN asset.call_number cn ON (cn.id = cp.call_number)
3683                         JOIN actor.org_unit a ON (cp.circ_lib = a.id)
3684                         JOIN asset.copy_location cl ON (cp.location = cl.id)
3685                         JOIN config.copy_status cs ON (cp.status = cs.id)
3686                         JOIN biblio.record_entry b ON (cn.record = b.id)
3687                   WHERE NOT cp.deleted
3688                         AND NOT cn.deleted
3689                         AND NOT b.deleted
3690                         AND cs.opac_visible
3691                         AND cl.opac_visible
3692                         AND cp.opac_visible
3693                         AND a.opac_visible
3694                             UNION
3695                 SELECT  cp.id, cp.circ_lib, pbcm.peer_record AS record, NULL AS call_number
3696                   FROM  asset.copy cp
3697                         JOIN biblio.peer_bib_copy_map pbcm ON (pbcm.target_copy = cp.id)
3698                         JOIN actor.org_unit a ON (cp.circ_lib = a.id)
3699                         JOIN asset.copy_location cl ON (cp.location = cl.id)
3700                         JOIN config.copy_status cs ON (cp.status = cs.id)
3701                   WHERE NOT cp.deleted
3702                         AND cs.opac_visible
3703                         AND cl.opac_visible
3704                         AND cp.opac_visible
3705                         AND a.opac_visible
3706                     ) AS x 
3707
3708     $$;
3709  
3710     remove_query := $$ DELETE FROM asset.opac_visible_copies WHERE copy_id IN ( SELECT id FROM asset.copy WHERE $$;
3711
3712     IF TG_TABLE_NAME = 'peer_bib_copy_map' THEN
3713         IF TG_OP = 'INSERT' THEN
3714             add_query := add_query || 'WHERE x.id = ' || NEW.target_copy || ' AND x.record = ' || NEW.peer_record || ';';
3715             EXECUTE add_query;
3716             RETURN NEW;
3717         ELSE
3718             remove_query := 'DELETE FROM asset.opac_visible_copies WHERE copy_id = ' || OLD.target_copy || ' AND record = ' || OLD.peer_record || ';';
3719             EXECUTE remove_query;
3720             RETURN OLD;
3721         END IF;
3722     END IF;
3723
3724     IF TG_OP = 'INSERT' THEN
3725
3726         IF TG_TABLE_NAME IN ('copy', 'unit') THEN
3727             add_query := add_query || 'WHERE x.id = ' || NEW.id || ';';
3728             EXECUTE add_query;
3729         END IF;
3730
3731         RETURN NEW;
3732
3733     END IF;
3734
3735     -- handle items first, since with circulation activity
3736     -- their statuses change frequently
3737     IF TG_TABLE_NAME IN ('copy', 'unit') THEN
3738
3739         IF OLD.location    <> NEW.location OR
3740            OLD.call_number <> NEW.call_number OR
3741            OLD.status      <> NEW.status OR
3742            OLD.circ_lib    <> NEW.circ_lib THEN
3743             -- any of these could change visibility, but
3744             -- we'll save some queries and not try to calculate
3745             -- the change directly
3746             do_remove := true;
3747             do_add := true;
3748         ELSE
3749
3750             IF OLD.deleted <> NEW.deleted THEN
3751                 IF NEW.deleted THEN
3752                     do_remove := true;
3753                 ELSE
3754                     do_add := true;
3755                 END IF;
3756             END IF;
3757
3758             IF OLD.opac_visible <> NEW.opac_visible THEN
3759                 IF OLD.opac_visible THEN
3760                     do_remove := true;
3761                 ELSIF NOT do_remove THEN -- handle edge case where deleted item
3762                                         -- is also marked opac_visible
3763                     do_add := true;
3764                 END IF;
3765             END IF;
3766
3767         END IF;
3768
3769         IF do_remove THEN
3770             DELETE FROM asset.opac_visible_copies WHERE copy_id = NEW.id;
3771         END IF;
3772         IF do_add THEN
3773             add_query := add_query || 'WHERE x.id = ' || NEW.id || ';';
3774             EXECUTE add_query;
3775         END IF;
3776
3777         RETURN NEW;
3778
3779     END IF;
3780
3781     IF TG_TABLE_NAME IN ('call_number', 'record_entry') THEN -- these have a 'deleted' column
3782  
3783         IF OLD.deleted AND NEW.deleted THEN -- do nothing
3784
3785             RETURN NEW;
3786  
3787         ELSIF NEW.deleted THEN -- remove rows
3788  
3789             IF TG_TABLE_NAME = 'call_number' THEN
3790                 DELETE FROM asset.opac_visible_copies WHERE copy_id IN (SELECT id FROM asset.copy WHERE call_number = NEW.id);
3791             ELSIF TG_TABLE_NAME = 'record_entry' THEN
3792                 DELETE FROM asset.opac_visible_copies WHERE record = NEW.id;
3793             END IF;
3794  
3795             RETURN NEW;
3796  
3797         ELSIF OLD.deleted THEN -- add rows
3798  
3799             IF TG_TABLE_NAME IN ('copy','unit') THEN
3800                 add_query := add_query || 'WHERE x.id = ' || NEW.id || ';';
3801             ELSIF TG_TABLE_NAME = 'call_number' THEN
3802                 add_query := add_query || 'WHERE x.call_number = ' || NEW.id || ';';
3803             ELSIF TG_TABLE_NAME = 'record_entry' THEN
3804                 add_query := add_query || 'WHERE x.record = ' || NEW.id || ';';
3805             END IF;
3806  
3807             EXECUTE add_query;
3808             RETURN NEW;
3809  
3810         END IF;
3811  
3812     END IF;
3813
3814     IF TG_TABLE_NAME = 'call_number' THEN
3815
3816         IF OLD.record <> NEW.record THEN
3817             -- call number is linked to different bib
3818             remove_query := remove_query || 'call_number = ' || NEW.id || ');';
3819             EXECUTE remove_query;
3820             add_query := add_query || 'WHERE x.call_number = ' || NEW.id || ';';
3821             EXECUTE add_query;
3822         END IF;
3823
3824         RETURN NEW;
3825
3826     END IF;
3827
3828     IF TG_TABLE_NAME IN ('record_entry') THEN
3829         RETURN NEW; -- don't have 'opac_visible'
3830     END IF;
3831
3832     -- actor.org_unit, asset.copy_location, asset.copy_status
3833     IF NEW.opac_visible = OLD.opac_visible THEN -- do nothing
3834
3835         RETURN NEW;
3836
3837     ELSIF NEW.opac_visible THEN -- add rows
3838
3839         IF TG_TABLE_NAME = 'org_unit' THEN
3840             add_query := add_query || 'AND cp.circ_lib = ' || NEW.id || ';';
3841         ELSIF TG_TABLE_NAME = 'copy_location' THEN
3842             add_query := add_query || 'AND cp.location = ' || NEW.id || ';';
3843         ELSIF TG_TABLE_NAME = 'copy_status' THEN
3844             add_query := add_query || 'AND cp.status = ' || NEW.id || ';';
3845         END IF;
3846  
3847         EXECUTE add_query;
3848  
3849     ELSE -- delete rows
3850
3851         IF TG_TABLE_NAME = 'org_unit' THEN
3852             remove_query := 'DELETE FROM asset.opac_visible_copies WHERE circ_lib = ' || NEW.id || ';';
3853         ELSIF TG_TABLE_NAME = 'copy_location' THEN
3854             remove_query := remove_query || 'location = ' || NEW.id || ');';
3855         ELSIF TG_TABLE_NAME = 'copy_status' THEN
3856             remove_query := remove_query || 'status = ' || NEW.id || ');';
3857         END IF;
3858  
3859         EXECUTE remove_query;
3860  
3861     END IF;
3862  
3863     RETURN NEW;
3864 END;
3865 $func$ LANGUAGE PLPGSQL;
3866 COMMENT ON FUNCTION asset.cache_copy_visibility() IS $$
3867 Trigger function to update the copy OPAC visiblity cache.
3868 $$;
3869
3870 CREATE TRIGGER a_opac_vis_mat_view_tgr AFTER INSERT OR DELETE ON biblio.peer_bib_copy_map FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
3871
3872 CREATE OR REPLACE FUNCTION biblio.indexing_ingest_or_delete () RETURNS TRIGGER AS $func$
3873 DECLARE
3874     transformed_xml TEXT;
3875     prev_xfrm       TEXT;
3876     normalizer      RECORD;
3877     xfrm            config.xml_transform%ROWTYPE;
3878     attr_value      TEXT;
3879     new_attrs       HSTORE := ''::HSTORE;
3880     attr_def        config.record_attr_definition%ROWTYPE;
3881 BEGIN
3882
3883     IF NEW.deleted IS TRUE THEN -- If this bib is deleted
3884         DELETE FROM metabib.metarecord_source_map WHERE source = NEW.id; -- Rid ourselves of the search-estimate-killing linkage
3885         DELETE FROM metabib.record_attr WHERE id = NEW.id; -- Kill the attrs hash, useless on deleted records
3886         DELETE FROM authority.bib_linking WHERE bib = NEW.id; -- Avoid updating fields in bibs that are no longer visible
3887         DELETE FROM biblio.peer_bib_copy_map WHERE peer_record = NEW.id; -- Separate any multi-homed items
3888         RETURN NEW; -- and we're done
3889     END IF;
3890
3891     IF TG_OP = 'UPDATE' THEN -- re-ingest?
3892         PERFORM * FROM config.internal_flag WHERE name = 'ingest.reingest.force_on_same_marc' AND enabled;
3893
3894         IF NOT FOUND AND OLD.marc = NEW.marc THEN -- don't do anything if the MARC didn't change
3895             RETURN NEW;
3896         END IF;
3897     END IF;
3898
3899     -- Record authority linking
3900     PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_authority_linking' AND enabled;
3901     IF NOT FOUND THEN
3902         PERFORM biblio.map_authority_linking( NEW.id, NEW.marc );
3903     END IF;
3904
3905     -- Flatten and insert the mfr data
3906     PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_full_rec' AND enabled;
3907     IF NOT FOUND THEN
3908         PERFORM metabib.reingest_metabib_full_rec(NEW.id);
3909
3910         -- Now we pull out attribute data, which is dependent on the mfr for all but XPath-based fields
3911         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_rec_descriptor' AND enabled;
3912         IF NOT FOUND THEN
3913             FOR attr_def IN SELECT * FROM config.record_attr_definition ORDER BY format LOOP
3914
3915                 IF attr_def.tag IS NOT NULL THEN -- tag (and optional subfield list) selection
3916                     SELECT  ARRAY_TO_STRING(ARRAY_ACCUM(value), COALESCE(attr_def.joiner,' ')) INTO attr_value
3917                       FROM  (SELECT * FROM metabib.full_rec ORDER BY tag, subfield) AS x
3918                       WHERE record = NEW.id
3919                             AND tag LIKE attr_def.tag
3920                             AND CASE
3921                                 WHEN attr_def.sf_list IS NOT NULL 
3922                                     THEN POSITION(subfield IN attr_def.sf_list) > 0
3923                                 ELSE TRUE
3924                                 END
3925                       GROUP BY tag
3926                       ORDER BY tag
3927                       LIMIT 1;
3928
3929                 ELSIF attr_def.fixed_field IS NOT NULL THEN -- a named fixed field, see config.marc21_ff_pos_map.fixed_field
3930                     attr_value := biblio.marc21_extract_fixed_field(NEW.id, attr_def.fixed_field);
3931
3932                 ELSIF attr_def.xpath IS NOT NULL THEN -- and xpath expression
3933
3934                     SELECT INTO xfrm * FROM config.xml_transform WHERE name = attr_def.format;
3935             
3936                     -- See if we can skip the XSLT ... it's expensive
3937                     IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
3938                         -- Can't skip the transform
3939                         IF xfrm.xslt <> '---' THEN
3940                             transformed_xml := oils_xslt_process(NEW.marc,xfrm.xslt);
3941                         ELSE
3942                             transformed_xml := NEW.marc;
3943                         END IF;
3944             
3945                         prev_xfrm := xfrm.name;
3946                     END IF;
3947
3948                     IF xfrm.name IS NULL THEN
3949                         -- just grab the marcxml (empty) transform
3950                         SELECT INTO xfrm * FROM config.xml_transform WHERE xslt = '---' LIMIT 1;
3951                         prev_xfrm := xfrm.name;
3952                     END IF;
3953
3954                     attr_value := oils_xpath_string(attr_def.xpath, transformed_xml, COALESCE(attr_def.joiner,' '), ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]);
3955
3956                 ELSIF attr_def.phys_char_sf IS NOT NULL THEN -- a named Physical Characteristic, see config.marc21_physical_characteristic_*_map
3957                     SELECT  value::TEXT INTO attr_value
3958                       FROM  biblio.marc21_physical_characteristics(NEW.id)
3959                       WHERE subfield = attr_def.phys_char_sf
3960                       LIMIT 1; -- Just in case ...
3961
3962                 END IF;
3963
3964                 -- apply index normalizers to attr_value
3965                 FOR normalizer IN
3966                     SELECT  n.func AS func,
3967                             n.param_count AS param_count,
3968                             m.params AS params
3969                       FROM  config.index_normalizer n
3970                             JOIN config.record_attr_index_norm_map m ON (m.norm = n.id)
3971                       WHERE attr = attr_def.name
3972                       ORDER BY m.pos LOOP
3973                         EXECUTE 'SELECT ' || normalizer.func || '(' ||
3974                             quote_literal( attr_value ) ||
3975                             CASE
3976                                 WHEN normalizer.param_count > 0
3977                                     THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
3978                                     ELSE ''
3979                                 END ||
3980                             ')' INTO attr_value;
3981         
3982                 END LOOP;
3983
3984                 -- Add the new value to the hstore
3985                 new_attrs := new_attrs || hstore( attr_def.name, attr_value );
3986
3987             END LOOP;
3988
3989             IF TG_OP = 'INSERT' OR OLD.deleted THEN -- initial insert OR revivication
3990                 INSERT INTO metabib.record_attr (id, attrs) VALUES (NEW.id, new_attrs);
3991             ELSE
3992                 UPDATE metabib.record_attr SET attrs = attrs || new_attrs WHERE id = NEW.id;
3993             END IF;
3994
3995         END IF;
3996     END IF;
3997
3998     -- Gather and insert the field entry data
3999     PERFORM metabib.reingest_metabib_field_entries(NEW.id);
4000
4001     -- Located URI magic
4002     IF TG_OP = 'INSERT' THEN
4003         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_located_uri' AND enabled;
4004         IF NOT FOUND THEN
4005             PERFORM biblio.extract_located_uris( NEW.id, NEW.marc, NEW.editor );
4006         END IF;
4007     ELSE
4008         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_located_uri' AND enabled;
4009         IF NOT FOUND THEN
4010             PERFORM biblio.extract_located_uris( NEW.id, NEW.marc, NEW.editor );
4011         END IF;
4012     END IF;
4013
4014     -- (re)map metarecord-bib linking
4015     IF TG_OP = 'INSERT' THEN -- if not deleted and performing an insert, check for the flag
4016         PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_insert' AND enabled;
4017         IF NOT FOUND THEN
4018             PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
4019         END IF;
4020     ELSE -- we're doing an update, and we're not deleted, remap
4021         PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_update' AND enabled;
4022         IF NOT FOUND THEN
4023             PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
4024         END IF;
4025     END IF;
4026
4027     RETURN NEW;
4028 END;
4029 $func$ LANGUAGE PLPGSQL;
4030
4031 -- 0513
4032 CREATE OR REPLACE FUNCTION unapi.mra ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
4033         SELECT  XMLELEMENT(
4034                     name attributes,
4035                     XMLATTRIBUTES(
4036                         CASE WHEN $9 THEN 'http://open-ils.org/spec/indexing/v1' ELSE NULL END AS xmlns,
4037                         'tag:open-ils.org:U2@mra/' || mra.id AS id,
4038                         'tag:open-ils.org:U2@bre/' || mra.id AS record
4039                     ),
4040                     (SELECT XMLAGG(foo.y)
4041                       FROM (SELECT XMLELEMENT(
4042                                 name field,
4043                                 XMLATTRIBUTES(
4044                                     key AS name,
4045                                     cvm.value AS "coded-value",
4046                                     rad.filter,
4047                                     rad.sorter
4048                                 ),
4049                                 x.value
4050                             )
4051                            FROM EACH(mra.attrs) AS x
4052                                 JOIN config.record_attr_definition rad ON (x.key = rad.name)
4053                                 LEFT JOIN config.coded_value_map cvm ON (cvm.ctype = x.key AND code = x.value)
4054                         )foo(y)
4055                     )
4056                 )
4057           FROM  metabib.record_attr mra
4058           WHERE mra.id = $1;
4059 $F$ LANGUAGE SQL;
4060
4061 CREATE OR REPLACE FUNCTION unapi.bre ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
4062 DECLARE
4063     me      biblio.record_entry%ROWTYPE;
4064     layout  unapi.bre_output_layout%ROWTYPE;
4065     xfrm    config.xml_transform%ROWTYPE;
4066     ouid    INT;
4067     tmp_xml TEXT;
4068     top_el  TEXT;
4069     output  XML;
4070     hxml    XML;
4071     axml    XML;
4072 BEGIN
4073
4074     SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
4075
4076     IF ouid IS NULL THEN
4077         RETURN NULL::XML;
4078     END IF;
4079
4080     IF format = 'holdings_xml' THEN -- the special case
4081         output := unapi.holdings_xml( obj_id, ouid, org, depth, includes, slimit, soffset, include_xmlns);
4082         RETURN output;
4083     END IF;
4084
4085     SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
4086
4087     IF layout.name IS NULL THEN
4088         RETURN NULL::XML;
4089     END IF;
4090
4091     SELECT * INTO xfrm FROM config.xml_transform WHERE name = layout.transform;
4092
4093     SELECT * INTO me FROM biblio.record_entry WHERE id = obj_id;
4094
4095     -- grab SVF if we need them
4096     IF ('mra' = ANY (includes)) THEN
4097         axml := unapi.mra(obj_id,NULL,NULL,NULL,NULL);
4098     ELSE
4099         axml := NULL::XML;
4100     END IF;
4101
4102     -- grab hodlings if we need them
4103     IF ('holdings_xml' = ANY (includes)) THEN
4104         hxml := unapi.holdings_xml(obj_id, ouid, org, depth, evergreen.array_remove_item_by_value(includes,'holdings_xml'), slimit, soffset, include_xmlns);
4105     ELSE
4106         hxml := NULL::XML;
4107     END IF;
4108
4109
4110     -- generate our item node
4111
4112
4113     IF format = 'marcxml' THEN
4114         tmp_xml := me.marc;
4115         IF tmp_xml !~ E'<marc:' THEN -- If we're not using the prefixed namespace in this record, then remove all declarations of it
4116            tmp_xml := REGEXP_REPLACE(tmp_xml, ' xmlns:marc="http://www.loc.gov/MARC21/slim"', '', 'g');
4117         END IF;
4118     ELSE
4119         tmp_xml := oils_xslt_process(me.marc, xfrm.xslt)::XML;
4120     END IF;
4121
4122     top_el := REGEXP_REPLACE(tmp_xml, E'^.*?<((?:\\S+:)?' || layout.holdings_element || ').*$', E'\\1');
4123
4124     IF axml IS NOT NULL THEN
4125         tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', axml || '</' || top_el || E'>\\1');
4126     END IF;
4127
4128     IF hxml IS NOT NULL THEN -- XXX how do we configure the holdings position?
4129         tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', hxml || '</' || top_el || E'>\\1');
4130     END IF;
4131
4132     IF ('bre.unapi' = ANY (includes)) THEN
4133         output := REGEXP_REPLACE(
4134             tmp_xml,
4135             '</' || top_el || '>(.*?)',
4136             XMLELEMENT(
4137                 name abbr,
4138                 XMLATTRIBUTES(
4139                     'http://www.w3.org/1999/xhtml' AS xmlns,
4140                     'unapi-id' AS class,
4141                     'tag:open-ils.org:U2@bre/' || obj_id || '/' || org AS title
4142                 )
4143             )::TEXT || '</' || top_el || E'>\\1'
4144         );
4145     ELSE
4146         output := tmp_xml;
4147     END IF;
4148
4149     RETURN output;
4150 END;
4151 $F$ LANGUAGE PLPGSQL;
4152
4153
4154 -- 0514
4155 CREATE OR REPLACE FUNCTION unapi.bre ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
4156 DECLARE
4157     me      biblio.record_entry%ROWTYPE;
4158     layout  unapi.bre_output_layout%ROWTYPE;
4159     xfrm    config.xml_transform%ROWTYPE;
4160     ouid    INT;
4161     tmp_xml TEXT;
4162     top_el  TEXT;
4163     output  XML;
4164     hxml    XML;
4165     axml    XML;
4166 BEGIN
4167
4168     SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
4169
4170     IF ouid IS NULL THEN
4171         RETURN NULL::XML;
4172     END IF;
4173
4174     IF format = 'holdings_xml' THEN -- the special case
4175         output := unapi.holdings_xml( obj_id, ouid, org, depth, includes, slimit, soffset, include_xmlns);
4176         RETURN output;
4177     END IF;
4178
4179     SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
4180
4181     IF layout.name IS NULL THEN
4182         RETURN NULL::XML;
4183     END IF;
4184
4185     SELECT * INTO xfrm FROM config.xml_transform WHERE name = layout.transform;
4186
4187     SELECT * INTO me FROM biblio.record_entry WHERE id = obj_id;
4188
4189     -- grab SVF if we need them
4190     IF ('mra' = ANY (includes)) THEN
4191         axml := unapi.mra(obj_id,NULL,NULL,NULL,NULL);
4192     ELSE
4193         axml := NULL::XML;
4194     END IF;
4195
4196     -- grab hodlings if we need them
4197     IF ('holdings_xml' = ANY (includes)) THEN
4198         hxml := unapi.holdings_xml(obj_id, ouid, org, depth, evergreen.array_remove_item_by_value(includes,'holdings_xml'), slimit, soffset, include_xmlns);
4199     ELSE
4200         hxml := NULL::XML;
4201     END IF;
4202
4203
4204     -- generate our item node
4205
4206
4207     IF format = 'marcxml' THEN
4208         tmp_xml := me.marc;
4209         IF tmp_xml !~ E'<marc:' THEN -- If we're not using the prefixed namespace in this record, then remove all declarations of it
4210            tmp_xml := REGEXP_REPLACE(tmp_xml, ' xmlns:marc="http://www.loc.gov/MARC21/slim"', '', 'g');
4211         END IF;
4212     ELSE
4213         tmp_xml := oils_xslt_process(me.marc, xfrm.xslt)::XML;
4214     END IF;
4215
4216     top_el := REGEXP_REPLACE(tmp_xml, E'^.*?<((?:\\S+:)?' || layout.holdings_element || ').*$', E'\\1');
4217
4218     IF axml IS NOT NULL THEN
4219         tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', axml || '</' || top_el || E'>\\1');
4220     END IF;
4221
4222     IF hxml IS NOT NULL THEN -- XXX how do we configure the holdings position?
4223         tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', hxml || '</' || top_el || E'>\\1');
4224     END IF;
4225
4226     IF ('bre.unapi' = ANY (includes)) THEN
4227         output := REGEXP_REPLACE(
4228             tmp_xml,
4229             '</' || top_el || '>(.*?)',
4230             XMLELEMENT(
4231                 name abbr,
4232                 XMLATTRIBUTES(
4233                     'http://www.w3.org/1999/xhtml' AS xmlns,
4234                     'unapi-id' AS class,
4235                     'tag:open-ils.org:U2@bre/' || obj_id || '/' || org AS title
4236                 )
4237             )::TEXT || '</' || top_el || E'>\\1'
4238         );
4239     ELSE
4240         output := tmp_xml;
4241     END IF;
4242
4243     output := REGEXP_REPLACE(output::TEXT,E'>\\s+<','><','gs')::XML;
4244     RETURN output;
4245 END;
4246 $F$ LANGUAGE PLPGSQL;
4247
4248
4249
4250 -- 0516
4251 CREATE OR REPLACE FUNCTION public.extract_acq_marc_field ( BIGINT, TEXT, TEXT) RETURNS TEXT AS $$    
4252     SELECT extract_marc_field('acq.lineitem', $1, $2, $3);
4253 $$ LANGUAGE SQL;
4254
4255 -- 0518
4256 CREATE OR REPLACE FUNCTION vandelay.marc21_extract_fixed_field( marc TEXT, ff TEXT ) RETURNS TEXT AS $func$
4257 DECLARE
4258     rtype       TEXT;
4259     ff_pos      RECORD;
4260     tag_data    RECORD;
4261     val         TEXT;
4262 BEGIN
4263     rtype := (vandelay.marc21_record_type( marc )).code;
4264     FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE fixed_field = ff AND rec_type = rtype ORDER BY tag DESC LOOP
4265         IF ff_pos.tag = 'ldr' THEN
4266             val := oils_xpath_string('//*[local-name()="leader"]', marc);
4267             IF val IS NOT NULL THEN
4268                 val := SUBSTRING( val, ff_pos.start_pos + 1, ff_pos.length );
4269                 RETURN val;
4270             END IF;
4271         ELSE 
4272             FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(ff_pos.tag) || '"]/text()', marc ) ) x(value) LOOP
4273                 val := SUBSTRING( tag_data.value, ff_pos.start_pos + 1, ff_pos.length );
4274                 RETURN val;
4275             END LOOP;
4276         END IF;
4277         val := REPEAT( ff_pos.default_val, ff_pos.length );
4278         RETURN val;
4279     END LOOP;
4280
4281     RETURN NULL;
4282 END;
4283 $func$ LANGUAGE PLPGSQL;
4284
4285
4286 -- 0519
4287 CREATE OR REPLACE FUNCTION vandelay.marc21_extract_all_fixed_fields( marc TEXT ) RETURNS SETOF biblio.record_ff_map AS $func$
4288 DECLARE
4289     tag_data    TEXT;
4290     rtype       TEXT;
4291     ff_pos      RECORD;
4292     output      biblio.record_ff_map%ROWTYPE;
4293 BEGIN
4294     rtype := (vandelay.marc21_record_type( marc )).code;
4295
4296     FOR ff_pos IN SELECT * FROM config.marc21_ff_pos_map WHERE rec_type = rtype ORDER BY tag DESC LOOP
4297         output.ff_name  := ff_pos.fixed_field;
4298         output.ff_value := NULL;
4299
4300         IF ff_pos.tag = 'ldr' THEN
4301             output.ff_value := oils_xpath_string('//*[local-name()="leader"]', marc);
4302             IF output.ff_value IS NOT NULL THEN
4303                 output.ff_value := SUBSTRING( output.ff_value, ff_pos.start_pos + 1, ff_pos.length );
4304                 RETURN NEXT output;
4305                 output.ff_value := NULL;
4306             END IF;
4307         ELSE
4308             FOR tag_data IN SELECT value FROM UNNEST( oils_xpath( '//*[@tag="' || UPPER(ff_pos.tag) || '"]/text()', marc ) ) x(value) LOOP
4309                 output.ff_value := SUBSTRING( tag_data, ff_pos.start_pos + 1, ff_pos.length );
4310                 IF output.ff_value IS NULL THEN output.ff_value := REPEAT( ff_pos.default_val, ff_pos.length ); END IF;
4311                 RETURN NEXT output;
4312                 output.ff_value := NULL;
4313             END LOOP;
4314         END IF;
4315     
4316     END LOOP;
4317
4318     RETURN;
4319 END;
4320 $func$ LANGUAGE PLPGSQL;
4321
4322
4323 -- 0521
4324 CREATE OR REPLACE FUNCTION biblio.extract_located_uris( bib_id BIGINT, marcxml TEXT, editor_id INT ) RETURNS VOID AS $func$
4325 DECLARE
4326     uris            TEXT[];
4327     uri_xml         TEXT;
4328     uri_label       TEXT;
4329     uri_href        TEXT;
4330     uri_use         TEXT;
4331     uri_owner_list  TEXT[];
4332     uri_owner       TEXT;
4333     uri_owner_id    INT;
4334     uri_id          INT;
4335     uri_cn_id       INT;
4336     uri_map_id      INT;
4337 BEGIN
4338
4339     -- Clear any URI mappings and call numbers for this bib.
4340     -- This leads to acn / auricnm inflation, but also enables
4341     -- old acn/auricnm's to go away and for bibs to be deleted.
4342     FOR uri_cn_id IN SELECT id FROM asset.call_number WHERE record = bib_id AND label = '##URI##' AND NOT deleted LOOP
4343         DELETE FROM asset.uri_call_number_map WHERE call_number = uri_cn_id;
4344         DELETE FROM asset.call_number WHERE id = uri_cn_id;
4345     END LOOP;
4346
4347     uris := oils_xpath('//*[@tag="856" and (@ind1="4" or @ind1="1") and (@ind2="0" or @ind2="1")]',marcxml);
4348     IF ARRAY_UPPER(uris,1) > 0 THEN
4349         FOR i IN 1 .. ARRAY_UPPER(uris, 1) LOOP
4350             -- First we pull info out of the 856
4351             uri_xml     := uris[i];
4352
4353             uri_href    := (oils_xpath('//*[@code="u"]/text()',uri_xml))[1];
4354             uri_label   := (oils_xpath('//*[@code="y"]/text()|//*[@code="3"]/text()|//*[@code="u"]/text()',uri_xml))[1];
4355             uri_use     := (oils_xpath('//*[@code="z"]/text()|//*[@code="2"]/text()|//*[@code="n"]/text()',uri_xml))[1];
4356             CONTINUE WHEN uri_href IS NULL OR uri_label IS NULL;
4357
4358             -- Get the distinct list of libraries wanting to use 
4359             SELECT  ARRAY_ACCUM(
4360                         DISTINCT REGEXP_REPLACE(
4361                             x,
4362                             $re$^.*?\((\w+)\).*$$re$,
4363                             E'\\1'
4364                         )
4365                     ) INTO uri_owner_list
4366               FROM  UNNEST(
4367                         oils_xpath(
4368                             '//*[@code="9"]/text()|//*[@code="w"]/text()|//*[@code="n"]/text()',
4369                             uri_xml
4370                         )
4371                     )x;
4372
4373             IF ARRAY_UPPER(uri_owner_list,1) > 0 THEN
4374
4375                 -- look for a matching uri
4376                 SELECT id INTO uri_id FROM asset.uri WHERE label = uri_label AND href = uri_href AND use_restriction = uri_use AND active;
4377                 IF NOT FOUND THEN -- create one
4378                     INSERT INTO asset.uri (label, href, use_restriction) VALUES (uri_label, uri_href, uri_use);
4379                     IF uri_use IS NULL THEN
4380                         SELECT id INTO uri_id FROM asset.uri WHERE label = uri_label AND href = uri_href AND use_restriction IS NULL AND active;
4381                     ELSE
4382                         SELECT id INTO uri_id FROM asset.uri WHERE label = uri_label AND href = uri_href AND use_restriction = uri_use AND active;
4383                     END IF;
4384                 END IF;
4385
4386                 FOR j IN 1 .. ARRAY_UPPER(uri_owner_list, 1) LOOP
4387                     uri_owner := uri_owner_list[j];
4388
4389                     SELECT id INTO uri_owner_id FROM actor.org_unit WHERE shortname = uri_owner;
4390                     CONTINUE WHEN NOT FOUND;
4391
4392                     -- we need a call number to link through
4393                     SELECT id INTO uri_cn_id FROM asset.call_number WHERE owning_lib = uri_owner_id AND record = bib_id AND label = '##URI##' AND NOT deleted;
4394                     IF NOT FOUND THEN
4395                         INSERT INTO asset.call_number (owning_lib, record, create_date, edit_date, creator, editor, label)
4396                             VALUES (uri_owner_id, bib_id, 'now', 'now', editor_id, editor_id, '##URI##');
4397                         SELECT id INTO uri_cn_id FROM asset.call_number WHERE owning_lib = uri_owner_id AND record = bib_id AND label = '##URI##' AND NOT deleted;
4398                     END IF;
4399
4400                     -- now, link them if they're not already
4401                     SELECT id INTO uri_map_id FROM asset.uri_call_number_map WHERE call_number = uri_cn_id AND uri = uri_id;
4402                     IF NOT FOUND THEN
4403                         INSERT INTO asset.uri_call_number_map (call_number, uri) VALUES (uri_cn_id, uri_id);
4404                     END IF;
4405
4406                 END LOOP;
4407
4408             END IF;
4409
4410         END LOOP;
4411     END IF;
4412
4413     RETURN;
4414 END;
4415 $func$ LANGUAGE PLPGSQL;
4416
4417
4418 -- 0522
4419 UPDATE config.org_unit_setting_type SET datatype = 'string' WHERE name = 'ui.general.button_bar';
4420
4421 INSERT INTO config.org_unit_setting_type ( name, label, description, datatype) VALUES ('ui.general.hotkeyset', 'GUI: Default Hotkeyset', 'Default Hotkeyset for clients (filename without the .keyset).  Examples: Default, Minimal, and None', 'string');
4422
4423 UPDATE actor.org_unit_setting SET value='"circ"' WHERE name = 'ui.general.button_bar' AND value='true';
4424
4425 UPDATE actor.org_unit_setting SET value='"none"' WHERE name = 'ui.general.button_bar' AND value='false';
4426
4427
4428 -- 0523
4429 INSERT into config.org_unit_setting_type
4430 ( name, label, description, datatype, fm_class ) VALUES
4431 ( 'cat.default_copy_status_fast',
4432   oils_i18n_gettext( 'cat.default_copy_status_fast', 'Cataloging: Default copy status (fast add)', 'coust', 'label'),
4433   oils_i18n_gettext( 'cat.default_copy_status_fast', 'Default status when a copy is created using the "Fast Add" interface.', 'coust', 'description'),
4434   'link', 'ccs'
4435 );
4436
4437 INSERT into config.org_unit_setting_type
4438 ( name, label, description, datatype, fm_class ) VALUES
4439 ( 'cat.default_copy_status_normal',
4440   oils_i18n_gettext( 'cat.default_copy_status_normal', 'Cataloging: Default copy status (normal)', 'coust', 'label'),
4441   oils_i18n_gettext( 'cat.default_copy_status_normal', 'Default status when a copy is created using the normal volume/copy creator interface.', 'coust', 'description'),
4442   'link', 'ccs'
4443 );
4444
4445 -- 0524
4446 INSERT into config.org_unit_setting_type
4447 ( name, label, description, datatype ) VALUES
4448 ( 'ui.unified_volume_copy_editor',
4449   oils_i18n_gettext( 'ui.unified_volume_copy_editor', 'GUI: Unified Volume/Item Creator/Editor', 'coust', 'label'),
4450   oils_i18n_gettext( 'ui.unified_volume_copy_editor', 'If true combines the Volume/Copy Creator and Item Attribute Editor in some instances.', 'coust', 'description'),
4451   'bool'
4452 );
4453
4454 -- 0525
4455 CREATE OR REPLACE FUNCTION biblio.indexing_ingest_or_delete () RETURNS TRIGGER AS $func$
4456 DECLARE
4457     transformed_xml TEXT;
4458     prev_xfrm       TEXT;
4459     normalizer      RECORD;
4460     xfrm            config.xml_transform%ROWTYPE;
4461     attr_value      TEXT;
4462     new_attrs       HSTORE := ''::HSTORE;
4463     attr_def        config.record_attr_definition%ROWTYPE;
4464 BEGIN
4465
4466     IF NEW.deleted IS TRUE THEN -- If this bib is deleted
4467         DELETE FROM metabib.metarecord_source_map WHERE source = NEW.id; -- Rid ourselves of the search-estimate-killing linkage
4468         DELETE FROM metabib.record_attr WHERE id = NEW.id; -- Kill the attrs hash, useless on deleted records
4469         DELETE FROM authority.bib_linking WHERE bib = NEW.id; -- Avoid updating fields in bibs that are no longer visible
4470         DELETE FROM biblio.peer_bib_copy_map WHERE peer_record = NEW.id; -- Separate any multi-homed items
4471         RETURN NEW; -- and we're done
4472     END IF;
4473
4474     IF TG_OP = 'UPDATE' THEN -- re-ingest?
4475         PERFORM * FROM config.internal_flag WHERE name = 'ingest.reingest.force_on_same_marc' AND enabled;
4476
4477         IF NOT FOUND AND OLD.marc = NEW.marc THEN -- don't do anything if the MARC didn't change
4478             RETURN NEW;
4479         END IF;
4480     END IF;
4481
4482     -- Record authority linking
4483     PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_authority_linking' AND enabled;
4484     IF NOT FOUND THEN
4485         PERFORM biblio.map_authority_linking( NEW.id, NEW.marc );
4486     END IF;
4487
4488     -- Flatten and insert the mfr data
4489     PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_full_rec' AND enabled;
4490     IF NOT FOUND THEN
4491         PERFORM metabib.reingest_metabib_full_rec(NEW.id);
4492
4493         -- Now we pull out attribute data, which is dependent on the mfr for all but XPath-based fields
4494         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_rec_descriptor' AND enabled;
4495         IF NOT FOUND THEN
4496             FOR attr_def IN SELECT * FROM config.record_attr_definition ORDER BY format LOOP
4497
4498                 IF attr_def.tag IS NOT NULL THEN -- tag (and optional subfield list) selection
4499                     SELECT  ARRAY_TO_STRING(ARRAY_ACCUM(value), COALESCE(attr_def.joiner,' ')) INTO attr_value
4500                       FROM  (SELECT * FROM metabib.full_rec ORDER BY tag, subfield) AS x
4501                       WHERE record = NEW.id
4502                             AND tag LIKE attr_def.tag
4503                             AND CASE
4504                                 WHEN attr_def.sf_list IS NOT NULL 
4505                                     THEN POSITION(subfield IN attr_def.sf_list) > 0
4506                                 ELSE TRUE
4507                                 END
4508                       GROUP BY tag
4509                       ORDER BY tag
4510                       LIMIT 1;
4511
4512                 ELSIF attr_def.fixed_field IS NOT NULL THEN -- a named fixed field, see config.marc21_ff_pos_map.fixed_field
4513                     attr_value := biblio.marc21_extract_fixed_field(NEW.id, attr_def.fixed_field);
4514
4515                 ELSIF attr_def.xpath IS NOT NULL THEN -- and xpath expression
4516
4517                     SELECT INTO xfrm * FROM config.xml_transform WHERE name = attr_def.format;
4518             
4519                     -- See if we can skip the XSLT ... it's expensive
4520                     IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
4521                         -- Can't skip the transform
4522                         IF xfrm.xslt <> '---' THEN
4523                             transformed_xml := oils_xslt_process(NEW.marc,xfrm.xslt);
4524                         ELSE
4525                             transformed_xml := NEW.marc;
4526                         END IF;
4527             
4528                         prev_xfrm := xfrm.name;
4529                     END IF;
4530
4531                     IF xfrm.name IS NULL THEN
4532                         -- just grab the marcxml (empty) transform
4533                         SELECT INTO xfrm * FROM config.xml_transform WHERE xslt = '---' LIMIT 1;
4534                         prev_xfrm := xfrm.name;
4535                     END IF;
4536
4537                     attr_value := oils_xpath_string(attr_def.xpath, transformed_xml, COALESCE(attr_def.joiner,' '), ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]);
4538
4539                 ELSIF attr_def.phys_char_sf IS NOT NULL THEN -- a named Physical Characteristic, see config.marc21_physical_characteristic_*_map
4540                     SELECT  m.value INTO attr_value
4541                       FROM  biblio.marc21_physical_characteristics(NEW.id) v
4542                             JOIN config.marc21_physical_characteristic_value_map m ON (m.id = v.value)
4543                       WHERE v.subfield = attr_def.phys_char_sf
4544                       LIMIT 1; -- Just in case ...
4545
4546                 END IF;
4547
4548                 -- apply index normalizers to attr_value
4549                 FOR normalizer IN
4550                     SELECT  n.func AS func,
4551                             n.param_count AS param_count,
4552                             m.params AS params
4553                       FROM  config.index_normalizer n
4554                             JOIN config.record_attr_index_norm_map m ON (m.norm = n.id)
4555                       WHERE attr = attr_def.name
4556                       ORDER BY m.pos LOOP
4557                         EXECUTE 'SELECT ' || normalizer.func || '(' ||
4558                             quote_literal( attr_value ) ||
4559                             CASE
4560                                 WHEN normalizer.param_count > 0
4561                                     THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
4562                                     ELSE ''
4563                                 END ||
4564                             ')' INTO attr_value;
4565         
4566                 END LOOP;
4567
4568                 -- Add the new value to the hstore
4569                 new_attrs := new_attrs || hstore( attr_def.name, attr_value );
4570
4571             END LOOP;
4572
4573             IF TG_OP = 'INSERT' OR OLD.deleted THEN -- initial insert OR revivication
4574                 INSERT INTO metabib.record_attr (id, attrs) VALUES (NEW.id, new_attrs);
4575             ELSE
4576                 UPDATE metabib.record_attr SET attrs = attrs || new_attrs WHERE id = NEW.id;
4577             END IF;
4578
4579         END IF;
4580     END IF;
4581
4582     -- Gather and insert the field entry data
4583     PERFORM metabib.reingest_metabib_field_entries(NEW.id);
4584
4585     -- Located URI magic
4586     IF TG_OP = 'INSERT' THEN
4587         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_located_uri' AND enabled;
4588         IF NOT FOUND THEN
4589             PERFORM biblio.extract_located_uris( NEW.id, NEW.marc, NEW.editor );
4590         END IF;
4591     ELSE
4592         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_located_uri' AND enabled;
4593         IF NOT FOUND THEN
4594             PERFORM biblio.extract_located_uris( NEW.id, NEW.marc, NEW.editor );
4595         END IF;
4596     END IF;
4597
4598     -- (re)map metarecord-bib linking
4599     IF TG_OP = 'INSERT' THEN -- if not deleted and performing an insert, check for the flag
4600         PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_insert' AND enabled;
4601         IF NOT FOUND THEN
4602             PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
4603         END IF;
4604     ELSE -- we're doing an update, and we're not deleted, remap
4605         PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_update' AND enabled;
4606         IF NOT FOUND THEN
4607             PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
4608         END IF;
4609     END IF;
4610
4611     RETURN NEW;
4612 END;
4613 $func$ LANGUAGE PLPGSQL;
4614
4615 ALTER TABLE config.circ_matrix_weights ADD COLUMN marc_bib_level NUMERIC(6,2) NOT NULL DEFAULT 0.0;
4616
4617 UPDATE config.circ_matrix_weights
4618 SET marc_bib_level = marc_vr_format;
4619
4620 ALTER TABLE config.hold_matrix_weights ADD COLUMN marc_bib_level NUMERIC(6, 2) NOT NULL DEFAULT 0.0;
4621
4622 UPDATE config.hold_matrix_weights
4623 SET marc_bib_level = marc_vr_format;
4624
4625 ALTER TABLE config.circ_matrix_weights ALTER COLUMN marc_bib_level DROP DEFAULT;
4626
4627 ALTER TABLE config.hold_matrix_weights ALTER COLUMN marc_bib_level DROP DEFAULT;
4628
4629 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$
4630 DECLARE
4631     cn_object       asset.call_number%ROWTYPE;
4632     rec_descriptor  metabib.rec_descriptor%ROWTYPE;
4633     cur_matchpoint  config.circ_matrix_matchpoint%ROWTYPE;
4634     matchpoint      config.circ_matrix_matchpoint%ROWTYPE;
4635     weights         config.circ_matrix_weights%ROWTYPE;
4636     user_age        INTERVAL;
4637     denominator     NUMERIC(6,2);
4638     row_list        INT[];
4639     result          action.found_circ_matrix_matchpoint;
4640 BEGIN
4641     -- Assume failure
4642     result.success = false;
4643
4644     -- Fetch useful data
4645     SELECT INTO cn_object       * FROM asset.call_number        WHERE id = item_object.call_number;
4646     SELECT INTO rec_descriptor  * FROM metabib.rec_descriptor   WHERE record = cn_object.record;
4647
4648     -- Pre-generate this so we only calc it once
4649     IF user_object.dob IS NOT NULL THEN
4650         SELECT INTO user_age age(user_object.dob);
4651     END IF;
4652
4653     -- Grab the closest set circ weight setting.
4654     SELECT INTO weights cw.*
4655       FROM config.weight_assoc wa
4656            JOIN config.circ_matrix_weights cw ON (cw.id = wa.circ_weights)
4657            JOIN actor.org_unit_ancestors_distance( context_ou ) d ON (wa.org_unit = d.id)
4658       WHERE active
4659       ORDER BY d.distance
4660       LIMIT 1;
4661
4662     -- No weights? Bad admin! Defaults to handle that anyway.
4663     IF weights.id IS NULL THEN
4664         weights.grp                 := 11.0;
4665         weights.org_unit            := 10.0;
4666         weights.circ_modifier       := 5.0;
4667         weights.marc_type           := 4.0;
4668         weights.marc_form           := 3.0;
4669         weights.marc_bib_level      := 2.0;
4670         weights.marc_vr_format      := 2.0;
4671         weights.copy_circ_lib       := 8.0;
4672         weights.copy_owning_lib     := 8.0;
4673         weights.user_home_ou        := 8.0;
4674         weights.ref_flag            := 1.0;
4675         weights.juvenile_flag       := 6.0;
4676         weights.is_renewal          := 7.0;
4677         weights.usr_age_lower_bound := 0.0;
4678         weights.usr_age_upper_bound := 0.0;
4679     END IF;
4680
4681     -- Determine the max (expected) depth (+1) of the org tree and max depth of the permisson tree
4682     -- If you break your org tree with funky parenting this may be wrong
4683     -- 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
4684     -- We use one denominator for all tree-based checks for when permission groups and org units have the same weighting
4685     WITH all_distance(distance) AS (
4686             SELECT depth AS distance FROM actor.org_unit_type
4687         UNION
4688             SELECT distance AS distance FROM permission.grp_ancestors_distance((SELECT id FROM permission.grp_tree WHERE parent IS NULL))
4689         )
4690     SELECT INTO denominator MAX(distance) + 1 FROM all_distance;
4691
4692     -- Loop over all the potential matchpoints
4693     FOR cur_matchpoint IN
4694         SELECT m.*
4695           FROM  config.circ_matrix_matchpoint m
4696                 /*LEFT*/ JOIN permission.grp_ancestors_distance( user_object.profile ) upgad ON m.grp = upgad.id
4697                 /*LEFT*/ JOIN actor.org_unit_ancestors_distance( context_ou ) ctoua ON m.org_unit = ctoua.id
4698                 LEFT JOIN actor.org_unit_ancestors_distance( cn_object.owning_lib ) cnoua ON m.copy_owning_lib = cnoua.id
4699                 LEFT JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) iooua ON m.copy_circ_lib = iooua.id
4700                 LEFT JOIN actor.org_unit_ancestors_distance( user_object.home_ou  ) uhoua ON m.user_home_ou = uhoua.id
4701           WHERE m.active
4702                 -- Permission Groups
4703              -- AND (m.grp                      IS NULL OR upgad.id IS NOT NULL) -- Optional Permission Group?
4704                 -- Org Units
4705              -- AND (m.org_unit                 IS NULL OR ctoua.id IS NOT NULL) -- Optional Org Unit?
4706                 AND (m.copy_owning_lib          IS NULL OR cnoua.id IS NOT NULL)
4707                 AND (m.copy_circ_lib            IS NULL OR iooua.id IS NOT NULL)
4708                 AND (m.user_home_ou             IS NULL OR uhoua.id IS NOT NULL)
4709                 -- Circ Type
4710                 AND (m.is_renewal               IS NULL OR m.is_renewal = renewal)
4711                 -- Static User Checks
4712                 AND (m.juvenile_flag            IS NULL OR m.juvenile_flag = user_object.juvenile)
4713                 AND (m.usr_age_lower_bound      IS NULL OR (user_age IS NOT NULL AND m.usr_age_lower_bound < user_age))
4714                 AND (m.usr_age_upper_bound      IS NULL OR (user_age IS NOT NULL AND m.usr_age_upper_bound > user_age))
4715                 -- Static Item Checks
4716                 AND (m.circ_modifier            IS NULL OR m.circ_modifier = item_object.circ_modifier)
4717                 AND (m.marc_type                IS NULL OR m.marc_type = COALESCE(item_object.circ_as_type, rec_descriptor.item_type))
4718                 AND (m.marc_form                IS NULL OR m.marc_form = rec_descriptor.item_form)
4719                 AND (m.marc_bib_level           IS NULL OR m.marc_bib_level = rec_descriptor.bib_level)
4720                 AND (m.marc_vr_format           IS NULL OR m.marc_vr_format = rec_descriptor.vr_format)
4721                 AND (m.ref_flag                 IS NULL OR m.ref_flag = item_object.ref)
4722           ORDER BY
4723                 -- Permission Groups
4724                 CASE WHEN upgad.distance        IS NOT NULL THEN 2^(2*weights.grp - (upgad.distance/denominator)) ELSE 0.0 END +
4725                 -- Org Units
4726                 CASE WHEN ctoua.distance        IS NOT NULL THEN 2^(2*weights.org_unit - (ctoua.distance/denominator)) ELSE 0.0 END +
4727                 CASE WHEN cnoua.distance        IS NOT NULL THEN 2^(2*weights.copy_owning_lib - (cnoua.distance/denominator)) ELSE 0.0 END +
4728                 CASE WHEN iooua.distance        IS NOT NULL THEN 2^(2*weights.copy_circ_lib - (iooua.distance/denominator)) ELSE 0.0 END +
4729                 CASE WHEN uhoua.distance        IS NOT NULL THEN 2^(2*weights.user_home_ou - (uhoua.distance/denominator)) ELSE 0.0 END +
4730                 -- Circ Type                    -- Note: 4^x is equiv to 2^(2*x)
4731                 CASE WHEN m.is_renewal          IS NOT NULL THEN 4^weights.is_renewal ELSE 0.0 END +
4732                 -- Static User Checks
4733                 CASE WHEN m.juvenile_flag       IS NOT NULL THEN 4^weights.juvenile_flag ELSE 0.0 END +
4734                 CASE WHEN m.usr_age_lower_bound IS NOT NULL THEN 4^weights.usr_age_lower_bound ELSE 0.0 END +
4735                 CASE WHEN m.usr_age_upper_bound IS NOT NULL THEN 4^weights.usr_age_upper_bound ELSE 0.0 END +
4736                 -- Static Item Checks
4737                 CASE WHEN m.circ_modifier       IS NOT NULL THEN 4^weights.circ_modifier ELSE 0.0 END +
4738                 CASE WHEN m.marc_type           IS NOT NULL THEN 4^weights.marc_type ELSE 0.0 END +
4739                 CASE WHEN m.marc_form           IS NOT NULL THEN 4^weights.marc_form ELSE 0.0 END +
4740                 CASE WHEN m.marc_vr_format      IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0.0 END +
4741                 CASE WHEN m.ref_flag            IS NOT NULL THEN 4^weights.ref_flag ELSE 0.0 END DESC,
4742                 -- Final sort on id, so that if two rules have the same sorting in the previous sort they have a defined order
4743                 -- This prevents "we changed the table order by updating a rule, and we started getting different results"
4744                 m.id LOOP
4745
4746         -- Record the full matching row list
4747         row_list := row_list || cur_matchpoint.id;
4748
4749         -- No matchpoint yet?
4750         IF matchpoint.id IS NULL THEN
4751             -- Take the entire matchpoint as a starting point
4752             matchpoint := cur_matchpoint;
4753             CONTINUE; -- No need to look at this row any more.
4754         END IF;
4755
4756         -- Incomplete matchpoint?
4757         IF matchpoint.circulate IS NULL THEN
4758             matchpoint.circulate := cur_matchpoint.circulate;
4759         END IF;
4760         IF matchpoint.duration_rule IS NULL THEN
4761             matchpoint.duration_rule := cur_matchpoint.duration_rule;
4762         END IF;
4763         IF matchpoint.recurring_fine_rule IS NULL THEN
4764             matchpoint.recurring_fine_rule := cur_matchpoint.recurring_fine_rule;
4765         END IF;
4766         IF matchpoint.max_fine_rule IS NULL THEN
4767             matchpoint.max_fine_rule := cur_matchpoint.max_fine_rule;
4768         END IF;
4769         IF matchpoint.hard_due_date IS NULL THEN
4770             matchpoint.hard_due_date := cur_matchpoint.hard_due_date;
4771         END IF;
4772         IF matchpoint.total_copy_hold_ratio IS NULL THEN
4773             matchpoint.total_copy_hold_ratio := cur_matchpoint.total_copy_hold_ratio;
4774         END IF;
4775         IF matchpoint.available_copy_hold_ratio IS NULL THEN
4776             matchpoint.available_copy_hold_ratio := cur_matchpoint.available_copy_hold_ratio;
4777         END IF;
4778         IF matchpoint.renewals IS NULL THEN
4779             matchpoint.renewals := cur_matchpoint.renewals;
4780         END IF;
4781         IF matchpoint.grace_period IS NULL THEN
4782             matchpoint.grace_period := cur_matchpoint.grace_period;
4783         END IF;
4784     END LOOP;
4785
4786     -- Check required fields
4787     IF matchpoint.circulate             IS NOT NULL AND
4788        matchpoint.duration_rule         IS NOT NULL AND
4789        matchpoint.recurring_fine_rule   IS NOT NULL AND
4790        matchpoint.max_fine_rule         IS NOT NULL THEN
4791         -- All there? We have a completed match.
4792         result.success := true;
4793     END IF;
4794
4795     -- Include the assembled matchpoint, even if it isn't complete
4796     result.matchpoint := matchpoint;
4797
4798     -- Include (for debugging) the full list of matching rows
4799     result.buildrows := row_list;
4800
4801     -- Hand the result back to caller
4802     RETURN result;
4803 END;
4804 $func$ LANGUAGE plpgsql;
4805
4806 CREATE OR REPLACE FUNCTION action.find_hold_matrix_matchpoint(pickup_ou integer, request_ou integer, match_item bigint, match_user integer, match_requestor integer)
4807   RETURNS integer AS
4808 $func$
4809 DECLARE
4810     requestor_object    actor.usr%ROWTYPE;
4811     user_object         actor.usr%ROWTYPE;
4812     item_object         asset.copy%ROWTYPE;
4813     item_cn_object      asset.call_number%ROWTYPE;
4814     rec_descriptor      metabib.rec_descriptor%ROWTYPE;
4815     matchpoint          config.hold_matrix_matchpoint%ROWTYPE;
4816     weights             config.hold_matrix_weights%ROWTYPE;
4817     denominator         NUMERIC(6,2);
4818 BEGIN
4819     SELECT INTO user_object         * FROM actor.usr                WHERE id = match_user;
4820     SELECT INTO requestor_object    * FROM actor.usr                WHERE id = match_requestor;
4821     SELECT INTO item_object         * FROM asset.copy               WHERE id = match_item;
4822     SELECT INTO item_cn_object      * FROM asset.call_number        WHERE id = item_object.call_number;
4823     SELECT INTO rec_descriptor      * FROM metabib.rec_descriptor   WHERE record = item_cn_object.record;
4824
4825     -- The item's owner should probably be the one determining if the item is holdable
4826     -- How to decide that is debatable. Decided to default to the circ library (where the item lives)
4827     -- This flag will allow for setting it to the owning library (where the call number "lives")
4828     PERFORM * FROM config.internal_flag WHERE name = 'circ.holds.weight_owner_not_circ' AND enabled;
4829
4830     -- Grab the closest set circ weight setting.
4831     IF NOT FOUND THEN
4832         -- Default to circ library
4833         SELECT INTO weights hw.*
4834           FROM config.weight_assoc wa
4835                JOIN config.hold_matrix_weights hw ON (hw.id = wa.hold_weights)
4836                JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) d ON (wa.org_unit = d.id)
4837           WHERE active
4838           ORDER BY d.distance
4839           LIMIT 1;
4840     ELSE
4841         -- Flag is set, use owning library
4842         SELECT INTO weights hw.*
4843           FROM config.weight_assoc wa
4844                JOIN config.hold_matrix_weights hw ON (hw.id = wa.hold_weights)
4845                JOIN actor.org_unit_ancestors_distance( cn_object.owning_lib ) d ON (wa.org_unit = d.id)
4846           WHERE active
4847           ORDER BY d.distance
4848           LIMIT 1;
4849     END IF;
4850
4851     -- No weights? Bad admin! Defaults to handle that anyway.
4852     IF weights.id IS NULL THEN
4853         weights.user_home_ou    := 5.0;
4854         weights.request_ou      := 5.0;
4855         weights.pickup_ou       := 5.0;
4856         weights.item_owning_ou  := 5.0;
4857         weights.item_circ_ou    := 5.0;
4858         weights.usr_grp         := 7.0;
4859         weights.requestor_grp   := 8.0;
4860         weights.circ_modifier   := 4.0;
4861         weights.marc_type       := 3.0;
4862         weights.marc_form       := 2.0;
4863         weights.marc_bib_level  := 1.0;
4864         weights.marc_vr_format  := 1.0;
4865         weights.juvenile_flag   := 4.0;
4866         weights.ref_flag        := 0.0;
4867     END IF;
4868
4869     -- Determine the max (expected) depth (+1) of the org tree and max depth of the permisson tree
4870     -- If you break your org tree with funky parenting this may be wrong
4871     -- 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
4872     -- We use one denominator for all tree-based checks for when permission groups and org units have the same weighting
4873     WITH all_distance(distance) AS (
4874             SELECT depth AS distance FROM actor.org_unit_type
4875         UNION
4876             SELECT distance AS distance FROM permission.grp_ancestors_distance((SELECT id FROM permission.grp_tree WHERE parent IS NULL))
4877         )
4878     SELECT INTO denominator MAX(distance) + 1 FROM all_distance;
4879
4880     -- To ATTEMPT to make this work like it used to, make it reverse the user/requestor profile ids.
4881     -- This may be better implemented as part of the upgrade script?
4882     -- Set usr_grp = requestor_grp, requestor_grp = 1 or something when this flag is already set
4883     -- Then remove this flag, of course.
4884     PERFORM * FROM config.internal_flag WHERE name = 'circ.holds.usr_not_requestor' AND enabled;
4885
4886     IF FOUND THEN
4887         -- Note: This, to me, is REALLY hacky. I put it in anyway.
4888         -- If you can't tell, this is a single call swap on two variables.
4889         SELECT INTO user_object.profile, requestor_object.profile
4890                     requestor_object.profile, user_object.profile;
4891     END IF;
4892
4893     -- Select the winning matchpoint into the matchpoint variable for returning
4894     SELECT INTO matchpoint m.*
4895       FROM  config.hold_matrix_matchpoint m
4896             /*LEFT*/ JOIN permission.grp_ancestors_distance( requestor_object.profile ) rpgad ON m.requestor_grp = rpgad.id
4897             LEFT JOIN permission.grp_ancestors_distance( user_object.profile ) upgad ON m.usr_grp = upgad.id
4898             LEFT JOIN actor.org_unit_ancestors_distance( pickup_ou ) puoua ON m.pickup_ou = puoua.id
4899             LEFT JOIN actor.org_unit_ancestors_distance( request_ou ) rqoua ON m.request_ou = rqoua.id
4900             LEFT JOIN actor.org_unit_ancestors_distance( item_cn_object.owning_lib ) cnoua ON m.item_owning_ou = cnoua.id
4901             LEFT JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) iooua ON m.item_circ_ou = iooua.id
4902             LEFT JOIN actor.org_unit_ancestors_distance( user_object.home_ou  ) uhoua ON m.user_home_ou = uhoua.id
4903       WHERE m.active
4904             -- Permission Groups
4905          -- AND (m.requestor_grp        IS NULL OR upgad.id IS NOT NULL) -- Optional Requestor Group?
4906             AND (m.usr_grp              IS NULL OR upgad.id IS NOT NULL)
4907             -- Org Units
4908             AND (m.pickup_ou            IS NULL OR (puoua.id IS NOT NULL AND (puoua.distance = 0 OR NOT m.strict_ou_match)))
4909             AND (m.request_ou           IS NULL OR (rqoua.id IS NOT NULL AND (rqoua.distance = 0 OR NOT m.strict_ou_match)))
4910             AND (m.item_owning_ou       IS NULL OR (cnoua.id IS NOT NULL AND (cnoua.distance = 0 OR NOT m.strict_ou_match)))
4911             AND (m.item_circ_ou         IS NULL OR (iooua.id IS NOT NULL AND (iooua.distance = 0 OR NOT m.strict_ou_match)))
4912             AND (m.user_home_ou         IS NULL OR (uhoua.id IS NOT NULL AND (uhoua.distance = 0 OR NOT m.strict_ou_match)))
4913             -- Static User Checks
4914             AND (m.juvenile_flag        IS NULL OR m.juvenile_flag = user_object.juvenile)
4915             -- Static Item Checks
4916             AND (m.circ_modifier        IS NULL OR m.circ_modifier = item_object.circ_modifier)
4917             AND (m.marc_type            IS NULL OR m.marc_type = COALESCE(item_object.circ_as_type, rec_descriptor.item_type))
4918             AND (m.marc_form            IS NULL OR m.marc_form = rec_descriptor.item_form)
4919             AND (m.marc_bib_level       IS NULL OR m.marc_bib_level = rec_descriptor.bib_level)
4920             AND (m.marc_vr_format       IS NULL OR m.marc_vr_format = rec_descriptor.vr_format)
4921             AND (m.ref_flag             IS NULL OR m.ref_flag = item_object.ref)
4922       ORDER BY
4923             -- Permission Groups
4924             CASE WHEN rpgad.distance    IS NOT NULL THEN 2^(2*weights.requestor_grp - (rpgad.distance/denominator)) ELSE 0.0 END +
4925             CASE WHEN upgad.distance    IS NOT NULL THEN 2^(2*weights.usr_grp - (upgad.distance/denominator)) ELSE 0.0 END +
4926             -- Org Units
4927             CASE WHEN puoua.distance    IS NOT NULL THEN 2^(2*weights.pickup_ou - (puoua.distance/denominator)) ELSE 0.0 END +
4928             CASE WHEN rqoua.distance    IS NOT NULL THEN 2^(2*weights.request_ou - (rqoua.distance/denominator)) ELSE 0.0 END +
4929             CASE WHEN cnoua.distance    IS NOT NULL THEN 2^(2*weights.item_owning_ou - (cnoua.distance/denominator)) ELSE 0.0 END +
4930             CASE WHEN iooua.distance    IS NOT NULL THEN 2^(2*weights.item_circ_ou - (iooua.distance/denominator)) ELSE 0.0 END +
4931             CASE WHEN uhoua.distance    IS NOT NULL THEN 2^(2*weights.user_home_ou - (uhoua.distance/denominator)) ELSE 0.0 END +
4932             -- Static User Checks       -- Note: 4^x is equiv to 2^(2*x)
4933             CASE WHEN m.juvenile_flag   IS NOT NULL THEN 4^weights.juvenile_flag ELSE 0.0 END +
4934             -- Static Item Checks
4935             CASE WHEN m.circ_modifier   IS NOT NULL THEN 4^weights.circ_modifier ELSE 0.0 END +
4936             CASE WHEN m.marc_type       IS NOT NULL THEN 4^weights.marc_type ELSE 0.0 END +
4937             CASE WHEN m.marc_form       IS NOT NULL THEN 4^weights.marc_form ELSE 0.0 END +
4938             CASE WHEN m.marc_vr_format  IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0.0 END +
4939             CASE WHEN m.ref_flag        IS NOT NULL THEN 4^weights.ref_flag ELSE 0.0 END DESC,
4940             -- Final sort on id, so that if two rules have the same sorting in the previous sort they have a defined order
4941             -- This prevents "we changed the table order by updating a rule, and we started getting different results"
4942             m.id;
4943
4944     -- Return just the ID for now
4945     RETURN matchpoint.id;
4946 END;
4947 $func$ LANGUAGE 'plpgsql';
4948
4949 -- 0528
4950 CREATE OR REPLACE FUNCTION maintain_control_numbers() RETURNS TRIGGER AS $func$
4951 use strict;
4952 use MARC::Record;
4953 use MARC::File::XML (BinaryEncoding => 'UTF-8');
4954 use MARC::Charset;
4955 use Encode;
4956 use Unicode::Normalize;
4957
4958 MARC::Charset->assume_unicode(1);
4959
4960 my $record = MARC::Record->new_from_xml($_TD->{new}{marc});
4961 my $schema = $_TD->{table_schema};
4962 my $rec_id = $_TD->{new}{id};
4963
4964 # Short-circuit if maintaining control numbers per MARC21 spec is not enabled
4965 my $enable = spi_exec_query("SELECT enabled FROM config.global_flag WHERE name = 'cat.maintain_control_numbers'");
4966 if (!($enable->{processed}) or $enable->{rows}[0]->{enabled} eq 'f') {
4967     return;
4968 }
4969
4970 # Get the control number identifier from an OU setting based on $_TD->{new}{owner}
4971 my $ou_cni = 'EVRGRN';
4972
4973 my $owner;
4974 if ($schema eq 'serial') {
4975     $owner = $_TD->{new}{owning_lib};
4976 } else {
4977     # are.owner and bre.owner can be null, so fall back to the consortial setting
4978     $owner = $_TD->{new}{owner} || 1;
4979 }
4980
4981 my $ous_rv = spi_exec_query("SELECT value FROM actor.org_unit_ancestor_setting('cat.marc_control_number_identifier', $owner)");
4982 if ($ous_rv->{processed}) {
4983     $ou_cni = $ous_rv->{rows}[0]->{value};
4984     $ou_cni =~ s/"//g; # Stupid VIM syntax highlighting"
4985 } else {
4986     # Fall back to the shortname of the OU if there was no OU setting
4987     $ous_rv = spi_exec_query("SELECT shortname FROM actor.org_unit WHERE id = $owner");
4988     if ($ous_rv->{processed}) {
4989         $ou_cni = $ous_rv->{rows}[0]->{shortname};
4990     }
4991 }
4992
4993 my ($create, $munge) = (0, 0);
4994
4995 my @scns = $record->field('035');
4996
4997 foreach my $id_field ('001', '003') {
4998     my $spec_value;
4999     my @controls = $record->field($id_field);
5000
5001     if ($id_field eq '001') {
5002         $spec_value = $rec_id;
5003     } else {
5004         $spec_value = $ou_cni;
5005     }
5006
5007     # Create the 001/003 if none exist
5008     if (scalar(@controls) == 1) {
5009         # Only one field; check to see if we need to munge it
5010         unless (grep $_->data() eq $spec_value, @controls) {
5011             $munge = 1;
5012         }
5013     } else {
5014         # Delete the other fields, as with more than 1 001/003 we do not know which 003/001 to match
5015         foreach my $control (@controls) {
5016             unless ($control->data() eq $spec_value) {
5017                 $record->delete_field($control);
5018             }
5019         }
5020         $record->insert_fields_ordered(MARC::Field->new($id_field, $spec_value));
5021         $create = 1;
5022     }
5023 }
5024
5025 # Now, if we need to munge the 001, we will first push the existing 001/003
5026 # into the 035; but if the record did not have one (and one only) 001 and 003
5027 # to begin with, skip this process
5028 if ($munge and not $create) {
5029     my $scn = "(" . $record->field('003')->data() . ")" . $record->field('001')->data();
5030
5031     # Do not create duplicate 035 fields
5032     unless (grep $_->subfield('a') eq $scn, @scns) {
5033         $record->insert_fields_ordered(MARC::Field->new('035', '', '', 'a' => $scn));
5034     }
5035 }
5036
5037 # Set the 001/003 and update the MARC
5038 if ($create or $munge) {
5039     $record->field('001')->data($rec_id);
5040     $record->field('003')->data($ou_cni);
5041
5042     my $xml = $record->as_xml_record();
5043     $xml =~ s/\n//sgo;
5044     $xml =~ s/^<\?xml.+\?\s*>//go;
5045     $xml =~ s/>\s+</></go;
5046     $xml =~ s/\p{Cc}//go;
5047
5048     # Embed a version of OpenILS::Application::AppUtils->entityize()
5049     # to avoid having to set PERL5LIB for PostgreSQL as well
5050
5051     # If we are going to convert non-ASCII characters to XML entities,
5052     # we had better be dealing with a UTF8 string to begin with
5053     $xml = decode_utf8($xml);
5054
5055     $xml = NFC($xml);
5056
5057     # Convert raw ampersands to entities
5058     $xml =~ s/&(?!\S+;)/&amp;/gso;
5059
5060     # Convert Unicode characters to entities
5061     $xml =~ s/([\x{0080}-\x{fffd}])/sprintf('&#x%X;',ord($1))/sgoe;
5062
5063     $xml =~ s/[\x00-\x1f]//go;
5064     $_TD->{new}{marc} = $xml;
5065
5066     return "MODIFY";
5067 }
5068
5069 return;
5070 $func$ LANGUAGE PLPERLU;
5071
5072 CREATE OR REPLACE FUNCTION authority.generate_overlay_template ( TEXT, BIGINT ) RETURNS TEXT AS $func$
5073
5074     use MARC::Record;
5075     use MARC::File::XML (BinaryEncoding => 'UTF-8');
5076     use MARC::Charset;
5077
5078     MARC::Charset->assume_unicode(1);
5079
5080     my $xml = shift;
5081     my $r = MARC::Record->new_from_xml( $xml );
5082
5083     return undef unless ($r);
5084
5085     my $id = shift() || $r->subfield( '901' => 'c' );
5086     $id =~ s/^\s*(?:\([^)]+\))?\s*(.+)\s*?$/$1/;
5087     return undef unless ($id); # We need an ID!
5088
5089     my $tmpl = MARC::Record->new();
5090     $tmpl->encoding( 'UTF-8' );
5091
5092     my @rule_fields;
5093     for my $field ( $r->field( '1..' ) ) { # Get main entry fields from the authority record
5094
5095         my $tag = $field->tag;
5096         my $i1 = $field->indicator(1);
5097         my $i2 = $field->indicator(2);
5098         my $sf = join '', map { $_->[0] } $field->subfields;
5099         my @data = map { @$_ } $field->subfields;
5100
5101         my @replace_them;
5102
5103         # Map the authority field to bib fields it can control.
5104         if ($tag >= 100 and $tag <= 111) {       # names
5105             @replace_them = map { $tag + $_ } (0, 300, 500, 600, 700);
5106         } elsif ($tag eq '130') {                # uniform title
5107             @replace_them = qw/130 240 440 730 830/;
5108         } elsif ($tag >= 150 and $tag <= 155) {  # subjects
5109             @replace_them = ($tag + 500);
5110         } elsif ($tag >= 180 and $tag <= 185) {  # floating subdivisions
5111             @replace_them = qw/100 400 600 700 800 110 410 610 710 810 111 411 611 711 811 130 240 440 730 830 650 651 655/;
5112         } else {
5113             next;
5114         }
5115
5116         # Dummy up the bib-side data
5117         $tmpl->append_fields(
5118             map {
5119                 MARC::Field->new( $_, $i1, $i2, @data )
5120             } @replace_them
5121         );
5122
5123         # Construct some 'replace' rules
5124         push @rule_fields, map { $_ . $sf . '[0~\)' .$id . '$]' } @replace_them;
5125     }
5126
5127     # Insert the replace rules into the template
5128     $tmpl->append_fields(
5129         MARC::Field->new( '905' => ' ' => ' ' => 'r' => join(',', @rule_fields ) )
5130     );
5131
5132     $xml = $tmpl->as_xml_record;
5133     $xml =~ s/^<\?.+?\?>$//mo;
5134     $xml =~ s/\n//sgo;
5135     $xml =~ s/>\s+</></sgo;
5136
5137     return $xml;
5138
5139 $func$ LANGUAGE PLPERLU;
5140
5141 CREATE OR REPLACE FUNCTION vandelay.add_field ( target_xml TEXT, source_xml TEXT, field TEXT, force_add INT ) RETURNS TEXT AS $_$
5142
5143     use MARC::Record;
5144     use MARC::File::XML (BinaryEncoding => 'UTF-8');
5145     use MARC::Charset;
5146     use strict;
5147
5148     MARC::Charset->assume_unicode(1);
5149
5150     my $target_xml = shift;
5151     my $source_xml = shift;
5152     my $field_spec = shift;
5153     my $force_add = shift || 0;
5154
5155     my $target_r = MARC::Record->new_from_xml( $target_xml );
5156     my $source_r = MARC::Record->new_from_xml( $source_xml );
5157
5158     return $target_xml unless ($target_r && $source_r);
5159
5160     my @field_list = split(',', $field_spec);
5161
5162     my %fields;
5163     for my $f (@field_list) {
5164         $f =~ s/^\s*//; $f =~ s/\s*$//;
5165         if ($f =~ /^(.{3})(\w*)(?:\[([^]]*)\])?$/) {
5166             my $field = $1;
5167             $field =~ s/\s+//;
5168             my $sf = $2;
5169             $sf =~ s/\s+//;
5170             my $match = $3;
5171             $match =~ s/^\s*//; $match =~ s/\s*$//;
5172             $fields{$field} = { sf => [ split('', $sf) ] };
5173             if ($match) {
5174                 my ($msf,$mre) = split('~', $match);
5175                 if (length($msf) > 0 and length($mre) > 0) {
5176                     $msf =~ s/^\s*//; $msf =~ s/\s*$//;
5177                     $mre =~ s/^\s*//; $mre =~ s/\s*$//;
5178                     $fields{$field}{match} = { sf => $msf, re => qr/$mre/ };
5179                 }
5180             }
5181         }
5182     }
5183
5184     for my $f ( keys %fields) {
5185         if ( @{$fields{$f}{sf}} ) {
5186             for my $from_field ($source_r->field( $f )) {
5187                 my @tos = $target_r->field( $f );
5188                 if (!@tos) {
5189                     next if (exists($fields{$f}{match}) and !$force_add);
5190                     my @new_fields = map { $_->clone } $source_r->field( $f );
5191                     $target_r->insert_fields_ordered( @new_fields );
5192                 } else {
5193                     for my $to_field (@tos) {
5194                         if (exists($fields{$f}{match})) {
5195                             next unless (grep { $_ =~ $fields{$f}{match}{re} } $to_field->subfield($fields{$f}{match}{sf}));
5196                         }
5197                         my @new_sf = map { ($_ => $from_field->subfield($_)) } @{$fields{$f}{sf}};
5198                         $to_field->add_subfields( @new_sf );
5199                     }
5200                 }
5201             }
5202         } else {
5203             my @new_fields = map { $_->clone } $source_r->field( $f );
5204             $target_r->insert_fields_ordered( @new_fields );
5205         }
5206     }
5207
5208     $target_xml = $target_r->as_xml_record;
5209     $target_xml =~ s/^<\?.+?\?>$//mo;
5210     $target_xml =~ s/\n//sgo;
5211     $target_xml =~ s/>\s+</></sgo;
5212
5213     return $target_xml;
5214
5215 $_$ LANGUAGE PLPERLU;
5216
5217 CREATE OR REPLACE FUNCTION authority.normalize_heading( TEXT ) RETURNS TEXT AS $func$
5218     use strict;
5219     use warnings;
5220
5221     use utf8;
5222     use MARC::Record;
5223     use MARC::File::XML (BinaryEncoding => 'UTF8');
5224     use MARC::Charset;
5225     use UUID::Tiny ':std';
5226
5227     MARC::Charset->assume_unicode(1);
5228
5229     my $xml = shift() or return undef;
5230
5231     my $r;
5232
5233     # Prevent errors in XML parsing from blowing out ungracefully
5234     eval {
5235         $r = MARC::Record->new_from_xml( $xml );
5236         1;
5237     } or do {
5238        return 'BAD_MARCXML_' . create_uuid_as_string(UUID_MD5, $xml);
5239     };
5240
5241     if (!$r) {
5242        return 'BAD_MARCXML_' . create_uuid_as_string(UUID_MD5, $xml);
5243     }
5244
5245     # From http://www.loc.gov/standards/sourcelist/subject.html
5246     my $thes_code_map = {
5247         a => 'lcsh',
5248         b => 'lcshac',
5249         c => 'mesh',
5250         d => 'nal',
5251         k => 'cash',
5252         n => 'notapplicable',
5253         r => 'aat',
5254         s => 'sears',
5255         v => 'rvm',
5256     };
5257
5258     # Default to "No attempt to code" if the leader is horribly broken
5259     my $fixed_field = $r->field('008');
5260     my $thes_char = '|';
5261     if ($fixed_field) { 
5262         $thes_char = substr($fixed_field->data(), 11, 1) || '|';
5263     }
5264
5265     my $thes_code = 'UNDEFINED';
5266
5267     if ($thes_char eq 'z') {
5268         # Grab the 040 $f per http://www.loc.gov/marc/authority/ad040.html
5269         $thes_code = $r->subfield('040', 'f') || 'UNDEFINED';
5270     } elsif ($thes_code_map->{$thes_char}) {
5271         $thes_code = $thes_code_map->{$thes_char};
5272     }
5273
5274     my $auth_txt = '';
5275     my $head = $r->field('1..');
5276     if ($head) {
5277         # Concatenate all of these subfields together, prefixed by their code
5278         # to prevent collisions along the lines of "Fiction, North Carolina"
5279         foreach my $sf ($head->subfields()) {
5280             $auth_txt .= '‡' . $sf->[0] . ' ' . $sf->[1];
5281         }
5282     }
5283     
5284     if ($auth_txt) {
5285         my $stmt = spi_prepare('SELECT public.naco_normalize($1) AS norm_text', 'TEXT');
5286         my $result = spi_exec_prepared($stmt, $auth_txt);
5287         my $norm_txt = $result->{rows}[0]->{norm_text};
5288         spi_freeplan($stmt);
5289         undef($stmt);
5290         return $head->tag() . "_" . $thes_code . " " . $norm_txt;
5291     }
5292
5293     return 'NOHEADING_' . $thes_code . ' ' . create_uuid_as_string(UUID_MD5, $xml);
5294 $func$ LANGUAGE 'plperlu' IMMUTABLE;
5295
5296 CREATE OR REPLACE FUNCTION vandelay.strip_field ( xml TEXT, field TEXT ) RETURNS TEXT AS $_$
5297
5298     use MARC::Record;
5299     use MARC::File::XML (BinaryEncoding => 'UTF-8');
5300     use MARC::Charset;
5301     use strict;
5302
5303     MARC::Charset->assume_unicode(1);
5304
5305     my $xml = shift;
5306     my $r = MARC::Record->new_from_xml( $xml );
5307
5308     return $xml unless ($r);
5309
5310     my $field_spec = shift;
5311     my @field_list = split(',', $field_spec);
5312
5313     my %fields;
5314     for my $f (@field_list) {
5315         $f =~ s/^\s*//; $f =~ s/\s*$//;
5316         if ($f =~ /^(.{3})(\w*)(?:\[([^]]*)\])?$/) {
5317             my $field = $1;
5318             $field =~ s/\s+//;
5319             my $sf = $2;
5320             $sf =~ s/\s+//;
5321             my $match = $3;
5322             $match =~ s/^\s*//; $match =~ s/\s*$//;
5323             $fields{$field} = { sf => [ split('', $sf) ] };
5324             if ($match) {
5325                 my ($msf,$mre) = split('~', $match);
5326                 if (length($msf) > 0 and length($mre) > 0) {
5327                     $msf =~ s/^\s*//; $msf =~ s/\s*$//;
5328                     $mre =~ s/^\s*//; $mre =~ s/\s*$//;
5329                     $fields{$field}{match} = { sf => $msf, re => qr/$mre/ };
5330                 }
5331             }
5332         }
5333     }
5334
5335     for my $f ( keys %fields) {
5336         for my $to_field ($r->field( $f )) {
5337             if (exists($fields{$f}{match})) {
5338                 next unless (grep { $_ =~ $fields{$f}{match}{re} } $to_field->subfield($fields{$f}{match}{sf}));
5339             }
5340
5341             if ( @{$fields{$f}{sf}} ) {
5342                 $to_field->delete_subfield(code => $fields{$f}{sf});
5343             } else {
5344                 $r->delete_field( $to_field );
5345             }
5346         }
5347     }
5348
5349     $xml = $r->as_xml_record;
5350     $xml =~ s/^<\?.+?\?>$//mo;
5351     $xml =~ s/\n//sgo;
5352     $xml =~ s/>\s+</></sgo;
5353
5354     return $xml;
5355
5356 $_$ LANGUAGE PLPERLU;
5357
5358 CREATE OR REPLACE FUNCTION biblio.flatten_marc ( TEXT ) RETURNS SETOF metabib.full_rec AS $func$
5359
5360 use MARC::Record;
5361 use MARC::File::XML (BinaryEncoding => 'UTF-8');
5362 use MARC::Charset;
5363
5364 MARC::Charset->assume_unicode(1);
5365
5366 my $xml = shift;
5367 my $r = MARC::Record->new_from_xml( $xml );
5368
5369 return_next( { tag => 'LDR', value => $r->leader } );
5370
5371 for my $f ( $r->fields ) {
5372         if ($f->is_control_field) {
5373                 return_next({ tag => $f->tag, value => $f->data });
5374         } else {
5375                 for my $s ($f->subfields) {
5376                         return_next({
5377                                 tag      => $f->tag,
5378                                 ind1     => $f->indicator(1),
5379                                 ind2     => $f->indicator(2),
5380                                 subfield => $s->[0],
5381                                 value    => $s->[1]
5382                         });
5383
5384                         if ( $f->tag eq '245' and $s->[0] eq 'a' ) {
5385                                 my $trim = $f->indicator(2) || 0;
5386                                 return_next({
5387                                         tag      => 'tnf',
5388                                         ind1     => $f->indicator(1),
5389                                         ind2     => $f->indicator(2),
5390                                         subfield => 'a',
5391                                         value    => substr( $s->[1], $trim )
5392                                 });
5393                         }
5394                 }
5395         }
5396 }
5397
5398 return undef;
5399
5400 $func$ LANGUAGE PLPERLU;
5401
5402 CREATE OR REPLACE FUNCTION authority.flatten_marc ( TEXT ) RETURNS SETOF authority.full_rec AS $func$
5403
5404 use MARC::Record;
5405 use MARC::File::XML (BinaryEncoding => 'UTF-8');
5406 use MARC::Charset;
5407
5408 MARC::Charset->assume_unicode(1);
5409
5410 my $xml = shift;
5411 my $r = MARC::Record->new_from_xml( $xml );
5412
5413 return_next( { tag => 'LDR', value => $r->leader } );
5414
5415 for my $f ( $r->fields ) {
5416     if ($f->is_control_field) {
5417         return_next({ tag => $f->tag, value => $f->data });
5418     } else {
5419         for my $s ($f->subfields) {
5420             return_next({
5421                 tag      => $f->tag,
5422                 ind1     => $f->indicator(1),
5423                 ind2     => $f->indicator(2),
5424                 subfield => $s->[0],
5425                 value    => $s->[1]
5426             });
5427
5428         }
5429     }
5430 }
5431
5432 return undef;
5433
5434 $func$ LANGUAGE PLPERLU;
5435
5436 -- 0530
5437 CREATE INDEX actor_usr_day_phone_idx_numeric ON actor.usr USING BTREE 
5438     (evergreen.lowercase(REGEXP_REPLACE(day_phone, '[^0-9]', '', 'g')));
5439
5440 CREATE INDEX actor_usr_evening_phone_idx_numeric ON actor.usr USING BTREE 
5441     (evergreen.lowercase(REGEXP_REPLACE(evening_phone, '[^0-9]', '', 'g')));
5442
5443 CREATE INDEX actor_usr_other_phone_idx_numeric ON actor.usr USING BTREE 
5444     (evergreen.lowercase(REGEXP_REPLACE(other_phone, '[^0-9]', '', 'g')));
5445
5446 -- 0533
5447 CREATE OR REPLACE FUNCTION action.age_circ_on_delete () RETURNS TRIGGER AS $$
5448 DECLARE
5449 found char := 'N';
5450 BEGIN
5451
5452     -- If there are any renewals for this circulation, don't archive or delete
5453     -- it yet.   We'll do so later, when we archive and delete the renewals.
5454
5455     SELECT 'Y' INTO found
5456     FROM action.circulation
5457     WHERE parent_circ = OLD.id
5458     LIMIT 1;
5459
5460     IF found = 'Y' THEN
5461         RETURN NULL;  -- don't delete
5462         END IF;
5463
5464     -- Archive a copy of the old row to action.aged_circulation
5465
5466     INSERT INTO action.aged_circulation
5467         (id,usr_post_code, usr_home_ou, usr_profile, usr_birth_year, copy_call_number, copy_location,
5468         copy_owning_lib, copy_circ_lib, copy_bib_record, xact_start, xact_finish, target_copy,
5469         circ_lib, circ_staff, checkin_staff, checkin_lib, renewal_remaining, grace_period, due_date,
5470         stop_fines_time, checkin_time, create_time, duration, fine_interval, recurring_fine,
5471         max_fine, phone_renewal, desk_renewal, opac_renewal, duration_rule, recurring_fine_rule,
5472         max_fine_rule, stop_fines, workstation, checkin_workstation, checkin_scan_time, parent_circ)
5473       SELECT
5474         id,usr_post_code, usr_home_ou, usr_profile, usr_birth_year, copy_call_number, copy_location,
5475         copy_owning_lib, copy_circ_lib, copy_bib_record, xact_start, xact_finish, target_copy,
5476         circ_lib, circ_staff, checkin_staff, checkin_lib, renewal_remaining, grace_period, due_date,
5477         stop_fines_time, checkin_time, create_time, duration, fine_interval, recurring_fine,
5478         max_fine, phone_renewal, desk_renewal, opac_renewal, duration_rule, recurring_fine_rule,
5479         max_fine_rule, stop_fines, workstation, checkin_workstation, checkin_scan_time, parent_circ
5480         FROM action.all_circulation WHERE id = OLD.id;
5481
5482     RETURN OLD;
5483 END;
5484 $$ LANGUAGE 'plpgsql';
5485
5486 -- 0534
5487 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$
5488 DECLARE
5489     matchpoint_id        INT;
5490     user_object        actor.usr%ROWTYPE;
5491     age_protect_object    config.rule_age_hold_protect%ROWTYPE;
5492     standing_penalty    config.standing_penalty%ROWTYPE;
5493     transit_range_ou_type    actor.org_unit_type%ROWTYPE;
5494     transit_source        actor.org_unit%ROWTYPE;
5495     item_object        asset.copy%ROWTYPE;
5496     item_cn_object     asset.call_number%ROWTYPE;
5497     ou_skip              actor.org_unit_setting%ROWTYPE;
5498     result            action.matrix_test_result;
5499     hold_test        config.hold_matrix_matchpoint%ROWTYPE;
5500     hold_count        INT;
5501     hold_transit_prox    INT;
5502     frozen_hold_count    INT;
5503     context_org_list    INT[];
5504     done            BOOL := FALSE;
5505 BEGIN
5506     SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
5507     SELECT INTO context_org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( pickup_ou );
5508
5509     result.success := TRUE;
5510
5511     -- Fail if we couldn't find a user
5512     IF user_object.id IS NULL THEN
5513         result.fail_part := 'no_user';
5514         result.success := FALSE;
5515         done := TRUE;
5516         RETURN NEXT result;
5517         RETURN;
5518     END IF;
5519
5520     SELECT INTO item_object * FROM asset.copy WHERE id = match_item;
5521
5522     -- Fail if we couldn't find a copy
5523     IF item_object.id IS NULL THEN
5524         result.fail_part := 'no_item';
5525         result.success := FALSE;
5526         done := TRUE;
5527         RETURN NEXT result;
5528         RETURN;
5529     END IF;
5530
5531     SELECT INTO matchpoint_id action.find_hold_matrix_matchpoint(pickup_ou, request_ou, match_item, match_user, match_requestor);
5532     result.matchpoint := matchpoint_id;
5533
5534     SELECT INTO ou_skip * FROM actor.org_unit_setting WHERE name = 'circ.holds.target_skip_me' AND org_unit = item_object.circ_lib;
5535
5536     -- Fail if the circ_lib for the item has circ.holds.target_skip_me set to true
5537     IF ou_skip.id IS NOT NULL AND ou_skip.value = 'true' THEN
5538         result.fail_part := 'circ.holds.target_skip_me';
5539         result.success := FALSE;
5540         done := TRUE;
5541         RETURN NEXT result;
5542         RETURN;
5543     END IF;
5544
5545     -- Fail if user is barred
5546     IF user_object.barred IS TRUE THEN
5547         result.fail_part := 'actor.usr.barred';
5548         result.success := FALSE;
5549         done := TRUE;
5550         RETURN NEXT result;
5551         RETURN;
5552     END IF;
5553
5554     -- Fail if we couldn't find any matchpoint (requires a default)
5555     IF matchpoint_id IS NULL THEN
5556         result.fail_part := 'no_matchpoint';
5557         result.success := FALSE;
5558         done := TRUE;
5559         RETURN NEXT result;
5560         RETURN;
5561     END IF;
5562
5563     SELECT INTO hold_test * FROM config.hold_matrix_matchpoint WHERE id = matchpoint_id;
5564
5565     IF hold_test.holdable IS FALSE THEN
5566         result.fail_part := 'config.hold_matrix_test.holdable';
5567         result.success := FALSE;
5568         done := TRUE;
5569         RETURN NEXT result;
5570     END IF;
5571
5572     IF hold_test.transit_range IS NOT NULL THEN
5573         SELECT INTO transit_range_ou_type * FROM actor.org_unit_type WHERE id = hold_test.transit_range;
5574         IF hold_test.distance_is_from_owner THEN
5575             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;
5576         ELSE
5577             SELECT INTO transit_source * FROM actor.org_unit WHERE id = item_object.circ_lib;
5578         END IF;
5579
5580         PERFORM * FROM actor.org_unit_descendants( transit_source.id, transit_range_ou_type.depth ) WHERE id = pickup_ou;
5581
5582         IF NOT FOUND THEN
5583             result.fail_part := 'transit_range';
5584             result.success := FALSE;
5585             done := TRUE;
5586             RETURN NEXT result;
5587         END IF;
5588     END IF;
5589  
5590     FOR standing_penalty IN
5591         SELECT  DISTINCT csp.*
5592           FROM  actor.usr_standing_penalty usp
5593                 JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
5594           WHERE usr = match_user
5595                 AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
5596                 AND (usp.stop_date IS NULL or usp.stop_date > NOW())
5597                 AND csp.block_list LIKE '%HOLD%' LOOP
5598
5599         result.fail_part := standing_penalty.name;
5600         result.success := FALSE;
5601         done := TRUE;
5602         RETURN NEXT result;
5603     END LOOP;
5604
5605     IF hold_test.stop_blocked_user IS TRUE THEN
5606         FOR standing_penalty IN
5607             SELECT  DISTINCT csp.*
5608               FROM  actor.usr_standing_penalty usp
5609                     JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
5610               WHERE usr = match_user
5611                     AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
5612                     AND (usp.stop_date IS NULL or usp.stop_date > NOW())
5613                     AND csp.block_list LIKE '%CIRC%' LOOP
5614     
5615             result.fail_part := standing_penalty.name;
5616             result.success := FALSE;
5617             done := TRUE;
5618             RETURN NEXT result;
5619         END LOOP;
5620     END IF;
5621
5622     IF hold_test.max_holds IS NOT NULL AND NOT retargetting THEN
5623         SELECT    INTO hold_count COUNT(*)
5624           FROM    action.hold_request
5625           WHERE    usr = match_user
5626             AND fulfillment_time IS NULL
5627             AND cancel_time IS NULL
5628             AND CASE WHEN hold_test.include_frozen_holds THEN TRUE ELSE frozen IS FALSE END;
5629
5630         IF hold_count >= hold_test.max_holds THEN
5631             result.fail_part := 'config.hold_matrix_test.max_holds';
5632             result.success := FALSE;
5633             done := TRUE;
5634             RETURN NEXT result;
5635         END IF;
5636     END IF;
5637
5638     IF item_object.age_protect IS NOT NULL THEN
5639         SELECT INTO age_protect_object * FROM config.rule_age_hold_protect WHERE id = item_object.age_protect;
5640
5641         IF item_object.create_date + age_protect_object.age > NOW() THEN
5642             IF hold_test.distance_is_from_owner THEN
5643                 SELECT INTO item_cn_object * FROM asset.call_number WHERE id = item_object.call_number;
5644                 SELECT INTO hold_transit_prox prox FROM actor.org_unit_proximity WHERE from_org = item_cn_object.owning_lib AND to_org = pickup_ou;
5645             ELSE
5646                 SELECT INTO hold_transit_prox prox FROM actor.org_unit_proximity WHERE from_org = item_object.circ_lib AND to_org = pickup_ou;
5647             END IF;
5648
5649             IF hold_transit_prox > age_protect_object.prox THEN
5650                 result.fail_part := 'config.rule_age_hold_protect.prox';
5651                 result.success := FALSE;
5652                 done := TRUE;
5653                 RETURN NEXT result;
5654             END IF;
5655         END IF;
5656     END IF;
5657
5658     IF NOT done THEN
5659         RETURN NEXT result;
5660     END IF;
5661
5662     RETURN;
5663 END;
5664 $func$ LANGUAGE plpgsql;
5665
5666 -- do potentially large updates last to save time if upgrader needs
5667 -- to manually tweak the upgrade script to resolve errors
5668
5669 -- 0505
5670 UPDATE metabib.facet_entry SET value = evergreen.force_unicode_normal_form(value,'NFC');
5671
5672 -- Update reporter.materialized_simple_record with normalized ISBN values
5673 -- This might not get all of them, but most ISBNs will have more than one hyphen
5674 DELETE FROM reporter.materialized_simple_record WHERE id IN (
5675     SELECT record FROM metabib.full_rec WHERE tag = '020' AND subfield IN ('a', 'z') AND value LIKE '%-%-%'
5676 );
5677
5678 INSERT INTO reporter.materialized_simple_record
5679     SELECT DISTINCT rossr.* FROM reporter.old_super_simple_record rossr INNER JOIN metabib.full_rec mfr ON mfr.record = rossr.id
5680         WHERE mfr.tag = '020' AND mfr.subfield IN ('a', 'z') AND mfr.value LIKE '%-%-%'
5681 ;
5682
5683 INSERT INTO config.upgrade_log (version) VALUES ('0542'); -- phasefx
5684
5685 INSERT INTO permission.perm_list
5686     SELECT  np.*
5687       FROM  (VALUES
5688                 (485, 'CREATE_VOLUME_SUFFIX', oils_i18n_gettext(485, 'Create suffix label definition.', 'ppl', 'description'))
5689                 ,(486, 'UPDATE_VOLUME_SUFFIX', oils_i18n_gettext(486, 'Update suffix label definition.', 'ppl', 'description'))
5690                 ,(487, 'DELETE_VOLUME_SUFFIX', oils_i18n_gettext(487, 'Delete suffix label definition.', 'ppl', 'description'))
5691                 ,(488, 'CREATE_VOLUME_PREFIX', oils_i18n_gettext(488, 'Create prefix label definition.', 'ppl', 'description'))
5692                 ,(489, 'UPDATE_VOLUME_PREFIX', oils_i18n_gettext(489, 'Update prefix label definition.', 'ppl', 'description'))
5693                 ,(490, 'DELETE_VOLUME_PREFIX', oils_i18n_gettext(490, 'Delete prefix label definition.', 'ppl', 'description'))
5694                 ,(491, 'CREATE_MONOGRAPH_PART', oils_i18n_gettext(491, 'Create monograph part definition.', 'ppl', 'description'))
5695                 ,(492, 'UPDATE_MONOGRAPH_PART', oils_i18n_gettext(492, 'Update monograph part definition.', 'ppl', 'description'))
5696                 ,(493, 'DELETE_MONOGRAPH_PART', oils_i18n_gettext(493, 'Delete monograph part definition.', 'ppl', 'description'))
5697                 ,(494, 'ADMIN_CODED_VALUE', oils_i18n_gettext(494, 'Create/Update/Delete SVF Record Attribute Coded Value Map', 'ppl', 'description'))
5698                 ,(495, 'ADMIN_SERIAL_ITEM', oils_i18n_gettext(495, 'Create/Retrieve/Update/Delete Serial Item', 'ppl', 'description'))
5699                 ,(496, 'ADMIN_SVF', oils_i18n_gettext(496, 'Create/Update/Delete SVF Record Attribute Defintion', 'ppl', 'description'))
5700                 ,(497, 'CREATE_BIB_PTYPE', oils_i18n_gettext(497, 'Create Bibliographic Record Peer Type', 'ppl', 'description'))
5701                 ,(498, 'CREATE_PURCHASE_REQUEST', oils_i18n_gettext(498, 'Create User Purchase Request', 'ppl', 'description'))
5702                 ,(499, 'DELETE_BIB_PTYPE', oils_i18n_gettext(499, 'Delete Bibliographic Record Peer Type', 'ppl', 'description'))
5703                 ,(500, 'MAP_MONOGRAPH_PART', oils_i18n_gettext(500, 'Create/Update/Delete Copy Monograph Part Map', 'ppl', 'description'))
5704                 ,(501, 'MARK_ITEM_MISSING_PIECES', oils_i18n_gettext(501, 'Allows the Mark Item Missing Pieces action.', 'ppl', 'description'))
5705                 ,(502, 'UPDATE_BIB_PTYPE', oils_i18n_gettext(502, 'Update Bibliographic Record Peer Type', 'ppl', 'description'))
5706                 ,(503, 'UPDATE_HOLD_REQUEST_TIME', oils_i18n_gettext(503, 'Allows editing of a hold''s request time, and/or its Cut-in-line/Top-of-queue flag.', 'ppl', 'description'))
5707                 ,(504, 'UPDATE_PICKLIST', oils_i18n_gettext(504, 'Allows update/re-use of an acquisitions pick/selection list.', 'ppl', 'description'))
5708                 ,(505, 'UPDATE_WORKSTATION', oils_i18n_gettext(505, 'Allows update of a workstation during workstation registration override.', 'ppl', 'description'))
5709                 ,(506, 'VIEW_USER_SETTING_TYPE', oils_i18n_gettext(506, 'Allows viewing of configurable user setting types.', 'ppl', 'description'))
5710             ) AS np(id,code,description)
5711             LEFT JOIN permission.perm_list pl USING (code)
5712       WHERE pl.id IS NULL;
5713 ;
5714
5715
5716 -- add new perms AND catch up on some missed upgrade data, if needed
5717
5718 -- we could get away from these fixed-id inserts here, but then this
5719 -- upgrade would be ahead of the mainline, I think
5720
5721 INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm)
5722         SELECT 8, oils_i18n_gettext(8, 'Cataloging Administrator', 'pgt', 'name'), 3, NULL, '3 years', TRUE, 'group_application.user.staff.cat_admin'
5723         WHERE NOT EXISTS (
5724                 SELECT 1
5725                 FROM permission.grp_tree
5726                 WHERE
5727                         id = 8
5728         );
5729
5730 INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm)
5731         SELECT 9, oils_i18n_gettext(9, 'Circulation Administrator', 'pgt', 'name'), 3, NULL, '3 years', TRUE, 'group_application.user.staff.circ_admin'
5732         WHERE NOT EXISTS (
5733                 SELECT 1
5734                 FROM permission.grp_tree
5735                 WHERE
5736                         id = 9
5737         );
5738
5739 UPDATE permission.grp_tree SET description = oils_i18n_gettext(10, 'Can do anything at the Branch level', 'pgt', 'description') WHERE id = 10;
5740
5741 INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm)
5742         SELECT 11, oils_i18n_gettext(11, 'Serials', 'pgt', 'name'), 3, oils_i18n_gettext(11, 'Serials (includes admin features)', 'pgt', 'description'), '3 years', TRUE, 'group_application.user.staff.serials'
5743         WHERE NOT EXISTS (
5744                 SELECT 1
5745                 FROM permission.grp_tree
5746                 WHERE
5747                         id = 11
5748         );
5749
5750 INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm)
5751         SELECT 12, oils_i18n_gettext(12, 'System Administrator', 'pgt', 'name'), 3, oils_i18n_gettext(12, 'Can do anything at the System level', 'pgt', 'description'), '3 years', TRUE, 'group_application.user.staff.admin.system_admin'
5752         WHERE NOT EXISTS (
5753                 SELECT 1
5754                 FROM permission.grp_tree
5755                 WHERE
5756                         id = 12
5757         );
5758
5759 INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm)
5760         SELECT 13, oils_i18n_gettext(13, 'Global Administrator', 'pgt', 'name'), 3, oils_i18n_gettext(13, 'Can do anything at the Consortium level', 'pgt', 'description'), '3 years', TRUE, 'group_application.user.staff.admin.global_admin'
5761         WHERE NOT EXISTS (
5762                 SELECT 1
5763                 FROM permission.grp_tree
5764                 WHERE
5765                         id = 13
5766         );
5767
5768 INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm)
5769         SELECT 14, oils_i18n_gettext(14, 'Data Review', 'pgt', 'name'), 3, NULL, '3 years', TRUE, 'group_application.user.staff.data_review'
5770         WHERE NOT EXISTS (
5771                 SELECT 1
5772                 FROM permission.grp_tree
5773                 WHERE
5774                         id = 14
5775         );
5776
5777 INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm)
5778         SELECT 15, oils_i18n_gettext(15, 'Volunteers', 'pgt', 'name'), 3, NULL, '3 years', TRUE, 'group_application.user.staff.volunteers'
5779         WHERE NOT EXISTS (
5780                 SELECT 1
5781                 FROM permission.grp_tree
5782                 WHERE
5783                         id = 15
5784         );
5785
5786
5787
5788 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
5789         SELECT
5790                 pgt.id, perm.id, aout.depth, TRUE
5791         FROM
5792                 permission.grp_tree pgt,
5793                 permission.perm_list perm,
5794                 actor.org_unit_type aout
5795         WHERE
5796                 pgt.name = 'Cataloging Administrator' AND
5797                 aout.name = 'Consortium' AND
5798                 perm.code IN (
5799                         'ADMIN_IMPORT_ITEM_ATTR_DEF',
5800                         'ADMIN_MERGE_PROFILE',
5801                         'CREATE_AUTHORITY_IMPORT_IMPORT_DEF',
5802                         'CREATE_BIB_IMPORT_FIELD_DEF',
5803                         'CREATE_BIB_PTYPE',
5804                         'CREATE_BIB_SOURCE',
5805                         'CREATE_IMPORT_ITEM_ATTR_DEF',
5806                         'CREATE_IMPORT_TRASH_FIELD',
5807                         'CREATE_MERGE_PROFILE',
5808                         'CREATE_MONOGRAPH_PART',
5809                         'CREATE_VOLUME_PREFIX',
5810                         'CREATE_VOLUME_SUFFIX',
5811                         'DELETE_AUTHORITY_IMPORT_IMPORT_FIELD_DEF',
5812                         'DELETE_BIB_PTYPE',
5813                         'DELETE_BIB_SOURCE',
5814                         'DELETE_IMPORT_ITEM_ATTR_DEF',
5815                         'DELETE_IMPORT_TRASH_FIELD',
5816                         'DELETE_MERGE_PROFILE',
5817                         'DELETE_MONOGRAPH_PART',
5818                         'DELETE_VOLUME_PREFIX',
5819                         'DELETE_VOLUME_SUFFIX',
5820                         'MAP_MONOGRAPH_PART',
5821                         'UPDATE_AUTHORITY_IMPORT_IMPORT_FIELD_DEF',
5822                         'UPDATE_BIB_IMPORT_IMPORT_FIELD_DEF',
5823                         'UPDATE_BIB_PTYPE',
5824                         'UPDATE_IMPORT_ITEM_ATTR_DEF',
5825                         'UPDATE_IMPORT_TRASH_FIELD',
5826                         'UPDATE_MERGE_PROFILE',
5827                         'UPDATE_MONOGRAPH_PART',
5828                         'UPDATE_VOLUME_PREFIX',
5829                         'UPDATE_VOLUME_SUFFIX'
5830                 ) AND NOT EXISTS (
5831                         SELECT 1
5832                         FROM permission.grp_perm_map AS map
5833                         WHERE
5834                                 map.grp = pgt.id
5835                                 AND map.perm = perm.id
5836                 );
5837
5838 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
5839         SELECT
5840                 pgt.id, perm.id, aout.depth, TRUE
5841         FROM
5842                 permission.grp_tree pgt,
5843                 permission.perm_list perm,
5844                 actor.org_unit_type aout
5845         WHERE
5846                 pgt.name = 'Circulation Administrator' AND
5847                 aout.name = 'Branch' AND
5848                 perm.code IN (
5849                         'DELETE_USER'
5850                 ) AND NOT EXISTS (
5851                         SELECT 1
5852                         FROM permission.grp_perm_map AS map
5853                         WHERE
5854                                 map.grp = pgt.id
5855                                 AND map.perm = perm.id
5856                 );
5857
5858 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
5859         SELECT
5860                 pgt.id, perm.id, aout.depth, TRUE
5861         FROM
5862                 permission.grp_tree pgt,
5863                 permission.perm_list perm,
5864                 actor.org_unit_type aout
5865         WHERE
5866                 pgt.name = 'Circulation Administrator' AND
5867                 aout.name = 'Consortium' AND
5868                 perm.code IN (
5869                         'ADMIN_MAX_FINE_RULE',
5870                         'CREATE_CIRC_DURATION',
5871                         'DELETE_CIRC_DURATION',
5872                         'MARK_ITEM_MISSING_PIECES',
5873                         'UPDATE_CIRC_DURATION',
5874                         'UPDATE_HOLD_REQUEST_TIME',
5875                         'UPDATE_NET_ACCESS_LEVEL',
5876                         'VIEW_CIRC_MATRIX_MATCHPOINT',
5877                         'VIEW_HOLD_MATRIX_MATCHPOINT'
5878                 ) AND NOT EXISTS (
5879                         SELECT 1
5880                         FROM permission.grp_perm_map AS map
5881                         WHERE
5882                                 map.grp = pgt.id
5883                                 AND map.perm = perm.id
5884                 );
5885
5886 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
5887         SELECT
5888                 pgt.id, perm.id, aout.depth, TRUE
5889         FROM
5890                 permission.grp_tree pgt,
5891                 permission.perm_list perm,
5892                 actor.org_unit_type aout
5893         WHERE
5894                 pgt.name = 'Circulation Administrator' AND
5895                 aout.name = 'System' AND
5896                 perm.code IN (
5897                         'ADMIN_BOOKING_RESERVATION',
5898                         'ADMIN_BOOKING_RESERVATION_ATTR_MAP',
5899                         'ADMIN_BOOKING_RESERVATION_ATTR_VALUE_MAP',
5900                         'ADMIN_BOOKING_RESOURCE',
5901                         'ADMIN_BOOKING_RESOURCE_ATTR',
5902                         'ADMIN_BOOKING_RESOURCE_ATTR_MAP',
5903                         'ADMIN_BOOKING_RESOURCE_ATTR_VALUE',
5904                         'ADMIN_BOOKING_RESOURCE_TYPE',
5905                         'ADMIN_COPY_LOCATION_ORDER',
5906                         'ADMIN_HOLD_CANCEL_CAUSE',
5907                         'ASSIGN_GROUP_PERM',
5908                         'BAR_PATRON',
5909                         'COPY_HOLDS',
5910                         'COPY_TRANSIT_RECEIVE',
5911                         'CREATE_BILL',
5912                         'CREATE_BILLING_TYPE',
5913                         'CREATE_NON_CAT_TYPE',
5914                         'CREATE_PATRON_STAT_CAT',
5915                         'CREATE_PATRON_STAT_CAT_ENTRY',
5916                         'CREATE_PATRON_STAT_CAT_ENTRY_MAP',
5917                         'CREATE_USER_GROUP_LINK',
5918                         'DELETE_BILLING_TYPE',
5919                         'DELETE_NON_CAT_TYPE',
5920                         'DELETE_PATRON_STAT_CAT',
5921                         'DELETE_PATRON_STAT_CAT_ENTRY',
5922                         'DELETE_PATRON_STAT_CAT_ENTRY_MAP',
5923                         'DELETE_TRANSIT',
5924                         'group_application.user.staff',
5925                         'MANAGE_BAD_DEBT',
5926                         'MARK_ITEM_AVAILABLE',
5927                         'MARK_ITEM_BINDERY',
5928                         'MARK_ITEM_CHECKED_OUT',
5929                         'MARK_ITEM_ILL',
5930                         'MARK_ITEM_IN_PROCESS',
5931                         'MARK_ITEM_IN_TRANSIT',
5932                         'MARK_ITEM_LOST',
5933                         'MARK_ITEM_MISSING',
5934                         'MARK_ITEM_ON_HOLDS_SHELF',
5935                         'MARK_ITEM_ON_ORDER',
5936                         'MARK_ITEM_RESHELVING',
5937                         'MERGE_USERS',
5938                         'money.collections_tracker.create',
5939                         'money.collections_tracker.delete',
5940                         'OFFLINE_EXECUTE',
5941                         'OFFLINE_UPLOAD',
5942                         'OFFLINE_VIEW',
5943                         'REMOVE_USER_GROUP_LINK',
5944                         'SET_CIRC_CLAIMS_RETURNED',
5945                         'SET_CIRC_CLAIMS_RETURNED.override',
5946                         'SET_CIRC_LOST',
5947                         'SET_CIRC_MISSING',
5948                         'UNBAR_PATRON',
5949                         'UPDATE_BILL_NOTE',
5950                         'UPDATE_NON_CAT_TYPE',
5951                         'UPDATE_PATRON_CLAIM_NEVER_CHECKED_OUT_COUNT',
5952                         'UPDATE_PATRON_CLAIM_RETURN_COUNT',
5953                         'UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF',
5954                         'UPDATE_PICKUP_LIB_FROM_TRANSIT',
5955                         'UPDATE_USER',
5956                         'VIEW_REPORT_OUTPUT',
5957                         'VIEW_STANDING_PENALTY',
5958                         'VOID_BILLING',
5959                         'VOLUME_HOLDS'
5960                 ) AND NOT EXISTS (
5961                         SELECT 1
5962                         FROM permission.grp_perm_map AS map
5963                         WHERE
5964                                 map.grp = pgt.id
5965                                 AND map.perm = perm.id
5966                 );
5967
5968 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
5969         SELECT
5970                 pgt.id, perm.id, aout.depth, TRUE
5971         FROM
5972                 permission.grp_tree pgt,
5973                 permission.perm_list perm,
5974                 actor.org_unit_type aout
5975         WHERE
5976                 pgt.name = 'Local Administrator' AND
5977                 aout.name = 'Branch' AND
5978                 perm.code IN (
5979                         'EVERYTHING'
5980                 ) AND NOT EXISTS (
5981                         SELECT 1
5982                         FROM permission.grp_perm_map AS map
5983                         WHERE
5984                                 map.grp = pgt.id
5985                                 AND map.perm = perm.id
5986                 );
5987
5988 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
5989         SELECT
5990                 pgt.id, perm.id, aout.depth, FALSE
5991         FROM
5992                 permission.grp_tree pgt,
5993                 permission.perm_list perm,
5994                 actor.org_unit_type aout
5995         WHERE
5996                 pgt.name = 'Serials' AND
5997                 aout.name = 'System' AND
5998                 perm.code IN (
5999                         'ADMIN_ASSET_COPY_TEMPLATE',
6000                         'ADMIN_SERIAL_CAPTION_PATTERN',
6001                         'ADMIN_SERIAL_DISTRIBUTION',
6002                         'ADMIN_SERIAL_ITEM',
6003                         'ADMIN_SERIAL_STREAM',
6004                         'ADMIN_SERIAL_SUBSCRIPTION',
6005                         'ISSUANCE_HOLDS',
6006                         'RECEIVE_SERIAL'
6007                 ) AND NOT EXISTS (
6008                         SELECT 1
6009                         FROM permission.grp_perm_map AS map
6010                         WHERE
6011                                 map.grp = pgt.id
6012                                 AND map.perm = perm.id
6013                 );
6014
6015 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6016         SELECT
6017                 pgt.id, perm.id, aout.depth, TRUE
6018         FROM
6019                 permission.grp_tree pgt,
6020                 permission.perm_list perm,
6021                 actor.org_unit_type aout
6022         WHERE
6023                 pgt.name = 'System Administrator' AND
6024                 aout.name = 'System' AND
6025                 perm.code IN (
6026                         'EVERYTHING'
6027                 ) AND NOT EXISTS (
6028                         SELECT 1
6029                         FROM permission.grp_perm_map AS map
6030                         WHERE
6031                                 map.grp = pgt.id
6032                                 AND map.perm = perm.id
6033                 );
6034
6035 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6036         SELECT
6037                 pgt.id, perm.id, aout.depth, FALSE
6038         FROM
6039                 permission.grp_tree pgt,
6040                 permission.perm_list perm,
6041                 actor.org_unit_type aout
6042         WHERE
6043                 pgt.name = 'System Administrator' AND
6044                 aout.name = 'Consortium' AND
6045                 perm.code ~ '^VIEW_TRIGGER'
6046                 AND NOT EXISTS (
6047                         SELECT 1
6048                         FROM permission.grp_perm_map AS map
6049                         WHERE
6050                                 map.grp = pgt.id
6051                                 AND map.perm = perm.id
6052                 );
6053
6054 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6055         SELECT
6056                 pgt.id, perm.id, aout.depth, TRUE
6057         FROM
6058                 permission.grp_tree pgt,
6059                 permission.perm_list perm,
6060                 actor.org_unit_type aout
6061         WHERE
6062                 pgt.name = 'Global Administrator' AND
6063                 aout.name = 'Consortium' AND
6064                 perm.code IN (
6065                         'EVERYTHING'
6066                 ) AND NOT EXISTS (
6067                         SELECT 1
6068                         FROM permission.grp_perm_map AS map
6069                         WHERE
6070                                 map.grp = pgt.id
6071                                 AND map.perm = perm.id
6072                 );
6073
6074 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6075         SELECT
6076                 pgt.id, perm.id, aout.depth, FALSE
6077         FROM
6078                 permission.grp_tree pgt,
6079                 permission.perm_list perm,
6080                 actor.org_unit_type aout
6081         WHERE
6082                 pgt.name = 'Data Review' AND
6083                 aout.name = 'Consortium' AND
6084                 perm.code IN (
6085                         'CREATE_COPY_TRANSIT',
6086                         'VIEW_BILLING_TYPE',
6087                         'VIEW_CIRCULATIONS',
6088                         'VIEW_COPY_NOTES',
6089                         'VIEW_HOLD',
6090                         'VIEW_ORG_SETTINGS',
6091                         'VIEW_TITLE_NOTES',
6092                         'VIEW_TRANSACTION',
6093                         'VIEW_USER',
6094                         'VIEW_USER_FINES_SUMMARY',
6095                         'VIEW_USER_TRANSACTIONS',
6096                         'VIEW_VOLUME_NOTES',
6097                         'VIEW_ZIP_DATA'
6098                 ) AND NOT EXISTS (
6099                         SELECT 1
6100                         FROM permission.grp_perm_map AS map
6101                         WHERE
6102                                 map.grp = pgt.id
6103                                 AND map.perm = perm.id
6104                 );
6105
6106 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6107         SELECT
6108                 pgt.id, perm.id, aout.depth, FALSE
6109         FROM
6110                 permission.grp_tree pgt,
6111                 permission.perm_list perm,
6112                 actor.org_unit_type aout
6113         WHERE
6114                 pgt.name = 'Data Review' AND
6115                 aout.name = 'System' AND
6116                 perm.code IN (
6117                         'COPY_CHECKOUT',
6118                         'COPY_HOLDS',
6119                         'CREATE_IN_HOUSE_USE',
6120                         'CREATE_TRANSACTION',
6121                         'OFFLINE_EXECUTE',
6122                         'OFFLINE_VIEW',
6123                         'STAFF_LOGIN',
6124                         'VOLUME_HOLDS'
6125                 ) AND NOT EXISTS (
6126                         SELECT 1
6127                         FROM permission.grp_perm_map AS map
6128                         WHERE
6129                                 map.grp = pgt.id
6130                                 AND map.perm = perm.id
6131                 );
6132
6133 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6134         SELECT
6135                 pgt.id, perm.id, aout.depth, FALSE
6136         FROM
6137                 permission.grp_tree pgt,
6138                 permission.perm_list perm,
6139                 actor.org_unit_type aout
6140         WHERE
6141                 pgt.name = 'Volunteers' AND
6142                 aout.name = 'Branch' AND
6143                 perm.code IN (
6144                         'COPY_CHECKOUT',
6145                         'CREATE_BILL',
6146                         'CREATE_IN_HOUSE_USE',
6147                         'CREATE_PAYMENT',
6148                         'VIEW_BILLING_TYPE',
6149                         'VIEW_CIRCS',
6150                         'VIEW_COPY_CHECKOUT',
6151                         'VIEW_HOLD',
6152                         'VIEW_TITLE_HOLDS',
6153                         'VIEW_TRANSACTION',
6154                         'VIEW_USER',
6155                         'VIEW_USER_FINES_SUMMARY',
6156                         'VIEW_USER_TRANSACTIONS'
6157                 ) AND NOT EXISTS (
6158                         SELECT 1
6159                         FROM permission.grp_perm_map AS map
6160                         WHERE
6161                                 map.grp = pgt.id
6162                                 AND map.perm = perm.id
6163                 );
6164
6165 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6166         SELECT
6167                 pgt.id, perm.id, aout.depth, FALSE
6168         FROM
6169                 permission.grp_tree pgt,
6170                 permission.perm_list perm,
6171                 actor.org_unit_type aout
6172         WHERE
6173                 pgt.name = 'Volunteers' AND
6174                 aout.name = 'Consortium' AND
6175                 perm.code IN (
6176                         'CREATE_COPY_TRANSIT',
6177                         'CREATE_TRANSACTION',
6178                         'CREATE_TRANSIT',
6179                         'STAFF_LOGIN',
6180                         'TRANSIT_COPY',
6181                         'VIEW_ORG_SETTINGS'
6182                 ) AND NOT EXISTS (
6183                         SELECT 1
6184                         FROM permission.grp_perm_map AS map
6185                         WHERE
6186                                 map.grp = pgt.id
6187                                 AND map.perm = perm.id
6188                 );
6189
6190
6191 -- stock Users group
6192 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6193         SELECT
6194                 pgt.id, perm.id, aout.depth, FALSE
6195         FROM
6196                 permission.grp_tree pgt,
6197                 permission.perm_list perm,
6198                 actor.org_unit_type aout
6199         WHERE
6200                 pgt.name = 'Users' AND
6201                 aout.name = 'Consortium' AND
6202                 perm.code IN (
6203                         'CREATE_PURCHASE_REQUEST'
6204                 ) AND NOT EXISTS (
6205                         SELECT 1
6206                         FROM permission.grp_perm_map AS map
6207                         WHERE
6208                                 map.grp = pgt.id
6209                                 AND map.perm = perm.id
6210                 );
6211
6212 -- stock Staff group
6213 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6214         SELECT
6215                 pgt.id, perm.id, aout.depth, FALSE
6216         FROM
6217                 permission.grp_tree pgt,
6218                 permission.perm_list perm,
6219                 actor.org_unit_type aout
6220         WHERE
6221                 pgt.name = 'Staff' AND
6222                 aout.name = 'Consortium' AND
6223                 perm.code IN (
6224                         'VIEW_USER_SETTING_TYPE'
6225                 ) AND NOT EXISTS (
6226                         SELECT 1
6227                         FROM permission.grp_perm_map AS map
6228                         WHERE
6229                                 map.grp = pgt.id
6230                                 AND map.perm = perm.id
6231                 );
6232
6233 -- stock Circulators group
6234 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6235         SELECT
6236                 pgt.id, perm.id, aout.depth, FALSE
6237         FROM
6238                 permission.grp_tree pgt,
6239                 permission.perm_list perm,
6240                 actor.org_unit_type aout
6241         WHERE
6242                 pgt.name = 'Circulators' AND
6243                 aout.name = 'Branch' AND
6244                 perm.code IN (
6245                         'MARK_ITEM_MISSING_PIECES'
6246                 ) AND NOT EXISTS (
6247                         SELECT 1
6248                         FROM permission.grp_perm_map AS map
6249                         WHERE
6250                                 map.grp = pgt.id
6251                                 AND map.perm = perm.id
6252                 );
6253
6254 -- stock Catalogers group
6255 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6256         SELECT
6257                 pgt.id, perm.id, aout.depth, FALSE
6258         FROM
6259                 permission.grp_tree pgt,
6260                 permission.perm_list perm,
6261                 actor.org_unit_type aout
6262         WHERE
6263                 pgt.name = 'Catalogers' AND
6264                 aout.name = 'System' AND
6265                 perm.code IN (
6266                         'MAP_MONOGRAPH_PART'
6267                 ) AND NOT EXISTS (
6268                         SELECT 1
6269                         FROM permission.grp_perm_map AS map
6270                         WHERE
6271                                 map.grp = pgt.id
6272                                 AND map.perm = perm.id
6273                 );
6274
6275 -- stock Acquisitions group
6276 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6277         SELECT
6278                 pgt.id, perm.id, aout.depth, FALSE
6279         FROM
6280                 permission.grp_tree pgt,
6281                 permission.perm_list perm,
6282                 actor.org_unit_type aout
6283         WHERE
6284                 pgt.name = 'Acquisitions' AND
6285                 aout.name = 'Consortium' AND
6286                 perm.code IN (
6287                         'UPDATE_PICKLIST'
6288                 ) AND NOT EXISTS (
6289                         SELECT 1
6290                         FROM permission.grp_perm_map AS map
6291                         WHERE
6292                                 map.grp = pgt.id
6293                                 AND map.perm = perm.id
6294                 );
6295
6296 -- stock Acq Admin group
6297 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6298         SELECT
6299                 pgt.id, perm.id, aout.depth, TRUE
6300         FROM
6301                 permission.grp_tree pgt,
6302                 permission.perm_list perm,
6303                 actor.org_unit_type aout
6304         WHERE
6305                 pgt.name = 'Acquisitions Administrator' AND
6306                 aout.name = 'Consortium' AND
6307                 perm.code IN (
6308                         'UPDATE_PICKLIST'
6309                 ) AND NOT EXISTS (
6310                         SELECT 1
6311                         FROM permission.grp_perm_map AS map
6312                         WHERE
6313                                 map.grp = pgt.id
6314                                 AND map.perm = perm.id
6315                 );
6316
6317 INSERT INTO config.upgrade_log (version) VALUES ('0547'); -- dbwells
6318
6319 -- account for spelling errors (Admin != Administrator)
6320 \qecho This might not insert much if you passed through 0542 on your way here,
6321 \qecho but one group was missed there as well
6322
6323 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6324         SELECT
6325                 pgt.id, perm.id, aout.depth, TRUE
6326         FROM
6327                 permission.grp_tree pgt,
6328                 permission.perm_list perm,
6329                 actor.org_unit_type aout
6330         WHERE
6331                 pgt.name = 'Cataloging Administrator' AND
6332                 aout.name = 'Consortium' AND
6333                 perm.code IN (
6334                         'ADMIN_IMPORT_ITEM_ATTR_DEF',
6335                         'ADMIN_MERGE_PROFILE',
6336                         'CREATE_AUTHORITY_IMPORT_IMPORT_DEF',
6337                         'CREATE_BIB_IMPORT_FIELD_DEF',
6338                         'CREATE_BIB_PTYPE',
6339                         'CREATE_BIB_SOURCE',
6340                         'CREATE_IMPORT_ITEM_ATTR_DEF',
6341                         'CREATE_IMPORT_TRASH_FIELD',
6342                         'CREATE_MERGE_PROFILE',
6343                         'CREATE_MONOGRAPH_PART',
6344                         'CREATE_VOLUME_PREFIX',
6345                         'CREATE_VOLUME_SUFFIX',
6346                         'DELETE_AUTHORITY_IMPORT_IMPORT_FIELD_DEF',
6347                         'DELETE_BIB_PTYPE',
6348                         'DELETE_BIB_SOURCE',
6349                         'DELETE_IMPORT_ITEM_ATTR_DEF',
6350                         'DELETE_IMPORT_TRASH_FIELD',
6351                         'DELETE_MERGE_PROFILE',
6352                         'DELETE_MONOGRAPH_PART',
6353                         'DELETE_VOLUME_PREFIX',
6354                         'DELETE_VOLUME_SUFFIX',
6355                         'MAP_MONOGRAPH_PART',
6356                         'UPDATE_AUTHORITY_IMPORT_IMPORT_FIELD_DEF',
6357                         'UPDATE_BIB_IMPORT_IMPORT_FIELD_DEF',
6358                         'UPDATE_BIB_PTYPE',
6359                         'UPDATE_IMPORT_ITEM_ATTR_DEF',
6360                         'UPDATE_IMPORT_TRASH_FIELD',
6361                         'UPDATE_MERGE_PROFILE',
6362                         'UPDATE_MONOGRAPH_PART',
6363                         'UPDATE_VOLUME_PREFIX',
6364                         'UPDATE_VOLUME_SUFFIX'
6365                 ) AND NOT EXISTS (
6366                         SELECT 1
6367                         FROM permission.grp_perm_map AS map
6368                         WHERE
6369                                 map.grp = pgt.id
6370                                 AND map.perm = perm.id
6371                 );
6372
6373 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6374     SELECT
6375         pgt.id, perm.id, aout.depth, TRUE
6376     FROM
6377         permission.grp_tree pgt,
6378         permission.perm_list perm,
6379         actor.org_unit_type aout
6380     WHERE
6381         pgt.name = 'Cataloging Administrator' AND
6382         aout.name = 'System' AND
6383         perm.code IN (
6384             'CREATE_COPY_STAT_CAT',
6385             'CREATE_COPY_STAT_CAT_ENTRY',
6386             'CREATE_COPY_STAT_CAT_ENTRY_MAP',
6387             'RUN_REPORTS',
6388             'SHARE_REPORT_FOLDER',
6389             'UPDATE_COPY_LOCATION',
6390             'UPDATE_COPY_STAT_CAT',
6391             'UPDATE_COPY_STAT_CAT_ENTRY',
6392             'VIEW_REPORT_OUTPUT'
6393         ) AND NOT EXISTS (
6394             SELECT 1
6395             FROM permission.grp_perm_map AS map
6396             WHERE
6397                 map.grp = pgt.id
6398                 AND map.perm = perm.id
6399         );
6400
6401 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6402         SELECT
6403                 pgt.id, perm.id, aout.depth, TRUE
6404         FROM
6405                 permission.grp_tree pgt,
6406                 permission.perm_list perm,
6407                 actor.org_unit_type aout
6408         WHERE
6409                 pgt.name = 'Circulation Administrator' AND
6410                 aout.name = 'Branch' AND
6411                 perm.code IN (
6412                         'DELETE_USER'
6413                 ) AND NOT EXISTS (
6414                         SELECT 1
6415                         FROM permission.grp_perm_map AS map
6416                         WHERE
6417                                 map.grp = pgt.id
6418                                 AND map.perm = perm.id
6419                 );
6420
6421 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6422         SELECT
6423                 pgt.id, perm.id, aout.depth, TRUE
6424         FROM
6425                 permission.grp_tree pgt,
6426                 permission.perm_list perm,
6427                 actor.org_unit_type aout
6428         WHERE
6429                 pgt.name = 'Circulation Administrator' AND
6430                 aout.name = 'Consortium' AND
6431                 perm.code IN (
6432                         'ADMIN_MAX_FINE_RULE',
6433                         'CREATE_CIRC_DURATION',
6434                         'DELETE_CIRC_DURATION',
6435                         'MARK_ITEM_MISSING_PIECES',
6436                         'UPDATE_CIRC_DURATION',
6437                         'UPDATE_HOLD_REQUEST_TIME',
6438                         'UPDATE_NET_ACCESS_LEVEL',
6439                         'VIEW_CIRC_MATRIX_MATCHPOINT',
6440                         'VIEW_HOLD_MATRIX_MATCHPOINT'
6441                 ) AND NOT EXISTS (
6442                         SELECT 1
6443                         FROM permission.grp_perm_map AS map
6444                         WHERE
6445                                 map.grp = pgt.id
6446                                 AND map.perm = perm.id
6447                 );
6448
6449 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6450         SELECT
6451                 pgt.id, perm.id, aout.depth, TRUE
6452         FROM
6453                 permission.grp_tree pgt,
6454                 permission.perm_list perm,
6455                 actor.org_unit_type aout
6456         WHERE
6457                 pgt.name = 'Circulation Administrator' AND
6458                 aout.name = 'System' AND
6459                 perm.code IN (
6460                         'ADMIN_BOOKING_RESERVATION',
6461                         'ADMIN_BOOKING_RESERVATION_ATTR_MAP',
6462                         'ADMIN_BOOKING_RESERVATION_ATTR_VALUE_MAP',
6463                         'ADMIN_BOOKING_RESOURCE',
6464                         'ADMIN_BOOKING_RESOURCE_ATTR',
6465                         'ADMIN_BOOKING_RESOURCE_ATTR_MAP',
6466                         'ADMIN_BOOKING_RESOURCE_ATTR_VALUE',
6467                         'ADMIN_BOOKING_RESOURCE_TYPE',
6468                         'ADMIN_COPY_LOCATION_ORDER',
6469                         'ADMIN_HOLD_CANCEL_CAUSE',
6470                         'ASSIGN_GROUP_PERM',
6471                         'BAR_PATRON',
6472                         'COPY_HOLDS',
6473                         'COPY_TRANSIT_RECEIVE',
6474                         'CREATE_BILL',
6475                         'CREATE_BILLING_TYPE',
6476                         'CREATE_NON_CAT_TYPE',
6477                         'CREATE_PATRON_STAT_CAT',
6478                         'CREATE_PATRON_STAT_CAT_ENTRY',
6479                         'CREATE_PATRON_STAT_CAT_ENTRY_MAP',
6480                         'CREATE_USER_GROUP_LINK',
6481                         'DELETE_BILLING_TYPE',
6482                         'DELETE_NON_CAT_TYPE',
6483                         'DELETE_PATRON_STAT_CAT',
6484                         'DELETE_PATRON_STAT_CAT_ENTRY',
6485                         'DELETE_PATRON_STAT_CAT_ENTRY_MAP',
6486                         'DELETE_TRANSIT',
6487                         'group_application.user.staff',
6488                         'MANAGE_BAD_DEBT',
6489                         'MARK_ITEM_AVAILABLE',
6490                         'MARK_ITEM_BINDERY',
6491                         'MARK_ITEM_CHECKED_OUT',
6492                         'MARK_ITEM_ILL',
6493                         'MARK_ITEM_IN_PROCESS',
6494                         'MARK_ITEM_IN_TRANSIT',
6495                         'MARK_ITEM_LOST',
6496                         'MARK_ITEM_MISSING',
6497                         'MARK_ITEM_ON_HOLDS_SHELF',
6498                         'MARK_ITEM_ON_ORDER',
6499                         'MARK_ITEM_RESHELVING',
6500                         'MERGE_USERS',
6501                         'money.collections_tracker.create',
6502                         'money.collections_tracker.delete',
6503                         'OFFLINE_EXECUTE',
6504                         'OFFLINE_UPLOAD',
6505                         'OFFLINE_VIEW',
6506                         'REMOVE_USER_GROUP_LINK',
6507                         'SET_CIRC_CLAIMS_RETURNED',
6508                         'SET_CIRC_CLAIMS_RETURNED.override',
6509                         'SET_CIRC_LOST',
6510                         'SET_CIRC_MISSING',
6511                         'UNBAR_PATRON',
6512                         'UPDATE_BILL_NOTE',
6513                         'UPDATE_NON_CAT_TYPE',
6514                         'UPDATE_PATRON_CLAIM_NEVER_CHECKED_OUT_COUNT',
6515                         'UPDATE_PATRON_CLAIM_RETURN_COUNT',
6516                         'UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF',
6517                         'UPDATE_PICKUP_LIB_FROM_TRANSIT',
6518                         'UPDATE_USER',
6519                         'VIEW_REPORT_OUTPUT',
6520                         'VIEW_STANDING_PENALTY',
6521                         'VOID_BILLING',
6522                         'VOLUME_HOLDS'
6523                 ) AND NOT EXISTS (
6524                         SELECT 1
6525                         FROM permission.grp_perm_map AS map
6526                         WHERE
6527                                 map.grp = pgt.id
6528                                 AND map.perm = perm.id
6529                 );
6530
6531 INSERT INTO config.upgrade_log (version) VALUES ('0557'); -- miker
6532
6533 CREATE OR REPLACE FUNCTION unapi.acl ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
6534     SELECT  XMLELEMENT(
6535                 name location,
6536                 XMLATTRIBUTES(
6537                     CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
6538                     id AS ident,
6539                     holdable,
6540                     opac_visible,
6541                     label_prefix AS prefix,
6542                     label_suffix AS suffix
6543                 ),
6544                 name
6545             )
6546       FROM  asset.copy_location
6547       WHERE id = $1;
6548 $F$ LANGUAGE SQL;
6549
6550 INSERT INTO config.upgrade_log (version) VALUES ('0558'); -- miker
6551
6552 CREATE OR REPLACE FUNCTION unapi.ccs ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
6553     SELECT  XMLELEMENT(
6554                 name status,
6555                 XMLATTRIBUTES(
6556                     CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
6557                     id AS ident,
6558                     holdable,
6559                     opac_visible
6560                 ),
6561                 name
6562             )
6563       FROM  config.copy_status
6564       WHERE id = $1;
6565 $F$ LANGUAGE SQL;
6566
6567 INSERT INTO config.upgrade_log (version) VALUES ('0560'); -- miker
6568
6569 CREATE OR REPLACE FUNCTION asset.cache_copy_visibility () RETURNS TRIGGER as $func$
6570 DECLARE
6571     add_query       TEXT;
6572     remove_query    TEXT;
6573     do_add          BOOLEAN := false;
6574     do_remove       BOOLEAN := false;
6575 BEGIN
6576     add_query := $$
6577             INSERT INTO asset.opac_visible_copies (copy_id, circ_lib, record)
6578               SELECT id, circ_lib, record FROM (
6579                 SELECT  cp.id, cp.circ_lib, cn.record, cn.id AS call_number, cp.location, cp.status
6580                   FROM  asset.copy cp
6581                         JOIN asset.call_number cn ON (cn.id = cp.call_number)
6582                         JOIN actor.org_unit a ON (cp.circ_lib = a.id)
6583                         JOIN asset.copy_location cl ON (cp.location = cl.id)
6584                         JOIN config.copy_status cs ON (cp.status = cs.id)
6585                         JOIN biblio.record_entry b ON (cn.record = b.id)
6586                   WHERE NOT cp.deleted
6587                         AND NOT cn.deleted
6588                         AND NOT b.deleted
6589                         AND cs.opac_visible
6590                         AND cl.opac_visible
6591                         AND cp.opac_visible
6592                         AND a.opac_visible
6593                             UNION
6594                 SELECT  cp.id, cp.circ_lib, pbcm.peer_record AS record, NULL AS call_number, cp.location, cp.status
6595                   FROM  asset.copy cp
6596                         JOIN biblio.peer_bib_copy_map pbcm ON (pbcm.target_copy = cp.id)
6597                         JOIN actor.org_unit a ON (cp.circ_lib = a.id)
6598                         JOIN asset.copy_location cl ON (cp.location = cl.id)
6599                         JOIN config.copy_status cs ON (cp.status = cs.id)
6600                   WHERE NOT cp.deleted
6601                         AND cs.opac_visible
6602                         AND cl.opac_visible
6603                         AND cp.opac_visible
6604                         AND a.opac_visible
6605                     ) AS x 
6606
6607     $$;
6608  
6609     remove_query := $$ DELETE FROM asset.opac_visible_copies WHERE copy_id IN ( SELECT id FROM asset.copy WHERE $$;
6610
6611     IF TG_TABLE_NAME = 'peer_bib_copy_map' THEN
6612         IF TG_OP = 'INSERT' THEN
6613             add_query := add_query || 'WHERE x.id = ' || NEW.target_copy || ' AND x.record = ' || NEW.peer_record || ';';
6614             EXECUTE add_query;
6615             RETURN NEW;
6616         ELSE
6617             remove_query := 'DELETE FROM asset.opac_visible_copies WHERE copy_id = ' || OLD.target_copy || ' AND record = ' || OLD.peer_record || ';';
6618             EXECUTE remove_query;
6619             RETURN OLD;
6620         END IF;
6621     END IF;
6622
6623     IF TG_OP = 'INSERT' THEN
6624
6625         IF TG_TABLE_NAME IN ('copy', 'unit') THEN
6626             add_query := add_query || 'WHERE x.id = ' || NEW.id || ';';
6627             EXECUTE add_query;
6628         END IF;
6629
6630         RETURN NEW;
6631
6632     END IF;
6633
6634     -- handle items first, since with circulation activity
6635     -- their statuses change frequently
6636     IF TG_TABLE_NAME IN ('copy', 'unit') THEN
6637
6638         IF OLD.location    <> NEW.location OR
6639            OLD.call_number <> NEW.call_number OR
6640            OLD.status      <> NEW.status OR
6641            OLD.circ_lib    <> NEW.circ_lib THEN
6642             -- any of these could change visibility, but
6643             -- we'll save some queries and not try to calculate
6644             -- the change directly
6645             do_remove := true;
6646             do_add := true;
6647         ELSE
6648
6649             IF OLD.deleted <> NEW.deleted THEN
6650                 IF NEW.deleted THEN
6651                     do_remove := true;
6652                 ELSE
6653                     do_add := true;
6654                 END IF;
6655             END IF;
6656
6657             IF OLD.opac_visible <> NEW.opac_visible THEN
6658                 IF OLD.opac_visible THEN
6659                     do_remove := true;
6660                 ELSIF NOT do_remove THEN -- handle edge case where deleted item
6661                                         -- is also marked opac_visible
6662                     do_add := true;
6663                 END IF;
6664             END IF;
6665
6666         END IF;
6667
6668         IF do_remove THEN
6669             DELETE FROM asset.opac_visible_copies WHERE copy_id = NEW.id;
6670         END IF;
6671         IF do_add THEN
6672             add_query := add_query || 'WHERE x.id = ' || NEW.id || ';';
6673             EXECUTE add_query;
6674         END IF;
6675
6676         RETURN NEW;
6677
6678     END IF;
6679
6680     IF TG_TABLE_NAME IN ('call_number', 'record_entry') THEN -- these have a 'deleted' column
6681  
6682         IF OLD.deleted AND NEW.deleted THEN -- do nothing
6683
6684             RETURN NEW;
6685  
6686         ELSIF NEW.deleted THEN -- remove rows
6687  
6688             IF TG_TABLE_NAME = 'call_number' THEN
6689                 DELETE FROM asset.opac_visible_copies WHERE copy_id IN (SELECT id FROM asset.copy WHERE call_number = NEW.id);
6690             ELSIF TG_TABLE_NAME = 'record_entry' THEN
6691                 DELETE FROM asset.opac_visible_copies WHERE record = NEW.id;
6692             END IF;
6693  
6694             RETURN NEW;
6695  
6696         ELSIF OLD.deleted THEN -- add rows
6697  
6698             IF TG_TABLE_NAME IN ('copy','unit') THEN
6699                 add_query := add_query || 'WHERE x.id = ' || NEW.id || ';';
6700             ELSIF TG_TABLE_NAME = 'call_number' THEN
6701                 add_query := add_query || 'WHERE x.call_number = ' || NEW.id || ';';
6702             ELSIF TG_TABLE_NAME = 'record_entry' THEN
6703                 add_query := add_query || 'WHERE x.record = ' || NEW.id || ';';
6704             END IF;
6705  
6706             EXECUTE add_query;
6707             RETURN NEW;
6708  
6709         END IF;
6710  
6711     END IF;
6712
6713     IF TG_TABLE_NAME = 'call_number' THEN
6714
6715         IF OLD.record <> NEW.record THEN
6716             -- call number is linked to different bib
6717             remove_query := remove_query || 'call_number = ' || NEW.id || ');';
6718             EXECUTE remove_query;
6719             add_query := add_query || 'WHERE x.call_number = ' || NEW.id || ';';
6720             EXECUTE add_query;
6721         END IF;
6722
6723         RETURN NEW;
6724
6725     END IF;
6726
6727     IF TG_TABLE_NAME IN ('record_entry') THEN
6728         RETURN NEW; -- don't have 'opac_visible'
6729     END IF;
6730
6731     -- actor.org_unit, asset.copy_location, asset.copy_status
6732     IF NEW.opac_visible = OLD.opac_visible THEN -- do nothing
6733
6734         RETURN NEW;
6735
6736     ELSIF NEW.opac_visible THEN -- add rows
6737
6738         IF TG_TABLE_NAME = 'org_unit' THEN
6739             add_query := add_query || 'WHERE x.circ_lib = ' || NEW.id || ';';
6740         ELSIF TG_TABLE_NAME = 'copy_location' THEN
6741             add_query := add_query || 'WHERE x.location = ' || NEW.id || ';';
6742         ELSIF TG_TABLE_NAME = 'copy_status' THEN
6743             add_query := add_query || 'WHERE x.status = ' || NEW.id || ';';
6744         END IF;
6745  
6746         EXECUTE add_query;
6747  
6748     ELSE -- delete rows
6749
6750         IF TG_TABLE_NAME = 'org_unit' THEN
6751             remove_query := 'DELETE FROM asset.opac_visible_copies WHERE circ_lib = ' || NEW.id || ';';
6752         ELSIF TG_TABLE_NAME = 'copy_location' THEN
6753             remove_query := remove_query || 'location = ' || NEW.id || ');';
6754         ELSIF TG_TABLE_NAME = 'copy_status' THEN
6755             remove_query := remove_query || 'status = ' || NEW.id || ');';
6756         END IF;
6757  
6758         EXECUTE remove_query;
6759  
6760     END IF;
6761  
6762     RETURN NEW;
6763 END;
6764 $func$ LANGUAGE PLPGSQL;
6765
6766 INSERT INTO config.upgrade_log (version) VALUES ('0563');
6767
6768 INSERT INTO permission.perm_list ( id, code, description ) 
6769     VALUES ( 510, 'UPDATE_PATRON_COLLECTIONS_EXEMPT', oils_i18n_gettext(510,
6770     'Allows a user to indicate that a patron is exempt from collections processing', 'ppl', 'description'));
6771
6772 --- stock Circulation Administrator group
6773
6774 INSERT INTO permission.grp_perm_map ( grp, perm, depth, grantable )
6775     SELECT
6776         4,
6777         id,
6778         0,
6779         't'
6780     FROM permission.perm_list
6781     WHERE code in ('UPDATE_PATRON_COLLECTIONS_EXEMPT');
6782
6783 INSERT INTO config.upgrade_log (version) VALUES ('0566');
6784
6785 CREATE OR REPLACE FUNCTION unapi.bre ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
6786 DECLARE
6787     me      biblio.record_entry%ROWTYPE;
6788     layout  unapi.bre_output_layout%ROWTYPE;
6789     xfrm    config.xml_transform%ROWTYPE;
6790     ouid    INT;
6791     tmp_xml TEXT;
6792     top_el  TEXT;
6793     output  XML;
6794     hxml    XML;
6795     axml    XML;
6796 BEGIN
6797
6798     SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
6799
6800     IF ouid IS NULL THEN
6801         RETURN NULL::XML;
6802     END IF;
6803
6804     IF format = 'holdings_xml' THEN -- the special case
6805         output := unapi.holdings_xml( obj_id, ouid, org, depth, includes, slimit, soffset, include_xmlns);
6806         RETURN output;
6807     END IF;
6808
6809     SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
6810
6811     IF layout.name IS NULL THEN
6812         RETURN NULL::XML;
6813     END IF;
6814
6815     SELECT * INTO xfrm FROM config.xml_transform WHERE name = layout.transform;
6816
6817     SELECT * INTO me FROM biblio.record_entry WHERE id = obj_id;
6818
6819     -- grab SVF if we need them
6820     IF ('mra' = ANY (includes)) THEN 
6821         axml := unapi.mra(obj_id,NULL,NULL,NULL,NULL);
6822     ELSE
6823         axml := NULL::XML;
6824     END IF;
6825
6826     -- grab hodlings if we need them
6827     IF ('holdings_xml' = ANY (includes)) THEN 
6828         hxml := unapi.holdings_xml(obj_id, ouid, org, depth, evergreen.array_remove_item_by_value(includes,'holdings_xml'), slimit, soffset, include_xmlns);
6829     ELSE
6830         hxml := NULL::XML;
6831     END IF;
6832
6833
6834     -- generate our item node
6835
6836
6837     IF format = 'marcxml' THEN
6838         tmp_xml := me.marc;
6839         IF tmp_xml !~ E'<marc:' THEN -- If we're not using the prefixed namespace in this record, then remove all declarations of it
6840            tmp_xml := REGEXP_REPLACE(tmp_xml, ' xmlns:marc="http://www.loc.gov/MARC21/slim"', '', 'g');
6841         END IF; 
6842     ELSE
6843         tmp_xml := oils_xslt_process(me.marc, xfrm.xslt)::XML;
6844     END IF;
6845
6846     top_el := REGEXP_REPLACE(tmp_xml, E'^.*?<((?:\\S+:)?' || layout.holdings_element || ').*$', E'\\1');
6847
6848     IF axml IS NOT NULL THEN 
6849         tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', axml || '</' || top_el || E'>\\1');
6850     END IF;
6851
6852     IF hxml IS NOT NULL THEN -- XXX how do we configure the holdings position?
6853         tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', hxml || '</' || top_el || E'>\\1');
6854     END IF;
6855
6856     IF ('bre.unapi' = ANY (includes)) THEN 
6857         output := REGEXP_REPLACE(
6858             tmp_xml,
6859             '</' || top_el || '>(.*?)',
6860             XMLELEMENT(
6861                 name abbr,
6862                 XMLATTRIBUTES(
6863                     'http://www.w3.org/1999/xhtml' AS xmlns,
6864                     'unapi-id' AS class,
6865                     'tag:open-ils.org:U2@bre/' || obj_id || '/' || org AS title
6866                 )
6867             )::TEXT || '</' || top_el || E'>\\1'
6868         );
6869     ELSE
6870         output := tmp_xml;
6871     END IF;
6872
6873     output := REGEXP_REPLACE(output::TEXT,E'>\\s+<','><','gs')::XML;
6874     RETURN output;
6875 END;
6876 $F$ LANGUAGE PLPGSQL;
6877
6878 CREATE OR REPLACE FUNCTION unapi.holdings_xml (bid BIGINT, ouid INT, org TEXT, depth INT DEFAULT NULL, includes TEXT[] DEFAULT NULL::TEXT[], slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE) RETURNS XML AS $F$
6879      SELECT  XMLELEMENT(
6880                  name holdings,
6881                  XMLATTRIBUTES(
6882                     CASE WHEN $8 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
6883                     CASE WHEN ('bre' = ANY ($5)) THEN 'tag:open-ils.org:U2@bre/' || $1 || '/' || $3 ELSE NULL END AS id
6884                  ),
6885                  XMLELEMENT(
6886                      name counts,
6887                      (SELECT  XMLAGG(XMLELEMENT::XML) FROM (
6888                          SELECT  XMLELEMENT(
6889                                      name count,
6890                                      XMLATTRIBUTES('public' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
6891                                  )::text
6892                            FROM  asset.opac_ou_record_copy_count($2,  $1)
6893                                      UNION
6894                          SELECT  XMLELEMENT(
6895                                      name count,
6896                                      XMLATTRIBUTES('staff' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
6897                                  )::text
6898                            FROM  asset.staff_ou_record_copy_count($2, $1)
6899                                      ORDER BY 1
6900                      )x)
6901                  ),
6902                  CASE 
6903                      WHEN ('bmp' = ANY ($5)) THEN
6904                         XMLELEMENT(
6905                             name monograph_parts,
6906                             (SELECT XMLAGG(bmp) FROM (
6907                                 SELECT  unapi.bmp( id, 'xml', 'monograph_part', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($5,'bre'), 'holdings_xml'), $3, $4, $6, $7, FALSE)
6908                                   FROM  biblio.monograph_part
6909                                   WHERE record = $1
6910                             )x)
6911                         )
6912                      ELSE NULL
6913                  END,
6914                  XMLELEMENT(
6915                      name volumes,
6916                      (SELECT XMLAGG(acn) FROM (
6917                         SELECT  unapi.acn(acn.id,'xml','volume', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($5,'holdings_xml'),'bre'), $3, $4, $6, $7, FALSE)
6918                           FROM  asset.call_number acn
6919                           WHERE acn.record = $1
6920                                 AND EXISTS (
6921                                     SELECT  1
6922                                       FROM  asset.copy acp
6923                                             JOIN actor.org_unit_descendants(
6924                                                 $2,
6925                                                 (COALESCE(
6926                                                     $4,
6927                                                     (SELECT aout.depth
6928                                                       FROM  actor.org_unit_type aout
6929                                                             JOIN actor.org_unit aou ON (aou.ou_type = aout.id AND aou.id = $2)
6930                                                     )
6931                                                 ))
6932                                             ) aoud ON (acp.circ_lib = aoud.id)
6933                                       LIMIT 1
6934                                )
6935                           ORDER BY label_sortkey
6936                           LIMIT $6
6937                           OFFSET $7
6938                      )x)
6939                  ),
6940                  CASE WHEN ('ssub' = ANY ($5)) THEN 
6941                      XMLELEMENT(
6942                          name subscriptions,
6943                          (SELECT XMLAGG(ssub) FROM (
6944                             SELECT  unapi.ssub(id,'xml','subscription','{}'::TEXT[], $3, $4, $6, $7, FALSE)
6945                               FROM  serial.subscription
6946                               WHERE record_entry = $1
6947                         )x)
6948                      )
6949                  ELSE NULL END,
6950                  CASE WHEN ('acp' = ANY ($5)) THEN 
6951                      XMLELEMENT(
6952                          name foreign_copies,
6953                          (SELECT XMLAGG(acp) FROM (
6954                             SELECT  unapi.acp(p.target_copy,'xml','copy','{}'::TEXT[], $3, $4, $6, $7, FALSE)
6955                               FROM  biblio.peer_bib_copy_map p
6956                                     JOIN asset.copy c ON (p.target_copy = c.id)
6957                               WHERE NOT c.deleted AND peer_record = $1
6958                         )x)
6959                      )
6960                  ELSE NULL END
6961              );
6962 $F$ LANGUAGE SQL;
6963
6964 CREATE OR REPLACE FUNCTION unapi.ssub ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
6965         SELECT  XMLELEMENT(
6966                     name subscription,
6967                     XMLATTRIBUTES(
6968                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
6969                         'tag:open-ils.org:U2@ssub/' || id AS id,
6970                         start_date AS start, end_date AS end, expected_date_offset
6971                     ),
6972                     unapi.aou( owning_lib, $2, 'owning_lib', evergreen.array_remove_item_by_value($4,'ssub'), $5, $6, $7, $8),
6973                     XMLELEMENT( name distributions,
6974                         CASE 
6975                             WHEN ('sdist' = ANY ($4)) THEN
6976                                 (SELECT XMLAGG(sdist) FROM (
6977                                     SELECT  unapi.sdist( id, 'xml', 'distribution', evergreen.array_remove_item_by_value($4,'ssub'), $5, $6, $7, $8, FALSE)
6978                                       FROM  serial.distribution
6979                                       WHERE subscription = ssub.id
6980                                 )x)
6981                             ELSE NULL
6982                         END
6983                     )
6984                 )
6985           FROM  serial.subscription ssub
6986           WHERE id = $1
6987           GROUP BY id, start_date, end_date, expected_date_offset, owning_lib;
6988 $F$ LANGUAGE SQL;
6989
6990 CREATE OR REPLACE FUNCTION unapi.sdist ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
6991         SELECT  XMLELEMENT(
6992                     name distribution,
6993                     XMLATTRIBUTES(
6994                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
6995                         'tag:open-ils.org:U2@sdist/' || id AS id,
6996                         'tag:open-ils.org:U2@acn/' || receive_call_number AS receive_call_number,
6997                         'tag:open-ils.org:U2@acn/' || bind_call_number AS bind_call_number,
6998                         unit_label_prefix, label, unit_label_suffix, summary_method
6999                     ),
7000                     unapi.aou( holding_lib, $2, 'holding_lib', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8),
7001                     CASE WHEN subscription IS NOT NULL AND ('ssub' = ANY ($4)) THEN unapi.ssub( subscription, 'xml', 'subscription', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE) ELSE NULL END,
7002                     XMLELEMENT( name streams,
7003                         CASE 
7004                             WHEN ('sstr' = ANY ($4)) THEN
7005                                 (SELECT XMLAGG(sstr) FROM (
7006                                     SELECT  unapi.sstr( id, 'xml', 'stream', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE)
7007                                       FROM  serial.stream
7008                                       WHERE distribution = sdist.id
7009                                 )x)
7010                             ELSE NULL
7011                         END
7012                     ),
7013                     XMLELEMENT( name summaries,
7014                         CASE 
7015                             WHEN ('ssum' = ANY ($4)) THEN
7016                                 (SELECT XMLAGG(sbsum) FROM (
7017                                     SELECT  unapi.sbsum( id, 'xml', 'serial_summary', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE)
7018                                       FROM  serial.basic_summary
7019                                       WHERE distribution = sdist.id
7020                                 )x)
7021                             ELSE NULL
7022                         END,
7023                         CASE 
7024                             WHEN ('ssum' = ANY ($4)) THEN
7025                                 (SELECT XMLAGG(sisum) FROM (
7026                                     SELECT  unapi.sisum( id, 'xml', 'serial_summary', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE)
7027                                       FROM  serial.index_summary
7028                                       WHERE distribution = sdist.id
7029                                 )x)
7030                             ELSE NULL
7031                         END,
7032                         CASE 
7033                             WHEN ('ssum' = ANY ($4)) THEN
7034                                 (SELECT XMLAGG(sssum) FROM (
7035                                     SELECT  unapi.sssum( id, 'xml', 'serial_summary', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE)
7036                                       FROM  serial.supplement_summary
7037                                       WHERE distribution = sdist.id
7038                                 )x)
7039                             ELSE NULL
7040                         END
7041                     )
7042                 )
7043           FROM  serial.distribution sdist
7044           WHERE id = $1
7045           GROUP BY id, label, unit_label_prefix, unit_label_suffix, holding_lib, summary_method, subscription, receive_call_number, bind_call_number;
7046 $F$ LANGUAGE SQL;
7047
7048 CREATE OR REPLACE FUNCTION unapi.sstr ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
7049     SELECT  XMLELEMENT(
7050                 name stream,
7051                 XMLATTRIBUTES(
7052                     CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
7053                     'tag:open-ils.org:U2@sstr/' || id AS id,
7054                     routing_label
7055                 ),
7056                 CASE WHEN distribution IS NOT NULL AND ('sdist' = ANY ($4)) THEN unapi.sssum( distribution, 'xml', 'distribtion', evergreen.array_remove_item_by_value($4,'sstr'), $5, $6, $7, $8, FALSE) ELSE NULL END,
7057                 XMLELEMENT( name items,
7058                     CASE 
7059                         WHEN ('sitem' = ANY ($4)) THEN
7060                             (SELECT XMLAGG(sitem) FROM (
7061                                 SELECT  unapi.sitem( id, 'xml', 'serial_item', evergreen.array_remove_item_by_value($4,'sstr'), $5, $6, $7, $8, FALSE)
7062                                   FROM  serial.item
7063                                   WHERE stream = sstr.id
7064                             )x)
7065                         ELSE NULL
7066                     END
7067                 )
7068             )
7069       FROM  serial.stream sstr
7070       WHERE id = $1
7071       GROUP BY id, routing_label, distribution;
7072 $F$ LANGUAGE SQL;
7073
7074 CREATE OR REPLACE FUNCTION unapi.siss ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
7075     SELECT  XMLELEMENT(
7076                 name issuance,
7077                 XMLATTRIBUTES(
7078                     CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
7079                     'tag:open-ils.org:U2@siss/' || id AS id,
7080                     create_date, edit_date, label, date_published,
7081                     holding_code, holding_type, holding_link_id
7082                 ),
7083                 CASE WHEN subscription IS NOT NULL AND ('ssub' = ANY ($4)) THEN unapi.ssub( subscription, 'xml', 'subscription', evergreen.array_remove_item_by_value($4,'siss'), $5, $6, $7, $8, FALSE) ELSE NULL END,
7084                 XMLELEMENT( name items,
7085                     CASE 
7086                         WHEN ('sitem' = ANY ($4)) THEN
7087                             (SELECT XMLAGG(sitem) FROM (
7088                                 SELECT  unapi.sitem( id, 'xml', 'serial_item', evergreen.array_remove_item_by_value($4,'siss'), $5, $6, $7, $8, FALSE)
7089                                   FROM  serial.item
7090                                   WHERE issuance = sstr.id
7091                             )x)
7092                         ELSE NULL
7093                     END
7094                 )
7095             )
7096       FROM  serial.issuance sstr
7097       WHERE id = $1
7098       GROUP BY id, create_date, edit_date, label, date_published, holding_code, holding_type, holding_link_id, subscription;
7099 $F$ LANGUAGE SQL;
7100
7101 CREATE OR REPLACE FUNCTION unapi.sitem ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
7102         SELECT  XMLELEMENT(
7103                     name serial_item,
7104                     XMLATTRIBUTES(
7105                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
7106                         'tag:open-ils.org:U2@sitem/' || id AS id,
7107                         'tag:open-ils.org:U2@siss/' || issuance AS issuance,
7108                         date_expected, date_received
7109                     ),
7110                     CASE WHEN issuance IS NOT NULL AND ('siss' = ANY ($4)) THEN unapi.siss( issuance, $2, 'issuance', evergreen.array_remove_item_by_value($4,'sitem'), $5, $6, $7, $8, FALSE) ELSE NULL END,
7111                     CASE WHEN stream IS NOT NULL AND ('sstr' = ANY ($4)) THEN unapi.sstr( stream, $2, 'stream', evergreen.array_remove_item_by_value($4,'sitem'), $5, $6, $7, $8, FALSE) ELSE NULL END,
7112                     CASE WHEN unit IS NOT NULL AND ('sunit' = ANY ($4)) THEN unapi.sunit( stream, $2, 'serial_unit', evergreen.array_remove_item_by_value($4,'sitem'), $5, $6, $7, $8, FALSE) ELSE NULL END,
7113                     CASE WHEN uri IS NOT NULL AND ('auri' = ANY ($4)) THEN unapi.auri( uri, $2, 'uri', evergreen.array_remove_item_by_value($4,'sitem'), $5, $6, $7, $8, FALSE) ELSE NULL END
7114 --                    XMLELEMENT( name notes,
7115 --                        CASE 
7116 --                            WHEN ('acpn' = ANY ($4)) THEN
7117 --                                (SELECT XMLAGG(acpn) FROM (
7118 --                                    SELECT  unapi.acpn( id, 'xml', 'copy_note', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8)
7119 --                                      FROM  asset.copy_note
7120 --                                      WHERE owning_copy = cp.id AND pub
7121 --                                )x)
7122 --                            ELSE NULL
7123 --                        END
7124 --                    )
7125                 )
7126           FROM  serial.item sitem
7127           WHERE id = $1;
7128 $F$ LANGUAGE SQL;
7129
7130
7131 CREATE OR REPLACE FUNCTION unapi.bmp ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
7132         SELECT  XMLELEMENT(
7133                     name monograph_part,
7134                     XMLATTRIBUTES(
7135                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
7136                         'tag:open-ils.org:U2@bmp/' || id AS id,
7137                         id AS ident,
7138                         label,
7139                         label_sortkey,
7140                         'tag:open-ils.org:U2@bre/' || record AS record
7141                     ),
7142                     CASE 
7143                         WHEN ('acp' = ANY ($4)) THEN
7144                             XMLELEMENT( name copies,
7145                                 (SELECT XMLAGG(acp) FROM (
7146                                     SELECT  unapi.acp( cp.id, 'xml', 'copy', evergreen.array_remove_item_by_value($4,'bmp'), $5, $6, $7, $8, FALSE)
7147                                       FROM  asset.copy cp
7148                                             JOIN asset.copy_part_map cpm ON (cpm.target_copy = cp.id)
7149                                       WHERE cpm.part = $1
7150                                       ORDER BY COALESCE(cp.copy_number,0), cp.barcode
7151                                       LIMIT $7
7152                                       OFFSET $8
7153                                 )x)
7154                             )
7155                         ELSE NULL
7156                     END,
7157                     CASE WHEN ('bre' = ANY ($4)) THEN unapi.bre( record, 'marcxml', 'record', evergreen.array_remove_item_by_value($4,'bmp'), $5, $6, $7, $8, FALSE) ELSE NULL END
7158                 )
7159           FROM  biblio.monograph_part
7160           WHERE id = $1
7161           GROUP BY id, label, label_sortkey, record;
7162 $F$ LANGUAGE SQL;
7163
7164 CREATE OR REPLACE FUNCTION unapi.acp ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
7165         SELECT  XMLELEMENT(
7166                     name copy,
7167                     XMLATTRIBUTES(
7168                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
7169                         'tag:open-ils.org:U2@acp/' || id AS id,
7170                         create_date, edit_date, copy_number, circulate, deposit,
7171                         ref, holdable, deleted, deposit_amount, price, barcode,
7172                         circ_modifier, circ_as_type, opac_visible
7173                     ),
7174                     unapi.ccs( status, $2, 'status', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE),
7175                     unapi.acl( location, $2, 'location', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE),
7176                     unapi.aou( circ_lib, $2, 'circ_lib', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8),
7177                     unapi.aou( circ_lib, $2, 'circlib', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8),
7178                     CASE WHEN ('acn' = ANY ($4)) THEN unapi.acn( call_number, $2, 'call_number', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE) ELSE NULL END,
7179                     XMLELEMENT( name copy_notes,
7180                         CASE 
7181                             WHEN ('acpn' = ANY ($4)) THEN
7182                                 (SELECT XMLAGG(acpn) FROM (
7183                                     SELECT  unapi.acpn( id, 'xml', 'copy_note', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE)
7184                                       FROM  asset.copy_note
7185                                       WHERE owning_copy = cp.id AND pub
7186                                 )x)
7187                             ELSE NULL
7188                         END
7189                     ),
7190                     XMLELEMENT( name statcats,
7191                         CASE 
7192                             WHEN ('ascecm' = ANY ($4)) THEN
7193                                 (SELECT XMLAGG(ascecm) FROM (
7194                                     SELECT  unapi.ascecm( stat_cat_entry, 'xml', 'statcat', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE)
7195                                       FROM  asset.stat_cat_entry_copy_map
7196                                       WHERE owning_copy = cp.id
7197                                 )x)
7198                             ELSE NULL
7199                         END
7200                     ),
7201                     XMLELEMENT( name foreign_records,
7202                         CASE
7203                             WHEN ('bre' = ANY ($4)) THEN
7204                                 (SELECT XMLAGG(bre) FROM (
7205                                     SELECT  unapi.bre(peer_record,'marcxml','record','{}'::TEXT[], $5, $6, $7, $8, FALSE)
7206                                       FROM  biblio.peer_bib_copy_map
7207                                       WHERE target_copy = cp.id
7208                                 )x)
7209                             ELSE NULL
7210                         END
7211
7212                     ),
7213                     CASE 
7214                         WHEN ('bmp' = ANY ($4)) THEN
7215                             XMLELEMENT( name monograph_parts,
7216                                 (SELECT XMLAGG(bmp) FROM (
7217                                     SELECT  unapi.bmp( part, 'xml', 'monograph_part', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE)
7218                                       FROM  asset.copy_part_map
7219                                       WHERE target_copy = cp.id
7220                                 )x)
7221                             )
7222                         ELSE NULL
7223                     END
7224                 )
7225           FROM  asset.copy cp
7226           WHERE id = $1
7227           GROUP BY id, status, location, circ_lib, call_number, create_date, edit_date, copy_number, circulate, deposit, ref, holdable, deleted, deposit_amount, price, barcode, circ_modifier, circ_as_type, opac_visible;
7228 $F$ LANGUAGE SQL;
7229
7230 CREATE OR REPLACE FUNCTION unapi.sunit ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
7231         SELECT  XMLELEMENT(
7232                     name serial_unit,
7233                     XMLATTRIBUTES(
7234                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
7235                         'tag:open-ils.org:U2@acp/' || id AS id,
7236                         create_date, edit_date, copy_number, circulate, deposit,
7237                         ref, holdable, deleted, deposit_amount, price, barcode,
7238                         circ_modifier, circ_as_type, opac_visible, status_changed_time,
7239                         floating, mint_condition, detailed_contents, sort_key, summary_contents, cost 
7240                     ),
7241                     unapi.ccs( status, $2, 'status', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($4,'acp'),'sunit'), $5, $6, $7, $8, FALSE),
7242                     unapi.acl( location, $2, 'location', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($4,'acp'),'sunit'), $5, $6, $7, $8, FALSE),
7243                     unapi.aou( circ_lib, $2, 'circ_lib', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($4,'acp'),'sunit'), $5, $6, $7, $8),
7244                     unapi.aou( circ_lib, $2, 'circlib', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($4,'acp'),'sunit'), $5, $6, $7, $8),
7245                     CASE WHEN ('acn' = ANY ($4)) THEN unapi.acn( call_number, $2, 'call_number', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE) ELSE NULL END,
7246                     XMLELEMENT( name copy_notes,
7247                         CASE 
7248                             WHEN ('acpn' = ANY ($4)) THEN
7249                                 (SELECT XMLAGG(acpn) FROM (
7250                                     SELECT  unapi.acpn( id, 'xml', 'copy_note', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($4,'acp'),'sunit'), $5, $6, $7, $8, FALSE)
7251                                       FROM  asset.copy_note
7252                                       WHERE owning_copy = cp.id AND pub
7253                                 )x)
7254                             ELSE NULL
7255                         END
7256                     ),
7257                     XMLELEMENT( name statcats,
7258                         CASE 
7259                             WHEN ('ascecm' = ANY ($4)) THEN
7260                                 (SELECT XMLAGG(ascecm) FROM (
7261                                     SELECT  unapi.ascecm( stat_cat_entry, 'xml', 'statcat', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE)
7262                                       FROM  asset.stat_cat_entry_copy_map
7263                                       WHERE owning_copy = cp.id
7264                                 )x)
7265                             ELSE NULL
7266                         END
7267                     ),
7268                     XMLELEMENT( name foreign_records,
7269                         CASE
7270                             WHEN ('bre' = ANY ($4)) THEN
7271                                 (SELECT XMLAGG(bre) FROM (
7272                                     SELECT  unapi.bre(peer_record,'marcxml','record','{}'::TEXT[], $5, $6, $7, $8, FALSE)
7273                                       FROM  biblio.peer_bib_copy_map
7274                                       WHERE target_copy = cp.id
7275                                 )x)
7276                             ELSE NULL
7277                         END
7278
7279                     ),
7280                     CASE 
7281                         WHEN ('bmp' = ANY ($4)) THEN
7282                             XMLELEMENT( name monograph_parts,
7283                                 (SELECT XMLAGG(bmp) FROM (
7284                                     SELECT  unapi.bmp( part, 'xml', 'monograph_part', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE)
7285                                       FROM  asset.copy_part_map
7286                                       WHERE target_copy = cp.id
7287                                 )x)
7288                             )
7289                         ELSE NULL
7290                     END
7291                 )
7292           FROM  serial.unit cp
7293           WHERE id = $1
7294           GROUP BY  id, status, location, circ_lib, call_number, create_date, edit_date, copy_number, circulate, floating, mint_condition,
7295                     deposit, ref, holdable, deleted, deposit_amount, price, barcode, circ_modifier, circ_as_type, opac_visible, status_changed_time, detailed_contents, sort_key, summary_contents, cost;
7296 $F$ LANGUAGE SQL;
7297
7298 INSERT INTO config.upgrade_log (version) VALUES ('0568'); -- miker for tsbere
7299
7300 CREATE OR REPLACE FUNCTION asset.cache_copy_visibility () RETURNS TRIGGER as $func$
7301 DECLARE
7302     add_front       TEXT;
7303     add_back        TEXT;
7304     add_base_query  TEXT;
7305     add_peer_query  TEXT;
7306     remove_query    TEXT;
7307     do_add          BOOLEAN := false;
7308     do_remove       BOOLEAN := false;
7309 BEGIN
7310     add_base_query := $$
7311         SELECT  cp.id, cp.circ_lib, cn.record, cn.id AS call_number, cp.location, cp.status
7312           FROM  asset.copy cp
7313                 JOIN asset.call_number cn ON (cn.id = cp.call_number)
7314                 JOIN actor.org_unit a ON (cp.circ_lib = a.id)
7315                 JOIN asset.copy_location cl ON (cp.location = cl.id)
7316                 JOIN config.copy_status cs ON (cp.status = cs.id)
7317                 JOIN biblio.record_entry b ON (cn.record = b.id)
7318           WHERE NOT cp.deleted
7319                 AND NOT cn.deleted
7320                 AND NOT b.deleted
7321                 AND cs.opac_visible
7322                 AND cl.opac_visible
7323                 AND cp.opac_visible
7324                 AND a.opac_visible
7325     $$;
7326     add_peer_query := $$
7327         SELECT  cp.id, cp.circ_lib, pbcm.peer_record AS record, NULL AS call_number, cp.location, cp.status
7328           FROM  asset.copy cp
7329                 JOIN biblio.peer_bib_copy_map pbcm ON (pbcm.target_copy = cp.id)
7330                 JOIN actor.org_unit a ON (cp.circ_lib = a.id)
7331                 JOIN asset.copy_location cl ON (cp.location = cl.id)
7332                 JOIN config.copy_status cs ON (cp.status = cs.id)
7333           WHERE NOT cp.deleted
7334                 AND cs.opac_visible
7335                 AND cl.opac_visible
7336                 AND cp.opac_visible
7337                 AND a.opac_visible
7338     $$;
7339     add_front := $$
7340         INSERT INTO asset.opac_visible_copies (copy_id, circ_lib, record)
7341           SELECT id, circ_lib, record FROM (
7342     $$;
7343     add_back := $$
7344         ) AS x
7345     $$;
7346  
7347     remove_query := $$ DELETE FROM asset.opac_visible_copies WHERE copy_id IN ( SELECT id FROM asset.copy WHERE $$;
7348
7349     IF TG_TABLE_NAME = 'peer_bib_copy_map' THEN
7350         IF TG_OP = 'INSERT' THEN
7351             add_peer_query := add_peer_query || ' AND cp.id = ' || NEW.target_copy || ' AND pbcm.record = ' || NEW.peer_record;
7352             EXECUTE add_front || add_peer_query || add_back;
7353             RETURN NEW;
7354         ELSE
7355             remove_query := 'DELETE FROM asset.opac_visible_copies WHERE copy_id = ' || OLD.target_copy || ' AND record = ' || OLD.peer_record || ';';
7356             EXECUTE remove_query;
7357             RETURN OLD;
7358         END IF;
7359     END IF;
7360
7361     IF TG_OP = 'INSERT' THEN
7362
7363         IF TG_TABLE_NAME IN ('copy', 'unit') THEN
7364             add_base_query := add_base_query || ' AND cp.id = ' || NEW.id;
7365             EXECUTE add_front || add_base_query || add_back;
7366         END IF;
7367
7368         RETURN NEW;
7369
7370     END IF;
7371
7372     -- handle items first, since with circulation activity
7373     -- their statuses change frequently
7374     IF TG_TABLE_NAME IN ('copy', 'unit') THEN
7375
7376         IF OLD.location    <> NEW.location OR
7377            OLD.call_number <> NEW.call_number OR
7378            OLD.status      <> NEW.status OR
7379            OLD.circ_lib    <> NEW.circ_lib THEN
7380             -- any of these could change visibility, but
7381             -- we'll save some queries and not try to calculate
7382             -- the change directly
7383             do_remove := true;
7384             do_add := true;
7385         ELSE
7386
7387             IF OLD.deleted <> NEW.deleted THEN
7388                 IF NEW.deleted THEN
7389                     do_remove := true;
7390                 ELSE
7391                     do_add := true;
7392                 END IF;
7393             END IF;
7394
7395             IF OLD.opac_visible <> NEW.opac_visible THEN
7396                 IF OLD.opac_visible THEN
7397                     do_remove := true;
7398                 ELSIF NOT do_remove THEN -- handle edge case where deleted item
7399                                         -- is also marked opac_visible
7400                     do_add := true;
7401                 END IF;
7402             END IF;
7403
7404         END IF;
7405
7406         IF do_remove THEN
7407             DELETE FROM asset.opac_visible_copies WHERE copy_id = NEW.id;
7408         END IF;
7409         IF do_add THEN
7410             add_base_query := add_base_query || ' AND cp.id = ' || NEW.id;
7411             add_peer_query := add_peer_query || ' AND cp.id = ' || NEW.id;
7412             EXECUTE add_front || add_base_query || ' UNION ' || add_peer_query || add_back;
7413         END IF;
7414
7415         RETURN NEW;
7416
7417     END IF;
7418
7419     IF TG_TABLE_NAME IN ('call_number', 'record_entry') THEN -- these have a 'deleted' column
7420  
7421         IF OLD.deleted AND NEW.deleted THEN -- do nothing
7422
7423             RETURN NEW;
7424  
7425         ELSIF NEW.deleted THEN -- remove rows
7426  
7427             IF TG_TABLE_NAME = 'call_number' THEN
7428                 DELETE FROM asset.opac_visible_copies WHERE copy_id IN (SELECT id FROM asset.copy WHERE call_number = NEW.id);
7429             ELSIF TG_TABLE_NAME = 'record_entry' THEN
7430                 DELETE FROM asset.opac_visible_copies WHERE record = NEW.id;
7431             END IF;
7432  
7433             RETURN NEW;
7434  
7435         ELSIF OLD.deleted THEN -- add rows
7436  
7437             IF TG_TABLE_NAME = 'call_number' THEN
7438                 add_base_query := add_base_query || ' AND cn.id = ' || NEW.id;
7439                 EXECUTE add_front || add_base_query || add_back;
7440             ELSIF TG_TABLE_NAME = 'record_entry' THEN
7441                 add_base_query := add_base_query || ' AND cn.record = ' || NEW.id;
7442                 add_peer_query := add_peer_query || ' AND pbcm.record = ' || NEW.id;
7443                 EXECUTE add_front || add_base_query || ' UNION ' || add_peer_query || add_back;
7444             END IF;
7445  
7446             RETURN NEW;
7447  
7448         END IF;
7449  
7450     END IF;
7451
7452     IF TG_TABLE_NAME = 'call_number' THEN
7453
7454         IF OLD.record <> NEW.record THEN
7455             -- call number is linked to different bib
7456             remove_query := remove_query || 'call_number = ' || NEW.id || ');';
7457             EXECUTE remove_query;
7458             add_base_query := add_base_query || ' AND cn.id = ' || NEW.id;
7459             EXECUTE add_front || add_base_query || add_back;
7460         END IF;
7461
7462         RETURN NEW;
7463
7464     END IF;
7465
7466     IF TG_TABLE_NAME IN ('record_entry') THEN
7467         RETURN NEW; -- don't have 'opac_visible'
7468     END IF;
7469
7470     -- actor.org_unit, asset.copy_location, asset.copy_status
7471     IF NEW.opac_visible = OLD.opac_visible THEN -- do nothing
7472
7473         RETURN NEW;
7474
7475     ELSIF NEW.opac_visible THEN -- add rows
7476
7477         IF TG_TABLE_NAME = 'org_unit' THEN
7478             add_base_query := add_base_query || ' AND cp.circ_lib = ' || NEW.id || ';';
7479             add_peer_query := add_peer_query || ' AND cp.circ_lib = ' || NEW.id || ';';
7480         ELSIF TG_TABLE_NAME = 'copy_location' THEN
7481             add_base_query := add_base_query || ' AND cp.location = ' || NEW.id || ';';
7482             add_peer_query := add_peer_query || ' AND cp.location = ' || NEW.id || ';';
7483         ELSIF TG_TABLE_NAME = 'copy_status' THEN
7484             add_base_query := add_base_query || ' AND cp.status = ' || NEW.id || ';';
7485             add_peer_query := add_peer_query || ' AND cp.status = ' || NEW.id || ';';
7486         END IF;
7487  
7488         EXECUTE add_front || add_base_query || ' UNION ' || add_peer_query || add_back;
7489  
7490     ELSE -- delete rows
7491
7492         IF TG_TABLE_NAME = 'org_unit' THEN
7493             remove_query := 'DELETE FROM asset.opac_visible_copies WHERE circ_lib = ' || NEW.id || ';';
7494         ELSIF TG_TABLE_NAME = 'copy_location' THEN
7495             remove_query := remove_query || 'location = ' || NEW.id || ');';
7496         ELSIF TG_TABLE_NAME = 'copy_status' THEN
7497             remove_query := remove_query || 'status = ' || NEW.id || ');';
7498         END IF;
7499  
7500         EXECUTE remove_query;
7501  
7502     END IF;
7503  
7504     RETURN NEW;
7505 END;
7506 $func$ LANGUAGE PLPGSQL;
7507
7508 INSERT INTO config.upgrade_log (version) VALUES ('0569'); --miker
7509
7510 CREATE OR REPLACE FUNCTION unapi.auri ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
7511         SELECT  XMLELEMENT(
7512                     name uri,
7513                     XMLATTRIBUTES(
7514                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
7515                         'tag:open-ils.org:U2@auri/' || uri.id AS id,
7516                         use_restriction,
7517                         href,
7518                         label
7519                     ),
7520                     XMLELEMENT( name copies,
7521                         CASE
7522                             WHEN ('acn' = ANY ($4)) THEN
7523                                 (SELECT XMLAGG(acn) FROM (SELECT unapi.acn( call_number, 'xml', 'copy', evergreen.array_remove_item_by_value($4,'auri'), $5, $6, $7, $8, FALSE) FROM asset.uri_call_number_map WHERE uri = uri.id)x)
7524                             ELSE NULL
7525                         END
7526                     )
7527                 ) AS x
7528           FROM  asset.uri uri
7529           WHERE uri.id = $1
7530           GROUP BY uri.id, use_restriction, href, label;
7531 $F$ LANGUAGE SQL;
7532
7533 INSERT INTO config.upgrade_log (version) VALUES ('0570');
7534
7535 -- Not everything in 1XX tags should become part of the authorsort field
7536 -- ($0 for example).  The list of subfields chosen here is a superset of all
7537 -- the fields found in the LoC authority mappin definitions for 1XX fields.
7538 -- Anyway, if more fields should be here, add them.
7539
7540 UPDATE config.record_attr_definition
7541     SET sf_list = 'abcdefgklmnopqrstvxyz'
7542     WHERE name='authorsort' AND sf_list IS NULL;
7543
7544 INSERT INTO config.upgrade_log (version) VALUES ('0571');
7545
7546 -- FIXME: add/check SQL statements to perform the upgrade
7547 CREATE OR REPLACE FUNCTION metabib.facet_normalize_trigger () RETURNS TRIGGER AS $$
7548 DECLARE
7549     normalizer  RECORD;
7550     facet_text  TEXT;
7551 BEGIN
7552     facet_text := NEW.value;
7553
7554     FOR normalizer IN
7555         SELECT  n.func AS func,
7556                 n.param_count AS param_count,
7557                 m.params AS params
7558           FROM  config.index_normalizer n
7559                 JOIN config.metabib_field_index_norm_map m ON (m.norm = n.id)
7560           WHERE m.field = NEW.field AND m.pos < 0
7561           ORDER BY m.pos LOOP
7562
7563             EXECUTE 'SELECT ' || normalizer.func || '(' ||
7564                 quote_literal( facet_text ) ||
7565                 CASE
7566                     WHEN normalizer.param_count > 0
7567                         THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
7568                         ELSE ''
7569                     END ||
7570                 ')' INTO facet_text;
7571
7572     END LOOP;
7573
7574     NEW.value = facet_text;
7575
7576     RETURN NEW;
7577 END;
7578 $$ LANGUAGE PLPGSQL;
7579
7580 CREATE TRIGGER facet_normalize_tgr
7581     BEFORE UPDATE OR INSERT ON metabib.facet_entry
7582     FOR EACH ROW EXECUTE PROCEDURE metabib.facet_normalize_trigger();
7583
7584
7585
7586 INSERT INTO config.upgrade_log (version) VALUES ('0578'); -- tsbere via miker
7587
7588 CREATE OR REPLACE VIEW reporter.hold_request_record AS
7589 SELECT  id,
7590         target,
7591         hold_type,
7592         CASE
7593                 WHEN hold_type = 'T'
7594                         THEN target
7595                 WHEN hold_type = 'I'
7596                         THEN (SELECT ssub.record_entry FROM serial.subscription ssub JOIN serial.issuance si ON (si.subscription = ssub.id) WHERE si.id = ahr.target)
7597                 WHEN hold_type = 'V'
7598                         THEN (SELECT cn.record FROM asset.call_number cn WHERE cn.id = ahr.target)
7599                 WHEN hold_type IN ('C','R','F')
7600                         THEN (SELECT cn.record FROM asset.call_number cn JOIN asset.copy cp ON (cn.id = cp.call_number) WHERE cp.id = ahr.target)
7601                 WHEN hold_type = 'M'
7602                         THEN (SELECT mr.master_record FROM metabib.metarecord mr WHERE mr.id = ahr.target)
7603         WHEN hold_type = 'P'
7604             THEN (SELECT bmp.record FROM biblio.monograph_part bmp WHERE bmp.id = ahr.target)
7605         END AS bib_record
7606   FROM  action.hold_request ahr;
7607
7608 INSERT INTO config.upgrade_log (version) VALUES ('0583');
7609
7610 CREATE OR REPLACE VIEW action.all_circulation AS
7611     SELECT  id,usr_post_code, usr_home_ou, usr_profile, usr_birth_year, copy_call_number, copy_location,
7612         copy_owning_lib, copy_circ_lib, copy_bib_record, xact_start, xact_finish, target_copy,
7613         circ_lib, circ_staff, checkin_staff, checkin_lib, renewal_remaining, grace_period, due_date,
7614         stop_fines_time, checkin_time, create_time, duration, fine_interval, recurring_fine,
7615         max_fine, phone_renewal, desk_renewal, opac_renewal, duration_rule, recurring_fine_rule,
7616         max_fine_rule, stop_fines, workstation, checkin_workstation, checkin_scan_time, parent_circ
7617       FROM  action.aged_circulation
7618             UNION ALL
7619     SELECT  DISTINCT circ.id,COALESCE(a.post_code,b.post_code) AS usr_post_code, p.home_ou AS usr_home_ou, p.profile AS usr_profile, EXTRACT(YEAR FROM p.dob)::INT AS usr_birth_year,
7620         cp.call_number AS copy_call_number, cp.location AS copy_location, cn.owning_lib AS copy_owning_lib, cp.circ_lib AS copy_circ_lib,
7621         cn.record AS copy_bib_record, circ.xact_start, circ.xact_finish, circ.target_copy, circ.circ_lib, circ.circ_staff, circ.checkin_staff,
7622         circ.checkin_lib, circ.renewal_remaining, circ.grace_period, circ.due_date, circ.stop_fines_time, circ.checkin_time, circ.create_time, circ.duration,
7623         circ.fine_interval, circ.recurring_fine, circ.max_fine, circ.phone_renewal, circ.desk_renewal, circ.opac_renewal, circ.duration_rule,
7624         circ.recurring_fine_rule, circ.max_fine_rule, circ.stop_fines, circ.workstation, circ.checkin_workstation, circ.checkin_scan_time,
7625         circ.parent_circ
7626       FROM  action.circulation circ
7627         JOIN asset.copy cp ON (circ.target_copy = cp.id)
7628         JOIN asset.call_number cn ON (cp.call_number = cn.id)
7629         JOIN actor.usr p ON (circ.usr = p.id)
7630         LEFT JOIN actor.usr_address a ON (p.mailing_address = a.id)
7631         LEFT JOIN actor.usr_address b ON (p.billing_address = b.id);
7632
7633
7634
7635 INSERT INTO config.upgrade_log (version) VALUES ('0590'); -- miker/tsbere
7636
7637 CREATE OR REPLACE FUNCTION asset.cache_copy_visibility () RETURNS TRIGGER as $func$
7638 DECLARE
7639     add_front       TEXT;
7640     add_back        TEXT;
7641     add_base_query  TEXT;
7642     add_peer_query  TEXT;
7643     remove_query    TEXT;
7644     do_add          BOOLEAN := false;
7645     do_remove       BOOLEAN := false;
7646 BEGIN
7647     add_base_query := $$
7648         SELECT  cp.id, cp.circ_lib, cn.record, cn.id AS call_number, cp.location, cp.status
7649           FROM  asset.copy cp
7650                 JOIN asset.call_number cn ON (cn.id = cp.call_number)
7651                 JOIN actor.org_unit a ON (cp.circ_lib = a.id)
7652                 JOIN asset.copy_location cl ON (cp.location = cl.id)
7653                 JOIN config.copy_status cs ON (cp.status = cs.id)
7654                 JOIN biblio.record_entry b ON (cn.record = b.id)
7655           WHERE NOT cp.deleted
7656                 AND NOT cn.deleted
7657                 AND NOT b.deleted
7658                 AND cs.opac_visible
7659                 AND cl.opac_visible
7660                 AND cp.opac_visible
7661                 AND a.opac_visible
7662     $$;
7663     add_peer_query := $$
7664         SELECT  cp.id, cp.circ_lib, pbcm.peer_record AS record, NULL AS call_number, cp.location, cp.status
7665           FROM  asset.copy cp
7666                 JOIN biblio.peer_bib_copy_map pbcm ON (pbcm.target_copy = cp.id)
7667                 JOIN actor.org_unit a ON (cp.circ_lib = a.id)
7668                 JOIN asset.copy_location cl ON (cp.location = cl.id)
7669                 JOIN config.copy_status cs ON (cp.status = cs.id)
7670           WHERE NOT cp.deleted
7671                 AND cs.opac_visible
7672                 AND cl.opac_visible
7673                 AND cp.opac_visible
7674                 AND a.opac_visible
7675     $$;
7676     add_front := $$
7677         INSERT INTO asset.opac_visible_copies (copy_id, circ_lib, record)
7678           SELECT id, circ_lib, record FROM (
7679     $$;
7680     add_back := $$
7681         ) AS x
7682     $$;
7683  
7684     remove_query := $$ DELETE FROM asset.opac_visible_copies WHERE copy_id IN ( SELECT id FROM asset.copy WHERE $$;
7685
7686     IF TG_TABLE_NAME = 'peer_bib_copy_map' THEN
7687         IF TG_OP = 'INSERT' THEN
7688             add_peer_query := add_peer_query || ' AND cp.id = ' || NEW.target_copy || ' AND pbcm.record = ' || NEW.peer_record;
7689             EXECUTE add_front || add_peer_query || add_back;
7690             RETURN NEW;
7691         ELSE
7692             remove_query := 'DELETE FROM asset.opac_visible_copies WHERE copy_id = ' || OLD.target_copy || ' AND record = ' || OLD.peer_record || ';';
7693             EXECUTE remove_query;
7694             RETURN OLD;
7695         END IF;
7696     END IF;
7697
7698     IF TG_OP = 'INSERT' THEN
7699
7700         IF TG_TABLE_NAME IN ('copy', 'unit') THEN
7701             add_base_query := add_base_query || ' AND cp.id = ' || NEW.id;
7702             EXECUTE add_front || add_base_query || add_back;
7703         END IF;
7704
7705         RETURN NEW;
7706
7707     END IF;
7708
7709     -- handle items first, since with circulation activity
7710     -- their statuses change frequently
7711     IF TG_TABLE_NAME IN ('copy', 'unit') THEN
7712
7713         IF OLD.location    <> NEW.location OR
7714            OLD.call_number <> NEW.call_number OR
7715            OLD.status      <> NEW.status OR
7716            OLD.circ_lib    <> NEW.circ_lib THEN
7717             -- any of these could change visibility, but
7718             -- we'll save some queries and not try to calculate
7719             -- the change directly
7720             do_remove := true;
7721             do_add := true;
7722         ELSE
7723
7724             IF OLD.deleted <> NEW.deleted THEN
7725                 IF NEW.deleted THEN
7726                     do_remove := true;
7727                 ELSE
7728                     do_add := true;
7729                 END IF;
7730             END IF;
7731
7732             IF OLD.opac_visible <> NEW.opac_visible THEN
7733                 IF OLD.opac_visible THEN
7734                     do_remove := true;
7735                 ELSIF NOT do_remove THEN -- handle edge case where deleted item
7736                                         -- is also marked opac_visible
7737                     do_add := true;
7738                 END IF;
7739             END IF;
7740
7741         END IF;
7742
7743         IF do_remove THEN
7744             DELETE FROM asset.opac_visible_copies WHERE copy_id = NEW.id;
7745         END IF;
7746         IF do_add THEN
7747             add_base_query := add_base_query || ' AND cp.id = ' || NEW.id;
7748             add_peer_query := add_peer_query || ' AND cp.id = ' || NEW.id;
7749             EXECUTE add_front || add_base_query || ' UNION ' || add_peer_query || add_back;
7750         END IF;
7751
7752         RETURN NEW;
7753
7754     END IF;
7755
7756     IF TG_TABLE_NAME IN ('call_number', 'record_entry') THEN -- these have a 'deleted' column
7757  
7758         IF OLD.deleted AND NEW.deleted THEN -- do nothing
7759
7760             RETURN NEW;
7761  
7762         ELSIF NEW.deleted THEN -- remove rows
7763  
7764             IF TG_TABLE_NAME = 'call_number' THEN
7765                 DELETE FROM asset.opac_visible_copies WHERE copy_id IN (SELECT id FROM asset.copy WHERE call_number = NEW.id);
7766             ELSIF TG_TABLE_NAME = 'record_entry' THEN
7767                 DELETE FROM asset.opac_visible_copies WHERE record = NEW.id;
7768             END IF;
7769  
7770             RETURN NEW;
7771  
7772         ELSIF OLD.deleted THEN -- add rows
7773  
7774             IF TG_TABLE_NAME = 'call_number' THEN
7775                 add_base_query := add_base_query || ' AND cn.id = ' || NEW.id;
7776                 EXECUTE add_front || add_base_query || add_back;
7777             ELSIF TG_TABLE_NAME = 'record_entry' THEN
7778                 add_base_query := add_base_query || ' AND cn.record = ' || NEW.id;
7779                 add_peer_query := add_peer_query || ' AND pbcm.record = ' || NEW.id;
7780                 EXECUTE add_front || add_base_query || ' UNION ' || add_peer_query || add_back;
7781             END IF;
7782  
7783             RETURN NEW;
7784  
7785         END IF;
7786  
7787     END IF;
7788
7789     IF TG_TABLE_NAME = 'call_number' THEN
7790
7791         IF OLD.record <> NEW.record THEN
7792             -- call number is linked to different bib
7793             remove_query := remove_query || 'call_number = ' || NEW.id || ');';
7794             EXECUTE remove_query;
7795             add_base_query := add_base_query || ' AND cn.id = ' || NEW.id;
7796             EXECUTE add_front || add_base_query || add_back;
7797         END IF;
7798
7799         RETURN NEW;
7800
7801     END IF;
7802
7803     IF TG_TABLE_NAME IN ('record_entry') THEN
7804         RETURN NEW; -- don't have 'opac_visible'
7805     END IF;
7806
7807     -- actor.org_unit, asset.copy_location, asset.copy_status
7808     IF NEW.opac_visible = OLD.opac_visible THEN -- do nothing
7809
7810         RETURN NEW;
7811
7812     ELSIF NEW.opac_visible THEN -- add rows
7813
7814         IF TG_TABLE_NAME = 'org_unit' THEN
7815             add_base_query := add_base_query || ' AND cp.circ_lib = ' || NEW.id;
7816             add_peer_query := add_peer_query || ' AND cp.circ_lib = ' || NEW.id;
7817         ELSIF TG_TABLE_NAME = 'copy_location' THEN
7818             add_base_query := add_base_query || ' AND cp.location = ' || NEW.id;
7819             add_peer_query := add_peer_query || ' AND cp.location = ' || NEW.id;
7820         ELSIF TG_TABLE_NAME = 'copy_status' THEN
7821             add_base_query := add_base_query || ' AND cp.status = ' || NEW.id;
7822             add_peer_query := add_peer_query || ' AND cp.status = ' || NEW.id;
7823         END IF;
7824  
7825         EXECUTE add_front || add_base_query || ' UNION ' || add_peer_query || add_back;
7826  
7827     ELSE -- delete rows
7828
7829         IF TG_TABLE_NAME = 'org_unit' THEN
7830             remove_query := 'DELETE FROM asset.opac_visible_copies WHERE circ_lib = ' || NEW.id || ';';
7831         ELSIF TG_TABLE_NAME = 'copy_location' THEN
7832             remove_query := remove_query || 'location = ' || NEW.id || ');';
7833         ELSIF TG_TABLE_NAME = 'copy_status' THEN
7834             remove_query := remove_query || 'status = ' || NEW.id || ');';
7835         END IF;
7836  
7837         EXECUTE remove_query;
7838  
7839     END IF;
7840  
7841     RETURN NEW;
7842 END;
7843 $func$ LANGUAGE PLPGSQL;
7844
7845 INSERT INTO config.upgrade_log (version) VALUES ('0591'); -- berick/miker
7846
7847 CREATE OR REPLACE FUNCTION action.usr_visible_circs (usr_id INT) RETURNS SETOF action.circulation AS $func$
7848 DECLARE
7849     c               action.circulation%ROWTYPE;
7850     view_age        INTERVAL;
7851     usr_view_age    actor.usr_setting%ROWTYPE;
7852     usr_view_start  actor.usr_setting%ROWTYPE;
7853 BEGIN
7854     SELECT * INTO usr_view_age FROM actor.usr_setting WHERE usr = usr_id AND name = 'history.circ.retention_age';
7855     SELECT * INTO usr_view_start FROM actor.usr_setting WHERE usr = usr_id AND name = 'history.circ.retention_start';
7856
7857     IF usr_view_age.value IS NOT NULL AND usr_view_start.value IS NOT NULL THEN
7858         -- User opted in and supplied a retention age
7859         IF oils_json_to_text(usr_view_age.value)::INTERVAL > AGE(NOW(), oils_json_to_text(usr_view_start.value)::TIMESTAMPTZ) THEN
7860             view_age := AGE(NOW(), oils_json_to_text(usr_view_start.value)::TIMESTAMPTZ);
7861         ELSE
7862             view_age := oils_json_to_text(usr_view_age.value)::INTERVAL;
7863         END IF;
7864     ELSIF usr_view_start.value IS NOT NULL THEN
7865         -- User opted in
7866         view_age := AGE(NOW(), oils_json_to_text(usr_view_start.value)::TIMESTAMPTZ);
7867     ELSE
7868         -- User did not opt in
7869         RETURN;
7870     END IF;
7871
7872     FOR c IN
7873         SELECT  *
7874           FROM  action.circulation
7875           WHERE usr = usr_id
7876                 AND parent_circ IS NULL
7877                 AND xact_start > NOW() - view_age
7878           ORDER BY xact_start DESC
7879     LOOP
7880         RETURN NEXT c;
7881     END LOOP;
7882
7883     RETURN;
7884 END;
7885 $func$ LANGUAGE PLPGSQL;
7886
7887 CREATE OR REPLACE FUNCTION action.usr_visible_holds (usr_id INT) RETURNS SETOF action.hold_request AS $func$
7888 DECLARE
7889     h               action.hold_request%ROWTYPE;
7890     view_age        INTERVAL;
7891     view_count      INT;
7892     usr_view_count  actor.usr_setting%ROWTYPE;
7893     usr_view_age    actor.usr_setting%ROWTYPE;
7894     usr_view_start  actor.usr_setting%ROWTYPE;
7895 BEGIN
7896     SELECT * INTO usr_view_count FROM actor.usr_setting WHERE usr = usr_id AND name = 'history.hold.retention_count';
7897     SELECT * INTO usr_view_age FROM actor.usr_setting WHERE usr = usr_id AND name = 'history.hold.retention_age';
7898     SELECT * INTO usr_view_start FROM actor.usr_setting WHERE usr = usr_id AND name = 'history.hold.retention_start';
7899
7900     FOR h IN
7901         SELECT  *
7902           FROM  action.hold_request
7903           WHERE usr = usr_id
7904                 AND fulfillment_time IS NULL
7905                 AND cancel_time IS NULL
7906           ORDER BY request_time DESC
7907     LOOP
7908         RETURN NEXT h;
7909     END LOOP;
7910
7911     IF usr_view_start.value IS NULL THEN
7912         RETURN;
7913     END IF;
7914
7915     IF usr_view_age.value IS NOT NULL THEN
7916         -- User opted in and supplied a retention age
7917         IF oils_json_to_text(usr_view_age.value)::INTERVAL > AGE(NOW(), oils_json_to_text(usr_view_start.value)::TIMESTAMPTZ) THEN
7918             view_age := AGE(NOW(), oils_json_to_text(usr_view_start.value)::TIMESTAMPTZ);
7919         ELSE
7920             view_age := oils_json_to_text(usr_view_age.value)::INTERVAL;
7921         END IF;
7922     ELSE
7923         -- User opted in
7924         view_age := AGE(NOW(), oils_json_to_text(usr_view_start.value)::TIMESTAMPTZ);
7925     END IF;
7926
7927     IF usr_view_count.value IS NOT NULL THEN
7928         view_count := oils_json_to_text(usr_view_count.value)::INT;
7929     ELSE
7930         view_count := 1000;
7931     END IF;
7932
7933     -- show some fulfilled/canceled holds
7934     FOR h IN
7935         SELECT  *
7936           FROM  action.hold_request
7937           WHERE usr = usr_id
7938                 AND ( fulfillment_time IS NOT NULL OR cancel_time IS NOT NULL )
7939                 AND request_time > NOW() - view_age
7940           ORDER BY request_time DESC
7941           LIMIT view_count
7942     LOOP
7943         RETURN NEXT h;
7944     END LOOP;
7945
7946     RETURN;
7947 END;
7948 $func$ LANGUAGE PLPGSQL;
7949
7950 INSERT INTO config.upgrade_log (version) VALUES ('0599'); -- miker/gmc
7951
7952 UPDATE config.metabib_field 
7953 SET xpath = $$//mods32:mods/mods32:name[@type='personal' and not(mods32:role/mods32:roleTerm[text()='creator'])]$$
7954 WHERE field_class = 'author'
7955 AND name = 'other'
7956 AND xpath = $$//mods32:mods/mods32:name[@type='personal' and not(mods32:role)]$$
7957 AND format = 'mods32';
7958
7959 \qecho To reindex bibs that use the author|other index definition,
7960 \qecho you can run something like this:
7961 \qecho
7962 \qecho SELECT metabib.reingest_metabib_field_entries(record)
7963 \qecho FROM (
7964 \qecho   SELECT DISTINCT record
7965 \qecho   FROM metabib.real_full_rec
7966 \qecho   WHERE tag IN ('600', '700', '720', '800')
7967 \qecho   AND   subfield IN ('4', 'e')
7968 \qecho ) a;
7969
7970 -- Resolves an error in calculating copy counts for org lassos
7971 -- Per LP 790329
7972 INSERT INTO config.upgrade_log (version) VALUES ('0603');
7973
7974 -- FIXME: add/check SQL statements to perform the upgrade
7975 CREATE OR REPLACE FUNCTION asset.opac_lasso_record_copy_count (i_lasso INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
7976 DECLARE
7977     ans RECORD;
7978     trans INT;
7979 BEGIN
7980     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
7981
7982     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
7983         RETURN QUERY
7984         SELECT  -1,
7985                 ans.id,
7986                 COUNT( av.id ),
7987                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
7988                 COUNT( av.id ),
7989                 trans
7990           FROM
7991                 actor.org_unit_descendants(ans.id) d
7992                 JOIN asset.opac_visible_copies av ON (av.record = rid AND av.circ_lib = d.id)
7993                 JOIN asset.copy cp ON (cp.id = av.copy_id)
7994           GROUP BY 1,2,6;
7995
7996         IF NOT FOUND THEN
7997             RETURN QUERY SELECT -1, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
7998         END IF;
7999
8000     END LOOP;   
8001                 
8002     RETURN;     
8003 END;            
8004 $f$ LANGUAGE PLPGSQL;
8005
8006
8007 -- Staff record copy counts also triggered an SQL error for org lassos
8008 -- Per LP790329
8009 --
8010 INSERT INTO config.upgrade_log (version) VALUES ('0604');
8011
8012 CREATE OR REPLACE FUNCTION asset.staff_lasso_record_copy_count (i_lasso INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
8013 DECLARE
8014     ans RECORD;
8015     trans INT;
8016 BEGIN
8017     SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
8018
8019     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
8020         RETURN QUERY
8021         SELECT  -1,
8022                 ans.id,
8023                 COUNT( cp.id ),
8024                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
8025                 COUNT( cp.id ),
8026                 trans
8027           FROM
8028                 actor.org_unit_descendants(ans.id) d
8029                 JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
8030                 JOIN asset.call_number cn ON (cn.record = rid AND cn.id = cp.call_number AND NOT cn.deleted)
8031           GROUP BY 1,2,6;
8032
8033         IF NOT FOUND THEN
8034             RETURN QUERY SELECT -1, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
8035         END IF;
8036
8037     END LOOP;
8038
8039     RETURN;
8040 END;
8041 $f$ LANGUAGE PLPGSQL;
8042
8043 INSERT INTO config.upgrade_log (version) VALUES ('0614'); --miker/phasefx
8044
8045 CREATE OR REPLACE FUNCTION asset.cache_copy_visibility () RETURNS TRIGGER as $func$
8046 DECLARE
8047     add_front       TEXT;
8048     add_back        TEXT;
8049     add_base_query  TEXT;
8050     add_peer_query  TEXT;
8051     remove_query    TEXT;
8052     do_add          BOOLEAN := false;
8053     do_remove       BOOLEAN := false;
8054 BEGIN
8055     add_base_query := $$
8056         SELECT  cp.id, cp.circ_lib, cn.record, cn.id AS call_number, cp.location, cp.status
8057           FROM  asset.copy cp
8058                 JOIN asset.call_number cn ON (cn.id = cp.call_number)
8059                 JOIN actor.org_unit a ON (cp.circ_lib = a.id)
8060                 JOIN asset.copy_location cl ON (cp.location = cl.id)
8061                 JOIN config.copy_status cs ON (cp.status = cs.id)
8062                 JOIN biblio.record_entry b ON (cn.record = b.id)
8063           WHERE NOT cp.deleted
8064                 AND NOT cn.deleted
8065                 AND NOT b.deleted
8066                 AND cs.opac_visible
8067                 AND cl.opac_visible
8068                 AND cp.opac_visible
8069                 AND a.opac_visible
8070     $$;
8071     add_peer_query := $$
8072         SELECT  cp.id, cp.circ_lib, pbcm.peer_record AS record, NULL AS call_number, cp.location, cp.status
8073           FROM  asset.copy cp
8074                 JOIN biblio.peer_bib_copy_map pbcm ON (pbcm.target_copy = cp.id)
8075                 JOIN actor.org_unit a ON (cp.circ_lib = a.id)
8076                 JOIN asset.copy_location cl ON (cp.location = cl.id)
8077                 JOIN config.copy_status cs ON (cp.status = cs.id)
8078           WHERE NOT cp.deleted
8079                 AND cs.opac_visible
8080                 AND cl.opac_visible
8081                 AND cp.opac_visible
8082                 AND a.opac_visible
8083     $$;
8084     add_front := $$
8085         INSERT INTO asset.opac_visible_copies (copy_id, circ_lib, record)
8086           SELECT id, circ_lib, record FROM (
8087     $$;
8088     add_back := $$
8089         ) AS x
8090     $$;
8091  
8092     remove_query := $$ DELETE FROM asset.opac_visible_copies WHERE copy_id IN ( SELECT id FROM asset.copy WHERE $$;
8093
8094     IF TG_TABLE_NAME = 'peer_bib_copy_map' THEN
8095         IF TG_OP = 'INSERT' THEN
8096             add_peer_query := add_peer_query || ' AND cp.id = ' || NEW.target_copy || ' AND pbcm.peer_record = ' || NEW.peer_record;
8097             EXECUTE add_front || add_peer_query || add_back;
8098             RETURN NEW;
8099         ELSE
8100             remove_query := 'DELETE FROM asset.opac_visible_copies WHERE copy_id = ' || OLD.target_copy || ' AND record = ' || OLD.peer_record || ';';
8101             EXECUTE remove_query;
8102             RETURN OLD;
8103         END IF;
8104     END IF;
8105
8106     IF TG_OP = 'INSERT' THEN
8107
8108         IF TG_TABLE_NAME IN ('copy', 'unit') THEN
8109             add_base_query := add_base_query || ' AND cp.id = ' || NEW.id;
8110             EXECUTE add_front || add_base_query || add_back;
8111         END IF;
8112
8113         RETURN NEW;
8114
8115     END IF;
8116
8117     -- handle items first, since with circulation activity
8118     -- their statuses change frequently
8119     IF TG_TABLE_NAME IN ('copy', 'unit') THEN
8120
8121         IF OLD.location    <> NEW.location OR
8122            OLD.call_number <> NEW.call_number OR
8123            OLD.status      <> NEW.status OR
8124            OLD.circ_lib    <> NEW.circ_lib THEN
8125             -- any of these could change visibility, but
8126             -- we'll save some queries and not try to calculate
8127             -- the change directly
8128             do_remove := true;
8129             do_add := true;
8130         ELSE
8131
8132             IF OLD.deleted <> NEW.deleted THEN
8133                 IF NEW.deleted THEN
8134                     do_remove := true;
8135                 ELSE
8136                     do_add := true;
8137                 END IF;
8138             END IF;
8139
8140             IF OLD.opac_visible <> NEW.opac_visible THEN
8141                 IF OLD.opac_visible THEN
8142                     do_remove := true;
8143                 ELSIF NOT do_remove THEN -- handle edge case where deleted item
8144                                         -- is also marked opac_visible
8145                     do_add := true;
8146                 END IF;
8147             END IF;
8148
8149         END IF;
8150
8151         IF do_remove THEN
8152             DELETE FROM asset.opac_visible_copies WHERE copy_id = NEW.id;
8153         END IF;
8154         IF do_add THEN
8155             add_base_query := add_base_query || ' AND cp.id = ' || NEW.id;
8156             add_peer_query := add_peer_query || ' AND cp.id = ' || NEW.id;
8157             EXECUTE add_front || add_base_query || ' UNION ' || add_peer_query || add_back;
8158         END IF;
8159
8160         RETURN NEW;
8161
8162     END IF;
8163
8164     IF TG_TABLE_NAME IN ('call_number', 'record_entry') THEN -- these have a 'deleted' column
8165  
8166         IF OLD.deleted AND NEW.deleted THEN -- do nothing
8167
8168             RETURN NEW;
8169  
8170         ELSIF NEW.deleted THEN -- remove rows
8171  
8172             IF TG_TABLE_NAME = 'call_number' THEN
8173                 DELETE FROM asset.opac_visible_copies WHERE copy_id IN (SELECT id FROM asset.copy WHERE call_number = NEW.id);
8174             ELSIF TG_TABLE_NAME = 'record_entry' THEN
8175                 DELETE FROM asset.opac_visible_copies WHERE record = NEW.id;
8176             END IF;
8177  
8178             RETURN NEW;
8179  
8180         ELSIF OLD.deleted THEN -- add rows
8181  
8182             IF TG_TABLE_NAME = 'call_number' THEN
8183                 add_base_query := add_base_query || ' AND cn.id = ' || NEW.id;
8184                 EXECUTE add_front || add_base_query || add_back;
8185             ELSIF TG_TABLE_NAME = 'record_entry' THEN
8186                 add_base_query := add_base_query || ' AND cn.record = ' || NEW.id;
8187                 add_peer_query := add_peer_query || ' AND pbcm.peer_record = ' || NEW.id;
8188                 EXECUTE add_front || add_base_query || ' UNION ' || add_peer_query || add_back;
8189             END IF;
8190  
8191             RETURN NEW;
8192  
8193         END IF;
8194  
8195     END IF;
8196
8197     IF TG_TABLE_NAME = 'call_number' THEN
8198
8199         IF OLD.record <> NEW.record THEN
8200             -- call number is linked to different bib
8201             remove_query := remove_query || 'call_number = ' || NEW.id || ');';
8202             EXECUTE remove_query;
8203             add_base_query := add_base_query || ' AND cn.id = ' || NEW.id;
8204             EXECUTE add_front || add_base_query || add_back;
8205         END IF;
8206
8207         RETURN NEW;
8208
8209     END IF;
8210
8211     IF TG_TABLE_NAME IN ('record_entry') THEN
8212         RETURN NEW; -- don't have 'opac_visible'
8213     END IF;
8214
8215     -- actor.org_unit, asset.copy_location, asset.copy_status
8216     IF NEW.opac_visible = OLD.opac_visible THEN -- do nothing
8217
8218         RETURN NEW;
8219
8220     ELSIF NEW.opac_visible THEN -- add rows
8221
8222         IF TG_TABLE_NAME = 'org_unit' THEN
8223             add_base_query := add_base_query || ' AND cp.circ_lib = ' || NEW.id;
8224             add_peer_query := add_peer_query || ' AND cp.circ_lib = ' || NEW.id;
8225         ELSIF TG_TABLE_NAME = 'copy_location' THEN
8226             add_base_query := add_base_query || ' AND cp.location = ' || NEW.id;
8227             add_peer_query := add_peer_query || ' AND cp.location = ' || NEW.id;
8228         ELSIF TG_TABLE_NAME = 'copy_status' THEN
8229             add_base_query := add_base_query || ' AND cp.status = ' || NEW.id;
8230             add_peer_query := add_peer_query || ' AND cp.status = ' || NEW.id;
8231         END IF;
8232  
8233         EXECUTE add_front || add_base_query || ' UNION ' || add_peer_query || add_back;
8234  
8235     ELSE -- delete rows
8236
8237         IF TG_TABLE_NAME = 'org_unit' THEN
8238             remove_query := 'DELETE FROM asset.opac_visible_copies WHERE circ_lib = ' || NEW.id || ';';
8239         ELSIF TG_TABLE_NAME = 'copy_location' THEN
8240             remove_query := remove_query || 'location = ' || NEW.id || ');';
8241         ELSIF TG_TABLE_NAME = 'copy_status' THEN
8242             remove_query := remove_query || 'status = ' || NEW.id || ');';
8243         END IF;
8244  
8245         EXECUTE remove_query;
8246  
8247     END IF;
8248  
8249     RETURN NEW;
8250 END;
8251 $func$ LANGUAGE PLPGSQL;
8252
8253 INSERT INTO config.upgrade_log (version) VALUES ('0579'); -- superceded by 0620
8254 INSERT INTO config.upgrade_log (version) VALUES ('0620'); -- tsbere via miker
8255
8256 CREATE OR REPLACE FUNCTION action.item_user_circ_test( circ_ou INT, match_item BIGINT, match_user INT, renewal BOOL ) RETURNS SETOF action.circ_matrix_test_result AS $func$
8257 DECLARE
8258     user_object             actor.usr%ROWTYPE;
8259     standing_penalty        config.standing_penalty%ROWTYPE;
8260     item_object             asset.copy%ROWTYPE;
8261     item_status_object      config.copy_status%ROWTYPE;
8262     item_location_object    asset.copy_location%ROWTYPE;
8263     result                  action.circ_matrix_test_result;
8264     circ_test               action.found_circ_matrix_matchpoint;
8265     circ_matchpoint         config.circ_matrix_matchpoint%ROWTYPE;
8266     out_by_circ_mod         config.circ_matrix_circ_mod_test%ROWTYPE;
8267     circ_mod_map            config.circ_matrix_circ_mod_test_map%ROWTYPE;
8268     hold_ratio              action.hold_stats%ROWTYPE;
8269     penalty_type            TEXT;
8270     items_out               INT;
8271     context_org_list        INT[];
8272     done                    BOOL := FALSE;
8273 BEGIN
8274     -- Assume success unless we hit a failure condition
8275     result.success := TRUE;
8276
8277     -- Need user info to look up matchpoints
8278     SELECT INTO user_object * FROM actor.usr WHERE id = match_user AND NOT deleted;
8279
8280     -- (Insta)Fail if we couldn't find the user
8281     IF user_object.id IS NULL THEN
8282         result.fail_part := 'no_user';
8283         result.success := FALSE;
8284         done := TRUE;
8285         RETURN NEXT result;
8286         RETURN;
8287     END IF;
8288
8289     -- Need item info to look up matchpoints
8290     SELECT INTO item_object * FROM asset.copy WHERE id = match_item AND NOT deleted;
8291
8292     -- (Insta)Fail if we couldn't find the item 
8293     IF item_object.id IS NULL THEN
8294         result.fail_part := 'no_item';
8295         result.success := FALSE;
8296         done := TRUE;
8297         RETURN NEXT result;
8298         RETURN;
8299     END IF;
8300
8301     SELECT INTO circ_test * FROM action.find_circ_matrix_matchpoint(circ_ou, item_object, user_object, renewal);
8302
8303     circ_matchpoint             := circ_test.matchpoint;
8304     result.matchpoint           := circ_matchpoint.id;
8305     result.circulate            := circ_matchpoint.circulate;
8306     result.duration_rule        := circ_matchpoint.duration_rule;
8307     result.recurring_fine_rule  := circ_matchpoint.recurring_fine_rule;
8308     result.max_fine_rule        := circ_matchpoint.max_fine_rule;
8309     result.hard_due_date        := circ_matchpoint.hard_due_date;
8310     result.renewals             := circ_matchpoint.renewals;
8311     result.grace_period         := circ_matchpoint.grace_period;
8312     result.buildrows            := circ_test.buildrows;
8313
8314     -- (Insta)Fail if we couldn't find a matchpoint
8315     IF circ_test.success = false THEN
8316         result.fail_part := 'no_matchpoint';
8317         result.success := FALSE;
8318         done := TRUE;
8319         RETURN NEXT result;
8320         RETURN;
8321     END IF;
8322
8323     -- All failures before this point are non-recoverable
8324     -- Below this point are possibly overridable failures
8325
8326     -- Fail if the user is barred
8327     IF user_object.barred IS TRUE THEN
8328         result.fail_part := 'actor.usr.barred';
8329         result.success := FALSE;
8330         done := TRUE;
8331         RETURN NEXT result;
8332     END IF;
8333
8334     -- Fail if the item can't circulate
8335     IF item_object.circulate IS FALSE THEN
8336         result.fail_part := 'asset.copy.circulate';
8337         result.success := FALSE;
8338         done := TRUE;
8339         RETURN NEXT result;
8340     END IF;
8341
8342     -- Fail if the item isn't in a circulateable status on a non-renewal
8343     IF NOT renewal AND item_object.status NOT IN ( 0, 7, 8 ) THEN 
8344         result.fail_part := 'asset.copy.status';
8345         result.success := FALSE;
8346         done := TRUE;
8347         RETURN NEXT result;
8348     -- Alternately, fail if the item isn't checked out on a renewal
8349     ELSIF renewal AND item_object.status <> 1 THEN
8350         result.fail_part := 'asset.copy.status';
8351         result.success := FALSE;
8352         done := TRUE;
8353         RETURN NEXT result;
8354     END IF;
8355
8356     -- Fail if the item can't circulate because of the shelving location
8357     SELECT INTO item_location_object * FROM asset.copy_location WHERE id = item_object.location;
8358     IF item_location_object.circulate IS FALSE THEN
8359         result.fail_part := 'asset.copy_location.circulate';
8360         result.success := FALSE;
8361         done := TRUE;
8362         RETURN NEXT result;
8363     END IF;
8364
8365     -- Use Circ OU for penalties and such
8366     SELECT INTO context_org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( circ_ou );
8367
8368     IF renewal THEN
8369         penalty_type = '%RENEW%';
8370     ELSE
8371         penalty_type = '%CIRC%';
8372     END IF;
8373
8374     FOR standing_penalty IN
8375         SELECT  DISTINCT csp.*
8376           FROM  actor.usr_standing_penalty usp
8377                 JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
8378           WHERE usr = match_user
8379                 AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
8380                 AND (usp.stop_date IS NULL or usp.stop_date > NOW())
8381                 AND csp.block_list LIKE penalty_type LOOP
8382
8383         result.fail_part := standing_penalty.name;
8384         result.success := FALSE;
8385         done := TRUE;
8386         RETURN NEXT result;
8387     END LOOP;
8388
8389     -- Fail if the test is set to hard non-circulating
8390     IF circ_matchpoint.circulate IS FALSE THEN
8391         result.fail_part := 'config.circ_matrix_test.circulate';
8392         result.success := FALSE;
8393         done := TRUE;
8394         RETURN NEXT result;
8395     END IF;
8396
8397     -- Fail if the total copy-hold ratio is too low
8398     IF circ_matchpoint.total_copy_hold_ratio IS NOT NULL THEN
8399         SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
8400         IF hold_ratio.total_copy_ratio IS NOT NULL AND hold_ratio.total_copy_ratio < circ_matchpoint.total_copy_hold_ratio THEN
8401             result.fail_part := 'config.circ_matrix_test.total_copy_hold_ratio';
8402             result.success := FALSE;
8403             done := TRUE;
8404             RETURN NEXT result;
8405         END IF;
8406     END IF;
8407
8408     -- Fail if the available copy-hold ratio is too low
8409     IF circ_matchpoint.available_copy_hold_ratio IS NOT NULL THEN
8410         IF hold_ratio.hold_count IS NULL THEN
8411             SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
8412         END IF;
8413         IF hold_ratio.available_copy_ratio IS NOT NULL AND hold_ratio.available_copy_ratio < circ_matchpoint.available_copy_hold_ratio THEN
8414             result.fail_part := 'config.circ_matrix_test.available_copy_hold_ratio';
8415             result.success := FALSE;
8416             done := TRUE;
8417             RETURN NEXT result;
8418         END IF;
8419     END IF;
8420
8421     -- Fail if the user has too many items with specific circ_modifiers checked out
8422     FOR out_by_circ_mod IN SELECT * FROM config.circ_matrix_circ_mod_test WHERE matchpoint = circ_matchpoint.id LOOP
8423         SELECT  INTO items_out COUNT(*)
8424           FROM  action.circulation circ
8425             JOIN asset.copy cp ON (cp.id = circ.target_copy)
8426           WHERE circ.usr = match_user
8427                AND circ.circ_lib IN ( SELECT * FROM unnest(context_org_list) )
8428             AND circ.checkin_time IS NULL
8429             AND (circ.stop_fines IN ('MAXFINES','LONGOVERDUE') OR circ.stop_fines IS NULL)
8430             AND cp.circ_modifier IN (SELECT circ_mod FROM config.circ_matrix_circ_mod_test_map WHERE circ_mod_test = out_by_circ_mod.id);
8431         IF items_out >= out_by_circ_mod.items_out THEN
8432             result.fail_part := 'config.circ_matrix_circ_mod_test';
8433             result.success := FALSE;
8434             done := TRUE;
8435             RETURN NEXT result;
8436         END IF;
8437     END LOOP;
8438
8439     -- If we passed everything, return the successful matchpoint
8440     IF NOT done THEN
8441         RETURN NEXT result;
8442     END IF;
8443
8444     RETURN;
8445 END;
8446 $func$ LANGUAGE plpgsql;
8447
8448
8449
8450 INSERT INTO config.upgrade_log (version) VALUES ('0628');
8451
8452 -- acq.fund_combined_balance and acq.fund_spent_balance are unchanged,
8453 -- however we need to drop them to recreate the other views.
8454 -- we need to drop all our views because we change the number of columns
8455 -- for example, debit_total does not need an encumberance column when we 
8456 -- have a sepearate total for that.
8457
8458 DROP VIEW acq.fund_spent_balance;
8459 DROP VIEW acq.fund_combined_balance;
8460 DROP VIEW acq.fund_encumbrance_total;
8461 DROP VIEW acq.fund_spent_total;
8462 DROP VIEW acq.fund_debit_total;
8463
8464 CREATE OR REPLACE VIEW acq.fund_debit_total AS
8465     SELECT  fund.id AS fund, 
8466             sum(COALESCE(fund_debit.amount, 0::numeric)) AS amount
8467     FROM acq.fund fund
8468         LEFT JOIN acq.fund_debit fund_debit ON fund.id = fund_debit.fund
8469     GROUP BY fund.id;
8470
8471 CREATE OR REPLACE VIEW acq.fund_encumbrance_total AS
8472     SELECT 
8473         fund.id AS fund, 
8474         sum(COALESCE(fund_debit.amount, 0::numeric)) AS amount 
8475     FROM acq.fund fund
8476         LEFT JOIN acq.fund_debit fund_debit ON fund.id = fund_debit.fund 
8477     WHERE fund_debit.encumbrance GROUP BY fund.id;
8478
8479 CREATE OR REPLACE VIEW acq.fund_spent_total AS
8480     SELECT  fund.id AS fund, 
8481             sum(COALESCE(fund_debit.amount, 0::numeric)) AS amount 
8482     FROM acq.fund fund
8483         LEFT JOIN acq.fund_debit fund_debit ON fund.id = fund_debit.fund 
8484     WHERE NOT fund_debit.encumbrance 
8485     GROUP BY fund.id;
8486
8487 CREATE OR REPLACE VIEW acq.fund_combined_balance AS
8488     SELECT  c.fund, 
8489             c.amount - COALESCE(d.amount, 0.0) AS amount
8490     FROM acq.fund_allocation_total c
8491     LEFT JOIN acq.fund_debit_total d USING (fund);
8492
8493 CREATE OR REPLACE VIEW acq.fund_spent_balance AS
8494     SELECT  c.fund,
8495             c.amount - COALESCE(d.amount,0.0) AS amount
8496       FROM  acq.fund_allocation_total c
8497             LEFT JOIN acq.fund_spent_total d USING (fund);
8498
8499
8500
8501 INSERT INTO config.upgrade_log (version) VALUES ('0631');
8502
8503 CREATE OR REPLACE FUNCTION search.query_parser_fts (
8504
8505     param_search_ou INT,
8506     param_depth     INT,
8507     param_query     TEXT,
8508     param_statuses  INT[],
8509     param_locations INT[],
8510     param_offset    INT,
8511     param_check     INT,
8512     param_limit     INT,
8513     metarecord      BOOL,
8514     staff           BOOL
8515  
8516 ) RETURNS SETOF search.search_result AS $func$
8517 DECLARE
8518
8519     current_res         search.search_result%ROWTYPE;
8520     search_org_list     INT[];
8521     luri_org_list       INT[];
8522     tmp_int_list        INT[];
8523
8524     check_limit         INT;
8525     core_limit          INT;
8526     core_offset         INT;
8527     tmp_int             INT;
8528
8529     core_result         RECORD;
8530     core_cursor         REFCURSOR;
8531     core_rel_query      TEXT;
8532
8533     total_count         INT := 0;
8534     check_count         INT := 0;
8535     deleted_count       INT := 0;
8536     visible_count       INT := 0;
8537     excluded_count      INT := 0;
8538
8539 BEGIN
8540
8541     check_limit := COALESCE( param_check, 1000 );
8542     core_limit  := COALESCE( param_limit, 25000 );
8543     core_offset := COALESCE( param_offset, 0 );
8544
8545     -- core_skip_chk := COALESCE( param_skip_chk, 1 );
8546
8547     IF param_search_ou > 0 THEN
8548         IF param_depth IS NOT NULL THEN
8549             SELECT array_accum(distinct id) INTO search_org_list FROM actor.org_unit_descendants( param_search_ou, param_depth );
8550         ELSE
8551             SELECT array_accum(distinct id) INTO search_org_list FROM actor.org_unit_descendants( param_search_ou );
8552         END IF;
8553
8554         SELECT array_accum(distinct id) INTO luri_org_list FROM actor.org_unit_ancestors( param_search_ou );
8555
8556     ELSIF param_search_ou < 0 THEN
8557         SELECT array_accum(distinct org_unit) INTO search_org_list FROM actor.org_lasso_map WHERE lasso = -param_search_ou;
8558
8559         FOR tmp_int IN SELECT * FROM UNNEST(search_org_list) LOOP
8560             SELECT array_accum(distinct id) INTO tmp_int_list FROM actor.org_unit_ancestors( tmp_int );
8561             luri_org_list := luri_org_list || tmp_int_list;
8562         END LOOP;
8563
8564         SELECT array_accum(DISTINCT x.id) INTO luri_org_list FROM UNNEST(luri_org_list) x(id);
8565
8566     ELSIF param_search_ou = 0 THEN
8567         -- reserved for user lassos (ou_buckets/type='lasso') with ID passed in depth ... hack? sure.
8568     END IF;
8569
8570     OPEN core_cursor FOR EXECUTE param_query;
8571
8572     LOOP
8573
8574         FETCH core_cursor INTO core_result;
8575         EXIT WHEN NOT FOUND;
8576         EXIT WHEN total_count >= core_limit;
8577
8578         total_count := total_count + 1;
8579
8580         CONTINUE WHEN total_count NOT BETWEEN  core_offset + 1 AND check_limit + core_offset;
8581
8582         check_count := check_count + 1;
8583
8584         PERFORM 1 FROM biblio.record_entry b WHERE NOT b.deleted AND b.id IN ( SELECT * FROM unnest( core_result.records ) );
8585         IF NOT FOUND THEN
8586             -- RAISE NOTICE ' % were all deleted ... ', core_result.records;
8587             deleted_count := deleted_count + 1;
8588             CONTINUE;
8589         END IF;
8590
8591         PERFORM 1
8592           FROM  biblio.record_entry b
8593                 JOIN config.bib_source s ON (b.source = s.id)
8594           WHERE s.transcendant
8595                 AND b.id IN ( SELECT * FROM unnest( core_result.records ) );
8596
8597         IF FOUND THEN
8598             -- RAISE NOTICE ' % were all transcendant ... ', core_result.records;
8599             visible_count := visible_count + 1;
8600
8601             current_res.id = core_result.id;
8602             current_res.rel = core_result.rel;
8603
8604             tmp_int := 1;
8605             IF metarecord THEN
8606                 SELECT COUNT(DISTINCT s.source) INTO tmp_int FROM metabib.metarecord_source_map s WHERE s.metarecord = core_result.id;
8607             END IF;
8608
8609             IF tmp_int = 1 THEN
8610                 current_res.record = core_result.records[1];
8611             ELSE
8612                 current_res.record = NULL;
8613             END IF;
8614
8615             RETURN NEXT current_res;
8616
8617             CONTINUE;
8618         END IF;
8619
8620         PERFORM 1
8621           FROM  asset.call_number cn
8622                 JOIN asset.uri_call_number_map map ON (map.call_number = cn.id)
8623                 JOIN asset.uri uri ON (map.uri = uri.id)
8624           WHERE NOT cn.deleted
8625                 AND cn.label = '##URI##'
8626                 AND uri.active
8627                 AND ( param_locations IS NULL OR array_upper(param_locations, 1) IS NULL )
8628                 AND cn.record IN ( SELECT * FROM unnest( core_result.records ) )
8629                 AND cn.owning_lib IN ( SELECT * FROM unnest( luri_org_list ) )
8630           LIMIT 1;
8631
8632         IF FOUND THEN
8633             -- RAISE NOTICE ' % have at least one URI ... ', core_result.records;
8634             visible_count := visible_count + 1;
8635
8636             current_res.id = core_result.id;
8637             current_res.rel = core_result.rel;
8638
8639             tmp_int := 1;
8640             IF metarecord THEN
8641                 SELECT COUNT(DISTINCT s.source) INTO tmp_int FROM metabib.metarecord_source_map s WHERE s.metarecord = core_result.id;
8642             END IF;
8643
8644             IF tmp_int = 1 THEN
8645                 current_res.record = core_result.records[1];
8646             ELSE
8647                 current_res.record = NULL;
8648             END IF;
8649
8650             RETURN NEXT current_res;
8651
8652             CONTINUE;
8653         END IF;
8654
8655         IF param_statuses IS NOT NULL AND array_upper(param_statuses, 1) > 0 THEN
8656
8657             PERFORM 1
8658               FROM  asset.call_number cn
8659                     JOIN asset.copy cp ON (cp.call_number = cn.id)
8660               WHERE NOT cn.deleted
8661                     AND NOT cp.deleted
8662                     AND cp.status IN ( SELECT * FROM unnest( param_statuses ) )
8663                     AND cn.record IN ( SELECT * FROM unnest( core_result.records ) )
8664                     AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
8665               LIMIT 1;
8666
8667             IF NOT FOUND THEN
8668                 PERFORM 1
8669                   FROM  biblio.peer_bib_copy_map pr
8670                         JOIN asset.copy cp ON (cp.id = pr.target_copy)
8671                   WHERE NOT cp.deleted
8672                         AND cp.status IN ( SELECT * FROM unnest( param_statuses ) )
8673                         AND pr.peer_record IN ( SELECT * FROM unnest( core_result.records ) )
8674                         AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
8675                   LIMIT 1;
8676
8677                 IF NOT FOUND THEN
8678                 -- RAISE NOTICE ' % and multi-home linked records were all status-excluded ... ', core_result.records;
8679                     excluded_count := excluded_count + 1;
8680                     CONTINUE;
8681                 END IF;
8682             END IF;
8683
8684         END IF;
8685
8686         IF param_locations IS NOT NULL AND array_upper(param_locations, 1) > 0 THEN
8687
8688             PERFORM 1
8689               FROM  asset.call_number cn
8690                     JOIN asset.copy cp ON (cp.call_number = cn.id)
8691               WHERE NOT cn.deleted
8692                     AND NOT cp.deleted
8693                     AND cp.location IN ( SELECT * FROM unnest( param_locations ) )
8694                     AND cn.record IN ( SELECT * FROM unnest( core_result.records ) )
8695                     AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
8696               LIMIT 1;
8697
8698             IF NOT FOUND THEN
8699                 PERFORM 1
8700                   FROM  biblio.peer_bib_copy_map pr
8701                         JOIN asset.copy cp ON (cp.id = pr.target_copy)
8702                   WHERE NOT cp.deleted
8703                         AND cp.location IN ( SELECT * FROM unnest( param_locations ) )
8704                         AND pr.peer_record IN ( SELECT * FROM unnest( core_result.records ) )
8705                         AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
8706                   LIMIT 1;
8707
8708                 IF NOT FOUND THEN
8709                     -- RAISE NOTICE ' % and multi-home linked records were all copy_location-excluded ... ', core_result.records;
8710                     excluded_count := excluded_count + 1;
8711                     CONTINUE;
8712                 END IF;
8713             END IF;
8714
8715         END IF;
8716
8717         IF staff IS NULL OR NOT staff THEN
8718
8719             PERFORM 1
8720               FROM  asset.opac_visible_copies
8721               WHERE circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
8722                     AND record IN ( SELECT * FROM unnest( core_result.records ) )
8723               LIMIT 1;
8724
8725             IF NOT FOUND THEN
8726                 PERFORM 1
8727                   FROM  biblio.peer_bib_copy_map pr
8728                         JOIN asset.opac_visible_copies cp ON (cp.copy_id = pr.target_copy)
8729                   WHERE cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
8730                         AND pr.peer_record IN ( SELECT * FROM unnest( core_result.records ) )
8731                   LIMIT 1;
8732
8733                 IF NOT FOUND THEN
8734
8735                     -- RAISE NOTICE ' % and multi-home linked records were all visibility-excluded ... ', core_result.records;
8736                     excluded_count := excluded_count + 1;
8737                     CONTINUE;
8738                 END IF;
8739             END IF;
8740
8741         ELSE
8742
8743             PERFORM 1
8744               FROM  asset.call_number cn
8745                     JOIN asset.copy cp ON (cp.call_number = cn.id)
8746               WHERE NOT cn.deleted
8747                     AND NOT cp.deleted
8748                     AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
8749                     AND cn.record IN ( SELECT * FROM unnest( core_result.records ) )
8750               LIMIT 1;
8751
8752             IF NOT FOUND THEN
8753
8754                 PERFORM 1
8755                   FROM  biblio.peer_bib_copy_map pr
8756                         JOIN asset.copy cp ON (cp.id = pr.target_copy)
8757                   WHERE NOT cp.deleted
8758                         AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
8759                         AND pr.peer_record IN ( SELECT * FROM unnest( core_result.records ) )
8760                   LIMIT 1;
8761
8762                 IF NOT FOUND THEN
8763
8764                     PERFORM 1
8765                       FROM  asset.call_number cn
8766                             JOIN asset.copy cp ON (cp.call_number = cn.id)
8767                       WHERE cn.record IN ( SELECT * FROM unnest( core_result.records ) )
8768                             AND NOT cp.deleted
8769                       LIMIT 1;
8770
8771                     IF FOUND THEN
8772                         -- RAISE NOTICE ' % and multi-home linked records were all visibility-excluded ... ', core_result.records;
8773                         excluded_count := excluded_count + 1;
8774                         CONTINUE;
8775                     END IF;
8776                 END IF;
8777
8778             END IF;
8779
8780         END IF;
8781
8782         visible_count := visible_count + 1;
8783
8784         current_res.id = core_result.id;
8785         current_res.rel = core_result.rel;
8786
8787         tmp_int := 1;
8788         IF metarecord THEN
8789             SELECT COUNT(DISTINCT s.source) INTO tmp_int FROM metabib.metarecord_source_map s WHERE s.metarecord = core_result.id;
8790         END IF;
8791
8792         IF tmp_int = 1 THEN
8793             current_res.record = core_result.records[1];
8794         ELSE
8795             current_res.record = NULL;
8796         END IF;
8797
8798         RETURN NEXT current_res;
8799
8800         IF visible_count % 1000 = 0 THEN
8801             -- RAISE NOTICE ' % visible so far ... ', visible_count;
8802         END IF;
8803
8804     END LOOP;
8805
8806     current_res.id = NULL;
8807     current_res.rel = NULL;
8808     current_res.record = NULL;
8809     current_res.total = total_count;
8810     current_res.checked = check_count;
8811     current_res.deleted = deleted_count;
8812     current_res.visible = visible_count;
8813     current_res.excluded = excluded_count;
8814
8815     CLOSE core_cursor;
8816
8817     RETURN NEXT current_res;
8818
8819 END;
8820 $func$ LANGUAGE PLPGSQL;
8821
8822
8823 INSERT INTO config.upgrade_log (version) VALUES ('0633');
8824 INSERT INTO config.upgrade_log (version) VALUES ('0634');
8825
8826 COMMIT;
8827
8828 --0633
8829 INSERT into config.org_unit_setting_type
8830 ( name, grp, label, description, datatype ) VALUES
8831 (
8832         'print.custom_js_file', 'circ',
8833         oils_i18n_gettext(
8834             'print.custom_js_file',
8835             'Printing: Custom Javascript File',
8836             'coust',
8837             'label'
8838         ),
8839         oils_i18n_gettext(
8840             'print.custom_js_file',
8841             'Full URL path to a Javascript File to be loaded when printing. Should'
8842             || ' implement a print_custom function for DOM manipulation. Can change'
8843             || ' the value of the do_print variable to false to cancel printing.',
8844             'coust',
8845             'description'
8846         ),
8847         'string'
8848     );
8849
8850
8851 --0634
8852 INSERT INTO permission.perm_list ( id, code, description ) VALUES
8853  ( 513, 'DEBUG_CLIENT', oils_i18n_gettext( 513,
8854     'Allows a user to use debug functions in the staff client', 'ppl', 'description' ));
8855
8856 UPDATE asset.call_number SET id = id WHERE deleted IS FALSE OR deleted = FALSE;
8857
8858 -- 0529
8859 INSERT INTO config.org_unit_setting_type 
8860 ( name, label, description, datatype ) VALUES 
8861 ( 'circ.user_merge.delete_addresses', 
8862   'Circ:  Patron Merge Address Delete', 
8863   'Delete address(es) of subordinate user(s) in a patron merge', 
8864    'bool'
8865 );
8866
8867 INSERT INTO config.org_unit_setting_type 
8868 ( name, label, description, datatype ) VALUES 
8869 ( 'circ.user_merge.delete_cards', 
8870   'Circ: Patron Merge Barcode Delete', 
8871   'Delete barcode(s) of subordinate user(s) in a patron merge', 
8872   'bool'
8873 );
8874
8875 INSERT INTO config.org_unit_setting_type 
8876 ( name, label, description, datatype ) VALUES 
8877 ( 'circ.user_merge.deactivate_cards', 
8878   'Circ:  Patron Merge Deactivate Card', 
8879   'Mark barcode(s) of subordinate user(s) in a patron merge as inactive', 
8880   'bool'
8881 );
8882
8883 DROP TRIGGER IF EXISTS mat_summary_add_tgr ON money.cash_payment;
8884 DROP TRIGGER IF EXISTS mat_summary_upd_tgr ON money.cash_payment;
8885 DROP TRIGGER IF EXISTS mat_summary_del_tgr ON money.cash_payment;
8886
8887 CREATE TRIGGER mat_summary_add_tgr AFTER INSERT ON money.cash_payment FOR EACH ROW EXECUTE PROCEDURE money.materialized_summary_payment_add ('cash_payment');
8888 CREATE TRIGGER mat_summary_upd_tgr AFTER UPDATE ON money.cash_payment FOR EACH ROW EXECUTE PROCEDURE money.materialized_summary_payment_update ('cash_payment');
8889 CREATE TRIGGER mat_summary_del_tgr BEFORE DELETE ON money.cash_payment FOR EACH ROW EXECUTE PROCEDURE money.materialized_summary_payment_del ('cash_payment');
8890
8891 DROP TRIGGER IF EXISTS mat_summary_add_tgr ON money.check_payment;
8892 DROP TRIGGER IF EXISTS mat_summary_upd_tgr ON money.check_payment;
8893 DROP TRIGGER IF EXISTS mat_summary_del_tgr ON money.check_payment;
8894
8895 CREATE TRIGGER mat_summary_add_tgr AFTER INSERT ON money.check_payment FOR EACH ROW EXECUTE PROCEDURE money.materialized_summary_payment_add ('check_payment');
8896 CREATE TRIGGER mat_summary_upd_tgr AFTER UPDATE ON money.check_payment FOR EACH ROW EXECUTE PROCEDURE money.materialized_summary_payment_update ('check_payment');
8897 CREATE TRIGGER mat_summary_del_tgr BEFORE DELETE ON money.check_payment FOR EACH ROW EXECUTE PROCEDURE money.materialized_summary_payment_del ('check_payment');
8898
8899
8900 UPDATE  metabib.record_attr
8901   SET   attrs = attrs || asort
8902   FROM  (SELECT record,
8903                 HSTORE('authorsort',FIRST(value)) AS asort
8904           FROM  metabib.full_rec
8905           WHERE tag like '1%'
8906         GROUP BY 1) x
8907   WHERE x.record = metabib.record_attr.id;
8908
8909 UPDATE  metabib.record_attr
8910   SET   attrs = attrs || tsort
8911   FROM  (SELECT record,
8912                 HSTORE('titlesort',FIRST(value)) AS tsort
8913           FROM  metabib.full_rec
8914           WHERE tag = 'tnf'
8915         GROUP BY 1) x
8916   WHERE x.record = metabib.record_attr.id;
8917
8918
8919