LP#1893463: Protect against null emails
[Evergreen.git] / Open-ILS / src / sql / Pg / reporter-schema.sql
1 /*
2  * Copyright (C) 2004-2008  Georgia Public Library Service
3  * Copyright (C) 2007-2008  Equinox Software, Inc.
4  * Mike Rylander <miker@esilibrary.com> 
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU General Public License
8  * as published by the Free Software Foundation; either version 2
9  * of the License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  */
17
18 DROP SCHEMA IF EXISTS reporter CASCADE;
19
20 BEGIN;
21
22 CREATE SCHEMA reporter;
23
24 CREATE TABLE reporter.template_folder (
25         id              SERIAL                          PRIMARY KEY,
26         parent          INT                             REFERENCES reporter.template_folder (id) DEFERRABLE INITIALLY DEFERRED,
27         owner           INT                             NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
28         create_time     TIMESTAMP WITH TIME ZONE        NOT NULL DEFAULT NOW(),
29         name            TEXT                            NOT NULL,
30         shared          BOOL                            NOT NULL DEFAULT FALSE,
31         share_with      INT                             REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED
32 );
33 CREATE INDEX rpt_tmpl_fldr_owner_idx ON reporter.template_folder (owner);
34 CREATE UNIQUE INDEX rpt_template_folder_once_parent_idx ON reporter.template_folder (name,parent);
35 CREATE UNIQUE INDEX rpt_template_folder_once_idx ON reporter.template_folder (name,owner) WHERE parent IS NULL;
36
37 CREATE TABLE reporter.report_folder (
38         id              SERIAL                          PRIMARY KEY,
39         parent          INT                             REFERENCES reporter.report_folder (id) DEFERRABLE INITIALLY DEFERRED,
40         owner           INT                             NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
41         create_time     TIMESTAMP WITH TIME ZONE        NOT NULL DEFAULT NOW(),
42         name            TEXT                            NOT NULL,
43         shared          BOOL                            NOT NULL DEFAULT FALSE,
44         share_with      INT                             REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED
45 );
46 CREATE INDEX rpt_rpt_fldr_owner_idx ON reporter.report_folder (owner);
47 CREATE UNIQUE INDEX rpt_report_folder_once_parent_idx ON reporter.report_folder (name,parent);
48 CREATE UNIQUE INDEX rpt_report_folder_once_idx ON reporter.report_folder (name,owner) WHERE parent IS NULL;
49
50 CREATE TABLE reporter.output_folder (
51         id              SERIAL                          PRIMARY KEY,
52         parent          INT                             REFERENCES reporter.output_folder (id) DEFERRABLE INITIALLY DEFERRED,
53         owner           INT                             NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
54         create_time     TIMESTAMP WITH TIME ZONE        NOT NULL DEFAULT NOW(),
55         name            TEXT                            NOT NULL,
56         shared          BOOL                            NOT NULL DEFAULT FALSE,
57         share_with      INT                             REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED
58 );
59 CREATE INDEX rpt_output_fldr_owner_idx ON reporter.output_folder (owner);
60 CREATE UNIQUE INDEX rpt_output_folder_once_parent_idx ON reporter.output_folder (name,parent);
61 CREATE UNIQUE INDEX rpt_output_folder_once_idx ON reporter.output_folder (name,owner) WHERE parent IS NULL;
62
63
64 CREATE TABLE reporter.template (
65         id              SERIAL                          PRIMARY KEY,
66         owner           INT                             NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
67         create_time     TIMESTAMP WITH TIME ZONE        NOT NULL DEFAULT NOW(),
68         name            TEXT                            NOT NULL,
69         description     TEXT                            NOT NULL DEFAULT '',
70         data            TEXT                            NOT NULL,
71         folder          INT                             NOT NULL REFERENCES reporter.template_folder (id) DEFERRABLE INITIALLY DEFERRED
72 );
73 CREATE INDEX rpt_tmpl_owner_idx ON reporter.template (owner);
74 CREATE INDEX rpt_tmpl_fldr_idx ON reporter.template (folder);
75 CREATE UNIQUE INDEX rtp_template_folder_once_idx ON reporter.template (name,folder);
76
77 CREATE TABLE reporter.report (
78         id              SERIAL                          PRIMARY KEY,
79         owner           INT                             NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
80         create_time     TIMESTAMP WITH TIME ZONE        NOT NULL DEFAULT NOW(),
81         name            TEXT                            NOT NULL DEFAULT '',
82         description     TEXT                            NOT NULL DEFAULT '',
83         template        INT                             NOT NULL REFERENCES reporter.template (id) DEFERRABLE INITIALLY DEFERRED,
84         data            TEXT                            NOT NULL,
85         folder          INT                             NOT NULL REFERENCES reporter.report_folder (id) DEFERRABLE INITIALLY DEFERRED,
86         recur           BOOL                            NOT NULL DEFAULT FALSE,
87         recurrence      INTERVAL
88 );
89 CREATE INDEX rpt_rpt_owner_idx ON reporter.report (owner);
90 CREATE INDEX rpt_rpt_fldr_idx ON reporter.report (folder);
91 CREATE UNIQUE INDEX rtp_report_folder_once_idx ON reporter.report (name,folder);
92
93 CREATE TABLE reporter.schedule (
94         id              SERIAL                          PRIMARY KEY,
95         report          INT                             NOT NULL REFERENCES reporter.report (id) DEFERRABLE INITIALLY DEFERRED,
96         folder          INT                             NOT NULL REFERENCES reporter.output_folder (id) DEFERRABLE INITIALLY DEFERRED,
97         runner          INT                             NOT NULL REFERENCES actor.usr (id) DEFERRABLE INITIALLY DEFERRED,
98         run_time        TIMESTAMP WITH TIME ZONE        NOT NULL DEFAULT NOW(),
99         start_time      TIMESTAMP WITH TIME ZONE,
100         complete_time   TIMESTAMP WITH TIME ZONE,
101         email           TEXT,
102         excel_format    BOOL                            NOT NULL DEFAULT TRUE,
103         html_format     BOOL                            NOT NULL DEFAULT TRUE,
104         csv_format      BOOL                            NOT NULL DEFAULT TRUE,
105         chart_pie       BOOL                            NOT NULL DEFAULT FALSE,
106         chart_bar       BOOL                            NOT NULL DEFAULT FALSE,
107         chart_line      BOOL                            NOT NULL DEFAULT FALSE,
108         error_code      INT,
109         error_text      TEXT
110 );
111 CREATE INDEX rpt_sched_runner_idx ON reporter.schedule (runner);
112 CREATE INDEX rpt_sched_folder_idx ON reporter.schedule (folder);
113 CREATE UNIQUE INDEX rpt_sched_recurrence_once_idx ON reporter.schedule (report,folder,runner,run_time,COALESCE(email,''));
114
115 CREATE OR REPLACE VIEW reporter.simple_record AS
116 SELECT  r.id,
117         s.metarecord,
118         r.fingerprint,
119         r.quality,
120         r.tcn_source,
121         r.tcn_value,
122         title.value AS title,
123         uniform_title.value AS uniform_title,
124         author.value AS author,
125         publisher.value AS publisher,
126         SUBSTRING(pubdate.value FROM $$\d+$$) AS pubdate,
127         series_title.value AS series_title,
128         series_statement.value AS series_statement,
129         summary.value AS summary,
130         ARRAY_AGG( DISTINCT REPLACE(SUBSTRING(isbn.value FROM $$^\S+$$), '-', '') ) AS isbn,
131         ARRAY_AGG( DISTINCT REGEXP_REPLACE(issn.value, E'^\\S*(\\d{4})[-\\s](\\d{3,4}x?)', E'\\1 \\2') ) AS issn,
132         ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '650' AND subfield = 'a' AND record = r.id)) AS topic_subject,
133         ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '651' AND subfield = 'a' AND record = r.id)) AS geographic_subject,
134         ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '655' AND subfield = 'a' AND record = r.id)) AS genre,
135         ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '600' AND subfield = 'a' AND record = r.id)) AS name_subject,
136         ARRAY((SELECT DISTINCT value FROM metabib.full_rec WHERE tag = '610' AND subfield = 'a' AND record = r.id)) AS corporate_subject,
137         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
138   FROM  biblio.record_entry r
139         JOIN metabib.metarecord_source_map s ON (s.source = r.id)
140         LEFT JOIN metabib.full_rec uniform_title ON (r.id = uniform_title.record AND uniform_title.tag = '240' AND uniform_title.subfield = 'a')
141         LEFT JOIN metabib.full_rec title ON (r.id = title.record AND title.tag = '245' AND title.subfield = 'a')
142         LEFT JOIN metabib.full_rec author ON (r.id = author.record AND author.tag = '100' AND author.subfield = 'a')
143         LEFT JOIN metabib.full_rec publisher ON (r.id = publisher.record AND (publisher.tag = '260' OR (publisher.tag = '264' AND publisher.ind2 = '1')) AND publisher.subfield = 'b')
144         LEFT JOIN metabib.full_rec pubdate ON (r.id = pubdate.record AND (pubdate.tag = '260' OR (pubdate.tag = '264' AND pubdate.ind2 = '1')) AND pubdate.subfield = 'c')
145         LEFT JOIN metabib.full_rec isbn ON (r.id = isbn.record AND isbn.tag IN ('024', '020') AND isbn.subfield IN ('a','z'))
146         LEFT JOIN metabib.full_rec issn ON (r.id = issn.record AND issn.tag = '022' AND issn.subfield = 'a')
147         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')
148         LEFT JOIN metabib.full_rec series_statement ON (r.id = series_statement.record AND series_statement.tag = '490' AND series_statement.subfield = 'a')
149         LEFT JOIN metabib.full_rec summary ON (r.id = summary.record AND summary.tag = '520' AND summary.subfield = 'a')
150   GROUP BY 1,2,3,4,5,6,7,8,9,10,11,12,13,14;
151
152 CREATE OR REPLACE VIEW reporter.old_super_simple_record AS
153 SELECT  r.id,
154     r.fingerprint,
155     r.quality,
156     r.tcn_source,
157     r.tcn_value,
158     evergreen.oils_json_to_text(d.title) AS title,
159     evergreen.oils_json_to_text(d.author) AS author,
160     evergreen.oils_json_to_text(d.publisher) AS publisher,
161     evergreen.oils_json_to_text(d.pubdate) AS pubdate,
162     CASE WHEN d.isbn = 'null'
163         THEN NULL
164         ELSE (SELECT ARRAY(SELECT json_array_elements_text(d.isbn::JSON)))
165     END AS isbn,
166     CASE WHEN d.issn = 'null'
167         THEN NULL
168         ELSE (SELECT ARRAY(SELECT json_array_elements_text(d.issn::JSON)))
169     END AS issn
170   FROM  biblio.record_entry r
171         JOIN metabib.wide_display_entry d ON (r.id = d.source);
172
173 CREATE TABLE reporter.materialized_simple_record AS SELECT * FROM reporter.old_super_simple_record WHERE 1=0;
174 ALTER TABLE reporter.materialized_simple_record ADD PRIMARY KEY (id);
175
176 CREATE VIEW reporter.super_simple_record AS SELECT * FROM reporter.materialized_simple_record;
177
178 CREATE OR REPLACE FUNCTION reporter.simple_rec_update (r_id BIGINT, deleted BOOL) RETURNS BOOL AS $$
179 BEGIN
180
181     DELETE FROM reporter.materialized_simple_record WHERE id = r_id;
182
183     IF NOT deleted THEN
184         INSERT INTO reporter.materialized_simple_record SELECT DISTINCT ON (id) * FROM reporter.old_super_simple_record WHERE id = r_id;
185     END IF;
186
187     RETURN TRUE;
188
189 END;
190 $$ LANGUAGE PLPGSQL;
191
192 CREATE OR REPLACE FUNCTION reporter.simple_rec_update (r_id BIGINT) RETURNS BOOL AS $$
193     SELECT reporter.simple_rec_update($1, FALSE);
194 $$ LANGUAGE SQL;
195
196 CREATE OR REPLACE FUNCTION reporter.simple_rec_delete (r_id BIGINT) RETURNS BOOL AS $$
197     SELECT reporter.simple_rec_update($1, TRUE);
198 $$ LANGUAGE SQL;
199
200 CREATE OR REPLACE FUNCTION reporter.simple_rec_trigger () RETURNS TRIGGER AS $func$
201 BEGIN
202     IF TG_OP = 'DELETE' THEN
203         PERFORM reporter.simple_rec_delete(NEW.id);
204     ELSE
205         PERFORM reporter.simple_rec_update(NEW.id);
206     END IF;
207
208     RETURN NEW;
209 END;
210 $func$ LANGUAGE PLPGSQL;
211
212 CREATE OR REPLACE FUNCTION reporter.disable_materialized_simple_record_trigger () RETURNS VOID AS $$
213     DROP TRIGGER IF EXISTS bbb_simple_rec_trigger ON biblio.record_entry;
214 $$ LANGUAGE SQL;
215
216 CREATE OR REPLACE FUNCTION reporter.enable_materialized_simple_record_trigger () RETURNS VOID AS $$
217
218     TRUNCATE TABLE reporter.materialized_simple_record;
219
220     INSERT INTO reporter.materialized_simple_record
221         (id,fingerprint,quality,tcn_source,tcn_value,title,author,publisher,pubdate,isbn,issn)
222         SELECT DISTINCT ON (id) * FROM reporter.old_super_simple_record;
223
224     CREATE TRIGGER bbb_simple_rec_trigger
225         AFTER INSERT OR UPDATE OR DELETE ON biblio.record_entry
226         FOR EACH ROW EXECUTE PROCEDURE reporter.simple_rec_trigger();
227
228 $$ LANGUAGE SQL;
229
230 CREATE OR REPLACE FUNCTION reporter.refresh_materialized_simple_record () RETURNS VOID AS $$
231     SELECT reporter.disable_materialized_simple_record_trigger();
232     SELECT reporter.enable_materialized_simple_record_trigger();
233 $$ LANGUAGE SQL;
234
235 CREATE OR REPLACE VIEW reporter.demographic AS
236 SELECT  u.id,
237         u.dob,
238         CASE
239                 WHEN u.dob IS NULL
240                         THEN 'Adult'
241                 WHEN AGE(u.dob) > '18 years'::INTERVAL
242                         THEN 'Adult'
243                 ELSE 'Juvenile'
244         END AS general_division
245   FROM  actor.usr u;
246
247 CREATE OR REPLACE VIEW reporter.circ_type AS
248 SELECT  id,
249         CASE WHEN opac_renewal OR phone_renewal OR desk_renewal OR auto_renewal
250                 THEN 'RENEWAL'
251                 ELSE 'CHECKOUT'
252         END AS "type"
253   FROM  action.circulation;
254
255 -- rhrr needs to be a real table, so it can be fast. To that end, we use
256 -- a materialized view updated via a trigger.
257 CREATE TABLE reporter.hold_request_record  AS
258 SELECT  id,
259         target,
260         hold_type,
261         CASE
262                 WHEN hold_type = 'T'
263                         THEN target
264                 WHEN hold_type = 'I'
265                         THEN (SELECT ssub.record_entry FROM serial.subscription ssub JOIN serial.issuance si ON (si.subscription = ssub.id) WHERE si.id = ahr.target)
266                 WHEN hold_type = 'V'
267                         THEN (SELECT cn.record FROM asset.call_number cn WHERE cn.id = ahr.target)
268                 WHEN hold_type IN ('C','R','F')
269                         THEN (SELECT cn.record FROM asset.call_number cn JOIN asset.copy cp ON (cn.id = cp.call_number) WHERE cp.id = ahr.target)
270                 WHEN hold_type = 'M'
271                         THEN (SELECT mr.master_record FROM metabib.metarecord mr WHERE mr.id = ahr.target)
272                 WHEN hold_type = 'P'
273                         THEN (SELECT bmp.record FROM biblio.monograph_part bmp WHERE bmp.id = ahr.target)
274         END AS bib_record
275   FROM  action.hold_request ahr;
276
277 CREATE UNIQUE INDEX reporter_hold_request_record_pkey_idx ON reporter.hold_request_record (id);
278 CREATE INDEX reporter_hold_request_record_bib_record_idx ON reporter.hold_request_record (bib_record);
279
280 ALTER TABLE reporter.hold_request_record ADD PRIMARY KEY USING INDEX reporter_hold_request_record_pkey_idx;
281
282 CREATE OR REPLACE FUNCTION reporter.hold_request_record_mapper () RETURNS TRIGGER AS $$
283 BEGIN
284     IF TG_OP = 'INSERT' THEN
285         INSERT INTO reporter.hold_request_record (id, target, hold_type, bib_record)
286         SELECT  NEW.id,
287                 NEW.target,
288                 NEW.hold_type,
289                 CASE
290                     WHEN NEW.hold_type = 'T'
291                         THEN NEW.target
292                     WHEN NEW.hold_type = 'I'
293                         THEN (SELECT ssub.record_entry FROM serial.subscription ssub JOIN serial.issuance si ON (si.subscription = ssub.id) WHERE si.id = NEW.target)
294                     WHEN NEW.hold_type = 'V'
295                         THEN (SELECT cn.record FROM asset.call_number cn WHERE cn.id = NEW.target)
296                     WHEN NEW.hold_type IN ('C','R','F')
297                         THEN (SELECT cn.record FROM asset.call_number cn JOIN asset.copy cp ON (cn.id = cp.call_number) WHERE cp.id = NEW.target)
298                     WHEN NEW.hold_type = 'M'
299                         THEN (SELECT mr.master_record FROM metabib.metarecord mr WHERE mr.id = NEW.target)
300                     WHEN NEW.hold_type = 'P'
301                         THEN (SELECT bmp.record FROM biblio.monograph_part bmp WHERE bmp.id = NEW.target)
302                 END AS bib_record;
303     ELSIF TG_OP = 'UPDATE' AND (OLD.target <> NEW.target OR OLD.hold_type <> NEW.hold_type) THEN
304         UPDATE  reporter.hold_request_record
305           SET   target = NEW.target,
306                 hold_type = NEW.hold_type,
307                 bib_record = CASE
308                     WHEN NEW.hold_type = 'T'
309                         THEN NEW.target
310                     WHEN NEW.hold_type = 'I'
311                         THEN (SELECT ssub.record_entry FROM serial.subscription ssub JOIN serial.issuance si ON (si.subscription = ssub.id) WHERE si.id = NEW.target)
312                     WHEN NEW.hold_type = 'V'
313                         THEN (SELECT cn.record FROM asset.call_number cn WHERE cn.id = NEW.target)
314                     WHEN NEW.hold_type IN ('C','R','F')
315                         THEN (SELECT cn.record FROM asset.call_number cn JOIN asset.copy cp ON (cn.id = cp.call_number) WHERE cp.id = NEW.target)
316                     WHEN NEW.hold_type = 'M'
317                         THEN (SELECT mr.master_record FROM metabib.metarecord mr WHERE mr.id = NEW.target)
318                     WHEN NEW.hold_type = 'P'
319                         THEN (SELECT bmp.record FROM biblio.monograph_part bmp WHERE bmp.id = NEW.target)
320                 END
321          WHERE  id = NEW.id;
322     END IF;
323     RETURN NEW;
324 END;
325 $$ LANGUAGE PLPGSQL;
326
327 CREATE TRIGGER reporter_hold_request_record_trigger AFTER INSERT OR UPDATE ON action.hold_request
328     FOR EACH ROW EXECUTE PROCEDURE reporter.hold_request_record_mapper();
329
330 CREATE OR REPLACE VIEW reporter.xact_billing_totals AS
331 SELECT  b.xact,
332         SUM( CASE WHEN b.voided THEN 0 ELSE amount END ) as unvoided,
333         SUM( CASE WHEN b.voided THEN amount ELSE 0 END ) as voided,
334         SUM( amount ) as total
335   FROM  money.billing b
336   GROUP BY 1;
337
338 CREATE OR REPLACE VIEW reporter.xact_paid_totals AS
339 SELECT  b.xact,
340         SUM( CASE WHEN b.voided THEN 0 ELSE amount END ) as unvoided,
341         SUM( CASE WHEN b.voided THEN amount ELSE 0 END ) as voided,
342         SUM( amount ) as total
343   FROM  money.payment b
344   GROUP BY 1;
345
346 CREATE OR REPLACE VIEW reporter.overdue_circs AS
347 SELECT  *
348   FROM  "action".circulation
349   WHERE checkin_time is null
350         AND (stop_fines NOT IN ('LOST','CLAIMSRETURNED') OR stop_fines IS NULL)
351         AND due_date < now();
352
353 CREATE OR REPLACE VIEW reporter.overdue_reports AS
354  SELECT s.id, c.barcode AS runner_barcode, r.name, s.run_time, s.run_time - now() AS scheduled_wait_time
355    FROM reporter.schedule s
356    JOIN reporter.report r ON r.id = s.report
357    JOIN actor.usr u ON s.runner = u.id
358    JOIN actor.card c ON c.id = u.card
359   WHERE s.start_time IS NULL AND s.run_time < now();
360
361 CREATE OR REPLACE VIEW reporter.pending_reports AS
362  SELECT s.id, c.barcode AS runner_barcode, r.name, s.run_time, s.run_time - now() AS scheduled_wait_time
363    FROM reporter.schedule s
364    JOIN reporter.report r ON r.id = s.report
365    JOIN actor.usr u ON s.runner = u.id
366    JOIN actor.card c ON c.id = u.card
367   WHERE s.start_time IS NULL;
368
369 CREATE OR REPLACE VIEW reporter.currently_running AS
370  SELECT s.id, c.barcode AS runner_barcode, r.name, s.run_time, s.run_time - now() AS scheduled_wait_time
371    FROM reporter.schedule s
372    JOIN reporter.report r ON r.id = s.report
373    JOIN actor.usr u ON s.runner = u.id
374    JOIN actor.card c ON c.id = u.card
375   WHERE s.start_time IS NOT NULL AND s.complete_time IS NULL;
376
377 COMMIT;
378