TPAC added content integration
authorBill Erickson <berick@esilibrary.com>
Mon, 14 May 2012 19:22:08 +0000 (15:22 -0400)
committerJeff Godin <jgodin@tadl.org>
Thu, 26 Jul 2012 09:05:18 +0000 (05:05 -0400)
https://bugs.launchpad.net/evergreen/+bug/984963

This adds a new tab on the detail page called Additional Content
(suggestions welcome).  When the tab is expanded, available content is
presented to the user via a series of sub-tabs.

At the start of loading the record detail page, kick off a series of
asynchronous HTTP HEAD requests, one per type of added content.  At the
end of context loading, read the results of the HTTP requests for any
that have completed.  If the status for a type is 200, the type is
marked as available.  If it's not 200 (usually 404) it's marked as not
available.  Otherwise, it's marked as unknown.

In the template, available content produces a link the user can click
to view the content.  Non-available content produces no links.  Unknown
content produces a link that may lead to content or no content when
JS/dojo is disabled.  When dojo is enabled, unknown content sends a
series of JS queries to determine the state of the content
asynchronously and results in displayed links for any type that is later
determined to have content.

TODO: when the user expands the Additional Content tab, no type is
chosen by default.  A default (available) type needs to be selected on
initial display.

TODO: better styling, particularly in the sub-tabs.

Signed-off-by: Bill Erickson <berick@esilibrary.com>
Signed-off-by: Mike Rylander <mrylander@gmail.com>
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Record.pm
Open-ILS/src/templates/opac/parts/js.tt2
Open-ILS/src/templates/opac/parts/record/addedcontent.tt2 [new file with mode: 0644]
Open-ILS/src/templates/opac/parts/record/body.tt2
Open-ILS/src/templates/opac/parts/record/extras.tt2
Open-ILS/web/css/skin/default/opac/style.css

index af1c3fc..f7d09f6 100644 (file)
@@ -5,8 +5,12 @@ use OpenSRF::Utils::Logger qw/$logger/;
 use OpenILS::Utils::CStoreEditor qw/:funcs/;
 use OpenILS::Utils::Fieldmapper;
 use OpenILS::Application::AppUtils;
+use Net::HTTP::NB;
+use IO::Select;
 my $U = 'OpenILS::Application::AppUtils';
 
+our $ac_types = ['toc',  'anotes', 'excerpt', 'summary', 'reviews'];
+
 # context additions: 
 #   record : bre object
 sub load_record {
@@ -15,6 +19,13 @@ sub load_record {
     $ctx->{page} = 'record';  
 
     $self->timelog("load_record() began");
+
+    my $rec_id = $ctx->{page_args}->[0]
+        or return Apache2::Const::HTTP_BAD_REQUEST;
+
+    $self->added_content_stage1($rec_id);
+    $self->timelog("past added content stage 1");
+
     my $org = $self->_get_search_lib();
     my $org_name = $ctx->{get_aou}->($org)->shortname;
     my $pref_ou = $self->_get_pref_lib();
@@ -29,9 +40,6 @@ sub load_record {
     my $copy_limit = int($self->cgi->param('copy_limit') || 10);
     my $copy_offset = int($self->cgi->param('copy_offset') || 0);
 
-    my $rec_id = $ctx->{page_args}->[0]
-        or return Apache2::Const::HTTP_BAD_REQUEST;
-
     $self->get_staff_search_settings;
     if ($ctx->{staff_saved_search_size}) {
         $ctx->{saved_searches} = ($self->staff_load_searches)[1];
@@ -126,6 +134,11 @@ sub load_record {
     }
 
     $self->timelog("past expandies");
+
+    $self->added_content_stage2($rec_id);
+
+    $self->timelog("past added content stage 2");
+
     return Apache2::Const::OK;
 }
 
@@ -398,4 +411,114 @@ sub load_email_record {
     return Apache2::Const::OK;
 }
 
+# for each type, fire off the reqeust to see if content is available
+# ctx.added_content.$type.status:
+#   1 == available
+#   2 == not available
+#   3 == unknown
+sub added_content_stage1 {
+    my $self = shift;
+    my $rec_id = shift;
+    my $ctx = $self->ctx;
+    my $sel_type = $self->cgi->param('ac') || '';
+    my $key = $self->get_ac_key($rec_id);
+    ($key = $key->{value}) =~ s/^\s+//g if $key;
+
+    $ctx->{added_content} = {};
+    for my $type (@$ac_types) {
+        $ctx->{added_content}->{$type} = {content => ''};
+        $ctx->{added_content}->{$type}->{status} = $key ? 3 : 2;
+
+        if ($key) {
+            $logger->debug("tpac: starting added content request for $key => $type");
+
+            my $req = Net::HTTP::NB->new(Host => $self->apache->hostname);
+
+            if (!$req) {
+                $logger->warn("Unable to fetch added content from " . $self->apache->hostname . ": $@");
+                next;
+            }
+
+            my $http_type = ($type eq $sel_type) ? 'GET' : 'HEAD';
+            $req->write_request($http_type => "/opac/extras/ac/$type/html/$key");
+            $ctx->{added_content}->{$type}->{request} = $req;
+        }
+    }
+}
+
+# check each outstanding request.  If it's ready, read the HTTP 
+# status and use it to determine if content is available.  Otherwise,
+# leave the status as unknown.
+sub added_content_stage2 {
+    my $self = shift;
+    my $ctx = $self->ctx;
+    my $sel_type = $self->cgi->param('ac') || '';
+
+    for my $type (keys %{$ctx->{added_content}}) {
+        my $content = $ctx->{added_content}->{$type};
+
+        if ($content->{status} == 3) {
+            $logger->debug("tpac: finishing added content request for $type");
+
+            my $req = $content->{request};
+            my $sel = IO::Select->new($req);
+
+            # if we are requesting a specific type of content, give the 
+            # backend code a little extra time to retrieve the content.
+            my $wait = $type eq $sel_type ? 3 : 0; # TODO: config?
+
+            if ($sel->can_read($wait)) {
+                my ($code) = $req->read_response_headers;
+                $content->{status} = $code eq '200' ? 1 : 2;
+                $logger->debug("tpac: added content request for $type returned $code");
+
+                if ($type eq $sel_type) {
+                    while (1) {
+                        my $buf;
+                        my $n = $req->read_entity_body($buf, 1024);
+                        last unless $n;
+                        $content->{content} .= $buf;
+                    }
+                }
+            }
+        }
+    }
+}
+
+# XXX this is copied directly from AddedContent.pm in 
+# working/user/jeff/ac_by_record_id_rebase.  When Jeff's
+# branch is merged and Evergreen gets added content 
+# lookup by ID, this can be removed.
+# returns [{tag => $tag, value => $value}, {tag => $tag2, value => $value2}]
+sub get_ac_key {
+    my $self = shift;
+    my $rec_id = shift;
+    my $key_data = $self->editor->json_query({
+        select => {mfr => ['tag', 'value']},
+        from => 'mfr',
+        where => {
+            record => $rec_id,
+            '-or' => [
+                {
+                    '-and' => [
+                        {tag => '020'},
+                        {subfield => 'a'}
+                    ]
+                }, {
+                    '-and' => [
+                        {tag => '024'},
+                        {subfield => 'a'},
+                        {ind1 => 1}
+                    ]
+                }
+            ]
+        }
+    });
+
+    return (
+        grep {$_->{tag} eq '020'} @$key_data,
+        grep {$_->{tag} eq '024'} @$key_data
+    )[0];
+}
+
 1;
index 0060bbe..4e31a90 100644 (file)
 </script>
 [% END; # use_autosuggest %]
 
+<script type="text/javascript">
+    [% ac_types = ['toc',  'anotes', 'excerpt', 'summary', 'reviews'] %]
+
+    /* Checks to see if a given type of added content has data to show.
+     * The first arg to callback() is boolean indicating the presence of data.
+     */
+    function acIsAvailable(ident, type, callback) {
+        var url = '/opac/extras/ac/' + type + '/html/' + ident;
+        dojo.xhr('HEAD', {
+            url : url, 
+            failOk : true, // http://bugs.dojotoolkit.org/ticket/11568
+            error : function(err) { callback(false, ident, type); },
+            load : function(result) { callback(true, ident, type); }
+        });
+    }
+
+    [% 
+        IF ctx.page == 'record';
+            # XXX revisit when ident=ctx.bre_id
+            ident = ctx.record_attrs.isbn_clean || ctx.record_attrs.upc; 
+            IF ident; 
+                FOR type IN ac_types;
+                    IF ctx.added_content.$type.status == '3' # status unknown %]
+                        dojo.addOnLoad(function() {
+                            var ident = '[% ident %]';
+                            var type = '[% type %]';
+                            acIsAvailable(ident, type, function(avail, ident, type) { 
+                                // if the content is available, un-hide the tab
+                                if (avail) dojo.removeClass(dojo.byId('ac:' + type), 'hidden');
+                            });
+                        });
+                    [% END; # IF status unknown
+                END; 
+            END; # IF ident
+        END;
+    %]
+</script>
+
 [%- END; # want_dojo -%]
diff --git a/Open-ILS/src/templates/opac/parts/record/addedcontent.tt2 b/Open-ILS/src/templates/opac/parts/record/addedcontent.tt2
new file mode 100644 (file)
index 0000000..21e81ef
--- /dev/null
@@ -0,0 +1,49 @@
+<div class='rdetail_extras_div'> 
+
+[% 
+    ac_types = {
+        reviews => l('Reviews'),
+        anotes  => l('Author Notes'),
+        toc     => l('Table of Contents'),
+        excerpt => l('Excerpt'),
+        summary => l('Summary')
+    };
+
+    selected_type = CGI.param('ac');
+    
+    # For each type of added content, render the link if it's known to have
+    # content, do not render the link if it's known to not have content.  If 
+    # the content status is unknown, render the link, but hide the link via CSS
+    # if dojo is enabled.  If dojo is not enabled, render and display the link.
+%]
+
+    <div id='ac_tab_wrapper'>
+        [% FOR type IN ac_types.keys;
+            tab_class = 'ac_tab';
+            IF ctx.added_content.$type.status != '2'; # no content
+                IF ctx.added_content.$type.status == '3' AND want_dojo; # status unknown
+                    tab_class = tab_class _ ' hidden';
+                END %]
+            <div class="[% tab_class %]" id="ac:[% type %]">
+                <a href="[% mkurl('', {ac => type}) _ '#addedcontent' %]">[% ac_types.$type %]</a>
+            </div>
+            [% END;
+        END %]
+    </div>
+
+    <div id='ac_content'>
+        <hr/>
+        [% 
+            IF selected_type; 
+                content = ctx.added_content.$selected_type.content;
+                IF content;
+                    content;
+                ELSE;
+                    l('No Content Available');
+                END;
+            END;
+        %]
+    </div>
+</div>
+
+
index 183cf16..0d9a58b 100644 (file)
@@ -2,6 +2,7 @@
     PROCESS "opac/parts/misc_util.tt2";
     PROCESS get_marc_attrs args=attrs;
     stop_parms = ['expand','cnoffset'];
+    ctx.record_attrs = attrs; # capture for JS
 %]
 <div id='canvas_main' class='canvas' itemscope itemtype='[% args.schema.itemtype %]'>
     [%- INCLUDE "opac/parts/record/navigation.tt2" %]
index 4a473c3..63f82ee 100644 (file)
@@ -22,6 +22,7 @@
             {name => 'excerpt',  label => l('Excerpt'), hide => 1},
             {name => 'issues',   label => l('Issues Held'), hide => !(ctx.have_holdings_to_show || ctx.have_mfhd_to_show)},
             {name => 'preview',  label => l('Preview'), hide => 1}, 
+            {name => 'addedcontent',  label => l('Additional Content')},  # hide if all content is known to not exist
             {name => 'cnbrowse', label => l('Shelf Browser')},
             {name => 'marchtml', label => l('MARC Record')}
         ];
@@ -40,7 +41,7 @@
             <div class="rdetail_extras_link">
                 [%  
                     IF tab_is_active(name);
-                        href = mkurl('', {}, ['expand']);
+                        href = mkurl('', {}, ['expand', 'ac']);
                         img_url = ctx.media_prefix _ '/images/rdetail_arrow_down.png';
                     ELSE;
                         href = mkurl('', {expand => name}) _ '#' _ name; 
index 6704721..def97d0 100644 (file)
@@ -1446,6 +1446,10 @@ a.preflib_change {
     border: thick solid red;
 }
 
+#ac_tab_wrapper { width : 100%; }
+.ac_tab { float: left; padding-right: 10px; font-size: 110%;  }
+#ac_content { clear: both; width: 100%; margin-top: 10px; }
+
 #locale_picker_form {
     float: right;
     padding: 0.5em 1em 0.5em 0;