From 8af8696787b7a3b4816bbb0b1cf11a6f1b036a80 Mon Sep 17 00:00:00 2001
From: Dorian Merz <merz@ub.uni-leipzig.de>
Date: Sat, 14 Nov 2020 11:52:52 +0100
Subject: [PATCH] refs #17407_merge [finc] amsl resources: add links,
 description, caching and fix toggling

* generate search links for mega collections
** alike old sources list
** configure by setting show_link = true in Amsl.ini

* show descriptions of mega collections
** use collection_description, configure with show_description = true and sub_description_key = 'collection_description'

* configure additional resources manually in Amsl.ini with [AdditionalResources] and entries like:
** mega_collection = source_id

* configure misspelled resources manually in Amls.ini with [MisspelledResources] and entries like:
** wrong collection label = correct collection label for links
** value of "" means no link, only text

* refactore button for collapse / expand all list items in sources-display.js
** disable button while collapsing
** toggle label after collapsing / expanding
** don't expand / collapse already expanded / collapsed panels when clicking

* sort output by source label when configured in Amsl.ini

* refine caching
** encapsulate clearing of cache into own function
** use own namespace
** set default caching time to one day if not configured in Amsl.ini
** cache fully rendered HTML instead of API output
** boosts performance

* remove unused features

co-authored by: Robert Lange <robert.lange@uni-leipzig.de>
---
 local/languages/de.ini                        |   1 +
 local/languages/en.ini                        |   1 +
 .../Controller/AmslResourceController.php     | 229 ++++++++++++++----
 .../AmslResourceControllerFactory.php         |   3 +-
 themes/finc/js/sources-display.js             |  16 +-
 themes/finc/scss/compiled.scss                |   8 +
 themes/finc/templates/amsl/sources-list.phtml |  34 ++-
 7 files changed, 240 insertions(+), 52 deletions(-)

diff --git a/local/languages/de.ini b/local/languages/de.ini
index 6f2899bb1c0..c0b5aeabfe3 100644
--- a/local/languages/de.ini
+++ b/local/languages/de.ini
@@ -1954,6 +1954,7 @@ sources_explanatory_line = "Folgende Ressourcen sind im lizenzierten Zeitraum du
 Filter list = "Liste filtern"
 Please enter filter term = "Bitte Begriff(e) zum Filtern eingeben"
 Expand all = "Alle ausklappen"
+Collapse all = "Alle einklappen"
 Expand = "Ausklappen"
 resources_cannot_received = "Fehler aufgetreten. Resourcen konnten nicht geladen werden"
 support_by_dfg = "Die Nationallizenzen wurden gefördert durch die"
diff --git a/local/languages/en.ini b/local/languages/en.ini
index 389553aabc0..a725c125450 100644
--- a/local/languages/en.ini
+++ b/local/languages/en.ini
@@ -2058,6 +2058,7 @@ Filter list = "Filter list "
 Please enter filter term = "Please enter filter term(s)"
 Expand all = "Expand all"
 Expand = "Expand"
+Collapse all = "Collapse all"
 resources_cannot_received = "Error occurred. List of resources cannot loaded"
 support_by_dfg = "National licenses were sponsored by "
 
diff --git a/module/finc/src/finc/Controller/AmslResourceController.php b/module/finc/src/finc/Controller/AmslResourceController.php
index 7d36b314fba..70b355af1c1 100644
--- a/module/finc/src/finc/Controller/AmslResourceController.php
+++ b/module/finc/src/finc/Controller/AmslResourceController.php
@@ -43,22 +43,40 @@ use Zend\ServiceManager\ServiceLocatorInterface;
  */
 class AmslResourceController extends AbstractBase
 {
+    const DEFAULT_TTL = 86400;
+
     /**
      * Amsl.ini configuration.
      *
-     * @var $config
-     * @access protected
+     * @var \Zend\Config\Config
      */
     protected $config = [];
 
+    /**
+     * @var mixed|null AMSL API configuration
+     */
+    protected $apiConfig;
+
     /**
      * HTTP client
      *
      * @var \Zend\Http\Client
-     * @access protected
      */
     protected $httpClient;
 
+    /**
+     * Cache manager
+     *
+     * @var \VuFind\Cache\Manager
+     */
+    protected $cacheManager;
+
+    /**
+     * base url
+     *
+     * @var string
+     */
+    protected $baseUrl;
 
     /**
      * Constructor
@@ -66,17 +84,20 @@ class AmslResourceController extends AbstractBase
      * @param ServiceLocatorInterface $sm
      * @param \Zend\Config\Config     $config     VuFind configuration
      * @param \VuFindHttp\HttpService $httpClient HttpClient
+     * @param \VuFind\Cache\Manager $cacheManager Cache manager (optional)
      *
-     * @access public
      */
     public function __construct(
         ServiceLocatorInterface $sm,
         \Zend\Config\Config $config,
-        \VuFindHttp\HttpService $httpClient
+        \VuFindHttp\HttpService $httpClient,
+        \VuFind\Cache\Manager $cacheManager = null
     ) {
         parent::__construct($sm);
         $this->config = $config;
+        $this->apiConfig = $config->API;
         $this->httpClient = $httpClient;
+        $this->cacheManager = $cacheManager;
     }
 
     /**
@@ -84,42 +105,62 @@ class AmslResourceController extends AbstractBase
      *
      * @return \Zend\View\Model\ViewModel
      * @throws \Exception
-     * @access public
      */
     public function homeAction()
     {
         // Make view
-        $api_conf = $this->config->get('API');
         $view = $this->createViewModel();
-        try {
-            if (null == ($result = $this->httpClient->get($api_conf->url))) {
-                throw new \Exception(
-                    'Unexpected value: No api result received'
+        $view->setTemplate('amsl/sources-list');
+
+        if ($data = $this->getCacheData('rendered')) {
+            $view->rendered_html = $data;
+        } else {
+            try {
+                $view->sources = $this->getData();
+            } catch (\Exception $e) {
+                $this->flashMessenger()->addMessage(
+                    'resources_cannot_received',
+                    'error'
                 );
             }
-            if ($result->isSuccess()) {
-                switch ($api_conf->response_type) {
-                    case 'application/json':
-                        $amsl_sources = json_decode($result->getBody(), true);
-                        break;
-                    default:
-                        throw new \Exception(
-                            'Invalid argument: No valid header scheme defined'
-                        );
-                        break;
-                }
+            $rendered = $this->getViewRenderer()->render($view);
+            $this->setCacheData($rendered,'rendered');
+            $view->rendered_html = $rendered;
+        }
+
+        return $view;
+    }
+
+    /**
+     * Retrieve amsl sources list, prepare and cache it
+     *
+     * @return array
+     */
+    protected function getData()
+    {
+        if (null == ($result = $this->httpClient->get($this->apiConfig->url))) {
+            throw new \Exception(
+                'Unexpected value: No api result received'
+            );
+        }
+        if ($result->isSuccess()) {
+            switch ($this->apiConfig->response_type) {
+                case 'application/json':
+                    $amsl_sources = json_decode($result->getBody(), true);
+                    break;
+                default:
+                    throw new \Exception(
+                        'Invalid argument: No valid header scheme defined'
+                    );
+                    break;
             }
+            
             if (isset($amsl_sources)) {
-                $view->sources = $this->createSourceHierarchy($amsl_sources);
+                $results = $this->createSourceHierarchy($amsl_sources);
             }
-        } catch (\Exception $e) {
-            $this->flashMessenger()->addMessage(
-                'resources_cannot_received',
-                'error'
-            );
         }
-        $view->setTemplate('amsl/sources-list');
-        return $view;
+
+        return $results;
     }
 
     /**
@@ -128,42 +169,44 @@ class AmslResourceController extends AbstractBase
      * @param array $amsl_sources
      *
      * @return array $out
-     * @access protected
      */
     protected function createSourceHierarchy(array $amsl_sources)
     {
-        $struct = $this->config->get('Mapping');
-        $main_key = $struct->main_key;
-        $sub_key = $struct->sub_key;
+        $mapping = $this->config->get('Mapping');
         $sources = [];
 
         foreach ($amsl_sources as $source) {
-            if (isset($source[$main_key])) {
-                if (isset($source[$sub_key])) {
-                    $label = $this->renderLabel($struct->sub_label, $source);
-                    $sources[$source[$main_key]][$label] = $source;
+            $this->prepareSourceForView($source);
+            if (isset($source[$mapping->main_key])) {
+                if (isset($source[$mapping->sub_key])) {
+                    $label = $this->renderLabel($mapping->sub_label, $source);
+                    $sources[$source[$mapping->main_key]][$label] = $source;
                 } else {
-                    $sources[$source[$main_key]][$struct->default_sub_label]
-                        = $source;
+                    $sources[$source[$mapping->main_key]][$mapping->default_sub_label] = $source;
                 }
             } else {
-                if (isset($source[$sub_key])) {
-                    $label = $this->renderLabel($struct->sub_label, $source);
+                if (isset($source[$mapping->sub_key])) {
+                    $label = $this->renderLabel($mapping->sub_label, $source);
                     $default[$label] = $source;
                 } else {
-                    $default[$struct->default_sub_label] = $source;
+                    $default[$mapping->default_sub_label] = $source;
                 }
             }
         }
         ksort($sources);
         $out = [];
         foreach ($sources as $main) {
-            $label = $this->renderLabel($struct->main_label, current($main));
+            $label = $this->renderLabel($mapping->main_label, current($main));
             $out[$label] = $main;
         }
         if (isset($default)) {
-            $out[$struct->default_main_label] = $default;
+            $out[$mapping->default_main_label] = $default;
         }
+
+        if (!empty($mapping->sortBySourceLabel)) {
+            ksort($out);
+        }
+
         return $out;
     }
 
@@ -174,7 +217,6 @@ class AmslResourceController extends AbstractBase
      * @param array  $input_array Input
      *
      * @return mixed
-     * @access protected
      */
     protected function renderLabel($pattern, $input_array)
     {
@@ -190,4 +232,99 @@ class AmslResourceController extends AbstractBase
             $pattern
         );
     }
+
+    /**
+     * Helper function to generate link and description
+     *
+     * @param array &$source Input
+     *
+     * @return void
+     */
+    protected function prepareSourceForView(&$source)
+    {
+        $mapping = $this->config->get('Mapping');
+
+        if (isset($mapping->sub_description_key) && isset($source[$mapping->sub_description_key])) {
+            if (!empty($mapping->show_description)) {
+                $source["desc"] = $source[$mapping->sub_description_key];
+            }
+            unset($source[$mapping->sub_description_key]);
+        }
+
+        if(!empty($mapping->show_link) && !empty($source[$mapping->sub_key])) {
+            $misspelled = $this->config->get('MisspelledResources');
+            $searchTerm = $source[$mapping->sub_key];
+            if (!empty($misspelled)) {
+                foreach ($misspelled as $wrongLabel => $rightLabel) {
+                    if (strpos($searchTerm, $wrongLabel) !== false) {
+                        $searchTerm = $rightLabel;
+                        break;
+                    }
+                }
+            }
+
+            if (!$this->baseUrl) {
+                $urlHelper = $this->getViewRenderer()->plugin('url');
+                $this->baseUrl = $urlHelper('search-results') . '?filter%5B%5D=mega_collection%3A"';
+            }
+
+            if (!empty($searchTerm)) {
+                $source["href"] = $this->baseUrl . urlencode($searchTerm) . '"';
+            }
+        }
+    }
+
+    /**
+     * @return string | null
+     * @throws \Exception
+     */
+    private function getCacheData($tag = ''): ?string
+    {
+        $cache = $this->cacheManager->getCache('object', $this->getCacheNamespace());
+
+        $cacheKey = $this->getCacheKey($tag);
+        $ttl = $this->apiConfig['ttl'] ?? self::DEFAULT_TTL;
+
+        if($cache) {
+            $cache->getOptions()->setTtl($ttl);
+            $cache->clearExpired();
+        }
+
+        return $cache->getItem($cacheKey);
+    }
+
+    /**
+     * Helper function for writing data to controller specific cache
+     *
+     * @param string $value Cache value to write
+     * @param string $tag   optional tag to be used for cache key
+     * @throws \Exception
+     */
+    private function setCacheData($value, $tag = '')
+    {
+        $cache = $this->cacheManager->getCache('object', $this->getCacheNamespace());
+        $cacheKey = $this->getCacheKey($tag);
+        $cache->setItem($cacheKey, $value);
+    }
+
+    /**
+     * Get the namespace to use for caching amls responses.
+     *
+     * @return string
+     */
+    protected function getCacheNamespace()
+    {
+        return 'amsl';
+    }
+
+    /**
+     * Helper function to generate config-specific cache key
+     *
+     * @param string $tag optional tag to make cache more specific
+     * @return string
+     */
+    protected function getCacheKey($tag = '')
+    {
+        return md5(json_encode($this->config->toArray()).$tag);
+    }
 }
diff --git a/module/finc/src/finc/Controller/AmslResourceControllerFactory.php b/module/finc/src/finc/Controller/AmslResourceControllerFactory.php
index da63025c162..92f8b637cc5 100644
--- a/module/finc/src/finc/Controller/AmslResourceControllerFactory.php
+++ b/module/finc/src/finc/Controller/AmslResourceControllerFactory.php
@@ -16,7 +16,8 @@ class AmslResourceControllerFactory
         return new AmslResourceController(
             $container,
             $container->get('VuFind\Config\PluginManager')->get('Amsl'),
-            $container->get('VuFindHttp\HttpService')
+            $container->get('VuFindHttp\HttpService'),
+            $container->get('VuFind\Cache\Manager')
         );
     }
 }
\ No newline at end of file
diff --git a/themes/finc/js/sources-display.js b/themes/finc/js/sources-display.js
index 7fe2902a9d2..a625605e06e 100644
--- a/themes/finc/js/sources-display.js
+++ b/themes/finc/js/sources-display.js
@@ -5,8 +5,20 @@ $('.collapse-toggler').click(function () {
 });
 
 // Collapse all button
-$('.collapse-all-toggler').click(function () {
-  $('#sources-list li ul').collapse('toggle');
+$('#collapse-all-toggler').click(function () {
+  $(this).prop('disabled', true);
+  if($(this).hasClass('expanded')) {
+    // don't collapse already collapsed
+    $('#sources-list li ul.panel-collapse').filter('.in').collapse('toggle');
+  } else {
+    // don't expand already expanded
+    $('#sources-list li ul.panel-collapse').filter(':not(.in)').collapse('toggle');
+  }
+  $('#collapse-all-toggler').toggleClass('expanded');
+});
+
+$('#sources-list li ul.panel-collapse').on('shown.bs.collapse hidden.bs.collapse', function() {
+  $('#collapse-all-toggler').prop('disabled', false);
 });
 
 // toggle chevron
diff --git a/themes/finc/scss/compiled.scss b/themes/finc/scss/compiled.scss
index f55b13bfe43..8725d299856 100644
--- a/themes/finc/scss/compiled.scss
+++ b/themes/finc/scss/compiled.scss
@@ -3071,6 +3071,14 @@ input {
   }
 }
 
+// #17407 change text of button when expanded / collapsed
+#collapse-all-toggler:not(.expanded) .text-expanded {
+  display: none;
+}
+#collapse-all-toggler.expanded .text-collapsed {
+  display: none;
+}
+
 // AMSL - END
 
 .template-dir-browse.template-name-home {
diff --git a/themes/finc/templates/amsl/sources-list.phtml b/themes/finc/templates/amsl/sources-list.phtml
index 7ff901ba411..371f906684d 100644
--- a/themes/finc/templates/amsl/sources-list.phtml
+++ b/themes/finc/templates/amsl/sources-list.phtml
@@ -1,3 +1,10 @@
+<?php
+  /* cache fully rendered HTML of THIS template result in AmslController at the first call */
+  if (isset($this->rendered_html)) {
+    echo $this->rendered_html;
+    return;
+  }
+?>
 <!-- finc: amsl/sources-list - home -->
 <?php
 // Set up page title:
@@ -22,10 +29,14 @@ $this->layout()->breadcrumbs .= '</li> <li class="active">' . $this->transEsc('L
   </form>
 
   <p>
-    <button data-toggle="collapse" class="btn btn-default collapse-all-toggler" href="javascript:void(0)"><?=$this->transEsc('Expand all')?></button>
+    <button id="collapse-all-toggler" class="btn btn-default" href="javascript:void(0)">
+      <span class="text-collapsed"><?=$this->transEsc('Expand all')?></span>
+      <span class="text-expanded"><?=$this->transEsc('Collapse all')?></span>
+    </button>
   </p>
 
   <ul id="sources-list">
+    <?$itemCount = 0;?>
     <?php foreach ($this->sources as $label => $source): ?>
       <?php if (!empty($source)): ?>
         <li>
@@ -36,7 +47,24 @@ $this->layout()->breadcrumbs .= '</li> <li class="active">' . $this->transEsc('L
           </a>
           <ul class="panel-collapse collapse" aria-expanded="false">
             <?php foreach ($source as $sub_label => $collection): ?>
-              <li><?=$sub_label?></li>
+              <li>
+                <?php if (!empty($collection['href'])): ?>
+                  <a title="<?=$this->transEsc("Search For")?> <?=$sub_label?>" href='<?=$collection["href"]?>' target="_blank">
+                    <?=$sub_label?>
+                  </a>
+                <?php else: ?>
+                  <div tabindex="0" aria-label="<?=$this->transEsc("Source Title")?>">
+                      <?=$sub_label?>
+                  </div>
+                <?php endif; ?>
+                <?php if (!empty($collection['desc'])): ?>
+                  <div class="margin-t" tabindex="0" aria-label="<?=$this->transEsc("Description")?>">
+                    <i class="fa fa-info-circle" aria-hidden="true"></i>
+                    <?=$collection['desc']?>
+                  </div>
+                <br/>
+                <?php endif; ?>
+            </li>
             <?php endforeach; ?>
           </ul>
         </li>
@@ -59,4 +87,4 @@ $this->layout()->breadcrumbs .= '</li> <li class="active">' . $this->transEsc('L
 <?php /* run collapse togglers + introduce a case-insensitive filter that is capable of filtering multiple filtering terms */
 echo $this->inlineScript(\Zend\View\Helper\HeadScript::FILE, 'sources-display.js', 'SET');
 ?>
-<!-- finc: amsl/sources-list - home - END -->
+<!-- finc: amsl/sources-list - home - END -->
\ No newline at end of file
-- 
GitLab