
This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "Evergreen ILS". The branch, main has been updated via 7d48946e9136cdd9d41c5c6a985ca5fe40fa0f9e (commit) via c51e15d942d130be66368abdcd3410952905d3fe (commit) via e5bf0d5cf03a937d07a2aa655ed1b94a0b19af9c (commit) via a5782c3799f35e2c293f0fc918e04e9e2892754a (commit) via d2ba29fb316af3b95b540fcf1eb2a2580e48ec3e (commit) via bc7b40bf7499ad7ffc66b936ed482726077b6a55 (commit) via 09063e7029a89b6baf624d77a5f2836c7a189efb (commit) from 42b93884386f01540f1e3c172187d6a02c03c5fd (commit) Those revisions listed above that are new to this repository have not appeared on any other notification email; so we list those revisions in full, below. - Log ----------------------------------------------------------------- commit 7d48946e9136cdd9d41c5c6a985ca5fe40fa0f9e Author: Mike Rylander <mrylander@gmail.com> Date: Thu Mar 20 14:11:57 2025 -0400 Stamping upgrade script Signed-off-by: Mike Rylander <mrylander@gmail.com> diff --git a/Open-ILS/src/sql/Pg/002.schema.config.sql b/Open-ILS/src/sql/Pg/002.schema.config.sql index e450796117..a1cb7926e2 100644 --- a/Open-ILS/src/sql/Pg/002.schema.config.sql +++ b/Open-ILS/src/sql/Pg/002.schema.config.sql @@ -92,7 +92,7 @@ CREATE TRIGGER no_overlapping_deps BEFORE INSERT OR UPDATE ON config.db_patch_dependencies FOR EACH ROW EXECUTE PROCEDURE evergreen.array_overlap_check ('deprecates'); -INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1463', :eg_version); -- sleary/jetheridge/rdavis/tmccanna +INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1464', :eg_version); -- sandbergja/miker CREATE TABLE config.bib_source ( id SERIAL PRIMARY KEY, diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.lasso_item_count_sums.sql b/Open-ILS/src/sql/Pg/upgrade/1464.schema.lasso_item_count_sums.sql similarity index 99% rename from Open-ILS/src/sql/Pg/upgrade/XXXX.schema.lasso_item_count_sums.sql rename to Open-ILS/src/sql/Pg/upgrade/1464.schema.lasso_item_count_sums.sql index f71c513c8f..c5b8ee7969 100644 --- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.lasso_item_count_sums.sql +++ b/Open-ILS/src/sql/Pg/upgrade/1464.schema.lasso_item_count_sums.sql @@ -1,6 +1,6 @@ BEGIN; --- SELECT evergreen.upgrade_deps_block_check('xxxx', :eg_version); +SELECT evergreen.upgrade_deps_block_check('1464', :eg_version); -- If you want to know how many items are available in a particular library group, -- you can't easily sum the results of, say, asset.staff_lasso_record_copy_count, commit c51e15d942d130be66368abdcd3410952905d3fe Author: Jane Sandberg <sandbergja@gmail.com> Date: Sun Feb 2 21:31:38 2025 -0800 LP2019439: Fix copy counts for records when user searches via Library Group This allows the record copy table's pagination to correctly calculate the Previous/Next buttons. To test on a box with the concerto data set: 1. Apply this patch 2. Edit /etc/apache2/sites-enabled/eg.conf to add physical_loc 9 (which is BM1). 3. Restart apache 4. Add a new global Library Group 5. Add mappings for your library group to BR1 and BM1. 6. Search for a fiction title in the OPAC 7. Open the record. 8. Confirm that the next button shows up in the copy table. Signed-off-by: Jane Sandberg <sandbergja@gmail.com> Sponsored-by: PaILS Signed-off-by: Elizabeth Davis <elizabeth.davis@sparkpa.org> Signed-off-by: Mike Rylander <mrylander@gmail.com> diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm index 83b4ca7c4e..9e96b0ad6b 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm @@ -374,6 +374,55 @@ sub record_id_to_copy_count { return [ sort { $a->{depth} <=> $b->{depth} } @count ]; } + +__PACKAGE__->register_method( + method => 'copy_total', + api_name => 'open-ils.search.biblio.record.copy_total', + signature => { + desc => 'returns a total of all public items on a record at the specified orgs and library groups', + params => [ + {desc => 'Record ID', type => 'number'}, + {desc => 'Org unit IDs', type => 'arrayref'}, + {desc => 'Org unit depth', type => 'number'}, + {desc => 'Library group IDs', type => 'arrayref'}, + ], + return => { + desc => 'total of all public items on the record', + type => 'bool' + } + } +); + +__PACKAGE__->register_method( + method => 'copy_total', + api_name => 'open-ils.search.biblio.record.copy_total.staff', + signature => { + desc => 'returns a total of all staff-visible items on a record at the specified orgs and library groups', + params => [ + {desc => 'Record ID', type => 'number'}, + {desc => 'Org unit IDs', type => 'arrayref'}, + {desc => 'Org unit depth', type => 'number'}, + {desc => 'Library group IDs', type => 'arrayref'}, + ], + return => { + desc => 'total of all staff-visible items on the record', + type => 'bool' + } + } +); + +sub copy_total { + my ($self, $client, $record_id, $org_unit_ids, $org_unit_depth, $library_group_ids) = @_; + my $total_function = $self->api_name =~ /staff/ ? 'asset.staff_copy_total' : 'asset.opac_copy_total'; + return new_editor->json_query( + {from => [$total_function => + $record_id, + '{' . join(',', @$org_unit_ids) . '}', + $org_unit_depth, + '{' . join(',', @$library_group_ids) . '}']} + )->[0]->{$total_function}; +} + __PACKAGE__->register_method( method => "record_has_holdable_copy", api_name => "open-ils.search.biblio.record.has_holdable_copy", diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Record.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Record.pm index 2d68a8f44b..fe827afad0 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Record.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Record.pm @@ -116,8 +116,7 @@ sub load_record { flesh => '{holdings_xml,bmp,mra,acp,acnp,acns}', site => $org_name, depth => $depth, - pref_lib => $pref_ou, - library_group => $ctx->{search_lasso} + pref_lib => $pref_ou }); $self->timelog("past get_records_and_facets()"); @@ -592,6 +591,10 @@ sub get_hold_copy_summary { my ($group_counts) = $search->request("$copy_count_meth.lasso", $org, $rec_id, $ctx->{search_lasso})->recv->content; unshift @{$self->ctx->{copy_summary}}, $group_counts->[0]; } + my $total_item_method = 'open-ils.search.biblio.record.copy_total'; + $total_item_method .= '.staff' if ($ctx->{is_staff}); + my $lassos = $ctx->{search_lasso} ? [$ctx->{search_lasso}] : []; + $self->ctx->{total_copies} = $search->request($total_item_method, $rec_id, [$org], $self->ctx->{copy_depth}, $lassos)->recv->content; # if org unit hiding applies, limit the hold count to holds # whose pickup library is within our depth-scoped tree diff --git a/Open-ILS/src/sql/Pg/040.schema.asset.sql b/Open-ILS/src/sql/Pg/040.schema.asset.sql index c4ce82262f..10b8b60acd 100644 --- a/Open-ILS/src/sql/Pg/040.schema.asset.sql +++ b/Open-ILS/src/sql/Pg/040.schema.asset.sql @@ -998,6 +998,39 @@ CREATE OR REPLACE FUNCTION asset.opac_lasso_metarecord_copy_count_sum(lasso_id I WHERE mmsm.metarecord = metarecord_id; $$ LANGUAGE SQL STABLE ROWS 1; +CREATE OR REPLACE FUNCTION asset.copy_org_ids(org_units INT[], depth INT, library_groups INT[]) +RETURNS TABLE (id INT) +AS $$ +DECLARE + ancestor INT; +BEGIN + RETURN QUERY SELECT org_unit FROM actor.org_lasso_map WHERE lasso = ANY(library_groups); + FOR ancestor IN SELECT unnest(org_units) + LOOP + RETURN QUERY + SELECT d.id + FROM actor.org_unit_descendants(ancestor, depth) d; + END LOOP; + RETURN; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION asset.staff_copy_total(rec_id INT, org_units INT[], depth INT, library_groups INT[]) +RETURNS INT AS $$ + SELECT COUNT(cp.id) total + FROM asset.copy cp + INNER JOIN asset.call_number cn ON (cn.id = cp.call_number AND NOT cn.deleted AND cn.record = rec_id) + INNER JOIN asset.copy_location cl ON (cp.location = cl.id AND NOT cl.deleted) + WHERE cp.circ_lib = ANY (SELECT asset.copy_org_ids(org_units, depth, library_groups)) + AND NOT cp.deleted; +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION asset.opac_copy_total(rec_id INT, org_units INT[], depth INT, library_groups INT[]) +RETURNS INT AS $$ +BEGIN +RAISE 'Not implemented'; +END; +$$ LANGUAGE PLPGSQL; CREATE OR REPLACE FUNCTION asset.metarecord_has_holdable_copy ( rid BIGINT, ou INT DEFAULT NULL) RETURNS BOOL AS $f$ BEGIN diff --git a/Open-ILS/src/sql/Pg/300.schema.staged_search.sql b/Open-ILS/src/sql/Pg/300.schema.staged_search.sql index f58aa4a199..8474baea13 100644 --- a/Open-ILS/src/sql/Pg/300.schema.staged_search.sql +++ b/Open-ILS/src/sql/Pg/300.schema.staged_search.sql @@ -2347,6 +2347,19 @@ CREATE OR REPLACE FUNCTION asset.opac_lasso_record_copy_count_sum(lasso_id INT, END; $$ LANGUAGE PLPGSQL STABLE ROWS 1; +CREATE OR REPLACE FUNCTION asset.opac_copy_total(rec_id INT, org_units INT[], depth INT, library_groups INT[]) +RETURNS INT AS $$ + SELECT COUNT(cp.id) total + FROM asset.copy cp + INNER JOIN asset.call_number cn ON (cn.id = cp.call_number AND NOT cn.deleted AND cn.record = rec_id) + INNER JOIN asset.copy_location cl ON (cp.location = cl.id AND NOT cl.deleted) + INNER JOIN asset.copy_vis_attr_cache av ON (cp.id = av.target_copy AND av.record = rec_id) + JOIN LATERAL (SELECT c_attrs FROM asset.patron_default_visibility_mask()) AS mask ON TRUE + WHERE av.vis_attr_vector @@ mask.c_attrs::query_int + AND cp.circ_lib = ANY (SELECT asset.copy_org_ids(org_units, depth, library_groups)) + AND NOT cp.deleted; +$$ LANGUAGE SQL; + CREATE TRIGGER maintain_symspell_entries_tgr AFTER INSERT OR UPDATE OR DELETE ON metabib.title_field_entry FOR EACH ROW EXECUTE PROCEDURE search.symspell_maintain_entries(); diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.lasso_item_count_sums.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.lasso_item_count_sums.sql index 817566b6a5..f71c513c8f 100644 --- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.lasso_item_count_sums.sql +++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.lasso_item_count_sums.sql @@ -84,6 +84,48 @@ CREATE OR REPLACE FUNCTION asset.opac_lasso_metarecord_copy_count_sum(lasso_id I WHERE mmsm.metarecord = metarecord_id; $$ LANGUAGE SQL STABLE ROWS 1; + +CREATE OR REPLACE FUNCTION asset.copy_org_ids(org_units INT[], depth INT, library_groups INT[]) +RETURNS TABLE (id INT) +AS $$ +DECLARE + ancestor INT; +BEGIN + RETURN QUERY SELECT org_unit FROM actor.org_lasso_map WHERE lasso = ANY(library_groups); + FOR ancestor IN SELECT unnest(org_units) + LOOP + RETURN QUERY + SELECT d.id + FROM actor.org_unit_descendants(ancestor, depth) d; + END LOOP; + RETURN; +END; +$$ LANGUAGE plpgsql; + + +CREATE OR REPLACE FUNCTION asset.staff_copy_total(rec_id INT, org_units INT[], depth INT, library_groups INT[]) +RETURNS INT AS $$ + SELECT COUNT(cp.id) total + FROM asset.copy cp + INNER JOIN asset.call_number cn ON (cn.id = cp.call_number AND NOT cn.deleted AND cn.record = rec_id) + INNER JOIN asset.copy_location cl ON (cp.location = cl.id AND NOT cl.deleted) + WHERE cp.circ_lib = ANY (SELECT asset.copy_org_ids(org_units, depth, library_groups)) + AND NOT cp.deleted; +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION asset.opac_copy_total(rec_id INT, org_units INT[], depth INT, library_groups INT[]) +RETURNS INT AS $$ + SELECT COUNT(cp.id) total + FROM asset.copy cp + INNER JOIN asset.call_number cn ON (cn.id = cp.call_number AND NOT cn.deleted AND cn.record = rec_id) + INNER JOIN asset.copy_location cl ON (cp.location = cl.id AND NOT cl.deleted) + INNER JOIN asset.copy_vis_attr_cache av ON (cp.id = av.target_copy AND av.record = rec_id) + JOIN LATERAL (SELECT c_attrs FROM asset.patron_default_visibility_mask()) AS mask ON TRUE + WHERE av.vis_attr_vector @@ mask.c_attrs::query_int + AND cp.circ_lib = ANY (SELECT asset.copy_org_ids(org_units, depth, library_groups)) + AND NOT cp.deleted; +$$ LANGUAGE SQL; + -- We are adding another argument, which means that we must delete functions with the old signature DROP FUNCTION IF EXISTS unapi.holdings_xml( bid BIGINT, diff --git a/Open-ILS/src/templates-bootstrap/opac/parts/record/copy_table.tt2 b/Open-ILS/src/templates-bootstrap/opac/parts/record/copy_table.tt2 index 53a61d38b6..7b4e132ad1 100755 --- a/Open-ILS/src/templates-bootstrap/opac/parts/record/copy_table.tt2 +++ b/Open-ILS/src/templates-bootstrap/opac/parts/record/copy_table.tt2 @@ -43,7 +43,6 @@ END; [%- IF has_copies or ctx.foreign_copies; depth = CGI.param('copy_depth').defined ? CGI.param('copy_depth') : CGI.param('depth').defined ? CGI.param('depth') : ctx.copy_summary.last.depth; - total_copies = ctx.copy_summary.$depth.count; %] [% IF ctx.geo_sort %] <form method="GET"> @@ -355,8 +354,7 @@ END; # FOREACH bib </tbody> </table> - - [% IF ctx.copy_limit < total_copies AND NOT serial_holdings %] + [% IF ctx.copy_limit < ctx.total_copies AND NOT serial_holdings %] <div class="row"> [%- IF ctx.copy_offset > 0; new_offset = ctx.copy_offset - ctx.copy_limit; @@ -366,7 +364,7 @@ END; # FOREACH bib l('Previous [_1]', ctx.copy_offset - new_offset) %]</a> </div> [%- END %] - [%- IF copies.size >= ctx.copy_limit AND (ctx.copy_offset + ctx.copy_limit < total_copies) %] + [%- IF copies.size >= ctx.copy_limit AND (ctx.copy_offset + ctx.copy_limit < ctx.total_copies) %] <div class="col text-right"> <a href="[% mkurl('', {copy_offset => ctx.copy_offset + ctx.copy_limit, copy_limit => ctx.copy_limit}) %]">[% l('Next [_1]', ctx.copy_limit) %] »</a> @@ -377,7 +375,7 @@ END; # FOREACH bib [% IF NOT serial_holdings -%] [%- more_copies_limit = 50 %] [%# TODO: config %] - [%- IF ctx.copy_limit != more_copies_limit AND copies.size >= ctx.copy_limit AND ctx.copy_limit < total_copies %] + [%- IF ctx.copy_limit != more_copies_limit AND copies.size >= ctx.copy_limit AND ctx.copy_limit < ctx.total_copies %] <div class="rdetail_show_copies"> <a href="[% mkurl('', {copy_limit => more_copies_limit, copy_offset => 0}) %]"><i class="fas fa-plus-square"></i> [% l('Show more copies') %]</a> </div> commit e5bf0d5cf03a937d07a2aa655ed1b94a0b19af9c Author: Jane Sandberg <sandbergja@gmail.com> Date: Sun Feb 2 22:46:32 2025 -0800 LP2019430: Release notes Signed-off-by: Jane Sandberg <sandbergja@gmail.com> Sponsored-by: PaILS Signed-off-by: Elizabeth Davis <elizabeth.davis@sparkpa.org> Signed-off-by: Mike Rylander <mrylander@gmail.com> diff --git a/docs/RELEASE_NOTES_NEXT/OPAC/library_group_counts_in_catalog.adoc b/docs/RELEASE_NOTES_NEXT/OPAC/library_group_counts_in_catalog.adoc new file mode 100644 index 0000000000..47549448ed --- /dev/null +++ b/docs/RELEASE_NOTES_NEXT/OPAC/library_group_counts_in_catalog.adoc @@ -0,0 +1,14 @@ +### Library Group Item Counts in catalog + +When a user selects a library group in the catalog (either the +public catalog or the staff catalog), the catalog now displays +the number of items held and available in that group. This +information supplements the existing holding statements +available in the catalog. + +If you have customized any of the following files, you will need +to update them to see the library group item counts. + +* opac/parts/misc_util.tt2 +* opac/parts/record/copy_counts.tt2 +* opac/parts/result/copy_counts.tt2 commit a5782c3799f35e2c293f0fc918e04e9e2892754a Author: Jane Sandberg <sandbergja@gmail.com> Date: Sun Feb 2 22:10:44 2025 -0800 LP2019430: Template Toolkit changes Signed-off-by: Jane Sandberg <sandbergja@gmail.com> Sponsored-by: PaILS Signed-off-by: Elizabeth Davis <elizabeth.davis@sparkpa.org> Signed-off-by: Mike Rylander <mrylander@gmail.com> diff --git a/Open-ILS/src/templates-bootstrap/opac/parts/misc_util.tt2 b/Open-ILS/src/templates-bootstrap/opac/parts/misc_util.tt2 index e44b078723..890b9b67cb 100755 --- a/Open-ILS/src/templates-bootstrap/opac/parts/misc_util.tt2 +++ b/Open-ILS/src/templates-bootstrap/opac/parts/misc_util.tt2 @@ -692,7 +692,7 @@ xpath = '//*[local-name()="counts"]/*[local-name()="count"][@type="' _ count_type _ '"]'; args.copy_counts = {}; FOR node IN xml.findnodes(xpath); - FOR attr IN ['count', 'available', 'unshadow', 'transcendant', 'org_unit']; + FOR attr IN ['count', 'available', 'unshadow', 'transcendant', 'org_unit', 'library_group']; depth = node.getAttribute('depth'); count_org_unit = node.getAttribute('org_unit'); args.copy_counts.$depth.$attr = node.getAttribute(attr); diff --git a/Open-ILS/src/templates-bootstrap/opac/parts/record/copy_counts.tt2 b/Open-ILS/src/templates-bootstrap/opac/parts/record/copy_counts.tt2 index 5d1bc1dec2..ecb0571db5 100755 --- a/Open-ILS/src/templates-bootstrap/opac/parts/record/copy_counts.tt2 +++ b/Open-ILS/src/templates-bootstrap/opac/parts/record/copy_counts.tt2 @@ -16,7 +16,14 @@ depth = depth + 1; NEXT; END; - ou_name = cp_org_unit.name; + library_group_name = ''; + FOREACH group IN ctx.lassos; + NEXT IF library_group_name; + IF group.id == ctx.copy_summary.$depth.library_group; + library_group_name = group.name; + END; + END; + ou_name = cp_org_unit.name || library_group_name; displayed_ous.$ou_name = 1; %] <li> diff --git a/Open-ILS/src/templates-bootstrap/opac/parts/result/copy_counts.tt2 b/Open-ILS/src/templates-bootstrap/opac/parts/result/copy_counts.tt2 index b198400bb7..7d7739bfb8 100755 --- a/Open-ILS/src/templates-bootstrap/opac/parts/result/copy_counts.tt2 +++ b/Open-ILS/src/templates-bootstrap/opac/parts/result/copy_counts.tt2 @@ -1,16 +1,23 @@ [%- depths = attrs.copy_counts.size; - depth = 0; + depth = -1; displayed_ous = {}; hiding_disabled = ctx.org_hiding_disabled(); WHILE depth < depths; org_unit = ctx.get_aou(attrs.copy_counts.$depth.org_unit); - ou_name = org_unit.name; + library_group_name = ''; + FOREACH group IN ctx.lassos; + NEXT IF library_group_name; + IF group.id == attrs.copy_counts.$depth.library_group; + library_group_name = group.name; + END; + END; + ou_name = org_unit.name || library_group_name; displayed_ous.$ou_name = 1; IF attrs.copy_counts.$depth.count > 0 AND ( hiding_disabled OR ctx.org_within_hiding_scope(org_unit.id)); %] <div class="result_count"> -[% IF ctx.get_aou(attrs.copy_counts.$depth.org_unit).opac_visible == 't' %] +[% IF ctx.get_aou(attrs.copy_counts.$depth.org_unit).opac_visible == 't' OR attrs.copy_counts.$depth.library_group %] [% l('[_1] of [quant,_2,copy,copies] available at [_3].', attrs.copy_counts.$depth.available, attrs.copy_counts.$depth.count, diff --git a/Open-ILS/src/templates/opac/parts/misc_util.tt2 b/Open-ILS/src/templates/opac/parts/misc_util.tt2 index e1a7f8d722..1de37d64d8 100644 --- a/Open-ILS/src/templates/opac/parts/misc_util.tt2 +++ b/Open-ILS/src/templates/opac/parts/misc_util.tt2 @@ -687,7 +687,7 @@ xpath = '//*[local-name()="counts"]/*[local-name()="count"][@type="' _ count_type _ '"]'; args.copy_counts = {}; FOR node IN xml.findnodes(xpath); - FOR attr IN ['count', 'available', 'unshadow', 'transcendant', 'org_unit']; + FOR attr IN ['count', 'available', 'unshadow', 'transcendant', 'org_unit', 'library_group']; depth = node.getAttribute('depth'); count_org_unit = node.getAttribute('org_unit'); args.copy_counts.$depth.$attr = node.getAttribute(attr); diff --git a/Open-ILS/src/templates/opac/parts/record/copy_counts.tt2 b/Open-ILS/src/templates/opac/parts/record/copy_counts.tt2 index e6e783f244..b86db6ed84 100644 --- a/Open-ILS/src/templates/opac/parts/record/copy_counts.tt2 +++ b/Open-ILS/src/templates/opac/parts/record/copy_counts.tt2 @@ -15,7 +15,14 @@ depth = depth + 1; NEXT; END; - ou_name = cp_org_unit.name; + library_group_name = ''; + FOREACH group IN ctx.lassos; + NEXT IF library_group_name; + IF group.id == ctx.copy_summary.$depth.library_group; + library_group_name = group.name; + END; + END; + ou_name = cp_org_unit.name || library_group_name; displayed_ous.$ou_name = 1; %] <li> diff --git a/Open-ILS/src/templates/opac/parts/result/copy_counts.tt2 b/Open-ILS/src/templates/opac/parts/result/copy_counts.tt2 index dc8aab797c..fa00e50bfd 100644 --- a/Open-ILS/src/templates/opac/parts/result/copy_counts.tt2 +++ b/Open-ILS/src/templates/opac/parts/result/copy_counts.tt2 @@ -1,16 +1,23 @@ [%- depths = attrs.copy_counts.size; - depth = 0; + depth = -1; displayed_ous = {}; hiding_disabled = ctx.org_hiding_disabled(); WHILE depth < depths; org_unit = ctx.get_aou(attrs.copy_counts.$depth.org_unit); - ou_name = org_unit.name; + library_group_name = ''; + FOREACH group IN ctx.lassos; + NEXT IF library_group_name; + IF group.id == attrs.copy_counts.$depth.library_group; + library_group_name = group.name; + END; + END; + ou_name = org_unit.name || library_group_name displayed_ous.$ou_name = 1; IF attrs.copy_counts.$depth.count > 0 AND ( hiding_disabled OR ctx.org_within_hiding_scope(org_unit.id)); %] <div class="result_count"> -[% IF ctx.get_aou(attrs.copy_counts.$depth.org_unit).opac_visible == 't' %] +[% IF ctx.get_aou(attrs.copy_counts.$depth.org_unit).opac_visible == 't' OR attrs.copy_counts.$depth.library_group %] [% l('[_1] of [quant,_2,copy,copies] available at [_3].', attrs.copy_counts.$depth.available, attrs.copy_counts.$depth.count, commit d2ba29fb316af3b95b540fcf1eb2a2580e48ec3e Author: Jane Sandberg <sandbergja@gmail.com> Date: Sun Jan 26 09:09:34 2025 -0800 LP2019430: Angular changes Signed-off-by: Jane Sandberg <sandbergja@gmail.com> Sponsored-by: PaILS Signed-off-by: Elizabeth Davis <elizabeth.davis@sparkpa.org> Signed-off-by: Mike Rylander <mrylander@gmail.com> diff --git a/Open-ILS/src/eg2/src/app/core/org.service.ts b/Open-ILS/src/eg2/src/app/core/org.service.ts index 0559fe0a0c..879dec442d 100644 --- a/Open-ILS/src/eg2/src/app/core/org.service.ts +++ b/Open-ILS/src/eg2/src/app/core/org.service.ts @@ -52,7 +52,10 @@ export class OrgService { } return nodeOrId as IdlObject; } - return this.orgMap[nodeOrId]; + if (nodeOrId > 0) { + return this.orgMap[nodeOrId]; + } + return null; } list(): IdlObject[] { diff --git a/Open-ILS/src/eg2/src/app/share/catalog/bib-record.service.spec.ts b/Open-ILS/src/eg2/src/app/share/catalog/bib-record.service.spec.ts index d94bd3187e..65f8797697 100644 --- a/Open-ILS/src/eg2/src/app/share/catalog/bib-record.service.spec.ts +++ b/Open-ILS/src/eg2/src/app/share/catalog/bib-record.service.spec.ts @@ -83,5 +83,17 @@ describe('BibRecordService', () => { expect(summary.recordNoteCount).toEqual(0); }); })); + it('can accept a library group id', waitForAsync(() => { + service.getBibSummary(248, 1, true, 15) + .subscribe(() => { + expect(mockNetService.request).toHaveBeenCalledWith( + 'open-ils.search', + 'open-ils.search.biblio.record.catalog_summary.staff', + 1, // org id + [248], // bib record ids + {library_group: 15} + ); + }); + })); }); }); diff --git a/Open-ILS/src/eg2/src/app/share/catalog/bib-record.service.ts b/Open-ILS/src/eg2/src/app/share/catalog/bib-record.service.ts index eb1bc33832..73fa257884 100644 --- a/Open-ILS/src/eg2/src/app/share/catalog/bib-record.service.ts +++ b/Open-ILS/src/eg2/src/app/share/catalog/bib-record.service.ts @@ -110,8 +110,10 @@ export class BibRecordService { } getBibSummary(id: number, - orgId?: number, isStaff?: boolean): Observable<BibRecordSummary> { - return this.getBibSummaries([id], orgId, isStaff); + orgId?: number, isStaff?: boolean, + library_group?: number): Observable<BibRecordSummary> { + const opts = library_group ? {library_group: library_group} : {}; + return this.getBibSummaries([id], orgId, isStaff, opts); } getBibSummaries(bibIds: number[], orgId?: number, diff --git a/Open-ILS/src/eg2/src/app/share/catalog/catalog.service.spec.ts b/Open-ILS/src/eg2/src/app/share/catalog/catalog.service.spec.ts new file mode 100644 index 0000000000..66bc0d166a --- /dev/null +++ b/Open-ILS/src/eg2/src/app/share/catalog/catalog.service.spec.ts @@ -0,0 +1,54 @@ +import { MockGenerators } from 'test_data/mock_generators'; +import { CatalogService } from './catalog.service'; +import { of } from 'rxjs'; +import { CatalogSearchContext } from './search-context'; +import { BibRecordService } from './bib-record.service'; + +describe('CatalogService', () => { + describe('fetchBibSummaries', () => { + it('passes library group information to the record service', async () => { + const mockBibNetService = MockGenerators.netService({ + 'open-ils.search.biblio.record.catalog_summary': of({ + record: MockGenerators.idlObject({id: 248, deleted: false}), + urls: [] + }) + }); + const service = new CatalogService( + MockGenerators.netService({ + 'open-ils.search.staff.location_groups_with_lassos': of(true) + }), + null, + null, + new BibRecordService( + mockBibNetService, + null, + MockGenerators.permService({PLACE_UNFILLABLE_HOLD: true}) + ), + null, + MockGenerators.serverStoreService(true) + ); + const context = new CatalogSearchContext(); + context.searchOrg = MockGenerators.idlObject({ + id: 300, + ou_type: MockGenerators.idlObject({ + depth: 2 + }) + }); + context.termSearch.locationGroupOrLasso = 'lasso(18)'; + context.resultIds = [248]; + context.pager.resultCount = 1; + context.pager.limit = 10; + context.prefOu = 300; + + await service.fetchBibSummaries(context); + + expect(mockBibNetService.request).toHaveBeenCalledWith( + 'open-ils.search', + 'open-ils.search.biblio.record.catalog_summary', + 300, // org id + [248], // bib record ids + {library_group: 18, pref_ou: 300} + ); + }); + }); +}); diff --git a/Open-ILS/src/eg2/src/app/share/catalog/catalog.service.ts b/Open-ILS/src/eg2/src/app/share/catalog/catalog.service.ts index c93be2f909..d9653f6922 100644 --- a/Open-ILS/src/eg2/src/app/share/catalog/catalog.service.ts +++ b/Open-ILS/src/eg2/src/app/share/catalog/catalog.service.ts @@ -1,11 +1,10 @@ /* eslint-disable */ /* eslint-disable no-empty, no-magic-numbers */ import {Injectable, EventEmitter} from '@angular/core'; -import {Observable} from 'rxjs'; +import {Observable, of} from 'rxjs'; import {map, tap, finalize} from 'rxjs/operators'; import {OrgService} from '@eg/core/org.service'; -import {UnapiService} from '@eg/share/catalog/unapi.service'; -import {IdlService, IdlObject} from '@eg/core/idl.service'; +import {IdlObject} from '@eg/core/idl.service'; import {NetService} from '@eg/core/net.service'; import {PcrudService} from '@eg/core/pcrud.service'; import {CatalogSearchContext, CatalogSearchState, CATALOG_CCVM_FILTERS} from './search-context'; @@ -35,10 +34,8 @@ export class CatalogService { onSearchComplete: EventEmitter<CatalogSearchContext>; constructor( - private idl: IdlService, private net: NetService, private org: OrgService, - private unapi: UnapiService, private pcrud: PcrudService, private bibService: BibRecordService, private basket: BasketService, @@ -235,6 +232,10 @@ export class CatalogService { options.pref_ou = ctx.prefOu; } + if (ctx.currentLasso()) { + options.library_group = ctx.currentLasso(); + } + if (isMeta) { observable = this.bibService.getMetabibSummaries( ctx.currentResultIds(), ctx.searchOrg.id(), ctx.isStaff, options); @@ -519,4 +520,20 @@ export class CatalogService { cbs.value, ctx.searchOrg.shortname(), cbs.limit, cbs.offset ).pipe(tap(result => ctx.searchState = CatalogSearchState.COMPLETE)); } + + orgOrLassoName(itemCount: any): Observable<string> { + if (itemCount.org_unit && itemCount.org_unit > 0) { + return of(this.org.get(itemCount.org_unit)?.shortname()); + } else if (itemCount.library_group) { + return this.getLasso(itemCount.library_group).pipe(map(lasso => lasso.name())); + } + return of(''); + } + + private getLasso(lassoId): Observable<IdlObject> { + if (this.libraryGroups.some((group) => group.id() == lassoId)) { + return of(this.libraryGroups.find((group) => group.id() == lassoId)); + } + return this.pcrud.retrieve('lasso', lassoId); + } } diff --git a/Open-ILS/src/eg2/src/app/share/catalog/search-context.ts b/Open-ILS/src/eg2/src/app/share/catalog/search-context.ts index f34c3b0ed3..8953bc5092 100644 --- a/Open-ILS/src/eg2/src/app/share/catalog/search-context.ts +++ b/Open-ILS/src/eg2/src/app/share/catalog/search-context.ts @@ -463,6 +463,13 @@ export class CatalogSearchContext { return ids; } + currentLasso(): number|null { + if (this.termSearch.locationGroupOrLasso?.includes('lasso')) { + return +this.termSearch.locationGroupOrLasso.match(/\d+/g)[0]; + } + return null; + } + addResultId(id: number, resultIdx: number ): void { this.resultIds[resultIdx + this.pager.offset] = Number(id); } diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.html index 1f495fdd16..a5252963d9 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.html +++ b/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.html @@ -169,7 +169,7 @@ </span> </div> <div class="float-start w-50"> - @ {{orgName(copyCount.org_unit)}} + @ {{orgName(copyCount) | async}} </div> </div> </div> diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.ts index e662afbdde..6dac842aaf 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.ts @@ -1,5 +1,5 @@ import {Component, OnInit, OnDestroy, Input, ViewChild} from '@angular/core'; -import {Subject, BehaviorSubject, Subscription, lastValueFrom, EMPTY} from 'rxjs'; +import {Subject, BehaviorSubject, Subscription, lastValueFrom, EMPTY, Observable} from 'rxjs'; import {catchError, takeUntil} from 'rxjs/operators'; import {Router} from '@angular/router'; import {OrgService} from '@eg/core/org.service'; @@ -112,8 +112,8 @@ export class ResultRecordComponent implements OnInit, OnDestroy { }); } - orgName(orgId: number): string { - return this.org.get(orgId)?.shortname(); + orgName(itemCount: any): Observable<string> { + return this.cat.orgOrLassoName(itemCount); } iconFormatLabel(code: string): string { diff --git a/Open-ILS/src/eg2/src/app/staff/share/bib-staff-view/bib-staff-view.component.html b/Open-ILS/src/eg2/src/app/staff/share/bib-staff-view/bib-staff-view.component.html index e69032d263..0175062d67 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/bib-staff-view/bib-staff-view.component.html +++ b/Open-ILS/src/eg2/src/app/staff/share/bib-staff-view/bib-staff-view.component.html @@ -208,7 +208,7 @@ <ul> <li><span i18n>{{summary.holdCount}} hold requests</span></li> <li *ngFor="let _count of summary.holdingsSummary"> - <span i18n>{{_count.available}} of {{_count.count}} copies available at {{orgName(_count.org_unit)}}.</span> + <span i18n>{{_count.available}} of {{_count.count}} copies available at {{orgName(_count) | async}}.</span> </li> </ul> </div> diff --git a/Open-ILS/src/eg2/src/app/staff/share/bib-staff-view/bib-staff-view.component.spec.ts b/Open-ILS/src/eg2/src/app/staff/share/bib-staff-view/bib-staff-view.component.spec.ts new file mode 100644 index 0000000000..eeeb525ff0 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/share/bib-staff-view/bib-staff-view.component.spec.ts @@ -0,0 +1,75 @@ +import { MockGenerators } from 'test_data/mock_generators'; +import { BibStaffViewComponent } from './bib-staff-view.component'; +import { BibRecordService } from '@eg/share/catalog/bib-record.service'; +import { of } from 'rxjs'; +import { CatalogSearchContext } from '@eg/share/catalog/search-context'; + +describe('BibStaffViewComponent', () => { + describe('loadSummary()', () => { + const mockBibNetService = MockGenerators.netService({ + 'open-ils.search.biblio.record.catalog_summary.staff': of({ + record: MockGenerators.idlObject({id: 248, deleted: false}), + urls: [], + copy_counts: [ + { + 'count': 18, + 'available': 18, + 'unshadow': 8, + 'org_unit': 1, + 'depth': 0, + 'transcendant': null + } + ] + }) + }); + + it('can fetch a summary', async () => { + const context = new CatalogSearchContext(); + context.searchOrg = MockGenerators.idlObject({id: 35}); + const component = new BibStaffViewComponent( + new BibRecordService( + mockBibNetService, + null, + MockGenerators.permService({PLACE_UNFILLABLE_HOLD: true}) + ), + null, + null, + null, + MockGenerators.staffCatService(context) + ); + component.recordId = 123; + + await component.loadSummary(); + + expect(component.summary.holdingsSummary[0].available).toEqual(18); + }); + it('can fetch a summary with a lasso', async () => { + const context = new CatalogSearchContext(); + context.searchOrg = MockGenerators.idlObject({id: 35}); + context.termSearch.locationGroupOrLasso = 'lasso(18)'; + + const component = new BibStaffViewComponent( + new BibRecordService( + mockBibNetService, + null, + MockGenerators.permService({PLACE_UNFILLABLE_HOLD: true}) + ), + null, + null, + null, + MockGenerators.staffCatService(context) + ); + component.recordId = 123; + + await component.loadSummary(); + + expect(mockBibNetService.request).toHaveBeenCalledWith( + 'open-ils.search', + 'open-ils.search.biblio.record.catalog_summary.staff', + 35, + [123], + {library_group: 18} + ); + }); + }); +}); diff --git a/Open-ILS/src/eg2/src/app/staff/share/bib-staff-view/bib-staff-view.component.ts b/Open-ILS/src/eg2/src/app/staff/share/bib-staff-view/bib-staff-view.component.ts index d32518d474..0fe1e46f5b 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/bib-staff-view/bib-staff-view.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/share/bib-staff-view/bib-staff-view.component.ts @@ -5,6 +5,7 @@ import {BibRecordService, BibRecordSummary import {ServerStoreService} from '@eg/core/server-store.service'; import {CatalogService} from '@eg/share/catalog/catalog.service'; import {StaffCatalogService} from '@eg/staff/catalog/catalog.service'; +import { firstValueFrom, Observable } from 'rxjs'; @Component({ selector: 'eg-bib-staff-view', @@ -70,21 +71,23 @@ export class BibStaffViewComponent implements OnInit { } loadSummary(): Promise<any> { - return this.bib.getBibSummary( + const summaryArgs: [number, number, boolean, number?] = [ this.recId, this.staffCat.searchContext.searchOrg.id(), - true // isStaff - ).toPromise() + true, // isStaff + ]; + if (this.staffCat.searchContext.currentLasso()) { + summaryArgs.push(this.staffCat.searchContext.currentLasso()); + } + return firstValueFrom(this.bib.getBibSummary(...summaryArgs)) .then(summary => { this.summary = summary; return summary.getBibCallNumber(); }); } - orgName(orgId: number): string { - if (orgId) { - return this.org.get(orgId).shortname(); - } + orgName(itemCount: any): Observable<string> { + return this.cat.orgOrLassoName(itemCount); } iconFormatLabel(code: string): string { diff --git a/Open-ILS/src/eg2/src/test_data/mock_generators.ts b/Open-ILS/src/eg2/src/test_data/mock_generators.ts index a24b92e974..8a9044ed6b 100644 --- a/Open-ILS/src/eg2/src/test_data/mock_generators.ts +++ b/Open-ILS/src/eg2/src/test_data/mock_generators.ts @@ -1,7 +1,12 @@ import { AuthService } from '@eg/core/auth.service'; import { IdlObject, IdlService } from '@eg/core/idl.service'; +import { NetService } from '@eg/core/net.service'; import { PcrudService } from '@eg/core/pcrud.service'; +import { PermService } from '@eg/core/perm.service'; +import { ServerStoreService } from '@eg/core/server-store.service'; import { StoreService } from '@eg/core/store.service'; +import { CatalogSearchContext } from '@eg/share/catalog/search-context'; +import { StaffCatalogService } from '@eg/staff/catalog/catalog.service'; import { SerialsService } from '@eg/staff/serials/serials.service'; import { of } from 'rxjs'; @@ -27,6 +32,27 @@ export class MockGenerators { return jasmine.createSpyObj<IdlService>(['getClassSelector'], {classes: classes}); } + // Use the method response map to say which OpenSRF methods + // you expect to call, and what the response should be. + // For example: + // {'opensrf.math.add', of(4)} + static netService(method_response_map: {}) { + const net = jasmine.createSpyObj<NetService>(['request']); + net.request.and.callFake((_service, method, _params) => { + if (method_response_map[method]) { + return method_response_map[method]; + } + return of(`OpenSRF method ${method} has not been mocked, returning this string instead`); + }); + return net; + } + + static permService(permissions_result: {}) { + const perm = jasmine.createSpyObj<PermService>(['hasWorkPermHere']); + perm.hasWorkPermHere.and.resolveTo(permissions_result); + return perm; + } + static pcrudService(returnValues: {[method: string]: any}) { const methods = ['search', 'retrieve', 'retrieveAll', 'create', 'update', 'remove']; const pcrud = jasmine.createSpyObj<PcrudService>(['search', 'retrieve', 'retrieveAll', 'create', 'update', 'remove']); @@ -46,6 +72,12 @@ export class MockGenerators { return store; } + static serverStoreService(valueFromStore: any) { + const store = jasmine.createSpyObj<ServerStoreService>(['getItem']); + store.getItem.and.resolveTo(valueFromStore); + return store; + } + static serialsService() { const methods = [ 'callNumberPrefixesAsComboboxEntries$', 'callNumbersAsComboboxEntries$', 'callNumberSuffixesAsComboboxEntries$', @@ -62,6 +94,12 @@ export class MockGenerators { return serials; } + static staffCatService(context: CatalogSearchContext) { + return jasmine.createSpyObj<StaffCatalogService>([], { + searchContext: context + }); + } + static orgService() { return {ancestors: () => []}; } commit bc7b40bf7499ad7ffc66b936ed482726077b6a55 Author: Jane Sandberg <sandbergja@gmail.com> Date: Sat Jan 25 13:11:27 2025 -0800 LP2019430: SQL changes Signed-off-by: Jane Sandberg <sandbergja@gmail.com> Sponsored-by: PaILS Signed-off-by: Elizabeth Davis <elizabeth.davis@sparkpa.org> Signed-off-by: Mike Rylander <mrylander@gmail.com> diff --git a/Open-ILS/src/sql/Pg/040.schema.asset.sql b/Open-ILS/src/sql/Pg/040.schema.asset.sql index 0e360e8fd1..c4ce82262f 100644 --- a/Open-ILS/src/sql/Pg/040.schema.asset.sql +++ b/Open-ILS/src/sql/Pg/040.schema.asset.sql @@ -750,6 +750,7 @@ BEGIN END; $f$ LANGUAGE PLPGSQL; + CREATE OR REPLACE FUNCTION asset.record_has_holdable_copy ( rid BIGINT, ou INT DEFAULT NULL) RETURNS BOOL AS $f$ BEGIN PERFORM 1 @@ -932,6 +933,72 @@ BEGIN END; $f$ LANGUAGE PLPGSQL; +-- If you want to know how many items are available in a particular library group, +-- you can't easily sum the results of, say, asset.staff_lasso_record_copy_count, +-- since library groups can theoretically include descendants of other org units +-- in the library group (for example, the group could include a system and a branch +-- within that same system), which means that certain items would be counted twice. +-- The following functions address that problem by providing deduplicated sums that +-- only count each item once. + +CREATE OR REPLACE FUNCTION asset.staff_lasso_record_copy_count_sum(lasso_id INT, record_id BIGINT) + RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT, library_group INT) AS $$ + BEGIN + IF (lasso_id IS NULL) THEN RETURN; END IF; + IF (record_id IS NULL) THEN RETURN; END IF; + RETURN QUERY SELECT + -1, + -1, + COUNT(cp.id), + SUM( CASE WHEN cp.status IN (SELECT id FROM config.copy_status WHERE holdable AND is_available) THEN 1 ELSE 0 END ), + SUM( CASE WHEN cl.opac_visible AND cp.opac_visible THEN 1 ELSE 0 END), + 0, + lasso_id + FROM ( SELECT DISTINCT descendants.id FROM actor.org_lasso_map aolmp JOIN LATERAL actor.org_unit_descendants(aolmp.org_unit) AS descendants ON TRUE WHERE aolmp.lasso = lasso_id) d + JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted) + JOIN asset.copy_location cl ON (cp.location = cl.id AND NOT cl.deleted) + JOIN asset.call_number cn ON (cn.record = record_id AND cn.id = cp.call_number AND NOT cn.deleted); + END; +$$ LANGUAGE PLPGSQL STABLE ROWS 1; + +CREATE OR REPLACE FUNCTION asset.opac_lasso_record_copy_count_sum(lasso_id INT, record_id BIGINT) + RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT, library_group INT) AS $$ + BEGIN + RAISE 'NOT IMPLEMENTED'; + END; +$$ LANGUAGE PLPGSQL STABLE ROWS 1; + +CREATE OR REPLACE FUNCTION asset.staff_lasso_metarecord_copy_count_sum(lasso_id INT, metarecord_id BIGINT) + RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT, library_group INT) AS $$ + SELECT ( + -1, + -1, + SUM(sums.visible)::bigint, + SUM(sums.available)::bigint, + SUM(sums.unshadow)::bigint, + MIN(sums.transcendant), + lasso_id + ) FROM metabib.metarecord_source_map mmsm + JOIN LATERAL (SELECT visible, available, unshadow, transcendant FROM asset.staff_lasso_record_copy_count_sum(lasso_id, mmsm.source)) sums ON TRUE + WHERE mmsm.metarecord = metarecord_id; +$$ LANGUAGE SQL STABLE ROWS 1; + +CREATE OR REPLACE FUNCTION asset.opac_lasso_metarecord_copy_count_sum(lasso_id INT, metarecord_id BIGINT) + RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT, library_group INT) AS $$ + SELECT ( + -1, + -1, + SUM(sums.visible)::bigint, + SUM(sums.available)::bigint, + SUM(sums.unshadow)::bigint, + MIN(sums.transcendant), + lasso_id + ) FROM metabib.metarecord_source_map mmsm + JOIN LATERAL (SELECT visible, available, unshadow, transcendant FROM asset.opac_lasso_record_copy_count_sum(lasso_id, mmsm.source)) sums ON TRUE + WHERE mmsm.metarecord = metarecord_id; +$$ LANGUAGE SQL STABLE ROWS 1; + + CREATE OR REPLACE FUNCTION asset.metarecord_has_holdable_copy ( rid BIGINT, ou INT DEFAULT NULL) RETURNS BOOL AS $f$ BEGIN PERFORM 1 diff --git a/Open-ILS/src/sql/Pg/300.schema.staged_search.sql b/Open-ILS/src/sql/Pg/300.schema.staged_search.sql index 2b10750966..f58aa4a199 100644 --- a/Open-ILS/src/sql/Pg/300.schema.staged_search.sql +++ b/Open-ILS/src/sql/Pg/300.schema.staged_search.sql @@ -2321,6 +2321,32 @@ BEGIN END; $f$ LANGUAGE PLPGSQL; +-- Defined here, rather than 040.asset, because it relies on +-- asset.patron_default_visibility_mask() being defined +CREATE OR REPLACE FUNCTION asset.opac_lasso_record_copy_count_sum(lasso_id INT, record_id BIGINT) + RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT, library_group INT) AS $$ + BEGIN + IF (lasso_id IS NULL) THEN RETURN; END IF; + IF (record_id IS NULL) THEN RETURN; END IF; + + RETURN QUERY SELECT + -1, + -1, + COUNT(cp.id), + SUM( CASE WHEN cp.status IN (SELECT id FROM config.copy_status WHERE holdable AND is_available) THEN 1 ELSE 0 END ), + SUM( CASE WHEN cl.opac_visible AND cp.opac_visible THEN 1 ELSE 0 END), + 0, + lasso_id + FROM ( SELECT DISTINCT descendants.id FROM actor.org_lasso_map aolmp JOIN LATERAL actor.org_unit_descendants(aolmp.org_unit) AS descendants ON TRUE WHERE aolmp.lasso = lasso_id) d + JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted) + JOIN asset.copy_location cl ON (cp.location = cl.id AND NOT cl.deleted) + JOIN asset.call_number cn ON (cn.record = record_id AND cn.id = cp.call_number AND NOT cn.deleted) + JOIN asset.copy_vis_attr_cache av ON (cp.id = av.target_copy AND av.record = record_id) + JOIN LATERAL (SELECT c_attrs FROM asset.patron_default_visibility_mask()) AS mask ON TRUE + WHERE av.vis_attr_vector @@ mask.c_attrs::query_int; + END; +$$ LANGUAGE PLPGSQL STABLE ROWS 1; + CREATE TRIGGER maintain_symspell_entries_tgr AFTER INSERT OR UPDATE OR DELETE ON metabib.title_field_entry FOR EACH ROW EXECUTE PROCEDURE search.symspell_maintain_entries(); diff --git a/Open-ILS/src/sql/Pg/990.schema.unapi.sql b/Open-ILS/src/sql/Pg/990.schema.unapi.sql index 5d6bd5da17..390cd30de6 100644 --- a/Open-ILS/src/sql/Pg/990.schema.unapi.sql +++ b/Open-ILS/src/sql/Pg/990.schema.unapi.sql @@ -306,7 +306,8 @@ CREATE OR REPLACE FUNCTION unapi.bre ( slimit HSTORE DEFAULT NULL, soffset HSTORE DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE, - pref_lib INT DEFAULT NULL + pref_lib INT DEFAULT NULL, + library_group INT DEFAULT NULL ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL STABLE; CREATE OR REPLACE FUNCTION unapi.mmr ( @@ -319,7 +320,8 @@ CREATE OR REPLACE FUNCTION unapi.mmr ( slimit HSTORE DEFAULT NULL, soffset HSTORE DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE, - pref_lib INT DEFAULT NULL + pref_lib INT DEFAULT NULL, + library_group INT DEFAULT NULL ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL STABLE; CREATE OR REPLACE FUNCTION unapi.bmp ( obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit HSTORE DEFAULT NULL, soffset HSTORE DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL STABLE; @@ -336,7 +338,8 @@ CREATE OR REPLACE FUNCTION unapi.holdings_xml ( slimit HSTORE DEFAULT NULL, soffset HSTORE DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE, - pref_lib INT DEFAULT NULL + pref_lib INT DEFAULT NULL, + library_group INT DEFAULT NULL ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL STABLE; @@ -349,13 +352,14 @@ CREATE OR REPLACE FUNCTION unapi.mmr_holdings_xml ( slimit HSTORE DEFAULT NULL, soffset HSTORE DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE, - pref_lib INT DEFAULT NULL + pref_lib INT DEFAULT NULL, + library_group INT DEFAULT NULL ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL STABLE; -CREATE OR REPLACE FUNCTION unapi.biblio_record_entry_feed ( id_list BIGINT[], format TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit HSTORE DEFAULT NULL, soffset HSTORE 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, pref_lib INT DEFAULT NULL ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL STABLE; +CREATE OR REPLACE FUNCTION unapi.biblio_record_entry_feed ( id_list BIGINT[], format TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit HSTORE DEFAULT NULL, soffset HSTORE 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, pref_lib INT DEFAULT NULL, library_group INT DEFAULT NULL ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL STABLE; -CREATE OR REPLACE FUNCTION unapi.metabib_virtual_record_feed ( id_list BIGINT[], format TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit HSTORE DEFAULT NULL, soffset HSTORE 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, pref_lib INT DEFAULT NULL ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL STABLE; +CREATE OR REPLACE FUNCTION unapi.metabib_virtual_record_feed ( id_list BIGINT[], format TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit HSTORE DEFAULT NULL, soffset HSTORE 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, pref_lib INT DEFAULT NULL, library_group INT DEFAULT NULL ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL STABLE; CREATE OR REPLACE FUNCTION unapi.memoize (classname TEXT, obj_id BIGINT, format TEXT, ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit HSTORE DEFAULT NULL, soffset HSTORE DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$ DECLARE @@ -384,7 +388,24 @@ BEGIN END; $F$ LANGUAGE PLPGSQL STABLE; -CREATE OR REPLACE FUNCTION unapi.biblio_record_entry_feed ( id_list BIGINT[], format TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit HSTORE DEFAULT NULL, soffset HSTORE 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, pref_lib INT DEFAULT NULL ) RETURNS XML AS $F$ +CREATE OR REPLACE FUNCTION unapi.biblio_record_entry_feed ( + id_list BIGINT[], + format TEXT, + includes TEXT[], + org TEXT, + depth INT DEFAULT NULL, + slimit HSTORE DEFAULT NULL, + soffset HSTORE 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, + pref_lib INT DEFAULT NULL, + library_group INT DEFAULT NULL + ) RETURNS XML AS $F$ DECLARE layout unapi.bre_output_layout%ROWTYPE; transform config.xml_transform%ROWTYPE; @@ -410,7 +431,7 @@ BEGIN xmlns_uri := COALESCE(transform.namespace_uri,xmlns_uri); -- Gather the bib xml - SELECT XMLAGG( unapi.bre(i, format, '', includes, org, depth, slimit, soffset, include_xmlns, pref_lib)) INTO tmp_xml FROM UNNEST( id_list ) i; + SELECT XMLAGG( unapi.bre(i, format, '', includes, org, depth, slimit, soffset, include_xmlns, pref_lib, library_group)) INTO tmp_xml FROM UNNEST( id_list ) i; IF layout.title_element IS NOT NULL THEN EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.title_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, title; @@ -443,7 +464,23 @@ BEGIN END; $F$ LANGUAGE PLPGSQL STABLE; -CREATE OR REPLACE FUNCTION unapi.metabib_virtual_record_feed ( id_list BIGINT[], format TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit HSTORE DEFAULT NULL, soffset HSTORE 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, pref_lib INT DEFAULT NULL ) RETURNS XML AS $F$ +CREATE OR REPLACE FUNCTION unapi.metabib_virtual_record_feed ( + id_list BIGINT[], + format TEXT, + includes TEXT[], + org TEXT, + depth INT DEFAULT NULL, + slimit HSTORE DEFAULT NULL, + soffset HSTORE 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, + pref_lib INT DEFAULT NULL, + library_group INT DEFAULT NULL ) RETURNS XML AS $F$ DECLARE layout unapi.bre_output_layout%ROWTYPE; transform config.xml_transform%ROWTYPE; @@ -469,7 +506,7 @@ BEGIN xmlns_uri := COALESCE(transform.namespace_uri,xmlns_uri); -- Gather the bib xml - SELECT XMLAGG( unapi.mmr(i, format, '', includes, org, depth, slimit, soffset, include_xmlns, pref_lib)) INTO tmp_xml FROM UNNEST( id_list ) i; + SELECT XMLAGG( unapi.mmr(i, format, '', includes, org, depth, slimit, soffset, include_xmlns, pref_lib, library_group)) INTO tmp_xml FROM UNNEST( id_list ) i; IF layout.title_element IS NOT NULL THEN EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.title_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, title; @@ -512,7 +549,8 @@ CREATE OR REPLACE FUNCTION unapi.bre ( slimit HSTORE DEFAULT NULL, soffset HSTORE DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE, - pref_lib INT DEFAULT NULL + pref_lib INT DEFAULT NULL, + library_group INT DEFAULT NULL ) RETURNS XML AS $F$ DECLARE @@ -539,7 +577,7 @@ BEGIN END IF; IF format = 'holdings_xml' THEN -- the special case - output := unapi.holdings_xml( obj_id, ouid, org, depth, includes, slimit, soffset, include_xmlns); + output := unapi.holdings_xml( obj_id, ouid, org, depth, includes, slimit, soffset, include_xmlns, NULL, library_group); RETURN output; END IF; @@ -569,7 +607,7 @@ BEGIN -- grab holdings if we need them IF ('holdings_xml' = ANY (includes)) THEN - hxml := unapi.holdings_xml(obj_id, ouid, org, depth, array_remove(includes,'holdings_xml'), slimit, soffset, include_xmlns, pref_lib); + hxml := unapi.holdings_xml(obj_id, ouid, org, depth, array_remove(includes,'holdings_xml'), slimit, soffset, include_xmlns, pref_lib, library_group); ELSE hxml := NULL::XML; END IF; @@ -659,7 +697,8 @@ CREATE OR REPLACE FUNCTION unapi.holdings_xml ( slimit HSTORE DEFAULT NULL, soffset HSTORE DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE, - pref_lib INT DEFAULT NULL + pref_lib INT DEFAULT NULL, + library_group INT DEFAULT NULL ) RETURNS XML AS $F$ SELECT XMLELEMENT( @@ -689,6 +728,18 @@ RETURNS XML AS $F$ XMLATTRIBUTES('pref_lib' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow) )::text FROM asset.opac_ou_record_copy_count($9, $1) + UNION + SELECT XMLELEMENT( + name count, + XMLATTRIBUTES('public' as type, depth, library_group, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow) + )::text + FROM asset.opac_lasso_record_copy_count_sum(library_group, $1) + UNION + SELECT XMLELEMENT( + name count, + XMLATTRIBUTES('staff' as type, depth, library_group, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow) + )::text + FROM asset.staff_lasso_record_copy_count_sum(library_group, $1) ORDER BY 1 )x) ), @@ -1585,7 +1636,8 @@ CREATE OR REPLACE FUNCTION unapi.mmr_holdings_xml ( slimit HSTORE DEFAULT NULL, soffset HSTORE DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE, - pref_lib INT DEFAULT NULL + pref_lib INT DEFAULT NULL, + library_group INT DEFAULT NULL ) RETURNS XML AS $F$ SELECT XMLELEMENT( @@ -1615,6 +1667,18 @@ RETURNS XML AS $F$ XMLATTRIBUTES('pref_lib' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow) )::text FROM asset.opac_ou_metarecord_copy_count($9, $1) + UNION + SELECT XMLELEMENT( + name count, + XMLATTRIBUTES('public' as type, depth, library_group, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow) + )::text + FROM asset.opac_lasso_metarecord_copy_count_sum(library_group, $1) + UNION + SELECT XMLELEMENT( + name count, + XMLATTRIBUTES('staff' as type, depth, library_group, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow) + )::text + FROM asset.staff_lasso_metarecord_copy_count_sum(library_group, $1) ORDER BY 1 )x) ), @@ -1654,7 +1718,8 @@ CREATE OR REPLACE FUNCTION unapi.mmr ( slimit HSTORE DEFAULT NULL, soffset HSTORE DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE, - pref_lib INT DEFAULT NULL + pref_lib INT DEFAULT NULL, + library_group INT DEFAULT NULL ) RETURNS XML AS $F$ DECLARE @@ -1703,7 +1768,7 @@ BEGIN output := unapi.mmr_holdings_xml( obj_id, ouid, org, depth, array_remove(includes,'holdings_xml'), - slimit, soffset, include_xmlns, pref_lib); + slimit, soffset, include_xmlns, pref_lib, library_group); RETURN output; END IF; @@ -1731,7 +1796,7 @@ BEGIN hxml := unapi.mmr_holdings_xml( obj_id, ouid, org, depth, array_remove(includes,'holdings_xml'), - slimit, soffset, include_xmlns, pref_lib); + slimit, soffset, include_xmlns, pref_lib, library_group); END IF; subxml := NULL::XML; diff --git a/Open-ILS/src/sql/Pg/live_t/unapi-holdings.pg b/Open-ILS/src/sql/Pg/live_t/unapi-holdings.pg new file mode 100644 index 0000000000..540005ab30 --- /dev/null +++ b/Open-ILS/src/sql/Pg/live_t/unapi-holdings.pg @@ -0,0 +1,47 @@ +BEGIN; + +SELECT plan(4); + +SELECT is( + (XPATH( + '//ns:count[@type="staff" and @org_unit="4"]/@count', + unapi.holdings_xml(243, 4, 'BR1', 2), + '{{ns,http://open-ils.org/spec/holdings/v1}}' + ))[1]::TEXT, + '10', + 'unapi.holdings_xml includes the staff count for the branch BR1' +); + +SELECT is( + XPATH_EXISTS( + '//ns:count[@type="staff" and @library_group="1000002"]/@count', + unapi.holdings_xml(243, 4, 'BR1', 2), + '{{ns,http://open-ils.org/spec/holdings/v1}}' + ), + FALSE, + 'unapi.holdings_xml does not include item counts for library groups if not requested' +); + + +SELECT is( + (XPATH( + '//ns:count[@type="staff" and @org_unit="4"]/@count', + unapi.holdings_xml(243, 4, 'BR1', 2, NULL::TEXT[], NULL, NULL, TRUE, NULL, 1000002), + '{{ns,http://open-ils.org/spec/holdings/v1}}' + ))[1]::TEXT, + '10', + 'unapi.holdings_xml includes the staff count for the branch BR1 when lasso is passed' +); + +SELECT is( + (XPATH( + '//ns:count[@type="staff" and @library_group="1000002"]/@count', + unapi.holdings_xml(243, 4, 'BR1', 2, NULL::TEXT[], NULL, NULL, TRUE, NULL, 1000002), + '{{ns,http://open-ils.org/spec/holdings/v1}}' + ))[1]::TEXT, + '2', + 'unapi.holdings_xml includes the staff count for the library_group 1000002 when lasso is passed' +); + + +ROLLBACK; diff --git a/Open-ILS/src/sql/Pg/t/library_group_copy_count_sums.pg b/Open-ILS/src/sql/Pg/t/library_group_copy_count_sums.pg new file mode 100644 index 0000000000..128e425731 --- /dev/null +++ b/Open-ILS/src/sql/Pg/t/library_group_copy_count_sums.pg @@ -0,0 +1,105 @@ +BEGIN; +INSERT INTO biblio.record_entry(marc, last_xact_id) VALUES ('<record/>', 'asset.opac_lasso_record_copy_count test'); +INSERT INTO biblio.record_entry(marc, last_xact_id) VALUES ('<record/>', 'asset.opac_lasso_record_copy_count test - another matching record on the same metarecord'); + +WITH + record AS (SELECT MAX(id) AS id FROM biblio.record_entry), + library AS (SELECT MAX(ou.id) AS id FROM actor.org_unit ou INNER JOIN actor.org_unit_type aout ON ou.ou_type = aout.id WHERE aout.can_have_vols), + editor AS (SELECT MAX(id) AS id FROM actor.usr) +INSERT INTO asset.call_number(record, creator, editor, owning_lib, label, label_class) SELECT + record.id, + editor.id, + editor.id, + library.id, + 'asset.opac_lasso_record_copy_count test', + 1 + FROM record, library, editor; + +WITH + call_number AS (SELECT id FROM asset.call_number cn + INNER JOIN (SELECT MAX(id) AS max_id FROM biblio.record_entry) AS max_record + ON cn.record = max_record.max_id LIMIT 1), + library AS (SELECT MAX(ou.id) AS id FROM actor.org_unit ou INNER JOIN actor.org_unit_type aout ON ou.ou_type = aout.id WHERE aout.can_have_vols), + editor AS (SELECT MAX(id) AS id FROM actor.usr) +INSERT INTO asset.copy (call_number, circ_lib, creator, editor, loan_duration, fine_level, barcode, opac_visible) + SELECT call_number.id, + library.id, + editor.id, + editor.id, + 1 AS loan_duration, + 1 AS fine_level, + md5(random()::text) AS barcode, + TRUE AS opac_visible + FROM call_number, library, editor + UNION ALL SELECT call_number.id, + library.id, + editor.id, + editor.id, + 1 AS loan_duration, + 1 AS fine_level, + md5(random()::text) AS barcode, + FALSE AS opac_visible + FROM call_number, library, editor + ; + +WITH new_org_lasso AS (INSERT INTO actor.org_lasso (name, global) VALUES ('New Lasso', TRUE) RETURNING id), + library AS (SELECT MAX(ou.id) AS max_id FROM actor.org_unit ou INNER JOIN actor.org_unit_type aout ON ou.ou_type = aout.id WHERE aout.can_have_vols) +INSERT INTO actor.org_lasso_map(lasso, org_unit) SELECT (SELECT id FROM new_org_lasso), library.max_id FROM library; + +SELECT plan(6); + +SELECT is_empty('SELECT * FROM asset.staff_lasso_record_copy_count_sum(NULL, NULL)', 'returns no rows if you pass in NULL'); + +PREPARE staff_lasso_counts AS + WITH record AS (SELECT MAX(id) AS id FROM biblio.record_entry), + lasso AS (SELECT id FROM actor.org_lasso WHERE name = 'New Lasso') + SELECT depth, visible, available, unshadow FROM asset.staff_lasso_record_copy_count_sum( + (SELECT id FROM lasso), + (SELECT id FROM record)); + +SELECT results_eq('staff_lasso_counts', + $$VALUES (-1::int, 2::bigint, 2::bigint, 1::bigint)$$, + 'asset.staff_lasso_record_copy_count_sum includes depth=-1, visible, available, and unshadow columns' +); + +SELECT is_empty('SELECT * FROM asset.opac_lasso_record_copy_count_sum(NULL, NULL)', 'returns no rows if you pass in NULL'); + +PREPARE opac_lasso_counts AS + WITH record AS (SELECT MAX(id) AS id FROM biblio.record_entry), + lasso AS (SELECT id FROM actor.org_lasso WHERE name = 'New Lasso') + SELECT depth, visible, available, unshadow FROM asset.opac_lasso_record_copy_count_sum( + (SELECT id FROM lasso), + (SELECT id FROM record)); + +SELECT results_eq('opac_lasso_counts', + $$VALUES (-1::int, 1::bigint, 1::bigint, 1::bigint)$$, + 'asset.opac_lasso_record_copy_count_sum includes depth=-1, visible, available, and unshadow columns' +); + + +PREPARE staff_metarecord_lasso_counts AS + WITH metarecord AS (SELECT metarecord FROM metabib.metarecord_source_map ORDER BY id DESC LIMIT 1), + lasso AS (SELECT id FROM actor.org_lasso WHERE name = 'New Lasso') + SELECT depth, visible, available, unshadow FROM asset.staff_lasso_metarecord_copy_count_sum( + (SELECT id FROM lasso), + (SELECT metarecord FROM metarecord)); + +SELECT results_eq('staff_metarecord_lasso_counts', + $$VALUES (-1::int, 2::bigint, 2::bigint, 1::bigint)$$, + 'asset.staff_lasso_metarecord_copy_count_sum includes depth=-1, visible, available, and unshadow columns' +); + +PREPARE opac_metarecord_lasso_counts AS + WITH metarecord AS (SELECT metarecord FROM metabib.metarecord_source_map ORDER BY id DESC LIMIT 1), + lasso AS (SELECT id FROM actor.org_lasso WHERE name = 'New Lasso') + SELECT depth, visible, available, unshadow FROM asset.opac_lasso_metarecord_copy_count_sum( + (SELECT id FROM lasso), + (SELECT metarecord FROM metarecord)); + +SELECT results_eq('opac_metarecord_lasso_counts', + $$VALUES (-1::int, 1::bigint, 1::bigint, 1::bigint)$$, + 'asset.opac_lasso_metarecord_copy_count_sum includes depth=-1, visible, available, and unshadow columns' +); + +SELECT * FROM finish(); +ROLLBACK; diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.lasso_item_count_sums.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.lasso_item_count_sums.sql new file mode 100644 index 0000000000..817566b6a5 --- /dev/null +++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.lasso_item_count_sums.sql @@ -0,0 +1,821 @@ +BEGIN; + +-- SELECT evergreen.upgrade_deps_block_check('xxxx', :eg_version); + +-- If you want to know how many items are available in a particular library group, +-- you can't easily sum the results of, say, asset.staff_lasso_record_copy_count, +-- since library groups can theoretically include descendants of other org units +-- in the library group (for example, the group could include a system and a branch +-- within that same system), which means that certain items would be counted twice. +-- The following functions address that problem by providing deduplicated sums that +-- only count each item once. + +CREATE OR REPLACE FUNCTION asset.staff_lasso_record_copy_count_sum(lasso_id INT, record_id BIGINT) + RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT, library_group INT) AS $$ + BEGIN + IF (lasso_id IS NULL) THEN RETURN; END IF; + IF (record_id IS NULL) THEN RETURN; END IF; + RETURN QUERY SELECT + -1, + -1, + COUNT(cp.id), + SUM( CASE WHEN cp.status IN (SELECT id FROM config.copy_status WHERE holdable AND is_available) THEN 1 ELSE 0 END ), + SUM( CASE WHEN cl.opac_visible AND cp.opac_visible THEN 1 ELSE 0 END), + 0, + lasso_id + FROM ( SELECT DISTINCT descendants.id FROM actor.org_lasso_map aolmp JOIN LATERAL actor.org_unit_descendants(aolmp.org_unit) AS descendants ON TRUE WHERE aolmp.lasso = lasso_id) d + JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted) + JOIN asset.copy_location cl ON (cp.location = cl.id AND NOT cl.deleted) + JOIN asset.call_number cn ON (cn.record = record_id AND cn.id = cp.call_number AND NOT cn.deleted); + END; +$$ LANGUAGE PLPGSQL STABLE ROWS 1; + +CREATE OR REPLACE FUNCTION asset.opac_lasso_record_copy_count_sum(lasso_id INT, record_id BIGINT) + RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT, library_group INT) AS $$ + BEGIN + IF (lasso_id IS NULL) THEN RETURN; END IF; + IF (record_id IS NULL) THEN RETURN; END IF; + + RETURN QUERY SELECT + -1, + -1, + COUNT(cp.id), + SUM( CASE WHEN cp.status IN (SELECT id FROM config.copy_status WHERE holdable AND is_available) THEN 1 ELSE 0 END ), + SUM( CASE WHEN cl.opac_visible AND cp.opac_visible THEN 1 ELSE 0 END), + 0, + lasso_id + FROM ( SELECT DISTINCT descendants.id FROM actor.org_lasso_map aolmp JOIN LATERAL actor.org_unit_descendants(aolmp.org_unit) AS descendants ON TRUE WHERE aolmp.lasso = lasso_id) d + JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted) + JOIN asset.copy_location cl ON (cp.location = cl.id AND NOT cl.deleted) + JOIN asset.call_number cn ON (cn.record = record_id AND cn.id = cp.call_number AND NOT cn.deleted) + JOIN asset.copy_vis_attr_cache av ON (cp.id = av.target_copy AND av.record = record_id) + JOIN LATERAL (SELECT c_attrs FROM asset.patron_default_visibility_mask()) AS mask ON TRUE + WHERE av.vis_attr_vector @@ mask.c_attrs::query_int; + END; +$$ LANGUAGE PLPGSQL STABLE ROWS 1; + +CREATE OR REPLACE FUNCTION asset.staff_lasso_metarecord_copy_count_sum(lasso_id INT, metarecord_id BIGINT) + RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT, library_group INT) AS $$ + SELECT ( + -1, + -1, + SUM(sums.visible)::bigint, + SUM(sums.available)::bigint, + SUM(sums.unshadow)::bigint, + MIN(sums.transcendant), + lasso_id + ) FROM metabib.metarecord_source_map mmsm + JOIN LATERAL (SELECT visible, available, unshadow, transcendant FROM asset.staff_lasso_record_copy_count_sum(lasso_id, mmsm.source)) sums ON TRUE + WHERE mmsm.metarecord = metarecord_id; +$$ LANGUAGE SQL STABLE ROWS 1; + +CREATE OR REPLACE FUNCTION asset.opac_lasso_metarecord_copy_count_sum(lasso_id INT, metarecord_id BIGINT) + RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT, library_group INT) AS $$ + SELECT ( + -1, + -1, + SUM(sums.visible)::bigint, + SUM(sums.available)::bigint, + SUM(sums.unshadow)::bigint, + MIN(sums.transcendant), + lasso_id + ) FROM metabib.metarecord_source_map mmsm + JOIN LATERAL (SELECT visible, available, unshadow, transcendant FROM asset.opac_lasso_record_copy_count_sum(lasso_id, mmsm.source)) sums ON TRUE + WHERE mmsm.metarecord = metarecord_id; +$$ LANGUAGE SQL STABLE ROWS 1; + +-- We are adding another argument, which means that we must delete functions with the old signature +DROP FUNCTION IF EXISTS unapi.holdings_xml( + bid BIGINT, + ouid INT, + org TEXT, + depth INT, + includes TEXT[], + slimit HSTORE, + soffset HSTORE, + include_xmlns BOOL, + pref_lib INT); + +CREATE OR REPLACE FUNCTION unapi.holdings_xml ( + bid BIGINT, + ouid INT, + org TEXT, + depth INT DEFAULT NULL, + includes TEXT[] DEFAULT NULL::TEXT[], + slimit HSTORE DEFAULT NULL, + soffset HSTORE DEFAULT NULL, + include_xmlns BOOL DEFAULT TRUE, + pref_lib INT DEFAULT NULL, + library_group INT DEFAULT NULL +) +RETURNS XML AS $F$ + SELECT XMLELEMENT( + name holdings, + XMLATTRIBUTES( + CASE WHEN $8 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns, + CASE WHEN ('bre' = ANY ($5)) THEN 'tag:open-ils.org:U2@bre/' || $1 || '/' || $3 ELSE NULL END AS id, + (SELECT record_has_holdable_copy FROM asset.record_has_holdable_copy($1)) AS has_holdable + ), + XMLELEMENT( + name counts, + (SELECT XMLAGG(XMLELEMENT::XML) FROM ( + SELECT XMLELEMENT( + name count, + XMLATTRIBUTES('public' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow) + )::text + FROM asset.opac_ou_record_copy_count($2, $1) + UNION + SELECT XMLELEMENT( + name count, + XMLATTRIBUTES('staff' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow) + )::text + FROM asset.staff_ou_record_copy_count($2, $1) + UNION + SELECT XMLELEMENT( + name count, + XMLATTRIBUTES('pref_lib' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow) + )::text + FROM asset.opac_ou_record_copy_count($9, $1) + UNION + SELECT XMLELEMENT( + name count, + XMLATTRIBUTES('public' as type, depth, library_group, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow) + )::text + FROM asset.opac_lasso_record_copy_count_sum(library_group, $1) + UNION + SELECT XMLELEMENT( + name count, + XMLATTRIBUTES('staff' as type, depth, library_group, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow) + )::text + FROM asset.staff_lasso_record_copy_count_sum(library_group, $1) + ORDER BY 1 + )x) + ), + CASE + WHEN ('bmp' = ANY ($5)) THEN + XMLELEMENT( + name monograph_parts, + (SELECT XMLAGG(bmp) FROM ( + SELECT unapi.bmp( id, 'xml', 'monograph_part', array_remove( array_remove($5,'bre'), 'holdings_xml'), $3, $4, $6, $7, FALSE) + FROM biblio.monograph_part + WHERE NOT deleted AND record = $1 + )x) + ) + ELSE NULL + END, + XMLELEMENT( + name volumes, + (SELECT XMLAGG(acn ORDER BY rank, name, label_sortkey) FROM ( + -- Physical copies + SELECT unapi.acn(y.id,'xml','volume',array_remove( array_remove($5,'holdings_xml'),'bre'), $3, $4, $6, $7, FALSE), y.rank, name, label_sortkey + FROM evergreen.ranked_volumes($1, $2, $4, $6, $7, $9, $5) AS y + UNION ALL + -- Located URIs + SELECT unapi.acn(uris.id,'xml','volume',array_remove( array_remove($5,'holdings_xml'),'bre'), $3, $4, $6, $7, FALSE), uris.rank, name, label_sortkey + FROM evergreen.located_uris($1, $2, $9) AS uris + )x) + ), + CASE WHEN ('ssub' = ANY ($5)) THEN + XMLELEMENT( + name subscriptions, + (SELECT XMLAGG(ssub) FROM ( + SELECT unapi.ssub(id,'xml','subscription','{}'::TEXT[], $3, $4, $6, $7, FALSE) + FROM serial.subscription + WHERE record_entry = $1 + )x) + ) + ELSE NULL END, + CASE WHEN ('acp' = ANY ($5)) THEN + XMLELEMENT( + name foreign_copies, + (SELECT XMLAGG(acp) FROM ( + SELECT unapi.acp(p.target_copy,'xml','copy',array_remove($5,'acp'), $3, $4, $6, $7, FALSE) + FROM biblio.peer_bib_copy_map p + JOIN asset.copy c ON (p.target_copy = c.id) + WHERE NOT c.deleted AND p.peer_record = $1 + LIMIT ($6 -> 'acp')::INT + OFFSET ($7 -> 'acp')::INT + )x) + ) + ELSE NULL END + ); +$F$ LANGUAGE SQL STABLE; + +DROP FUNCTION IF EXISTS unapi.bre ( + obj_id BIGINT, + format TEXT, + ename TEXT, + includes TEXT[], + org TEXT, + depth INT, + slimit HSTORE, + soffset HSTORE, + include_xmlns BOOL, + pref_lib INT); + +CREATE OR REPLACE FUNCTION unapi.bre ( + obj_id BIGINT, + format TEXT, + ename TEXT, + includes TEXT[], + org TEXT, + depth INT DEFAULT NULL, + slimit HSTORE DEFAULT NULL, + soffset HSTORE DEFAULT NULL, + include_xmlns BOOL DEFAULT TRUE, + pref_lib INT DEFAULT NULL, + library_group INT DEFAULT NULL +) +RETURNS XML AS $F$ +DECLARE + me biblio.record_entry%ROWTYPE; + layout unapi.bre_output_layout%ROWTYPE; + xfrm config.xml_transform%ROWTYPE; + ouid INT; + tmp_xml TEXT; + top_el TEXT; + output XML; + hxml XML; + axml XML; + source XML; +BEGIN + + IF org = '-' OR org IS NULL THEN + SELECT shortname INTO org FROM evergreen.org_top(); + END IF; + + SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org; + + IF ouid IS NULL THEN + RETURN NULL::XML; + END IF; + + IF format = 'holdings_xml' THEN -- the special case + output := unapi.holdings_xml( obj_id, ouid, org, depth, includes, slimit, soffset, include_xmlns, NULL, library_group); + RETURN output; + END IF; + + SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format; + + IF layout.name IS NULL THEN + RETURN NULL::XML; + END IF; + + SELECT * INTO xfrm FROM config.xml_transform WHERE name = layout.transform; + + SELECT * INTO me FROM biblio.record_entry WHERE id = obj_id; + + -- grab bib_source, if any + IF ('cbs' = ANY (includes) AND me.source IS NOT NULL) THEN + source := unapi.cbs(me.source,NULL,NULL,NULL,NULL); + ELSE + source := NULL::XML; + END IF; + + -- grab SVF if we need them + IF ('mra' = ANY (includes)) THEN + axml := unapi.mra(obj_id,NULL,NULL,NULL,NULL); + ELSE + axml := NULL::XML; + END IF; + + -- grab holdings if we need them + IF ('holdings_xml' = ANY (includes)) THEN + hxml := unapi.holdings_xml(obj_id, ouid, org, depth, array_remove(includes,'holdings_xml'), slimit, soffset, include_xmlns, pref_lib, library_group); + ELSE + hxml := NULL::XML; + END IF; + + + -- generate our item node + + + IF format = 'marcxml' THEN + tmp_xml := me.marc; + IF tmp_xml !~ E'<marc:' THEN -- If we're not using the prefixed namespace in this record, then remove all declarations of it + tmp_xml := REGEXP_REPLACE(tmp_xml, ' xmlns:marc="http://www.loc.gov/MARC21/slim"', '', 'g'); + END IF; + ELSE + tmp_xml := oils_xslt_process(me.marc, xfrm.xslt)::XML; + END IF; + + top_el := REGEXP_REPLACE(tmp_xml, E'^.*?<((?:\\S+:)?' || layout.holdings_element || ').*$', E'\\1'); + + IF source IS NOT NULL THEN + tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', source || '</' || top_el || E'>\\1'); + END IF; + + IF axml IS NOT NULL THEN + tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', axml || '</' || top_el || E'>\\1'); + END IF; + + IF hxml IS NOT NULL THEN -- XXX how do we configure the holdings position? + tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', hxml || '</' || top_el || E'>\\1'); + END IF; + + IF ('bre.unapi' = ANY (includes)) THEN + output := REGEXP_REPLACE( + tmp_xml, + '</' || top_el || '>(.*?)', + XMLELEMENT( + name abbr, + XMLATTRIBUTES( + 'http://www.w3.org/1999/xhtml' AS xmlns, + 'unapi-id' AS class, + 'tag:open-ils.org:U2@bre/' || obj_id || '/' || org AS title + ) + )::TEXT || '</' || top_el || E'>\\1' + ); + ELSE + output := tmp_xml; + END IF; + + IF ('bre.extern' = ANY (includes)) THEN + output := REGEXP_REPLACE( + tmp_xml, + '</' || top_el || '>(.*?)', + XMLELEMENT( + name extern, + XMLATTRIBUTES( + 'http://open-ils.org/spec/biblio/v1' AS xmlns, + me.creator AS creator, + me.editor AS editor, + me.create_date AS create_date, + me.edit_date AS edit_date, + me.quality AS quality, + me.fingerprint AS fingerprint, + me.tcn_source AS tcn_source, + me.tcn_value AS tcn_value, + me.owner AS owner, + me.share_depth AS share_depth, + me.active AS active, + me.deleted AS deleted + ) + )::TEXT || '</' || top_el || E'>\\1' + ); + ELSE + output := tmp_xml; + END IF; + + output := REGEXP_REPLACE(output::TEXT,E'>\\s+<','><','gs')::XML; + RETURN output; +END; +$F$ LANGUAGE PLPGSQL STABLE; + +DROP FUNCTION IF EXISTS unapi.biblio_record_entry_feed ( + id_list BIGINT[], + format TEXT, + includes TEXT[], + org TEXT, + depth INT, + slimit HSTORE, + soffset HSTORE, + include_xmlns BOOL, + title TEXT, + description TEXT, + creator TEXT, + update_ts TEXT, + unapi_url TEXT, + header_xml XML, + pref_lib INT); +CREATE OR REPLACE FUNCTION unapi.biblio_record_entry_feed ( + id_list BIGINT[], + format TEXT, + includes TEXT[], + org TEXT, + depth INT DEFAULT NULL, + slimit HSTORE DEFAULT NULL, + soffset HSTORE 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, + pref_lib INT DEFAULT NULL, + library_group INT DEFAULT NULL + ) RETURNS XML AS $F$ +DECLARE + layout unapi.bre_output_layout%ROWTYPE; + transform config.xml_transform%ROWTYPE; + item_format TEXT; + tmp_xml TEXT; + xmlns_uri TEXT := 'http://open-ils.org/spec/feed-xml/v1'; + ouid INT; + element_list TEXT[]; +BEGIN + + IF org = '-' OR org IS NULL THEN + SELECT shortname INTO org FROM evergreen.org_top(); + END IF; + + SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org; + SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format; + + IF layout.name IS NULL THEN + RETURN NULL::XML; + END IF; + + SELECT * INTO transform FROM config.xml_transform WHERE name = layout.transform; + xmlns_uri := COALESCE(transform.namespace_uri,xmlns_uri); + + -- Gather the bib xml + SELECT XMLAGG( unapi.bre(i, format, '', includes, org, depth, slimit, soffset, include_xmlns, pref_lib, library_group)) INTO tmp_xml FROM UNNEST( id_list ) i; + + IF layout.title_element IS NOT NULL THEN + EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.title_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, title; + END IF; + + IF layout.description_element IS NOT NULL THEN + EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.description_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, description; + END IF; + + IF layout.creator_element IS NOT NULL THEN + EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.creator_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, creator; + END IF; + + IF layout.update_ts_element IS NOT NULL THEN + EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.update_ts_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, update_ts; + END IF; + + IF unapi_url IS NOT NULL THEN + 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; + END IF; + + IF header_xml IS NOT NULL THEN tmp_xml := XMLCONCAT(header_xml,tmp_xml::XML); END IF; + + element_list := regexp_split_to_array(layout.feed_top,E'\\.'); + FOR i IN REVERSE ARRAY_UPPER(element_list, 1) .. 1 LOOP + EXECUTE 'SELECT XMLELEMENT( name '|| quote_ident(element_list[i]) ||', XMLATTRIBUTES( $1 AS xmlns), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML; + END LOOP; + + RETURN tmp_xml::XML; +END; +$F$ LANGUAGE PLPGSQL STABLE; + + +DROP FUNCTION IF EXISTS unapi.metabib_virtual_record_feed ( + id_list BIGINT[], + format TEXT, + includes TEXT[], + org TEXT, + depth INT, + slimit HSTORE, + soffset HSTORE, + include_xmlns BOOL, title TEXT, + description TEXT, + creator TEXT, + update_ts TEXT, + unapi_url TEXT, + header_xml XML, + pref_lib INT); +CREATE OR REPLACE FUNCTION unapi.metabib_virtual_record_feed ( + id_list BIGINT[], + format TEXT, + includes TEXT[], + org TEXT, + depth INT DEFAULT NULL, + slimit HSTORE DEFAULT NULL, + soffset HSTORE 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, + pref_lib INT DEFAULT NULL, + library_group INT DEFAULT NULL ) RETURNS XML AS $F$ +DECLARE + layout unapi.bre_output_layout%ROWTYPE; + transform config.xml_transform%ROWTYPE; + item_format TEXT; + tmp_xml TEXT; + xmlns_uri TEXT := 'http://open-ils.org/spec/feed-xml/v1'; + ouid INT; + element_list TEXT[]; +BEGIN + + IF org = '-' OR org IS NULL THEN + SELECT shortname INTO org FROM evergreen.org_top(); + END IF; + + SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org; + SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format; + + IF layout.name IS NULL THEN + RETURN NULL::XML; + END IF; + + SELECT * INTO transform FROM config.xml_transform WHERE name = layout.transform; + xmlns_uri := COALESCE(transform.namespace_uri,xmlns_uri); + + -- Gather the bib xml + SELECT XMLAGG( unapi.mmr(i, format, '', includes, org, depth, slimit, soffset, include_xmlns, pref_lib, library_group)) INTO tmp_xml FROM UNNEST( id_list ) i; + + IF layout.title_element IS NOT NULL THEN + EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.title_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, title; + END IF; + + IF layout.description_element IS NOT NULL THEN + EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.description_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, description; + END IF; + + IF layout.creator_element IS NOT NULL THEN + EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.creator_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, creator; + END IF; + + IF layout.update_ts_element IS NOT NULL THEN + EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.update_ts_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, update_ts; + END IF; + + IF unapi_url IS NOT NULL THEN + 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; + END IF; + + IF header_xml IS NOT NULL THEN tmp_xml := XMLCONCAT(header_xml,tmp_xml::XML); END IF; + + element_list := regexp_split_to_array(layout.feed_top,E'\\.'); + FOR i IN REVERSE ARRAY_UPPER(element_list, 1) .. 1 LOOP + EXECUTE 'SELECT XMLELEMENT( name '|| quote_ident(element_list[i]) ||', XMLATTRIBUTES( $1 AS xmlns), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML; + END LOOP; + + RETURN tmp_xml::XML; +END; +$F$ LANGUAGE PLPGSQL STABLE; + + +DROP FUNCTION IF EXISTS unapi.mmr( + obj_id BIGINT, + format TEXT, + ename TEXT, + includes TEXT[], + org TEXT, + depth INT, + slimit HSTORE, + soffset HSTORE, + include_xmlns BOOL, + pref_lib INT +); + +CREATE OR REPLACE FUNCTION unapi.mmr ( + obj_id BIGINT, + format TEXT, + ename TEXT, + includes TEXT[], + org TEXT, + depth INT DEFAULT NULL, + slimit HSTORE DEFAULT NULL, + soffset HSTORE DEFAULT NULL, + include_xmlns BOOL DEFAULT TRUE, + pref_lib INT DEFAULT NULL, + library_group INT DEFAULT NULL +) +RETURNS XML AS $F$ +DECLARE + mmrec metabib.metarecord%ROWTYPE; + leadrec biblio.record_entry%ROWTYPE; + subrec biblio.record_entry%ROWTYPE; + layout unapi.bre_output_layout%ROWTYPE; + xfrm config.xml_transform%ROWTYPE; + ouid INT; + xml_buf TEXT; -- growing XML document + tmp_xml TEXT; -- single-use XML string + xml_frag TEXT; -- single-use XML fragment + top_el TEXT; + output XML; + hxml XML; + axml XML; + subxml XML; -- subordinate records elements + sub_xpath TEXT; + parts TEXT[]; +BEGIN + + -- xpath for extracting bre.marc values from subordinate records + -- so they may be appended to the MARC of the master record prior + -- to XSLT processing. + -- subjects, isbn, issn, upc -- anything else? + sub_xpath := + '//*[starts-with(@tag, "6") or @tag="020" or @tag="022" or @tag="024"]'; + + IF org = '-' OR org IS NULL THEN + SELECT shortname INTO org FROM evergreen.org_top(); + END IF; + + SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org; + + IF ouid IS NULL THEN + RETURN NULL::XML; + END IF; + + SELECT INTO mmrec * FROM metabib.metarecord WHERE id = obj_id; + IF NOT FOUND THEN + RETURN NULL::XML; + END IF; + + -- TODO: aggregate holdings from constituent records + IF format = 'holdings_xml' THEN -- the special case + output := unapi.mmr_holdings_xml( + obj_id, ouid, org, depth, + array_remove(includes,'holdings_xml'), + slimit, soffset, include_xmlns, pref_lib, library_group); + RETURN output; + END IF; + + SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format; + + IF layout.name IS NULL THEN + RETURN NULL::XML; + END IF; + + SELECT * INTO xfrm FROM config.xml_transform WHERE name = layout.transform; + + SELECT INTO leadrec * FROM biblio.record_entry WHERE id = mmrec.master_record; + + -- Grab distinct MVF for all records if requested + IF ('mra' = ANY (includes)) THEN + axml := unapi.mmr_mra(obj_id,NULL,NULL,NULL,org,depth,NULL,NULL,TRUE,pref_lib); + ELSE + axml := NULL::XML; + END IF; + + xml_buf = leadrec.marc; + + hxml := NULL::XML; + IF ('holdings_xml' = ANY (includes)) THEN + hxml := unapi.mmr_holdings_xml( + obj_id, ouid, org, depth, + array_remove(includes,'holdings_xml'), + slimit, soffset, include_xmlns, pref_lib, library_group); + END IF; + + subxml := NULL::XML; + parts := '{}'::TEXT[]; + FOR subrec IN SELECT bre.* FROM biblio.record_entry bre + JOIN metabib.metarecord_source_map mmsm ON (mmsm.source = bre.id) + JOIN metabib.metarecord mmr ON (mmr.id = mmsm.metarecord) + WHERE mmr.id = obj_id AND NOT bre.deleted + ORDER BY CASE WHEN bre.id = mmr.master_record THEN 0 ELSE bre.id END + LIMIT COALESCE((slimit->'bre')::INT, 5) LOOP + + IF subrec.id = leadrec.id THEN CONTINUE; END IF; + -- Append choice data from the the non-lead records to the + -- the lead record document + + parts := parts || xpath(sub_xpath, subrec.marc::XML)::TEXT[]; + END LOOP; + + SELECT STRING_AGG( DISTINCT p , '' )::XML INTO subxml FROM UNNEST(parts) p; + + -- append data from the subordinate records to the + -- main record document before applying the XSLT + + IF subxml IS NOT NULL THEN + xml_buf := REGEXP_REPLACE(xml_buf, + '</record>(.*?)$', subxml || '</record>' || E'\\1'); + END IF; + + IF format = 'marcxml' THEN + -- If we're not using the prefixed namespace in + -- this record, then remove all declarations of it + IF xml_buf !~ E'<marc:' THEN + xml_buf := REGEXP_REPLACE(xml_buf, + ' xmlns:marc="http://www.loc.gov/MARC21/slim"', '', 'g'); + END IF; + ELSE + xml_buf := oils_xslt_process(xml_buf, xfrm.xslt)::XML; + END IF; + + -- update top_el to reflect the change in xml_buf, which may + -- now be a different type of document (e.g. record -> mods) + top_el := REGEXP_REPLACE(xml_buf, E'^.*?<((?:\\S+:)?' || + layout.holdings_element || ').*$', E'\\1'); + + IF axml IS NOT NULL THEN + xml_buf := REGEXP_REPLACE(xml_buf, + '</' || top_el || '>(.*?)$', axml || '</' || top_el || E'>\\1'); + END IF; + + IF hxml IS NOT NULL THEN + xml_buf := REGEXP_REPLACE(xml_buf, + '</' || top_el || '>(.*?)$', hxml || '</' || top_el || E'>\\1'); + END IF; + + IF ('mmr.unapi' = ANY (includes)) THEN + output := REGEXP_REPLACE( + xml_buf, + '</' || top_el || '>(.*?)', + XMLELEMENT( + name abbr, + XMLATTRIBUTES( + 'http://www.w3.org/1999/xhtml' AS xmlns, + 'unapi-id' AS class, + 'tag:open-ils.org:U2@mmr/' || obj_id || '/' || org AS title + ) + )::TEXT || '</' || top_el || E'>\\1' + ); + ELSE + output := xml_buf; + END IF; + + -- remove ignorable whitesace + output := REGEXP_REPLACE(output::TEXT,E'>\\s+<','><','gs')::XML; + RETURN output; +END; +$F$ LANGUAGE PLPGSQL STABLE; + +DROP FUNCTION IF EXISTS unapi.mmr_holdings_xml ( + mid BIGINT, + ouid INT, + org TEXT, + depth INT, + includes TEXT[], + slimit HSTORE, + soffset HSTORE, + include_xmlns BOOL, + pref_lib INT +); + +CREATE OR REPLACE FUNCTION unapi.mmr_holdings_xml ( + mid BIGINT, + ouid INT, + org TEXT, + depth INT DEFAULT NULL, + includes TEXT[] DEFAULT NULL::TEXT[], + slimit HSTORE DEFAULT NULL, + soffset HSTORE DEFAULT NULL, + include_xmlns BOOL DEFAULT TRUE, + pref_lib INT DEFAULT NULL, + library_group INT DEFAULT NULL +) +RETURNS XML AS $F$ + SELECT XMLELEMENT( + name holdings, + XMLATTRIBUTES( + CASE WHEN $8 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns, + CASE WHEN ('mmr' = ANY ($5)) THEN 'tag:open-ils.org:U2@mmr/' || $1 || '/' || $3 ELSE NULL END AS id, + (SELECT metarecord_has_holdable_copy FROM asset.metarecord_has_holdable_copy($1)) AS has_holdable + ), + XMLELEMENT( + name counts, + (SELECT XMLAGG(XMLELEMENT::XML) FROM ( + SELECT XMLELEMENT( + name count, + XMLATTRIBUTES('public' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow) + )::text + FROM asset.opac_ou_metarecord_copy_count($2, $1) + UNION + SELECT XMLELEMENT( + name count, + XMLATTRIBUTES('staff' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow) + )::text + FROM asset.staff_ou_metarecord_copy_count($2, $1) + UNION + SELECT XMLELEMENT( + name count, + XMLATTRIBUTES('pref_lib' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow) + )::text + FROM asset.opac_ou_metarecord_copy_count($9, $1) + UNION + SELECT XMLELEMENT( + name count, + XMLATTRIBUTES('public' as type, depth, library_group, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow) + )::text + FROM asset.opac_lasso_metarecord_copy_count_sum(library_group, $1) + UNION + SELECT XMLELEMENT( + name count, + XMLATTRIBUTES('staff' as type, depth, library_group, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow) + )::text + FROM asset.staff_lasso_metarecord_copy_count_sum(library_group, $1) + ORDER BY 1 + )x) + ), + -- XXX monograph_parts and foreign_copies are skipped in MRs ... put them back some day? + XMLELEMENT( + name volumes, + (SELECT XMLAGG(acn ORDER BY rank, name, label_sortkey) FROM ( + -- Physical copies + SELECT unapi.acn(y.id,'xml','volume',array_remove( array_remove($5,'holdings_xml'),'bre'), $3, $4, $6, $7, FALSE), y.rank, name, label_sortkey + FROM evergreen.ranked_volumes((SELECT ARRAY_AGG(source) FROM metabib.metarecord_source_map WHERE metarecord = $1), $2, $4, $6, $7, $9, $5) AS y + UNION ALL + -- Located URIs + SELECT unapi.acn(uris.id,'xml','volume',array_remove( array_remove($5,'holdings_xml'),'bre'), $3, $4, $6, $7, FALSE), uris.rank, name, label_sortkey + FROM evergreen.located_uris((SELECT ARRAY_AGG(source) FROM metabib.metarecord_source_map WHERE metarecord = $1), $2, $9) AS uris + )x) + ), + CASE WHEN ('ssub' = ANY ($5)) THEN + XMLELEMENT( + name subscriptions, + (SELECT XMLAGG(ssub) FROM ( + SELECT unapi.ssub(id,'xml','subscription','{}'::TEXT[], $3, $4, $6, $7, FALSE) + FROM serial.subscription + WHERE record_entry IN (SELECT source FROM metabib.metarecord_source_map WHERE metarecord = $1) + )x) + ) + ELSE NULL END + ); +$F$ LANGUAGE SQL STABLE; + +COMMIT; commit 09063e7029a89b6baf624d77a5f2836c7a189efb Author: Jane Sandberg <sandbergja@gmail.com> Date: Sat Jan 25 10:01:15 2025 -0800 LP2019430: Perl changes Signed-off-by: Jane Sandberg <sandbergja@gmail.com> Sponsored-by: PaILS Signed-off-by: Elizabeth Davis <elizabeth.davis@sparkpa.org> Signed-off-by: Mike Rylander <mrylander@gmail.com> diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm index 0f203a139a..83b4ca7c4e 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm @@ -254,17 +254,115 @@ __PACKAGE__->register_method( } ); +__PACKAGE__->register_method( + method => "record_id_to_copy_count", + api_name => "open-ils.search.biblio.record.copy_count.lasso", + signature => { + desc => q/Returns a copy summary for the given record for the context library group/, + params => [ + {desc => 'Context org unit id', type => 'number'}, + {desc => 'Record ID', type => 'number'}, + {desc => 'Library Group ID', type => 'number'} + ], + return => { + desc => q/summary object per org unit in the set, where the set + includes the context org unit and all parent org units. + Object includes the keys "transcendant", "count", "org_unit", "depth", + "unshadow", "available". Each is a count, except "org_unit" which is + the context org unit and "depth" which is the depth of the context org unit + /, + type => 'array' + } + } +); + +__PACKAGE__->register_method( + method => "record_id_to_copy_count", + api_name => "open-ils.search.biblio.record.copy_count.staff.lasso", + authoritative => 1, + signature => { + desc => q/Returns a copy summary for the given record for the context library group/, + params => [ + {desc => 'Context org unit id', type => 'number'}, + {desc => 'Record ID', type => 'number'}, + {desc => 'Library Group ID', type => 'number'} + ], + return => { + desc => q/summary object per org unit in the set, where the set + includes the context org unit and all parent org units. + Object includes the keys "transcendant", "count", "org_unit", "depth", + "unshadow", "available". Each is a count, except "org_unit" which is + the context org unit and "depth" which is the depth of the context org unit + /, + type => 'array' + } + } +); + +__PACKAGE__->register_method( + method => "record_id_to_copy_count", + api_name => "open-ils.search.biblio.metarecord.copy_count.lasso", + signature => { + desc => q/Returns a copy summary for the given record for the context library group/, + params => [ + {desc => 'Context org unit id', type => 'number'}, + {desc => 'Record ID', type => 'number'}, + {desc => 'Library Group ID', type => 'number'} + ], + return => { + desc => q/summary object per org unit in the set, where the set + includes the context org unit and all parent org units. + Object includes the keys "transcendant", "count", "org_unit", "depth", + "unshadow", "available". Each is a count, except "org_unit" which is + the context org unit and "depth" which is the depth of the context org unit + /, + type => 'array' + } + } +); + +__PACKAGE__->register_method( + method => "record_id_to_copy_count", + api_name => "open-ils.search.biblio.metarecord.copy_count.staff.lasso", + signature => { + desc => q/Returns a copy summary for the given record for the context library group/, + params => [ + {desc => 'Context org unit id', type => 'number'}, + {desc => 'Record ID', type => 'number'}, + {desc => 'Library Group ID', type => 'number'} + ], + return => { + desc => q/summary object per org unit in the set, where the set + includes the context org unit and all parent org units. + Object includes the keys "transcendant", "count", "org_unit", "depth", + "unshadow", "available". Each is a count, except "org_unit" which is + the context org unit and "depth" which is the depth of the context org + unit. "depth" is always -1 when the count from a lasso search is + performed, since depth doesn't mean anything in a lasso context. + /, + type => 'array' + } + } +); + sub record_id_to_copy_count { - my( $self, $client, $org_id, $record_id ) = @_; + my( $self, $client, $org_id, $record_id, $lasso_id ) = @_; return [] unless $record_id; my $key = $self->api_name =~ /metarecord/ ? 'metarecord' : 'record'; my $staff = $self->api_name =~ /staff/ ? 't' : 'f'; + my $args; + if ($lasso_id) { + my $scope = $self->api_name =~ /staff/ ? 'staff' : 'opac'; + $args = ['asset.' . $scope . '_lasso_' . $key . '_copy_count_sum' => $lasso_id => $record_id]; + } else { + $args = ['asset.' . $key . '_copy_count' => $org_id => $record_id => $staff]; + } my $data = $U->cstorereq( "open-ils.cstore.json_query.atomic", - { from => ['asset.' . $key . '_copy_count' => $org_id => $record_id => $staff] } + { from => $args } ); my @count; @@ -3145,7 +3243,9 @@ __PACKAGE__->register_method( desc => 'Stream of record data suitable for catalog display', params => [ {desc => 'Context org unit ID', type => 'number'}, - {desc => 'Array of Record IDs', type => 'array'} + {desc => 'Array of Record IDs', type => 'array'}, + {desc => 'Options hash. Keys can include pref_ou, flesh_copies, copy_limit, copy_depth, copy_offset, and library_group', + type => 'hashref'} ], return => { desc => q/ @@ -3193,6 +3293,7 @@ sub catalog_record_summary { my $e = new_editor(); $options ||= {}; my $pref_ou = $options->{pref_ou}; + my $library_group = $options->{library_group}; my $is_meta = ($self->api_name =~ /metabib/); my $is_staff = ($self->api_name =~ /staff/); @@ -3201,13 +3302,13 @@ sub catalog_record_summary { 'open-ils.circ.mmr.holds.count' : 'open-ils.circ.bre.holds.count'; - my $copy_method = $is_meta ? + my $copy_method_name = $is_meta ? 'open-ils.search.biblio.metarecord.copy_count': 'open-ils.search.biblio.record.copy_count'; - $copy_method .= '.staff' if $is_staff; + $copy_method_name .= '.staff' if $is_staff; - $copy_method = $self->method_lookup($copy_method); # local method + my $copy_method = $self->method_lookup($copy_method_name); # local method my $holdable_method = $is_meta ? 'open-ils.search.biblio.metarecord.has_holdable_copy': @@ -3282,6 +3383,11 @@ sub catalog_record_summary { ($response->{copy_counts}) = $copy_method->run($org_id, $rec_id); + if ($library_group) { + my ($group_counts) = $self->method_lookup("$copy_method_name.lasso")->run($org_id, $rec_id, $library_group); + unshift @{$response->{copy_counts}}, $group_counts->[0]; + } + $response->{first_call_number} = get_first_call_number( $e, $rec_id, $org_id, $is_staff, $is_meta, $options); diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Record.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Record.pm index 7fc37ea515..2d68a8f44b 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Record.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Record.pm @@ -116,7 +116,8 @@ sub load_record { flesh => '{holdings_xml,bmp,mra,acp,acnp,acns}', site => $org_name, depth => $depth, - pref_lib => $pref_ou + pref_lib => $pref_ou, + library_group => $ctx->{search_lasso} }); $self->timelog("past get_records_and_facets()"); @@ -586,6 +587,11 @@ sub get_hold_copy_summary { $copy_count_meth .= '.staff'; } my $req1 = $search->request($copy_count_meth, $org, $rec_id); + $self->ctx->{copy_summary} = $req1->recv->content; + if ($ctx->{search_lasso}) { + my ($group_counts) = $search->request("$copy_count_meth.lasso", $org, $rec_id, $ctx->{search_lasso})->recv->content; + unshift @{$self->ctx->{copy_summary}}, $group_counts->[0]; + } # if org unit hiding applies, limit the hold count to holds # whose pickup library is within our depth-scoped tree @@ -599,8 +605,6 @@ sub get_hold_copy_summary { 'open-ils.circ', 'open-ils.circ.bre.holds.count', $rec_id, $count_args); - $self->ctx->{copy_summary} = $req1->recv->content; - $search->kill_me; } diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm index af8aad103c..061f20f025 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm @@ -574,6 +574,7 @@ sub load_rresults { metarecord => $is_meta, depth => $depth, pref_lib => $ctx->{pref_ou}, + library_group => $ctx->{search_lasso}, } ); $self->timelog("Returned from get_records_and_facets()"); diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm index f1e6e2259f..93d8e71fc5 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm @@ -490,7 +490,8 @@ sub get_records_and_facets { $unapi_args->{site}, $unapi_args->{depth}, $unapi_args->{flesh_depth}, - ($unapi_args->{pref_lib} || '') + ($unapi_args->{pref_lib} || ''), + ($unapi_args->{library_group} || '') ); my %tmp_data; @@ -537,7 +538,8 @@ sub get_records_and_facets { $unapi_args->{depth}, $slimit, undef, undef, undef, undef, undef, undef, undef, undef, - $unapi_args->{pref_lib} + $unapi_args->{pref_lib}, + $unapi_args->{library_group} ]} ); diff --git a/Open-ILS/src/perlmods/live_t/39-record-summary.t b/Open-ILS/src/perlmods/live_t/39-record-summary.t index 4a7b54629a..2b189838aa 100644 --- a/Open-ILS/src/perlmods/live_t/39-record-summary.t +++ b/Open-ILS/src/perlmods/live_t/39-record-summary.t @@ -2,7 +2,7 @@ use strict; use warnings; -use Test::More tests => 4; +use Test::More tests => 5; use OpenILS::Utils::TestUtils; use OpenILS::Utils::CStoreEditor qw/:funcs/; @@ -79,6 +79,22 @@ subtest('metarecord flavor', sub { is($response->{record_note_count}, '2', 'includes the sum count of notes on all individual records'); }); +subtest('with location_group option', sub { + plan tests => 1; + + my $org_unit = 4; + my @record_ids = (248); + my $response = $apputils->simplereq( + 'open-ils.search', + 'open-ils.search.biblio.record.catalog_summary.staff', + $org_unit, + \@record_ids, + {library_group => 1000001}); + my @library_group_counts = grep { $_->{lasso} == 1000001 } @{$response->{copy_counts}}; + + is($library_group_counts[0]->{available}, 4, 'includes the total items in the specified library group'); +}); + subtest('cleanup', sub { plan tests => 1; $e->xact_begin; ----------------------------------------------------------------------- Summary of changes: Open-ILS/src/eg2/src/app/core/org.service.ts | 5 +- .../app/share/catalog/bib-record.service.spec.ts | 12 + .../src/app/share/catalog/bib-record.service.ts | 6 +- .../src/app/share/catalog/catalog.service.spec.ts | 54 ++ .../eg2/src/app/share/catalog/catalog.service.ts | 27 +- .../eg2/src/app/share/catalog/search-context.ts | 7 + .../app/staff/catalog/result/record.component.html | 2 +- .../app/staff/catalog/result/record.component.ts | 6 +- .../bib-staff-view/bib-staff-view.component.html | 2 +- .../bib-staff-view.component.spec.ts | 75 ++ .../bib-staff-view/bib-staff-view.component.ts | 17 +- Open-ILS/src/eg2/src/test_data/mock_generators.ts | 38 + .../lib/OpenILS/Application/Search/Biblio.pm | 167 +++- .../perlmods/lib/OpenILS/WWW/EGCatLoader/Record.pm | 11 +- .../perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm | 1 + .../perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm | 6 +- Open-ILS/src/perlmods/live_t/39-record-summary.t | 18 +- Open-ILS/src/sql/Pg/002.schema.config.sql | 2 +- Open-ILS/src/sql/Pg/040.schema.asset.sql | 100 +++ Open-ILS/src/sql/Pg/300.schema.staged_search.sql | 39 + Open-ILS/src/sql/Pg/990.schema.unapi.sql | 101 ++- Open-ILS/src/sql/Pg/live_t/unapi-holdings.pg | 47 ++ .../src/sql/Pg/t/library_group_copy_count_sums.pg | 105 +++ .../upgrade/1464.schema.lasso_item_count_sums.sql | 863 +++++++++++++++++++++ .../templates-bootstrap/opac/parts/misc_util.tt2 | 2 +- .../opac/parts/record/copy_counts.tt2 | 9 +- .../opac/parts/record/copy_table.tt2 | 8 +- .../opac/parts/result/copy_counts.tt2 | 13 +- Open-ILS/src/templates/opac/parts/misc_util.tt2 | 2 +- .../templates/opac/parts/record/copy_counts.tt2 | 9 +- .../templates/opac/parts/result/copy_counts.tt2 | 13 +- .../OPAC/library_group_counts_in_catalog.adoc | 14 + 32 files changed, 1716 insertions(+), 65 deletions(-) create mode 100644 Open-ILS/src/eg2/src/app/share/catalog/catalog.service.spec.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/share/bib-staff-view/bib-staff-view.component.spec.ts create mode 100644 Open-ILS/src/sql/Pg/live_t/unapi-holdings.pg create mode 100644 Open-ILS/src/sql/Pg/t/library_group_copy_count_sums.pg create mode 100644 Open-ILS/src/sql/Pg/upgrade/1464.schema.lasso_item_count_sums.sql create mode 100644 docs/RELEASE_NOTES_NEXT/OPAC/library_group_counts_in_catalog.adoc hooks/post-receive -- Evergreen ILS