LP#1053397 TPAC metarecord search and holds UI
authorBill Erickson <berick@esilibrary.com>
Mon, 10 Feb 2014 19:01:47 +0000 (14:01 -0500)
committerDan Wells <dbw2@calvin.edu>
Fri, 21 Feb 2014 20:38:53 +0000 (15:38 -0500)
API, TPAC backend, and UI bits for TPAC metarecord searching and holds.

* Group Formats and Editions options in advanced search / searchbar
* MR holds placement form, allowing selected formats and languages
* MR holds targeting updated to work w/ new holdable formats composite
  definitions

Signed-off-by: Bill Erickson <berick@esilibrary.com>
Signed-off-by: Mike Rylander <mrylander@gmail.com>
Signed-off-by: Dan Wells <dbw2@calvin.edu>
17 files changed:
Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/action.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGWeb.pm
Open-ILS/src/templates/opac/css/style.css.tt2
Open-ILS/src/templates/opac/myopac/holds.tt2
Open-ILS/src/templates/opac/myopac/holds/edit.tt2
Open-ILS/src/templates/opac/parts/advanced/search.tt2
Open-ILS/src/templates/opac/parts/config.tt2
Open-ILS/src/templates/opac/parts/metarecord_hold_filters.tt2 [new file with mode: 0644]
Open-ILS/src/templates/opac/parts/misc_util.tt2
Open-ILS/src/templates/opac/parts/place_hold.tt2
Open-ILS/src/templates/opac/parts/result/table.tt2
Open-ILS/src/templates/opac/results.tt2
Open-ILS/web/js/ui/default/opac/simple.js

index 8ba48ab..4f56877 100644 (file)
@@ -92,8 +92,15 @@ sub test_and_create_hold_batch {
     elsif ($$params{'hold_type'} eq 'P') { $target_field = 'partid'; }
     else { return undef; }
 
+    my $formats_map = delete $$params{holdable_formats_map};
+
     foreach (@$target_list) {
         $$params{$target_field} = $_;
+
+        # copy the requested formats from the target->formats map
+        # into the top-level formats attr for each hold
+        $$params{holdable_formats} = $formats_map->{$_};
+
         my $res;
         ($res) = $self->method_lookup(
             'open-ils.circ.title_hold.is_possible')->run($auth, $params, $override ? $oargs : {});
@@ -2470,10 +2477,9 @@ sub do_possibility_checks {
 
     } elsif( $hold_type eq OILS_HOLD_TYPE_METARECORD ) {
 
-        my $maps = $e->search_metabib_metarecord_source_map({metarecord=>$mrid});
-        my @recs = map { $_->source } @$maps;
+        my ($recs) = __PACKAGE__->method_lookup('open-ils.circ.holds.metarecord.filterd_records')->run($mrid, $holdable_formats);
         my @status = ();
-        for my $rec (@recs) {
+        for my $rec (@$recs) {
             @status = _check_title_hold_is_possible(
                 $rec, $depth, $request_lib, $patron, $e->requestor, $pickup_lib, $selection_ou, $holdable_formats, $oargs
             );
@@ -2484,6 +2490,15 @@ sub do_possibility_checks {
 #   else { Unrecognized hold_type ! }   # FIXME: return error? or 0?
 }
 
+sub MR_filter_records {
+    return $U->storagereq('open-ils.storage.metarecord.filtered_records.atomic', $_[2], $_[3]);
+}
+__PACKAGE__->register_method(
+    method   => 'MR_filter_records',
+    api_name => 'open-ils.circ.holds.metarecord.filterd_records',
+);
+
+
 my %prox_cache;
 sub create_ranged_org_filter {
     my($e, $selection_ou, $depth) = @_;
@@ -2511,11 +2526,7 @@ sub create_ranged_org_filter {
 
 sub _check_title_hold_is_possible {
     my( $titleid, $depth, $request_lib, $patron, $requestor, $pickup_lib, $selection_ou, $holdable_formats, $oargs ) = @_;
-
-    my ($types, $formats, $lang);
-    if (defined($holdable_formats)) {
-        ($types, $formats, $lang) = split '-', $holdable_formats;
-    }
+    # $holdable_formats is now unused. We pre-filter the MR's records.
 
     my $e = new_editor();
     my %org_filter = create_ranged_org_filter($e, $selection_ou, $depth);
@@ -2529,23 +2540,7 @@ sub _check_title_hold_is_possible {
                     acn => {
                         field  => 'id',
                         fkey   => 'call_number',
-                        'join' => {
-                            bre => {
-                                field  => 'id',
-                                filter => { id => $titleid },
-                                fkey   => 'record'
-                            },
-                            mrd => {
-                                field  => 'record',
-                                fkey   => 'record',
-                                filter => {
-                                    record => $titleid,
-                                    ( $types   ? (item_type => [split '', $types])   : () ),
-                                    ( $formats ? (item_form => [split '', $formats]) : () ),
-                                    ( $lang    ? (item_lang => $lang)                : () )
-                                }
-                            }
-                        }
+                        filter => { record => $titleid }
                     },
                     acpl => { field => 'id', filter => { holdable => 't'}, fkey => 'location' },
                     ccs  => { field => 'id', filter => { holdable => 't'}, fkey => 'status'   },
@@ -4205,4 +4200,135 @@ sub calculate_expire_time
     return undef;
 }
 
+
+__PACKAGE__->register_method(
+    method    => 'mr_hold_filter_attrs',
+    api_name  => 'open-ils.circ.mmr.holds.filters',
+    authoritative => 1,
+    stream => 1,
+    signature => {
+        desc => q/
+            Returns the set of available formats and languages for the
+            constituent records of the provided metarcord.
+            If an array of hold IDs is also provided, information about
+            each is returned as well.  This information includes:
+            1. a slightly easier to read version of holdable_formats
+            2. attributes describing the set of format icons included
+               in the set of desired, constituent records.
+        /,
+        params => [
+            {desc => 'Metarecord ID', type => 'number'},
+            {desc => 'Hold ID List', type => 'array'},
+        ],
+        return => {
+            desc => q/
+                Stream of objects.  The first will have a 'metarecord' key
+                containing non-hold-specific metarecord information, subsequent
+                responses will contain a 'hold' key containing hold-specific
+                information
+            /, 
+            type => 'object'
+        }
+    }
+);
+
+sub mr_hold_filter_attrs {
+    my ($self, $client, $mr_id, $hold_ids) = @_;
+    my $e = new_editor();
+
+
+    my $mr = $e->retrieve_metabib_metarecord($mr_id) or return $e->event;
+    my $bre_ids = $e->json_query({
+        select => {mmrsm => ['source']},
+        from => 'mmrsm',
+        where => {'+mmrsm' => {metarecord => $mr_id}}
+    });
+    $bre_ids = [map {$_->{source}} @$bre_ids];
+
+    my $item_lang_attr = 'item_lang'; # configurable?
+    my $format_attr = $e->retrieve_config_global_flag(
+        'opac.metarecord.holds.format_attr')->value;
+
+    # helper sub for fetching ccvms for a batch of record IDs
+    sub get_batch_ccvms {
+        my ($e, $attr, $bre_ids) = @_;
+        return [] unless $bre_ids and @$bre_ids;
+        my $vals = $e->search_metabib_record_attr_flat({
+            attr => $attr,
+            id => $bre_ids
+        });
+        return [] unless @$vals;
+        return $e->search_config_coded_value_map({
+            ctype => $attr,
+            code => [map {$_->value} @$vals]
+        });
+    }
+
+    my $langs = get_batch_ccvms($e, $item_lang_attr, $bre_ids);
+    my $formats = get_batch_ccvms($e, $format_attr, $bre_ids);
+
+    $client->respond({
+        metarecord => {
+            id => $mr_id,
+            formats => $formats,
+            langs => $langs
+        }
+    });
+
+    return unless $hold_ids;
+    my $icon_attr = $e->retrieve_config_global_flag('opac.icon_attr');
+    $icon_attr = $icon_attr ? $icon_attr->value : '';
+
+    for my $hold_id (@$hold_ids) {
+        my $hold = $e->retrieve_action_hold_request($hold_id) 
+            or return $e->event;
+
+        next unless $hold->hold_type eq 'M';
+
+        my $resp = {
+            hold => {
+                id => $hold_id,
+                formats => [],
+                langs => []
+            }
+        };
+
+        # collect the ccvm's for the selected formats / language (
+        # (i.e. the holdable formats) on the MR.
+        # this assumes a two-key structure for format / language,
+        # though assumption is made about the keys themselves.
+        my $hformats = OpenSRF::Utils::JSON->JSON2perl($hold->holdable_formats);
+        my $lang_vals = [];
+        my $format_vals = [];
+        for my $val (values %$hformats) {
+            # val is either a single ccvm or an array of them
+            $val = [$val] unless ref $val eq 'ARRAY';
+            for my $node (@$val) {
+                push (@$lang_vals, $node->{_val})   
+                    if $node->{_attr} eq $item_lang_attr; 
+                push (@$format_vals, $node->{_val})   
+                    if $node->{_attr} eq $format_attr;
+            }
+        }
+
+        # fetch the ccvm's for consistency with the {metarecord} blob
+        $resp->{hold}{formats} = $e->search_config_coded_value_map({
+            ctype => $format_attr, code => $format_vals});
+        $resp->{hold}{langs} = $e->search_config_coded_value_map({
+            ctype => $item_lang_attr, code => $lang_vals});
+
+        # find all of the bib records within this metarcord whose 
+        # format / language match the holdable formats on the hold
+        my ($bre_ids) = $self->method_lookup(
+            'open-ils.circ.holds.metarecord.filterd_records')->run(
+                $hold->target, $hold->holdable_formats);
+
+        # now find all of the 'icon' attributes for the records
+        $resp->{hold}{icons} = get_batch_ccvms($e, $icon_attr, $bre_ids);
+        $client->respond($resp);
+    }
+
+    return;
+}
+
 1;
index b81e847..2f98fa5 100644 (file)
@@ -1283,6 +1283,47 @@ __PACKAGE__->register_method(
 );
 
 
+sub MR_records_matching_format {
+    my $self = shift;
+    my $client = shift;
+    my $MR = shift;
+    my $filter = shift;
+
+    # find filters for MR holds
+    my $mr_filter;
+    if (defined($filter)) {
+        ($mr_filter) = @{action::hold_request->db_Main->selectcol_arrayref(
+            'SELECT metabib.compile_composite_attr(?)',
+            {},
+            $filter
+        )};
+    }
+
+    my $records = [metabib::metarecord->retrieve($MR)->source_records];
+
+    if (!$mr_filter) {
+        $client->respond( $_->id ) for @$records;
+    } else {
+        for my $r ( map { isTrue($_->deleted) ?  () : ($_->id) } @$records ) {
+            $client->respond($r) if
+                @{action::hold_request->db_Main->selectcol_arrayref(
+                    'SELECT source FROM metabib.record_attr_vector_list WHERE source = ? AND vlist @@ ?',
+                    {},
+                    $r,
+                    $mr_filter
+                )};
+        }
+    }
+    return; # discard final l-val
+}
+__PACKAGE__->register_method(
+    api_name        => 'open-ils.storage.metarecord.filtered_records',
+    api_level       => 1,
+    stream          => 1,
+    argc            => 2,
+    method          => 'MR_records_matching_format',
+);
+
 
 sub new_hold_copy_targeter {
     my $self = shift;
@@ -1432,41 +1473,23 @@ sub new_hold_copy_targeter {
 
             my $all_copies = [];
 
-            # find filters for MR holds
-            my ($types, $formats, $lang);
-            if (defined($hold->holdable_formats)) {
-                ($types, $formats, $lang) = split '-', $hold->holdable_formats;
-            }
-
             # find all the potential copies
             if ($hold->hold_type eq 'M') {
-                my $records = [
-                    map {
-                        isTrue($_->deleted) ?  () : ($_->id)
-                    } metabib::metarecord->retrieve($hold->target)->source_records
-                ];
-                if(@$records > 0) {
-                    for my $r ( map
-                            {$_->record}
-                            metabib::record_descriptor
-                                ->search(
-                                    record => $records,
-                                    ( $types   ? (item_type => [split '', $types])   : () ),
-                                    ( $formats ? (item_form => [split '', $formats]) : () ),
-                                    ( $lang    ? (item_lang => $lang)                : () ),
-                                )
-                    ) {
-                        my ($rtree) = $self
-                            ->method_lookup( 'open-ils.storage.biblio.record_entry.ranged_tree')
-                            ->run( $r->id, $hold->selection_ou, $hold->selection_depth );
-
-                        for my $cn ( @{ $rtree->call_numbers } ) {
-                            push @$all_copies,
-                                asset::copy->search_where(
-                                    { id => [map {$_->id} @{ $cn->copies }],
-                                      deleted => 'f' }
-                                ) if ($cn && @{ $cn->copies });
-                        }
+                for my $r_id (
+                    $self->method_lookup(
+                        'open-ils.storage.metarecord.filtered_records'
+                    )->run( $hold->target, $hold->holdable_formats )
+                ) {
+                    my ($rtree) = $self
+                        ->method_lookup( 'open-ils.storage.biblio.record_entry.ranged_tree')
+                        ->run( $r_id, $hold->selection_ou, $hold->selection_depth );
+
+                    for my $cn ( @{ $rtree->call_numbers } ) {
+                        push @$all_copies,
+                            asset::copy->search_where(
+                                { id => [map {$_->id} @{ $cn->copies }],
+                                  deleted => 'f' }
+                            ) if ($cn && @{ $cn->copies });
                     }
                 }
             } elsif ($hold->hold_type eq 'T') {
@@ -2323,32 +2346,5 @@ sub title_hold_capture {
     $self->volume_hold_capture($hold,$cn_list) if (ref $cn_list and @$cn_list);
 }
 
-sub metarecord_hold_capture {
-    my $self = shift;
-    my $hold = shift;
-
-    my $titles;
-    try {
-        $titles = [ metabib::metarecord_source_map->search( metarecord => $hold->target) ];
-    
-    } catch Error with {
-        my $e = shift;
-        die "Could not retrieve initial title list:\n\n$e\n";
-    };
-
-    try {
-        my @recs = map {$_->record} metabib::record_descriptor->search( record => $titles, item_type => [split '', $hold->holdable_formats] ); 
-
-        $titles = [ biblio::record_entry->search( id => \@recs ) ];
-    
-    } catch Error with {
-        my $e = shift;
-        die "Could not retrieve format-pruned title list:\n\n$e\n";
-    };
-
-
-    $cache{titles}{$_->id} = $_ for (@$titles);
-    $self->title_hold_capture($hold,$titles) if (ref $titles and @$titles);
-}
 
 1;
index ac11d61..3961984 100644 (file)
@@ -571,9 +571,35 @@ sub fetch_user_holds {
 
         if(@collected) {
             while(my $blob = pop(@collected)) {
-                my (undef, @data) = $self->get_records_and_facets(
-                    [$blob->{hold}->{bre_id}], undef, {flesh => '{mra}'}
-                );
+                my @data;
+
+                # in the holds edit UI, we need to know what formats and
+                # languages the user selected for this hold, plus what
+                # formats/langs are available on the MR as a whole.
+                if ($blob->{hold}{hold}->hold_type eq 'M') {
+                    my $hold = $blob->{hold}->{hold};
+
+                    # for MR, fetch the combined MR unapi blob
+                    (undef, @data) = $self->get_records_and_facets(
+                        [$hold->target], undef, {flesh => '{mra}', metarecord => 1});
+
+                    my $filter_data = $U->simplereq(
+                        'open-ils.circ',
+                        'open-ils.circ.mmr.holds.filters.authoritative.atomic', 
+                        $hold->target, [$hold->id]
+                    );
+
+                    $blob->{metarecord_filters} = 
+                        $filter_data->[0]->{metarecord};
+                    $blob->{metarecord_selected_filters} = 
+                        $filter_data->[1]->{hold};
+                } else {
+
+                    (undef, @data) = $self->get_records_and_facets(
+                        [$blob->{hold}->{bre_id}], undef, {flesh => '{mra}'}
+                    );
+                }
+
                 $blob->{marc_xml} = $data[0]->{marc_xml};
                 push(@holds, $blob);
             }
@@ -652,6 +678,10 @@ sub handle_hold_update {
                     m:^(\d{2})/(\d{2})/(\d{4})$:;
                 $val->{$field} = "$3-$1-$2";
             }
+
+            $val->{holdable_formats} = # no-op for non-MR holds
+                $self->compile_holdable_formats(undef, $_);
+
             $val;
         } @hold_ids;
 
@@ -777,8 +807,37 @@ sub load_place_hold {
     };
 
     my $type_dispatch = {
+        M => sub {
+            # target metarecords
+            my $mrecs = $e->batch_retrieve_metabib_metarecord([
+                \@targets, 
+                {flesh => 1, flesh_fields => {mmr => ['master_record']}}], 
+                {substream => 1}
+            );
+
+            for my $id (@targets) {
+                my ($mr) = grep {$_->id eq $id} @$mrecs;
+
+                my $filter_data = $U->simplereq(
+                    'open-ils.circ',
+                    'open-ils.circ.mmr.holds.filters.authoritative', $mr->id);
+
+                my $holdable_formats = 
+                    $self->compile_holdable_formats($mr->id);
+
+                push(@hold_data, $data_filler->({
+                    target => $mr, 
+                    record => $mr->master_record,
+                    holdable_formats => $holdable_formats,
+                    metarecord_filters => $filter_data->{metarecord}
+                }));
+            }
+        },
         T => sub {
-            my $recs = $e->batch_retrieve_biblio_record_entry(\@targets, {substream => 1});
+            my $recs = $e->batch_retrieve_biblio_record_entry(
+                [\@targets,  {flesh => 1, flesh_fields => {bre => ['metarecord']}}],
+                {substream => 1}
+            );
 
             for my $id (@targets) { # force back into the correct order
                 my ($rec) = grep {$_->id eq $id} @$recs;
@@ -984,13 +1043,23 @@ sub attempt_hold_placement {
 
     if(@create_targets) {
 
+        # holdable formats may be different for each MR hold.
+        # map each set to the ID of the target.
+        my $holdable_formats = {};
+        if ($hold_type eq 'M') {
+            $holdable_formats->{$_->{target_id}} = 
+                $_->{holdable_formats} for @hold_data;
+        }
+
         my $bses = OpenSRF::AppSession->create('open-ils.circ');
         my $breq = $bses->request( 
             $method, 
             $e->authtoken, 
-            $data_filler->({   patronid => $usr,
+            $data_filler->({   
+                patronid => $usr,
                 pickup_lib => $pickup_lib, 
-                hold_type => $hold_type
+                hold_type => $hold_type,
+                holdable_formats_map => $holdable_formats
             }),
             \@create_targets
         );
@@ -1053,6 +1122,63 @@ sub attempt_hold_placement {
     }
 }
 
+# pull the selected formats and languages for metarecord holds
+# from the CGI params and map them into the JSON holdable
+# formats...er, format.
+# if no metarecord is provided, we'll pull it from the target
+# of the provided hold.
+sub compile_holdable_formats {
+    my ($self, $mr_id, $hold_id) = @_;
+    my $e = $self->editor;
+    my $cgi = $self->cgi;
+
+    # exit early if not needed
+    return "" unless 
+        grep /metarecord_formats_|metarecord_langs_/, 
+        $cgi->param;
+
+    # CGI params are based on the MR id, since during hold placement
+    # we have no old ID.  During hold edit, map the hold ID back to 
+    # the metarecod target.
+    $mr_id = 
+        $e->retrieve_action_hold_request($hold_id)->target 
+        unless $mr_id;
+
+    my $format_attr = $self->ctx->{get_cgf}->(
+        'opac.metarecord.holds.format_attr');
+
+    if (!$format_attr) {
+        $logger->error("Missing config.global_flag: ".
+            "opac.metarecord.holds.format_attr!");
+        return "";
+    }
+
+    $format_attr = $format_attr->value;
+
+    # during hold placement or edit submission, the user selects
+    # which of the available formats/langs are acceptable.
+    # Capture those here as the holdable_formats for the MR hold.
+    my @selected_formats = $cgi->param("metarecord_formats_$mr_id");
+    my @selected_langs = $cgi->param("metarecord_langs_$mr_id");
+
+    # map the selected attrs into the JSON holdable_formats structure
+    my $blob = {};
+    if (@selected_formats) {
+        $blob->{0} = [
+            map { {_attr => $format_attr, _val => $_} } 
+            @selected_formats
+        ];
+    }
+    if (@selected_langs) {
+        $blob->{1} = [
+            map { {_attr => 'item_lang', _val => $_} } 
+            @selected_langs
+        ];
+    }
+
+    return OpenSRF::Utils::JSON->perl2JSON($blob);
+}
+
 sub fetch_user_circs {
     my $self = shift;
     my $flesh = shift; # flesh bib data, etc.
index 00c4c49..26df556 100644 (file)
@@ -67,7 +67,8 @@ sub _prepare_biblio_search {
 
     foreach ($cgi->param('modifier')) {
         # The unless bit is to avoid stacking modifiers.
-        $query = ('#' . $_ . ' ' . $query) unless $query =~ qr/\#\Q$_/;
+        $query = ('#' . $_ . ' ' . $query) unless 
+            $query =~ qr/\#\Q$_/ or $_ eq 'metabib';
     }
 
     # filters
@@ -307,6 +308,13 @@ sub load_rresults {
     my $ctx = $self->ctx;
     my $e = $self->editor;
 
+    # 1. param->metarecord : view constituent bib records for a metarecord
+    # 2. param->modifier=metabib : perform a metarecord search
+    my $metarecord = $ctx->{metarecord} = $cgi->param('metarecord');
+    my @mods = $cgi->param('modifier');
+    my $is_meta = (@mods and grep {$_ eq 'metabib'} @mods and !$metarecord);
+    my $id_key = $is_meta ? 'mmr_id' : 'bre_id';
+
     # find the last record in the set, then redirect
     my $find_last = $cgi->param('find_last');
 
@@ -343,7 +351,6 @@ sub load_rresults {
     $ctx->{search_ou} = $self->_get_search_lib();
     $ctx->{pref_ou} = $self->_get_pref_lib() || $ctx->{search_ou};
     my $offset = $page * $limit;
-    my $metarecord = $cgi->param('metarecord');
     my $results; 
     my $tag_circs = $self->tag_circed_items;
     $self->timelog("Got search parameters");
@@ -408,12 +415,14 @@ sub load_rresults {
 
         $query = "$_ $query" for @facets;
 
-        $logger->activity("EGWeb: [search] $query");
+        my $ltag = $is_meta ? '[mmr search]' : '[bre search]';
+        $logger->activity("EGWeb: $ltag $query");
 
         try {
 
             my $method = 'open-ils.search.biblio.multiclass.query';
             $method .= '.staff' if $ctx->{is_staff};
+            $method =~ s/biblio/metabib/ if $is_meta;
 
             my $ses = OpenSRF::AppSession->create('open-ils.search');
 
@@ -440,6 +449,7 @@ sub load_rresults {
         my $rec_id = pop @$rec_ids;
         $cgi->delete('find_last');
         my $url = $cgi->url(-full => 1, -path => 1, -query => 1);
+        # TODO: metarecord => /rresults?metarecord=$mmr_id
         $url =~ s|/results|/record/$rec_id|;
         return $self->generic_redirect($url);
     }
@@ -448,12 +458,27 @@ sub load_rresults {
 
     $self->load_rresults_bookbag_item_notes($rec_ids) if $ctx->{bookbag};
 
+    my $fetch_recs = $rec_ids;
+
+    my $metarecord_master;
+    if ($metarecord) {
+        # when listing the contents of a metarecord, be sure to fetch
+        # the lead record for summary display.  Adding the ID to
+        # $fetch_recs lets us grab the record (if necessary) w/o it
+        # unintentially becoming a member of the result set.
+        my $mr = $e->retrieve_metabib_metarecord($metarecord);
+        push(@$fetch_recs, $mr->master_record)
+            unless grep {$_ eq $mr->master_record} @$fetch_recs;
+        $metarecord_master = $mr->master_record;
+    }
+
     $self->timelog("Calling get_records_and_facets()");
     my ($facets, @data) = $self->get_records_and_facets(
-        $rec_ids, $results->{facet_key}, 
+        $fetch_recs, $results->{facet_key}, 
         {
             flesh => '{holdings_xml,mra,acp,acnp,acns,bmp}',
             site => $site,
+            metarecord => $is_meta,
             depth => $depth,
             pref_lib => $ctx->{pref_ou},
         }
@@ -461,6 +486,7 @@ sub load_rresults {
     $self->timelog("Returned from get_records_and_facets()");
 
     if ($page == 0) {
+        # TODO: handle metarecords
         my $stat = $self->check_1hit_redirect($rec_ids);
         return $stat if $stat;
     }
@@ -470,19 +496,26 @@ sub load_rresults {
 
     # shove recs into context in search results order
     for my $rec_id (@$rec_ids) {
-        push(
-            @{$ctx->{records}},
-            grep { $_->{id} == $rec_id } @data
-        );
+        my ($rec) = grep { $_->{$id_key} == $rec_id } @data;
+        push(@{$ctx->{records}}, $rec);
+
+        $ctx->{metarecord_master} = $rec
+            if $metarecord_master and $metarecord_master eq $rec_id;
+
+        # MR's with multiple constituent records will have a
+        # null value in position 2 of the result set.  
+        my ($res_rec) = grep { $_->[0] == $rec_id} @{$results->{ids}};
+        $rec->{mr_has_multi} = !$res_rec->[2];
     }
 
     if ($tag_circs) {
         for my $rec (@{$ctx->{records}}) {
-            my ($res_rec) = grep { $_->[0] == $rec->{id} } @{$results->{ids}};
+            my ($res_rec) = grep { $_->[0] == $rec->{$id_key} } @{$results->{ids}};
             # index 1 in the per-record result array is a boolean which
             # indicates whether the record in question is in the users
             # accessible circ history list
-            $rec->{user_circulated} = 1 if $res_rec->[1];
+            my $index = $is_meta ? 3 : 1;
+            $rec->{user_circulated} = 1 if $res_rec->[$index];
         }
     }
 
index ce009dd..2bc2c16 100644 (file)
@@ -277,9 +277,13 @@ sub get_records_and_facets {
     $unapi_args->{depth} ||= $self->ctx->{aou_tree}->()->ou_type->depth;
     $unapi_args->{flesh_depth} ||= 5;
 
+    my $is_meta = delete $unapi_args->{metarecord};
+    my $unapi_type = $is_meta ? 'unapi.mmr' : 'unapi.bre';
+
     $unapi_cache ||= OpenSRF::Utils::Cache->new('global');
     my $unapi_cache_key_suffix = join(
         '_',
+        $is_meta || 0,
         $unapi_args->{site},
         $unapi_args->{depth},
         $unapi_args->{flesh_depth},
@@ -299,36 +303,59 @@ sub get_records_and_facets {
             $outer_self->timelog("get_records_and_facets(): got response content");
 
             # Protect against requests for non-existent records
-            return unless $data->{'unapi.bre'};
+            return unless $data->{$unapi_type};
 
-            my $xml = XML::LibXML->new->parse_string($data->{'unapi.bre'})->documentElement;
+            my $xml = XML::LibXML->new->parse_string($data->{$unapi_type})->documentElement;
 
             $outer_self->timelog("get_records_and_facets(): parsed xml");
             # Protect against legacy invalid MARCXML that might not have a 901c
             my $bre_id;
+            my $mmr_id;
             my $bre_id_nodes =  $xml->find('*[@tag="901"]/*[@code="c"]');
             if ($bre_id_nodes) {
                 $bre_id =  $bre_id_nodes->[0]->textContent;
             } else {
                 $logger->warn("Missing 901 subfield 'c' in " . $xml->toString());
             }
-            $tmp_data{$bre_id} = {id => $bre_id, marc_xml => $xml};
 
-            if ($bre_id) {
+            if ($is_meta) {
+                # extract metarecord ID from mmr.unapi tag
+                for my $node ($xml->getElementsByTagName('abbr')) {
+                    my $title = $node->getAttribute('title');
+                    ($mmr_id = $title) =~ 
+                        s/tag:open-ils.org:U2\@mmr\/(\d+)\/.*/$1/g;
+                    last if $mmr_id;
+                }
+            }
+
+            my $rec_id = $mmr_id ? $mmr_id : $bre_id;
+            $tmp_data{$rec_id} = {
+                id => $rec_id, 
+                bre_id => $bre_id, 
+                mmr_id => $mmr_id,
+                marc_xml => $xml
+            };
+
+            if ($rec_id) {
                 # Let other backends grab our data now that we're done.
-                my $key = 'TPAC_unapi_cache_'.$bre_id.'_'.$unapi_cache_key_suffix;
+                my $key = 'TPAC_unapi_cache_'.$rec_id.'_'.$unapi_cache_key_suffix;
                 my $cache_data = $unapi_cache->get_cache($key);
                 if ($$cache_data{running}) {
-                    $unapi_cache->put_cache($key, { id => $bre_id, marc_xml => $data->{'unapi.bre'} }, 10);
+                    $unapi_cache->put_cache($key, {
+                        bre_id => $bre_id,
+                        mmr_id => $mmr_id,
+                        id => $rec_id, 
+                        marc_xml => $data->{$unapi_type} 
+                    }, 10);
                 }
             }
 
-
             $outer_self->timelog("get_records_and_facets(): end of success handler");
         }
     );
 
-    $self->timelog("get_records_and_facets(): about to call unapi.bre via json_query (rec_ids has " . scalar(@$rec_ids));
+    $self->timelog("get_records_and_facets(): about to call ".
+        "$unapi_type via json_query (rec_ids has " . scalar(@$rec_ids));
 
     my @loop_recs = @$rec_ids;
     my %rec_timeout;
@@ -359,19 +386,26 @@ sub get_records_and_facets {
             $tmp_data{$unapi_data->{id}} = $unapi_data;
         } else { # we're the first or we timed out. success_handler will populate the real value
             $unapi_cache->put_cache($unapi_cache_key, { running => $$ }, 10);
+
+            my $sdepth = $unapi_args->{flesh_depth};
+            my $slimit = "acn=>$sdepth,acp=>$sdepth";
+            $slimit .= ",bre=>$sdepth" if $is_meta;
+            my $flesh = $unapi_args->{flesh} || '';
+
+            # tag the record with the MR id
+            $flesh =~ s/}$/,mmr.unapi}/g if $is_meta;
+
             $ses->request(
                 'open-ils.cstore.json_query',
                  {from => [
-                    'unapi.bre', $bid, 'marcxml','record', 
-                    $unapi_args->{flesh}, 
+                    $unapi_type, $bid, 'marcxml','record', $flesh,
                     $unapi_args->{site}, 
                     $unapi_args->{depth}, 
-                    'acn=>' . $unapi_args->{flesh_depth} . ',acp=>' . $unapi_args->{flesh_depth}, 
+                    $slimit,
                     undef, undef, $unapi_args->{pref_lib}
                 ]}
             );
         }
-
     }
 
 
index 57cb02b..8b72e53 100644 (file)
@@ -186,7 +186,10 @@ sub load_context {
         parse_accept_lang($r->headers_in->get('Accept-Language'));
 
     # set the editor default locale for each page load
-    OpenSRF::AppSession->default_locale(parse_eg_locale($ctx->{locale}));
+    my $ses_locale = parse_eg_locale($ctx->{locale});
+    OpenSRF::AppSession->default_locale($ses_locale);
+    # give templates access to the en-US style locale
+    $ctx->{eg_locale} = $ses_locale;
 
     my $mprefix = $ctx->{media_prefix};
     if($mprefix and $mprefix !~ /^http/ and $mprefix !~ /^\//) {
index e57741f..0c5e71b 100644 (file)
@@ -982,6 +982,7 @@ table.acct_notes th {
 div.adv_search_available {
     margin-top: 1em;
 }
+
 #myopac_loading {
     width:100%;
     text-align:center;
index 81ebc34..0a7b04a 100644 (file)
                     </td>
                     <td>
                         <div class="format_icon">
-                            [% IF attrs.format_icon %]
-                            <img title="[% attrs.format_label | html %]" alt="[% attrs.format_label | html %]" src="[% attrs.format_icon %]" />
+                            [% IF attrs.all_formats.size %]
+                                [% FOR format IN attrs.all_formats %]
+                                <img title="[% format.label | html %]" alt="[% format.label | html %]" src="[% format.icon %]" />
+                                [% END %]
                             [% END %]
                         </div>
                     </td>
index 2fd2e92..e0981bc 100644 (file)
@@ -2,6 +2,7 @@
     PROCESS "opac/parts/misc_util.tt2";
     PROCESS "opac/parts/hold_status.tt2";
     PROCESS "opac/parts/org_selector.tt2";
+    PROCESS "opac/parts/metarecord_hold_filters.tt2";
     WRAPPER "opac/parts/myopac/base.tt2";
     myopac_page = "holds"; # in this case, just for tab coloring.
 
                         </td>
                     </tr>
                     [% END %]
+                     
+                    <tr><td colspan='2'>
+                    [% IF hold.metarecord_filters.formats.size OR
+                        hold.metarecord_filters.langs.size > 1;
+                            PROCESS metarecord_hold_filters_selector 
+                                hold_data=hold; END %]
+                    </td></tr>
+
                     <tr>
                         <td colspan="2" class="hold-editor-controls">
                             <a href="[% ctx.opac_root %]/myopac/holds"><button 
index d5a663f..eb863a7 100644 (file)
                 CASE "sort_selector";
                     INCLUDE "opac/parts/filtersort.tt2"
                         value=CGI.param('sort') class='results_header_sel';
+                    %]
 
-                CASE "copy_location" %]
+                    [% IF NOT metarecords.disabled %]
+                        <br/><!-- <br> may seem redundant, but it allows the
+                            <input> (below) to drop down inline w/ its label -->
+                        <div class="adv_search_available">
+                            <input type='checkbox' name="modifier" value="metabib"
+                              [%- CGI.param('modifier').grep('metabib').size ? 
+                                    ' checked="checked"' : '' %]
+                                id='opac.result.ismetabib' />
+                            <label for='opac.result.ismetabib'>
+                                [% l("Group Formats and Editions") %]</label>
+                        </div>
+                    [% END %]
+
+                [% CASE "copy_location" %]
                     <select id="adv_copy_location_selector" 
                         aria-label="[% l('Select Shelving Location') %]"
                         name="fi:locations" size="3" multiple="multiple">
index 77e1936..509d6a1 100644 (file)
@@ -162,4 +162,11 @@ ctx.google_books_preview = 0;
 #
 # ctx.maintenance_message = "The system will not be available February 29, 2104.";
 
+
+##############################################################################
+# Metarecords configuration
+# metarecords.disabled = 1; # disable all metarecord access points
+##############################################################################
+
+
 %]
diff --git a/Open-ILS/src/templates/opac/parts/metarecord_hold_filters.tt2 b/Open-ILS/src/templates/opac/parts/metarecord_hold_filters.tt2
new file mode 100644 (file)
index 0000000..452feed
--- /dev/null
@@ -0,0 +1,89 @@
+[%#
+Draws the format multi-select and the language multi-select for
+limiting the set of desired records for a given metarecord.
+%]
+
+<style>
+  /* TODO: MOVE ME */
+  .metarecord_filters {
+      padding: 5px;
+      margin-top: 5px;
+      border-bottom: 1px solid #333;
+      border-top: 1px solid #333;
+  }
+  .metarecord_filter_container {
+    float : left;
+    margin-right: 10px;
+  }
+  .metarecord_filter_container select {
+    padding: 2px;
+    width: 13em; /* consistent w/ adv search selectors */
+  }
+  .metarecord_filter_header {
+    padding-bottom: 5px;
+  }
+</style>
+
+[% BLOCK metarecord_hold_filters_selector;
+    # in edit mode, pull the target from the existing hold
+    target_id = hold_data.target.id || hold_data.hold.hold.target;
+
+    selected_formats = {};
+    selected_langs = {};
+    FOR fmt IN hold_data.metarecord_selected_filters.formats;
+        code = fmt.code;
+        selected_formats.$code = fmt;
+    END;
+    FOR lang IN hold_data.metarecord_selected_filters.langs;
+        code = lang.code;
+        selected_langs.$code = lang;
+    END;
+%]
+
+<div class="metarecord_filters">
+  <div class="metarecord_filter_container">
+    <div class="metarecord_filter_header">
+      <div>[% l('Select your desired format(s).') %]</div>
+    </div>
+    <select multiple='multiple' 
+      name="metarecord_formats_[% target_id %]">
+      [% FOR ccvm IN 
+        hold_data.metarecord_filters.formats.sort('search_label') %]
+        <option value="[% ccvm.code %]"[%- code = ccvm.code; 
+            IF selected_formats.$code %] selected='selected'[% END -%]>
+          [% ccvm.search_label | html %]
+        </option>
+      [% END %]
+    </select>
+  </div>
+  [% IF hold_data.metarecord_filters.langs.size;
+        my_lang = ctx.get_i18n_l(ctx.eg_locale).marc_code;
+  %]
+  <div class="metarecord_filter_container">
+    <div class="metarecord_filter_header">
+      [% l('Select your desired language(s)') %]
+    </div>
+    <select multiple='multiple' 
+      name="metarecord_langs_[% target_id %]">
+      [% FOR lang_ccvm IN hold_data.metarecord_filters.langs.sort('value') %]
+        [%  selected = 0; 
+            code = lang_ccvm.code;
+            IF selected_langs.size;
+                # user has already selected their preferred language(s)
+                SET selected = 1 IF selected_langs.$code;
+            ELSE;
+                # no prefered language selected, default to current locale
+                SET selected = 1 IF code == my_lang;
+            END;
+        %]
+        <option value="[% lang_ccvm.code %]"[%- 
+            IF selected %] selected='selected'[%- END %]>
+          [% lang_ccvm.value | html %]
+        </option>
+      [% END %]
+    </select>
+  </div>
+  [% END %]
+  <div class="clear-both">&nbsp;</div>
+</div>
+[% END # metarecord_hold_filters_selector %]
index 78898fb..589a2c1 100644 (file)
         END;
     END;
 
-    # Get CCVM labels
     BLOCK get_ccvm_label;
-        IF !ctx.ccvm_cache.$id;
-            fetch_ccvm = ctx.search_ccvm('id', id);
-            IF fetch_ccvm;
-                ctx.ccvm_cache.$id = fetch_ccvm.0;
-            END;
-        END;
-        IF search_label and ctx.ccvm_cache.$id.search_label;
-            ctx.ccvm_cache.$id.search_label;
+        ccvm = ctx.get_ccvm(id); # caches internally
+        IF search_label and ccvm.search_label;
+            ccvm.search_label;
         ELSE;
-            ctx.ccvm_cache.$id.value;
+            ccvm.$id.value;
         END;
     END;
 
 
         # "mattype" == "custom marc format specifier"
         icon_style = ctx.get_cgf('opac.icon_attr').value || 'item_type';
-        node = xml.findnodes(
-            '//*[local-name()="attributes"]/*[local-name()="field"][@name="' _ icon_style _ '"]');
-        IF node AND node.textContent;
-            type = node.textContent;
-            args.format_label = PROCESS get_ccvm_label id=node.getAttribute('cvmid') search_label=1;
-            IF !args.format_label;
-                args.format_label = node.getAttribute('coded-value');
+        formats_xpath = '//*[local-name()="attributes"]/*[local-name()="field"][@name="' _ icon_style _ '"]';
+
+        args.all_formats = [];
+        FOR node IN xml.findnodes(formats_xpath);
+            IF node AND node.textContent;
+                type = node.textContent;
+                label = PROCESS get_ccvm_label id=node.getAttribute('cvmid') search_label=1;
+                itemtype = schema_typemap.$type || 'CreativeWork';
+                icon = ctx.media_prefix _ '/images/format_icons/' _ icon_style _ '/' _ type _ '.png';
+                # collect all formats for metarecord support
+                args.all_formats.push({label => label, icon => icon, itemtype => itemtype});
+                IF !args.format_label;
+                    # use the first format as the default
+                    args.format_label = label; 
+                    args.schema.itemtype = itemtype;
+                    args.format_icon = icon;
+                END;
             END;
-            args.schema.itemtype = schema_typemap.$type || 'CreativeWork';
-            args.format_icon = ctx.media_prefix _ '/images/format_icons/' _ icon_style _ '/' _ type _ '.png';
-            LAST;
         END;
        
         args.bibid = [];
index f12cd56..390b9ea 100644 (file)
@@ -1,5 +1,6 @@
 [%  PROCESS "opac/parts/misc_util.tt2";
     PROCESS "opac/parts/hold_error_messages.tt2";
+    PROCESS "opac/parts/metarecord_hold_filters.tt2";
 %]
 
 <div id='holds_box' class='canvas' style='margin-top: 6px;'>
                         <input type='hidden' name='part' value=''/>
                         [% END %]
                     [% END %]
+                    [% IF NOT metarecords.disabled %]
+                        [% IF CGI.param('hold_type') == 'T' AND hdata.record.metarecord %]
+                            <a href="[% mkurl('', {hold_type => 'M', hold_target => hdata.record.metarecord.id}) %]">
+                                [% l('Advanced Hold Options') %]</a>
+                        [% END %]
+                        [% IF hdata.metarecord_filters.formats.size OR # should this be size > 1
+                            hdata.metarecord_filters.langs.size > 1;
+                            PROCESS metarecord_hold_filters_selector hold_data=hdata;
+                        END;
+                    END %]
                 </td>
             </tr>
         [% END %]
index 0922a70..b927de2 100644 (file)
                             IF CGI.param('detail_record_view');
                                 attrs.title = attrs.title_extended;
                             END;
+                            # note: rec.id refers to the record identifier, regardless
+                            # of the type of record. i.e. rec.id = mmr_id ? mmr_id : bre_id
+                            IF rec.mmr_id;
+                                IF rec.mr_has_multi;
+                                    # metarecords link to record list page
+                                    record_url = mkurl(ctx.opac_root _ '/results', 
+                                        {metarecord => rec.mmr_id}, ['page']);
+                                ELSE;
+                                    # for MR, bre_id refers to the master and in
+                                    # this case, only, record
+                                    record_url = mkurl(ctx.opac_root _ '/record/' _ rec.bre_id);
+                                END;
+                                hold_type = 'M';
+                            ELSE;
+                                record_url = mkurl(ctx.opac_root _ '/record/' _ rec.bre_id);
+                                hold_type = 'T';
+                            END;
                     -%]
                         <tr class="result_table_row">
                                             <td class="results_row_count" name="results_row_count">[%
                                                     result_count; result_count = result_count + 1
                                                 %].</td>
                                             <td class='result_table_pic_header'>
-                                                <a href="[% mkurl(ctx.opac_root _ '/record/' _ rec.id) %]"><img alt="[% l('Image of item') %]"
+                                                <a href="[% record_url %]"><img alt="[% l('Image of item') %]"
                                                         name='item_jacket' class='result_table_pic' width="55"
-                                                        src='[% ctx.media_prefix %]/opac/extras/ac/jacket/small/r/[% rec.id | uri %]' /></a><br />
+                                                        src='[% ctx.media_prefix %]/opac/extras/ac/jacket/small/r/[% rec.bre_id | uri %]' /></a><br />
                                             </td>
                                             <td class='result_table_title_cell' name='result_table_title_cell'>
                                                <div class="result_metadata">
-                                                    <abbr class="unapi-id" title='tag:[% ctx.hostname %],[% date.format(date.now, '%Y') %]:biblio-record_entry/[% rec.id %]'></abbr>
+                                                    [% IF rec.mmr_id %]
+                                                    <abbr class="unapi-id" 
+                                                      title='tag:[% ctx.hostname %],[% date.format(date.now, '%Y') %]:metabib-metarecord/[% rec.mmr_id %]'>
+                                                    </abbr>
+                                                    [% ELSE %]
+                                                    <abbr class="unapi-id" 
+                                                      title='tag:[% ctx.hostname %],[% date.format(date.now, '%Y') %]:biblio-record_entry/[% rec.bre_id %]'>
+                                                    </abbr>
+                                                    [% END %]
                                                     <a class='record_title search_link' name='record_[% rec.id %]'
-                                                        href="[% mkurl(ctx.opac_root _ '/record/' _ rec.id) %]"
+                                                        href="[% record_url %]"
                                                         [% html_text_attr('title', l('Display record details for "[_1]"', attrs.title)) %]
                                                         >[% attrs.title | html %]</a>
 [%-
@@ -96,7 +121,12 @@ END;
                                                     </div>
                                                     <div class='result_table_title_cell'>
                                                     [%- IF attrs.format_label; %]
-                                                        <img title="[% attrs.format_label | html %]" alt="[% attrs.format_label | html %]" src="[% attrs.format_icon %]" /> [% attrs.format_label; %]
+                                                        [% FOR format IN attrs.all_formats %]
+                                                            <img title="[% format.label | html %]" 
+                                                                alt="[% format.label | html %]" 
+                                                                src="[% format.icon %]" /> 
+                                                            [% format.label | html %]
+                                                        [% END %]
                                                     [%- END %]
                                                     [%- UNLESS CGI.param('detail_record_view')
                                                             OR (show_more_details.default == 'true'
@@ -294,7 +324,8 @@ END;
 %]
                                                         <div class="results_aux_utils place_hold"><a
                                                                 href="[% mkurl(ctx.opac_root _ '/place_hold',
-                                                                    {hold_target => rec.id, hold_type => 'T', hold_source_page => mkurl()}, ['query']) %]"
+                                                                    {hold_target => rec.id, hold_type => hold_type, 
+                                                                      hold_source_page => mkurl()}, ['query']) %]"
                                                                 [% html_text_attr('title', l('Place Hold on [_1]', attrs.title)) %]
                                                                     class="no-dec"><img
                                                                 src="[% ctx.media_prefix %]/images/green_check.png"
index d1a3da2..6bd1c94 100644 (file)
     [% INCLUDE "opac/parts/searchbar.tt2" took_care_of_form=1 %]
     <h3 class="sr-only">[% l('Additional search filters and navigation') %]</h3>
     <div class="almost-content-wrapper">
-        <div id="results_header_bar">
+
+        [%# hide the header bar when displaying metarecord constituents 
+          instead of skipping it altogether to allow the search form
+          variables to propagate %]
+        [% IF ctx.metarecord;
+          mr_attrs = {marc_xml => ctx.metarecord_master.marc_xml};
+          PROCESS get_marc_attrs args=mr_attrs %]
+          <div class="results_header_lbl">
+            [% l('Viewing Results for Grouped Record: [_1]', 
+                mr_attrs.title) | html %]
+          </div>
+        [% END %]
+        <div class="results_header_bar[%- IF ctx.metarecord %] hidden[% END -%]">
             <div id="results_header_inner">
                 <div class="results_header_btns">
                     <a href="[% mkurl(ctx.opac_root _ '/home', {$loc_name => loc_value}, 1) %]">[% l('Another Search') %]</a>
 
                     <label class="results_header_lbl" for="limit_to_available">
                         <input type="checkbox" id="limit_to_available" name="modifier" value="available"
-                            onchange="limit_to_avail_onchange(this, true)"
+                            onchange="search_modifier_onchange('available', this, true)"
                             [% CGI.param('modifier').grep('available').size ? ' checked="checked"' : '' %] />
                         [% l('Limit to available items') %]
                     </label>
+                    [% IF NOT metarecords.disabled %]
+                        <label class="results_header_lbl">
+                            <input type="checkbox" name="modifier" value="metabib"
+                                onchange="search_modifier_onchange('metabib', this, true)"
+                                [% CGI.param('modifier').grep('metabib').size ? ' checked="checked"' : '' %] />
+                            [% l('Group Formats and Editions') %]
+                        </label>
+                    [% END %]
                     [% IF CGI.param('detail_record_view') %]
                         <input type="hidden" name="detail_record_view" value="1" />
                     [% END %]
index 6e3d29f..b491586 100644 (file)
@@ -74,10 +74,11 @@ function select_all_checkboxes(name, checked) {
     }
 }
 
-function limit_to_avail_onchange(checkbox, submitOnChange) {
+function search_modifier_onchange(type, checkbox, submitOnChange) {
     if (checkbox.form._adv && !checkbox.checked) {
         var search_box = $('search_box');
-        search_box.value = search_box.value.replace(/#available ?/g, "");
+        var reg = new RegExp('#' + type + ' ?', 'g');
+        search_box.value = search_box.value.replace(reg, "");
     }
 
     if (submitOnChange) {