]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/sql/Pg/version-upgrade/2.0-2.1-upgrade-db.sql
3638766491e17b95049a26037e340858cff6a450
[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 CREATE OR REPLACE FUNCTION biblio.extract_located_uris( bib_id BIGINT, marcxml TEXT, editor_id INT ) RETURNS VOID AS $func$
4323 DECLARE
4324     uris            TEXT[];
4325     uri_xml         TEXT;
4326     uri_label       TEXT;
4327     uri_href        TEXT;
4328     uri_use         TEXT;
4329     uri_owner_list  TEXT[];
4330     uri_owner       TEXT;
4331     uri_owner_id    INT;
4332     uri_id          INT;
4333     uri_cn_id       INT;
4334     uri_map_id      INT;
4335 BEGIN
4336
4337     -- Clear any URI mappings and call numbers for this bib.
4338     -- This leads to acn / auricnm inflation, but also enables
4339     -- old acn/auricnm's to go away and for bibs to be deleted.
4340     FOR uri_cn_id IN SELECT id FROM asset.call_number WHERE record = bib_id AND label = '##URI##' AND NOT deleted LOOP
4341         DELETE FROM asset.uri_call_number_map WHERE call_number = uri_cn_id;
4342         DELETE FROM asset.call_number WHERE id = uri_cn_id;
4343     END LOOP;
4344
4345     uris := oils_xpath('//*[@tag="856" and (@ind1="4" or @ind1="1") and (@ind2="0" or @ind2="1")]',marcxml);
4346     IF ARRAY_UPPER(uris,1) > 0 THEN
4347         FOR i IN 1 .. ARRAY_UPPER(uris, 1) LOOP
4348             -- First we pull info out of the 856
4349             uri_xml     := uris[i];
4350
4351             uri_href    := (oils_xpath('//*[@code="u"]/text()',uri_xml))[1];
4352             uri_label   := (oils_xpath('//*[@code="y"]/text()|//*[@code="3"]/text()',uri_xml))[1];
4353             uri_use     := (oils_xpath('//*[@code="z"]/text()|//*[@code="2"]/text()|//*[@code="n"]/text()',uri_xml))[1];
4354
4355             IF uri_label IS NULL THEN
4356                 uri_label := uri_href;
4357             END IF;
4358             CONTINUE WHEN uri_href IS NULL;
4359
4360             -- Get the distinct list of libraries wanting to use 
4361             SELECT  ARRAY_ACCUM(
4362                         DISTINCT REGEXP_REPLACE(
4363                             x,
4364                             $re$^.*?\((\w+)\).*$$re$,
4365                             E'\\1'
4366                         )
4367                     ) INTO uri_owner_list
4368               FROM  UNNEST(
4369                         oils_xpath(
4370                             '//*[@code="9"]/text()|//*[@code="w"]/text()|//*[@code="n"]/text()',
4371                             uri_xml
4372                         )
4373                     )x;
4374
4375             IF ARRAY_UPPER(uri_owner_list,1) > 0 THEN
4376
4377                 -- look for a matching uri
4378                 IF uri_use IS NULL THEN
4379                     SELECT id INTO uri_id
4380                         FROM asset.uri
4381                         WHERE label = uri_label AND href = uri_href AND use_restriction IS NULL AND active
4382                         ORDER BY id LIMIT 1;
4383                     IF NOT FOUND THEN -- create one
4384                         INSERT INTO asset.uri (label, href, use_restriction) VALUES (uri_label, uri_href, uri_use);
4385                         SELECT id INTO uri_id
4386                             FROM asset.uri
4387                             WHERE label = uri_label AND href = uri_href AND use_restriction IS NULL AND active;
4388                     END IF;
4389                 ELSE
4390                     SELECT id INTO uri_id
4391                         FROM asset.uri
4392                         WHERE label = uri_label AND href = uri_href AND use_restriction = uri_use AND active
4393                         ORDER BY id LIMIT 1;
4394                     IF NOT FOUND THEN -- create one
4395                         INSERT INTO asset.uri (label, href, use_restriction) VALUES (uri_label, uri_href, uri_use);
4396                         SELECT id INTO uri_id
4397                             FROM asset.uri
4398                             WHERE label = uri_label AND href = uri_href AND use_restriction = uri_use AND active;
4399                     END IF;
4400                 END IF;
4401
4402                 FOR j IN 1 .. ARRAY_UPPER(uri_owner_list, 1) LOOP
4403                     uri_owner := uri_owner_list[j];
4404
4405                     SELECT id INTO uri_owner_id FROM actor.org_unit WHERE shortname = uri_owner;
4406                     CONTINUE WHEN NOT FOUND;
4407
4408                     -- we need a call number to link through
4409                     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;
4410                     IF NOT FOUND THEN
4411                         INSERT INTO asset.call_number (owning_lib, record, create_date, edit_date, creator, editor, label)
4412                             VALUES (uri_owner_id, bib_id, 'now', 'now', editor_id, editor_id, '##URI##');
4413                         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;
4414                     END IF;
4415
4416                     -- now, link them if they're not already
4417                     SELECT id INTO uri_map_id FROM asset.uri_call_number_map WHERE call_number = uri_cn_id AND uri = uri_id;
4418                     IF NOT FOUND THEN
4419                         INSERT INTO asset.uri_call_number_map (call_number, uri) VALUES (uri_cn_id, uri_id);
4420                     END IF;
4421
4422                 END LOOP;
4423
4424             END IF;
4425
4426         END LOOP;
4427     END IF;
4428
4429     RETURN;
4430 END;
4431 $func$ LANGUAGE PLPGSQL;
4432
4433 -- 0522
4434 UPDATE config.org_unit_setting_type SET datatype = 'string' WHERE name = 'ui.general.button_bar';
4435
4436 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');
4437
4438 UPDATE actor.org_unit_setting SET value='"circ"' WHERE name = 'ui.general.button_bar' AND value='true';
4439
4440 UPDATE actor.org_unit_setting SET value='"none"' WHERE name = 'ui.general.button_bar' AND value='false';
4441
4442
4443 -- 0523
4444 INSERT into config.org_unit_setting_type
4445 ( name, label, description, datatype, fm_class ) VALUES
4446 ( 'cat.default_copy_status_fast',
4447   oils_i18n_gettext( 'cat.default_copy_status_fast', 'Cataloging: Default copy status (fast add)', 'coust', 'label'),
4448   oils_i18n_gettext( 'cat.default_copy_status_fast', 'Default status when a copy is created using the "Fast Add" interface.', 'coust', 'description'),
4449   'link', 'ccs'
4450 );
4451
4452 INSERT into config.org_unit_setting_type
4453 ( name, label, description, datatype, fm_class ) VALUES
4454 ( 'cat.default_copy_status_normal',
4455   oils_i18n_gettext( 'cat.default_copy_status_normal', 'Cataloging: Default copy status (normal)', 'coust', 'label'),
4456   oils_i18n_gettext( 'cat.default_copy_status_normal', 'Default status when a copy is created using the normal volume/copy creator interface.', 'coust', 'description'),
4457   'link', 'ccs'
4458 );
4459
4460 -- 0524
4461 INSERT into config.org_unit_setting_type
4462 ( name, label, description, datatype ) VALUES
4463 ( 'ui.unified_volume_copy_editor',
4464   oils_i18n_gettext( 'ui.unified_volume_copy_editor', 'GUI: Unified Volume/Item Creator/Editor', 'coust', 'label'),
4465   oils_i18n_gettext( 'ui.unified_volume_copy_editor', 'If true combines the Volume/Copy Creator and Item Attribute Editor in some instances.', 'coust', 'description'),
4466   'bool'
4467 );
4468
4469 -- 0525
4470 CREATE OR REPLACE FUNCTION biblio.indexing_ingest_or_delete () RETURNS TRIGGER AS $func$
4471 DECLARE
4472     transformed_xml TEXT;
4473     prev_xfrm       TEXT;
4474     normalizer      RECORD;
4475     xfrm            config.xml_transform%ROWTYPE;
4476     attr_value      TEXT;
4477     new_attrs       HSTORE := ''::HSTORE;
4478     attr_def        config.record_attr_definition%ROWTYPE;
4479 BEGIN
4480
4481     IF NEW.deleted IS TRUE THEN -- If this bib is deleted
4482         DELETE FROM metabib.metarecord_source_map WHERE source = NEW.id; -- Rid ourselves of the search-estimate-killing linkage
4483         DELETE FROM metabib.record_attr WHERE id = NEW.id; -- Kill the attrs hash, useless on deleted records
4484         DELETE FROM authority.bib_linking WHERE bib = NEW.id; -- Avoid updating fields in bibs that are no longer visible
4485         DELETE FROM biblio.peer_bib_copy_map WHERE peer_record = NEW.id; -- Separate any multi-homed items
4486         RETURN NEW; -- and we're done
4487     END IF;
4488
4489     IF TG_OP = 'UPDATE' THEN -- re-ingest?
4490         PERFORM * FROM config.internal_flag WHERE name = 'ingest.reingest.force_on_same_marc' AND enabled;
4491
4492         IF NOT FOUND AND OLD.marc = NEW.marc THEN -- don't do anything if the MARC didn't change
4493             RETURN NEW;
4494         END IF;
4495     END IF;
4496
4497     -- Record authority linking
4498     PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_authority_linking' AND enabled;
4499     IF NOT FOUND THEN
4500         PERFORM biblio.map_authority_linking( NEW.id, NEW.marc );
4501     END IF;
4502
4503     -- Flatten and insert the mfr data
4504     PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_full_rec' AND enabled;
4505     IF NOT FOUND THEN
4506         PERFORM metabib.reingest_metabib_full_rec(NEW.id);
4507
4508         -- Now we pull out attribute data, which is dependent on the mfr for all but XPath-based fields
4509         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_metabib_rec_descriptor' AND enabled;
4510         IF NOT FOUND THEN
4511             FOR attr_def IN SELECT * FROM config.record_attr_definition ORDER BY format LOOP
4512
4513                 IF attr_def.tag IS NOT NULL THEN -- tag (and optional subfield list) selection
4514                     SELECT  ARRAY_TO_STRING(ARRAY_ACCUM(value), COALESCE(attr_def.joiner,' ')) INTO attr_value
4515                       FROM  (SELECT * FROM metabib.full_rec ORDER BY tag, subfield) AS x
4516                       WHERE record = NEW.id
4517                             AND tag LIKE attr_def.tag
4518                             AND CASE
4519                                 WHEN attr_def.sf_list IS NOT NULL 
4520                                     THEN POSITION(subfield IN attr_def.sf_list) > 0
4521                                 ELSE TRUE
4522                                 END
4523                       GROUP BY tag
4524                       ORDER BY tag
4525                       LIMIT 1;
4526
4527                 ELSIF attr_def.fixed_field IS NOT NULL THEN -- a named fixed field, see config.marc21_ff_pos_map.fixed_field
4528                     attr_value := biblio.marc21_extract_fixed_field(NEW.id, attr_def.fixed_field);
4529
4530                 ELSIF attr_def.xpath IS NOT NULL THEN -- and xpath expression
4531
4532                     SELECT INTO xfrm * FROM config.xml_transform WHERE name = attr_def.format;
4533             
4534                     -- See if we can skip the XSLT ... it's expensive
4535                     IF prev_xfrm IS NULL OR prev_xfrm <> xfrm.name THEN
4536                         -- Can't skip the transform
4537                         IF xfrm.xslt <> '---' THEN
4538                             transformed_xml := oils_xslt_process(NEW.marc,xfrm.xslt);
4539                         ELSE
4540                             transformed_xml := NEW.marc;
4541                         END IF;
4542             
4543                         prev_xfrm := xfrm.name;
4544                     END IF;
4545
4546                     IF xfrm.name IS NULL THEN
4547                         -- just grab the marcxml (empty) transform
4548                         SELECT INTO xfrm * FROM config.xml_transform WHERE xslt = '---' LIMIT 1;
4549                         prev_xfrm := xfrm.name;
4550                     END IF;
4551
4552                     attr_value := oils_xpath_string(attr_def.xpath, transformed_xml, COALESCE(attr_def.joiner,' '), ARRAY[ARRAY[xfrm.prefix, xfrm.namespace_uri]]);
4553
4554                 ELSIF attr_def.phys_char_sf IS NOT NULL THEN -- a named Physical Characteristic, see config.marc21_physical_characteristic_*_map
4555                     SELECT  m.value INTO attr_value
4556                       FROM  biblio.marc21_physical_characteristics(NEW.id) v
4557                             JOIN config.marc21_physical_characteristic_value_map m ON (m.id = v.value)
4558                       WHERE v.subfield = attr_def.phys_char_sf
4559                       LIMIT 1; -- Just in case ...
4560
4561                 END IF;
4562
4563                 -- apply index normalizers to attr_value
4564                 FOR normalizer IN
4565                     SELECT  n.func AS func,
4566                             n.param_count AS param_count,
4567                             m.params AS params
4568                       FROM  config.index_normalizer n
4569                             JOIN config.record_attr_index_norm_map m ON (m.norm = n.id)
4570                       WHERE attr = attr_def.name
4571                       ORDER BY m.pos LOOP
4572                         EXECUTE 'SELECT ' || normalizer.func || '(' ||
4573                             quote_literal( attr_value ) ||
4574                             CASE
4575                                 WHEN normalizer.param_count > 0
4576                                     THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
4577                                     ELSE ''
4578                                 END ||
4579                             ')' INTO attr_value;
4580         
4581                 END LOOP;
4582
4583                 -- Add the new value to the hstore
4584                 new_attrs := new_attrs || hstore( attr_def.name, attr_value );
4585
4586             END LOOP;
4587
4588             IF TG_OP = 'INSERT' OR OLD.deleted THEN -- initial insert OR revivication
4589                 INSERT INTO metabib.record_attr (id, attrs) VALUES (NEW.id, new_attrs);
4590             ELSE
4591                 UPDATE metabib.record_attr SET attrs = attrs || new_attrs WHERE id = NEW.id;
4592             END IF;
4593
4594         END IF;
4595     END IF;
4596
4597     -- Gather and insert the field entry data
4598     PERFORM metabib.reingest_metabib_field_entries(NEW.id);
4599
4600     -- Located URI magic
4601     IF TG_OP = 'INSERT' THEN
4602         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_located_uri' AND enabled;
4603         IF NOT FOUND THEN
4604             PERFORM biblio.extract_located_uris( NEW.id, NEW.marc, NEW.editor );
4605         END IF;
4606     ELSE
4607         PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_located_uri' AND enabled;
4608         IF NOT FOUND THEN
4609             PERFORM biblio.extract_located_uris( NEW.id, NEW.marc, NEW.editor );
4610         END IF;
4611     END IF;
4612
4613     -- (re)map metarecord-bib linking
4614     IF TG_OP = 'INSERT' THEN -- if not deleted and performing an insert, check for the flag
4615         PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_insert' AND enabled;
4616         IF NOT FOUND THEN
4617             PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
4618         END IF;
4619     ELSE -- we're doing an update, and we're not deleted, remap
4620         PERFORM * FROM config.internal_flag WHERE name = 'ingest.metarecord_mapping.skip_on_update' AND enabled;
4621         IF NOT FOUND THEN
4622             PERFORM metabib.remap_metarecord_for_bib( NEW.id, NEW.fingerprint );
4623         END IF;
4624     END IF;
4625
4626     RETURN NEW;
4627 END;
4628 $func$ LANGUAGE PLPGSQL;
4629
4630 ALTER TABLE config.circ_matrix_weights ADD COLUMN marc_bib_level NUMERIC(6,2) NOT NULL DEFAULT 0.0;
4631
4632 UPDATE config.circ_matrix_weights
4633 SET marc_bib_level = marc_vr_format;
4634
4635 ALTER TABLE config.hold_matrix_weights ADD COLUMN marc_bib_level NUMERIC(6, 2) NOT NULL DEFAULT 0.0;
4636
4637 UPDATE config.hold_matrix_weights
4638 SET marc_bib_level = marc_vr_format;
4639
4640 ALTER TABLE config.circ_matrix_weights ALTER COLUMN marc_bib_level DROP DEFAULT;
4641
4642 ALTER TABLE config.hold_matrix_weights ALTER COLUMN marc_bib_level DROP DEFAULT;
4643
4644 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$
4645 DECLARE
4646     cn_object       asset.call_number%ROWTYPE;
4647     rec_descriptor  metabib.rec_descriptor%ROWTYPE;
4648     cur_matchpoint  config.circ_matrix_matchpoint%ROWTYPE;
4649     matchpoint      config.circ_matrix_matchpoint%ROWTYPE;
4650     weights         config.circ_matrix_weights%ROWTYPE;
4651     user_age        INTERVAL;
4652     denominator     NUMERIC(6,2);
4653     row_list        INT[];
4654     result          action.found_circ_matrix_matchpoint;
4655 BEGIN
4656     -- Assume failure
4657     result.success = false;
4658
4659     -- Fetch useful data
4660     SELECT INTO cn_object       * FROM asset.call_number        WHERE id = item_object.call_number;
4661     SELECT INTO rec_descriptor  * FROM metabib.rec_descriptor   WHERE record = cn_object.record;
4662
4663     -- Pre-generate this so we only calc it once
4664     IF user_object.dob IS NOT NULL THEN
4665         SELECT INTO user_age age(user_object.dob);
4666     END IF;
4667
4668     -- Grab the closest set circ weight setting.
4669     SELECT INTO weights cw.*
4670       FROM config.weight_assoc wa
4671            JOIN config.circ_matrix_weights cw ON (cw.id = wa.circ_weights)
4672            JOIN actor.org_unit_ancestors_distance( context_ou ) d ON (wa.org_unit = d.id)
4673       WHERE active
4674       ORDER BY d.distance
4675       LIMIT 1;
4676
4677     -- No weights? Bad admin! Defaults to handle that anyway.
4678     IF weights.id IS NULL THEN
4679         weights.grp                 := 11.0;
4680         weights.org_unit            := 10.0;
4681         weights.circ_modifier       := 5.0;
4682         weights.marc_type           := 4.0;
4683         weights.marc_form           := 3.0;
4684         weights.marc_bib_level      := 2.0;
4685         weights.marc_vr_format      := 2.0;
4686         weights.copy_circ_lib       := 8.0;
4687         weights.copy_owning_lib     := 8.0;
4688         weights.user_home_ou        := 8.0;
4689         weights.ref_flag            := 1.0;
4690         weights.juvenile_flag       := 6.0;
4691         weights.is_renewal          := 7.0;
4692         weights.usr_age_lower_bound := 0.0;
4693         weights.usr_age_upper_bound := 0.0;
4694     END IF;
4695
4696     -- Determine the max (expected) depth (+1) of the org tree and max depth of the permisson tree
4697     -- If you break your org tree with funky parenting this may be wrong
4698     -- 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
4699     -- We use one denominator for all tree-based checks for when permission groups and org units have the same weighting
4700     WITH all_distance(distance) AS (
4701             SELECT depth AS distance FROM actor.org_unit_type
4702         UNION
4703             SELECT distance AS distance FROM permission.grp_ancestors_distance((SELECT id FROM permission.grp_tree WHERE parent IS NULL))
4704         )
4705     SELECT INTO denominator MAX(distance) + 1 FROM all_distance;
4706
4707     -- Loop over all the potential matchpoints
4708     FOR cur_matchpoint IN
4709         SELECT m.*
4710           FROM  config.circ_matrix_matchpoint m
4711                 /*LEFT*/ JOIN permission.grp_ancestors_distance( user_object.profile ) upgad ON m.grp = upgad.id
4712                 /*LEFT*/ JOIN actor.org_unit_ancestors_distance( context_ou ) ctoua ON m.org_unit = ctoua.id
4713                 LEFT JOIN actor.org_unit_ancestors_distance( cn_object.owning_lib ) cnoua ON m.copy_owning_lib = cnoua.id
4714                 LEFT JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) iooua ON m.copy_circ_lib = iooua.id
4715                 LEFT JOIN actor.org_unit_ancestors_distance( user_object.home_ou  ) uhoua ON m.user_home_ou = uhoua.id
4716           WHERE m.active
4717                 -- Permission Groups
4718              -- AND (m.grp                      IS NULL OR upgad.id IS NOT NULL) -- Optional Permission Group?
4719                 -- Org Units
4720              -- AND (m.org_unit                 IS NULL OR ctoua.id IS NOT NULL) -- Optional Org Unit?
4721                 AND (m.copy_owning_lib          IS NULL OR cnoua.id IS NOT NULL)
4722                 AND (m.copy_circ_lib            IS NULL OR iooua.id IS NOT NULL)
4723                 AND (m.user_home_ou             IS NULL OR uhoua.id IS NOT NULL)
4724                 -- Circ Type
4725                 AND (m.is_renewal               IS NULL OR m.is_renewal = renewal)
4726                 -- Static User Checks
4727                 AND (m.juvenile_flag            IS NULL OR m.juvenile_flag = user_object.juvenile)
4728                 AND (m.usr_age_lower_bound      IS NULL OR (user_age IS NOT NULL AND m.usr_age_lower_bound < user_age))
4729                 AND (m.usr_age_upper_bound      IS NULL OR (user_age IS NOT NULL AND m.usr_age_upper_bound > user_age))
4730                 -- Static Item Checks
4731                 AND (m.circ_modifier            IS NULL OR m.circ_modifier = item_object.circ_modifier)
4732                 AND (m.marc_type                IS NULL OR m.marc_type = COALESCE(item_object.circ_as_type, rec_descriptor.item_type))
4733                 AND (m.marc_form                IS NULL OR m.marc_form = rec_descriptor.item_form)
4734                 AND (m.marc_bib_level           IS NULL OR m.marc_bib_level = rec_descriptor.bib_level)
4735                 AND (m.marc_vr_format           IS NULL OR m.marc_vr_format = rec_descriptor.vr_format)
4736                 AND (m.ref_flag                 IS NULL OR m.ref_flag = item_object.ref)
4737           ORDER BY
4738                 -- Permission Groups
4739                 CASE WHEN upgad.distance        IS NOT NULL THEN 2^(2*weights.grp - (upgad.distance/denominator)) ELSE 0.0 END +
4740                 -- Org Units
4741                 CASE WHEN ctoua.distance        IS NOT NULL THEN 2^(2*weights.org_unit - (ctoua.distance/denominator)) ELSE 0.0 END +
4742                 CASE WHEN cnoua.distance        IS NOT NULL THEN 2^(2*weights.copy_owning_lib - (cnoua.distance/denominator)) ELSE 0.0 END +
4743                 CASE WHEN iooua.distance        IS NOT NULL THEN 2^(2*weights.copy_circ_lib - (iooua.distance/denominator)) ELSE 0.0 END +
4744                 CASE WHEN uhoua.distance        IS NOT NULL THEN 2^(2*weights.user_home_ou - (uhoua.distance/denominator)) ELSE 0.0 END +
4745                 -- Circ Type                    -- Note: 4^x is equiv to 2^(2*x)
4746                 CASE WHEN m.is_renewal          IS NOT NULL THEN 4^weights.is_renewal ELSE 0.0 END +
4747                 -- Static User Checks
4748                 CASE WHEN m.juvenile_flag       IS NOT NULL THEN 4^weights.juvenile_flag ELSE 0.0 END +
4749                 CASE WHEN m.usr_age_lower_bound IS NOT NULL THEN 4^weights.usr_age_lower_bound ELSE 0.0 END +
4750                 CASE WHEN m.usr_age_upper_bound IS NOT NULL THEN 4^weights.usr_age_upper_bound ELSE 0.0 END +
4751                 -- Static Item Checks
4752                 CASE WHEN m.circ_modifier       IS NOT NULL THEN 4^weights.circ_modifier ELSE 0.0 END +
4753                 CASE WHEN m.marc_type           IS NOT NULL THEN 4^weights.marc_type ELSE 0.0 END +
4754                 CASE WHEN m.marc_form           IS NOT NULL THEN 4^weights.marc_form ELSE 0.0 END +
4755                 CASE WHEN m.marc_vr_format      IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0.0 END +
4756                 CASE WHEN m.ref_flag            IS NOT NULL THEN 4^weights.ref_flag ELSE 0.0 END DESC,
4757                 -- Final sort on id, so that if two rules have the same sorting in the previous sort they have a defined order
4758                 -- This prevents "we changed the table order by updating a rule, and we started getting different results"
4759                 m.id LOOP
4760
4761         -- Record the full matching row list
4762         row_list := row_list || cur_matchpoint.id;
4763
4764         -- No matchpoint yet?
4765         IF matchpoint.id IS NULL THEN
4766             -- Take the entire matchpoint as a starting point
4767             matchpoint := cur_matchpoint;
4768             CONTINUE; -- No need to look at this row any more.
4769         END IF;
4770
4771         -- Incomplete matchpoint?
4772         IF matchpoint.circulate IS NULL THEN
4773             matchpoint.circulate := cur_matchpoint.circulate;
4774         END IF;
4775         IF matchpoint.duration_rule IS NULL THEN
4776             matchpoint.duration_rule := cur_matchpoint.duration_rule;
4777         END IF;
4778         IF matchpoint.recurring_fine_rule IS NULL THEN
4779             matchpoint.recurring_fine_rule := cur_matchpoint.recurring_fine_rule;
4780         END IF;
4781         IF matchpoint.max_fine_rule IS NULL THEN
4782             matchpoint.max_fine_rule := cur_matchpoint.max_fine_rule;
4783         END IF;
4784         IF matchpoint.hard_due_date IS NULL THEN
4785             matchpoint.hard_due_date := cur_matchpoint.hard_due_date;
4786         END IF;
4787         IF matchpoint.total_copy_hold_ratio IS NULL THEN
4788             matchpoint.total_copy_hold_ratio := cur_matchpoint.total_copy_hold_ratio;
4789         END IF;
4790         IF matchpoint.available_copy_hold_ratio IS NULL THEN
4791             matchpoint.available_copy_hold_ratio := cur_matchpoint.available_copy_hold_ratio;
4792         END IF;
4793         IF matchpoint.renewals IS NULL THEN
4794             matchpoint.renewals := cur_matchpoint.renewals;
4795         END IF;
4796         IF matchpoint.grace_period IS NULL THEN
4797             matchpoint.grace_period := cur_matchpoint.grace_period;
4798         END IF;
4799     END LOOP;
4800
4801     -- Check required fields
4802     IF matchpoint.circulate             IS NOT NULL AND
4803        matchpoint.duration_rule         IS NOT NULL AND
4804        matchpoint.recurring_fine_rule   IS NOT NULL AND
4805        matchpoint.max_fine_rule         IS NOT NULL THEN
4806         -- All there? We have a completed match.
4807         result.success := true;
4808     END IF;
4809
4810     -- Include the assembled matchpoint, even if it isn't complete
4811     result.matchpoint := matchpoint;
4812
4813     -- Include (for debugging) the full list of matching rows
4814     result.buildrows := row_list;
4815
4816     -- Hand the result back to caller
4817     RETURN result;
4818 END;
4819 $func$ LANGUAGE plpgsql;
4820
4821 CREATE OR REPLACE FUNCTION action.find_hold_matrix_matchpoint(pickup_ou integer, request_ou integer, match_item bigint, match_user integer, match_requestor integer)
4822   RETURNS integer AS
4823 $func$
4824 DECLARE
4825     requestor_object    actor.usr%ROWTYPE;
4826     user_object         actor.usr%ROWTYPE;
4827     item_object         asset.copy%ROWTYPE;
4828     item_cn_object      asset.call_number%ROWTYPE;
4829     rec_descriptor      metabib.rec_descriptor%ROWTYPE;
4830     matchpoint          config.hold_matrix_matchpoint%ROWTYPE;
4831     weights             config.hold_matrix_weights%ROWTYPE;
4832     denominator         NUMERIC(6,2);
4833 BEGIN
4834     SELECT INTO user_object         * FROM actor.usr                WHERE id = match_user;
4835     SELECT INTO requestor_object    * FROM actor.usr                WHERE id = match_requestor;
4836     SELECT INTO item_object         * FROM asset.copy               WHERE id = match_item;
4837     SELECT INTO item_cn_object      * FROM asset.call_number        WHERE id = item_object.call_number;
4838     SELECT INTO rec_descriptor      * FROM metabib.rec_descriptor   WHERE record = item_cn_object.record;
4839
4840     -- The item's owner should probably be the one determining if the item is holdable
4841     -- How to decide that is debatable. Decided to default to the circ library (where the item lives)
4842     -- This flag will allow for setting it to the owning library (where the call number "lives")
4843     PERFORM * FROM config.internal_flag WHERE name = 'circ.holds.weight_owner_not_circ' AND enabled;
4844
4845     -- Grab the closest set circ weight setting.
4846     IF NOT FOUND THEN
4847         -- Default to circ library
4848         SELECT INTO weights hw.*
4849           FROM config.weight_assoc wa
4850                JOIN config.hold_matrix_weights hw ON (hw.id = wa.hold_weights)
4851                JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) d ON (wa.org_unit = d.id)
4852           WHERE active
4853           ORDER BY d.distance
4854           LIMIT 1;
4855     ELSE
4856         -- Flag is set, use owning library
4857         SELECT INTO weights hw.*
4858           FROM config.weight_assoc wa
4859                JOIN config.hold_matrix_weights hw ON (hw.id = wa.hold_weights)
4860                JOIN actor.org_unit_ancestors_distance( cn_object.owning_lib ) d ON (wa.org_unit = d.id)
4861           WHERE active
4862           ORDER BY d.distance
4863           LIMIT 1;
4864     END IF;
4865
4866     -- No weights? Bad admin! Defaults to handle that anyway.
4867     IF weights.id IS NULL THEN
4868         weights.user_home_ou    := 5.0;
4869         weights.request_ou      := 5.0;
4870         weights.pickup_ou       := 5.0;
4871         weights.item_owning_ou  := 5.0;
4872         weights.item_circ_ou    := 5.0;
4873         weights.usr_grp         := 7.0;
4874         weights.requestor_grp   := 8.0;
4875         weights.circ_modifier   := 4.0;
4876         weights.marc_type       := 3.0;
4877         weights.marc_form       := 2.0;
4878         weights.marc_bib_level  := 1.0;
4879         weights.marc_vr_format  := 1.0;
4880         weights.juvenile_flag   := 4.0;
4881         weights.ref_flag        := 0.0;
4882     END IF;
4883
4884     -- Determine the max (expected) depth (+1) of the org tree and max depth of the permisson tree
4885     -- If you break your org tree with funky parenting this may be wrong
4886     -- 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
4887     -- We use one denominator for all tree-based checks for when permission groups and org units have the same weighting
4888     WITH all_distance(distance) AS (
4889             SELECT depth AS distance FROM actor.org_unit_type
4890         UNION
4891             SELECT distance AS distance FROM permission.grp_ancestors_distance((SELECT id FROM permission.grp_tree WHERE parent IS NULL))
4892         )
4893     SELECT INTO denominator MAX(distance) + 1 FROM all_distance;
4894
4895     -- To ATTEMPT to make this work like it used to, make it reverse the user/requestor profile ids.
4896     -- This may be better implemented as part of the upgrade script?
4897     -- Set usr_grp = requestor_grp, requestor_grp = 1 or something when this flag is already set
4898     -- Then remove this flag, of course.
4899     PERFORM * FROM config.internal_flag WHERE name = 'circ.holds.usr_not_requestor' AND enabled;
4900
4901     IF FOUND THEN
4902         -- Note: This, to me, is REALLY hacky. I put it in anyway.
4903         -- If you can't tell, this is a single call swap on two variables.
4904         SELECT INTO user_object.profile, requestor_object.profile
4905                     requestor_object.profile, user_object.profile;
4906     END IF;
4907
4908     -- Select the winning matchpoint into the matchpoint variable for returning
4909     SELECT INTO matchpoint m.*
4910       FROM  config.hold_matrix_matchpoint m
4911             /*LEFT*/ JOIN permission.grp_ancestors_distance( requestor_object.profile ) rpgad ON m.requestor_grp = rpgad.id
4912             LEFT JOIN permission.grp_ancestors_distance( user_object.profile ) upgad ON m.usr_grp = upgad.id
4913             LEFT JOIN actor.org_unit_ancestors_distance( pickup_ou ) puoua ON m.pickup_ou = puoua.id
4914             LEFT JOIN actor.org_unit_ancestors_distance( request_ou ) rqoua ON m.request_ou = rqoua.id
4915             LEFT JOIN actor.org_unit_ancestors_distance( item_cn_object.owning_lib ) cnoua ON m.item_owning_ou = cnoua.id
4916             LEFT JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) iooua ON m.item_circ_ou = iooua.id
4917             LEFT JOIN actor.org_unit_ancestors_distance( user_object.home_ou  ) uhoua ON m.user_home_ou = uhoua.id
4918       WHERE m.active
4919             -- Permission Groups
4920          -- AND (m.requestor_grp        IS NULL OR upgad.id IS NOT NULL) -- Optional Requestor Group?
4921             AND (m.usr_grp              IS NULL OR upgad.id IS NOT NULL)
4922             -- Org Units
4923             AND (m.pickup_ou            IS NULL OR (puoua.id IS NOT NULL AND (puoua.distance = 0 OR NOT m.strict_ou_match)))
4924             AND (m.request_ou           IS NULL OR (rqoua.id IS NOT NULL AND (rqoua.distance = 0 OR NOT m.strict_ou_match)))
4925             AND (m.item_owning_ou       IS NULL OR (cnoua.id IS NOT NULL AND (cnoua.distance = 0 OR NOT m.strict_ou_match)))
4926             AND (m.item_circ_ou         IS NULL OR (iooua.id IS NOT NULL AND (iooua.distance = 0 OR NOT m.strict_ou_match)))
4927             AND (m.user_home_ou         IS NULL OR (uhoua.id IS NOT NULL AND (uhoua.distance = 0 OR NOT m.strict_ou_match)))
4928             -- Static User Checks
4929             AND (m.juvenile_flag        IS NULL OR m.juvenile_flag = user_object.juvenile)
4930             -- Static Item Checks
4931             AND (m.circ_modifier        IS NULL OR m.circ_modifier = item_object.circ_modifier)
4932             AND (m.marc_type            IS NULL OR m.marc_type = COALESCE(item_object.circ_as_type, rec_descriptor.item_type))
4933             AND (m.marc_form            IS NULL OR m.marc_form = rec_descriptor.item_form)
4934             AND (m.marc_bib_level       IS NULL OR m.marc_bib_level = rec_descriptor.bib_level)
4935             AND (m.marc_vr_format       IS NULL OR m.marc_vr_format = rec_descriptor.vr_format)
4936             AND (m.ref_flag             IS NULL OR m.ref_flag = item_object.ref)
4937       ORDER BY
4938             -- Permission Groups
4939             CASE WHEN rpgad.distance    IS NOT NULL THEN 2^(2*weights.requestor_grp - (rpgad.distance/denominator)) ELSE 0.0 END +
4940             CASE WHEN upgad.distance    IS NOT NULL THEN 2^(2*weights.usr_grp - (upgad.distance/denominator)) ELSE 0.0 END +
4941             -- Org Units
4942             CASE WHEN puoua.distance    IS NOT NULL THEN 2^(2*weights.pickup_ou - (puoua.distance/denominator)) ELSE 0.0 END +
4943             CASE WHEN rqoua.distance    IS NOT NULL THEN 2^(2*weights.request_ou - (rqoua.distance/denominator)) ELSE 0.0 END +
4944             CASE WHEN cnoua.distance    IS NOT NULL THEN 2^(2*weights.item_owning_ou - (cnoua.distance/denominator)) ELSE 0.0 END +
4945             CASE WHEN iooua.distance    IS NOT NULL THEN 2^(2*weights.item_circ_ou - (iooua.distance/denominator)) ELSE 0.0 END +
4946             CASE WHEN uhoua.distance    IS NOT NULL THEN 2^(2*weights.user_home_ou - (uhoua.distance/denominator)) ELSE 0.0 END +
4947             -- Static User Checks       -- Note: 4^x is equiv to 2^(2*x)
4948             CASE WHEN m.juvenile_flag   IS NOT NULL THEN 4^weights.juvenile_flag ELSE 0.0 END +
4949             -- Static Item Checks
4950             CASE WHEN m.circ_modifier   IS NOT NULL THEN 4^weights.circ_modifier ELSE 0.0 END +
4951             CASE WHEN m.marc_type       IS NOT NULL THEN 4^weights.marc_type ELSE 0.0 END +
4952             CASE WHEN m.marc_form       IS NOT NULL THEN 4^weights.marc_form ELSE 0.0 END +
4953             CASE WHEN m.marc_vr_format  IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0.0 END +
4954             CASE WHEN m.ref_flag        IS NOT NULL THEN 4^weights.ref_flag ELSE 0.0 END DESC,
4955             -- Final sort on id, so that if two rules have the same sorting in the previous sort they have a defined order
4956             -- This prevents "we changed the table order by updating a rule, and we started getting different results"
4957             m.id;
4958
4959     -- Return just the ID for now
4960     RETURN matchpoint.id;
4961 END;
4962 $func$ LANGUAGE 'plpgsql';
4963
4964 -- 0528
4965 CREATE OR REPLACE FUNCTION maintain_control_numbers() RETURNS TRIGGER AS $func$
4966 use strict;
4967 use MARC::Record;
4968 use MARC::File::XML (BinaryEncoding => 'UTF-8');
4969 use MARC::Charset;
4970 use Encode;
4971 use Unicode::Normalize;
4972
4973 MARC::Charset->assume_unicode(1);
4974
4975 my $record = MARC::Record->new_from_xml($_TD->{new}{marc});
4976 my $schema = $_TD->{table_schema};
4977 my $rec_id = $_TD->{new}{id};
4978
4979 # Short-circuit if maintaining control numbers per MARC21 spec is not enabled
4980 my $enable = spi_exec_query("SELECT enabled FROM config.global_flag WHERE name = 'cat.maintain_control_numbers'");
4981 if (!($enable->{processed}) or $enable->{rows}[0]->{enabled} eq 'f') {
4982     return;
4983 }
4984
4985 # Get the control number identifier from an OU setting based on $_TD->{new}{owner}
4986 my $ou_cni = 'EVRGRN';
4987
4988 my $owner;
4989 if ($schema eq 'serial') {
4990     $owner = $_TD->{new}{owning_lib};
4991 } else {
4992     # are.owner and bre.owner can be null, so fall back to the consortial setting
4993     $owner = $_TD->{new}{owner} || 1;
4994 }
4995
4996 my $ous_rv = spi_exec_query("SELECT value FROM actor.org_unit_ancestor_setting('cat.marc_control_number_identifier', $owner)");
4997 if ($ous_rv->{processed}) {
4998     $ou_cni = $ous_rv->{rows}[0]->{value};
4999     $ou_cni =~ s/"//g; # Stupid VIM syntax highlighting"
5000 } else {
5001     # Fall back to the shortname of the OU if there was no OU setting
5002     $ous_rv = spi_exec_query("SELECT shortname FROM actor.org_unit WHERE id = $owner");
5003     if ($ous_rv->{processed}) {
5004         $ou_cni = $ous_rv->{rows}[0]->{shortname};
5005     }
5006 }
5007
5008 my ($create, $munge) = (0, 0);
5009
5010 my @scns = $record->field('035');
5011
5012 foreach my $id_field ('001', '003') {
5013     my $spec_value;
5014     my @controls = $record->field($id_field);
5015
5016     if ($id_field eq '001') {
5017         $spec_value = $rec_id;
5018     } else {
5019         $spec_value = $ou_cni;
5020     }
5021
5022     # Create the 001/003 if none exist
5023     if (scalar(@controls) == 1) {
5024         # Only one field; check to see if we need to munge it
5025         unless (grep $_->data() eq $spec_value, @controls) {
5026             $munge = 1;
5027         }
5028     } else {
5029         # Delete the other fields, as with more than 1 001/003 we do not know which 003/001 to match
5030         foreach my $control (@controls) {
5031             unless ($control->data() eq $spec_value) {
5032                 $record->delete_field($control);
5033             }
5034         }
5035         $record->insert_fields_ordered(MARC::Field->new($id_field, $spec_value));
5036         $create = 1;
5037     }
5038 }
5039
5040 # Now, if we need to munge the 001, we will first push the existing 001/003
5041 # into the 035; but if the record did not have one (and one only) 001 and 003
5042 # to begin with, skip this process
5043 if ($munge and not $create) {
5044     my $scn = "(" . $record->field('003')->data() . ")" . $record->field('001')->data();
5045
5046     # Do not create duplicate 035 fields
5047     unless (grep $_->subfield('a') eq $scn, @scns) {
5048         $record->insert_fields_ordered(MARC::Field->new('035', '', '', 'a' => $scn));
5049     }
5050 }
5051
5052 # Set the 001/003 and update the MARC
5053 if ($create or $munge) {
5054     $record->field('001')->data($rec_id);
5055     $record->field('003')->data($ou_cni);
5056
5057     my $xml = $record->as_xml_record();
5058     $xml =~ s/\n//sgo;
5059     $xml =~ s/^<\?xml.+\?\s*>//go;
5060     $xml =~ s/>\s+</></go;
5061     $xml =~ s/\p{Cc}//go;
5062
5063     # Embed a version of OpenILS::Application::AppUtils->entityize()
5064     # to avoid having to set PERL5LIB for PostgreSQL as well
5065
5066     # If we are going to convert non-ASCII characters to XML entities,
5067     # we had better be dealing with a UTF8 string to begin with
5068     $xml = decode_utf8($xml);
5069
5070     $xml = NFC($xml);
5071
5072     # Convert raw ampersands to entities
5073     $xml =~ s/&(?!\S+;)/&amp;/gso;
5074
5075     # Convert Unicode characters to entities
5076     $xml =~ s/([\x{0080}-\x{fffd}])/sprintf('&#x%X;',ord($1))/sgoe;
5077
5078     $xml =~ s/[\x00-\x1f]//go;
5079     $_TD->{new}{marc} = $xml;
5080
5081     return "MODIFY";
5082 }
5083
5084 return;
5085 $func$ LANGUAGE PLPERLU;
5086
5087 CREATE OR REPLACE FUNCTION authority.generate_overlay_template ( TEXT, BIGINT ) RETURNS TEXT AS $func$
5088
5089     use MARC::Record;
5090     use MARC::File::XML (BinaryEncoding => 'UTF-8');
5091     use MARC::Charset;
5092
5093     MARC::Charset->assume_unicode(1);
5094
5095     my $xml = shift;
5096     my $r = MARC::Record->new_from_xml( $xml );
5097
5098     return undef unless ($r);
5099
5100     my $id = shift() || $r->subfield( '901' => 'c' );
5101     $id =~ s/^\s*(?:\([^)]+\))?\s*(.+)\s*?$/$1/;
5102     return undef unless ($id); # We need an ID!
5103
5104     my $tmpl = MARC::Record->new();
5105     $tmpl->encoding( 'UTF-8' );
5106
5107     my @rule_fields;
5108     for my $field ( $r->field( '1..' ) ) { # Get main entry fields from the authority record
5109
5110         my $tag = $field->tag;
5111         my $i1 = $field->indicator(1);
5112         my $i2 = $field->indicator(2);
5113         my $sf = join '', map { $_->[0] } $field->subfields;
5114         my @data = map { @$_ } $field->subfields;
5115
5116         my @replace_them;
5117
5118         # Map the authority field to bib fields it can control.
5119         if ($tag >= 100 and $tag <= 111) {       # names
5120             @replace_them = map { $tag + $_ } (0, 300, 500, 600, 700);
5121         } elsif ($tag eq '130') {                # uniform title
5122             @replace_them = qw/130 240 440 730 830/;
5123         } elsif ($tag >= 150 and $tag <= 155) {  # subjects
5124             @replace_them = ($tag + 500);
5125         } elsif ($tag >= 180 and $tag <= 185) {  # floating subdivisions
5126             @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/;
5127         } else {
5128             next;
5129         }
5130
5131         # Dummy up the bib-side data
5132         $tmpl->append_fields(
5133             map {
5134                 MARC::Field->new( $_, $i1, $i2, @data )
5135             } @replace_them
5136         );
5137
5138         # Construct some 'replace' rules
5139         push @rule_fields, map { $_ . $sf . '[0~\)' .$id . '$]' } @replace_them;
5140     }
5141
5142     # Insert the replace rules into the template
5143     $tmpl->append_fields(
5144         MARC::Field->new( '905' => ' ' => ' ' => 'r' => join(',', @rule_fields ) )
5145     );
5146
5147     $xml = $tmpl->as_xml_record;
5148     $xml =~ s/^<\?.+?\?>$//mo;
5149     $xml =~ s/\n//sgo;
5150     $xml =~ s/>\s+</></sgo;
5151
5152     return $xml;
5153
5154 $func$ LANGUAGE PLPERLU;
5155
5156 CREATE OR REPLACE FUNCTION vandelay.add_field ( target_xml TEXT, source_xml TEXT, field TEXT, force_add INT ) RETURNS TEXT AS $_$
5157
5158     use MARC::Record;
5159     use MARC::File::XML (BinaryEncoding => 'UTF-8');
5160     use MARC::Charset;
5161     use strict;
5162
5163     MARC::Charset->assume_unicode(1);
5164
5165     my $target_xml = shift;
5166     my $source_xml = shift;
5167     my $field_spec = shift;
5168     my $force_add = shift || 0;
5169
5170     my $target_r = MARC::Record->new_from_xml( $target_xml );
5171     my $source_r = MARC::Record->new_from_xml( $source_xml );
5172
5173     return $target_xml unless ($target_r && $source_r);
5174
5175     my @field_list = split(',', $field_spec);
5176
5177     my %fields;
5178     for my $f (@field_list) {
5179         $f =~ s/^\s*//; $f =~ s/\s*$//;
5180         if ($f =~ /^(.{3})(\w*)(?:\[([^]]*)\])?$/) {
5181             my $field = $1;
5182             $field =~ s/\s+//;
5183             my $sf = $2;
5184             $sf =~ s/\s+//;
5185             my $match = $3;
5186             $match =~ s/^\s*//; $match =~ s/\s*$//;
5187             $fields{$field} = { sf => [ split('', $sf) ] };
5188             if ($match) {
5189                 my ($msf,$mre) = split('~', $match);
5190                 if (length($msf) > 0 and length($mre) > 0) {
5191                     $msf =~ s/^\s*//; $msf =~ s/\s*$//;
5192                     $mre =~ s/^\s*//; $mre =~ s/\s*$//;
5193                     $fields{$field}{match} = { sf => $msf, re => qr/$mre/ };
5194                 }
5195             }
5196         }
5197     }
5198
5199     for my $f ( keys %fields) {
5200         if ( @{$fields{$f}{sf}} ) {
5201             for my $from_field ($source_r->field( $f )) {
5202                 my @tos = $target_r->field( $f );
5203                 if (!@tos) {
5204                     next if (exists($fields{$f}{match}) and !$force_add);
5205                     my @new_fields = map { $_->clone } $source_r->field( $f );
5206                     $target_r->insert_fields_ordered( @new_fields );
5207                 } else {
5208                     for my $to_field (@tos) {
5209                         if (exists($fields{$f}{match})) {
5210                             next unless (grep { $_ =~ $fields{$f}{match}{re} } $to_field->subfield($fields{$f}{match}{sf}));
5211                         }
5212                         my @new_sf = map { ($_ => $from_field->subfield($_)) } @{$fields{$f}{sf}};
5213                         $to_field->add_subfields( @new_sf );
5214                     }
5215                 }
5216             }
5217         } else {
5218             my @new_fields = map { $_->clone } $source_r->field( $f );
5219             $target_r->insert_fields_ordered( @new_fields );
5220         }
5221     }
5222
5223     $target_xml = $target_r->as_xml_record;
5224     $target_xml =~ s/^<\?.+?\?>$//mo;
5225     $target_xml =~ s/\n//sgo;
5226     $target_xml =~ s/>\s+</></sgo;
5227
5228     return $target_xml;
5229
5230 $_$ LANGUAGE PLPERLU;
5231
5232 CREATE OR REPLACE FUNCTION authority.normalize_heading( TEXT ) RETURNS TEXT AS $func$
5233     use strict;
5234     use warnings;
5235
5236     use utf8;
5237     use MARC::Record;
5238     use MARC::File::XML (BinaryEncoding => 'UTF8');
5239     use MARC::Charset;
5240     use UUID::Tiny ':std';
5241
5242     MARC::Charset->assume_unicode(1);
5243
5244     my $xml = shift() or return undef;
5245
5246     my $r;
5247
5248     # Prevent errors in XML parsing from blowing out ungracefully
5249     eval {
5250         $r = MARC::Record->new_from_xml( $xml );
5251         1;
5252     } or do {
5253        return 'BAD_MARCXML_' . create_uuid_as_string(UUID_MD5, $xml);
5254     };
5255
5256     if (!$r) {
5257        return 'BAD_MARCXML_' . create_uuid_as_string(UUID_MD5, $xml);
5258     }
5259
5260     # From http://www.loc.gov/standards/sourcelist/subject.html
5261     my $thes_code_map = {
5262         a => 'lcsh',
5263         b => 'lcshac',
5264         c => 'mesh',
5265         d => 'nal',
5266         k => 'cash',
5267         n => 'notapplicable',
5268         r => 'aat',
5269         s => 'sears',
5270         v => 'rvm',
5271     };
5272
5273     # Default to "No attempt to code" if the leader is horribly broken
5274     my $fixed_field = $r->field('008');
5275     my $thes_char = '|';
5276     if ($fixed_field) { 
5277         $thes_char = substr($fixed_field->data(), 11, 1) || '|';
5278     }
5279
5280     my $thes_code = 'UNDEFINED';
5281
5282     if ($thes_char eq 'z') {
5283         # Grab the 040 $f per http://www.loc.gov/marc/authority/ad040.html
5284         $thes_code = $r->subfield('040', 'f') || 'UNDEFINED';
5285     } elsif ($thes_code_map->{$thes_char}) {
5286         $thes_code = $thes_code_map->{$thes_char};
5287     }
5288
5289     my $auth_txt = '';
5290     my $head = $r->field('1..');
5291     if ($head) {
5292         # Concatenate all of these subfields together, prefixed by their code
5293         # to prevent collisions along the lines of "Fiction, North Carolina"
5294         foreach my $sf ($head->subfields()) {
5295             $auth_txt .= '‡' . $sf->[0] . ' ' . $sf->[1];
5296         }
5297     }
5298     
5299     if ($auth_txt) {
5300         my $stmt = spi_prepare('SELECT public.naco_normalize($1) AS norm_text', 'TEXT');
5301         my $result = spi_exec_prepared($stmt, $auth_txt);
5302         my $norm_txt = $result->{rows}[0]->{norm_text};
5303         spi_freeplan($stmt);
5304         undef($stmt);
5305         return $head->tag() . "_" . $thes_code . " " . $norm_txt;
5306     }
5307
5308     return 'NOHEADING_' . $thes_code . ' ' . create_uuid_as_string(UUID_MD5, $xml);
5309 $func$ LANGUAGE 'plperlu' IMMUTABLE;
5310
5311 CREATE OR REPLACE FUNCTION vandelay.strip_field ( xml TEXT, field TEXT ) RETURNS TEXT AS $_$
5312
5313     use MARC::Record;
5314     use MARC::File::XML (BinaryEncoding => 'UTF-8');
5315     use MARC::Charset;
5316     use strict;
5317
5318     MARC::Charset->assume_unicode(1);
5319
5320     my $xml = shift;
5321     my $r = MARC::Record->new_from_xml( $xml );
5322
5323     return $xml unless ($r);
5324
5325     my $field_spec = shift;
5326     my @field_list = split(',', $field_spec);
5327
5328     my %fields;
5329     for my $f (@field_list) {
5330         $f =~ s/^\s*//; $f =~ s/\s*$//;
5331         if ($f =~ /^(.{3})(\w*)(?:\[([^]]*)\])?$/) {
5332             my $field = $1;
5333             $field =~ s/\s+//;
5334             my $sf = $2;
5335             $sf =~ s/\s+//;
5336             my $match = $3;
5337             $match =~ s/^\s*//; $match =~ s/\s*$//;
5338             $fields{$field} = { sf => [ split('', $sf) ] };
5339             if ($match) {
5340                 my ($msf,$mre) = split('~', $match);
5341                 if (length($msf) > 0 and length($mre) > 0) {
5342                     $msf =~ s/^\s*//; $msf =~ s/\s*$//;
5343                     $mre =~ s/^\s*//; $mre =~ s/\s*$//;
5344                     $fields{$field}{match} = { sf => $msf, re => qr/$mre/ };
5345                 }
5346             }
5347         }
5348     }
5349
5350     for my $f ( keys %fields) {
5351         for my $to_field ($r->field( $f )) {
5352             if (exists($fields{$f}{match})) {
5353                 next unless (grep { $_ =~ $fields{$f}{match}{re} } $to_field->subfield($fields{$f}{match}{sf}));
5354             }
5355
5356             if ( @{$fields{$f}{sf}} ) {
5357                 $to_field->delete_subfield(code => $fields{$f}{sf});
5358             } else {
5359                 $r->delete_field( $to_field );
5360             }
5361         }
5362     }
5363
5364     $xml = $r->as_xml_record;
5365     $xml =~ s/^<\?.+?\?>$//mo;
5366     $xml =~ s/\n//sgo;
5367     $xml =~ s/>\s+</></sgo;
5368
5369     return $xml;
5370
5371 $_$ LANGUAGE PLPERLU;
5372
5373 CREATE OR REPLACE FUNCTION biblio.flatten_marc ( TEXT ) RETURNS SETOF metabib.full_rec AS $func$
5374
5375 use MARC::Record;
5376 use MARC::File::XML (BinaryEncoding => 'UTF-8');
5377 use MARC::Charset;
5378
5379 MARC::Charset->assume_unicode(1);
5380
5381 my $xml = shift;
5382 my $r = MARC::Record->new_from_xml( $xml );
5383
5384 return_next( { tag => 'LDR', value => $r->leader } );
5385
5386 for my $f ( $r->fields ) {
5387         if ($f->is_control_field) {
5388                 return_next({ tag => $f->tag, value => $f->data });
5389         } else {
5390                 for my $s ($f->subfields) {
5391                         return_next({
5392                                 tag      => $f->tag,
5393                                 ind1     => $f->indicator(1),
5394                                 ind2     => $f->indicator(2),
5395                                 subfield => $s->[0],
5396                                 value    => $s->[1]
5397                         });
5398
5399                         if ( $f->tag eq '245' and $s->[0] eq 'a' ) {
5400                                 my $trim = $f->indicator(2) || 0;
5401                                 return_next({
5402                                         tag      => 'tnf',
5403                                         ind1     => $f->indicator(1),
5404                                         ind2     => $f->indicator(2),
5405                                         subfield => 'a',
5406                                         value    => substr( $s->[1], $trim )
5407                                 });
5408                         }
5409                 }
5410         }
5411 }
5412
5413 return undef;
5414
5415 $func$ LANGUAGE PLPERLU;
5416
5417 CREATE OR REPLACE FUNCTION authority.flatten_marc ( TEXT ) RETURNS SETOF authority.full_rec AS $func$
5418
5419 use MARC::Record;
5420 use MARC::File::XML (BinaryEncoding => 'UTF-8');
5421 use MARC::Charset;
5422
5423 MARC::Charset->assume_unicode(1);
5424
5425 my $xml = shift;
5426 my $r = MARC::Record->new_from_xml( $xml );
5427
5428 return_next( { tag => 'LDR', value => $r->leader } );
5429
5430 for my $f ( $r->fields ) {
5431     if ($f->is_control_field) {
5432         return_next({ tag => $f->tag, value => $f->data });
5433     } else {
5434         for my $s ($f->subfields) {
5435             return_next({
5436                 tag      => $f->tag,
5437                 ind1     => $f->indicator(1),
5438                 ind2     => $f->indicator(2),
5439                 subfield => $s->[0],
5440                 value    => $s->[1]
5441             });
5442
5443         }
5444     }
5445 }
5446
5447 return undef;
5448
5449 $func$ LANGUAGE PLPERLU;
5450
5451 -- 0530
5452 CREATE INDEX actor_usr_day_phone_idx_numeric ON actor.usr USING BTREE 
5453     (evergreen.lowercase(REGEXP_REPLACE(day_phone, '[^0-9]', '', 'g')));
5454
5455 CREATE INDEX actor_usr_evening_phone_idx_numeric ON actor.usr USING BTREE 
5456     (evergreen.lowercase(REGEXP_REPLACE(evening_phone, '[^0-9]', '', 'g')));
5457
5458 CREATE INDEX actor_usr_other_phone_idx_numeric ON actor.usr USING BTREE 
5459     (evergreen.lowercase(REGEXP_REPLACE(other_phone, '[^0-9]', '', 'g')));
5460
5461 -- 0533
5462 CREATE OR REPLACE FUNCTION action.age_circ_on_delete () RETURNS TRIGGER AS $$
5463 DECLARE
5464 found char := 'N';
5465 BEGIN
5466
5467     -- If there are any renewals for this circulation, don't archive or delete
5468     -- it yet.   We'll do so later, when we archive and delete the renewals.
5469
5470     SELECT 'Y' INTO found
5471     FROM action.circulation
5472     WHERE parent_circ = OLD.id
5473     LIMIT 1;
5474
5475     IF found = 'Y' THEN
5476         RETURN NULL;  -- don't delete
5477         END IF;
5478
5479     -- Archive a copy of the old row to action.aged_circulation
5480
5481     INSERT INTO action.aged_circulation
5482         (id,usr_post_code, usr_home_ou, usr_profile, usr_birth_year, copy_call_number, copy_location,
5483         copy_owning_lib, copy_circ_lib, copy_bib_record, xact_start, xact_finish, target_copy,
5484         circ_lib, circ_staff, checkin_staff, checkin_lib, renewal_remaining, grace_period, due_date,
5485         stop_fines_time, checkin_time, create_time, duration, fine_interval, recurring_fine,
5486         max_fine, phone_renewal, desk_renewal, opac_renewal, duration_rule, recurring_fine_rule,
5487         max_fine_rule, stop_fines, workstation, checkin_workstation, checkin_scan_time, parent_circ)
5488       SELECT
5489         id,usr_post_code, usr_home_ou, usr_profile, usr_birth_year, copy_call_number, copy_location,
5490         copy_owning_lib, copy_circ_lib, copy_bib_record, xact_start, xact_finish, target_copy,
5491         circ_lib, circ_staff, checkin_staff, checkin_lib, renewal_remaining, grace_period, due_date,
5492         stop_fines_time, checkin_time, create_time, duration, fine_interval, recurring_fine,
5493         max_fine, phone_renewal, desk_renewal, opac_renewal, duration_rule, recurring_fine_rule,
5494         max_fine_rule, stop_fines, workstation, checkin_workstation, checkin_scan_time, parent_circ
5495         FROM action.all_circulation WHERE id = OLD.id;
5496
5497     RETURN OLD;
5498 END;
5499 $$ LANGUAGE 'plpgsql';
5500
5501 -- 0534
5502 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$
5503 DECLARE
5504     matchpoint_id        INT;
5505     user_object        actor.usr%ROWTYPE;
5506     age_protect_object    config.rule_age_hold_protect%ROWTYPE;
5507     standing_penalty    config.standing_penalty%ROWTYPE;
5508     transit_range_ou_type    actor.org_unit_type%ROWTYPE;
5509     transit_source        actor.org_unit%ROWTYPE;
5510     item_object        asset.copy%ROWTYPE;
5511     item_cn_object     asset.call_number%ROWTYPE;
5512     ou_skip              actor.org_unit_setting%ROWTYPE;
5513     result            action.matrix_test_result;
5514     hold_test        config.hold_matrix_matchpoint%ROWTYPE;
5515     hold_count        INT;
5516     hold_transit_prox    INT;
5517     frozen_hold_count    INT;
5518     context_org_list    INT[];
5519     done            BOOL := FALSE;
5520 BEGIN
5521     SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
5522     SELECT INTO context_org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( pickup_ou );
5523
5524     result.success := TRUE;
5525
5526     -- Fail if we couldn't find a user
5527     IF user_object.id IS NULL THEN
5528         result.fail_part := 'no_user';
5529         result.success := FALSE;
5530         done := TRUE;
5531         RETURN NEXT result;
5532         RETURN;
5533     END IF;
5534
5535     SELECT INTO item_object * FROM asset.copy WHERE id = match_item;
5536
5537     -- Fail if we couldn't find a copy
5538     IF item_object.id IS NULL THEN
5539         result.fail_part := 'no_item';
5540         result.success := FALSE;
5541         done := TRUE;
5542         RETURN NEXT result;
5543         RETURN;
5544     END IF;
5545
5546     SELECT INTO matchpoint_id action.find_hold_matrix_matchpoint(pickup_ou, request_ou, match_item, match_user, match_requestor);
5547     result.matchpoint := matchpoint_id;
5548
5549     SELECT INTO ou_skip * FROM actor.org_unit_setting WHERE name = 'circ.holds.target_skip_me' AND org_unit = item_object.circ_lib;
5550
5551     -- Fail if the circ_lib for the item has circ.holds.target_skip_me set to true
5552     IF ou_skip.id IS NOT NULL AND ou_skip.value = 'true' THEN
5553         result.fail_part := 'circ.holds.target_skip_me';
5554         result.success := FALSE;
5555         done := TRUE;
5556         RETURN NEXT result;
5557         RETURN;
5558     END IF;
5559
5560     -- Fail if user is barred
5561     IF user_object.barred IS TRUE THEN
5562         result.fail_part := 'actor.usr.barred';
5563         result.success := FALSE;
5564         done := TRUE;
5565         RETURN NEXT result;
5566         RETURN;
5567     END IF;
5568
5569     -- Fail if we couldn't find any matchpoint (requires a default)
5570     IF matchpoint_id IS NULL THEN
5571         result.fail_part := 'no_matchpoint';
5572         result.success := FALSE;
5573         done := TRUE;
5574         RETURN NEXT result;
5575         RETURN;
5576     END IF;
5577
5578     SELECT INTO hold_test * FROM config.hold_matrix_matchpoint WHERE id = matchpoint_id;
5579
5580     IF hold_test.holdable IS FALSE THEN
5581         result.fail_part := 'config.hold_matrix_test.holdable';
5582         result.success := FALSE;
5583         done := TRUE;
5584         RETURN NEXT result;
5585     END IF;
5586
5587     IF hold_test.transit_range IS NOT NULL THEN
5588         SELECT INTO transit_range_ou_type * FROM actor.org_unit_type WHERE id = hold_test.transit_range;
5589         IF hold_test.distance_is_from_owner THEN
5590             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;
5591         ELSE
5592             SELECT INTO transit_source * FROM actor.org_unit WHERE id = item_object.circ_lib;
5593         END IF;
5594
5595         PERFORM * FROM actor.org_unit_descendants( transit_source.id, transit_range_ou_type.depth ) WHERE id = pickup_ou;
5596
5597         IF NOT FOUND THEN
5598             result.fail_part := 'transit_range';
5599             result.success := FALSE;
5600             done := TRUE;
5601             RETURN NEXT result;
5602         END IF;
5603     END IF;
5604  
5605     FOR standing_penalty IN
5606         SELECT  DISTINCT csp.*
5607           FROM  actor.usr_standing_penalty usp
5608                 JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
5609           WHERE usr = match_user
5610                 AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
5611                 AND (usp.stop_date IS NULL or usp.stop_date > NOW())
5612                 AND csp.block_list LIKE '%HOLD%' LOOP
5613
5614         result.fail_part := standing_penalty.name;
5615         result.success := FALSE;
5616         done := TRUE;
5617         RETURN NEXT result;
5618     END LOOP;
5619
5620     IF hold_test.stop_blocked_user IS TRUE THEN
5621         FOR standing_penalty IN
5622             SELECT  DISTINCT csp.*
5623               FROM  actor.usr_standing_penalty usp
5624                     JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
5625               WHERE usr = match_user
5626                     AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
5627                     AND (usp.stop_date IS NULL or usp.stop_date > NOW())
5628                     AND csp.block_list LIKE '%CIRC%' LOOP
5629     
5630             result.fail_part := standing_penalty.name;
5631             result.success := FALSE;
5632             done := TRUE;
5633             RETURN NEXT result;
5634         END LOOP;
5635     END IF;
5636
5637     IF hold_test.max_holds IS NOT NULL AND NOT retargetting THEN
5638         SELECT    INTO hold_count COUNT(*)
5639           FROM    action.hold_request
5640           WHERE    usr = match_user
5641             AND fulfillment_time IS NULL
5642             AND cancel_time IS NULL
5643             AND CASE WHEN hold_test.include_frozen_holds THEN TRUE ELSE frozen IS FALSE END;
5644
5645         IF hold_count >= hold_test.max_holds THEN
5646             result.fail_part := 'config.hold_matrix_test.max_holds';
5647             result.success := FALSE;
5648             done := TRUE;
5649             RETURN NEXT result;
5650         END IF;
5651     END IF;
5652
5653     IF item_object.age_protect IS NOT NULL THEN
5654         SELECT INTO age_protect_object * FROM config.rule_age_hold_protect WHERE id = item_object.age_protect;
5655
5656         IF item_object.create_date + age_protect_object.age > NOW() THEN
5657             IF hold_test.distance_is_from_owner THEN
5658                 SELECT INTO item_cn_object * FROM asset.call_number WHERE id = item_object.call_number;
5659                 SELECT INTO hold_transit_prox prox FROM actor.org_unit_proximity WHERE from_org = item_cn_object.owning_lib AND to_org = pickup_ou;
5660             ELSE
5661                 SELECT INTO hold_transit_prox prox FROM actor.org_unit_proximity WHERE from_org = item_object.circ_lib AND to_org = pickup_ou;
5662             END IF;
5663
5664             IF hold_transit_prox > age_protect_object.prox THEN
5665                 result.fail_part := 'config.rule_age_hold_protect.prox';
5666                 result.success := FALSE;
5667                 done := TRUE;
5668                 RETURN NEXT result;
5669             END IF;
5670         END IF;
5671     END IF;
5672
5673     IF NOT done THEN
5674         RETURN NEXT result;
5675     END IF;
5676
5677     RETURN;
5678 END;
5679 $func$ LANGUAGE plpgsql;
5680
5681 -- do potentially large updates last to save time if upgrader needs
5682 -- to manually tweak the upgrade script to resolve errors
5683
5684 -- 0505
5685 UPDATE metabib.facet_entry SET value = evergreen.force_unicode_normal_form(value,'NFC');
5686
5687 -- Update reporter.materialized_simple_record with normalized ISBN values
5688 -- This might not get all of them, but most ISBNs will have more than one hyphen
5689 DELETE FROM reporter.materialized_simple_record WHERE id IN (
5690     SELECT record FROM metabib.full_rec WHERE tag = '020' AND subfield IN ('a', 'z') AND value LIKE '%-%-%'
5691 );
5692
5693 INSERT INTO reporter.materialized_simple_record
5694     SELECT DISTINCT rossr.* FROM reporter.old_super_simple_record rossr INNER JOIN metabib.full_rec mfr ON mfr.record = rossr.id
5695         WHERE mfr.tag = '020' AND mfr.subfield IN ('a', 'z') AND mfr.value LIKE '%-%-%'
5696 ;
5697
5698 INSERT INTO config.upgrade_log (version) VALUES ('0542'); -- phasefx
5699
5700 INSERT INTO permission.perm_list
5701     SELECT  np.*
5702       FROM  (VALUES
5703                 (485, 'CREATE_VOLUME_SUFFIX', oils_i18n_gettext(485, 'Create suffix label definition.', 'ppl', 'description'))
5704                 ,(486, 'UPDATE_VOLUME_SUFFIX', oils_i18n_gettext(486, 'Update suffix label definition.', 'ppl', 'description'))
5705                 ,(487, 'DELETE_VOLUME_SUFFIX', oils_i18n_gettext(487, 'Delete suffix label definition.', 'ppl', 'description'))
5706                 ,(488, 'CREATE_VOLUME_PREFIX', oils_i18n_gettext(488, 'Create prefix label definition.', 'ppl', 'description'))
5707                 ,(489, 'UPDATE_VOLUME_PREFIX', oils_i18n_gettext(489, 'Update prefix label definition.', 'ppl', 'description'))
5708                 ,(490, 'DELETE_VOLUME_PREFIX', oils_i18n_gettext(490, 'Delete prefix label definition.', 'ppl', 'description'))
5709                 ,(491, 'CREATE_MONOGRAPH_PART', oils_i18n_gettext(491, 'Create monograph part definition.', 'ppl', 'description'))
5710                 ,(492, 'UPDATE_MONOGRAPH_PART', oils_i18n_gettext(492, 'Update monograph part definition.', 'ppl', 'description'))
5711                 ,(493, 'DELETE_MONOGRAPH_PART', oils_i18n_gettext(493, 'Delete monograph part definition.', 'ppl', 'description'))
5712                 ,(494, 'ADMIN_CODED_VALUE', oils_i18n_gettext(494, 'Create/Update/Delete SVF Record Attribute Coded Value Map', 'ppl', 'description'))
5713                 ,(495, 'ADMIN_SERIAL_ITEM', oils_i18n_gettext(495, 'Create/Retrieve/Update/Delete Serial Item', 'ppl', 'description'))
5714                 ,(496, 'ADMIN_SVF', oils_i18n_gettext(496, 'Create/Update/Delete SVF Record Attribute Defintion', 'ppl', 'description'))
5715                 ,(497, 'CREATE_BIB_PTYPE', oils_i18n_gettext(497, 'Create Bibliographic Record Peer Type', 'ppl', 'description'))
5716                 ,(498, 'CREATE_PURCHASE_REQUEST', oils_i18n_gettext(498, 'Create User Purchase Request', 'ppl', 'description'))
5717                 ,(499, 'DELETE_BIB_PTYPE', oils_i18n_gettext(499, 'Delete Bibliographic Record Peer Type', 'ppl', 'description'))
5718                 ,(500, 'MAP_MONOGRAPH_PART', oils_i18n_gettext(500, 'Create/Update/Delete Copy Monograph Part Map', 'ppl', 'description'))
5719                 ,(501, 'MARK_ITEM_MISSING_PIECES', oils_i18n_gettext(501, 'Allows the Mark Item Missing Pieces action.', 'ppl', 'description'))
5720                 ,(502, 'UPDATE_BIB_PTYPE', oils_i18n_gettext(502, 'Update Bibliographic Record Peer Type', 'ppl', 'description'))
5721                 ,(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'))
5722                 ,(504, 'UPDATE_PICKLIST', oils_i18n_gettext(504, 'Allows update/re-use of an acquisitions pick/selection list.', 'ppl', 'description'))
5723                 ,(505, 'UPDATE_WORKSTATION', oils_i18n_gettext(505, 'Allows update of a workstation during workstation registration override.', 'ppl', 'description'))
5724                 ,(506, 'VIEW_USER_SETTING_TYPE', oils_i18n_gettext(506, 'Allows viewing of configurable user setting types.', 'ppl', 'description'))
5725             ) AS np(id,code,description)
5726             LEFT JOIN permission.perm_list pl USING (code)
5727       WHERE pl.id IS NULL;
5728 ;
5729
5730
5731 -- add new perms AND catch up on some missed upgrade data, if needed
5732
5733 -- Prevent conflicts with existing custom permission groups; as of 2.0.10, 
5734 -- highest permission.grp_tree ID was 10 for Local System Administrator
5735 DO $$
5736 DECLARE i INTEGER;
5737 BEGIN
5738     FOR i IN
5739         SELECT id FROM permission.grp_tree WHERE id > 10 ORDER BY id DESC
5740     LOOP
5741         UPDATE permission.grp_tree SET id = id + 100 WHERE id = i;
5742     END LOOP;
5743 END;
5744 $$;
5745 UPDATE permission.grp_tree SET parent = parent + 100 WHERE parent > 10;
5746 UPDATE actor.usr SET profile = profile + 100 WHERE profile > 10;
5747 UPDATE permission.grp_perm_map SET grp = grp + 100 WHERE grp > 10;
5748 UPDATE permission.usr_grp_map SET grp = grp + 100 WHERE grp > 10;
5749 UPDATE permission.grp_penalty_threshold SET grp = grp + 100 WHERE grp > 10;
5750 UPDATE config.circ_matrix_matchpoint SET grp = grp + 100 WHERE grp > 10;
5751 UPDATE config.hold_matrix_matchpoint SET requestor_grp = requestor_grp + 100 WHERE requestor_grp > 10;
5752 UPDATE config.hold_matrix_matchpoint SET usr_grp = usr_grp + 100 WHERE usr_grp > 10;
5753
5754 -- we could get away from these fixed-id inserts here, but then this
5755 -- upgrade would be ahead of the mainline, I think
5756
5757 INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm)
5758         SELECT 8, oils_i18n_gettext(8, 'Cataloging Administrator', 'pgt', 'name'), 3, NULL, '3 years', TRUE, 'group_application.user.staff.cat_admin'
5759         WHERE NOT EXISTS (
5760                 SELECT 1
5761                 FROM permission.grp_tree
5762                 WHERE
5763                         id = 8
5764         );
5765
5766 INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm)
5767         SELECT 9, oils_i18n_gettext(9, 'Circulation Administrator', 'pgt', 'name'), 3, NULL, '3 years', TRUE, 'group_application.user.staff.circ_admin'
5768         WHERE NOT EXISTS (
5769                 SELECT 1
5770                 FROM permission.grp_tree
5771                 WHERE
5772                         id = 9
5773         );
5774
5775 UPDATE permission.grp_tree SET description = oils_i18n_gettext(10, 'Can do anything at the Branch level', 'pgt', 'description') WHERE id = 10;
5776
5777 INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm)
5778         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'
5779         WHERE NOT EXISTS (
5780                 SELECT 1
5781                 FROM permission.grp_tree
5782                 WHERE
5783                         id = 11
5784         );
5785
5786 INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm)
5787         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'
5788         WHERE NOT EXISTS (
5789                 SELECT 1
5790                 FROM permission.grp_tree
5791                 WHERE
5792                         id = 12
5793         );
5794
5795 INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm)
5796         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'
5797         WHERE NOT EXISTS (
5798                 SELECT 1
5799                 FROM permission.grp_tree
5800                 WHERE
5801                         id = 13
5802         );
5803
5804 INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm)
5805         SELECT 14, oils_i18n_gettext(14, 'Data Review', 'pgt', 'name'), 3, NULL, '3 years', TRUE, 'group_application.user.staff.data_review'
5806         WHERE NOT EXISTS (
5807                 SELECT 1
5808                 FROM permission.grp_tree
5809                 WHERE
5810                         id = 14
5811         );
5812
5813 INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm)
5814         SELECT 15, oils_i18n_gettext(15, 'Volunteers', 'pgt', 'name'), 3, NULL, '3 years', TRUE, 'group_application.user.staff.volunteers'
5815         WHERE NOT EXISTS (
5816                 SELECT 1
5817                 FROM permission.grp_tree
5818                 WHERE
5819                         id = 15
5820         );
5821
5822 SELECT SETVAL('permission.grp_tree_id_seq'::TEXT, (SELECT MAX(id) FROM permission.grp_tree));
5823
5824 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
5825         SELECT
5826                 pgt.id, perm.id, aout.depth, TRUE
5827         FROM
5828                 permission.grp_tree pgt,
5829                 permission.perm_list perm,
5830                 actor.org_unit_type aout
5831         WHERE
5832                 pgt.name = 'Cataloging Administrator' AND
5833                 aout.name = 'Consortium' AND
5834                 perm.code IN (
5835                         'ADMIN_IMPORT_ITEM_ATTR_DEF',
5836                         'ADMIN_MERGE_PROFILE',
5837                         'CREATE_AUTHORITY_IMPORT_IMPORT_DEF',
5838                         'CREATE_BIB_IMPORT_FIELD_DEF',
5839                         'CREATE_BIB_PTYPE',
5840                         'CREATE_BIB_SOURCE',
5841                         'CREATE_IMPORT_ITEM_ATTR_DEF',
5842                         'CREATE_IMPORT_TRASH_FIELD',
5843                         'CREATE_MERGE_PROFILE',
5844                         'CREATE_MONOGRAPH_PART',
5845                         'CREATE_VOLUME_PREFIX',
5846                         'CREATE_VOLUME_SUFFIX',
5847                         'DELETE_AUTHORITY_IMPORT_IMPORT_FIELD_DEF',
5848                         'DELETE_BIB_PTYPE',
5849                         'DELETE_BIB_SOURCE',
5850                         'DELETE_IMPORT_ITEM_ATTR_DEF',
5851                         'DELETE_IMPORT_TRASH_FIELD',
5852                         'DELETE_MERGE_PROFILE',
5853                         'DELETE_MONOGRAPH_PART',
5854                         'DELETE_VOLUME_PREFIX',
5855                         'DELETE_VOLUME_SUFFIX',
5856                         'MAP_MONOGRAPH_PART',
5857                         'UPDATE_AUTHORITY_IMPORT_IMPORT_FIELD_DEF',
5858                         'UPDATE_BIB_IMPORT_IMPORT_FIELD_DEF',
5859                         'UPDATE_BIB_PTYPE',
5860                         'UPDATE_IMPORT_ITEM_ATTR_DEF',
5861                         'UPDATE_IMPORT_TRASH_FIELD',
5862                         'UPDATE_MERGE_PROFILE',
5863                         'UPDATE_MONOGRAPH_PART',
5864                         'UPDATE_VOLUME_PREFIX',
5865                         'UPDATE_VOLUME_SUFFIX'
5866                 ) AND NOT EXISTS (
5867                         SELECT 1
5868                         FROM permission.grp_perm_map AS map
5869                         WHERE
5870                                 map.grp = pgt.id
5871                                 AND map.perm = perm.id
5872                 );
5873
5874 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
5875         SELECT
5876                 pgt.id, perm.id, aout.depth, TRUE
5877         FROM
5878                 permission.grp_tree pgt,
5879                 permission.perm_list perm,
5880                 actor.org_unit_type aout
5881         WHERE
5882                 pgt.name = 'Circulation Administrator' AND
5883                 aout.name = 'Branch' AND
5884                 perm.code IN (
5885                         'DELETE_USER'
5886                 ) AND NOT EXISTS (
5887                         SELECT 1
5888                         FROM permission.grp_perm_map AS map
5889                         WHERE
5890                                 map.grp = pgt.id
5891                                 AND map.perm = perm.id
5892                 );
5893
5894 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
5895         SELECT
5896                 pgt.id, perm.id, aout.depth, TRUE
5897         FROM
5898                 permission.grp_tree pgt,
5899                 permission.perm_list perm,
5900                 actor.org_unit_type aout
5901         WHERE
5902                 pgt.name = 'Circulation Administrator' AND
5903                 aout.name = 'Consortium' AND
5904                 perm.code IN (
5905                         'ADMIN_MAX_FINE_RULE',
5906                         'CREATE_CIRC_DURATION',
5907                         'DELETE_CIRC_DURATION',
5908                         'MARK_ITEM_MISSING_PIECES',
5909                         'UPDATE_CIRC_DURATION',
5910                         'UPDATE_HOLD_REQUEST_TIME',
5911                         'UPDATE_NET_ACCESS_LEVEL',
5912                         'VIEW_CIRC_MATRIX_MATCHPOINT',
5913                         'VIEW_HOLD_MATRIX_MATCHPOINT'
5914                 ) AND NOT EXISTS (
5915                         SELECT 1
5916                         FROM permission.grp_perm_map AS map
5917                         WHERE
5918                                 map.grp = pgt.id
5919                                 AND map.perm = perm.id
5920                 );
5921
5922 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
5923         SELECT
5924                 pgt.id, perm.id, aout.depth, TRUE
5925         FROM
5926                 permission.grp_tree pgt,
5927                 permission.perm_list perm,
5928                 actor.org_unit_type aout
5929         WHERE
5930                 pgt.name = 'Circulation Administrator' AND
5931                 aout.name = 'System' AND
5932                 perm.code IN (
5933                         'ADMIN_BOOKING_RESERVATION',
5934                         'ADMIN_BOOKING_RESERVATION_ATTR_MAP',
5935                         'ADMIN_BOOKING_RESERVATION_ATTR_VALUE_MAP',
5936                         'ADMIN_BOOKING_RESOURCE',
5937                         'ADMIN_BOOKING_RESOURCE_ATTR',
5938                         'ADMIN_BOOKING_RESOURCE_ATTR_MAP',
5939                         'ADMIN_BOOKING_RESOURCE_ATTR_VALUE',
5940                         'ADMIN_BOOKING_RESOURCE_TYPE',
5941                         'ADMIN_COPY_LOCATION_ORDER',
5942                         'ADMIN_HOLD_CANCEL_CAUSE',
5943                         'ASSIGN_GROUP_PERM',
5944                         'BAR_PATRON',
5945                         'COPY_HOLDS',
5946                         'COPY_TRANSIT_RECEIVE',
5947                         'CREATE_BILL',
5948                         'CREATE_BILLING_TYPE',
5949                         'CREATE_NON_CAT_TYPE',
5950                         'CREATE_PATRON_STAT_CAT',
5951                         'CREATE_PATRON_STAT_CAT_ENTRY',
5952                         'CREATE_PATRON_STAT_CAT_ENTRY_MAP',
5953                         'CREATE_USER_GROUP_LINK',
5954                         'DELETE_BILLING_TYPE',
5955                         'DELETE_NON_CAT_TYPE',
5956                         'DELETE_PATRON_STAT_CAT',
5957                         'DELETE_PATRON_STAT_CAT_ENTRY',
5958                         'DELETE_PATRON_STAT_CAT_ENTRY_MAP',
5959                         'DELETE_TRANSIT',
5960                         'group_application.user.staff',
5961                         'MANAGE_BAD_DEBT',
5962                         'MARK_ITEM_AVAILABLE',
5963                         'MARK_ITEM_BINDERY',
5964                         'MARK_ITEM_CHECKED_OUT',
5965                         'MARK_ITEM_ILL',
5966                         'MARK_ITEM_IN_PROCESS',
5967                         'MARK_ITEM_IN_TRANSIT',
5968                         'MARK_ITEM_LOST',
5969                         'MARK_ITEM_MISSING',
5970                         'MARK_ITEM_ON_HOLDS_SHELF',
5971                         'MARK_ITEM_ON_ORDER',
5972                         'MARK_ITEM_RESHELVING',
5973                         'MERGE_USERS',
5974                         'money.collections_tracker.create',
5975                         'money.collections_tracker.delete',
5976                         'OFFLINE_EXECUTE',
5977                         'OFFLINE_UPLOAD',
5978                         'OFFLINE_VIEW',
5979                         'REMOVE_USER_GROUP_LINK',
5980                         'SET_CIRC_CLAIMS_RETURNED',
5981                         'SET_CIRC_CLAIMS_RETURNED.override',
5982                         'SET_CIRC_LOST',
5983                         'SET_CIRC_MISSING',
5984                         'UNBAR_PATRON',
5985                         'UPDATE_BILL_NOTE',
5986                         'UPDATE_NON_CAT_TYPE',
5987                         'UPDATE_PATRON_CLAIM_NEVER_CHECKED_OUT_COUNT',
5988                         'UPDATE_PATRON_CLAIM_RETURN_COUNT',
5989                         'UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF',
5990                         'UPDATE_PICKUP_LIB_FROM_TRANSIT',
5991                         'UPDATE_USER',
5992                         'VIEW_REPORT_OUTPUT',
5993                         'VIEW_STANDING_PENALTY',
5994                         'VOID_BILLING',
5995                         'VOLUME_HOLDS'
5996                 ) AND NOT EXISTS (
5997                         SELECT 1
5998                         FROM permission.grp_perm_map AS map
5999                         WHERE
6000                                 map.grp = pgt.id
6001                                 AND map.perm = perm.id
6002                 );
6003
6004 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6005         SELECT
6006                 pgt.id, perm.id, aout.depth, TRUE
6007         FROM
6008                 permission.grp_tree pgt,
6009                 permission.perm_list perm,
6010                 actor.org_unit_type aout
6011         WHERE
6012                 pgt.name = 'Local Administrator' AND
6013                 aout.name = 'Branch' AND
6014                 perm.code IN (
6015                         'EVERYTHING'
6016                 ) AND NOT EXISTS (
6017                         SELECT 1
6018                         FROM permission.grp_perm_map AS map
6019                         WHERE
6020                                 map.grp = pgt.id
6021                                 AND map.perm = perm.id
6022                 );
6023
6024 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6025         SELECT
6026                 pgt.id, perm.id, aout.depth, FALSE
6027         FROM
6028                 permission.grp_tree pgt,
6029                 permission.perm_list perm,
6030                 actor.org_unit_type aout
6031         WHERE
6032                 pgt.name = 'Serials' AND
6033                 aout.name = 'System' AND
6034                 perm.code IN (
6035                         'ADMIN_ASSET_COPY_TEMPLATE',
6036                         'ADMIN_SERIAL_CAPTION_PATTERN',
6037                         'ADMIN_SERIAL_DISTRIBUTION',
6038                         'ADMIN_SERIAL_ITEM',
6039                         'ADMIN_SERIAL_STREAM',
6040                         'ADMIN_SERIAL_SUBSCRIPTION',
6041                         'ISSUANCE_HOLDS',
6042                         'RECEIVE_SERIAL'
6043                 ) AND NOT EXISTS (
6044                         SELECT 1
6045                         FROM permission.grp_perm_map AS map
6046                         WHERE
6047                                 map.grp = pgt.id
6048                                 AND map.perm = perm.id
6049                 );
6050
6051 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6052         SELECT
6053                 pgt.id, perm.id, aout.depth, TRUE
6054         FROM
6055                 permission.grp_tree pgt,
6056                 permission.perm_list perm,
6057                 actor.org_unit_type aout
6058         WHERE
6059                 pgt.name = 'System Administrator' AND
6060                 aout.name = 'System' AND
6061                 perm.code IN (
6062                         'EVERYTHING'
6063                 ) AND NOT EXISTS (
6064                         SELECT 1
6065                         FROM permission.grp_perm_map AS map
6066                         WHERE
6067                                 map.grp = pgt.id
6068                                 AND map.perm = perm.id
6069                 );
6070
6071 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6072         SELECT
6073                 pgt.id, perm.id, aout.depth, FALSE
6074         FROM
6075                 permission.grp_tree pgt,
6076                 permission.perm_list perm,
6077                 actor.org_unit_type aout
6078         WHERE
6079                 pgt.name = 'System Administrator' AND
6080                 aout.name = 'Consortium' AND
6081                 perm.code ~ '^VIEW_TRIGGER'
6082                 AND NOT EXISTS (
6083                         SELECT 1
6084                         FROM permission.grp_perm_map AS map
6085                         WHERE
6086                                 map.grp = pgt.id
6087                                 AND map.perm = perm.id
6088                 );
6089
6090 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6091         SELECT
6092                 pgt.id, perm.id, aout.depth, TRUE
6093         FROM
6094                 permission.grp_tree pgt,
6095                 permission.perm_list perm,
6096                 actor.org_unit_type aout
6097         WHERE
6098                 pgt.name = 'Global Administrator' AND
6099                 aout.name = 'Consortium' AND
6100                 perm.code IN (
6101                         'EVERYTHING'
6102                 ) AND NOT EXISTS (
6103                         SELECT 1
6104                         FROM permission.grp_perm_map AS map
6105                         WHERE
6106                                 map.grp = pgt.id
6107                                 AND map.perm = perm.id
6108                 );
6109
6110 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6111         SELECT
6112                 pgt.id, perm.id, aout.depth, FALSE
6113         FROM
6114                 permission.grp_tree pgt,
6115                 permission.perm_list perm,
6116                 actor.org_unit_type aout
6117         WHERE
6118                 pgt.name = 'Data Review' AND
6119                 aout.name = 'Consortium' AND
6120                 perm.code IN (
6121                         'CREATE_COPY_TRANSIT',
6122                         'VIEW_BILLING_TYPE',
6123                         'VIEW_CIRCULATIONS',
6124                         'VIEW_COPY_NOTES',
6125                         'VIEW_HOLD',
6126                         'VIEW_ORG_SETTINGS',
6127                         'VIEW_TITLE_NOTES',
6128                         'VIEW_TRANSACTION',
6129                         'VIEW_USER',
6130                         'VIEW_USER_FINES_SUMMARY',
6131                         'VIEW_USER_TRANSACTIONS',
6132                         'VIEW_VOLUME_NOTES',
6133                         'VIEW_ZIP_DATA'
6134                 ) AND NOT EXISTS (
6135                         SELECT 1
6136                         FROM permission.grp_perm_map AS map
6137                         WHERE
6138                                 map.grp = pgt.id
6139                                 AND map.perm = perm.id
6140                 );
6141
6142 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6143         SELECT
6144                 pgt.id, perm.id, aout.depth, FALSE
6145         FROM
6146                 permission.grp_tree pgt,
6147                 permission.perm_list perm,
6148                 actor.org_unit_type aout
6149         WHERE
6150                 pgt.name = 'Data Review' AND
6151                 aout.name = 'System' AND
6152                 perm.code IN (
6153                         'COPY_CHECKOUT',
6154                         'COPY_HOLDS',
6155                         'CREATE_IN_HOUSE_USE',
6156                         'CREATE_TRANSACTION',
6157                         'OFFLINE_EXECUTE',
6158                         'OFFLINE_VIEW',
6159                         'STAFF_LOGIN',
6160                         'VOLUME_HOLDS'
6161                 ) AND NOT EXISTS (
6162                         SELECT 1
6163                         FROM permission.grp_perm_map AS map
6164                         WHERE
6165                                 map.grp = pgt.id
6166                                 AND map.perm = perm.id
6167                 );
6168
6169 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6170         SELECT
6171                 pgt.id, perm.id, aout.depth, FALSE
6172         FROM
6173                 permission.grp_tree pgt,
6174                 permission.perm_list perm,
6175                 actor.org_unit_type aout
6176         WHERE
6177                 pgt.name = 'Volunteers' AND
6178                 aout.name = 'Branch' AND
6179                 perm.code IN (
6180                         'COPY_CHECKOUT',
6181                         'CREATE_BILL',
6182                         'CREATE_IN_HOUSE_USE',
6183                         'CREATE_PAYMENT',
6184                         'VIEW_BILLING_TYPE',
6185                         'VIEW_CIRCS',
6186                         'VIEW_COPY_CHECKOUT',
6187                         'VIEW_HOLD',
6188                         'VIEW_TITLE_HOLDS',
6189                         'VIEW_TRANSACTION',
6190                         'VIEW_USER',
6191                         'VIEW_USER_FINES_SUMMARY',
6192                         'VIEW_USER_TRANSACTIONS'
6193                 ) AND NOT EXISTS (
6194                         SELECT 1
6195                         FROM permission.grp_perm_map AS map
6196                         WHERE
6197                                 map.grp = pgt.id
6198                                 AND map.perm = perm.id
6199                 );
6200
6201 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6202         SELECT
6203                 pgt.id, perm.id, aout.depth, FALSE
6204         FROM
6205                 permission.grp_tree pgt,
6206                 permission.perm_list perm,
6207                 actor.org_unit_type aout
6208         WHERE
6209                 pgt.name = 'Volunteers' AND
6210                 aout.name = 'Consortium' AND
6211                 perm.code IN (
6212                         'CREATE_COPY_TRANSIT',
6213                         'CREATE_TRANSACTION',
6214                         'CREATE_TRANSIT',
6215                         'STAFF_LOGIN',
6216                         'TRANSIT_COPY',
6217                         'VIEW_ORG_SETTINGS'
6218                 ) AND NOT EXISTS (
6219                         SELECT 1
6220                         FROM permission.grp_perm_map AS map
6221                         WHERE
6222                                 map.grp = pgt.id
6223                                 AND map.perm = perm.id
6224                 );
6225
6226
6227 -- stock Users group
6228 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6229         SELECT
6230                 pgt.id, perm.id, aout.depth, FALSE
6231         FROM
6232                 permission.grp_tree pgt,
6233                 permission.perm_list perm,
6234                 actor.org_unit_type aout
6235         WHERE
6236                 pgt.name = 'Users' AND
6237                 aout.name = 'Consortium' AND
6238                 perm.code IN (
6239                         'CREATE_PURCHASE_REQUEST'
6240                 ) AND NOT EXISTS (
6241                         SELECT 1
6242                         FROM permission.grp_perm_map AS map
6243                         WHERE
6244                                 map.grp = pgt.id
6245                                 AND map.perm = perm.id
6246                 );
6247
6248 -- stock Staff group
6249 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6250         SELECT
6251                 pgt.id, perm.id, aout.depth, FALSE
6252         FROM
6253                 permission.grp_tree pgt,
6254                 permission.perm_list perm,
6255                 actor.org_unit_type aout
6256         WHERE
6257                 pgt.name = 'Staff' AND
6258                 aout.name = 'Consortium' AND
6259                 perm.code IN (
6260                         'VIEW_USER_SETTING_TYPE'
6261                 ) AND NOT EXISTS (
6262                         SELECT 1
6263                         FROM permission.grp_perm_map AS map
6264                         WHERE
6265                                 map.grp = pgt.id
6266                                 AND map.perm = perm.id
6267                 );
6268
6269 -- stock Circulators group
6270 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6271         SELECT
6272                 pgt.id, perm.id, aout.depth, FALSE
6273         FROM
6274                 permission.grp_tree pgt,
6275                 permission.perm_list perm,
6276                 actor.org_unit_type aout
6277         WHERE
6278                 pgt.name = 'Circulators' AND
6279                 aout.name = 'Branch' AND
6280                 perm.code IN (
6281                         'MARK_ITEM_MISSING_PIECES'
6282                 ) AND NOT EXISTS (
6283                         SELECT 1
6284                         FROM permission.grp_perm_map AS map
6285                         WHERE
6286                                 map.grp = pgt.id
6287                                 AND map.perm = perm.id
6288                 );
6289
6290 -- stock Catalogers group
6291 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6292         SELECT
6293                 pgt.id, perm.id, aout.depth, FALSE
6294         FROM
6295                 permission.grp_tree pgt,
6296                 permission.perm_list perm,
6297                 actor.org_unit_type aout
6298         WHERE
6299                 pgt.name = 'Catalogers' AND
6300                 aout.name = 'System' AND
6301                 perm.code IN (
6302                         'MAP_MONOGRAPH_PART'
6303                 ) AND NOT EXISTS (
6304                         SELECT 1
6305                         FROM permission.grp_perm_map AS map
6306                         WHERE
6307                                 map.grp = pgt.id
6308                                 AND map.perm = perm.id
6309                 );
6310
6311 -- stock Acquisitions group
6312 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6313         SELECT
6314                 pgt.id, perm.id, aout.depth, FALSE
6315         FROM
6316                 permission.grp_tree pgt,
6317                 permission.perm_list perm,
6318                 actor.org_unit_type aout
6319         WHERE
6320                 pgt.name = 'Acquisitions' AND
6321                 aout.name = 'Consortium' AND
6322                 perm.code IN (
6323                         'UPDATE_PICKLIST'
6324                 ) AND NOT EXISTS (
6325                         SELECT 1
6326                         FROM permission.grp_perm_map AS map
6327                         WHERE
6328                                 map.grp = pgt.id
6329                                 AND map.perm = perm.id
6330                 );
6331
6332 -- stock Acq Admin group
6333 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6334         SELECT
6335                 pgt.id, perm.id, aout.depth, TRUE
6336         FROM
6337                 permission.grp_tree pgt,
6338                 permission.perm_list perm,
6339                 actor.org_unit_type aout
6340         WHERE
6341                 pgt.name = 'Acquisitions Administrator' AND
6342                 aout.name = 'Consortium' AND
6343                 perm.code IN (
6344                         'UPDATE_PICKLIST'
6345                 ) AND NOT EXISTS (
6346                         SELECT 1
6347                         FROM permission.grp_perm_map AS map
6348                         WHERE
6349                                 map.grp = pgt.id
6350                                 AND map.perm = perm.id
6351                 );
6352
6353 INSERT INTO config.upgrade_log (version) VALUES ('0547'); -- dbwells
6354
6355 -- account for spelling errors (Admin != Administrator)
6356 \qecho This might not insert much if you passed through 0542 on your way here,
6357 \qecho but one group was missed there as well
6358
6359 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6360         SELECT
6361                 pgt.id, perm.id, aout.depth, TRUE
6362         FROM
6363                 permission.grp_tree pgt,
6364                 permission.perm_list perm,
6365                 actor.org_unit_type aout
6366         WHERE
6367                 pgt.name = 'Cataloging Administrator' AND
6368                 aout.name = 'Consortium' AND
6369                 perm.code IN (
6370                         'ADMIN_IMPORT_ITEM_ATTR_DEF',
6371                         'ADMIN_MERGE_PROFILE',
6372                         'CREATE_AUTHORITY_IMPORT_IMPORT_DEF',
6373                         'CREATE_BIB_IMPORT_FIELD_DEF',
6374                         'CREATE_BIB_PTYPE',
6375                         'CREATE_BIB_SOURCE',
6376                         'CREATE_IMPORT_ITEM_ATTR_DEF',
6377                         'CREATE_IMPORT_TRASH_FIELD',
6378                         'CREATE_MERGE_PROFILE',
6379                         'CREATE_MONOGRAPH_PART',
6380                         'CREATE_VOLUME_PREFIX',
6381                         'CREATE_VOLUME_SUFFIX',
6382                         'DELETE_AUTHORITY_IMPORT_IMPORT_FIELD_DEF',
6383                         'DELETE_BIB_PTYPE',
6384                         'DELETE_BIB_SOURCE',
6385                         'DELETE_IMPORT_ITEM_ATTR_DEF',
6386                         'DELETE_IMPORT_TRASH_FIELD',
6387                         'DELETE_MERGE_PROFILE',
6388                         'DELETE_MONOGRAPH_PART',
6389                         'DELETE_VOLUME_PREFIX',
6390                         'DELETE_VOLUME_SUFFIX',
6391                         'MAP_MONOGRAPH_PART',
6392                         'UPDATE_AUTHORITY_IMPORT_IMPORT_FIELD_DEF',
6393                         'UPDATE_BIB_IMPORT_IMPORT_FIELD_DEF',
6394                         'UPDATE_BIB_PTYPE',
6395                         'UPDATE_IMPORT_ITEM_ATTR_DEF',
6396                         'UPDATE_IMPORT_TRASH_FIELD',
6397                         'UPDATE_MERGE_PROFILE',
6398                         'UPDATE_MONOGRAPH_PART',
6399                         'UPDATE_VOLUME_PREFIX',
6400                         'UPDATE_VOLUME_SUFFIX'
6401                 ) AND NOT EXISTS (
6402                         SELECT 1
6403                         FROM permission.grp_perm_map AS map
6404                         WHERE
6405                                 map.grp = pgt.id
6406                                 AND map.perm = perm.id
6407                 );
6408
6409 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6410     SELECT
6411         pgt.id, perm.id, aout.depth, TRUE
6412     FROM
6413         permission.grp_tree pgt,
6414         permission.perm_list perm,
6415         actor.org_unit_type aout
6416     WHERE
6417         pgt.name = 'Cataloging Administrator' AND
6418         aout.name = 'System' AND
6419         perm.code IN (
6420             'CREATE_COPY_STAT_CAT',
6421             'CREATE_COPY_STAT_CAT_ENTRY',
6422             'CREATE_COPY_STAT_CAT_ENTRY_MAP',
6423             'RUN_REPORTS',
6424             'SHARE_REPORT_FOLDER',
6425             'UPDATE_COPY_LOCATION',
6426             'UPDATE_COPY_STAT_CAT',
6427             'UPDATE_COPY_STAT_CAT_ENTRY',
6428             'VIEW_REPORT_OUTPUT'
6429         ) AND NOT EXISTS (
6430             SELECT 1
6431             FROM permission.grp_perm_map AS map
6432             WHERE
6433                 map.grp = pgt.id
6434                 AND map.perm = perm.id
6435         );
6436
6437 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6438         SELECT
6439                 pgt.id, perm.id, aout.depth, TRUE
6440         FROM
6441                 permission.grp_tree pgt,
6442                 permission.perm_list perm,
6443                 actor.org_unit_type aout
6444         WHERE
6445                 pgt.name = 'Circulation Administrator' AND
6446                 aout.name = 'Branch' AND
6447                 perm.code IN (
6448                         'DELETE_USER'
6449                 ) AND NOT EXISTS (
6450                         SELECT 1
6451                         FROM permission.grp_perm_map AS map
6452                         WHERE
6453                                 map.grp = pgt.id
6454                                 AND map.perm = perm.id
6455                 );
6456
6457 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6458         SELECT
6459                 pgt.id, perm.id, aout.depth, TRUE
6460         FROM
6461                 permission.grp_tree pgt,
6462                 permission.perm_list perm,
6463                 actor.org_unit_type aout
6464         WHERE
6465                 pgt.name = 'Circulation Administrator' AND
6466                 aout.name = 'Consortium' AND
6467                 perm.code IN (
6468                         'ADMIN_MAX_FINE_RULE',
6469                         'CREATE_CIRC_DURATION',
6470                         'DELETE_CIRC_DURATION',
6471                         'MARK_ITEM_MISSING_PIECES',
6472                         'UPDATE_CIRC_DURATION',
6473                         'UPDATE_HOLD_REQUEST_TIME',
6474                         'UPDATE_NET_ACCESS_LEVEL',
6475                         'VIEW_CIRC_MATRIX_MATCHPOINT',
6476                         'VIEW_HOLD_MATRIX_MATCHPOINT'
6477                 ) AND NOT EXISTS (
6478                         SELECT 1
6479                         FROM permission.grp_perm_map AS map
6480                         WHERE
6481                                 map.grp = pgt.id
6482                                 AND map.perm = perm.id
6483                 );
6484
6485 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
6486         SELECT
6487                 pgt.id, perm.id, aout.depth, TRUE
6488         FROM
6489                 permission.grp_tree pgt,
6490                 permission.perm_list perm,
6491                 actor.org_unit_type aout
6492         WHERE
6493                 pgt.name = 'Circulation Administrator' AND
6494                 aout.name = 'System' AND
6495                 perm.code IN (
6496                         'ADMIN_BOOKING_RESERVATION',
6497                         'ADMIN_BOOKING_RESERVATION_ATTR_MAP',
6498                         'ADMIN_BOOKING_RESERVATION_ATTR_VALUE_MAP',
6499                         'ADMIN_BOOKING_RESOURCE',
6500                         'ADMIN_BOOKING_RESOURCE_ATTR',
6501                         'ADMIN_BOOKING_RESOURCE_ATTR_MAP',
6502                         'ADMIN_BOOKING_RESOURCE_ATTR_VALUE',
6503                         'ADMIN_BOOKING_RESOURCE_TYPE',
6504                         'ADMIN_COPY_LOCATION_ORDER',
6505                         'ADMIN_HOLD_CANCEL_CAUSE',
6506                         'ASSIGN_GROUP_PERM',
6507                         'BAR_PATRON',
6508                         'COPY_HOLDS',
6509                         'COPY_TRANSIT_RECEIVE',
6510                         'CREATE_BILL',
6511                         'CREATE_BILLING_TYPE',
6512                         'CREATE_NON_CAT_TYPE',
6513                         'CREATE_PATRON_STAT_CAT',
6514                         'CREATE_PATRON_STAT_CAT_ENTRY',
6515                         'CREATE_PATRON_STAT_CAT_ENTRY_MAP',
6516                         'CREATE_USER_GROUP_LINK',
6517                         'DELETE_BILLING_TYPE',
6518                         'DELETE_NON_CAT_TYPE',
6519                         'DELETE_PATRON_STAT_CAT',
6520                         'DELETE_PATRON_STAT_CAT_ENTRY',
6521                         'DELETE_PATRON_STAT_CAT_ENTRY_MAP',
6522                         'DELETE_TRANSIT',
6523                         'group_application.user.staff',
6524                         'MANAGE_BAD_DEBT',
6525                         'MARK_ITEM_AVAILABLE',
6526                         'MARK_ITEM_BINDERY',
6527                         'MARK_ITEM_CHECKED_OUT',
6528                         'MARK_ITEM_ILL',
6529                         'MARK_ITEM_IN_PROCESS',
6530                         'MARK_ITEM_IN_TRANSIT',
6531                         'MARK_ITEM_LOST',
6532                         'MARK_ITEM_MISSING',
6533                         'MARK_ITEM_ON_HOLDS_SHELF',
6534                         'MARK_ITEM_ON_ORDER',
6535                         'MARK_ITEM_RESHELVING',
6536                         'MERGE_USERS',
6537                         'money.collections_tracker.create',
6538                         'money.collections_tracker.delete',
6539                         'OFFLINE_EXECUTE',
6540                         'OFFLINE_UPLOAD',
6541                         'OFFLINE_VIEW',
6542                         'REMOVE_USER_GROUP_LINK',
6543                         'SET_CIRC_CLAIMS_RETURNED',
6544                         'SET_CIRC_CLAIMS_RETURNED.override',
6545                         'SET_CIRC_LOST',
6546                         'SET_CIRC_MISSING',
6547                         'UNBAR_PATRON',
6548                         'UPDATE_BILL_NOTE',
6549                         'UPDATE_NON_CAT_TYPE',
6550                         'UPDATE_PATRON_CLAIM_NEVER_CHECKED_OUT_COUNT',
6551                         'UPDATE_PATRON_CLAIM_RETURN_COUNT',
6552                         'UPDATE_PICKUP_LIB_FROM_HOLDS_SHELF',
6553                         'UPDATE_PICKUP_LIB_FROM_TRANSIT',
6554                         'UPDATE_USER',
6555                         'VIEW_REPORT_OUTPUT',
6556                         'VIEW_STANDING_PENALTY',
6557                         'VOID_BILLING',
6558                         'VOLUME_HOLDS'
6559                 ) AND NOT EXISTS (
6560                         SELECT 1
6561                         FROM permission.grp_perm_map AS map
6562                         WHERE
6563                                 map.grp = pgt.id
6564                                 AND map.perm = perm.id
6565                 );
6566
6567 INSERT INTO config.upgrade_log (version) VALUES ('0557'); -- miker
6568
6569 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$
6570     SELECT  XMLELEMENT(
6571                 name location,
6572                 XMLATTRIBUTES(
6573                     CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
6574                     id AS ident,
6575                     holdable,
6576                     opac_visible,
6577                     label_prefix AS prefix,
6578                     label_suffix AS suffix
6579                 ),
6580                 name
6581             )
6582       FROM  asset.copy_location
6583       WHERE id = $1;
6584 $F$ LANGUAGE SQL;
6585
6586 INSERT INTO config.upgrade_log (version) VALUES ('0558'); -- miker
6587
6588 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$
6589     SELECT  XMLELEMENT(
6590                 name status,
6591                 XMLATTRIBUTES(
6592                     CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
6593                     id AS ident,
6594                     holdable,
6595                     opac_visible
6596                 ),
6597                 name
6598             )
6599       FROM  config.copy_status
6600       WHERE id = $1;
6601 $F$ LANGUAGE SQL;
6602
6603 INSERT INTO config.upgrade_log (version) VALUES ('0560'); -- miker
6604
6605 CREATE OR REPLACE FUNCTION asset.cache_copy_visibility () RETURNS TRIGGER as $func$
6606 DECLARE
6607     add_query       TEXT;
6608     remove_query    TEXT;
6609     do_add          BOOLEAN := false;
6610     do_remove       BOOLEAN := false;
6611 BEGIN
6612     add_query := $$
6613             INSERT INTO asset.opac_visible_copies (copy_id, circ_lib, record)
6614               SELECT id, circ_lib, record FROM (
6615                 SELECT  cp.id, cp.circ_lib, cn.record, cn.id AS call_number, cp.location, cp.status
6616                   FROM  asset.copy cp
6617                         JOIN asset.call_number cn ON (cn.id = cp.call_number)
6618                         JOIN actor.org_unit a ON (cp.circ_lib = a.id)
6619                         JOIN asset.copy_location cl ON (cp.location = cl.id)
6620                         JOIN config.copy_status cs ON (cp.status = cs.id)
6621                         JOIN biblio.record_entry b ON (cn.record = b.id)
6622                   WHERE NOT cp.deleted
6623                         AND NOT cn.deleted
6624                         AND NOT b.deleted
6625                         AND cs.opac_visible
6626                         AND cl.opac_visible
6627                         AND cp.opac_visible
6628                         AND a.opac_visible
6629                             UNION
6630                 SELECT  cp.id, cp.circ_lib, pbcm.peer_record AS record, NULL AS call_number, cp.location, cp.status
6631                   FROM  asset.copy cp
6632                         JOIN biblio.peer_bib_copy_map pbcm ON (pbcm.target_copy = cp.id)
6633                         JOIN actor.org_unit a ON (cp.circ_lib = a.id)
6634                         JOIN asset.copy_location cl ON (cp.location = cl.id)
6635                         JOIN config.copy_status cs ON (cp.status = cs.id)
6636                   WHERE NOT cp.deleted
6637                         AND cs.opac_visible
6638                         AND cl.opac_visible
6639                         AND cp.opac_visible
6640                         AND a.opac_visible
6641                     ) AS x 
6642
6643     $$;
6644  
6645     remove_query := $$ DELETE FROM asset.opac_visible_copies WHERE copy_id IN ( SELECT id FROM asset.copy WHERE $$;
6646
6647     IF TG_TABLE_NAME = 'peer_bib_copy_map' THEN
6648         IF TG_OP = 'INSERT' THEN
6649             add_query := add_query || 'WHERE x.id = ' || NEW.target_copy || ' AND x.record = ' || NEW.peer_record || ';';
6650             EXECUTE add_query;
6651             RETURN NEW;
6652         ELSE
6653             remove_query := 'DELETE FROM asset.opac_visible_copies WHERE copy_id = ' || OLD.target_copy || ' AND record = ' || OLD.peer_record || ';';
6654             EXECUTE remove_query;
6655             RETURN OLD;
6656         END IF;
6657     END IF;
6658
6659     IF TG_OP = 'INSERT' THEN
6660
6661         IF TG_TABLE_NAME IN ('copy', 'unit') THEN
6662             add_query := add_query || 'WHERE x.id = ' || NEW.id || ';';
6663             EXECUTE add_query;
6664         END IF;
6665
6666         RETURN NEW;
6667
6668     END IF;
6669
6670     -- handle items first, since with circulation activity
6671     -- their statuses change frequently
6672     IF TG_TABLE_NAME IN ('copy', 'unit') THEN
6673
6674         IF OLD.location    <> NEW.location OR
6675            OLD.call_number <> NEW.call_number OR
6676            OLD.status      <> NEW.status OR
6677            OLD.circ_lib    <> NEW.circ_lib THEN
6678             -- any of these could change visibility, but
6679             -- we'll save some queries and not try to calculate
6680             -- the change directly
6681             do_remove := true;
6682             do_add := true;
6683         ELSE
6684
6685             IF OLD.deleted <> NEW.deleted THEN
6686                 IF NEW.deleted THEN
6687                     do_remove := true;
6688                 ELSE
6689                     do_add := true;
6690                 END IF;
6691             END IF;
6692
6693             IF OLD.opac_visible <> NEW.opac_visible THEN
6694                 IF OLD.opac_visible THEN
6695                     do_remove := true;
6696                 ELSIF NOT do_remove THEN -- handle edge case where deleted item
6697                                         -- is also marked opac_visible
6698                     do_add := true;
6699                 END IF;
6700             END IF;
6701
6702         END IF;
6703
6704         IF do_remove THEN
6705             DELETE FROM asset.opac_visible_copies WHERE copy_id = NEW.id;
6706         END IF;
6707         IF do_add THEN
6708             add_query := add_query || 'WHERE x.id = ' || NEW.id || ';';
6709             EXECUTE add_query;
6710         END IF;
6711
6712         RETURN NEW;
6713
6714     END IF;
6715
6716     IF TG_TABLE_NAME IN ('call_number', 'record_entry') THEN -- these have a 'deleted' column
6717  
6718         IF OLD.deleted AND NEW.deleted THEN -- do nothing
6719
6720             RETURN NEW;
6721  
6722         ELSIF NEW.deleted THEN -- remove rows
6723  
6724             IF TG_TABLE_NAME = 'call_number' THEN
6725                 DELETE FROM asset.opac_visible_copies WHERE copy_id IN (SELECT id FROM asset.copy WHERE call_number = NEW.id);
6726             ELSIF TG_TABLE_NAME = 'record_entry' THEN
6727                 DELETE FROM asset.opac_visible_copies WHERE record = NEW.id;
6728             END IF;
6729  
6730             RETURN NEW;
6731  
6732         ELSIF OLD.deleted THEN -- add rows
6733  
6734             IF TG_TABLE_NAME IN ('copy','unit') THEN
6735                 add_query := add_query || 'WHERE x.id = ' || NEW.id || ';';
6736             ELSIF TG_TABLE_NAME = 'call_number' THEN
6737                 add_query := add_query || 'WHERE x.call_number = ' || NEW.id || ';';
6738             ELSIF TG_TABLE_NAME = 'record_entry' THEN
6739                 add_query := add_query || 'WHERE x.record = ' || NEW.id || ';';
6740             END IF;
6741  
6742             EXECUTE add_query;
6743             RETURN NEW;
6744  
6745         END IF;
6746  
6747     END IF;
6748
6749     IF TG_TABLE_NAME = 'call_number' THEN
6750
6751         IF OLD.record <> NEW.record THEN
6752             -- call number is linked to different bib
6753             remove_query := remove_query || 'call_number = ' || NEW.id || ');';
6754             EXECUTE remove_query;
6755             add_query := add_query || 'WHERE x.call_number = ' || NEW.id || ';';
6756             EXECUTE add_query;
6757         END IF;
6758
6759         RETURN NEW;
6760
6761     END IF;
6762
6763     IF TG_TABLE_NAME IN ('record_entry') THEN
6764         RETURN NEW; -- don't have 'opac_visible'
6765     END IF;
6766
6767     -- actor.org_unit, asset.copy_location, asset.copy_status
6768     IF NEW.opac_visible = OLD.opac_visible THEN -- do nothing
6769
6770         RETURN NEW;
6771
6772     ELSIF NEW.opac_visible THEN -- add rows
6773
6774         IF TG_TABLE_NAME = 'org_unit' THEN
6775             add_query := add_query || 'WHERE x.circ_lib = ' || NEW.id || ';';
6776         ELSIF TG_TABLE_NAME = 'copy_location' THEN
6777             add_query := add_query || 'WHERE x.location = ' || NEW.id || ';';
6778         ELSIF TG_TABLE_NAME = 'copy_status' THEN
6779             add_query := add_query || 'WHERE x.status = ' || NEW.id || ';';
6780         END IF;
6781  
6782         EXECUTE add_query;
6783  
6784     ELSE -- delete rows
6785
6786         IF TG_TABLE_NAME = 'org_unit' THEN
6787             remove_query := 'DELETE FROM asset.opac_visible_copies WHERE circ_lib = ' || NEW.id || ';';
6788         ELSIF TG_TABLE_NAME = 'copy_location' THEN
6789             remove_query := remove_query || 'location = ' || NEW.id || ');';
6790         ELSIF TG_TABLE_NAME = 'copy_status' THEN
6791             remove_query := remove_query || 'status = ' || NEW.id || ');';
6792         END IF;
6793  
6794         EXECUTE remove_query;
6795  
6796     END IF;
6797  
6798     RETURN NEW;
6799 END;
6800 $func$ LANGUAGE PLPGSQL;
6801
6802 INSERT INTO config.upgrade_log (version) VALUES ('0563');
6803
6804 INSERT INTO permission.perm_list ( id, code, description ) 
6805     VALUES ( 510, 'UPDATE_PATRON_COLLECTIONS_EXEMPT', oils_i18n_gettext(510,
6806     'Allows a user to indicate that a patron is exempt from collections processing', 'ppl', 'description'));
6807
6808 --- stock Circulation Administrator group
6809
6810 INSERT INTO permission.grp_perm_map ( grp, perm, depth, grantable )
6811     SELECT
6812         4,
6813         id,
6814         0,
6815         't'
6816     FROM permission.perm_list
6817     WHERE code in ('UPDATE_PATRON_COLLECTIONS_EXEMPT');
6818
6819 INSERT INTO config.upgrade_log (version) VALUES ('0566');
6820
6821 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$
6822 DECLARE
6823     me      biblio.record_entry%ROWTYPE;
6824     layout  unapi.bre_output_layout%ROWTYPE;
6825     xfrm    config.xml_transform%ROWTYPE;
6826     ouid    INT;
6827     tmp_xml TEXT;
6828     top_el  TEXT;
6829     output  XML;
6830     hxml    XML;
6831     axml    XML;
6832 BEGIN
6833
6834     SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
6835
6836     IF ouid IS NULL THEN
6837         RETURN NULL::XML;
6838     END IF;
6839
6840     IF format = 'holdings_xml' THEN -- the special case
6841         output := unapi.holdings_xml( obj_id, ouid, org, depth, includes, slimit, soffset, include_xmlns);
6842         RETURN output;
6843     END IF;
6844
6845     SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
6846
6847     IF layout.name IS NULL THEN
6848         RETURN NULL::XML;
6849     END IF;
6850
6851     SELECT * INTO xfrm FROM config.xml_transform WHERE name = layout.transform;
6852
6853     SELECT * INTO me FROM biblio.record_entry WHERE id = obj_id;
6854
6855     -- grab SVF if we need them
6856     IF ('mra' = ANY (includes)) THEN 
6857         axml := unapi.mra(obj_id,NULL,NULL,NULL,NULL);
6858     ELSE
6859         axml := NULL::XML;
6860     END IF;
6861
6862     -- grab hodlings if we need them
6863     IF ('holdings_xml' = ANY (includes)) THEN 
6864         hxml := unapi.holdings_xml(obj_id, ouid, org, depth, evergreen.array_remove_item_by_value(includes,'holdings_xml'), slimit, soffset, include_xmlns);
6865     ELSE
6866         hxml := NULL::XML;
6867     END IF;
6868
6869
6870     -- generate our item node
6871
6872
6873     IF format = 'marcxml' THEN
6874         tmp_xml := me.marc;
6875         IF tmp_xml !~ E'<marc:' THEN -- If we're not using the prefixed namespace in this record, then remove all declarations of it
6876            tmp_xml := REGEXP_REPLACE(tmp_xml, ' xmlns:marc="http://www.loc.gov/MARC21/slim"', '', 'g');
6877         END IF; 
6878     ELSE
6879         tmp_xml := oils_xslt_process(me.marc, xfrm.xslt)::XML;
6880     END IF;
6881
6882     top_el := REGEXP_REPLACE(tmp_xml, E'^.*?<((?:\\S+:)?' || layout.holdings_element || ').*$', E'\\1');
6883
6884     IF axml IS NOT NULL THEN 
6885         tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', axml || '</' || top_el || E'>\\1');
6886     END IF;
6887
6888     IF hxml IS NOT NULL THEN -- XXX how do we configure the holdings position?
6889         tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', hxml || '</' || top_el || E'>\\1');
6890     END IF;
6891
6892     IF ('bre.unapi' = ANY (includes)) THEN 
6893         output := REGEXP_REPLACE(
6894             tmp_xml,
6895             '</' || top_el || '>(.*?)',
6896             XMLELEMENT(
6897                 name abbr,
6898                 XMLATTRIBUTES(
6899                     'http://www.w3.org/1999/xhtml' AS xmlns,
6900                     'unapi-id' AS class,
6901                     'tag:open-ils.org:U2@bre/' || obj_id || '/' || org AS title
6902                 )
6903             )::TEXT || '</' || top_el || E'>\\1'
6904         );
6905     ELSE
6906         output := tmp_xml;
6907     END IF;
6908
6909     output := REGEXP_REPLACE(output::TEXT,E'>\\s+<','><','gs')::XML;
6910     RETURN output;
6911 END;
6912 $F$ LANGUAGE PLPGSQL;
6913
6914 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$
6915      SELECT  XMLELEMENT(
6916                  name holdings,
6917                  XMLATTRIBUTES(
6918                     CASE WHEN $8 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
6919                     CASE WHEN ('bre' = ANY ($5)) THEN 'tag:open-ils.org:U2@bre/' || $1 || '/' || $3 ELSE NULL END AS id
6920                  ),
6921                  XMLELEMENT(
6922                      name counts,
6923                      (SELECT  XMLAGG(XMLELEMENT::XML) FROM (
6924                          SELECT  XMLELEMENT(
6925                                      name count,
6926                                      XMLATTRIBUTES('public' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
6927                                  )::text
6928                            FROM  asset.opac_ou_record_copy_count($2,  $1)
6929                                      UNION
6930                          SELECT  XMLELEMENT(
6931                                      name count,
6932                                      XMLATTRIBUTES('staff' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
6933                                  )::text
6934                            FROM  asset.staff_ou_record_copy_count($2, $1)
6935                                      ORDER BY 1
6936                      )x)
6937                  ),
6938                  CASE 
6939                      WHEN ('bmp' = ANY ($5)) THEN
6940                         XMLELEMENT(
6941                             name monograph_parts,
6942                             (SELECT XMLAGG(bmp) FROM (
6943                                 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)
6944                                   FROM  biblio.monograph_part
6945                                   WHERE record = $1
6946                             )x)
6947                         )
6948                      ELSE NULL
6949                  END,
6950                  XMLELEMENT(
6951                      name volumes,
6952                      (SELECT XMLAGG(acn) FROM (
6953                         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)
6954                           FROM  asset.call_number acn
6955                           WHERE acn.record = $1
6956                                 AND EXISTS (
6957                                     SELECT  1
6958                                       FROM  asset.copy acp
6959                                             JOIN actor.org_unit_descendants(
6960                                                 $2,
6961                                                 (COALESCE(
6962                                                     $4,
6963                                                     (SELECT aout.depth
6964                                                       FROM  actor.org_unit_type aout
6965                                                             JOIN actor.org_unit aou ON (aou.ou_type = aout.id AND aou.id = $2)
6966                                                     )
6967                                                 ))
6968                                             ) aoud ON (acp.circ_lib = aoud.id)
6969                                       LIMIT 1
6970                                )
6971                           ORDER BY label_sortkey
6972                           LIMIT $6
6973                           OFFSET $7
6974                      )x)
6975                  ),
6976                  CASE WHEN ('ssub' = ANY ($5)) THEN 
6977                      XMLELEMENT(
6978                          name subscriptions,
6979                          (SELECT XMLAGG(ssub) FROM (
6980                             SELECT  unapi.ssub(id,'xml','subscription','{}'::TEXT[], $3, $4, $6, $7, FALSE)
6981                               FROM  serial.subscription
6982                               WHERE record_entry = $1
6983                         )x)
6984                      )
6985                  ELSE NULL END,
6986                  CASE WHEN ('acp' = ANY ($5)) THEN 
6987                      XMLELEMENT(
6988                          name foreign_copies,
6989                          (SELECT XMLAGG(acp) FROM (
6990                             SELECT  unapi.acp(p.target_copy,'xml','copy','{}'::TEXT[], $3, $4, $6, $7, FALSE)
6991                               FROM  biblio.peer_bib_copy_map p
6992                                     JOIN asset.copy c ON (p.target_copy = c.id)
6993                               WHERE NOT c.deleted AND peer_record = $1
6994                         )x)
6995                      )
6996                  ELSE NULL END
6997              );
6998 $F$ LANGUAGE SQL;
6999
7000 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$
7001         SELECT  XMLELEMENT(
7002                     name subscription,
7003                     XMLATTRIBUTES(
7004                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
7005                         'tag:open-ils.org:U2@ssub/' || id AS id,
7006                         start_date AS start, end_date AS end, expected_date_offset
7007                     ),
7008                     unapi.aou( owning_lib, $2, 'owning_lib', evergreen.array_remove_item_by_value($4,'ssub'), $5, $6, $7, $8),
7009                     XMLELEMENT( name distributions,
7010                         CASE 
7011                             WHEN ('sdist' = ANY ($4)) THEN
7012                                 (SELECT XMLAGG(sdist) FROM (
7013                                     SELECT  unapi.sdist( id, 'xml', 'distribution', evergreen.array_remove_item_by_value($4,'ssub'), $5, $6, $7, $8, FALSE)
7014                                       FROM  serial.distribution
7015                                       WHERE subscription = ssub.id
7016                                 )x)
7017                             ELSE NULL
7018                         END
7019                     )
7020                 )
7021           FROM  serial.subscription ssub
7022           WHERE id = $1
7023           GROUP BY id, start_date, end_date, expected_date_offset, owning_lib;
7024 $F$ LANGUAGE SQL;
7025
7026 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$
7027         SELECT  XMLELEMENT(
7028                     name distribution,
7029                     XMLATTRIBUTES(
7030                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
7031                         'tag:open-ils.org:U2@sdist/' || id AS id,
7032                         'tag:open-ils.org:U2@acn/' || receive_call_number AS receive_call_number,
7033                         'tag:open-ils.org:U2@acn/' || bind_call_number AS bind_call_number,
7034                         unit_label_prefix, label, unit_label_suffix, summary_method
7035                     ),
7036                     unapi.aou( holding_lib, $2, 'holding_lib', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8),
7037                     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,
7038                     XMLELEMENT( name streams,
7039                         CASE 
7040                             WHEN ('sstr' = ANY ($4)) THEN
7041                                 (SELECT XMLAGG(sstr) FROM (
7042                                     SELECT  unapi.sstr( id, 'xml', 'stream', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE)
7043                                       FROM  serial.stream
7044                                       WHERE distribution = sdist.id
7045                                 )x)
7046                             ELSE NULL
7047                         END
7048                     ),
7049                     XMLELEMENT( name summaries,
7050                         CASE 
7051                             WHEN ('ssum' = ANY ($4)) THEN
7052                                 (SELECT XMLAGG(sbsum) FROM (
7053                                     SELECT  unapi.sbsum( id, 'xml', 'serial_summary', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE)
7054                                       FROM  serial.basic_summary
7055                                       WHERE distribution = sdist.id
7056                                 )x)
7057                             ELSE NULL
7058                         END,
7059                         CASE 
7060                             WHEN ('ssum' = ANY ($4)) THEN
7061                                 (SELECT XMLAGG(sisum) FROM (
7062                                     SELECT  unapi.sisum( id, 'xml', 'serial_summary', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE)
7063                                       FROM  serial.index_summary
7064                                       WHERE distribution = sdist.id
7065                                 )x)
7066                             ELSE NULL
7067                         END,
7068                         CASE 
7069                             WHEN ('ssum' = ANY ($4)) THEN
7070                                 (SELECT XMLAGG(sssum) FROM (
7071                                     SELECT  unapi.sssum( id, 'xml', 'serial_summary', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE)
7072                                       FROM  serial.supplement_summary
7073                                       WHERE distribution = sdist.id
7074                                 )x)
7075                             ELSE NULL
7076                         END
7077                     )
7078                 )
7079           FROM  serial.distribution sdist
7080           WHERE id = $1
7081           GROUP BY id, label, unit_label_prefix, unit_label_suffix, holding_lib, summary_method, subscription, receive_call_number, bind_call_number;
7082 $F$ LANGUAGE SQL;
7083
7084 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$
7085     SELECT  XMLELEMENT(
7086                 name stream,
7087                 XMLATTRIBUTES(
7088                     CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
7089                     'tag:open-ils.org:U2@sstr/' || id AS id,
7090                     routing_label
7091                 ),
7092                 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,
7093                 XMLELEMENT( name items,
7094                     CASE 
7095                         WHEN ('sitem' = ANY ($4)) THEN
7096                             (SELECT XMLAGG(sitem) FROM (
7097                                 SELECT  unapi.sitem( id, 'xml', 'serial_item', evergreen.array_remove_item_by_value($4,'sstr'), $5, $6, $7, $8, FALSE)
7098                                   FROM  serial.item
7099                                   WHERE stream = sstr.id
7100                             )x)
7101                         ELSE NULL
7102                     END
7103                 )
7104             )
7105       FROM  serial.stream sstr
7106       WHERE id = $1
7107       GROUP BY id, routing_label, distribution;
7108 $F$ LANGUAGE SQL;
7109
7110 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$
7111     SELECT  XMLELEMENT(
7112                 name issuance,
7113                 XMLATTRIBUTES(
7114                     CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
7115                     'tag:open-ils.org:U2@siss/' || id AS id,
7116                     create_date, edit_date, label, date_published,
7117                     holding_code, holding_type, holding_link_id
7118                 ),
7119                 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,
7120                 XMLELEMENT( name items,
7121                     CASE 
7122                         WHEN ('sitem' = ANY ($4)) THEN
7123                             (SELECT XMLAGG(sitem) FROM (
7124                                 SELECT  unapi.sitem( id, 'xml', 'serial_item', evergreen.array_remove_item_by_value($4,'siss'), $5, $6, $7, $8, FALSE)
7125                                   FROM  serial.item
7126                                   WHERE issuance = sstr.id
7127                             )x)
7128                         ELSE NULL
7129                     END
7130                 )
7131             )
7132       FROM  serial.issuance sstr
7133       WHERE id = $1
7134       GROUP BY id, create_date, edit_date, label, date_published, holding_code, holding_type, holding_link_id, subscription;
7135 $F$ LANGUAGE SQL;
7136
7137 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$
7138         SELECT  XMLELEMENT(
7139                     name serial_item,
7140                     XMLATTRIBUTES(
7141                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
7142                         'tag:open-ils.org:U2@sitem/' || id AS id,
7143                         'tag:open-ils.org:U2@siss/' || issuance AS issuance,
7144                         date_expected, date_received
7145                     ),
7146                     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,
7147                     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,
7148                     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,
7149                     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
7150 --                    XMLELEMENT( name notes,
7151 --                        CASE 
7152 --                            WHEN ('acpn' = ANY ($4)) THEN
7153 --                                (SELECT XMLAGG(acpn) FROM (
7154 --                                    SELECT  unapi.acpn( id, 'xml', 'copy_note', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8)
7155 --                                      FROM  asset.copy_note
7156 --                                      WHERE owning_copy = cp.id AND pub
7157 --                                )x)
7158 --                            ELSE NULL
7159 --                        END
7160 --                    )
7161                 )
7162           FROM  serial.item sitem
7163           WHERE id = $1;
7164 $F$ LANGUAGE SQL;
7165
7166
7167 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$
7168         SELECT  XMLELEMENT(
7169                     name monograph_part,
7170                     XMLATTRIBUTES(
7171                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
7172                         'tag:open-ils.org:U2@bmp/' || id AS id,
7173                         id AS ident,
7174                         label,
7175                         label_sortkey,
7176                         'tag:open-ils.org:U2@bre/' || record AS record
7177                     ),
7178                     CASE 
7179                         WHEN ('acp' = ANY ($4)) THEN
7180                             XMLELEMENT( name copies,
7181                                 (SELECT XMLAGG(acp) FROM (
7182                                     SELECT  unapi.acp( cp.id, 'xml', 'copy', evergreen.array_remove_item_by_value($4,'bmp'), $5, $6, $7, $8, FALSE)
7183                                       FROM  asset.copy cp
7184                                             JOIN asset.copy_part_map cpm ON (cpm.target_copy = cp.id)
7185                                       WHERE cpm.part = $1
7186                                       ORDER BY COALESCE(cp.copy_number,0), cp.barcode
7187                                       LIMIT $7
7188                                       OFFSET $8
7189                                 )x)
7190                             )
7191                         ELSE NULL
7192                     END,
7193                     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
7194                 )
7195           FROM  biblio.monograph_part
7196           WHERE id = $1
7197           GROUP BY id, label, label_sortkey, record;
7198 $F$ LANGUAGE SQL;
7199
7200 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$
7201         SELECT  XMLELEMENT(
7202                     name copy,
7203                     XMLATTRIBUTES(
7204                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
7205                         'tag:open-ils.org:U2@acp/' || id AS id,
7206                         create_date, edit_date, copy_number, circulate, deposit,
7207                         ref, holdable, deleted, deposit_amount, price, barcode,
7208                         circ_modifier, circ_as_type, opac_visible
7209                     ),
7210                     unapi.ccs( status, $2, 'status', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE),
7211                     unapi.acl( location, $2, 'location', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE),
7212                     unapi.aou( circ_lib, $2, 'circ_lib', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8),
7213                     unapi.aou( circ_lib, $2, 'circlib', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8),
7214                     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,
7215                     XMLELEMENT( name copy_notes,
7216                         CASE 
7217                             WHEN ('acpn' = ANY ($4)) THEN
7218                                 (SELECT XMLAGG(acpn) FROM (
7219                                     SELECT  unapi.acpn( id, 'xml', 'copy_note', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE)
7220                                       FROM  asset.copy_note
7221                                       WHERE owning_copy = cp.id AND pub
7222                                 )x)
7223                             ELSE NULL
7224                         END
7225                     ),
7226                     XMLELEMENT( name statcats,
7227                         CASE 
7228                             WHEN ('ascecm' = ANY ($4)) THEN
7229                                 (SELECT XMLAGG(ascecm) FROM (
7230                                     SELECT  unapi.ascecm( stat_cat_entry, 'xml', 'statcat', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE)
7231                                       FROM  asset.stat_cat_entry_copy_map
7232                                       WHERE owning_copy = cp.id
7233                                 )x)
7234                             ELSE NULL
7235                         END
7236                     ),
7237                     XMLELEMENT( name foreign_records,
7238                         CASE
7239                             WHEN ('bre' = ANY ($4)) THEN
7240                                 (SELECT XMLAGG(bre) FROM (
7241                                     SELECT  unapi.bre(peer_record,'marcxml','record','{}'::TEXT[], $5, $6, $7, $8, FALSE)
7242                                       FROM  biblio.peer_bib_copy_map
7243                                       WHERE target_copy = cp.id
7244                                 )x)
7245                             ELSE NULL
7246                         END
7247
7248                     ),
7249                     CASE 
7250                         WHEN ('bmp' = ANY ($4)) THEN
7251                             XMLELEMENT( name monograph_parts,
7252                                 (SELECT XMLAGG(bmp) FROM (
7253                                     SELECT  unapi.bmp( part, 'xml', 'monograph_part', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE)
7254                                       FROM  asset.copy_part_map
7255                                       WHERE target_copy = cp.id
7256                                 )x)
7257                             )
7258                         ELSE NULL
7259                     END
7260                 )
7261           FROM  asset.copy cp
7262           WHERE id = $1
7263           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;
7264 $F$ LANGUAGE SQL;
7265
7266 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$
7267         SELECT  XMLELEMENT(
7268                     name serial_unit,
7269                     XMLATTRIBUTES(
7270                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
7271                         'tag:open-ils.org:U2@acp/' || id AS id,
7272                         create_date, edit_date, copy_number, circulate, deposit,
7273                         ref, holdable, deleted, deposit_amount, price, barcode,
7274                         circ_modifier, circ_as_type, opac_visible, status_changed_time,
7275                         floating, mint_condition, detailed_contents, sort_key, summary_contents, cost 
7276                     ),
7277                     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),
7278                     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),
7279                     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),
7280                     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),
7281                     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,
7282                     XMLELEMENT( name copy_notes,
7283                         CASE 
7284                             WHEN ('acpn' = ANY ($4)) THEN
7285                                 (SELECT XMLAGG(acpn) FROM (
7286                                     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)
7287                                       FROM  asset.copy_note
7288                                       WHERE owning_copy = cp.id AND pub
7289                                 )x)
7290                             ELSE NULL
7291                         END
7292                     ),
7293                     XMLELEMENT( name statcats,
7294                         CASE 
7295                             WHEN ('ascecm' = ANY ($4)) THEN
7296                                 (SELECT XMLAGG(ascecm) FROM (
7297                                     SELECT  unapi.ascecm( stat_cat_entry, 'xml', 'statcat', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE)
7298                                       FROM  asset.stat_cat_entry_copy_map
7299                                       WHERE owning_copy = cp.id
7300                                 )x)
7301                             ELSE NULL
7302                         END
7303                     ),
7304                     XMLELEMENT( name foreign_records,
7305                         CASE
7306                             WHEN ('bre' = ANY ($4)) THEN
7307                                 (SELECT XMLAGG(bre) FROM (
7308                                     SELECT  unapi.bre(peer_record,'marcxml','record','{}'::TEXT[], $5, $6, $7, $8, FALSE)
7309                                       FROM  biblio.peer_bib_copy_map
7310                                       WHERE target_copy = cp.id
7311                                 )x)
7312                             ELSE NULL
7313                         END
7314
7315                     ),
7316                     CASE 
7317                         WHEN ('bmp' = ANY ($4)) THEN
7318                             XMLELEMENT( name monograph_parts,
7319                                 (SELECT XMLAGG(bmp) FROM (
7320                                     SELECT  unapi.bmp( part, 'xml', 'monograph_part', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE)
7321                                       FROM  asset.copy_part_map
7322                                       WHERE target_copy = cp.id
7323                                 )x)
7324                             )
7325                         ELSE NULL
7326                     END
7327                 )
7328           FROM  serial.unit cp
7329           WHERE id = $1
7330           GROUP BY  id, status, location, circ_lib, call_number, create_date, edit_date, copy_number, circulate, floating, mint_condition,
7331                     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;
7332 $F$ LANGUAGE SQL;
7333
7334 INSERT INTO config.upgrade_log (version) VALUES ('0568'); -- miker for tsbere
7335
7336 CREATE OR REPLACE FUNCTION asset.cache_copy_visibility () RETURNS TRIGGER as $func$
7337 DECLARE
7338     add_front       TEXT;
7339     add_back        TEXT;
7340     add_base_query  TEXT;
7341     add_peer_query  TEXT;
7342     remove_query    TEXT;
7343     do_add          BOOLEAN := false;
7344     do_remove       BOOLEAN := false;
7345 BEGIN
7346     add_base_query := $$
7347         SELECT  cp.id, cp.circ_lib, cn.record, cn.id AS call_number, cp.location, cp.status
7348           FROM  asset.copy cp
7349                 JOIN asset.call_number cn ON (cn.id = cp.call_number)
7350                 JOIN actor.org_unit a ON (cp.circ_lib = a.id)
7351                 JOIN asset.copy_location cl ON (cp.location = cl.id)
7352                 JOIN config.copy_status cs ON (cp.status = cs.id)
7353                 JOIN biblio.record_entry b ON (cn.record = b.id)
7354           WHERE NOT cp.deleted
7355                 AND NOT cn.deleted
7356                 AND NOT b.deleted
7357                 AND cs.opac_visible
7358                 AND cl.opac_visible
7359                 AND cp.opac_visible
7360                 AND a.opac_visible
7361     $$;
7362     add_peer_query := $$
7363         SELECT  cp.id, cp.circ_lib, pbcm.peer_record AS record, NULL AS call_number, cp.location, cp.status
7364           FROM  asset.copy cp
7365                 JOIN biblio.peer_bib_copy_map pbcm ON (pbcm.target_copy = cp.id)
7366                 JOIN actor.org_unit a ON (cp.circ_lib = a.id)
7367                 JOIN asset.copy_location cl ON (cp.location = cl.id)
7368                 JOIN config.copy_status cs ON (cp.status = cs.id)
7369           WHERE NOT cp.deleted
7370                 AND cs.opac_visible
7371                 AND cl.opac_visible
7372                 AND cp.opac_visible
7373                 AND a.opac_visible
7374     $$;
7375     add_front := $$
7376         INSERT INTO asset.opac_visible_copies (copy_id, circ_lib, record)
7377           SELECT id, circ_lib, record FROM (
7378     $$;
7379     add_back := $$
7380         ) AS x
7381     $$;
7382  
7383     remove_query := $$ DELETE FROM asset.opac_visible_copies WHERE copy_id IN ( SELECT id FROM asset.copy WHERE $$;
7384
7385     IF TG_TABLE_NAME = 'peer_bib_copy_map' THEN
7386         IF TG_OP = 'INSERT' THEN
7387             add_peer_query := add_peer_query || ' AND cp.id = ' || NEW.target_copy || ' AND pbcm.record = ' || NEW.peer_record;
7388             EXECUTE add_front || add_peer_query || add_back;
7389             RETURN NEW;
7390         ELSE
7391             remove_query := 'DELETE FROM asset.opac_visible_copies WHERE copy_id = ' || OLD.target_copy || ' AND record = ' || OLD.peer_record || ';';
7392             EXECUTE remove_query;
7393             RETURN OLD;
7394         END IF;
7395     END IF;
7396
7397     IF TG_OP = 'INSERT' THEN
7398
7399         IF TG_TABLE_NAME IN ('copy', 'unit') THEN
7400             add_base_query := add_base_query || ' AND cp.id = ' || NEW.id;
7401             EXECUTE add_front || add_base_query || add_back;
7402         END IF;
7403
7404         RETURN NEW;
7405
7406     END IF;
7407
7408     -- handle items first, since with circulation activity
7409     -- their statuses change frequently
7410     IF TG_TABLE_NAME IN ('copy', 'unit') THEN
7411
7412         IF OLD.location    <> NEW.location OR
7413            OLD.call_number <> NEW.call_number OR
7414            OLD.status      <> NEW.status OR
7415            OLD.circ_lib    <> NEW.circ_lib THEN
7416             -- any of these could change visibility, but
7417             -- we'll save some queries and not try to calculate
7418             -- the change directly
7419             do_remove := true;
7420             do_add := true;
7421         ELSE
7422
7423             IF OLD.deleted <> NEW.deleted THEN
7424                 IF NEW.deleted THEN
7425                     do_remove := true;
7426                 ELSE
7427                     do_add := true;
7428                 END IF;
7429             END IF;
7430
7431             IF OLD.opac_visible <> NEW.opac_visible THEN
7432                 IF OLD.opac_visible THEN
7433                     do_remove := true;
7434                 ELSIF NOT do_remove THEN -- handle edge case where deleted item
7435                                         -- is also marked opac_visible
7436                     do_add := true;
7437                 END IF;
7438             END IF;
7439
7440         END IF;
7441
7442         IF do_remove THEN
7443             DELETE FROM asset.opac_visible_copies WHERE copy_id = NEW.id;
7444         END IF;
7445         IF do_add THEN
7446             add_base_query := add_base_query || ' AND cp.id = ' || NEW.id;
7447             add_peer_query := add_peer_query || ' AND cp.id = ' || NEW.id;
7448             EXECUTE add_front || add_base_query || ' UNION ' || add_peer_query || add_back;
7449         END IF;
7450
7451         RETURN NEW;
7452
7453     END IF;
7454
7455     IF TG_TABLE_NAME IN ('call_number', 'record_entry') THEN -- these have a 'deleted' column
7456  
7457         IF OLD.deleted AND NEW.deleted THEN -- do nothing
7458
7459             RETURN NEW;
7460  
7461         ELSIF NEW.deleted THEN -- remove rows
7462  
7463             IF TG_TABLE_NAME = 'call_number' THEN
7464                 DELETE FROM asset.opac_visible_copies WHERE copy_id IN (SELECT id FROM asset.copy WHERE call_number = NEW.id);
7465             ELSIF TG_TABLE_NAME = 'record_entry' THEN
7466                 DELETE FROM asset.opac_visible_copies WHERE record = NEW.id;
7467             END IF;
7468  
7469             RETURN NEW;
7470  
7471         ELSIF OLD.deleted THEN -- add rows
7472  
7473             IF TG_TABLE_NAME = 'call_number' THEN
7474                 add_base_query := add_base_query || ' AND cn.id = ' || NEW.id;
7475                 EXECUTE add_front || add_base_query || add_back;
7476             ELSIF TG_TABLE_NAME = 'record_entry' THEN
7477                 add_base_query := add_base_query || ' AND cn.record = ' || NEW.id;
7478                 add_peer_query := add_peer_query || ' AND pbcm.record = ' || NEW.id;
7479                 EXECUTE add_front || add_base_query || ' UNION ' || add_peer_query || add_back;
7480             END IF;
7481  
7482             RETURN NEW;
7483  
7484         END IF;
7485  
7486     END IF;
7487
7488     IF TG_TABLE_NAME = 'call_number' THEN
7489
7490         IF OLD.record <> NEW.record THEN
7491             -- call number is linked to different bib
7492             remove_query := remove_query || 'call_number = ' || NEW.id || ');';
7493             EXECUTE remove_query;
7494             add_base_query := add_base_query || ' AND cn.id = ' || NEW.id;
7495             EXECUTE add_front || add_base_query || add_back;
7496         END IF;
7497
7498         RETURN NEW;
7499
7500     END IF;
7501
7502     IF TG_TABLE_NAME IN ('record_entry') THEN
7503         RETURN NEW; -- don't have 'opac_visible'
7504     END IF;
7505
7506     -- actor.org_unit, asset.copy_location, asset.copy_status
7507     IF NEW.opac_visible = OLD.opac_visible THEN -- do nothing
7508
7509         RETURN NEW;
7510
7511     ELSIF NEW.opac_visible THEN -- add rows
7512
7513         IF TG_TABLE_NAME = 'org_unit' THEN
7514             add_base_query := add_base_query || ' AND cp.circ_lib = ' || NEW.id || ';';
7515             add_peer_query := add_peer_query || ' AND cp.circ_lib = ' || NEW.id || ';';
7516         ELSIF TG_TABLE_NAME = 'copy_location' THEN
7517             add_base_query := add_base_query || ' AND cp.location = ' || NEW.id || ';';
7518             add_peer_query := add_peer_query || ' AND cp.location = ' || NEW.id || ';';
7519         ELSIF TG_TABLE_NAME = 'copy_status' THEN
7520             add_base_query := add_base_query || ' AND cp.status = ' || NEW.id || ';';
7521             add_peer_query := add_peer_query || ' AND cp.status = ' || NEW.id || ';';
7522         END IF;
7523  
7524         EXECUTE add_front || add_base_query || ' UNION ' || add_peer_query || add_back;
7525  
7526     ELSE -- delete rows
7527
7528         IF TG_TABLE_NAME = 'org_unit' THEN
7529             remove_query := 'DELETE FROM asset.opac_visible_copies WHERE circ_lib = ' || NEW.id || ';';
7530         ELSIF TG_TABLE_NAME = 'copy_location' THEN
7531             remove_query := remove_query || 'location = ' || NEW.id || ');';
7532         ELSIF TG_TABLE_NAME = 'copy_status' THEN
7533             remove_query := remove_query || 'status = ' || NEW.id || ');';
7534         END IF;
7535  
7536         EXECUTE remove_query;
7537  
7538     END IF;
7539  
7540     RETURN NEW;
7541 END;
7542 $func$ LANGUAGE PLPGSQL;
7543
7544 INSERT INTO config.upgrade_log (version) VALUES ('0569'); --miker
7545
7546 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$
7547         SELECT  XMLELEMENT(
7548                     name uri,
7549                     XMLATTRIBUTES(
7550                         CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
7551                         'tag:open-ils.org:U2@auri/' || uri.id AS id,
7552                         use_restriction,
7553                         href,
7554                         label
7555                     ),
7556                     XMLELEMENT( name copies,
7557                         CASE
7558                             WHEN ('acn' = ANY ($4)) THEN
7559                                 (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)
7560                             ELSE NULL
7561                         END
7562                     )
7563                 ) AS x
7564           FROM  asset.uri uri
7565           WHERE uri.id = $1
7566           GROUP BY uri.id, use_restriction, href, label;
7567 $F$ LANGUAGE SQL;
7568
7569 INSERT INTO config.upgrade_log (version) VALUES ('0570');
7570
7571 -- Not everything in 1XX tags should become part of the authorsort field
7572 -- ($0 for example).  The list of subfields chosen here is a superset of all
7573 -- the fields found in the LoC authority mappin definitions for 1XX fields.
7574 -- Anyway, if more fields should be here, add them.
7575
7576 UPDATE config.record_attr_definition
7577     SET sf_list = 'abcdefgklmnopqrstvxyz'
7578     WHERE name='authorsort' AND sf_list IS NULL;
7579
7580 INSERT INTO config.upgrade_log (version) VALUES ('0571');
7581
7582 -- FIXME: add/check SQL statements to perform the upgrade
7583 CREATE OR REPLACE FUNCTION metabib.facet_normalize_trigger () RETURNS TRIGGER AS $$
7584 DECLARE
7585     normalizer  RECORD;
7586     facet_text  TEXT;
7587 BEGIN
7588     facet_text := NEW.value;
7589
7590     FOR normalizer IN
7591         SELECT  n.func AS func,
7592                 n.param_count AS param_count,
7593                 m.params AS params
7594           FROM  config.index_normalizer n
7595                 JOIN config.metabib_field_index_norm_map m ON (m.norm = n.id)
7596           WHERE m.field = NEW.field AND m.pos < 0
7597           ORDER BY m.pos LOOP
7598
7599             EXECUTE 'SELECT ' || normalizer.func || '(' ||
7600                 quote_literal( facet_text ) ||
7601                 CASE
7602                     WHEN normalizer.param_count > 0
7603                         THEN ',' || REPLACE(REPLACE(BTRIM(normalizer.params,'[]'),E'\'',E'\\\''),E'"',E'\'')
7604                         ELSE ''
7605                     END ||
7606                 ')' INTO facet_text;
7607
7608     END LOOP;
7609
7610     NEW.value = facet_text;
7611
7612     RETURN NEW;
7613 END;
7614 $$ LANGUAGE PLPGSQL;
7615
7616 CREATE TRIGGER facet_normalize_tgr
7617     BEFORE UPDATE OR INSERT ON metabib.facet_entry
7618     FOR EACH ROW EXECUTE PROCEDURE metabib.facet_normalize_trigger();
7619
7620
7621
7622 INSERT INTO config.upgrade_log (version) VALUES ('0578'); -- tsbere via miker
7623
7624 CREATE OR REPLACE VIEW reporter.hold_request_record AS
7625 SELECT  id,
7626         target,
7627         hold_type,
7628         CASE
7629                 WHEN hold_type = 'T'
7630                         THEN target
7631                 WHEN hold_type = 'I'
7632                         THEN (SELECT ssub.record_entry FROM serial.subscription ssub JOIN serial.issuance si ON (si.subscription = ssub.id) WHERE si.id = ahr.target)
7633                 WHEN hold_type = 'V'
7634                         THEN (SELECT cn.record FROM asset.call_number cn WHERE cn.id = ahr.target)
7635                 WHEN hold_type IN ('C','R','F')
7636                         THEN (SELECT cn.record FROM asset.call_number cn JOIN asset.copy cp ON (cn.id = cp.call_number) WHERE cp.id = ahr.target)
7637                 WHEN hold_type = 'M'
7638                         THEN (SELECT mr.master_record FROM metabib.metarecord mr WHERE mr.id = ahr.target)
7639         WHEN hold_type = 'P'
7640             THEN (SELECT bmp.record FROM biblio.monograph_part bmp WHERE bmp.id = ahr.target)
7641         END AS bib_record
7642   FROM  action.hold_request ahr;
7643
7644 INSERT INTO config.upgrade_log (version) VALUES ('0583');
7645
7646 CREATE OR REPLACE VIEW action.all_circulation AS
7647     SELECT  id,usr_post_code, usr_home_ou, usr_profile, usr_birth_year, copy_call_number, copy_location,
7648         copy_owning_lib, copy_circ_lib, copy_bib_record, xact_start, xact_finish, target_copy,
7649         circ_lib, circ_staff, checkin_staff, checkin_lib, renewal_remaining, grace_period, due_date,
7650         stop_fines_time, checkin_time, create_time, duration, fine_interval, recurring_fine,
7651         max_fine, phone_renewal, desk_renewal, opac_renewal, duration_rule, recurring_fine_rule,
7652         max_fine_rule, stop_fines, workstation, checkin_workstation, checkin_scan_time, parent_circ
7653       FROM  action.aged_circulation
7654             UNION ALL
7655     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,
7656         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,
7657         cn.record AS copy_bib_record, circ.xact_start, circ.xact_finish, circ.target_copy, circ.circ_lib, circ.circ_staff, circ.checkin_staff,
7658         circ.checkin_lib, circ.renewal_remaining, circ.grace_period, circ.due_date, circ.stop_fines_time, circ.checkin_time, circ.create_time, circ.duration,
7659         circ.fine_interval, circ.recurring_fine, circ.max_fine, circ.phone_renewal, circ.desk_renewal, circ.opac_renewal, circ.duration_rule,
7660         circ.recurring_fine_rule, circ.max_fine_rule, circ.stop_fines, circ.workstation, circ.checkin_workstation, circ.checkin_scan_time,
7661         circ.parent_circ
7662       FROM  action.circulation circ
7663         JOIN asset.copy cp ON (circ.target_copy = cp.id)
7664         JOIN asset.call_number cn ON (cp.call_number = cn.id)
7665         JOIN actor.usr p ON (circ.usr = p.id)
7666         LEFT JOIN actor.usr_address a ON (p.mailing_address = a.id)
7667         LEFT JOIN actor.usr_address b ON (p.billing_address = b.id);
7668
7669
7670
7671 INSERT INTO config.upgrade_log (version) VALUES ('0590'); -- miker/tsbere
7672
7673 CREATE OR REPLACE FUNCTION asset.cache_copy_visibility () RETURNS TRIGGER as $func$
7674 DECLARE
7675     add_front       TEXT;
7676     add_back        TEXT;
7677     add_base_query  TEXT;
7678     add_peer_query  TEXT;
7679     remove_query    TEXT;
7680     do_add          BOOLEAN := false;
7681     do_remove       BOOLEAN := false;
7682 BEGIN
7683     add_base_query := $$
7684         SELECT  cp.id, cp.circ_lib, cn.record, cn.id AS call_number, cp.location, cp.status
7685           FROM  asset.copy cp
7686                 JOIN asset.call_number cn ON (cn.id = cp.call_number)
7687                 JOIN actor.org_unit a ON (cp.circ_lib = a.id)
7688                 JOIN asset.copy_location cl ON (cp.location = cl.id)
7689                 JOIN config.copy_status cs ON (cp.status = cs.id)
7690                 JOIN biblio.record_entry b ON (cn.record = b.id)
7691           WHERE NOT cp.deleted
7692                 AND NOT cn.deleted
7693                 AND NOT b.deleted
7694                 AND cs.opac_visible
7695                 AND cl.opac_visible
7696                 AND cp.opac_visible
7697                 AND a.opac_visible
7698     $$;
7699     add_peer_query := $$
7700         SELECT  cp.id, cp.circ_lib, pbcm.peer_record AS record, NULL AS call_number, cp.location, cp.status
7701           FROM  asset.copy cp
7702                 JOIN biblio.peer_bib_copy_map pbcm ON (pbcm.target_copy = cp.id)
7703                 JOIN actor.org_unit a ON (cp.circ_lib = a.id)
7704                 JOIN asset.copy_location cl ON (cp.location = cl.id)
7705                 JOIN config.copy_status cs ON (cp.status = cs.id)
7706           WHERE NOT cp.deleted
7707                 AND cs.opac_visible
7708                 AND cl.opac_visible
7709                 AND cp.opac_visible
7710                 AND a.opac_visible
7711     $$;
7712     add_front := $$
7713         INSERT INTO asset.opac_visible_copies (copy_id, circ_lib, record)
7714           SELECT id, circ_lib, record FROM (
7715     $$;
7716     add_back := $$
7717         ) AS x
7718     $$;
7719  
7720     remove_query := $$ DELETE FROM asset.opac_visible_copies WHERE copy_id IN ( SELECT id FROM asset.copy WHERE $$;
7721
7722     IF TG_TABLE_NAME = 'peer_bib_copy_map' THEN
7723         IF TG_OP = 'INSERT' THEN
7724             add_peer_query := add_peer_query || ' AND cp.id = ' || NEW.target_copy || ' AND pbcm.record = ' || NEW.peer_record;
7725             EXECUTE add_front || add_peer_query || add_back;
7726             RETURN NEW;
7727         ELSE
7728             remove_query := 'DELETE FROM asset.opac_visible_copies WHERE copy_id = ' || OLD.target_copy || ' AND record = ' || OLD.peer_record || ';';
7729             EXECUTE remove_query;
7730             RETURN OLD;
7731         END IF;
7732     END IF;
7733
7734     IF TG_OP = 'INSERT' THEN
7735
7736         IF TG_TABLE_NAME IN ('copy', 'unit') THEN
7737             add_base_query := add_base_query || ' AND cp.id = ' || NEW.id;
7738             EXECUTE add_front || add_base_query || add_back;
7739         END IF;
7740
7741         RETURN NEW;
7742
7743     END IF;
7744
7745     -- handle items first, since with circulation activity
7746     -- their statuses change frequently
7747     IF TG_TABLE_NAME IN ('copy', 'unit') THEN
7748
7749         IF OLD.location    <> NEW.location OR
7750            OLD.call_number <> NEW.call_number OR
7751            OLD.status      <> NEW.status OR
7752            OLD.circ_lib    <> NEW.circ_lib THEN
7753             -- any of these could change visibility, but
7754             -- we'll save some queries and not try to calculate
7755             -- the change directly
7756             do_remove := true;
7757             do_add := true;
7758         ELSE
7759
7760             IF OLD.deleted <> NEW.deleted THEN
7761                 IF NEW.deleted THEN
7762                     do_remove := true;
7763                 ELSE
7764                     do_add := true;
7765                 END IF;
7766             END IF;
7767
7768             IF OLD.opac_visible <> NEW.opac_visible THEN
7769                 IF OLD.opac_visible THEN
7770                     do_remove := true;
7771                 ELSIF NOT do_remove THEN -- handle edge case where deleted item
7772                                         -- is also marked opac_visible
7773                     do_add := true;
7774                 END IF;
7775             END IF;
7776
7777         END IF;
7778
7779         IF do_remove THEN
7780             DELETE FROM asset.opac_visible_copies WHERE copy_id = NEW.id;
7781         END IF;
7782         IF do_add THEN
7783             add_base_query := add_base_query || ' AND cp.id = ' || NEW.id;
7784             add_peer_query := add_peer_query || ' AND cp.id = ' || NEW.id;
7785             EXECUTE add_front || add_base_query || ' UNION ' || add_peer_query || add_back;
7786         END IF;
7787
7788         RETURN NEW;
7789
7790     END IF;
7791
7792     IF TG_TABLE_NAME IN ('call_number', 'record_entry') THEN -- these have a 'deleted' column
7793  
7794         IF OLD.deleted AND NEW.deleted THEN -- do nothing
7795
7796             RETURN NEW;
7797  
7798         ELSIF NEW.deleted THEN -- remove rows
7799  
7800             IF TG_TABLE_NAME = 'call_number' THEN
7801                 DELETE FROM asset.opac_visible_copies WHERE copy_id IN (SELECT id FROM asset.copy WHERE call_number = NEW.id);
7802             ELSIF TG_TABLE_NAME = 'record_entry' THEN
7803                 DELETE FROM asset.opac_visible_copies WHERE record = NEW.id;
7804             END IF;
7805  
7806             RETURN NEW;
7807  
7808         ELSIF OLD.deleted THEN -- add rows
7809  
7810             IF TG_TABLE_NAME = 'call_number' THEN
7811                 add_base_query := add_base_query || ' AND cn.id = ' || NEW.id;
7812                 EXECUTE add_front || add_base_query || add_back;
7813             ELSIF TG_TABLE_NAME = 'record_entry' THEN
7814                 add_base_query := add_base_query || ' AND cn.record = ' || NEW.id;
7815                 add_peer_query := add_peer_query || ' AND pbcm.record = ' || NEW.id;
7816                 EXECUTE add_front || add_base_query || ' UNION ' || add_peer_query || add_back;
7817             END IF;
7818  
7819             RETURN NEW;
7820  
7821         END IF;
7822  
7823     END IF;
7824
7825     IF TG_TABLE_NAME = 'call_number' THEN
7826
7827         IF OLD.record <> NEW.record THEN
7828             -- call number is linked to different bib
7829             remove_query := remove_query || 'call_number = ' || NEW.id || ');';
7830             EXECUTE remove_query;
7831             add_base_query := add_base_query || ' AND cn.id = ' || NEW.id;
7832             EXECUTE add_front || add_base_query || add_back;
7833         END IF;
7834
7835         RETURN NEW;
7836
7837     END IF;
7838
7839     IF TG_TABLE_NAME IN ('record_entry') THEN
7840         RETURN NEW; -- don't have 'opac_visible'
7841     END IF;
7842
7843     -- actor.org_unit, asset.copy_location, asset.copy_status
7844     IF NEW.opac_visible = OLD.opac_visible THEN -- do nothing
7845
7846         RETURN NEW;
7847
7848     ELSIF NEW.opac_visible THEN -- add rows
7849
7850         IF TG_TABLE_NAME = 'org_unit' THEN
7851             add_base_query := add_base_query || ' AND cp.circ_lib = ' || NEW.id;
7852             add_peer_query := add_peer_query || ' AND cp.circ_lib = ' || NEW.id;
7853         ELSIF TG_TABLE_NAME = 'copy_location' THEN
7854             add_base_query := add_base_query || ' AND cp.location = ' || NEW.id;
7855             add_peer_query := add_peer_query || ' AND cp.location = ' || NEW.id;
7856         ELSIF TG_TABLE_NAME = 'copy_status' THEN
7857             add_base_query := add_base_query || ' AND cp.status = ' || NEW.id;
7858             add_peer_query := add_peer_query || ' AND cp.status = ' || NEW.id;
7859         END IF;
7860  
7861         EXECUTE add_front || add_base_query || ' UNION ' || add_peer_query || add_back;
7862  
7863     ELSE -- delete rows
7864
7865         IF TG_TABLE_NAME = 'org_unit' THEN
7866             remove_query := 'DELETE FROM asset.opac_visible_copies WHERE circ_lib = ' || NEW.id || ';';
7867         ELSIF TG_TABLE_NAME = 'copy_location' THEN
7868             remove_query := remove_query || 'location = ' || NEW.id || ');';
7869         ELSIF TG_TABLE_NAME = 'copy_status' THEN
7870             remove_query := remove_query || 'status = ' || NEW.id || ');';
7871         END IF;
7872  
7873         EXECUTE remove_query;
7874  
7875     END IF;
7876  
7877     RETURN NEW;
7878 END;
7879 $func$ LANGUAGE PLPGSQL;
7880
7881 INSERT INTO config.upgrade_log (version) VALUES ('0591'); -- berick/miker
7882
7883 CREATE OR REPLACE FUNCTION action.usr_visible_circs (usr_id INT) RETURNS SETOF action.circulation AS $func$
7884 DECLARE
7885     c               action.circulation%ROWTYPE;
7886     view_age        INTERVAL;
7887     usr_view_age    actor.usr_setting%ROWTYPE;
7888     usr_view_start  actor.usr_setting%ROWTYPE;
7889 BEGIN
7890     SELECT * INTO usr_view_age FROM actor.usr_setting WHERE usr = usr_id AND name = 'history.circ.retention_age';
7891     SELECT * INTO usr_view_start FROM actor.usr_setting WHERE usr = usr_id AND name = 'history.circ.retention_start';
7892
7893     IF usr_view_age.value IS NOT NULL AND usr_view_start.value IS NOT NULL THEN
7894         -- User opted in and supplied a retention age
7895         IF oils_json_to_text(usr_view_age.value)::INTERVAL > AGE(NOW(), oils_json_to_text(usr_view_start.value)::TIMESTAMPTZ) THEN
7896             view_age := AGE(NOW(), oils_json_to_text(usr_view_start.value)::TIMESTAMPTZ);
7897         ELSE
7898             view_age := oils_json_to_text(usr_view_age.value)::INTERVAL;
7899         END IF;
7900     ELSIF usr_view_start.value IS NOT NULL THEN
7901         -- User opted in
7902         view_age := AGE(NOW(), oils_json_to_text(usr_view_start.value)::TIMESTAMPTZ);
7903     ELSE
7904         -- User did not opt in
7905         RETURN;
7906     END IF;
7907
7908     FOR c IN
7909         SELECT  *
7910           FROM  action.circulation
7911           WHERE usr = usr_id
7912                 AND parent_circ IS NULL
7913                 AND xact_start > NOW() - view_age
7914           ORDER BY xact_start DESC
7915     LOOP
7916         RETURN NEXT c;
7917     END LOOP;
7918
7919     RETURN;
7920 END;
7921 $func$ LANGUAGE PLPGSQL;
7922
7923 CREATE OR REPLACE FUNCTION action.usr_visible_holds (usr_id INT) RETURNS SETOF action.hold_request AS $func$
7924 DECLARE
7925     h               action.hold_request%ROWTYPE;
7926     view_age        INTERVAL;
7927     view_count      INT;
7928     usr_view_count  actor.usr_setting%ROWTYPE;
7929     usr_view_age    actor.usr_setting%ROWTYPE;
7930     usr_view_start  actor.usr_setting%ROWTYPE;
7931 BEGIN
7932     SELECT * INTO usr_view_count FROM actor.usr_setting WHERE usr = usr_id AND name = 'history.hold.retention_count';
7933     SELECT * INTO usr_view_age FROM actor.usr_setting WHERE usr = usr_id AND name = 'history.hold.retention_age';
7934     SELECT * INTO usr_view_start FROM actor.usr_setting WHERE usr = usr_id AND name = 'history.hold.retention_start';
7935
7936     FOR h IN
7937         SELECT  *
7938           FROM  action.hold_request
7939           WHERE usr = usr_id
7940                 AND fulfillment_time IS NULL
7941                 AND cancel_time IS NULL
7942           ORDER BY request_time DESC
7943     LOOP
7944         RETURN NEXT h;
7945     END LOOP;
7946
7947     IF usr_view_start.value IS NULL THEN
7948         RETURN;
7949     END IF;
7950
7951     IF usr_view_age.value IS NOT NULL THEN
7952         -- User opted in and supplied a retention age
7953         IF oils_json_to_text(usr_view_age.value)::INTERVAL > AGE(NOW(), oils_json_to_text(usr_view_start.value)::TIMESTAMPTZ) THEN
7954             view_age := AGE(NOW(), oils_json_to_text(usr_view_start.value)::TIMESTAMPTZ);
7955         ELSE
7956             view_age := oils_json_to_text(usr_view_age.value)::INTERVAL;
7957         END IF;
7958     ELSE
7959         -- User opted in
7960         view_age := AGE(NOW(), oils_json_to_text(usr_view_start.value)::TIMESTAMPTZ);
7961     END IF;
7962
7963     IF usr_view_count.value IS NOT NULL THEN
7964         view_count := oils_json_to_text(usr_view_count.value)::INT;
7965     ELSE
7966         view_count := 1000;
7967     END IF;
7968
7969     -- show some fulfilled/canceled holds
7970     FOR h IN
7971         SELECT  *
7972           FROM  action.hold_request
7973           WHERE usr = usr_id
7974                 AND ( fulfillment_time IS NOT NULL OR cancel_time IS NOT NULL )
7975                 AND request_time > NOW() - view_age
7976           ORDER BY request_time DESC
7977           LIMIT view_count
7978     LOOP
7979         RETURN NEXT h;
7980     END LOOP;
7981
7982     RETURN;
7983 END;
7984 $func$ LANGUAGE PLPGSQL;
7985
7986 INSERT INTO config.upgrade_log (version) VALUES ('0599'); -- miker/gmc
7987
7988 UPDATE config.metabib_field 
7989 SET xpath = $$//mods32:mods/mods32:name[@type='personal' and not(mods32:role/mods32:roleTerm[text()='creator'])]$$
7990 WHERE field_class = 'author'
7991 AND name = 'other'
7992 AND xpath = $$//mods32:mods/mods32:name[@type='personal' and not(mods32:role)]$$
7993 AND format = 'mods32';
7994
7995 \qecho To reindex bibs that use the author|other index definition,
7996 \qecho you can run something like this:
7997 \qecho
7998 \qecho SELECT metabib.reingest_metabib_field_entries(record)
7999 \qecho FROM (
8000 \qecho   SELECT DISTINCT record
8001 \qecho   FROM metabib.real_full_rec
8002 \qecho   WHERE tag IN ('600', '700', '720', '800')
8003 \qecho   AND   subfield IN ('4', 'e')
8004 \qecho ) a;
8005
8006 -- Resolves an error in calculating copy counts for org lassos
8007 -- Per LP 790329
8008 INSERT INTO config.upgrade_log (version) VALUES ('0603');
8009
8010 -- FIXME: add/check SQL statements to perform the upgrade
8011 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$
8012 DECLARE
8013     ans RECORD;
8014     trans INT;
8015 BEGIN
8016     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;
8017
8018     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
8019         RETURN QUERY
8020         SELECT  -1,
8021                 ans.id,
8022                 COUNT( av.id ),
8023                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
8024                 COUNT( av.id ),
8025                 trans
8026           FROM
8027                 actor.org_unit_descendants(ans.id) d
8028                 JOIN asset.opac_visible_copies av ON (av.record = rid AND av.circ_lib = d.id)
8029                 JOIN asset.copy cp ON (cp.id = av.copy_id)
8030           GROUP BY 1,2,6;
8031
8032         IF NOT FOUND THEN
8033             RETURN QUERY SELECT -1, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
8034         END IF;
8035
8036     END LOOP;   
8037                 
8038     RETURN;     
8039 END;            
8040 $f$ LANGUAGE PLPGSQL;
8041
8042
8043 -- Staff record copy counts also triggered an SQL error for org lassos
8044 -- Per LP790329
8045 --
8046 INSERT INTO config.upgrade_log (version) VALUES ('0604');
8047
8048 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$
8049 DECLARE
8050     ans RECORD;
8051     trans INT;
8052 BEGIN
8053     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;
8054
8055     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
8056         RETURN QUERY
8057         SELECT  -1,
8058                 ans.id,
8059                 COUNT( cp.id ),
8060                 SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
8061                 COUNT( cp.id ),
8062                 trans
8063           FROM
8064                 actor.org_unit_descendants(ans.id) d
8065                 JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
8066                 JOIN asset.call_number cn ON (cn.record = rid AND cn.id = cp.call_number AND NOT cn.deleted)
8067           GROUP BY 1,2,6;
8068
8069         IF NOT FOUND THEN
8070             RETURN QUERY SELECT -1, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
8071         END IF;
8072
8073     END LOOP;
8074
8075     RETURN;
8076 END;
8077 $f$ LANGUAGE PLPGSQL;
8078
8079 INSERT INTO config.upgrade_log (version) VALUES ('0614'); --miker/phasefx
8080
8081 CREATE OR REPLACE FUNCTION asset.cache_copy_visibility () RETURNS TRIGGER as $func$
8082 DECLARE
8083     add_front       TEXT;
8084     add_back        TEXT;
8085     add_base_query  TEXT;
8086     add_peer_query  TEXT;
8087     remove_query    TEXT;
8088     do_add          BOOLEAN := false;
8089     do_remove       BOOLEAN := false;
8090 BEGIN
8091     add_base_query := $$
8092         SELECT  cp.id, cp.circ_lib, cn.record, cn.id AS call_number, cp.location, cp.status
8093           FROM  asset.copy cp
8094                 JOIN asset.call_number cn ON (cn.id = cp.call_number)
8095                 JOIN actor.org_unit a ON (cp.circ_lib = a.id)
8096                 JOIN asset.copy_location cl ON (cp.location = cl.id)
8097                 JOIN config.copy_status cs ON (cp.status = cs.id)
8098                 JOIN biblio.record_entry b ON (cn.record = b.id)
8099           WHERE NOT cp.deleted
8100                 AND NOT cn.deleted
8101                 AND NOT b.deleted
8102                 AND cs.opac_visible
8103                 AND cl.opac_visible
8104                 AND cp.opac_visible
8105                 AND a.opac_visible
8106     $$;
8107     add_peer_query := $$
8108         SELECT  cp.id, cp.circ_lib, pbcm.peer_record AS record, NULL AS call_number, cp.location, cp.status
8109           FROM  asset.copy cp
8110                 JOIN biblio.peer_bib_copy_map pbcm ON (pbcm.target_copy = cp.id)
8111                 JOIN actor.org_unit a ON (cp.circ_lib = a.id)
8112                 JOIN asset.copy_location cl ON (cp.location = cl.id)
8113                 JOIN config.copy_status cs ON (cp.status = cs.id)
8114           WHERE NOT cp.deleted
8115                 AND cs.opac_visible
8116                 AND cl.opac_visible
8117                 AND cp.opac_visible
8118                 AND a.opac_visible
8119     $$;
8120     add_front := $$
8121         INSERT INTO asset.opac_visible_copies (copy_id, circ_lib, record)
8122           SELECT id, circ_lib, record FROM (
8123     $$;
8124     add_back := $$
8125         ) AS x
8126     $$;
8127  
8128     remove_query := $$ DELETE FROM asset.opac_visible_copies WHERE copy_id IN ( SELECT id FROM asset.copy WHERE $$;
8129
8130     IF TG_TABLE_NAME = 'peer_bib_copy_map' THEN
8131         IF TG_OP = 'INSERT' THEN
8132             add_peer_query := add_peer_query || ' AND cp.id = ' || NEW.target_copy || ' AND pbcm.peer_record = ' || NEW.peer_record;
8133             EXECUTE add_front || add_peer_query || add_back;
8134             RETURN NEW;
8135         ELSE
8136             remove_query := 'DELETE FROM asset.opac_visible_copies WHERE copy_id = ' || OLD.target_copy || ' AND record = ' || OLD.peer_record || ';';
8137             EXECUTE remove_query;
8138             RETURN OLD;
8139         END IF;
8140     END IF;
8141
8142     IF TG_OP = 'INSERT' THEN
8143
8144         IF TG_TABLE_NAME IN ('copy', 'unit') THEN
8145             add_base_query := add_base_query || ' AND cp.id = ' || NEW.id;
8146             EXECUTE add_front || add_base_query || add_back;
8147         END IF;
8148
8149         RETURN NEW;
8150
8151     END IF;
8152
8153     -- handle items first, since with circulation activity
8154     -- their statuses change frequently
8155     IF TG_TABLE_NAME IN ('copy', 'unit') THEN
8156
8157         IF OLD.location    <> NEW.location OR
8158            OLD.call_number <> NEW.call_number OR
8159            OLD.status      <> NEW.status OR
8160            OLD.circ_lib    <> NEW.circ_lib THEN
8161             -- any of these could change visibility, but
8162             -- we'll save some queries and not try to calculate
8163             -- the change directly
8164             do_remove := true;
8165             do_add := true;
8166         ELSE
8167
8168             IF OLD.deleted <> NEW.deleted THEN
8169                 IF NEW.deleted THEN
8170                     do_remove := true;
8171                 ELSE
8172                     do_add := true;
8173                 END IF;
8174             END IF;
8175
8176             IF OLD.opac_visible <> NEW.opac_visible THEN
8177                 IF OLD.opac_visible THEN
8178                     do_remove := true;
8179                 ELSIF NOT do_remove THEN -- handle edge case where deleted item
8180                                         -- is also marked opac_visible
8181                     do_add := true;
8182                 END IF;
8183             END IF;
8184
8185         END IF;
8186
8187         IF do_remove THEN
8188             DELETE FROM asset.opac_visible_copies WHERE copy_id = NEW.id;
8189         END IF;
8190         IF do_add THEN
8191             add_base_query := add_base_query || ' AND cp.id = ' || NEW.id;
8192             add_peer_query := add_peer_query || ' AND cp.id = ' || NEW.id;
8193             EXECUTE add_front || add_base_query || ' UNION ' || add_peer_query || add_back;
8194         END IF;
8195
8196         RETURN NEW;
8197
8198     END IF;
8199
8200     IF TG_TABLE_NAME IN ('call_number', 'record_entry') THEN -- these have a 'deleted' column
8201  
8202         IF OLD.deleted AND NEW.deleted THEN -- do nothing
8203
8204             RETURN NEW;
8205  
8206         ELSIF NEW.deleted THEN -- remove rows
8207  
8208             IF TG_TABLE_NAME = 'call_number' THEN
8209                 DELETE FROM asset.opac_visible_copies WHERE copy_id IN (SELECT id FROM asset.copy WHERE call_number = NEW.id);
8210             ELSIF TG_TABLE_NAME = 'record_entry' THEN
8211                 DELETE FROM asset.opac_visible_copies WHERE record = NEW.id;
8212             END IF;
8213  
8214             RETURN NEW;
8215  
8216         ELSIF OLD.deleted THEN -- add rows
8217  
8218             IF TG_TABLE_NAME = 'call_number' THEN
8219                 add_base_query := add_base_query || ' AND cn.id = ' || NEW.id;
8220                 EXECUTE add_front || add_base_query || add_back;
8221             ELSIF TG_TABLE_NAME = 'record_entry' THEN
8222                 add_base_query := add_base_query || ' AND cn.record = ' || NEW.id;
8223                 add_peer_query := add_peer_query || ' AND pbcm.peer_record = ' || NEW.id;
8224                 EXECUTE add_front || add_base_query || ' UNION ' || add_peer_query || add_back;
8225             END IF;
8226  
8227             RETURN NEW;
8228  
8229         END IF;
8230  
8231     END IF;
8232
8233     IF TG_TABLE_NAME = 'call_number' THEN
8234
8235         IF OLD.record <> NEW.record THEN
8236             -- call number is linked to different bib
8237             remove_query := remove_query || 'call_number = ' || NEW.id || ');';
8238             EXECUTE remove_query;
8239             add_base_query := add_base_query || ' AND cn.id = ' || NEW.id;
8240             EXECUTE add_front || add_base_query || add_back;
8241         END IF;
8242
8243         RETURN NEW;
8244
8245     END IF;
8246
8247     IF TG_TABLE_NAME IN ('record_entry') THEN
8248         RETURN NEW; -- don't have 'opac_visible'
8249     END IF;
8250
8251     -- actor.org_unit, asset.copy_location, asset.copy_status
8252     IF NEW.opac_visible = OLD.opac_visible THEN -- do nothing
8253
8254         RETURN NEW;
8255
8256     ELSIF NEW.opac_visible THEN -- add rows
8257
8258         IF TG_TABLE_NAME = 'org_unit' THEN
8259             add_base_query := add_base_query || ' AND cp.circ_lib = ' || NEW.id;
8260             add_peer_query := add_peer_query || ' AND cp.circ_lib = ' || NEW.id;
8261         ELSIF TG_TABLE_NAME = 'copy_location' THEN
8262             add_base_query := add_base_query || ' AND cp.location = ' || NEW.id;
8263             add_peer_query := add_peer_query || ' AND cp.location = ' || NEW.id;
8264         ELSIF TG_TABLE_NAME = 'copy_status' THEN
8265             add_base_query := add_base_query || ' AND cp.status = ' || NEW.id;
8266             add_peer_query := add_peer_query || ' AND cp.status = ' || NEW.id;
8267         END IF;
8268  
8269         EXECUTE add_front || add_base_query || ' UNION ' || add_peer_query || add_back;
8270  
8271     ELSE -- delete rows
8272
8273         IF TG_TABLE_NAME = 'org_unit' THEN
8274             remove_query := 'DELETE FROM asset.opac_visible_copies WHERE circ_lib = ' || NEW.id || ';';
8275         ELSIF TG_TABLE_NAME = 'copy_location' THEN
8276             remove_query := remove_query || 'location = ' || NEW.id || ');';
8277         ELSIF TG_TABLE_NAME = 'copy_status' THEN
8278             remove_query := remove_query || 'status = ' || NEW.id || ');';
8279         END IF;
8280  
8281         EXECUTE remove_query;
8282  
8283     END IF;
8284  
8285     RETURN NEW;
8286 END;
8287 $func$ LANGUAGE PLPGSQL;
8288
8289 INSERT INTO config.upgrade_log (version) VALUES ('0579'); -- superceded by 0620
8290 INSERT INTO config.upgrade_log (version) VALUES ('0620'); -- tsbere via miker
8291
8292 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$
8293 DECLARE
8294     user_object             actor.usr%ROWTYPE;
8295     standing_penalty        config.standing_penalty%ROWTYPE;
8296     item_object             asset.copy%ROWTYPE;
8297     item_status_object      config.copy_status%ROWTYPE;
8298     item_location_object    asset.copy_location%ROWTYPE;
8299     result                  action.circ_matrix_test_result;
8300     circ_test               action.found_circ_matrix_matchpoint;
8301     circ_matchpoint         config.circ_matrix_matchpoint%ROWTYPE;
8302     out_by_circ_mod         config.circ_matrix_circ_mod_test%ROWTYPE;
8303     circ_mod_map            config.circ_matrix_circ_mod_test_map%ROWTYPE;
8304     hold_ratio              action.hold_stats%ROWTYPE;
8305     penalty_type            TEXT;
8306     items_out               INT;
8307     context_org_list        INT[];
8308     done                    BOOL := FALSE;
8309 BEGIN
8310     -- Assume success unless we hit a failure condition
8311     result.success := TRUE;
8312
8313     -- Need user info to look up matchpoints
8314     SELECT INTO user_object * FROM actor.usr WHERE id = match_user AND NOT deleted;
8315
8316     -- (Insta)Fail if we couldn't find the user
8317     IF user_object.id IS NULL THEN
8318         result.fail_part := 'no_user';
8319         result.success := FALSE;
8320         done := TRUE;
8321         RETURN NEXT result;
8322         RETURN;
8323     END IF;
8324
8325     -- Need item info to look up matchpoints
8326     SELECT INTO item_object * FROM asset.copy WHERE id = match_item AND NOT deleted;
8327
8328     -- (Insta)Fail if we couldn't find the item 
8329     IF item_object.id IS NULL THEN
8330         result.fail_part := 'no_item';
8331         result.success := FALSE;
8332         done := TRUE;
8333         RETURN NEXT result;
8334         RETURN;
8335     END IF;
8336
8337     SELECT INTO circ_test * FROM action.find_circ_matrix_matchpoint(circ_ou, item_object, user_object, renewal);
8338
8339     circ_matchpoint             := circ_test.matchpoint;
8340     result.matchpoint           := circ_matchpoint.id;
8341     result.circulate            := circ_matchpoint.circulate;
8342     result.duration_rule        := circ_matchpoint.duration_rule;
8343     result.recurring_fine_rule  := circ_matchpoint.recurring_fine_rule;
8344     result.max_fine_rule        := circ_matchpoint.max_fine_rule;
8345     result.hard_due_date        := circ_matchpoint.hard_due_date;
8346     result.renewals             := circ_matchpoint.renewals;
8347     result.grace_period         := circ_matchpoint.grace_period;
8348     result.buildrows            := circ_test.buildrows;
8349
8350     -- (Insta)Fail if we couldn't find a matchpoint
8351     IF circ_test.success = false THEN
8352         result.fail_part := 'no_matchpoint';
8353         result.success := FALSE;
8354         done := TRUE;
8355         RETURN NEXT result;
8356         RETURN;
8357     END IF;
8358
8359     -- All failures before this point are non-recoverable
8360     -- Below this point are possibly overridable failures
8361
8362     -- Fail if the user is barred
8363     IF user_object.barred IS TRUE THEN
8364         result.fail_part := 'actor.usr.barred';
8365         result.success := FALSE;
8366         done := TRUE;
8367         RETURN NEXT result;
8368     END IF;
8369
8370     -- Fail if the item can't circulate
8371     IF item_object.circulate IS FALSE THEN
8372         result.fail_part := 'asset.copy.circulate';
8373         result.success := FALSE;
8374         done := TRUE;
8375         RETURN NEXT result;
8376     END IF;
8377
8378     -- Fail if the item isn't in a circulateable status on a non-renewal
8379     IF NOT renewal AND item_object.status NOT IN ( 0, 7, 8 ) THEN 
8380         result.fail_part := 'asset.copy.status';
8381         result.success := FALSE;
8382         done := TRUE;
8383         RETURN NEXT result;
8384     -- Alternately, fail if the item isn't checked out on a renewal
8385     ELSIF renewal AND item_object.status <> 1 THEN
8386         result.fail_part := 'asset.copy.status';
8387         result.success := FALSE;
8388         done := TRUE;
8389         RETURN NEXT result;
8390     END IF;
8391
8392     -- Fail if the item can't circulate because of the shelving location
8393     SELECT INTO item_location_object * FROM asset.copy_location WHERE id = item_object.location;
8394     IF item_location_object.circulate IS FALSE THEN
8395         result.fail_part := 'asset.copy_location.circulate';
8396         result.success := FALSE;
8397         done := TRUE;
8398         RETURN NEXT result;
8399     END IF;
8400
8401     -- Use Circ OU for penalties and such
8402     SELECT INTO context_org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( circ_ou );
8403
8404     IF renewal THEN
8405         penalty_type = '%RENEW%';
8406     ELSE
8407         penalty_type = '%CIRC%';
8408     END IF;
8409
8410     FOR standing_penalty IN
8411         SELECT  DISTINCT csp.*
8412           FROM  actor.usr_standing_penalty usp
8413                 JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
8414           WHERE usr = match_user
8415                 AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
8416                 AND (usp.stop_date IS NULL or usp.stop_date > NOW())
8417                 AND csp.block_list LIKE penalty_type LOOP
8418
8419         result.fail_part := standing_penalty.name;
8420         result.success := FALSE;
8421         done := TRUE;
8422         RETURN NEXT result;
8423     END LOOP;
8424
8425     -- Fail if the test is set to hard non-circulating
8426     IF circ_matchpoint.circulate IS FALSE THEN
8427         result.fail_part := 'config.circ_matrix_test.circulate';
8428         result.success := FALSE;
8429         done := TRUE;
8430         RETURN NEXT result;
8431     END IF;
8432
8433     -- Fail if the total copy-hold ratio is too low
8434     IF circ_matchpoint.total_copy_hold_ratio IS NOT NULL THEN
8435         SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
8436         IF hold_ratio.total_copy_ratio IS NOT NULL AND hold_ratio.total_copy_ratio < circ_matchpoint.total_copy_hold_ratio THEN
8437             result.fail_part := 'config.circ_matrix_test.total_copy_hold_ratio';
8438             result.success := FALSE;
8439             done := TRUE;
8440             RETURN NEXT result;
8441         END IF;
8442     END IF;
8443
8444     -- Fail if the available copy-hold ratio is too low
8445     IF circ_matchpoint.available_copy_hold_ratio IS NOT NULL THEN
8446         IF hold_ratio.hold_count IS NULL THEN
8447             SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
8448         END IF;
8449         IF hold_ratio.available_copy_ratio IS NOT NULL AND hold_ratio.available_copy_ratio < circ_matchpoint.available_copy_hold_ratio THEN
8450             result.fail_part := 'config.circ_matrix_test.available_copy_hold_ratio';
8451             result.success := FALSE;
8452             done := TRUE;
8453             RETURN NEXT result;
8454         END IF;
8455     END IF;
8456
8457     -- Fail if the user has too many items with specific circ_modifiers checked out
8458     FOR out_by_circ_mod IN SELECT * FROM config.circ_matrix_circ_mod_test WHERE matchpoint = circ_matchpoint.id LOOP
8459         SELECT  INTO items_out COUNT(*)
8460           FROM  action.circulation circ
8461             JOIN asset.copy cp ON (cp.id = circ.target_copy)
8462           WHERE circ.usr = match_user
8463                AND circ.circ_lib IN ( SELECT * FROM unnest(context_org_list) )
8464             AND circ.checkin_time IS NULL
8465             AND (circ.stop_fines IN ('MAXFINES','LONGOVERDUE') OR circ.stop_fines IS NULL)
8466             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);
8467         IF items_out >= out_by_circ_mod.items_out THEN
8468             result.fail_part := 'config.circ_matrix_circ_mod_test';
8469             result.success := FALSE;
8470             done := TRUE;
8471             RETURN NEXT result;
8472         END IF;
8473     END LOOP;
8474
8475     -- If we passed everything, return the successful matchpoint
8476     IF NOT done THEN
8477         RETURN NEXT result;
8478     END IF;
8479
8480     RETURN;
8481 END;
8482 $func$ LANGUAGE plpgsql;
8483
8484
8485
8486 INSERT INTO config.upgrade_log (version) VALUES ('0628');
8487
8488 -- acq.fund_combined_balance and acq.fund_spent_balance are unchanged,
8489 -- however we need to drop them to recreate the other views.
8490 -- we need to drop all our views because we change the number of columns
8491 -- for example, debit_total does not need an encumberance column when we 
8492 -- have a sepearate total for that.
8493
8494 DROP VIEW acq.fund_spent_balance;
8495 DROP VIEW acq.fund_combined_balance;
8496 DROP VIEW acq.fund_encumbrance_total;
8497 DROP VIEW acq.fund_spent_total;
8498 DROP VIEW acq.fund_debit_total;
8499
8500 CREATE OR REPLACE VIEW acq.fund_debit_total AS
8501     SELECT  fund.id AS fund, 
8502             sum(COALESCE(fund_debit.amount, 0::numeric)) AS amount
8503     FROM acq.fund fund
8504         LEFT JOIN acq.fund_debit fund_debit ON fund.id = fund_debit.fund
8505     GROUP BY fund.id;
8506
8507 CREATE OR REPLACE VIEW acq.fund_encumbrance_total AS
8508     SELECT 
8509         fund.id AS fund, 
8510         sum(COALESCE(fund_debit.amount, 0::numeric)) AS amount 
8511     FROM acq.fund fund
8512         LEFT JOIN acq.fund_debit fund_debit ON fund.id = fund_debit.fund 
8513     WHERE fund_debit.encumbrance GROUP BY fund.id;
8514
8515 CREATE OR REPLACE VIEW acq.fund_spent_total AS
8516     SELECT  fund.id AS fund, 
8517             sum(COALESCE(fund_debit.amount, 0::numeric)) AS amount 
8518     FROM acq.fund fund
8519         LEFT JOIN acq.fund_debit fund_debit ON fund.id = fund_debit.fund 
8520     WHERE NOT fund_debit.encumbrance 
8521     GROUP BY fund.id;
8522
8523 CREATE OR REPLACE VIEW acq.fund_combined_balance AS
8524     SELECT  c.fund, 
8525             c.amount - COALESCE(d.amount, 0.0) AS amount
8526     FROM acq.fund_allocation_total c
8527     LEFT JOIN acq.fund_debit_total d USING (fund);
8528
8529 CREATE OR REPLACE VIEW acq.fund_spent_balance AS
8530     SELECT  c.fund,
8531             c.amount - COALESCE(d.amount,0.0) AS amount
8532       FROM  acq.fund_allocation_total c
8533             LEFT JOIN acq.fund_spent_total d USING (fund);
8534
8535
8536
8537 INSERT INTO config.upgrade_log (version) VALUES ('0631');
8538
8539 CREATE OR REPLACE FUNCTION search.query_parser_fts (
8540
8541     param_search_ou INT,
8542     param_depth     INT,
8543     param_query     TEXT,
8544     param_statuses  INT[],
8545     param_locations INT[],
8546     param_offset    INT,
8547     param_check     INT,
8548     param_limit     INT,
8549     metarecord      BOOL,
8550     staff           BOOL
8551  
8552 ) RETURNS SETOF search.search_result AS $func$
8553 DECLARE
8554
8555     current_res         search.search_result%ROWTYPE;
8556     search_org_list     INT[];
8557     luri_org_list       INT[];
8558     tmp_int_list        INT[];
8559
8560     check_limit         INT;
8561     core_limit          INT;
8562     core_offset         INT;
8563     tmp_int             INT;
8564
8565     core_result         RECORD;
8566     core_cursor         REFCURSOR;
8567     core_rel_query      TEXT;
8568
8569     total_count         INT := 0;
8570     check_count         INT := 0;
8571     deleted_count       INT := 0;
8572     visible_count       INT := 0;
8573     excluded_count      INT := 0;
8574
8575 BEGIN
8576
8577     check_limit := COALESCE( param_check, 1000 );
8578     core_limit  := COALESCE( param_limit, 25000 );
8579     core_offset := COALESCE( param_offset, 0 );
8580
8581     -- core_skip_chk := COALESCE( param_skip_chk, 1 );
8582
8583     IF param_search_ou > 0 THEN
8584         IF param_depth IS NOT NULL THEN
8585             SELECT array_accum(distinct id) INTO search_org_list FROM actor.org_unit_descendants( param_search_ou, param_depth );
8586         ELSE
8587             SELECT array_accum(distinct id) INTO search_org_list FROM actor.org_unit_descendants( param_search_ou );
8588         END IF;
8589
8590         SELECT array_accum(distinct id) INTO luri_org_list FROM actor.org_unit_ancestors( param_search_ou );
8591
8592     ELSIF param_search_ou < 0 THEN
8593         SELECT array_accum(distinct org_unit) INTO search_org_list FROM actor.org_lasso_map WHERE lasso = -param_search_ou;
8594
8595         FOR tmp_int IN SELECT * FROM UNNEST(search_org_list) LOOP
8596             SELECT array_accum(distinct id) INTO tmp_int_list FROM actor.org_unit_ancestors( tmp_int );
8597             luri_org_list := luri_org_list || tmp_int_list;
8598         END LOOP;
8599
8600         SELECT array_accum(DISTINCT x.id) INTO luri_org_list FROM UNNEST(luri_org_list) x(id);
8601
8602     ELSIF param_search_ou = 0 THEN
8603         -- reserved for user lassos (ou_buckets/type='lasso') with ID passed in depth ... hack? sure.
8604     END IF;
8605
8606     OPEN core_cursor FOR EXECUTE param_query;
8607
8608     LOOP
8609
8610         FETCH core_cursor INTO core_result;
8611         EXIT WHEN NOT FOUND;
8612         EXIT WHEN total_count >= core_limit;
8613
8614         total_count := total_count + 1;
8615
8616         CONTINUE WHEN total_count NOT BETWEEN  core_offset + 1 AND check_limit + core_offset;
8617
8618         check_count := check_count + 1;
8619
8620         PERFORM 1 FROM biblio.record_entry b WHERE NOT b.deleted AND b.id IN ( SELECT * FROM unnest( core_result.records ) );
8621         IF NOT FOUND THEN
8622             -- RAISE NOTICE ' % were all deleted ... ', core_result.records;
8623             deleted_count := deleted_count + 1;
8624             CONTINUE;
8625         END IF;
8626
8627         PERFORM 1
8628           FROM  biblio.record_entry b
8629                 JOIN config.bib_source s ON (b.source = s.id)
8630           WHERE s.transcendant
8631                 AND b.id IN ( SELECT * FROM unnest( core_result.records ) );
8632
8633         IF FOUND THEN
8634             -- RAISE NOTICE ' % were all transcendant ... ', core_result.records;
8635             visible_count := visible_count + 1;
8636
8637             current_res.id = core_result.id;
8638             current_res.rel = core_result.rel;
8639
8640             tmp_int := 1;
8641             IF metarecord THEN
8642                 SELECT COUNT(DISTINCT s.source) INTO tmp_int FROM metabib.metarecord_source_map s WHERE s.metarecord = core_result.id;
8643             END IF;
8644
8645             IF tmp_int = 1 THEN
8646                 current_res.record = core_result.records[1];
8647             ELSE
8648                 current_res.record = NULL;
8649             END IF;
8650
8651             RETURN NEXT current_res;
8652
8653             CONTINUE;
8654         END IF;
8655
8656         PERFORM 1
8657           FROM  asset.call_number cn
8658                 JOIN asset.uri_call_number_map map ON (map.call_number = cn.id)
8659                 JOIN asset.uri uri ON (map.uri = uri.id)
8660           WHERE NOT cn.deleted
8661                 AND cn.label = '##URI##'
8662                 AND uri.active
8663                 AND ( param_locations IS NULL OR array_upper(param_locations, 1) IS NULL )
8664                 AND cn.record IN ( SELECT * FROM unnest( core_result.records ) )
8665                 AND cn.owning_lib IN ( SELECT * FROM unnest( luri_org_list ) )
8666           LIMIT 1;
8667
8668         IF FOUND THEN
8669             -- RAISE NOTICE ' % have at least one URI ... ', core_result.records;
8670             visible_count := visible_count + 1;
8671
8672             current_res.id = core_result.id;
8673             current_res.rel = core_result.rel;
8674
8675             tmp_int := 1;
8676             IF metarecord THEN
8677                 SELECT COUNT(DISTINCT s.source) INTO tmp_int FROM metabib.metarecord_source_map s WHERE s.metarecord = core_result.id;
8678             END IF;
8679
8680             IF tmp_int = 1 THEN
8681                 current_res.record = core_result.records[1];
8682             ELSE
8683                 current_res.record = NULL;
8684             END IF;
8685
8686             RETURN NEXT current_res;
8687
8688             CONTINUE;
8689         END IF;
8690
8691         IF param_statuses IS NOT NULL AND array_upper(param_statuses, 1) > 0 THEN
8692
8693             PERFORM 1
8694               FROM  asset.call_number cn
8695                     JOIN asset.copy cp ON (cp.call_number = cn.id)
8696               WHERE NOT cn.deleted
8697                     AND NOT cp.deleted
8698                     AND cp.status IN ( SELECT * FROM unnest( param_statuses ) )
8699                     AND cn.record IN ( SELECT * FROM unnest( core_result.records ) )
8700                     AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
8701               LIMIT 1;
8702
8703             IF NOT FOUND THEN
8704                 PERFORM 1
8705                   FROM  biblio.peer_bib_copy_map pr
8706                         JOIN asset.copy cp ON (cp.id = pr.target_copy)
8707                   WHERE NOT cp.deleted
8708                         AND cp.status IN ( SELECT * FROM unnest( param_statuses ) )
8709                         AND pr.peer_record IN ( SELECT * FROM unnest( core_result.records ) )
8710                         AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
8711                   LIMIT 1;
8712
8713                 IF NOT FOUND THEN
8714                 -- RAISE NOTICE ' % and multi-home linked records were all status-excluded ... ', core_result.records;
8715                     excluded_count := excluded_count + 1;
8716                     CONTINUE;
8717                 END IF;
8718             END IF;
8719
8720         END IF;
8721
8722         IF param_locations IS NOT NULL AND array_upper(param_locations, 1) > 0 THEN
8723
8724             PERFORM 1
8725               FROM  asset.call_number cn
8726                     JOIN asset.copy cp ON (cp.call_number = cn.id)
8727               WHERE NOT cn.deleted
8728                     AND NOT cp.deleted
8729                     AND cp.location IN ( SELECT * FROM unnest( param_locations ) )
8730                     AND cn.record IN ( SELECT * FROM unnest( core_result.records ) )
8731                     AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
8732               LIMIT 1;
8733
8734             IF NOT FOUND THEN
8735                 PERFORM 1
8736                   FROM  biblio.peer_bib_copy_map pr
8737                         JOIN asset.copy cp ON (cp.id = pr.target_copy)
8738                   WHERE NOT cp.deleted
8739                         AND cp.location IN ( SELECT * FROM unnest( param_locations ) )
8740                         AND pr.peer_record IN ( SELECT * FROM unnest( core_result.records ) )
8741                         AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
8742                   LIMIT 1;
8743
8744                 IF NOT FOUND THEN
8745                     -- RAISE NOTICE ' % and multi-home linked records were all copy_location-excluded ... ', core_result.records;
8746                     excluded_count := excluded_count + 1;
8747                     CONTINUE;
8748                 END IF;
8749             END IF;
8750
8751         END IF;
8752
8753         IF staff IS NULL OR NOT staff THEN
8754
8755             PERFORM 1
8756               FROM  asset.opac_visible_copies
8757               WHERE circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
8758                     AND record IN ( SELECT * FROM unnest( core_result.records ) )
8759               LIMIT 1;
8760
8761             IF NOT FOUND THEN
8762                 PERFORM 1
8763                   FROM  biblio.peer_bib_copy_map pr
8764                         JOIN asset.opac_visible_copies cp ON (cp.copy_id = pr.target_copy)
8765                   WHERE cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
8766                         AND pr.peer_record IN ( SELECT * FROM unnest( core_result.records ) )
8767                   LIMIT 1;
8768
8769                 IF NOT FOUND THEN
8770
8771                     -- RAISE NOTICE ' % and multi-home linked records were all visibility-excluded ... ', core_result.records;
8772                     excluded_count := excluded_count + 1;
8773                     CONTINUE;
8774                 END IF;
8775             END IF;
8776
8777         ELSE
8778
8779             PERFORM 1
8780               FROM  asset.call_number cn
8781                     JOIN asset.copy cp ON (cp.call_number = cn.id)
8782               WHERE NOT cn.deleted
8783                     AND NOT cp.deleted
8784                     AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
8785                     AND cn.record IN ( SELECT * FROM unnest( core_result.records ) )
8786               LIMIT 1;
8787
8788             IF NOT FOUND THEN
8789
8790                 PERFORM 1
8791                   FROM  biblio.peer_bib_copy_map pr
8792                         JOIN asset.copy cp ON (cp.id = pr.target_copy)
8793                   WHERE NOT cp.deleted
8794                         AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
8795                         AND pr.peer_record IN ( SELECT * FROM unnest( core_result.records ) )
8796                   LIMIT 1;
8797
8798                 IF NOT FOUND THEN
8799
8800                     PERFORM 1
8801                       FROM  asset.call_number cn
8802                             JOIN asset.copy cp ON (cp.call_number = cn.id)
8803                       WHERE cn.record IN ( SELECT * FROM unnest( core_result.records ) )
8804                             AND NOT cp.deleted
8805                       LIMIT 1;
8806
8807                     IF FOUND THEN
8808                         -- RAISE NOTICE ' % and multi-home linked records were all visibility-excluded ... ', core_result.records;
8809                         excluded_count := excluded_count + 1;
8810                         CONTINUE;
8811                     END IF;
8812                 END IF;
8813
8814             END IF;
8815
8816         END IF;
8817
8818         visible_count := visible_count + 1;
8819
8820         current_res.id = core_result.id;
8821         current_res.rel = core_result.rel;
8822
8823         tmp_int := 1;
8824         IF metarecord THEN
8825             SELECT COUNT(DISTINCT s.source) INTO tmp_int FROM metabib.metarecord_source_map s WHERE s.metarecord = core_result.id;
8826         END IF;
8827
8828         IF tmp_int = 1 THEN
8829             current_res.record = core_result.records[1];
8830         ELSE
8831             current_res.record = NULL;
8832         END IF;
8833
8834         RETURN NEXT current_res;
8835
8836         IF visible_count % 1000 = 0 THEN
8837             -- RAISE NOTICE ' % visible so far ... ', visible_count;
8838         END IF;
8839
8840     END LOOP;
8841
8842     current_res.id = NULL;
8843     current_res.rel = NULL;
8844     current_res.record = NULL;
8845     current_res.total = total_count;
8846     current_res.checked = check_count;
8847     current_res.deleted = deleted_count;
8848     current_res.visible = visible_count;
8849     current_res.excluded = excluded_count;
8850
8851     CLOSE core_cursor;
8852
8853     RETURN NEXT current_res;
8854
8855 END;
8856 $func$ LANGUAGE PLPGSQL;
8857
8858
8859 INSERT INTO config.upgrade_log (version) VALUES ('0633');
8860 INSERT INTO config.upgrade_log (version) VALUES ('0634');
8861
8862 COMMIT;
8863
8864 --0633
8865 INSERT into config.org_unit_setting_type
8866 ( name, grp, label, description, datatype ) VALUES
8867 (
8868         'print.custom_js_file', 'circ',
8869         oils_i18n_gettext(
8870             'print.custom_js_file',
8871             'Printing: Custom Javascript File',
8872             'coust',
8873             'label'
8874         ),
8875         oils_i18n_gettext(
8876             'print.custom_js_file',
8877             'Full URL path to a Javascript File to be loaded when printing. Should'
8878             || ' implement a print_custom function for DOM manipulation. Can change'
8879             || ' the value of the do_print variable to false to cancel printing.',
8880             'coust',
8881             'description'
8882         ),
8883         'string'
8884     );
8885
8886
8887 --0634
8888 INSERT INTO permission.perm_list ( id, code, description ) VALUES
8889  ( 513, 'DEBUG_CLIENT', oils_i18n_gettext( 513,
8890     'Allows a user to use debug functions in the staff client', 'ppl', 'description' ));
8891
8892 UPDATE asset.call_number SET id = id WHERE deleted IS FALSE OR deleted = FALSE;
8893
8894 -- 0529
8895 INSERT INTO config.org_unit_setting_type 
8896 ( name, label, description, datatype ) VALUES 
8897 ( 'circ.user_merge.delete_addresses', 
8898   'Circ:  Patron Merge Address Delete', 
8899   'Delete address(es) of subordinate user(s) in a patron merge', 
8900    'bool'
8901 );
8902
8903 INSERT INTO config.org_unit_setting_type 
8904 ( name, label, description, datatype ) VALUES 
8905 ( 'circ.user_merge.delete_cards', 
8906   'Circ: Patron Merge Barcode Delete', 
8907   'Delete barcode(s) of subordinate user(s) in a patron merge', 
8908   'bool'
8909 );
8910
8911 INSERT INTO config.org_unit_setting_type 
8912 ( name, label, description, datatype ) VALUES 
8913 ( 'circ.user_merge.deactivate_cards', 
8914   'Circ:  Patron Merge Deactivate Card', 
8915   'Mark barcode(s) of subordinate user(s) in a patron merge as inactive', 
8916   'bool'
8917 );
8918
8919 DROP TRIGGER IF EXISTS mat_summary_add_tgr ON money.cash_payment;
8920 DROP TRIGGER IF EXISTS mat_summary_upd_tgr ON money.cash_payment;
8921 DROP TRIGGER IF EXISTS mat_summary_del_tgr ON money.cash_payment;
8922
8923 CREATE TRIGGER mat_summary_add_tgr AFTER INSERT ON money.cash_payment FOR EACH ROW EXECUTE PROCEDURE money.materialized_summary_payment_add ('cash_payment');
8924 CREATE TRIGGER mat_summary_upd_tgr AFTER UPDATE ON money.cash_payment FOR EACH ROW EXECUTE PROCEDURE money.materialized_summary_payment_update ('cash_payment');
8925 CREATE TRIGGER mat_summary_del_tgr BEFORE DELETE ON money.cash_payment FOR EACH ROW EXECUTE PROCEDURE money.materialized_summary_payment_del ('cash_payment');
8926
8927 DROP TRIGGER IF EXISTS mat_summary_add_tgr ON money.check_payment;
8928 DROP TRIGGER IF EXISTS mat_summary_upd_tgr ON money.check_payment;
8929 DROP TRIGGER IF EXISTS mat_summary_del_tgr ON money.check_payment;
8930
8931 CREATE TRIGGER mat_summary_add_tgr AFTER INSERT ON money.check_payment FOR EACH ROW EXECUTE PROCEDURE money.materialized_summary_payment_add ('check_payment');
8932 CREATE TRIGGER mat_summary_upd_tgr AFTER UPDATE ON money.check_payment FOR EACH ROW EXECUTE PROCEDURE money.materialized_summary_payment_update ('check_payment');
8933 CREATE TRIGGER mat_summary_del_tgr BEFORE DELETE ON money.check_payment FOR EACH ROW EXECUTE PROCEDURE money.materialized_summary_payment_del ('check_payment');
8934
8935
8936 UPDATE  metabib.record_attr
8937   SET   attrs = attrs || asort
8938   FROM  (SELECT record,
8939                 HSTORE('authorsort',FIRST(value)) AS asort
8940           FROM  metabib.full_rec
8941           WHERE tag like '1%'
8942         GROUP BY 1) x
8943   WHERE x.record = metabib.record_attr.id;
8944
8945 UPDATE  metabib.record_attr
8946   SET   attrs = attrs || tsort
8947   FROM  (SELECT record,
8948                 HSTORE('titlesort',FIRST(value)) AS tsort
8949           FROM  metabib.full_rec
8950           WHERE tag = 'tnf'
8951         GROUP BY 1) x
8952   WHERE x.record = metabib.record_attr.id;
8953
8954 UPDATE metabib.record_attr
8955    SET attrs = attrs || ('pubdate' => (attrs->'date1'))
8956    WHERE defined(attrs, 'pubdate') IS FALSE
8957    AND defined(attrs, 'date1') IS TRUE;