diff --git a/config/vufind/RecordTabs.ini b/config/vufind/RecordTabs.ini index 778f986418bac43f89950c880413c93c0d79be46..95f776008847f2ed3328599b715158d629391c2d 100644 --- a/config/vufind/RecordTabs.ini +++ b/config/vufind/RecordTabs.ini @@ -62,6 +62,7 @@ tabs[HierarchyTree] = HierarchyTree ;tabs[CollectionList] = CollectionList ;tabs[CollectionHierarchyTree] = CollectionHierarchyTree tabs[Map] = Map +tabs[Versions] = Versions tabs[Similar] = SimilarItemsCarousel tabs[Details] = StaffViewArray defaultTab = null @@ -83,6 +84,7 @@ tabs[HierarchyTree] = HierarchyTree ;tabs[Search2CollectionList] = Search2CollectionList ;tabs[CollectionHierarchyTree] = CollectionHierarchyTree tabs[Map] = Map +tabs[Versions] = Versions tabs[Similar] = SimilarItemsCarousel tabs[Details] = StaffViewArray defaultTab = null @@ -102,6 +104,7 @@ tabs[Excerpt] = Excerpt tabs[Preview] = preview tabs[HierarchyTree] = HierarchyTree tabs[Map] = Map +tabs[Versions] = Versions tabs[Similar] = SimilarItemsCarousel tabs[Details] = StaffViewMARC defaultTab = null @@ -116,6 +119,7 @@ tabs[Excerpt] = Excerpt tabs[Preview] = preview tabs[HierarchyTree] = HierarchyTree tabs[Map] = Map +tabs[Versions] = Versions tabs[Similar] = SimilarItemsCarousel tabs[Details] = StaffViewOverdrive defaultTab = null @@ -144,3 +148,7 @@ defaultTab = null ; by a tab. Settings are named by the tab name (X above) and may be repeated if ; multiple scripts are required. [TabScripts] +Versions[] = openurl.js +Versions[] = combined-search.js +Versions[] = check_item_statuses.js +Versions[] = record_versions.js diff --git a/config/vufind/Search2.ini b/config/vufind/Search2.ini index 44879ebded9e0fb5d7c5fc33e8966533c36fc1d6..5d18744c7a48ef3b3f9a8fe71fcdf65ca947ce2f 100644 --- a/config/vufind/Search2.ini +++ b/config/vufind/Search2.ini @@ -63,6 +63,7 @@ retain_filters_by_default = true ;default_filters[] = "institution:MyInstitution" ;default_filters[] = "(format:Book AND institution:MyInstitution)" ;default_record_fields = "*,score" +display_versions = true [Cache] type = File @@ -103,6 +104,7 @@ title = sort_title [DefaultSortingByType] CallNumber = callnumber-sort +WorkKeys = year [SideRecommendations] ;Subject[] = SideFacets @@ -113,6 +115,7 @@ Author[] = AuthorFacets Author[] = SpellingSuggestions ;Author[] = WorldCatIdentities CallNumber[] = "TopFacets:ResultsTop" +WorkKeys[] = false [NoResultsRecommendations] CallNumber[] = SwitchQuery::wildcard:truncatechar diff --git a/config/vufind/searches.ini b/config/vufind/searches.ini index 65d8446857f98d3484b28f64f7401eba155fe7a6..4ee8c8899bd0debd617fd39a822f930031584dd9 100644 --- a/config/vufind/searches.ini +++ b/config/vufind/searches.ini @@ -109,6 +109,9 @@ retain_filters_by_default = true ; line below. ;default_record_fields = "*,score" +; Whether the "versions" (FRBR) link and record tab are enabled. Default is true. +;display_versions = true + [Cache] ; This controls whether the parsed searchspecs.yaml file will be stored to ; improve search performance; legal options are APC (use APC cache), File (store @@ -186,6 +189,7 @@ title = sort_title ; using the default_sort setting in the [General] section above. [DefaultSortingByType] CallNumber = callnumber-sort +WorkKeys = year ; Each search type defined in searchspecs.yaml can have one or more "recommendations ; modules" associated with it in the following sections. These plug-ins will cause @@ -440,11 +444,14 @@ Author[] = AuthorFacets Author[] = SpellingSuggestions ;Author[] = WorldCatIdentities CallNumber[] = "TopFacets:ResultsTop" ; disable spelling in this context +; Do not show top recommendations for the Versions (WorkKeys) search by default +WorkKeys[] = false [NoResultsRecommendations] CallNumber[] = SwitchQuery::wildcard:truncatechar CallNumber[] = RemoveFilters ;CallNumber[] = AlphaBrowseLink:lcc +WorkKeys[] = false ; These settings control the top and side recommendations within the special Author ; module (the page accessed by clicking on an author's name within the search diff --git a/config/vufind/searchspecs.yaml b/config/vufind/searchspecs.yaml index a3fd4a6ead7779825888e36e10f64412a7f0799d..249ba430d391ec5ad685c28088f42a96af2f02f0 100644 --- a/config/vufind/searchspecs.yaml +++ b/config/vufind/searchspecs.yaml @@ -431,3 +431,9 @@ oclc_num: QueryFields: oclc_num: - [oclc_num, ~] + +WorkKeys: + QueryFields: + work_keys_str_mv: + - [and, 100] + - [or, ~] diff --git a/import-marc.bat b/import-marc.bat index dca28dbb708a9f28c4d6054f6a37df5e363b514e..fb2278bfc27f602294070d0feac9c406897d7d26 100644 --- a/import-marc.bat +++ b/import-marc.bat @@ -119,7 +119,7 @@ for %%a in (%VUFIND_HOME%\import\solrmarc_core_*.jar) do set JAR_FILE=%%a rem ##################################################### rem # Execute Importer rem ##################################################### -set RUN_CMD=%JAVA% %INDEX_OPTIONS% -Duser.timezone=UTC -Dlog4j.configuration="file:///%LOG4J_CONFIG%" %EXTRA_SOLRMARC_SETTINGS% -jar %JAR_FILE% %PROPERTIES_FILE% -solrj %VUFIND_HOME%\solr\vendor\dist\solrj-lib %1 +set RUN_CMD=%JAVA% %INDEX_OPTIONS% -Duser.timezone=UTC -Dlog4j.configuration="file:///%LOG4J_CONFIG%" %EXTRA_SOLRMARC_SETTINGS% -jar %JAR_FILE% %PROPERTIES_FILE% -solrj %VUFIND_HOME%\solr\vendor\dist\solrj-lib -lib_local %VUFIND_HOME%\import\lib_local;%VUFIND_HOME%\solr\vendor\contrib\analysis-extras\lib %1 echo Now Importing %1 ... echo %RUN_CMD% %RUN_CMD% diff --git a/import-marc.sh b/import-marc.sh index 309cc11501e7a5cf98206ab2fba0a819291a3b41..8c3cd3aa66b0a6aa4e4eab3009622786e0e7fd78 100755 --- a/import-marc.sh +++ b/import-marc.sh @@ -140,7 +140,7 @@ MARC_FILE=`basename $1` # Execute Importer ##################################################### -RUN_CMD="$JAVA $INDEX_OPTIONS -Duser.timezone=UTC -Dlog4j.configuration=file://$LOG4J_CONFIG $EXTRA_SOLRMARC_SETTINGS -jar $JAR_FILE $PROPERTIES_FILE -solrj $VUFIND_HOME/solr/vendor/dist/solrj-lib $MARC_PATH/$MARC_FILE" +RUN_CMD="$JAVA $INDEX_OPTIONS -Duser.timezone=UTC -Dlog4j.configuration=file://$LOG4J_CONFIG $EXTRA_SOLRMARC_SETTINGS -jar $JAR_FILE $PROPERTIES_FILE -solrj $VUFIND_HOME/solr/vendor/dist/solrj-lib -lib_local "$VUFIND_HOME/import/lib_local\;$VUFIND_HOME/solr/vendor/contrib/analysis-extras/lib" $MARC_PATH/$MARC_FILE" echo "Now Importing $1 ..." # solrmarc writes log messages to stderr, write RUN_CMD to the same place echo "`date '+%h %d, %H:%M:%S'` $RUN_CMD" >&2 diff --git a/import/archivesspace.properties b/import/archivesspace.properties index 108e6f6b6fbc90b1861a7dd4db735b54b8151f18..119a7dfc5949820c45dccb380347fa7c6373eaec 100644 --- a/import/archivesspace.properties +++ b/import/archivesspace.properties @@ -11,6 +11,7 @@ xslt = archivesspace.xsl ; name; if you do not include a namespace, the class will automatically be assumed ; to live in the \VuFind\XSLT\Import namespace. custom_class[] = VuFind +custom_class[] = VuFindWorkKeys ; OPTIONAL: If true, all custom_class settings above will be passed to the XSLT with ; their namespaces stripped off; for example, \VuFind\XSLT\Import\VuFind would be ; treated as \VuFind in XSLT files. This allows more compact syntax within XSLT @@ -28,3 +29,13 @@ collection = "Archives" ; specify a more narrow prefix here if you wish to filter to a particular subset ; of URLs indexed into VuFind. ;urlPrefix = "http://hdl.handle.net" + +; These settings will influence work key generation for identifying record versions. +; You can define regular expressions to either specifically include or specifically +; exclude particular characters, and/or you can use Transliterator rules when +; generating keys to identify works. See +; https://unicode-org.github.io/icu/userguide/transforms/general/#icu-transliterators +; for more information on the transliteration rules. +workKey_include_regEx = "" +workKey_exclude_regEx = "" +workKey_transliterator_rules = ":: NFD; :: lower; :: Latin; :: [^[:letter:] [:number:]] Remove; :: NFKC;" diff --git a/import/index_java/src/org/vufind/index/FieldSpecTools.java b/import/index_java/src/org/vufind/index/FieldSpecTools.java index 821073d657f3553ea77ac937bf39c42b21abda0d..3a572410a4fb5dabf2673707f5e7f904d21a0981 100644 --- a/import/index_java/src/org/vufind/index/FieldSpecTools.java +++ b/import/index_java/src/org/vufind/index/FieldSpecTools.java @@ -20,9 +20,14 @@ package org.vufind.index; import org.marc4j.marc.Record; import org.marc4j.marc.DataField; +import org.marc4j.marc.Subfield; +import org.solrmarc.index.extractor.formatter.FieldFormatter; +import org.solrmarc.index.extractor.formatter.FieldFormatterBase; +import org.solrmarc.index.extractor.formatter.FieldFormatter.eCleanVal; import org.solrmarc.index.SolrIndexer; +import java.lang.StringBuilder; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashSet; @@ -71,16 +76,32 @@ public class FieldSpecTools * @return Set */ public static final Set<String> getFieldsByTagList(final Record record, final String tagList) + { + return getFieldsByTagList(record, tagList, false); + } + + /** + * Get field data specified by a SolrMarc tag list + * + * @param record Record + * @param tagList The field specification + * @param removeNonFiling Whether to remove non-filing characters + * + * @return Set + */ + public static final Set<String> getFieldsByTagList(final Record record, final String tagList, Boolean removeNonFiling) { Set<String> result = new LinkedHashSet<String>(); final HashMap<String, Set<String>> parsedTagList = getParsedTagList(tagList); final List fields = SolrIndexer.instance().getFieldSetMatchingTagList(record, tagList); + final FieldFormatter formatter = removeNonFiling + ? new FieldFormatterBase(false).addCleanVal(eCleanVal.STRIP_INDICATOR) : null; if (fields != null) { Iterator fieldsIter = fields.iterator(); while (fieldsIter.hasNext()) { DataField field = (DataField) fieldsIter.next(); for (String subfields : parsedTagList.get(field.getTag())) { - String current = SolrIndexer.instance().getDataFromVariableField(field, "["+subfields+"]", " ", false); + String current = getFieldData(field, subfields, formatter); if (null != current) { result.add(current); } @@ -89,4 +110,35 @@ public class FieldSpecTools } return result; } + + /** + * Get subfields from a data field + * + * @param dataFiel Data field + * @param subfieldCode Subfield codes to get + * @param formatter Formatter to use (or null) + * + * @return Set + */ + protected static final String getFieldData(DataField dataField, String subfieldCodes, FieldFormatter formatter) + { + StringBuilder result = new StringBuilder(64); + final List<Subfield> subfields = dataField.getSubfields(); + for (Subfield subfield : subfields) { + final char subfieldCode = subfield.getCode(); + if (subfieldCodes.indexOf(subfieldCode) != -1) { + if (result.length() > 0) { + result.append(' '); + } + final String subfieldData = subfield.getData().trim(); + if (null != formatter) { + result.append(formatter.cleanData(dataField, 'a' == subfieldCode, subfieldData)); + } else { + result.append(subfieldData); + } + } + } + + return result.length() > 0 ? result.toString() : null; + } } diff --git a/import/index_java/src/org/vufind/index/WorkKeys.java b/import/index_java/src/org/vufind/index/WorkKeys.java new file mode 100644 index 0000000000000000000000000000000000000000..4725fb363d495a6eb09d7e152d1829dff7e5d7f1 --- /dev/null +++ b/import/index_java/src/org/vufind/index/WorkKeys.java @@ -0,0 +1,142 @@ +package org.vufind.index; + +/** + * Indexing routines for creating work keys for FRBR functionality. + * + * Copyright (C) The National Library of Finland 2020. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +import org.marc4j.marc.Record; + +import org.vufind.index.FieldSpecTools; + +import com.ibm.icu.text.Transliterator; + +import java.text.Normalizer; +import java.text.Normalizer.Form; +import java.util.Hashtable; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Indexing routines for creating work keys for FRBR functionality. + */ +public class WorkKeys +{ + /** + * Cache for Transliterator instances to avoid having to recreate them + */ + Hashtable<String, Transliterator> transliterators = new Hashtable<String, Transliterator>(); + + /** + * Get all work identification keys for the record. + * + * @param record MARC record + * @param uniformTitleTagList The field specification for uniform titles + * @param titleTagList The field specification for titles + * @param titleTagListNF The field specification for titles with + * non-filing characters removed + * @param authorTagList The field specification for authors + * @param includeRegEx Regular expression defining characters to keep + * @param excludeRegEx Regular expression defining characters to remove + * @param transliterationRules ICU transliteration rules to be applied before + * the include and exclude regex's. See + * https://unicode-org.github.io/icu/userguide/transforms/general/#icu-transliterators + * for more information. + * @return set of keys + */ + public Set<String> getWorkKeys(final Record record, final String uniformTitleTagList, + final String titleTagList, final String titleTagListNF, final String authorTagList, + final String includeRegEx, final String excludeRegEx, final String transliterationRules + + ) { + Set<String> workKeys = new LinkedHashSet<String>(); + + if (!transliterationRules.isEmpty()) { + if (!this.transliterators.containsKey(transliterationRules)) { + this.transliterators.put( + transliterationRules, + Transliterator.createFromRules("workkeys", transliterationRules, Transliterator.FORWARD) + ); + } + } + final Transliterator transliterator = transliterationRules.isEmpty() + ? null : this.transliterators.get(transliterationRules); + + // Uniform title + final Set<String> uniformTitles = FieldSpecTools.getFieldsByTagList(record, uniformTitleTagList); + for (String uniformTitle : uniformTitles) { + final String normalizedUniformTitle + = normalizeWorkKey(uniformTitle, includeRegEx, excludeRegEx, transliterator); + if (!normalizedUniformTitle.isEmpty()) { + workKeys.add("UT ".concat(normalizedUniformTitle)); + } + } + + // Title + Author + Set<String> titles = FieldSpecTools.getFieldsByTagList(record, titleTagList); + titles.addAll(FieldSpecTools.getFieldsByTagList(record, titleTagListNF, true)); + final Set<String> authors = FieldSpecTools.getFieldsByTagList(record, authorTagList); + + if (!authors.isEmpty()) { + for (String title : titles) { + final String normalizedTitle + = normalizeWorkKey(title, includeRegEx, excludeRegEx, transliterator); + if (!normalizedTitle.isEmpty()) { + for (String author : authors) { + final String normalizedAuthor + = normalizeWorkKey(author, includeRegEx, excludeRegEx, transliterator); + if (!normalizedAuthor.isEmpty()) { + workKeys.add("AT ".concat(normalizedAuthor).concat(" ").concat(normalizedTitle)); + } + } + } + } + } + + return workKeys; + } + + /** + * Create a key string + * + * @param s String to normalize + * @param includeRegEx Regular expression defining characters to keep + * @param excludeRegEx Regular expression defining characters to remove + * @param transliterator Optional ICU transliterator to use + */ + protected String normalizeWorkKey(final String s, final String includeRegEx, final String excludeRegEx, + final Transliterator transliterator + ) { + String normalized = transliterator != null ? transliterator.transliterate(s) + : Normalizer.normalize(s, Normalizer.Form.NFKC); + if (!includeRegEx.isBlank()) { + StringBuilder result = new StringBuilder(); + Matcher m = Pattern.compile(includeRegEx).matcher(normalized); + while (m.find()) { + result.append(m.group()); + } + normalized = result.toString(); + } + if (!excludeRegEx.isBlank()) { + normalized = normalized.replaceAll(excludeRegEx, ""); + } + int length = normalized.length(); + return normalized.toLowerCase().substring(0, length > 255 ? 255 : length); + } +} diff --git a/import/xsl/archivesspace.xsl b/import/xsl/archivesspace.xsl index 61a700856979152d77524c9896bce9ab3857dd8f..e260200ac7faabdb705267a39dd6676aae78e27e 100644 --- a/import/xsl/archivesspace.xsl +++ b/import/xsl/archivesspace.xsl @@ -9,6 +9,9 @@ <xsl:param name="institution">My University</xsl:param> <xsl:param name="collection">Archives</xsl:param> <xsl:param name="urlPrefix">http</xsl:param> + <xsl:param name="workKey_include_regEx"></xsl:param> + <xsl:param name="workKey_exclude_regEx"></xsl:param> + <xsl:param name="workKey_transliterator_rules">:: NFD; :: lower; :: Latin; :: [^[:letter:] [:number:]] Remove; :: NFKC;</xsl:param> <xsl:template match="oai_dc:dc"> <add> <doc> @@ -145,6 +148,13 @@ </field> </xsl:if> </xsl:for-each> + + <!-- Work Keys --> + <xsl:for-each select="php:function('VuFindWorkKeys::getWorkKeys', '', //dc:title[normalize-space()], php:function('VuFind::stripArticles', string(//dc:title[normalize-space()])), //dc:creator, $workKey_include_regEx, $workKey_exclude_regEx, $workKey_transliterator_rules)/workKey"> + <field name="work_keys_str_mv"> + <xsl:value-of select="." /> + </field> + </xsl:for-each> </doc> </add> </xsl:template> diff --git a/languages/en.ini b/languages/en.ini index 80d52ad39d064c4a0a44d27fd7bb998998535193..8e17f613fb4944afda41f068205ea2bba92c87c8 100644 --- a/languages/en.ini +++ b/languages/en.ini @@ -858,6 +858,9 @@ Other Authors = "Other Authors" Other Editions = "Other Editions" Other Libraries = "Other Libraries" Other Sources = "Other Sources" +other_versions_link = "Show other versions (%%count%%)" +other_versions_search_link = "Show all versions (%%count%%)" +other_versions_title = "Other Versions (%%count%%)" Page not found. = "Page not found." page_first = "Go to First Page" page_last = "Go to Last Page" @@ -1237,6 +1240,7 @@ verification_email_subject = "VuFind Email Verification" verification_email_url_pretext = "You can verify your email address at this URL: %%url%%" verification_too_soon = "Your email requires validation. An email was recently sent to your registered email address. If you did not receive it, please wait a few minutes and try again." verification_user_not_found = "We could not find your account" +Versions = "Versions" VHS = "VHS" Video = "Video" Video Clips = "Video Clips" diff --git a/languages/fi.ini b/languages/fi.ini index 2e6d17bc3c5247970fa54fe4e471a2d1e9d5247c..5f9dfaf9b6b401380af73ceb0e0598dfdbee3c8e 100644 --- a/languages/fi.ini +++ b/languages/fi.ini @@ -862,6 +862,9 @@ Other Authors = "Muut tekijät" Other Editions = "Muut painokset" Other Libraries = "Muut kirjastot" Other Sources = "Muut lähteet" +other_versions_link = "Näytä muut versiot (%%count%%)" +other_versions_search_link = "Näytä kaikki versiot (%%count%%)" +other_versions_title = "Muut versiot (%%count%%)" Page not found. = "Sivua ei löytynyt." page_first = "Siirry ensimmäiselle sivulle" page_last = "Siirry viimeiselle sivulle" @@ -1241,6 +1244,7 @@ verification_email_subject = "VuFindin sähköpostiosoitteen vahvistaminen" verification_email_url_pretext = "Voit vahvistaa sähköpostiosoitteesi seuraavassa osoitteessa: %%url%%" verification_too_soon = "Sähköpostiosoitteesi tulee vahvistaa. Sähköpostiosoitteeseesi on lähetetty viesti vahvistamisesta. Jos et saanut viestiä, yritä uudelleen muutaman minuutin kuluttua." verification_user_not_found = "Tiliäsi ei löytynyt" +Versions = "Versiot" VHS = "VHS" Video = "Video" Video Clips = "Videoleikkeet" diff --git a/languages/sv.ini b/languages/sv.ini index 1c58cab1c389a26152552c50c2ba524c2915e536..1c52c302fc4c4a9b9fa61f14d2fbed96f4c1dff7 100644 --- a/languages/sv.ini +++ b/languages/sv.ini @@ -856,6 +856,9 @@ Other Authors = "Övriga upphovsmän" Other Editions = "Andra upplagor" Other Libraries = "Andra bibliotek" Other Sources = "Andra källor" +other_versions_link = "Visa andra versioner (%%count%%)" +other_versions_search_link = "Visa alla versioner (%%count%%)" +other_versions_title = "Andra versioner (%%count%%)" Page not found. = "Sidan hittades inte." page_first = "GÃ¥ till första sida" page_last = "GÃ¥ till sista sida" @@ -1235,6 +1238,7 @@ verification_email_subject = "Verifiering av e-postadress i VuFind" verification_email_url_pretext = "Du kan verifiera din e-postadress med detta länk: %%url%%" verification_too_soon = "Din e-postadress kräver verifiering. En meddelande har skickats till din e-postadress. Om du inte fÃ¥r meddelanden, var god försök pÃ¥ nytt efter en stund." verification_user_not_found = "Ditt konto hittades inte" +Versions = "Versioner" VHS = "VHS" Video = "Video" Video Clips = "Videoklipp" diff --git a/module/VuFind/config/module.config.php b/module/VuFind/config/module.config.php index f93e5666e989c2c568a5600901b3b5bb8bf7cdc4..832d18ee5983afc9688f5412c4ca9c9b3698eba0 100644 --- a/module/VuFind/config/module.config.php +++ b/module/VuFind/config/module.config.php @@ -674,8 +674,9 @@ $staticRoutes = [ 'Search/EditMemory', 'Search/Email', 'Search/FacetList', 'Search/History', 'Search/Home', 'Search/NewItem', 'Search/OpenSearch', 'Search/Reserves', 'Search/ReservesFacetList', - 'Search/Results', 'Search/Suggest', - 'Search2/Advanced', 'Search2/Home', 'Search2/Results', + 'Search/Results', 'Search/Suggest', 'Search/Versions', + 'Search2/Advanced', 'Search2/FacetList', 'Search2/Home', 'Search2/Results', + 'Search2/Versions', 'Summon/Advanced', 'Summon/FacetList', 'Summon/Home', 'Summon/Search', 'Tag/Home', 'Upgrade/Home', 'Upgrade/FixAnonymousTags', 'Upgrade/FixDuplicateTags', diff --git a/module/VuFind/src/VuFind/AjaxHandler/GetRecordVersions.php b/module/VuFind/src/VuFind/AjaxHandler/GetRecordVersions.php new file mode 100644 index 0000000000000000000000000000000000000000..5221ceacb133756b608f763cff67f0f541bd8900 --- /dev/null +++ b/module/VuFind/src/VuFind/AjaxHandler/GetRecordVersions.php @@ -0,0 +1,108 @@ +<?php +/** + * AJAX handler for fetching versions link + * + * PHP version 7 + * + * Copyright (C) The National Library of Finland 2019. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category VuFind + * @package AJAX + * @author Ere Maijala <ere.maijala@helsinki.fi> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +namespace VuFind\AjaxHandler; + +use Laminas\Mvc\Controller\Plugin\Params; +use VuFind\Record\Loader; +use VuFind\RecordTab\TabManager; +use VuFind\Session\Settings as SessionSettings; +use VuFind\View\Helper\Root\Record; + +/** + * AJAX handler for fetching versions link + * + * @category VuFind + * @package AJAX + * @author Ere Maijala <ere.maijala@helsinki.fi> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +class GetRecordVersions extends \VuFind\AjaxHandler\AbstractBase +{ + /** + * Record loader + * + * @var Loader + */ + protected $recordLoader; + + /** + * Record plugin + * + * @var Record + */ + protected $recordPlugin; + + /** + * Tab manager + * + * @var TabManager + */ + protected $tabManager; + + /** + * Constructor + * + * @param SessionSettings $ss Session settings + * @param Loader $loader Record loader + * @param Record $rp Record plugin + * @param TabManager $tm Tab manager + */ + public function __construct(SessionSettings $ss, Loader $loader, Record $rp, + TabManager $tm + ) { + $this->sessionSettings = $ss; + $this->recordLoader = $loader; + $this->recordPlugin = $rp; + $this->tabManager = $tm; + } + + /** + * Handle a request. + * + * @param Params $params Parameter helper from controller + * + * @return array [response data, HTTP status code] + */ + public function handleRequest(Params $params) + { + $this->disableSessionWrites(); // avoid session write timing bug + + $id = $params->fromPost('id') ?: $params->fromQuery('id'); + $source = $params->fromPost('source') ?: $params->fromQuery('source'); + $driver = $this->recordLoader->load($id, $source); + $tabs = $this->tabManager->getTabsForRecord($driver); + $full = true; + + $html = ($this->recordPlugin)($driver)->renderTemplate( + 'versions-link.phtml', compact('driver', 'tabs', 'full') + ); + + return $this->formatResponse($html); + } +} diff --git a/module/VuFind/src/VuFind/AjaxHandler/GetRecordVersionsFactory.php b/module/VuFind/src/VuFind/AjaxHandler/GetRecordVersionsFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..8c53721ab14685af47b68892e376729f84bcf566 --- /dev/null +++ b/module/VuFind/src/VuFind/AjaxHandler/GetRecordVersionsFactory.php @@ -0,0 +1,74 @@ +<?php +/** + * Factory for GetRecordVersions AJAX handler. + * + * PHP version 7 + * + * Copyright (C) The National Library of Finland 2019. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category VuFind + * @package AJAX + * @author Ere Maijala <ere.maijala@helsinki.fi> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +namespace VuFind\AjaxHandler; + +use Interop\Container\ContainerInterface; + +/** + * Factory for GetRecordVersions AJAX handler. + * + * @category VuFind + * @package AJAX + * @author Ere Maijala <ere.maijala@helsinki.fi> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +class GetRecordVersionsFactory + implements \Laminas\ServiceManager\Factory\FactoryInterface +{ + /** + * Create an object + * + * @param ContainerInterface $container Service manager + * @param string $requestedName Service being created + * @param null|array $options Extra options (optional) + * + * @return object + * + * @throws ServiceNotFoundException if unable to resolve the service. + * @throws ServiceNotCreatedException if an exception is raised when + * creating a service. + * @throws ContainerException if any other error occurs + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function __invoke(ContainerInterface $container, $requestedName, + array $options = null + ) { + if (!empty($options)) { + throw new \Exception('Unexpected options passed to factory.'); + } + $result = new $requestedName( + $container->get(\VuFind\Session\Settings::class), + $container->get(\VuFind\Record\Loader::class), + $container->get('ViewRenderer')->plugin('record'), + $container->get(\VuFind\RecordTab\TabManager::class) + ); + return $result; + } +} diff --git a/module/VuFind/src/VuFind/AjaxHandler/PluginManager.php b/module/VuFind/src/VuFind/AjaxHandler/PluginManager.php index d8b6a958df96825598a4e14f10637a75e5c0444b..2a2546079bb4f709ec428e79a90afa0c998da882 100644 --- a/module/VuFind/src/VuFind/AjaxHandler/PluginManager.php +++ b/module/VuFind/src/VuFind/AjaxHandler/PluginManager.php @@ -57,6 +57,7 @@ class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager 'getRecordCover' => GetRecordCover::class, 'getRecordDetails' => GetRecordDetails::class, 'getRecordTags' => GetRecordTags::class, + 'getRecordVersions' => GetRecordVersions::class, 'getRequestGroupPickupLocations' => GetRequestGroupPickupLocations::class, 'getResolverLinks' => GetResolverLinks::class, 'getSaveStatuses' => GetSaveStatuses::class, @@ -95,6 +96,7 @@ class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager GetRecordCover::class => GetRecordCoverFactory::class, GetRecordDetails::class => GetRecordDetailsFactory::class, GetRecordTags::class => GetRecordTagsFactory::class, + GetRecordVersions::class => GetRecordVersionsFactory::class, GetRequestGroupPickupLocations::class => AbstractIlsAndUserActionFactory::class, GetResolverLinks::class => GetResolverLinksFactory::class, diff --git a/module/VuFind/src/VuFind/Config/Upgrade.php b/module/VuFind/src/VuFind/Config/Upgrade.php index 1fe1d0a753af903c34b906f6bda1fc5b3b2bfa33..1c047a69a4477115a3dd42a5e12a4747e688f974 100644 --- a/module/VuFind/src/VuFind/Config/Upgrade.php +++ b/module/VuFind/src/VuFind/Config/Upgrade.php @@ -876,7 +876,7 @@ class Upgrade } } } - $this->upgradeSpellingSettings('searches.ini', ['CallNumber']); + $this->upgradeSpellingSettings('searches.ini', ['CallNumber', 'WorkKeys']); // save the file $this->saveModifiedConfig('searches.ini'); diff --git a/module/VuFind/src/VuFind/Controller/AbstractSolrSearch.php b/module/VuFind/src/VuFind/Controller/AbstractSolrSearch.php index 179cfa2acae20b207ec15d6e0e2bb0671c3253fb..9fd2f3e6ef84ce988d0bcdc99e37944f1b39dc02 100644 --- a/module/VuFind/src/VuFind/Controller/AbstractSolrSearch.php +++ b/module/VuFind/src/VuFind/Controller/AbstractSolrSearch.php @@ -38,6 +38,8 @@ namespace VuFind\Controller; */ class AbstractSolrSearch extends AbstractSearch { + use Feature\RecordVersionsSearchTrait; + /** * Handle an advanced search * diff --git a/module/VuFind/src/VuFind/Controller/Feature/RecordVersionsSearchTrait.php b/module/VuFind/src/VuFind/Controller/Feature/RecordVersionsSearchTrait.php new file mode 100644 index 0000000000000000000000000000000000000000..143c970f02ac8a488b4bb7598eab745aaeca2f96 --- /dev/null +++ b/module/VuFind/src/VuFind/Controller/Feature/RecordVersionsSearchTrait.php @@ -0,0 +1,103 @@ +<?php +/** + * VuFind Action Feature Trait - Record Versions Search + * Depends on method getSearchResultsView and record driver's method getWorkKeys. + * + * PHP version 7 + * + * Copyright (C) The National Library of Finland 2020. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category VuFind + * @package Controller_Plugins + * @author Ere Maijala <ere.maijala@helsinki.fi> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ +namespace VuFind\Controller\Feature; + +/** + * VuFind Action Feature Trait - Record Versions Search + * + * @category VuFind + * @package Controller_Plugins + * @author Ere Maijala <ere.maijala@helsinki.fi> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ +trait RecordVersionsSearchTrait +{ + /** + * Show results of versions search. + * + * @return mixed + */ + public function versionsAction() + { + $id = $this->params()->fromQuery('id'); + $keys = $this->params()->fromQuery('keys'); + $record = null; + if ($id) { + $loader = $this->serviceLocator->get(\VuFind\Record\Loader::class); + $record = $loader->load($id, $this->searchClassId, true); + if ($record instanceof \VuFind\RecordDriver\Missing) { + $record = null; + } else { + $keys = $record->tryMethod('getWorkKeys'); + } + } + + if (empty($keys)) { + return $this->forwardTo('Search', 'Home'); + } + + $mapFunc = function ($val) { + return '"' . addcslashes($val, '"') . '"'; + }; + + $query = $this->getRequest()->getQuery(); + $query->lookfor = implode(' OR ', array_map($mapFunc, (array)$keys)); + $query->type = 'WorkKeys'; + + // Don't save to history -- history page doesn't handle correctly: + $this->saveToHistory = false; + + $callback = function ($runner, $params, $searchId) { + $defaultCallback = is_callable([$this, 'getSearchSetupCallback']) + ? $this->getSearchSetupCallback() : null; + if (is_callable($defaultCallback)) { + $defaultCallback($runner, $params, $searchId); + } + $options = $params->getOptions(); + $options->disableHighlighting(); + $options->spellcheckEnabled(false); + }; + + $view = $this->getSearchResultsView($callback); + + // Customize the URL helper to make sure it builds proper versions URLs + // (but only do this if we have access to a results object, which we + // won't in RSS mode): + if (isset($view->results)) { + $view->results->getUrlQuery() + ->setDefaultParameter('id', $id) + ->setDefaultParameter('keys', $keys) + ->setSuppressQuery(true); + $view->driver = $record; + } + + return $view; + } +} diff --git a/module/VuFind/src/VuFind/RecordDriver/Feature/VersionAwareInterface.php b/module/VuFind/src/VuFind/RecordDriver/Feature/VersionAwareInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..a79edfb4337c0082c5565f6900a1ed767627ceac --- /dev/null +++ b/module/VuFind/src/VuFind/RecordDriver/Feature/VersionAwareInterface.php @@ -0,0 +1,47 @@ +<?php +/** + * Version aware marker interface. + * + * PHP version 7 + * + * Copyright (C) The National Library of Finland 2020. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category VuFind + * @package RecordDrivers + * @author Ere Maijala <ere.maijala@helsinki.fi> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Site + */ +namespace VuFind\RecordDriver\Feature; + +/** + * Version aware marker interface. + * + * @category VuFind + * @package RecordDrivers + * @author Ere Maijala <ere.maijala@helsinki.fi> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Site + */ +interface VersionAwareInterface +{ + /** + * Get work identification keys + * + * @return array + */ + public function getWorkKeys(); +} diff --git a/module/VuFind/src/VuFind/RecordDriver/Feature/VersionAwareTrait.php b/module/VuFind/src/VuFind/RecordDriver/Feature/VersionAwareTrait.php new file mode 100644 index 0000000000000000000000000000000000000000..c62ea4ff4a89266de9f884bbd6d58b134c117486 --- /dev/null +++ b/module/VuFind/src/VuFind/RecordDriver/Feature/VersionAwareTrait.php @@ -0,0 +1,114 @@ +<?php +/** + * Logic for record versions support. Depends on versionAwareInterface. + * + * PHP version 7 + * + * Copyright (C) The National Library of Finland 2020. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category VuFind + * @package RecordDrivers + * @author Ere Maijala <ere.maijala@helsinki.fi> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Site + */ +namespace VuFind\RecordDriver\Feature; + +/** + * Logic for record versions support. + * + * @category VuFind + * @package RecordDrivers + * @author Ere Maijala <ere.maijala@helsinki.fi> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Site + */ +trait VersionAwareTrait +{ + /** + * Cached result of other versions (work expressions) count + * + * @var int + */ + protected $otherVersionsCount = null; + + /** + * Return count of other versions available + * + * @return int + */ + public function getOtherVersionCount() + { + if (null === $this->searchService) { + return false; + } + + if (!isset($this->otherVersionsCount)) { + if (!($workKeys = $this->tryMethod('getWorkKeys'))) { + if (!($this instanceof VersionAwareInterface)) { + throw new \Exception( + 'VersionAwareTrait requires VersionAwareInterface' + ); + } + return false; + } + + $params = new \VuFindSearch\ParamBag(); + $params->add('rows', 0); + $results = $this->searchService->workExpressions( + $this->getSourceIdentifier(), + $this->getUniqueID(), + $workKeys, + $params + ); + $this->otherVersionsCount = $results->getTotal(); + } + return $this->otherVersionsCount; + } + + /** + * Retrieve versions as a search result + * + * @param bool $includeSelf Whether to include this record + * @param int $count Maximum number of records to display + * @param int $offset Start position (0-based) + * + * @return \VuFindSearch\Response\RecordCollectionInterface + */ + public function getVersions($includeSelf = false, $count = 20, $offset = 0) + { + if (null === $this->searchService) { + return false; + } + + if (!($workKeys = $this->getWorkKeys())) { + return false; + } + + if (!isset($this->otherVersions)) { + $params = new \VuFindSearch\ParamBag(); + $params->add('rows', $count); + $params->add('start', $offset); + $this->otherVersions = $this->searchService->workExpressions( + $this->getSourceIdentifier(), + $includeSelf ? '' : $this->getUniqueID(), + $workKeys, + $params + ); + } + return $this->otherVersions; + } +} diff --git a/module/VuFind/src/VuFind/RecordDriver/SolrDefault.php b/module/VuFind/src/VuFind/RecordDriver/SolrDefault.php index 375ef1b92bfa18df715e6b7cc6e0bfe565244737..9f4c93ca256c0895ddb28ada2676e96a75ecdadd 100644 --- a/module/VuFind/src/VuFind/RecordDriver/SolrDefault.php +++ b/module/VuFind/src/VuFind/RecordDriver/SolrDefault.php @@ -6,6 +6,7 @@ * PHP version 7 * * Copyright (C) Villanova University 2010. + * Copyright (C) The National Library of Finland 2019. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, @@ -23,6 +24,7 @@ * @category VuFind * @package RecordDrivers * @author Demian Katz <demian.katz@villanova.edu> + * @author Ere Maijala <ere.maijala@helsinki.fi> * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org/wiki/development:plugins:record_drivers Wiki */ @@ -37,14 +39,16 @@ namespace VuFind\RecordDriver; * @category VuFind * @package RecordDrivers * @author Demian Katz <demian.katz@villanova.edu> + * @author Ere Maijala <ere.maijala@helsinki.fi> * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org/wiki/development:plugins:record_drivers Wiki * * @SuppressWarnings(PHPMD.ExcessivePublicCount) */ -class SolrDefault extends DefaultRecord +class SolrDefault extends DefaultRecord implements Feature\VersionAwareInterface { use HierarchyAwareTrait; + use Feature\VersionAwareTrait; /** * These Solr fields should be used for snippets if available (listed in order @@ -67,7 +71,8 @@ class SolrDefault extends DefaultRecord protected $forbiddenSnippetFields = [ 'author', 'title', 'title_short', 'title_full', 'title_full_unstemmed', 'title_auth', 'title_sub', 'spelling', 'id', - 'ctrlnum', 'author_variant', 'author2_variant', 'fullrecord' + 'ctrlnum', 'author_variant', 'author2_variant', 'fullrecord', + 'work_keys_str_mv', ]; /** @@ -133,6 +138,7 @@ class SolrDefault extends DefaultRecord $this->containerLinking = !isset($mainConfig->Hierarchy->simpleContainerLinks) ? false : $mainConfig->Hierarchy->simpleContainerLinks; + parent::__construct($mainConfig, $recordConfig, $searchSettings); } @@ -298,4 +304,14 @@ class SolrDefault extends DefaultRecord && !empty($this->fields['hierarchy_parent_id']) ? $this->fields['hierarchy_parent_id'][0] : ''; } + + /** + * Get work identification keys + * + * @return array + */ + public function getWorkKeys() + { + return $this->fields['work_keys_str_mv'] ?? []; + } } diff --git a/module/VuFind/src/VuFind/RecordTab/PluginManager.php b/module/VuFind/src/VuFind/RecordTab/PluginManager.php index 23136d73539df3a31030e315fff0d8ca94bcc2b1..ec3cd07191ca367fb9e7d0a85e8f8189f988b6b4 100644 --- a/module/VuFind/src/VuFind/RecordTab/PluginManager.php +++ b/module/VuFind/src/VuFind/RecordTab/PluginManager.php @@ -65,6 +65,7 @@ class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager 'staffviewoverdrive' => StaffViewOverdrive::class, 'toc' => TOC::class, 'usercomments' => UserComments::class, + 'versions' => Versions::class, ]; /** @@ -92,6 +93,7 @@ class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager StaffViewOverdrive::class => InvokableFactory::class, TOC::class => TOCFactory::class, UserComments::class => UserCommentsFactory::class, + Versions::class => VersionsFactory::class, ]; /** diff --git a/module/VuFind/src/VuFind/RecordTab/Versions.php b/module/VuFind/src/VuFind/RecordTab/Versions.php new file mode 100644 index 0000000000000000000000000000000000000000..995bde5542c582579ac434a3c561b4268b2f0a7e --- /dev/null +++ b/module/VuFind/src/VuFind/RecordTab/Versions.php @@ -0,0 +1,98 @@ +<?php +/** + * Versions tab + * + * PHP version 7 + * + * Copyright (C) The National Library of Finland 2019-2020. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category VuFind + * @package RecordTabs + * @author Ere Maijala <ere.maijala@helsinki.fi> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org/wiki/vufind2:record_tabs Wiki + */ +namespace VuFind\RecordTab; + +use VuFind\I18n\Translator\TranslatorAwareInterface; +use VuFind\I18n\Translator\TranslatorAwareTrait; + +/** + * Versions tab + * + * @category VuFind + * @package RecordTabs + * @author Ere Maijala <ere.maijala@helsinki.fi> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org/wiki/vufind2:record_tabs Wiki + */ +class Versions extends \VuFind\RecordTab\AbstractBase + implements TranslatorAwareInterface +{ + use TranslatorAwareTrait; + + /** + * Main configuration + * + * @var \Zend\Config\Config + */ + protected $config; + + /** + * Search options plugin manager + * + * @var \VuFind\Search\Options\PluginManager; + */ + protected $searchOptionsManager; + + /** + * Constructor + * + * @param \Zend\Config\Config $config Configuration + * @param \VuFind\Search\Options\PluginManager $som Search options plugin + * manager + */ + public function __construct(\Laminas\Config\Config $config, + \VuFind\Search\Options\PluginManager $som + ) { + $this->config = $config; + $this->searchOptionsManager = $som; + } + + /** + * Is this tab active? + * + * @return bool + */ + public function isActive() + { + $options = $this->searchOptionsManager + ->get($this->getRecordDriver()->getSourceIdentifier()); + return $options->getVersionsAction() + && $this->getRecordDriver()->tryMethod('getOtherVersionCount') > 0; + } + + /** + * Get the on-screen description for this tab. + * + * @return string + */ + public function getDescription() + { + $count = $this->getRecordDriver()->tryMethod('getOtherVersionCount'); + return $this->translate('other_versions_title', ['%%count%%' => $count]); + } +} diff --git a/module/VuFind/src/VuFind/RecordTab/VersionsFactory.php b/module/VuFind/src/VuFind/RecordTab/VersionsFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..0bef1e4d93286b97e938e9bee5d1c666d1a8e45c --- /dev/null +++ b/module/VuFind/src/VuFind/RecordTab/VersionsFactory.php @@ -0,0 +1,73 @@ +<?php +/** + * Factory for building the Versions tab. + * + * PHP version 7 + * + * Copyright (C) Villanova University 2019. + * Copyright (C) The National Library of Finland 2020. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category VuFind + * @package RecordTabs + * @author Demian Katz <demian.katz@villanova.edu> + * @author Ere Maijala <ere.maijala@helsinki.fi> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +namespace VuFind\RecordTab; + +use Interop\Container\ContainerInterface; + +/** + * Factory for building the Versions tab. + * + * @category VuFind + * @package RecordTabs + * @author Demian Katz <demian.katz@villanova.edu> + * @author Ere Maijala <ere.maijala@helsinki.fi> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +class VersionsFactory implements \Laminas\ServiceManager\Factory\FactoryInterface +{ + /** + * Create an object + * + * @param ContainerInterface $container Service manager + * @param string $requestedName Service being created + * @param null|array $options Extra options (optional) + * + * @return object + * + * @throws ServiceNotFoundException if unable to resolve the service. + * @throws ServiceNotCreatedException if an exception is raised when + * creating a service. + * @throws ContainerException if any other error occurs + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function __invoke(ContainerInterface $container, $requestedName, + array $options = null + ) { + if (!empty($options)) { + throw new \Exception('Unexpected options passed to factory.'); + } + return new $requestedName( + $container->get(\VuFind\Config\PluginManager::class)->get('config'), + $container->get(\VuFind\Search\Options\PluginManager::class) + ); + } +} diff --git a/module/VuFind/src/VuFind/Search/Base/Options.php b/module/VuFind/src/VuFind/Search/Base/Options.php index 50ee1f249190f5dacdcae4f054483aa7d9b2d517..85273c4f9e35c7cb83ad0907c7e2b0b6a56a5c09 100644 --- a/module/VuFind/src/VuFind/Search/Base/Options.php +++ b/module/VuFind/src/VuFind/Search/Base/Options.php @@ -796,6 +796,17 @@ abstract class Options implements TranslatorAwareInterface return false; } + /** + * Return the route name for the versions search action. Returns false to cover + * unimplemented support. + * + * @return string|bool + */ + public function getVersionsAction() + { + return false; + } + /** * Does this search option support the cart/book bag? * diff --git a/module/VuFind/src/VuFind/Search/RecommendListener.php b/module/VuFind/src/VuFind/Search/RecommendListener.php index 9cbdbb2c9bc0dd8cc5817ab4823666edf991608e..8d68f03f75cd87af8eda4bd67975d9b0da82c009 100644 --- a/module/VuFind/src/VuFind/Search/RecommendListener.php +++ b/module/VuFind/src/VuFind/Search/RecommendListener.php @@ -150,6 +150,9 @@ class RecommendListener // Break apart the setting into module name and extra parameters: $current = explode(':', $current); $module = array_shift($current); + if (empty($module)) { + continue; + } $config = implode(':', $current); if (!$this->pluginManager->has($module)) { throw new \Exception( diff --git a/module/VuFind/src/VuFind/Search/Search2/Options.php b/module/VuFind/src/VuFind/Search/Search2/Options.php index 7089af07a33f3667273befe117944dd37347658d..62747b48101f4033ae6d1328bbd582c35bd33794 100644 --- a/module/VuFind/src/VuFind/Search/Search2/Options.php +++ b/module/VuFind/src/VuFind/Search/Search2/Options.php @@ -50,6 +50,27 @@ class Options extends \VuFind\Search\Solr\Options parent::__construct($configLoader); } + /** + * Return the route name for the facet list action. Returns false to cover + * unimplemented support. + * + * @return string|bool + */ + public function getFacetListAction() + { + return 'search2-facetlist'; + } + + /** + * Return the route name for the versions search action or false if disabled. + * + * @return string|bool + */ + public function getVersionsAction() + { + return $this->displayRecordVersions ? 'search2-versions' : false; + } + /** * Return the route name for the search results action. * diff --git a/module/VuFind/src/VuFind/Search/Solr/DeduplicationListener.php b/module/VuFind/src/VuFind/Search/Solr/DeduplicationListener.php index ab7356832611057c457a8da73e34e4dee3aafd26..a2cf5aa11b76a59fb978ddba69f8ab3bcdcdfb40 100644 --- a/module/VuFind/src/VuFind/Search/Solr/DeduplicationListener.php +++ b/module/VuFind/src/VuFind/Search/Solr/DeduplicationListener.php @@ -9,7 +9,7 @@ * PHP version 7 * * Copyright (C) Villanova University 2013. - * Copyright (C) The National Library of Finland 2013. + * Copyright (C) The National Library of Finland 2013-2020. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, @@ -137,7 +137,8 @@ class DeduplicationListener if ($backend === $this->backend) { $params = $event->getParam('params'); $context = $event->getParam('context'); - if ($params && in_array($context, ['search', 'similar', 'getids'])) { + $contexts = ['search', 'similar', 'getids', 'workExpressions']; + if ($params && in_array($context, $contexts)) { // If deduplication is enabled, filter out merged child records, // otherwise filter out dedup records. if ($this->enabled && 'getids' !== $context @@ -186,7 +187,8 @@ class DeduplicationListener return $event; } $context = $event->getParam('context'); - if ($this->enabled && ($context == 'search' || $context == 'similar')) { + $contexts = ['search', 'similar', 'workExpressions']; + if ($this->enabled && in_array($context, $contexts)) { $this->fetchLocalRecords($event); } return $event; diff --git a/module/VuFind/src/VuFind/Search/Solr/Options.php b/module/VuFind/src/VuFind/Search/Solr/Options.php index de96e13cdb6affb583d951bb09b452b35fcb5998..8354835128a4df2174933b5ef3141b8e04bd51ef 100644 --- a/module/VuFind/src/VuFind/Search/Solr/Options.php +++ b/module/VuFind/src/VuFind/Search/Solr/Options.php @@ -70,6 +70,13 @@ class Options extends \VuFind\Search\Base\Options */ protected $emptySearchRelevanceOverride = null; + /** + * Whether to display record versions + * + * @var bool + */ + protected $displayRecordVersions = true; + /** * Constructor * @@ -114,6 +121,11 @@ class Options extends \VuFind\Search\Base\Options $this->defaultFilters = $searchSettings->General->default_filters ->toArray(); } + if (isset($searchSettings->General->display_versions)) { + $this->displayRecordVersions + = $searchSettings->General->display_versions; + } + // Result limit: if (isset($searchSettings->General->result_limit)) { $this->resultLimit = $searchSettings->General->result_limit; @@ -272,6 +284,16 @@ class Options extends \VuFind\Search\Base\Options return 'search-facetlist'; } + /** + * Return the route name for the versions search action or false if disabled. + * + * @return string|bool + */ + public function getVersionsAction() + { + return $this->displayRecordVersions ? 'search-versions' : false; + } + /** * Get the relevance sort override for empty searches. * diff --git a/module/VuFind/src/VuFind/View/Helper/Root/RecordLink.php b/module/VuFind/src/VuFind/View/Helper/Root/RecordLink.php index 8a3204f03d0b8ef4a11ef3c85e58b83f16aa0876..8d7b356bbeab01d85f1ab590273139682e74e03f 100644 --- a/module/VuFind/src/VuFind/View/Helper/Root/RecordLink.php +++ b/module/VuFind/src/VuFind/View/Helper/Root/RecordLink.php @@ -244,6 +244,32 @@ class RecordLink extends \Laminas\View\Helper\AbstractHelper return $escaper($url); } + /** + * Return search URL for all versions + * + * @param \VuFind\RecordDriver\AbstractBase $driver Record driver + * + * @return string + */ + public function getVersionsSearchUrl($driver) + { + $route = $this->getVersionsActionForSource($driver->getSourceIdentifier()); + if (false === $route) { + return ''; + } + + $urlParams = [ + 'id' => $driver->getUniqueID(), + 'keys' => $driver->tryMethod('getWorkKeys', [], []) + ]; + + $urlHelper = $this->getView()->plugin('url'); + $url = $urlHelper($route, [], ['query' => $urlParams]); + // Make sure everything is properly HTML encoded: + $escaper = $this->getView()->plugin('escapehtml'); + return $escaper($url); + } + /** * Given a record source ID, return the route name for searching its backend. * @@ -256,4 +282,18 @@ class RecordLink extends \Laminas\View\Helper\AbstractHelper $optionsHelper = $this->getView()->plugin('searchOptions'); return $optionsHelper->__invoke($source)->getSearchAction(); } + + /** + * Given a record source ID, return the route name for version search with its + * backend. + * + * @param string $source Record source identifier. + * + * @return string|bool + */ + protected function getVersionsActionForSource($source) + { + $optionsHelper = $this->getView()->plugin('searchOptions'); + return $optionsHelper->__invoke($source)->getVersionsAction(); + } } diff --git a/module/VuFind/src/VuFind/XSLT/Import/VuFindWorkKeys.php b/module/VuFind/src/VuFind/XSLT/Import/VuFindWorkKeys.php new file mode 100644 index 0000000000000000000000000000000000000000..84664286e8086578aa47be5435f914ec547ebc91 --- /dev/null +++ b/module/VuFind/src/VuFind/XSLT/Import/VuFindWorkKeys.php @@ -0,0 +1,160 @@ +<?php +/** + * XSLT importer support methods for work key generation. + * + * PHP version 7 + * + * Copyright (c) Demian Katz 2020. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category VuFind + * @package Import_Tools + * @author Demian Katz <demian.katz@villanova.edu> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/indexing Wiki + */ +namespace VuFind\XSLT\Import; + +use DOMDocument; +use Normalizer; + +/** + * XSLT importer support methods for work key generation. + * + * @category VuFind + * @package Import_Tools + * @author Demian Katz <demian.katz@villanova.edu> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/indexing Wiki + */ +class VuFindWorkKeys +{ + /** + * Get all work identification keys for the record. + * + * @param Iterable $uniformTitles Uniform title(s) for the work + * @param Iterable $titles Other title(s) for the work + * @param Iterable $trimmedTitles Title(s) with leading articles, etc., + * removed + * @param Iterable $authors Author(s) for the work + * @param string $includeRegEx Regular expression defining characters to + * keep + * @param string $excludeRegEx Regular expression defining characters to + * remove + * @param string $transliteratorRules Optional ICU transliteration rules to be + * applied before the include and exclude regex's. See + * https://unicode-org.github.io/icu/userguide/transforms/general/ + * #icu-transliterators for more information on the transliteration rules. + * + * @return DOMDocument + */ + public static function getWorkKeys($uniformTitles, $titles, $trimmedTitles, + $authors, $includeRegEx = '', $excludeRegEx = '', $transliteratorRules = '' + ) { + $transliterator = $transliteratorRules + ? \Transliterator::createFromRules( + $transliteratorRules, \Transliterator::FORWARD + ) : null; + + $dom = new DOMDocument('1.0', 'utf-8'); + + $uniformTitles = is_iterable($uniformTitles) + ? $uniformTitles : (array)$uniformTitles; + foreach ($uniformTitles as $uniformTitle) { + $normalizedTitle = self::normalize( + $uniformTitle, $includeRegEx, $excludeRegEx, $transliterator + ); + if (!empty($normalizedTitle)) { + $element = $dom->createElement('workKey', 'UT ' . $normalizedTitle); + $dom->appendChild($element); + } + } + + // Exit early if there are no authors, since we can't make author/title keys: + $authors = is_iterable($authors) ? $authors : (array)$authors; + if (empty($authors)) { + return $dom; + } + $titles = $titles instanceof \Traversable + ? iterator_to_array($titles) : (array)$titles; + $trimmedTitles = $trimmedTitles instanceof \Traversable + ? iterator_to_array($trimmedTitles) : (array)$trimmedTitles; + $normalizedTitles = []; + foreach (array_merge($titles, $trimmedTitles) as $title) { + $normalizedTitle = self::normalize( + $title, $includeRegEx, $excludeRegEx, $transliterator + ); + if (empty($normalizedTitle) // skip empties + || in_array($normalizedTitle, $normalizedTitles) // avoid dupes + ) { + continue; + } + $normalizedTitles[] = $normalizedTitle; + foreach ($authors as $author) { + $normalizedAuthor = self::normalize( + $author, $includeRegEx, $excludeRegEx, $transliterator + ); + if (!empty($author)) { + $key = 'AT ' . $normalizedAuthor . ' ' . $normalizedTitle; + $element = $dom->createElement('workKey', $key); + $dom->appendChild($element); + } + } + } + + return $dom; + } + + /** + * Force a value to a string, even if it's a DOMElement. + * + * @param string|DOMElement $string String to normalize + * + * @return string + */ + protected static function deDom($string): string + { + return $string->textContent ?? (string)$string; + } + + /** + * Create a key string. + * + * @param string|DOMElement $rawString String to normalize + * @param string $includeRegEx Regular expression defining + * characters to keep + * @param string $excludeRegEx Regular expression defining + * characters to remove + * @param \Transliterator $transliterator Transliterator + * + * @return string + */ + protected static function normalize($rawString, $includeRegEx, $excludeRegEx, + $transliterator + ) { + // Handle strings and/or DOM elements: + $string = self::deDom($rawString); + $normalized = $transliterator ? $transliterator->transliterate($string) + : Normalizer::normalize($string, Normalizer::FORM_KC); + if (!empty($includeRegEx)) { + preg_match_all($includeRegEx, $normalized, $matches); + $normalized = implode($matches[0] ?? []); + } + if (!empty($excludeRegEx)) { + $normalized = preg_replace($excludeRegEx, '', $normalized); + } + return substr(strtolower($normalized), 0, 255); + } +} diff --git a/module/VuFind/src/VuFindTest/Unit/MinkTestCase.php b/module/VuFind/src/VuFindTest/Unit/MinkTestCase.php index 2997f9054bd2745760846b00f1e9595f0e9ef912..7fc5d31cc47d50c4a636e19d94034f56bce18790 100644 --- a/module/VuFind/src/VuFindTest/Unit/MinkTestCase.php +++ b/module/VuFind/src/VuFindTest/Unit/MinkTestCase.php @@ -342,13 +342,14 @@ abstract class MinkTestCase extends DbTestCase * * @param string $query Search term(s) * @param string $handler Search type (optional) + * @param string $path Path to use as search starting point (optional) * * @return \Behat\Mink\Element\Element */ - protected function performSearch($query, $handler = null) + protected function performSearch($query, $handler = null, $path = '/Search') { $session = $this->getMinkSession(); - $session->visit($this->getVuFindUrl() . '/Search/Home'); + $session->visit($this->getVuFindUrl() . $path); $page = $session->getPage(); $this->findCss($page, '#searchForm_lookfor')->setValue($query); if ($handler) { diff --git a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/RecordVersionsTest.php b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/RecordVersionsTest.php new file mode 100644 index 0000000000000000000000000000000000000000..335975211973a9520059adc8a2a41f7e44ff5bd4 --- /dev/null +++ b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/RecordVersionsTest.php @@ -0,0 +1,175 @@ +<?php +/** + * Record versions test class. + * + * PHP version 7 + * + * Copyright (C) Villanova University 2021. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category VuFind + * @package Tests + * @author Demian Katz <demian.katz@villanova.edu> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ +namespace VuFindTest\Mink; + +/** + * Record versions test class. + * + * @category VuFind + * @package Tests + * @author Demian Katz <demian.katz@villanova.edu> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + * @retry 4 + */ +class RecordVersionsTest extends \VuFindTest\Unit\MinkTestCase +{ + use \VuFindTest\Unit\AutoRetryTrait; + + /** + * Standard setup method. + * + * @return void + */ + public function setUp(): void + { + // Give up if we're not running in CI: + if (!$this->continuousIntegrationRunning()) { + $this->markTestSkipped('Continuous integration not running.'); + return; + } + } + + /** + * Run test procedure for record versions. + * + * @param string $path Path to search from. + * + * @return void + */ + protected function runVersionsTest($path) + { + // Search for an item known to have other versions in test data: + $page = $this->performSearch('id:0001732009-0', null, $path); + + // Confirm that "other versions" link exists: + $this->assertEquals( + 'Show other versions (3)', + $this->findCss($page, 'div.record-versions a')->getText() + ); + + // Click on the "other versions" link: + $this->clickCss($page, 'div.record-versions a'); + $this->snooze(); + + // Confirm that we've landed on an other versions tab: + $this->assertEquals( + 'Other Versions (3)', + $this->findCss($page, 'li.record-tab.active')->getText() + ); + + // Click the "see all versions" link: + $this->clickCss($page, 'div.search-controls a.more-link'); + $this->snooze(); + + // Confirm that all four versions are now visible in the versions display: + $this->assertEquals( + 'Versions - The collected letters of Thomas and Jane Welsh Carlyle :', + $this->findCss($page, 'ul.breadcrumb li.active')->getText() + ); + $results = $page->findAll('css', '.result'); + $this->assertEquals(4, count($results)); + } + + /** + * Test accessing a record with multiple versions. + * + * @return void + */ + public function testVersions() + { + $this->runVersionsTest('/Search'); + } + + /** + * Test accessing a record with multiple versions via secondary search. + * + * @return void + */ + public function testVersionsInSearch2() + { + $this->runVersionsTest('/Search2'); + } + + /** + * Confirm that links operate differently when the record versions tab is + * disabled but other version settings are enabled. + * + * @return void + */ + public function testDisabledVersionsTab() + { + // Disable versions tab: + $extraConfigs['RecordTabs']['VuFind\RecordDriver\SolrMarc'] = [ + 'tabs[Versions]' => false + ]; + $this->changeConfigs($extraConfigs); + // Search for an item known to have other versions in test data: + $page = $this->performSearch('id:0001732009-0', null, '/Search'); + + // Confirm that "all versions" link exists: + $this->assertEquals( + 'Show all versions (4)', + $this->findCss($page, 'div.record-versions a')->getText() + ); + + // Click on the "all versions" link: + $this->clickCss($page, 'div.record-versions a'); + $this->snooze(); + + // Confirm that we have jumped directly to the "show all versions" screen + // and that all four versions are now visible in the versions display: + $this->assertEquals( + 'Versions - The collected letters of Thomas and Jane Welsh Carlyle :', + $this->findCss($page, 'ul.breadcrumb li.active')->getText() + ); + $results = $page->findAll('css', '.result'); + $this->assertEquals(4, count($results)); + } + + /** + * Confirm that version controls do not appear in search results when the setting + * is disabled. + * + * @return void + */ + public function testDisabledVersions() + { + // Disable versions: + $extraConfigs['searches']['General'] = ['display_versions' => false]; + $this->changeConfigs($extraConfigs); + + // Search for an item known to have other versions in test data: + $page = $this->performSearch('id:0001732009-0'); + + // Click on the "other versions" link: + $this->assertEquals( + 0, count($page->findAll('css', 'div.record-versions a')) + ); + } +} diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Config/UpgradeTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Config/UpgradeTest.php index c95afffbafc87683ed8ad4ef80628b0e22e5b80c..cbf0f74b97bed34e7537344f8878d777b411dcf7 100644 --- a/module/VuFind/tests/unit-tests/src/VuFindTest/Config/UpgradeTest.php +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Config/UpgradeTest.php @@ -133,7 +133,8 @@ class UpgradeTest extends \VuFindTest\Unit\TestCase $this->assertEquals( [ 'Author' => ['AuthorFacets', 'SpellingSuggestions'], - 'CallNumber' => ['TopFacets:ResultsTop'] + 'CallNumber' => ['TopFacets:ResultsTop'], + 'WorkKeys' => [''] ], $results['searches.ini']['TopRecommendations'] ); diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/XSLT/Import/VuFindWorkKeysTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/XSLT/Import/VuFindWorkKeysTest.php new file mode 100644 index 0000000000000000000000000000000000000000..015300b81c5d7395892d3ef9febdda419e340af7 --- /dev/null +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/XSLT/Import/VuFindWorkKeysTest.php @@ -0,0 +1,178 @@ +<?php +/** + * XSLT helper tests for VuFindWorkKeys. + * + * PHP version 7 + * + * Copyright (C) Villanova University 2020. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category VuFind + * @package Tests + * @author Demian Katz <demian.katz@villanova.edu> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:testing:unit_tests Wiki + */ +namespace VuFindTest\XSLT\Import; + +use VuFind\XSLT\Import\VuFindWorkKeys; + +/** + * XSLT helper tests for VuFindWorkKeys. + * + * @category VuFind + * @package Tests + * @author Demian Katz <demian.katz@villanova.edu> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:testing:unit_tests Wiki + */ +class VuFindWorkKeysTest extends \VuFindTest\Unit\TestCase +{ + /** + * Test the work keys helper with an include regex. + * + * @return void + */ + public function testGetWorkKeysWithIncludeRegEx() + { + $expected = '<?xml version="1.0" encoding="utf-8"?>' + . "\n<workKey>UT uniformtitle</workKey>\n" + . "<workKey>AT author1 thenonuniformtitle</workKey>\n" + . "<workKey>AT author2 thenonuniformtitle</workKey>\n" + . "<workKey>AT author1 nonuniformtitle</workKey>\n" + . "<workKey>AT author2 nonuniformtitle</workKey>\n"; + + $result = VuFindWorkKeys::getWorkKeys( + 'UNIFORM title.', + ['The nonUniform Title'], + ['nonUniform Title'], + ['AUTHOR 1', 'author 2'], + '/([0-9A-Za-z]+)/', + '' + ); + $this->assertEquals( + $expected, + simplexml_import_dom($result)->asXml() + ); + } + + /** + * Test the work keys helper with an include regex and trimmed titles === titles. + * + * @return void + */ + public function testGetWorkKeysWithIncludeRegExAndDuplicateTitles() + { + $expected = '<?xml version="1.0" encoding="utf-8"?>' + . "\n<workKey>UT uniformtitle</workKey>\n" + . "<workKey>AT author1 nonuniformtitle</workKey>\n" + . "<workKey>AT author2 nonuniformtitle</workKey>\n"; + + $result = VuFindWorkKeys::getWorkKeys( + 'UNIFORM title.', + ['nonUniform Title'], + ['nonUniform Title'], + ['AUTHOR 1', 'author 2'], + '/([0-9A-Za-z]+)/', + '' + ); + $this->assertEquals( + $expected, + simplexml_import_dom($result)->asXml() + ); + } + + /** + * Test the work keys helper with an exclude regex. + * + * @return void + */ + public function testGetWorkKeysWithExcludeRegEx() + { + $expected = '<?xml version="1.0" encoding="utf-8"?>' + . "\n<workKey>UT unformttle</workKey>\n" + . "<workKey>AT author1 thenonunformttle</workKey>\n" + . "<workKey>AT author2 thenonunformttle</workKey>\n" + . "<workKey>AT author1 nonunformttle</workKey>\n" + . "<workKey>AT author2 nonunformttle</workKey>\n"; + + $result = VuFindWorkKeys::getWorkKeys( + 'UNIFORM title', + ['The nonUniform Title'], + ['nonUniform Title'], + ['AUTHOR 1', 'author 2'], + '', + '/[i ]/i' // arbitrarily exclude spaces and i's for testing purposes + ); + $this->assertEquals( + $expected, + simplexml_import_dom($result)->asXml() + ); + } + + /** + * Test the work keys helper with an ICU tranliteration. + * + * @return void + */ + public function testGetWorkKeysWithTransliteration() + { + $expected = '<?xml version="1.0" encoding="utf-8"?>' + . "\n<workKey>UT uniformtitlea</workKey>\n" + . "<workKey>AT author1 thenonuniformtitle</workKey>\n" + . "<workKey>AT author2 thenonuniformtitle</workKey>\n" + . "<workKey>AT author1 nonuniformtitle</workKey>\n" + . "<workKey>AT author2 nonuniformtitle</workKey>\n"; + + $result = VuFindWorkKeys::getWorkKeys( + 'UNIFORM title +Ã…', + ['The nonUniform Titlë'], + ['nonUniform Titlë'], + ['AUTHOR * 1', 'author - 2'], + '', + '', + ':: NFD; :: lower; :: Latin; :: [^[:letter:] [:number:]] Remove; :: NFKC;' + ); + $this->assertEquals( + $expected, + simplexml_import_dom($result)->asXml() + ); + } + + /** + * Test the work keys helper with an ICU tranliteration. + * + * @return void + */ + public function testGetWorkKeysWithoutAuthors() + { + $expected = '<?xml version="1.0" encoding="utf-8"?>' + . "\n<workKey>UT uniformtitlea</workKey>\n"; + + $result = VuFindWorkKeys::getWorkKeys( + 'UNIFORM title +Ã…', + ['The nonUniform Titlë'], + ['nonUniform Titlë'], + [], + '', + '', + ':: NFD; :: lower; :: Latin; :: [^[:letter:] [:number:]] Remove; :: NFKC;' + ); + $this->assertEquals( + $expected, + simplexml_import_dom($result)->asXml() + ); + } +} diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Backend.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Backend.php index e48b8567a21d26365e1ee823a493ad0daf2544ce..deb257f80e2893a26d4a89b73e3272c5f83186f3 100644 --- a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Backend.php +++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Backend.php @@ -39,6 +39,7 @@ use VuFindSearch\Feature\RandomInterface; use VuFindSearch\Feature\RetrieveBatchInterface; use VuFindSearch\Feature\SimilarInterface; +use VuFindSearch\Feature\WorkExpressionsInterface; use VuFindSearch\ParamBag; use VuFindSearch\Query\AbstractQuery; @@ -58,7 +59,7 @@ use VuFindSearch\Response\RecordCollectionInterface; */ class Backend extends AbstractBackend implements SimilarInterface, RetrieveBatchInterface, RandomInterface, - GetIdsInterface + GetIdsInterface, WorkExpressionsInterface { /** * Limit for records per query in a batch retrieval. @@ -351,6 +352,41 @@ class Backend extends AbstractBackend return $this->deserialize($response); } + /** + * Return work expressions. + * + * @param string $id Id of record to compare with + * @param array $workKeys Work identification keys + * @param ParamBag $defaultParams Search backend parameters + * + * @return RecordCollectionInterface + */ + public function workExpressions($id, $workKeys, ParamBag $defaultParams = null) + { + $params = $defaultParams ? clone $defaultParams + : new \VuFindSearch\ParamBag(); + $this->injectResponseWriter($params); + $query = []; + foreach ($workKeys as $key) { + $key = addcslashes($key, '+-&|!(){}[]^"~*?:\\/'); + $query[] = "work_keys_str_mv:(\"$key\")"; + } + $params->set('q', implode(' OR ', $query)); + if ($id) { + $params->add('fq', sprintf('-id:"%s"', addcslashes($id, '"'))); + } + if (!$params->hasParam('rows')) { + $params->add('rows', 100); + } + if (!$params->hasParam('sort')) { + $params->add('sort', 'publishDateSort desc, title_sort asc'); + } + $response = $this->connector->search($params); + $collection = $this->createRecordCollection($response); + $this->injectSourceIdentifier($collection); + return $collection; + } + /** * Set the query builder. * diff --git a/module/VuFindSearch/src/VuFindSearch/Feature/WorkExpressionsInterface.php b/module/VuFindSearch/src/VuFindSearch/Feature/WorkExpressionsInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..693510b7c5cd3beda823101b869f9d637d970265 --- /dev/null +++ b/module/VuFindSearch/src/VuFindSearch/Feature/WorkExpressionsInterface.php @@ -0,0 +1,54 @@ +<?php + +/** + * Work expressions feature interface definition. + * + * PHP version 7 + * + * Copyright (C) The National Library of Finland 2019. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category VuFind + * @package Search + * @author Ere Maijala <ere.maijala@helsinki.fi> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org + */ +namespace VuFindSearch\Feature; + +use VuFindSearch\ParamBag; + +/** + * Work expressions feature interface definition. + * + * @category VuFind + * @package Search + * @author Ere Maijala <ere.maijala@helsinki.fi> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org + */ +interface WorkExpressionsInterface +{ + /** + * Return work expressions. + * + * @param string $id Id of record to compare with + * @param array $workKeys Work identification keys + * @param ParamBag $defaultParams Search backend parameters + * + * @return RecordCollectionInterface + */ + public function workExpressions($id, $workKeys, ParamBag $defaultParams = null); +} diff --git a/module/VuFindSearch/src/VuFindSearch/Service.php b/module/VuFindSearch/src/VuFindSearch/Service.php index 3680fbd11e81eca7fece0313910dacea33aa2888..bfac972b7a41c0a01634fedfd3ecae12f22a9650 100644 --- a/module/VuFindSearch/src/VuFindSearch/Service.php +++ b/module/VuFindSearch/src/VuFindSearch/Service.php @@ -6,6 +6,7 @@ * PHP version 7 * * Copyright (C) Villanova University 2010. + * Copyright (C) The National Library of Finland 2019. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, @@ -23,6 +24,7 @@ * @category VuFind * @package Search * @author David Maus <maus@hab.de> + * @author Ere Maijala <ere.maijala@helsinki.fi> * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org */ @@ -44,6 +46,7 @@ use VuFindSearch\Response\RecordCollectionInterface; * @category VuFind * @package Search * @author David Maus <maus@hab.de> + * @author Ere Maijala <ere.maijala@helsinki.fi> * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org */ @@ -347,6 +350,50 @@ class Service return $response; } + /** + * Return records for work expressions. + * + * @param string $backend Search backend identifier + * @param string $id Id of record to compare with + * @param array $workKeys Work identification keys (optional; retrieved from + * the record to compare with if not specified) + * @param ParamBag $params Search backend parameters + * + * @return RecordCollectionInterface + */ + public function workExpressions($backend, $id, $workKeys = null, + ParamBag $params = null + ) { + $params = $params ?: new \VufindSearch\ParamBag(); + $context = __FUNCTION__; + $args = compact('backend', 'id', 'params', 'context', 'workKeys'); + $backendInstance = $this->resolve($backend, $args); + $args['backend_instance'] = $backendInstance; + + $this->triggerPre($backendInstance, $args); + try { + if (!($backendInstance instanceof Feature\WorkExpressionsInterface)) { + throw new BackendException( + "$backend does not support workExpressions()" + ); + } + if (empty($args['workKeys'])) { + $records = $backendInstance->retrieve($id)->getRecords(); + if (!empty($records[0])) { + $fields = $records[0]->getRawData(); + $args['workKeys'] = $fields['work_keys_str_mv'] ?? []; + } + } + $response = $backendInstance + ->workExpressions($id, $args['workKeys'], $params); + } catch (BackendException $e) { + $this->triggerError($e, $args); + throw $e; + } + $this->triggerPost($response, $args); + return $response; + } + /** * Set EventManager instance. * diff --git a/themes/bootstrap3/js/combined-search.js b/themes/bootstrap3/js/combined-search.js index 13b3508406465b71fd09cbe7271d01c968f688b9..9a1871f3ea890e2aeb13e71f2f89201688f81b17 100644 --- a/themes/bootstrap3/js/combined-search.js +++ b/themes/bootstrap3/js/combined-search.js @@ -1,20 +1,26 @@ /*global VuFind, checkSaveStatuses, setupQRCodeLinks */ VuFind.combinedSearch = (function CombinedSearch() { - var init = function init(container, url) { + function initResultScripts(container) { + VuFind.openurl.init(container); + VuFind.itemStatuses.init(container); + checkSaveStatuses(container); + setupQRCodeLinks(container); + VuFind.recordVersions.init(container); + } + + function init(container, url) { container.load(url, '', function containerLoad(responseText) { if (responseText.length === 0) { container.hide(); } else { - VuFind.openurl.init(container); - VuFind.itemStatuses.init(container); - checkSaveStatuses(container); - setupQRCodeLinks(container); + initResultScripts(container); } }); - }; + } var my = { - init: init + init: init, + initResultScripts: initResultScripts }; return my; diff --git a/themes/bootstrap3/js/record_versions.js b/themes/bootstrap3/js/record_versions.js new file mode 100644 index 0000000000000000000000000000000000000000..8295b136266b20599e1ebc02ad1062cb19cf580a --- /dev/null +++ b/themes/bootstrap3/js/record_versions.js @@ -0,0 +1,57 @@ +/*global Hunt, VuFind */ + +VuFind.register('recordVersions', function recordVersions() { + function checkRecordVersions(_container) { + var container = typeof _container === 'undefined' ? $(document) : $(_container); + + var elements = container.hasClass('record-versions') && container.hasClass('ajax') + ? container : container.find('.record-versions.ajax'); + elements.each(function checkVersions() { + var $elem = $(this); + if ($elem.hasClass('loaded')) { + return; + } + $elem.addClass('loaded'); + $elem.removeClass('hidden'); + $elem.append('<span class="js-load">' + VuFind.translate('loading') + '...</span>'); + var $item = $(this).parents('.result'); + var id = $item.find('.hiddenId')[0].value; + var source = $item.find('.hiddenSource')[0].value; + $.getJSON( + VuFind.path + '/AJAX/JSON', + { + method: 'getRecordVersions', + id: id, + source: source + } + ) + .done(function onGetVersionsDone(response) { + if (response.data.length > 0) { + $elem.html(response.data); + } else { + $elem.text(''); + } + }) + .fail(function onGetVersionsFail() { + $elem.text(VuFind.translate('error_occurred')); + }); + }); + } + + function init(_container) { + if (typeof Hunt === 'undefined') { + checkRecordVersions(_container); + } else { + var container = typeof _container === 'undefined' + ? document.body + : _container; + new Hunt( + $(container).find('.record-versions.ajax').toArray(), + { enter: checkRecordVersions } + ); + } + } + + return { init: init, check: checkRecordVersions }; +}); + diff --git a/themes/bootstrap3/less/components/search.less b/themes/bootstrap3/less/components/search.less index 43c31909e92ca16ddfa45c1af7d3ae1bdb27fcb2..e3948ef8c1486a91ca7cae34d7ad9b9dbfedfe60 100644 --- a/themes/bootstrap3/less/components/search.less +++ b/themes/bootstrap3/less/components/search.less @@ -49,6 +49,17 @@ header .container.navbar { margin-bottom: 0; } .search-controls { text-align: right; } } +.versions-tab { + .search-controls { + padding-top: 0.5rem; + padding-left: 1rem; + text-align: left; + } + .search-header { + .search-stats { flex-grow: 0; } + } +} + .record-nav, .bulkActionButtons { .clearfix(); diff --git a/themes/bootstrap3/scss/components/search.scss b/themes/bootstrap3/scss/components/search.scss index b2377416c21c9dbc41d6368ef997ebaa40f8ab94..f5bf702e7258128dd5aa818b236930890028a445 100644 --- a/themes/bootstrap3/scss/components/search.scss +++ b/themes/bootstrap3/scss/components/search.scss @@ -49,6 +49,17 @@ header .container.navbar { margin-bottom: 0; } .search-controls { text-align: right; } } +.versions-tab { + .search-controls { + padding-top: 0.5rem; + padding-left: 1rem; + text-align: left; + } + .search-header { + .search-stats { flex-grow: 0; } + } +} + .record-nav, .bulkActionButtons { @include clearfix(); diff --git a/themes/bootstrap3/templates/RecordDriver/DefaultRecord/core.phtml b/themes/bootstrap3/templates/RecordDriver/DefaultRecord/core.phtml index 2b3d52454692becde21c978b669b097ddfab28f9..f03f1c355908841e4a81972d417cead92f2a7b93 100644 --- a/themes/bootstrap3/templates/RecordDriver/DefaultRecord/core.phtml +++ b/themes/bootstrap3/templates/RecordDriver/DefaultRecord/core.phtml @@ -34,6 +34,15 @@ <h1 property="name"><?=$this->escapeHtml($this->driver->getShortTitle() . ' ' . $this->driver->getSubtitle() . ' ' . $this->driver->getTitleSection())?></h1> + <?php if(!empty($this->extraControls)): ?> + <?=$this->extraControls['actionControls'] ?? ''?> + <?=$this->extraControls['availabilityInfo'] ?? ''?> + <?php endif; ?> + + <?php if ($this->searchOptions($this->driver->getSourceIdentifier())->getVersionsAction()): ?> + <?=$this->record($this->driver)->renderTemplate('versions-link.phtml')?> + <?php endif; ?> + <?php $summary = $this->driver->getSummary(); $summary = isset($summary[0]) ? $this->escapeHtml($summary[0]) : false; ?> <?php if ($summary): ?> <p><?=$this->truncate($summary, 300)?></p> diff --git a/themes/bootstrap3/templates/RecordDriver/DefaultRecord/result-list.phtml b/themes/bootstrap3/templates/RecordDriver/DefaultRecord/result-list.phtml index 354fed19b0fc8f7e1299637cbbc2a73c066e668d..99ec3394e1c41044212e777bc540d690c57a64b6 100644 --- a/themes/bootstrap3/templates/RecordDriver/DefaultRecord/result-list.phtml +++ b/themes/bootstrap3/templates/RecordDriver/DefaultRecord/result-list.phtml @@ -20,7 +20,7 @@ <div class="media-body"> <div class="result-body"> <div> - <a href="<?=$this->recordLink()->getUrl($this->driver)?>" class="title getFull" data-view="<?=$this->params->getOptions()->getListViewOption() ?>"> + <a href="<?=$this->recordLink()->getUrl($this->driver)?>" class="title getFull" data-view="<?=isset($this->params) ? $this->params->getOptions()->getListViewOption() : 'list' ?>"> <?=$this->record($this->driver)->getTitleHtml()?> </a> </div> @@ -97,6 +97,10 @@ </div> <?php endif; ?> + <?php if ($this->driver->tryMethod('getWorkKeys') && $this->searchOptions($this->driver->getSourceIdentifier())->getVersionsAction()): ?> + <div class="record-versions ajax"></div> + <?php endif; ?> + <div class="callnumAndLocation ajax-availability hidden"> <?php if ($this->driver->supportsAjaxStatus()): ?> <strong class="hideIfDetailed"><?=$this->transEsc('Call Number')?>:</strong> @@ -172,7 +176,7 @@ </span> <?php endif; ?> - <?php if ($this->cart()->isActiveInSearch() && $this->params->getOptions()->supportsCart() && $this->cart()->isActive()): ?> + <?php if ($this->cart()->isActiveInSearch() && isset($this->params) && $this->params->getOptions()->supportsCart() && $this->cart()->isActive()): ?> <?=$this->render('record/cart-buttons.phtml', ['id' => $this->driver->getUniqueId(), 'source' => $this->driver->getSourceIdentifier()]); ?><br/> <?php endif; ?> diff --git a/themes/bootstrap3/templates/RecordDriver/DefaultRecord/versions-link.phtml b/themes/bootstrap3/templates/RecordDriver/DefaultRecord/versions-link.phtml new file mode 100644 index 0000000000000000000000000000000000000000..4db0dbad6852969126e14429ed7390280bf40980 --- /dev/null +++ b/themes/bootstrap3/templates/RecordDriver/DefaultRecord/versions-link.phtml @@ -0,0 +1,20 @@ +<?php + $versions = $this->driver->tryMethod('getOtherVersionCount'); + if (!$versions) { + return; + } + if (isset($this->tabs['Versions'])) { + $url = !empty($this->full) ? $this->escapeHtmlAttr($this->recordLink()->getTabUrl($this->driver, 'Versions')) . '#tabnav' : '#versions'; + $translationKey = 'other_versions_link'; + } else { + $url = $this->recordLink()->getVersionsSearchUrl($this->driver); + $translationKey = 'other_versions_search_link'; + // Search results include this record, so increase the count + $versions++; + } +?> +<div class="record-versions"> + <a href="<?=$url?>"> + <i class="fa fa-arrow-right" aria-hidden="true"></i> <?=$this->transEsc($translationKey, ['%%count%%' => $versions])?> + </a> +</div> diff --git a/themes/bootstrap3/templates/RecordTab/versions.phtml b/themes/bootstrap3/templates/RecordTab/versions.phtml new file mode 100644 index 0000000000000000000000000000000000000000..c0cf3c1b5d5b1d83b31a871234e335ab14e39596 --- /dev/null +++ b/themes/bootstrap3/templates/RecordTab/versions.phtml @@ -0,0 +1,50 @@ +<?php + $results = $this->driver->tryMethod('getVersions', [false, 20]); + $view = 'list'; + $this->render('search/results-scripts.phtml', ['displayVersions' => true]); +?> +<div class="results result-view-<?=$this->escapeHtmlAttr($view)?>"> + <nav class="search-header hidden-print"> + <div class="search-stats"> + <?=$this->translate( + 'showing_results_of_html', [ + '%%start%%' => 1, + '%%end%%' => count($results), + '%%total%%' => $this->localizedNumber($results->getTotal()) + ] + ); + ?> + </div> + <div class="search-controls"> + <a class="more-link" href="<?=$this->recordLink()->getVersionsSearchUrl($this->driver)?>" rel="nofollow"> + <i class="fa fa-arrow-right" aria-hidden="true"></i> <?=$this->transEsc('other_versions_search_link', ['%%count%%' => $results->getTotal() + 1])?> + </a> + </div> + </nav> + <?php $recordNumber = $results->getOffset(); ?> + <?php foreach ($results as $current): ?> + <div id="result<?=++$recordNumber ?>" class="result clearfix<?=$current->supportsAjaxStatus() ? ' ajaxItem' : ''?>"> + <span class="sr-only"><?=$this->transEsc('Search Result');?> <?=$recordNumber ?></span> + <?=$this->record($current)->getSearchResult($view)?> + </div> + <?php endforeach; ?> + <div class="search-tools hidden-print"> + <div class="search-controls"> + <a class="more-link" href="<?=$this->recordLink()->getVersionsSearchUrl($this->driver)?>" rel="nofollow"> + <i class="fa fa-arrow-right" aria-hidden="true"></i> <?=$this->transEsc('other_versions_search_link', ['%%count%%' => $results->getTotal() + 1])?> + </a> + </div> + </div> +</div> + +<?php +// Add any translations added when processing the search results: +$translations = $this->jsTranslations()->getJSON(); +// Use combinedSearch.initResultScripts to avoid duplicating the list of scripts +// required for results loaded via AJAX: +$script = <<<JS +VuFind.addTranslations($translations); +VuFind.combinedSearch.initResultScripts($('.results')); +JS; +?> +<?=$this->inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $script, 'SET'); ?> diff --git a/themes/bootstrap3/templates/combined/results.phtml b/themes/bootstrap3/templates/combined/results.phtml index 0dbb615ba7eab7d87c02fd101a8f17d6eb51ff82..c488a7226ba6b29feb17beb08978c5bee65d8fa2 100644 --- a/themes/bootstrap3/templates/combined/results.phtml +++ b/themes/bootstrap3/templates/combined/results.phtml @@ -34,10 +34,18 @@ // Enable bulk options if appropriate: $this->showCheckboxes = $this->showCartControls || $this->showBulkOptions; + // Check if any results have version support enabled: + $displayVersions = false; + foreach (array_keys($this->combinedResults) as $configSection) { + list($searchClassId) = explode(':', $configSection); + if ($this->searchOptions($searchClassId)->getVersionsAction()) { + $displayVersions = true; + break; + } + } + // Load Javascript dependencies into header: - $this->headScript()->appendFile("vendor/hunt.min.js"); - $this->headScript()->appendFile("check_item_statuses.js"); - $this->headScript()->appendFile("check_save_statuses.js"); + $this->render('search/results-scripts.phtml', compact('displayVersions')); $this->headScript()->appendFile("combined-search.js"); // Style $this->headLink()->appendStylesheet('combined-search.css'); diff --git a/themes/bootstrap3/templates/search/controls/results-tools.phtml b/themes/bootstrap3/templates/search/controls/results-tools.phtml new file mode 100644 index 0000000000000000000000000000000000000000..a46e3805e1692daa1b97482814f6b236608ff0e8 --- /dev/null +++ b/themes/bootstrap3/templates/search/controls/results-tools.phtml @@ -0,0 +1,16 @@ +<div class="searchtools hidden-print"> + <strong><?=$this->transEsc('Search Tools')?>:</strong> + <a href="<?=$this->results->getUrlQuery()->setViewParam('rss')?>"><i class="fa fa-bell" aria-hidden="true"></i> <?=$this->transEsc('Get RSS Feed')?></a> + — + <a href="<?=$this->url('search-email')?>" class="mailSearch" data-lightbox id="mailSearch<?=$this->escapeHtmlAttr($this->results->getSearchId())?>"> + <i class="fa fa-envelope" aria-hidden="true"></i> <?=$this->transEsc('Email this Search')?> + </a> + <?php if ($this->accountCapabilities()->getSavedSearchSetting() === 'enabled' && is_numeric($this->results->getSearchId())): ?> + — + <?php if ($this->results->isSavedSearch()): ?> + <a href="<?=$this->url('myresearch-savesearch')?>?delete=<?=urlencode($this->results->getSearchId())?>"><i class="fa fa-remove" aria-hidden="true"></i> <?=$this->transEsc('save_search_remove')?></a> + <?php else: ?> + <a href="<?=$this->url('myresearch-savesearch')?>?save=<?=urlencode($this->results->getSearchId())?>"><i class="fa fa-save" aria-hidden="true"></i> <?=$this->transEsc('save_search')?></a> + <?php endif; ?> + <?php endif; ?> +</div> diff --git a/themes/bootstrap3/templates/search/controls/showing.phtml b/themes/bootstrap3/templates/search/controls/showing.phtml index b2598bb229b6908bfaf09fef5bb6cbf00e574d98..704ef616163e9c4eae3f8305921ce545310745ef 100644 --- a/themes/bootstrap3/templates/search/controls/showing.phtml +++ b/themes/bootstrap3/templates/search/controls/showing.phtml @@ -11,8 +11,11 @@ $transParams ); ?> +<?php // Use search-heading if it's set, but append only if it's not empty to avoid extra whitespace: ?> <?php if ($this->slot('search-heading')->isset()): ?> - <?php $showingResults .= ' ' . $this->slot('search-heading')->get(); ?> + <?php if ($heading = $this->slot('search-heading')->get()): ?> + <?php $showingResults .= " $heading"; ?> + <?php endif; ?> <?php elseif ($this->params->getSearchType() == 'basic'): $showingResults = $this->translate( isset($this->skipTotalCount) ? 'showing_results_for_html' : 'showing_results_of_for_html', diff --git a/themes/bootstrap3/templates/search/results-scripts.phtml b/themes/bootstrap3/templates/search/results-scripts.phtml new file mode 100644 index 0000000000000000000000000000000000000000..1c61bccca75455ce75f8d1887e2798002becf7c1 --- /dev/null +++ b/themes/bootstrap3/templates/search/results-scripts.phtml @@ -0,0 +1,9 @@ +<?php +// Load Javascript dependencies into header: +$this->headScript()->appendFile("vendor/hunt.min.js"); +$this->headScript()->appendFile("check_item_statuses.js"); +$this->headScript()->appendFile("check_save_statuses.js"); +if ($this->displayVersions) { + $this->headScript()->appendFile("record_versions.js"); + $this->headScript()->appendFile("combined-search.js"); +} diff --git a/themes/bootstrap3/templates/search/results.phtml b/themes/bootstrap3/templates/search/results.phtml index 938d198775f6ee9e9c9db6c7f1e023761ed8216e..55444d5650d01e48514c1544e72e1f9bf006890c 100644 --- a/themes/bootstrap3/templates/search/results.phtml +++ b/themes/bootstrap3/templates/search/results.phtml @@ -34,16 +34,13 @@ // Enable bulk options if appropriate: $this->showCheckboxes = $this->showCartControls || $this->showBulkOptions; - // Load Javascript only if list view parameter is NOT full: - if ($this->params->getOptions()->getListViewOption() != "full") { - $this->headScript()->appendFile("record.js"); - $this->headScript()->appendFile("embedded_record.js"); - } + $this->render('search/results-scripts.phtml', ['displayVersions' => !empty($this->params->getOptions()->getVersionsAction())]); - // Load Javascript dependencies into header: - $this->headScript()->appendFile("vendor/hunt.min.js"); - $this->headScript()->appendFile("check_item_statuses.js"); - $this->headScript()->appendFile("check_save_statuses.js"); + // Load only if list view parameter is NOT full: + if ($this->params->getOptions()->getListViewOption() !== 'full') { + $this->headScript()->appendFile("record.js"); + $this->headScript()->appendFile("embedded_record.js"); + } ?> <div class="<?=$this->layoutClass('mainbody')?>"> @@ -103,25 +100,7 @@ <?=$this->render('search/list-' . $this->params->getView() . '.phtml')?> <?=$this->context($this)->renderInContext('search/bulk-action-buttons.phtml', ['idPrefix' => 'bottom_', 'formAttr' => 'search-cart-form'])?> <?=$this->paginationControl($this->results->getPaginator(), 'Sliding', 'search/pagination.phtml', ['results' => $this->results, 'options' => isset($this->paginationOptions) ? $this->paginationOptions : []])?> - - <div class="searchtools hidden-print"> - <strong><?=$this->transEsc('Search Tools')?>:</strong> - <a href="<?=$this->results->getUrlQuery()->setViewParam('rss')?>"><i class="fa fa-bell" aria-hidden="true"></i> <?=$this->transEsc('Get RSS Feed')?></a> - — - <a href="<?=$this->url('search-email')?>" class="mailSearch" data-lightbox id="mailSearch<?=$this->escapeHtmlAttr($this->results->getSearchId())?>"> - <i class="fa fa-envelope" aria-hidden="true"></i> <?=$this->transEsc('Email this Search')?> - </a> - <?php if ($this->accountCapabilities()->getSavedSearchSetting() === 'enabled'): ?> - — - <?php if (is_numeric($this->results->getSearchId())): ?> - <?php if ($this->results->isSavedSearch()): ?> - <a href="<?=$this->url('myresearch-savesearch')?>?delete=<?=urlencode($this->results->getSearchId())?>"><i class="fa fa-remove" aria-hidden="true"></i> <?=$this->transEsc('save_search_remove')?></a> - <?php else: ?> - <a href="<?=$this->url('myresearch-savesearch')?>?save=<?=urlencode($this->results->getSearchId())?>"><i class="fa fa-save" aria-hidden="true"></i> <?=$this->transEsc('save_search')?></a> - <?php endif; ?> - <?php endif; ?> - <?php endif; ?> - </div> + <?=$this->context($this)->renderInContext('search/controls/results-tools.phtml', ['results' => $this->results])?> <?php endif; ?> </div> <?php /* End Main Listing */ ?> diff --git a/themes/bootstrap3/templates/search/versions-searchbox.phtml b/themes/bootstrap3/templates/search/versions-searchbox.phtml new file mode 100644 index 0000000000000000000000000000000000000000..2a687d247faa857f2b495ddad1d7a6426e3248d8 --- /dev/null +++ b/themes/bootstrap3/templates/search/versions-searchbox.phtml @@ -0,0 +1,24 @@ +<?php + $options = $this->searchOptions($this->searchClassId); + $searchHome = $options->getSearchHomeAction(); + $advSearch = $options->getAdvancedSearchAction(); + + $hiddenFilters = $this->searchTabs()->getHiddenFilters($this->searchClassId, false, false); + if (empty($hiddenFilters)) { + $hiddenFilters = $this->searchMemory()->getLastHiddenFilters($this->searchClassId); + if (empty($hiddenFilters)) { + $hiddenFilters = $this->searchTabs()->getHiddenFilters($this->searchClassId); + } + } + $hiddenFilterParams = $this->searchTabs()->getCurrentHiddenFilterParams($this->searchClassId, false, '?'); +?> +<?php $tabConfig = $this->searchTabs()->getTabConfig($this->searchClassId, '', $this->searchIndex, 'basic', $hiddenFilters); ?> +<?php $tabs = $this->context($this)->renderInContext('search/searchTabs', ['searchTabs' => $tabConfig['tabs']]); ?> +<?php if (!empty($tabs)): ?><?=$tabs ?><div class="tab-content clearfix"><?php endif; ?> + <p class="adv_search_links"> + <a href="<?=$this->url($searchHome) . $hiddenFilterParams?>"><?=$this->transEsc("Start a new Basic Search")?></a> + <?php if ($advSearch): ?> + | <a href="<?=$this->url($advSearch) . $hiddenFilterParams?>"><?=$this->transEsc("Start a new Advanced Search")?></a> + <?php endif; ?> + </p> +<?php if (!empty($tabs)): ?></div><?php endif; ?> diff --git a/themes/bootstrap3/templates/search/versions.phtml b/themes/bootstrap3/templates/search/versions.phtml new file mode 100644 index 0000000000000000000000000000000000000000..39762ece16be6f431cace5e05e7848309c29eb79 --- /dev/null +++ b/themes/bootstrap3/templates/search/versions.phtml @@ -0,0 +1,44 @@ +<?php + // Set up versions-specific details: + $title = $this->translate('Versions'); + if ($this->driver) { + $title .= ' - ' . $this->driver->getBreadcrumb(); + } + $this->slot('head-title')->set($title); + $this->slot('search-heading')->set(''); + $this->slot('empty-message')->set( + $this->translate('nohit_lookfor_html', ['%%lookfor%%' => $this->transEsc('Versions')]) + ); + + // Call the standard search template: + echo $this->render('search/results.phtml'); + + // Override searchbox: + $this->layout()->searchbox = $this->context($this)->renderInContext( + 'search/versions-searchbox.phtml', + [ + 'searchType' => 'basic', + 'searchIndex' => $this->params->getSearchHandler(), + 'searchClassId' => $this->params->getSearchClassId(), + 'selectedShards' => $this->params->getSelectedShards(), + ] + ) . $this->context($this)->renderInContext( + 'search/filters.phtml', + [ + 'params' => $this->params, + 'urlQuery' => $this->results->getUrlQuery(), + 'filterList' => $this->params->getFilterList(true), + 'checkboxFilters' => $this->params->getCheckboxFacets(), + 'searchClassId' => $this->params->getSearchClassId(), + 'searchType' => $this->params->getSearchType(), + ] + ); + + // Override breadcrumbs: + $titleLink = ''; + if ($this->driver) { + $titleLink .= ' - <a href="' . $this->escapeHtmlAttr($this->recordLink()->getUrl($this->driver)) . '">' . $this->driver->getBreadcrumb() . '</a>'; + } + + $this->layout()->breadcrumbs = '<li class="active">' . $this->transEsc('Versions') . $titleLink . '</li>'; +?> diff --git a/themes/bootstrap3/templates/search2/versions.phtml b/themes/bootstrap3/templates/search2/versions.phtml new file mode 100644 index 0000000000000000000000000000000000000000..c8c616a732f59e0837642464530973b435b6ab9f --- /dev/null +++ b/themes/bootstrap3/templates/search2/versions.phtml @@ -0,0 +1 @@ +<?=$this->render('search/versions.phtml'); ?>