From 2c38d582aa4b536cfcada67b880f07638692ebf4 Mon Sep 17 00:00:00 2001
From: Pasi Kallinen <pasi.kallinen@joensuu.fi>
Date: Mon, 20 Aug 2018 09:51:55 +0300
Subject: [PATCH] Bug 11175: Show record component parts in the detail view
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Shows the component records of a host, on the hosts detail view in
staff client or OPAC, with clickable links to the component records.

The host does not require linking entries to the components, but
components do require a link to the host record via 773$w.

Adds a new search index, Control-number-identifier (aka cni), which
indexes the 003 controlfield.

Adds 'Yet Another System Preference', ShowComponentRecords, which can
be used to turn this feature on or off in staff client and/or OPAC,
and defaults to off.

When looking up the component part records, the code searches for
records with (773$w=Host001 and 003=Host003) or 773$w='Host003 Host001'
or, if the 003 is not defined in the Host, 773$w=Host001.

Does not use easyanalytics or useControlNumber.

Only for MARC21 biblios - UNIMARC has not been updated.

staff-global.css and opac.css have not been recreated, so you need
to use sass to recreate those from staff-global.scss and opac.scss

Test plan:

0) Apply patch
1) perl bulkmarcimport -file /tmp/easypiano.mrc -m MARCXML
   (This file is an attachment on the bug)
2) rebuild the zebra biblio index
3) Search for "easy piano" in staff client, and go to
   the biblio detail page. You should not see anything different
   in the record detail page.
4) Do the same on OPAC.
5) Change the ShowComponentRecords syspref appropriately and check
   the record detail page in staff client and OPAC.
   You should see a list of component part records.

Rebased-by: Joonas Kylmälä <joonas.kylmala@helsinki.fi>

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Pasi Kallinen <pasi.kallinen@koha-suomi.fi>
---
 C4/Search.pm                                  |  2 +
 C4/XSLT.pm                                    | 23 ++++++-
 Koha/Biblio.pm                                | 45 +++++++++++++
 etc/zebradb/biblios/etc/bib1.att              |  1 +
 etc/zebradb/ccl.properties                    |  3 +
 .../marc21/biblios/biblio-koha-indexdefs.xml  |  3 +
 .../marc21/biblios/biblio-zebra-indexdefs.xsl |  5 ++
 .../data/mysql/atomicupdate/bug_11175.perl    |  6 ++
 installer/data/mysql/mandatory/sysprefs.sql   |  1 +
 .../prog/css/src/staff-global.scss            |  8 +++
 .../admin/preferences/staff_interface.pref    |  9 +++
 .../en/xslt/MARC21slim2intranetDetail.xsl     |  2 +
 .../prog/en/xslt/MARC21slimUtils.xsl          | 66 +++++++++++++++++++
 .../opac-tmpl/bootstrap/css/src/opac.scss     |  8 +++
 .../en/xslt/MARC21slim2OPACDetail.xsl         |  2 +
 .../bootstrap/en/xslt/MARC21slimUtils.xsl     | 66 +++++++++++++++++++
 16 files changed, 249 insertions(+), 1 deletion(-)
 create mode 100644 installer/data/mysql/atomicupdate/bug_11175.perl

diff --git a/C4/Search.pm b/C4/Search.pm
index 811e6c1809..f0990a0be6 100644
--- a/C4/Search.pm
+++ b/C4/Search.pm
@@ -1075,6 +1075,8 @@ sub getIndexes{
                     'Conference-name-seealso',
                     'Content-type',
                     'Control-number',
+                    'Control-number-identifier',
+                    'cni',
                     'copydate',
                     'Corporate-name',
                     'Corporate-name-heading',
diff --git a/C4/XSLT.pm b/C4/XSLT.pm
index 1b3941c7fb..e0ebcf545d 100644
--- a/C4/XSLT.pm
+++ b/C4/XSLT.pm
@@ -272,7 +272,28 @@ sub XSLTParse4Display {
     }
     $varxml .= "</variables>\n";
 
-    $xmlrecord =~ s/\<\/record\>/$itemsxml$sysxml$varxml\<\/record\>/;
+    my $partsxml = '';
+    # possibly insert component records into Detail views
+    if ($xslsyspref =~ m/Details/) {
+        my $showcomp = C4::Context->preference('ShowComponentRecords');
+        if ( $showcomp eq 'both' ||
+             ($showcomp eq 'staff' && $xslsyspref !~ m/OPAC/ ) ||
+             ($showcomp eq 'opac' && $xslsyspref =~ m/OPAC/  ) ) {
+            my $biblio = Koha::Biblios->find( $biblionumber );
+            if ( $biblio->components() ) {
+                my @componentPartRecordXML = ('<componentPartRecords>');
+                for my $cb ( @{ $biblio->components() } ) {
+                    # Remove the xml header
+                    $cb =~ s/^<\?xml.*?\?>//;
+                    push @componentPartRecordXML, decode('utf8', $cb);
+                }
+                push @componentPartRecordXML, '</componentPartRecords>';
+                $partsxml = join "\n", @componentPartRecordXML;
+            }
+        }
+    }
+
+    $xmlrecord =~ s/\<\/record\>/$itemsxml$sysxml$varxml$partsxml\<\/record\>/;
     if ($fixamps) { # We need to correct the ampersand entities that Zebra outputs
         $xmlrecord =~ s/\&amp;amp;/\&amp;/g;
         $xmlrecord =~ s/\&amp\;lt\;/\&lt\;/g;
diff --git a/Koha/Biblio.pm b/Koha/Biblio.pm
index 4d335e2c23..5ff2e88778 100644
--- a/Koha/Biblio.pm
+++ b/Koha/Biblio.pm
@@ -43,6 +43,8 @@ use Koha::Items;
 use Koha::Libraries;
 use Koha::Suggestions;
 use Koha::Subscriptions;
+use Koha::SearchEngine;
+use Koha::SearchEngine::Search;
 
 =head1 NAME
 
@@ -493,6 +495,49 @@ sub suggestions {
     return Koha::Suggestions->_new_from_dbic( $suggestions_rs );
 }
 
+=head3 components
+
+my $components = $self->components();
+
+Returns an array of MARCXML data, which are component parts of
+this object (MARC21 773$w points to this)
+
+=cut
+
+sub components {
+    my ($self) = @_;
+
+    return undef if (C4::Context->preference('marcflavour') ne 'MARC21');
+
+    if (!defined($self->{_components})) {
+        my $marc = C4::Biblio::GetMarcBiblio({ biblionumber => $self->id });
+        my $pf001 = $marc->field('001') || undef;
+        my $searcher = Koha::SearchEngine::Search->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
+
+        if (defined($pf001)) {
+            my $pf003 = $marc->field('003') || undef;
+            my $searchstr;
+
+            if (!defined($pf003)) {
+                # search for 773$w='Host001'
+                $searchstr = "rcn='".$pf001->data()."'";
+            } else {
+                # search for (773$w='Host001' and 003='Host003') or 773$w='Host003 Host001')
+                $searchstr = "(rcn='".$pf001->data()."' and cni='".$pf003->data()."')";
+                $searchstr .= " or rcn='".$pf003->data()." ".$pf001->data()."'";
+            }
+
+            my ( $errors, $results, $total_hits ) = $searcher->simple_search_compat( $searchstr, 0, undef );
+
+            $self->{_components} = $results if ( defined($results) && scalar(@$results) );
+        } else {
+            warn "Record $self->id has no 001";
+        }
+    }
+
+    return $self->{_components};
+}
+
 =head3 subscriptions
 
 my $subscriptions = $self->subscriptions
diff --git a/etc/zebradb/biblios/etc/bib1.att b/etc/zebradb/biblios/etc/bib1.att
index d14d617f93..57df88386b 100644
--- a/etc/zebradb/biblios/etc/bib1.att
+++ b/etc/zebradb/biblios/etc/bib1.att
@@ -221,6 +221,7 @@ att 9010    cn-suffix
 att 9011    Suppress
 att 9012    Identifier-other
 att 9013    not-onloan-count
+att 9014    Control-number-identifier
 
 # Items Index
 att 8001    withdrawn
diff --git a/etc/zebradb/ccl.properties b/etc/zebradb/ccl.properties
index 5c003d301d..53f042c184 100644
--- a/etc/zebradb/ccl.properties
+++ b/etc/zebradb/ccl.properties
@@ -996,6 +996,9 @@ llength 1=llength
 Summary 1=Summary
 not-onloan-count 1=9013 4=109
 
+Control-number-identifier 1=9014
+cni Control-number-identifier
+
 ###
 # Items Index
 withdrawn 1=8001
diff --git a/etc/zebradb/marc_defs/marc21/biblios/biblio-koha-indexdefs.xml b/etc/zebradb/marc_defs/marc21/biblios/biblio-koha-indexdefs.xml
index e1941793c9..474ac6cd55 100644
--- a/etc/zebradb/marc_defs/marc21/biblios/biblio-koha-indexdefs.xml
+++ b/etc/zebradb/marc_defs/marc21/biblios/biblio-koha-indexdefs.xml
@@ -16,6 +16,9 @@
   <index_control_field tag="001">
     <target_index>Control-number:w</target_index>
   </index_control_field>
+  <index_control_field tag="003">
+    <target_index>Control-number-identifier:w</target_index>
+  </index_control_field>
   <!--record.abs line 44: melm 005        Date/time-last-modified-->
   <index_control_field tag="005">
     <target_index>Date/time-last-modified:w</target_index>
diff --git a/etc/zebradb/marc_defs/marc21/biblios/biblio-zebra-indexdefs.xsl b/etc/zebradb/marc_defs/marc21/biblios/biblio-zebra-indexdefs.xsl
index 8e5514af72..4eb20b61ba 100644
--- a/etc/zebradb/marc_defs/marc21/biblios/biblio-zebra-indexdefs.xsl
+++ b/etc/zebradb/marc_defs/marc21/biblios/biblio-zebra-indexdefs.xsl
@@ -62,6 +62,11 @@ definition file (probably something like {biblio,authority}-koha-indexdefs.xml)
       <xslo:value-of select="."/>
     </z:index>
   </xslo:template>
+  <xslo:template match="marc:controlfield[@tag='003']">
+    <z:index name="Control-number-identifier:w">
+      <xslo:value-of select="."/>
+    </z:index>
+  </xslo:template>
   <xslo:template match="marc:controlfield[@tag='005']">
     <z:index name="Date/time-last-modified:w">
       <xslo:value-of select="."/>
diff --git a/installer/data/mysql/atomicupdate/bug_11175.perl b/installer/data/mysql/atomicupdate/bug_11175.perl
new file mode 100644
index 0000000000..889a816331
--- /dev/null
+++ b/installer/data/mysql/atomicupdate/bug_11175.perl
@@ -0,0 +1,6 @@
+$DBversion = 'XXX';  # will be replaced by the RM
+if( CheckVersion( $DBversion ) ) {
+    $dbh->do("INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, `type` ) VALUES ('ShowComponentRecords', 'nowhere', 'nowhere|staff|opac|both','In which record detail pages to show list of the component records, as linked via 773','Choice')");
+    SetVersion( $DBversion );
+    print "Upgrade to $DBversion done (Bug 11175: Show component records in detail views)\n";
+}
diff --git a/installer/data/mysql/mandatory/sysprefs.sql b/installer/data/mysql/mandatory/sysprefs.sql
index 2ea427bdd2..83d9f52910 100644
--- a/installer/data/mysql/mandatory/sysprefs.sql
+++ b/installer/data/mysql/mandatory/sysprefs.sql
@@ -601,6 +601,7 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, `
 ('ShelfBrowserUsesHomeBranch','1','1','Use the item home branch when finding items for the shelf browser.','YesNo'),
 ('ShelfBrowserUsesLocation','1','1','Use the item location when finding items for the shelf browser.','YesNo'),
 ('ShowAllCheckins', '0', '', 'Show all checkins', 'YesNo'),
+('ShowComponentRecords', 'nowhere', 'nowhere|staff|opac|both','In which record detail pages to show list of the component records, as linked via 773','Choice'),
 ('showLastPatron','0','','If ON, enables the last patron feature in the intranet','YesNo'),
 ('ShowPatronImageInWebBasedSelfCheck','0','','If ON, displays patron image when a patron uses web-based self-checkout','YesNo'),
 ('ShowReviewer','full','none|full|first|surname|firstandinitial|username','Choose how a commenter\'s identity is presented alongside comments in the OPAC','Choice'),
diff --git a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss b/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss
index 4b243ca1e9..cff8de5387 100644
--- a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss
+++ b/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss
@@ -578,6 +578,14 @@ ol {
     padding: 7px 0;
 }
 
+.componentPartRecordsContainer {
+    float: right;
+    overflow-y: auto;
+    overflow-x: hidden;
+    max-width: 50%;
+    max-height: 25em;
+}
+
 #editions {
     table,
     td {
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/staff_interface.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/staff_interface.pref
index 249a783e4f..66726ecdfa 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/staff_interface.pref
+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/staff_interface.pref
@@ -181,3 +181,12 @@ Staff interface:
                   yes: Show
                   no: "Don't show"
             - a search field pulldown for 'Search the catalog' boxes.
+        -
+            - Show a list of component records, as linked via field 773, in
+            - pref: ShowComponentRecords
+              choices:
+                  nowhere: "no"
+                  staff: "staff client"
+                  opac: "OPAC"
+                  both: "both staff client and OPAC"
+            - record detail pages.
diff --git a/koha-tmpl/intranet-tmpl/prog/en/xslt/MARC21slim2intranetDetail.xsl b/koha-tmpl/intranet-tmpl/prog/en/xslt/MARC21slim2intranetDetail.xsl
index ea90b2b90c..ce0a5145e2 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/xslt/MARC21slim2intranetDetail.xsl
+++ b/koha-tmpl/intranet-tmpl/prog/en/xslt/MARC21slim2intranetDetail.xsl
@@ -115,6 +115,8 @@
             </h1>
         </xsl:if>
 
+	<xsl:call-template name="showComponentParts"/>
+
         <!--Bug 13381 -->
         <xsl:if test="marc:datafield[@tag=245]">
             <h1 class="title" property="name">
diff --git a/koha-tmpl/intranet-tmpl/prog/en/xslt/MARC21slimUtils.xsl b/koha-tmpl/intranet-tmpl/prog/en/xslt/MARC21slimUtils.xsl
index aff65acb8a..13e5887fec 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/xslt/MARC21slimUtils.xsl
+++ b/koha-tmpl/intranet-tmpl/prog/en/xslt/MARC21slimUtils.xsl
@@ -578,6 +578,72 @@
         </xsl:if>
     </xsl:template>
 
+    <xsl:template name="showComponentParts">
+        <!-- Component part records: Displaying title and author of component part records -->
+        <xsl:if test="marc:componentPartRecords">
+            <div class="results_summary componentPartRecordsContainer">
+                <h5>Component part records</h5>
+                <ol class="componentParts">
+                    <xsl:for-each select="marc:componentPartRecords/marc:record">
+                        <li>
+                            <span class="componentPartRecord">
+                                <span class="componentPartRecordTitle">
+                                    <a>
+                                    <xsl:attribute name="href">/cgi-bin/koha/catalogue/detail.pl?biblionumber=<xsl:value-of select="marc:datafield[@tag=999]/marc:subfield[@code='c']" /></xsl:attribute>
+                                    <xsl:choose>
+                                        <xsl:when test="marc:datafield[@tag=245]/marc:subfield[@code='a']">
+                                            <xsl:value-of select="substring-before( concat(marc:datafield[@tag=245]/marc:subfield[@code='a'], '/'), '/')" />
+                                        </xsl:when>
+                                        <xsl:when test="marc:datafield[@tag=240]/marc:subfield[@code='a']">
+					  <xsl:for-each select="marc:datafield[@tag=240]">
+					    <xsl:call-template name="chopPunctuation">
+					      <xsl:with-param name="chopString">
+						<xsl:call-template name="subfieldSelect">
+						  <xsl:with-param name="codes">amnp</xsl:with-param>
+						</xsl:call-template>
+					      </xsl:with-param>
+					    </xsl:call-template>
+					  </xsl:for-each>
+                                        </xsl:when>
+                                        <xsl:when test="marc:datafield[@tag=130]/marc:subfield[@code='a']">
+					  <xsl:for-each select="marc:datafield[@tag=130]">
+					    <xsl:call-template name="chopPunctuation">
+					      <xsl:with-param name="chopString">
+						<xsl:call-template name="subfieldSelect">
+						  <xsl:with-param name="codes">amnp</xsl:with-param>
+						</xsl:call-template>
+					      </xsl:with-param>
+					    </xsl:call-template>
+					  </xsl:for-each>
+                                        </xsl:when>
+                                        <xsl:otherwise>
+                                            <xsl:text>[Record with no title statement]</xsl:text>
+                                        </xsl:otherwise>
+                                    </xsl:choose>
+                                    </a>
+                                </span>
+				<xsl:choose>
+                                  <xsl:when test="marc:datafield[@tag=100]/marc:subfield[@code='a']">
+                                    -
+                                    <span class="componentPartRecordAuthor">
+                                        <xsl:value-of select="marc:datafield[@tag=100]/marc:subfield[@code='a']" />
+                                    </span>
+                                  </xsl:when>
+                                  <xsl:when test="marc:datafield[@tag=110]/marc:subfield[@code='a']">
+                                    -
+                                    <span class="componentPartRecordAuthor">
+                                        <xsl:value-of select="marc:datafield[@tag=110]/marc:subfield[@code='a']" />
+                                    </span>
+                                  </xsl:when>
+				</xsl:choose>
+                            </span>
+                        </li>
+                    </xsl:for-each>
+                </ol>
+            </div>
+        </xsl:if>
+    </xsl:template>
+
 </xsl:stylesheet>
 
 <!-- Stylus Studio meta-information - (c)1998-2002 eXcelon Corp.
diff --git a/koha-tmpl/opac-tmpl/bootstrap/css/src/opac.scss b/koha-tmpl/opac-tmpl/bootstrap/css/src/opac.scss
index 24f095c819..8df9d7b545 100644
--- a/koha-tmpl/opac-tmpl/bootstrap/css/src/opac.scss
+++ b/koha-tmpl/opac-tmpl/bootstrap/css/src/opac.scss
@@ -261,6 +261,14 @@ th {
 }
 
 
+.componentPartRecordsContainer {
+    float: right;
+    overflow-y: auto;
+    overflow-x: hidden;
+    max-width: 50%;
+    max-height: 25em;
+}
+
 #members {
     p {
         color: #727272;
diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/xslt/MARC21slim2OPACDetail.xsl b/koha-tmpl/opac-tmpl/bootstrap/en/xslt/MARC21slim2OPACDetail.xsl
index 685f4b1196..d22b4dda75 100644
--- a/koha-tmpl/opac-tmpl/bootstrap/en/xslt/MARC21slim2OPACDetail.xsl
+++ b/koha-tmpl/opac-tmpl/bootstrap/en/xslt/MARC21slim2OPACDetail.xsl
@@ -127,6 +127,8 @@
             </h2>
         </xsl:if>
 
+	<xsl:call-template name="showComponentParts"/>
+
             <!--Bug 13381 -->
             <xsl:if test="marc:datafield[@tag=245]">
                 <h2 class="title" property="name">
diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/xslt/MARC21slimUtils.xsl b/koha-tmpl/opac-tmpl/bootstrap/en/xslt/MARC21slimUtils.xsl
index eb380379db..b51017cf08 100644
--- a/koha-tmpl/opac-tmpl/bootstrap/en/xslt/MARC21slimUtils.xsl
+++ b/koha-tmpl/opac-tmpl/bootstrap/en/xslt/MARC21slimUtils.xsl
@@ -542,6 +542,72 @@
         </xsl:if>
     </xsl:template>
 
+    <xsl:template name="showComponentParts">
+        <!-- Component part records: Displaying title and author of component part records -->
+        <xsl:if test="marc:componentPartRecords">
+            <div class="results_summary componentPartRecordsContainer">
+                <h5>Component part records</h5>
+                <ol class="componentParts">
+                    <xsl:for-each select="marc:componentPartRecords/marc:record">
+                        <li>
+                            <span class="componentPartRecord">
+                                <span class="componentPartRecordTitle">
+                                    <a>
+                                    <xsl:attribute name="href">/cgi-bin/koha/opac-detail.pl?biblionumber=<xsl:value-of select="marc:datafield[@tag=999]/marc:subfield[@code='c']" /></xsl:attribute>
+                                    <xsl:choose>
+                                        <xsl:when test="marc:datafield[@tag=245]/marc:subfield[@code='a']">
+                                            <xsl:value-of select="substring-before( concat(marc:datafield[@tag=245]/marc:subfield[@code='a'], '/'), '/')" />
+                                        </xsl:when>
+                                        <xsl:when test="marc:datafield[@tag=240]/marc:subfield[@code='a']">
+					  <xsl:for-each select="marc:datafield[@tag=240]">
+					    <xsl:call-template name="chopPunctuation">
+					      <xsl:with-param name="chopString">
+						<xsl:call-template name="subfieldSelect">
+						  <xsl:with-param name="codes">amnp</xsl:with-param>
+						</xsl:call-template>
+					      </xsl:with-param>
+					    </xsl:call-template>
+					  </xsl:for-each>
+                                        </xsl:when>
+                                        <xsl:when test="marc:datafield[@tag=130]/marc:subfield[@code='a']">
+					  <xsl:for-each select="marc:datafield[@tag=130]">
+					    <xsl:call-template name="chopPunctuation">
+					      <xsl:with-param name="chopString">
+						<xsl:call-template name="subfieldSelect">
+						  <xsl:with-param name="codes">amnp</xsl:with-param>
+						</xsl:call-template>
+					      </xsl:with-param>
+					    </xsl:call-template>
+					  </xsl:for-each>
+                                        </xsl:when>
+                                        <xsl:otherwise>
+                                            <xsl:text>[Record with no title statement]</xsl:text>
+                                        </xsl:otherwise>
+                                    </xsl:choose>
+                                    </a>
+                                </span>
+				<xsl:choose>
+                                  <xsl:when test="marc:datafield[@tag=100]/marc:subfield[@code='a']">
+                                    -
+                                    <span class="componentPartRecordAuthor">
+                                        <xsl:value-of select="marc:datafield[@tag=100]/marc:subfield[@code='a']" />
+                                    </span>
+                                  </xsl:when>
+                                  <xsl:when test="marc:datafield[@tag=110]/marc:subfield[@code='a']">
+                                    -
+                                    <span class="componentPartRecordAuthor">
+                                        <xsl:value-of select="marc:datafield[@tag=110]/marc:subfield[@code='a']" />
+                                    </span>
+                                  </xsl:when>
+				</xsl:choose>
+                            </span>
+                        </li>
+                    </xsl:for-each>
+                </ol>
+            </div>
+        </xsl:if>
+    </xsl:template>
+
 </xsl:stylesheet>
 
 <!-- Stylus Studio meta-information - (c)1998-2002 eXcelon Corp.
-- 
2.20.1