Add structured data for holdings via http://schema.org/Offer
authorDan Scott <dscott@laurentian.ca>
Sun, 25 Aug 2013 04:40:46 +0000 (00:40 -0400)
committerBen Shum <bshum@biblio.org>
Mon, 26 Aug 2013 16:16:25 +0000 (12:16 -0400)
Map library name to Offer/seller, shelving location to
Offer/availableAtOrFrom, call number to Offer/sku, barcode to
Offer/serialNumber, copy status to Offer/availability, and ISBN-13 to
gtin13. Use the additionalType of Product to give these offers an
obvious relationship.

Surface copy counts as AggregateOffer instances.

Cut over to RDFa Lite instead of microdata for schema.org, as RDFa Lite
is more easily extensible with other vocabularies, and is as accepted as
microdata by schema.org processors.

Signed-off-by: Dan Scott <dscott@laurentian.ca>
Signed-off-by: Ben Shum <bshum@biblio.org>
Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm
Open-ILS/src/templates/opac/parts/ac_google_books.tt2
Open-ILS/src/templates/opac/parts/misc_util.tt2
Open-ILS/src/templates/opac/parts/record/authors.tt2
Open-ILS/src/templates/opac/parts/record/body.tt2
Open-ILS/src/templates/opac/parts/record/contents.tt2
Open-ILS/src/templates/opac/parts/record/copy_counts.tt2
Open-ILS/src/templates/opac/parts/record/copy_table.tt2
Open-ILS/src/templates/opac/parts/record/subjects.tt2
Open-ILS/src/templates/opac/parts/record/summary.tt2

index 00d7bf2..5a6d385 100644 (file)
@@ -1961,6 +1961,7 @@ sub basic_opac_copy_query {
                 {column => 'holdable', alias => 'location_holdable'}
             ],
             ccs => [
+                {column => 'id', alias => 'status_code'},
                 {column => 'name', alias => 'copy_status'},
                 {column => 'holdable', alias => 'status_holdable'}
             ],
index 3dbb5f9..cbec24f 100644 (file)
@@ -100,7 +100,7 @@ function GBShowHidePreview(from_button) {
 dojo.addOnLoad(function() {
   var spans = dojo.query('li.rdetail_isbns span.rdetail_value');
   for (var i = 0; i < spans.length; i++) {
-    var prop = spans[i].getAttribute('itemprop');
+    var prop = spans[i].getAttribute('property');
     if (!prop) {
       continue;
     }
index 3f31205..363318d 100644 (file)
         schema_typemap.e = 'http://schema.org/Map';
         schema_typemap.j = 'http://schema.org/MusicAlbum';
 
+        # Hard-coded to match defaults in config.copy_status for all OPAC-visible statuses
+        schema_copy_status = {};
+        schema_copy_status.0 = '<link property="availability" href="http://schema.org/InStock" />'; # Available
+        schema_copy_status.1 = '<link property="availability" href="http://schema.org/OutOfStock" />'; # Checked out
+        schema_copy_status.5 = '<link property="availability" href="http://schema.org/PreOrder" />'; # In process
+        schema_copy_status.6 = '<link property="availability" href="http://schema.org/PreOrder" />'; # In transit
+        schema_copy_status.7 = '<link property="availability" href="http://schema.org/InStock" />'; # Reshelving
+        schema_copy_status.8 = '<link property="availability" href="http://schema.org/OutOfStock" />'; # On holds shelf
+        schema_copy_status.9 = '<link property="availability" href="http://schema.org/PreOrder" />'; # On order
+        schema_copy_status.12 = '<link property="availability" href="http://schema.org/InStoreOnly" />'; # Reserves
+
         args.isbns = [];
         FOR isbn IN xml.findnodes('//*[@tag="020"]/*[@code="a"]');
             args.isbns.push(isbn.textContent);
 
         # clean up the ISBN
         args.isbn_clean = args.isbns.0.replace('\ .*', '');
+        FOR isbn IN args.isbns;
+            clean_isbn = isbn.replace('\ .*', '');
+            clean_isbn = clean_isbn.replace('-', '');
+            IF clean_isbn.length == 13;
+                args.gtin13 = clean_isbn;
+                LAST;
+            END;
+        END;
 
         # Extract the 856 URLs that are not otherwise represented by asset.uri's
         args.online_res = [];
                         location => loc.textContent,
                         library => circlib.textContent,
                         status => status.textContent,
+                        status_code => status.getAttribute('ident'),
                         barcode => copy.getAttribute('barcode'),
                         owner => volume.getAttribute('lib')
                     };
             IF node AND node.textContent;
                 type = node.textContent;
                 args.format_label = node.getAttribute('coded-value')
-                args.schema.itemtype = schema_typemap.$type;
+                args.schema.itemtype = schema_typemap.$type || 'CreativeWork';
                 args.format_icon = ctx.media_prefix _ '/images/format_icons/' _ icon_style _ '/' _ type _ '.png';
                 LAST;
             END;
index 005b48a..6d25b3c 100644 (file)
@@ -68,28 +68,28 @@ BLOCK build_author_links;
         # schema.org changes
         IF type == 'author';
             IF tag.substr(1,2) == '10' && args.schema.itemtype && args.schema.itemtype.match('MusicAlbum');
-                iprop = ' itemtype="http://schema.org/MusicGroup" itemscope itemprop="byArtist"';
+                iprop = ' typeof="MusicGroup" property="byArtist"';
             ELSIF tag.substr(1,2) == '00';
-                iprop = ' itemtype="http://schema.org/Person" itemscope itemprop="author"';
+                iprop = ' typeof="Person" property="author"';
             ELSE;
-                iprop = ' itemtype="http://schema.org/Organization" itemscope itemprop="author"';
+                iprop = ' typeof="Organization" property="author"';
             END;
         ELSIF type == 'added';
             IF tag.substr(1,2) == '00';
-                iprop = ' itemtype="http://schema.org/Person" itemscope itemprop="contributor"';
+                iprop = ' typeOf="Person" property="contributor"';
             ELSE;
-                iprop = ' itemtype="http://schema.org/Organization" itemscope itemprop="contributor"';
+                iprop = ' typeOf="Organization" property="contributor"';
             END;
         END;
         '<a href="' _ url _ '"' _ iprop _ '>';
-        IF iprop; '<span itemprop="name">'; END;
+        IF iprop; '<span property="name">'; END;
         term.replace('^\s+', '');
         IF iprop; '</span>'; END;
         IF birthdate;
-            ' <span itemprop="birthDate">' _ birthdate _ '</span>-';
+            ' <span property="birthDate">' _ birthdate _ '</span>-';
         END;
         IF deathdate;
-            '<span itemprop="deathDate">' _ deathdate _ '</span>';
+            '<span property="deathDate">' _ deathdate _ '</span>';
         END;
         '</a>';
         FOREACH link880 IN graphics;
index 2e74d66..b8ba910 100644 (file)
@@ -4,7 +4,7 @@
     stop_parms = ['expand','cnoffset','copy_offset','copy_limit'];
     ctx.record_attrs = attrs; # capture for JS
 %]
-<div id='canvas_main' class='canvas' itemscope itemtype='[% args.schema.itemtype %]'>
+<div id='canvas_main' class='canvas' vocab="http://schema.org/" typeof='[% args.schema.itemtype %] Product'>
     [%- INCLUDE "opac/parts/record/navigation.tt2" %]
     [%- IF ctx.bib_is_dead %]
     <div id='rdetail_deleted_exp'>
index fcf9e54..29fc33b 100644 (file)
@@ -187,7 +187,7 @@ END
 -%]
 <tr>
     <td class='rdetail_content_type'>[% cont.label %]</td>
-    <td class='rdetail_content_value' itemprop='keywords'>[% content %]</td>
+    <td class='rdetail_content_value' property='keywords'>[% content %]</td>
 </tr>
         [%- END; %]
     [%- END; %]
index a400aad..fd97eab 100644 (file)
@@ -17,7 +17,9 @@
             ou_name = cp_org_unit.name;
             displayed_ous.$ou_name = 1;
     %]
-    <li>
+    <li property="offers" typeof="AggregateOffer">
+        <meta property="offerCount" content="[% ou_avail %]">
+        <meta property="seller" content="[% ou_name | html %]">
     [% l('[quant,_1,copy,copies] at [_2].', ou_avail, ou_name) | html %]
     [%- this_depth = ctx.get_aou(ou_id).ou_type.depth;
         IF ou_avail > 0 && this_depth != ctx.copy_depth %]
         UNLESS depth < 0 || displayed_ous.exists(ou_name);
     %]
     [%- IF attrs.plib_copy_counts.$depth.count > 0; %]
-    <li class="preferred">[%
+    <li class="preferred" property="offers" typeof="AggregateOffer">
+        <meta property="offerCount" content="[% ou_avail %]">
+        <meta property="seller" content="[% ou_name | html %]">
+    [%-
         l('[_1] of [quant,_2,copy,copies] available at [_3].',
             attrs.plib_copy_counts.$depth.available,
             attrs.plib_copy_counts.$depth.count,
index 5bab2b7..57d1d41 100644 (file)
@@ -89,11 +89,11 @@ END; # FOREACH bib
                 callnum = callnum  _ " " _ callnum_suffix;
             END;
         -%]
-        <tr>
+        <tr property="offers" typeof="Offer">
             [%- IF serial_holdings %]<td header='copy_header_holding_label' class='rdetail-issue-issue'>
                 [%- copy_info.holding_label | html; -%]
             </td>
-            [%- ELSE %]<td header='copy_header_library'>
+            [%- ELSE %]<td header='copy_header_library' property="seller">
             [%-
                 org_name = ctx.get_aou(copy_info.circ_lib).name;
                 lib_url = ctx.get_org_setting(copy_info.circ_lib, 'lib.info_url');
@@ -101,17 +101,22 @@ END; # FOREACH bib
                 org_name | html;
                 IF lib_url; '</a>'; END;
             -%]
+                <link property="businessFunction" href="http://purl.org/goodrelations/v1#LeaseOut">
             </td>[% END %]
-            <td header='copy_header_callnumber'>[% callnum | html %] [% IF ctx.get_org_setting(CGI.param('loc') OR ctx.aou_tree.id, 'sms.enable') == 1 %](<a href="[% mkurl(ctx.opac_root _ '/sms_cn', {copy_id => copy_info.id}) %]">Text</a>)[% END %]</td>
+            <td header='copy_header_callnumber'><span property="sku">[% callnum | html %]</span> [% IF ctx.get_org_setting(CGI.param('loc') OR ctx.aou_tree.id, 'sms.enable') == 1 %](<a href="[% mkurl(ctx.opac_root _ '/sms_cn', {copy_id => copy_info.id}) %]">Text</a>)[% END %]</td>
             [%- IF has_parts == 'true' %]
             <td header='copy_header_part'>[% copy_info.part_label | html %]</td>
             [%- END %]
-            <td header='copy_header_barcode'>
+            <td header='copy_header_barcode' property="serialNumber">
                 [%- IF ctx.is_staff -%]
                     <a href="javascript:void(0)" onclick="xulG.new_tab(xulG.urls.XUL_COPY_STATUS, {}, {'from_item_details_new': true, 'barcodes': ['[%- copy_info.barcode | html | replace('\'', '\\\'') -%]']})">[% copy_info.barcode | html %]</a>
                 [%- ELSE -%][% copy_info.barcode | html %]
-                [%- END -%]</td>
-            <td header='copy_header_shelfloc'>[% copy_info.copy_location | html %]</td>
+                [%- END -%]
+                [%- IF attrs.gtin13;
+                    '<meta property="gtin13" content="' _ attrs.gtin13 _ '" />';
+                END; -%]
+            </td>
+            <td header='copy_header_shelfloc' property="availableAtOrFrom">[% copy_info.copy_location | html %]</td>
             [%- IF ctx.is_staff %]
             <td header='copy_header_age_hold'>
                 [% copy_info.age_protect ?
@@ -171,7 +176,10 @@ END; # FOREACH bib
                     l("Not holdable");
                 END %]</td>
             [%- END %]
-            <td header='copy_header_status'>[% copy_info.copy_status | html %]</td>
+            <td header='copy_header_status'>[%-
+                schema_copy_status.${copy_info.status_code};
+                copy_info.copy_status | html;
+            -%]</td>
             <td header='due_date'>[%
                 IF copy_info.due_date;
                     date.format(
@@ -185,7 +193,7 @@ END; # FOREACH bib
 
         [% IF copy_info.notes; %]
             [% FOREACH note IN copy_info.notes; %]
-                <tr><td>&nbsp;</td><td class="copy_note" colspan="4"><strong>[% note.title | html %]:</strong> [% note.value | html %]</td></tr>
+                <tr><td>&nbsp;</td><td class="copy_note" colspan="4" property="description"><strong>[% note.title | html %]:</strong> [% note.value | html %]</td></tr>
             [% END %]
         [% END %]
 
index 3914908..5812690 100644 (file)
@@ -68,7 +68,7 @@
             <tbody>
                 <tr>
                     <td class='rdetail_subject_type'>[% subj.label %]</td>
-                    <td class='rdetail_subject_value' itemprop='keywords'>[% content %]</td>
+                    <td class='rdetail_subject_value' property='keywords'>[% content %]</td>
                 </tr>
             </tbody>
         </table>
index a38bd54..b9a91a8 100644 (file)
@@ -61,7 +61,7 @@
             <img alt="[% attrs.format_label %]" title="[% attrs.format_label | html %]" src="[% attrs.format_icon %]" />
         </div>
         [%- END %]
-        <h1 id='rdetail_title' itemprop="name">[% attrs.title_extended | html %]</h1>
+        <h1 id='rdetail_title' property="name">[% attrs.title_extended | html %]</h1>
         [%-
             FOR link880 IN attrs.graphic_titles;
                 FOR alt IN link880.graphic;
@@ -108,8 +108,14 @@ IF num_uris > 0;
 <div class="rdetail_uris">
     [%- IF num_uris > 1 %]<ul>[% END %]
     [%- FOR uri IN merged_uris %]
-        [%- IF num_uris == 1 %]<p class="rdetail_uri">[% ELSE %]<li class="rdetail_uri">[% END %]
+        [%- IF num_uris == 1 -%]
+            <p class="rdetail_uri" property="offers" vocab="http://schema.org/" typeof="Offer">
+        [%- ELSE -%]
+            <li class="rdetail_uri" property="offers" vocab="http://schema.org/" typeof="Offer">
+        [%- END -%]
         <a href="[% uri.href %]">[% uri.link %]</a>[% ' - ' _ uri.note IF uri.note %]
+        <link property="availability" href="http://schema.org/OnlineOnly" />
+        [%- IF attrs.gtin13; '<meta property="gtin13" content="' _ attrs.gtin13 _ '" />'; END; %]
         [%- IF num_uris == 1 %]</p>[% ELSE %]</li>[% END %]
     [%- END %]
     [%- IF num_uris > 1 %]</ul>[% END %]
@@ -148,7 +154,7 @@ IF num_uris > 0;
     [%- IF attrs.isbns.0; FOR isbn IN attrs.isbns %]
     <li class='rdetail_isbns'>
         <strong class='rdetail_label'>[% l('ISBN:'); %]</strong>
-        <span class='rdetail_value' itemprop='isbn'>[% isbn | html  %]</span>
+        <span class='rdetail_value' property='isbn'>[% isbn | html  %]</span>
     </li>
         [%- END %]
     [%- END %]
@@ -189,14 +195,14 @@ IF num_uris > 0;
     [%- IF attrs.publisher %]
     <li id='rdetail_publisher'>
         <strong class='rdetail_label'>[% l("Publisher:") %]</strong>
-        <span class='rdetail_value' itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
+        <span class='rdetail_value' property="publisher" typeof="Organization">
         [%- IF attrs.pubplace; %]
-            <span itemprop="location">[% attrs.pubplace | html; %]</span>
+            <span property="location">[% attrs.pubplace | html; %]</span>
         [%- END; %]
-            <span itemprop="name">[% attrs.publisher | html; %]</span>
+            <span property="name">[% attrs.publisher | html; %]</span>
         </span>
         [%- IF attrs.pubdate; %]
-            <span itemprop="datePublished">[% attrs.pubdate | html; %]</span>
+            <span property="datePublished">[% attrs.pubdate | html; %]</span>
         [%- END; %]
         [%-
         IF attrs.graphic_pubinfos.size > 0;