From 01cb95e8a57f954e72f290d78dfc5adb73525f67 Mon Sep 17 00:00:00 2001
From: gloas <gloas@afi-sa.fr>
Date: Wed, 20 Jun 2018 16:19:33 +0200
Subject: [PATCH] dev #72825 handle laons table columns configuration

---
 .../admin/controllers/ModulesController.php   |  18 +-
 .../opac/controllers/AbonneController.php     |   4 +-
 .../opac/views/scripts/abonne/prets.phtml     |  48 +++--
 library/Class/Profil/Preferences/Loans.php    | 186 ++++++++++++++++++
 library/Class/Profil/Preferences/LoansPNB.php |  91 +++++++++
 library/Class/TableDescription.php            |  57 +++++-
 library/ZendAfi/Form/Configuration/Loans.php  | 101 +---------
 library/ZendAfi/Form/User/LoanSearch.php      |   5 +-
 library/ZendAfi/View/Helper/Abonne/Loans.php  | 103 ++++------
 .../ZendAfi/View/Helper/Abonne/LoansPNB.php   |  37 +---
 .../ZendAfi/View/Helper/Abonne/Operation.php  |  20 ++
 library/ZendAfi/View/Helper/RenderTable.php   |  61 +++++-
 .../controllers/AbonneControllerPretsTest.php |   4 +-
 .../HandleBranchcode/HandleBranchcodeTest.php |  24 +--
 14 files changed, 509 insertions(+), 250 deletions(-)
 create mode 100644 library/Class/Profil/Preferences/Loans.php
 create mode 100644 library/Class/Profil/Preferences/LoansPNB.php

diff --git a/application/modules/admin/controllers/ModulesController.php b/application/modules/admin/controllers/ModulesController.php
index f7898ca5df9..1b3f90c0107 100644
--- a/application/modules/admin/controllers/ModulesController.php
+++ b/application/modules/admin/controllers/ModulesController.php
@@ -88,11 +88,21 @@ class Admin_ModulesController extends ZendAfi_Controller_Action {
   }
 
   public function abonnePretsAction() {
-    $this->view->form = ZendAfi_Form_Configuration_Loans::newWith($this->preferences);
-    $this->view->form->setAction($this->view->url());
-    if ($this->_request->isPost())
-      $this->updateEtRetour($this->_request->getPost());
+    $params = Class_Profil::getCurrentProfil()
+      ->getConfigurationOf('abonne', 'prets', '');
+    $post_values = $this->_request->getPost();
+
+    $params = array_merge($params, $post_values);
+
+    $this->view->titre = $this->_('Configuration du tableau des prêts');
 
+    $form = ZendAfi_Form_Configuration_Loans::newWith($params);
+    $form->setAction($this->view->url());
+
+    if ($this->_request->isPost() && $form->isValid($post_values))
+      return $this->updateEtRetour($post_values);
+
+    $this->view->form = $form;
   }
 
 
diff --git a/application/modules/opac/controllers/AbonneController.php b/application/modules/opac/controllers/AbonneController.php
index 4e701d9e0d0..6f9bfa3df70 100644
--- a/application/modules/opac/controllers/AbonneController.php
+++ b/application/modules/opac/controllers/AbonneController.php
@@ -374,11 +374,11 @@ class AbonneController extends ZendAfi_Controller_Action {
                                                      'start_issue_date' => $this->_getPost('start_issue_date'),
                                                      'end_issue_date' => $this->_getPost('end_issue_date')]);
 
-    if (Class_AdminVar::searchLoanIsActive())
+    if ((new Class_Profil_Preferences_Loans)->isSearchEnabled(Class_Profil::getCurrentProfil()))
       $this->view->form = ZendAfi_Form_User_LoanSearch::newWith($this->_request->getParams());
 
     $cards = new Class_User_Cards($this->_user);
-    $this->view->loans = $cards->getLoansWithOutPNB($this->_request->getParams());
+    $this->view->loans = $loans = $cards->getLoansWithOutPNB($this->_request->getParams());
     $renewable_loans = $cards->getRenewableLoans($this->view->loans);
 
     $this->view->loans_ids = implode(';',
diff --git a/application/modules/opac/views/scripts/abonne/prets.phtml b/application/modules/opac/views/scripts/abonne/prets.phtml
index b12860dbee5..21f48c3219f 100644
--- a/application/modules/opac/views/scripts/abonne/prets.phtml
+++ b/application/modules/opac/views/scripts/abonne/prets.phtml
@@ -1,33 +1,39 @@
 <?php
-$this->openBoite('Prêts en cours');
-echo $this->tag('div',
-                $this->user->getNomAff(),
-                ['class' => 'abonneTitre']);
+$this->openBoite($this->_('Prêts en cours'));
 
-if($this->form)
-  echo $this->renderForm($this->form);
+$loans = $this->loans;
 
-$extend_all = $this->tagAnchor(['action' => 'prolongerPret',
-                                'id_pret' => $this->loans_ids],
-                               $this->_('Tout prolonger'),
-                               ['class' => 'extend_all']);
+$extend_all = $this->loans_ids
+  ? ($this->tagAnchor(['action' => 'prolongerPret',
+                       'id_pret' => $this->loans_ids],
+                      $this->_('Tout prolonger'),
+                      ['class' => 'extend_all']))
+  : '';
 
-if ($this->error)
-  echo $this->tag('p', $this->error, ['class' => 'error']);
+$pnb = ($this->user->hasPNB())
+  ? ($this->tag('h2', $this->_('Prêts numériques en cours'))
+     . $this->abonne_LoansPNB($this->user->getPNBLoans()))
+  : '';
 
-echo $extend_all;
+$content = [$this->tag('div',
+                       $this->user->getNomAff(),
+                       ['class' => 'abonneTitre']),
 
-if ($emprunts = $this->user->getEmprunts())
-  echo $this->abonne_LoanExport();
+            ($this->form ? $this->renderForm($this->form) : ''),
 
-echo $this->abonne_Loans($this->loans);
+            $extend_all,
 
-echo $extend_all;
+            ($this->error ? $this->tag('p', $this->error, ['class' => 'error']): ''),
 
-if($this->user->hasPNB()) {
-  echo $this->tag('h2', $this->_('Prêts numériques en cours'));
-  echo $this->abonne_LoansPNB($this->user->getPNBLoans());
-}
+            ($loans->isEmpty() ? '' : $this->abonne_LoanExport()),
 
+            $this->abonne_Loans($loans),
+
+            $extend_all,
+
+            $pnb
+];
+
+echo implode($content);
 $this->closeBoite();
 echo $this->abonne_RetourFiche();
diff --git a/library/Class/Profil/Preferences/Loans.php b/library/Class/Profil/Preferences/Loans.php
new file mode 100644
index 00000000000..b41f296c99e
--- /dev/null
+++ b/library/Class/Profil/Preferences/Loans.php
@@ -0,0 +1,186 @@
+<?php
+/**
+ * Copyright (c) 2012-2017, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH 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 AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class Class_Profil_Preferences_Loans {
+  use Trait_Translator;
+
+
+  public function getTableComposition($value) {
+    return (new Class_Entity(['Id' => 'table_composition',
+                              'AvailableHeader' => $this->_('Colonnes disponibles'),
+                              'SelectedHeader' => $this->_('Colonnes activées'),
+                              'Available' => $this->_getAvailablesItems($value),
+                              'Selected' => $this->_getSelectedItems($value)]));
+  }
+
+
+  public function getTableCompositionOf($profile) {
+    $conf = $profile->getCfgModulesPreferences('abonne', 'prets', '');
+    $table_composition = isset($conf['table_composition'])
+      ? $conf['table_composition']
+      : '';
+
+    return $this->getTableComposition($table_composition);
+  }
+
+
+  public function isSearchEnabled($profile) {
+    $conf = $profile->getCfgModulesPreferences('abonne', 'prets', '');
+    return isset($conf['search_tool'])
+      ? (boolean) $conf['search_tool']
+      : false;
+  }
+
+
+  protected function _getSelectedItems($value) {
+    if (!$value)
+      return $this->_getDefaultItems();
+
+    $selected = [];
+    foreach($this->_getHeaderCompositionFromString($value) as $id)
+      $selected[] = $this->_getEntity($id);
+
+    return array_filter($selected);
+  }
+
+
+  protected function _getEntity($id) {
+    foreach($this->_getAllItems() as $instance)
+      if($id == $instance->getId())
+        return $instance;
+    return null;
+  }
+
+
+  protected function _getHeaderCompositionFromString($value) {
+    return explode(';', $value);
+  }
+
+
+  protected function _getAllItems() {
+    return array_merge($this->_getDefaultItems(),
+                       $this->_getOptionalItems());
+  }
+
+
+  protected function _getDefaultItems() {
+    return [(new Class_Entity())
+            ->setId('loaned_by')
+            ->setLabel($this->_('Emprunté par'))
+            ->whenCalledDo('renderWith', function($view_helper, $loan)
+                           {
+                             return $loan->getUserFullName();
+                           }),
+
+            (new Class_Entity())
+            ->setId('doctype')
+            ->setLabel($this->_('Support'))
+            ->whenCalledDo('renderWith', function($view_helper, $loan)
+                           {
+                             return $view_helper->renderDocTypeLabel($loan);
+                           }),
+
+            (new Class_Entity())
+            ->setId('thumbnail')
+            ->setLabel($this->_('Vignette'))
+            ->whenCalledDo('renderWith', function($view_helper, $loan)
+                           {
+                             return $view_helper->renderThumbnail($loan, ['retour_abonne' => 'prets']);
+                           }),
+
+            (new Class_Entity())
+            ->setId('title')
+            ->setLabel($this->_('Titre'))
+            ->whenCalledDo('renderWith', function($view_helper, $loan)
+                           {
+                             return $view_helper->renderTitle($loan, ['retour_abonne' => 'prets']);
+                           }),
+
+            (new Class_Entity())
+            ->setId('author')
+            ->setLabel($this->_('Auteur'))
+            ->whenCalledDo('renderWith', function($view_helper, $loan)
+                           {
+                             return $view_helper->renderAuthor($loan, ['retour_abonne' => 'prets']);
+                           }),
+
+            (new Class_Entity())
+            ->setId('library')
+            ->setLabel($this->_('Bibliothèque'))
+            ->whenCalledDo('renderWith', function($view_helper, $loan)
+                           {
+                             return $loan->getBibliotheque();
+                           }),
+
+            (new Class_Entity())
+            ->setId('return_date')
+            ->setLabel($this->_('Retour prévu'))
+            ->setParams(['class' => 'date_retour'])
+            ->whenCalledDo('renderWith', function($view_helper, $loan)
+                           {
+                             $view = $view_helper->view;
+                             return $loan->getDateRetour() . ' ' . $view->abonne_LoanAction($loan);
+                           }),
+
+            (new Class_Entity())
+            ->setId('Informations')
+            ->setLabel($this->_('Informations'))
+            ->whenCalledDo('renderWith', function($view_helper, $loan)
+                           {
+                             return $loan->getType();
+                           })
+            ->whenCalledDo('getRawParams', function ($loan)
+                           {
+                             return $loan
+                               ? (($loan->isLate()) ? ['class' => 'pret_en_retard'] : [])
+                               : [];
+                           })];
+  }
+
+
+  protected function _getAvailablesItems($value) {
+      if(!$value)
+        return $this->_getOptionalItems();
+
+      $settings = $this->_getHeaderCompositionFromString($value);
+
+      return (new Storm_Collection($this->_getAllItems()))
+        ->select(function($item) use ($settings)
+                 {
+                   return !in_array($item->getId(), $settings);
+                 })
+        ->getArrayCopy();
+  }
+
+
+  protected function _getOptionalItems() {
+    return [(new Class_Entity())
+            ->setId('Onhold')
+            ->setLabel($this->_('Déjà réservé par d\'autres'))
+            ->whenCalledDo('renderWith', function($view_helper, $loan)
+                           {
+                             return $loan->getBookedByOthers()
+                               ? $this->_('Oui')
+                               : $this->_('Non');
+                           })];
+  }
+}
diff --git a/library/Class/Profil/Preferences/LoansPNB.php b/library/Class/Profil/Preferences/LoansPNB.php
new file mode 100644
index 00000000000..d255c903b7d
--- /dev/null
+++ b/library/Class/Profil/Preferences/LoansPNB.php
@@ -0,0 +1,91 @@
+<?php
+/**
+ * Copyright (c) 2012-2017, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH 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 AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class Class_Profil_Preferences_LoansPNB extends Class_Profil_Preferences_Loans{
+
+  public function getTableCompositionOf($profile) {
+    $conf = $profile->getCfgModulesPreferences('abonne', 'prets', '');
+    $table_composition = isset($conf['pnb_table_composition'])
+      ? $conf['pnb_table_composition']
+      : '';
+
+    return $this->getTableComposition($table_composition);
+  }
+
+
+  protected function _getDefaultItems() {
+    return [(new Class_Entity())
+            ->setId('number')
+            ->setLabel($this->_('n°'))
+            ->whenCalledDo('renderWith', function($view_helper, $loan)
+                           {
+                             return $view_helper->line_no++;
+                           }),
+
+            (new Class_Entity())
+            ->setId('title')
+            ->setLabel($this->_('Titre'))
+            ->whenCalledDo('renderWith', function($view_helper, $loan)
+                           {
+                             $view = $view_helper->view;
+                             return ($record_id = $loan->getNoticeOPACId())
+                               ? $view->tagAnchor($view->url(
+                                                             ['controller' => 'recherche',
+                                                              'action' => 'viewnotice',
+                                                              'id' => $record_id,
+                                                              'retour_abonne' => 'prets'],
+                                                             null, true),
+                                                  $loan->getTitre())
+                               : $loan->getTitre();
+                           }),
+
+            (new Class_Entity())
+            ->setId('author')
+            ->setLabel($this->_('Auteur'))
+            ->whenCalledDo('renderWith', function($view_helper, $loan)
+                           {
+                             return $loan->getAuteur();
+                           }),
+
+            (new Class_Entity())
+            ->setId('loan_date')
+            ->setLabel($this->_('Date d\'emprunt'))
+            ->whenCalledDo('renderWith', function($view_helper, $loan)
+                           {
+                             return date('d/m/Y', strtotime($loan->getIssueDate()));
+                           }),
+
+            (new Class_Entity())
+            ->setId('return_date')
+            ->setLabel($this->_('Retour prévu'))
+            ->setCellParams(['class' => 'date_retour'])
+            ->whenCalledDo('renderWith', function($view_helper, $loan)
+                           {
+                             return date('d/m/Y', strtotime($loan->getDateRetour()));
+                           })];
+  }
+
+
+  protected function _getOptionalItems() {
+    return [];
+  }
+}
diff --git a/library/Class/TableDescription.php b/library/Class/TableDescription.php
index 50b69f6e39e..cae8d037890 100644
--- a/library/Class/TableDescription.php
+++ b/library/Class/TableDescription.php
@@ -66,7 +66,7 @@ class Class_TableDescription {
     $this
       ->_columns
       ->add($this->newColumn($label, $description)
-                 ->setOptions($description['options']));
+            ->setOptions($description['options']));
 
     return $this;
   }
@@ -175,6 +175,54 @@ abstract class Class_TableDescription_ColumnAbstract {
   }
 
 
+  public function getOptionsParams() {
+    $options = $this->_options;
+    if(is_callable($options))
+      return call_user_func($options);
+
+    if(!is_array($options))
+      return [];
+
+    if($this->_hasSubOptions($options))
+      return [];
+
+    return $options ? $options : [];
+  }
+
+
+  protected function _hasSubOptions($option) {
+    foreach($option as $value)
+      if(!is_scalar($value))
+        return true;
+    return false;
+  }
+
+
+  public function getRawParams($model) {
+    return $this->_getSubOption('raw_params', $model);
+  }
+
+
+  public function getCellParams($model) {
+    return $this->_getSubOption('cell_params', $model);
+  }
+
+
+  protected function _getSubOption($key, $model) {
+    $options = $this->getOptions();
+    if(!isset($options[$key]))
+      return [];
+
+    $sub_option = $options[$key];
+
+    $result = is_callable($sub_option)
+      ? $sub_option($model)
+      : $sub_option;
+
+    return $result ? $result : [];
+  }
+
+
   abstract public function renderModelOn($model, $canvas);
 }
 
@@ -194,9 +242,10 @@ class Class_TableDescription_ColumnForCallback extends Class_TableDescription_Co
 
 
   public function renderModelOn($model, $canvas) {
-    return $canvas->renderContent(call_user_func($this->_callback,
-                                                 $model,
-                                                 $this->_attribute));
+    return $canvas
+      ->renderContent(call_user_func($this->_callback,
+                                     $model,
+                                     $this->_attribute));
   }
 }
 
diff --git a/library/ZendAfi/Form/Configuration/Loans.php b/library/ZendAfi/Form/Configuration/Loans.php
index 636dedeea2f..8574ff2be32 100644
--- a/library/ZendAfi/Form/Configuration/Loans.php
+++ b/library/ZendAfi/Form/Configuration/Loans.php
@@ -30,6 +30,10 @@ class ZendAfi_Form_Configuration_Loans extends ZendAfi_Form {
                    ['label' => $this->_('Titre'),
                     'size' => $this->_text_size])
 
+      ->addElement('checkbox',
+                   'search_tool',
+                   ['label' => $this->_('Afficher l\'outil de recherche')])
+
       ->addElement('select',
                    'boite',
                    ['label' => $this->_('Style de boite'),
@@ -41,102 +45,11 @@ class ZendAfi_Form_Configuration_Loans extends ZendAfi_Form {
                       ['label' => $this->_('Composition du tableau'),
                        'value' => null,
                        'entityfactory' => function($value) {
-                          $pref = new Class_Entity(['Id' => 'table_composition',
-                                                    'AvailableHeader' => $this->_('Colonnes disponibles'),
-                                                    'SelectedHeader' => $this->_('Colonnes activées'),
-                                                    'Available' => $this->_getAvailablesItems($value),
-                                                    'Selected' => $this->_getSelectedItems($value)]);
-                          return $pref;
+                          return (new Class_Profil_Preferences_Loans)->getTableComposition($value);
                         }])
-      ->addToDisplaySettingsGroup(['table_composition'])
+      ->addToDisplaySettingsGroup(['search_tool',
+                                   'table_composition'])
       ->addToStyleGroup(['titre',
                          'boite']);
   }
-
-
-  protected function _getSelectedItems($value) {
-    if (!$value)
-      return $this->_getDefaultItems();
-
-    $selected = [];
-    foreach($this->_getHeaderCompositionFromString($value) as $id)
-      $selected[] = $this->_getEntity($id);
-
-    return array_filter($selected);
-  }
-
-
-  protected function _getEntity($id) {
-    foreach($this->_getAllItems() as $instance)
-      if($id == $instance->getId())
-        return $instance;
-    return null;
-  }
-
-
-  protected function _getHeaderCompositionFromString($value) {
-    return explode(';', $value);
-  }
-
-
-  protected function _getAllItems() {
-    return array_merge($this->_getDefaultItems(),
-                       $this->_getOptionalItems());
-  }
-
-
-  protected function _getDefaultItems() {
-    return [(new Class_Entity())
-            ->setId('loaned_by')
-            ->setLabel($this->_('Emprunté par')),
-
-            (new Class_Entity())
-            ->setId('doctype')
-            ->setLabel($this->_('Support')),
-
-            (new Class_Entity())
-            ->setId('thumbnail')
-            ->setLabel($this->_('Vignette')),
-
-            (new Class_Entity())
-            ->setId('title')
-            ->setLabel($this->_('Titre')),
-
-            (new Class_Entity())
-            ->setId('author')
-            ->setLabel($this->_('Auteur')),
-
-            (new Class_Entity())
-            ->setId('library')
-            ->setLabel($this->_('Bibliothèque')),
-
-            (new Class_Entity())
-            ->setId('return_date')
-            ->setLabel($this->_('Retour prévu'))];
-  }
-
-
-  protected function _getAvailablesItems($value) {
-      if(!$value)
-        return $this->_getOptionalItems();
-
-      $settings = $this->_getHeaderCompositionFromString($value);
-
-      return (new Storm_Collection($this->_getAllItems()))
-        ->select(function($item) use ($settings)
-                 {
-                   return !in_array($item->getId(), $settings);
-                 })
-        ->getArrayCopy();
-  }
-
-
-  protected function _getOptionalItems() {
-    return [(new Class_Entity())
-            ->setId('Informations')
-            ->setLabel($this->_('Informations')),
-            (new Class_Entity())
-            ->setId('Onhold')
-            ->setLabel($this->_('Déjà réservé par d\'autres'))];
-  }
 }
diff --git a/library/ZendAfi/Form/User/LoanSearch.php b/library/ZendAfi/Form/User/LoanSearch.php
index 05b6b14da99..af51b2fd706 100644
--- a/library/ZendAfi/Form/User/LoanSearch.php
+++ b/library/ZendAfi/Form/User/LoanSearch.php
@@ -41,10 +41,9 @@ class ZendAfi_Form_User_LoanSearch extends ZendAfi_Form {
       ->addElement('select',
                    'onhold',
                    ['label' => $this->_('Réservé par d\'autres'),
-                    'multiOptions' => [
-                                       'no' => $this->_('Non'),
+                    'multiOptions' => ['all' => $this->_('Indifférent'),
                                        'yes' => $this->_('Oui'),
-                                       'all' => $this->_('Indifférent')]])
+                                       'no' => $this->_('Non')]])
 
       ->addUniqDisplayGroup('loan_search_group')
       ->setAction(Class_Url::absolute('/opac/abonne/prets'));
diff --git a/library/ZendAfi/View/Helper/Abonne/Loans.php b/library/ZendAfi/View/Helper/Abonne/Loans.php
index ee6f280c466..b303618cf93 100644
--- a/library/ZendAfi/View/Helper/Abonne/Loans.php
+++ b/library/ZendAfi/View/Helper/Abonne/Loans.php
@@ -27,81 +27,48 @@ class ZendAfi_View_Helper_Abonne_Loans extends ZendAfi_View_Helper_Abonne_Operat
 
   public function abonne_Loans($loans) {
     $this->_operations = $loans;
-    Class_ScriptLoader::getInstance()->loadTableSorter();
-    return
-      $this->_renderStormLimit()
-      . $this->_tag('table',
-                    $this->renderHeader()
-                    . $this->renderLoans(),
-                    ['class' => 'tablesorter loans']);
-  }
-
-
-
-  protected function renderHeader() {
-    $headers = array_map(
-                         function($title) {
-                           return $this->_tag('th', $title);
-                         },
-                         $this->_tableColumns());
-
-    return $this->_tag('thead',
-                       $this->_tag('tr',
-                                   implode('', $headers)));
-  }
 
+    if($loans->isEmpty() || (!$description = $this->_getDescription()))
+      return $this->_tag('p', $this->_('Vous n\'avez pas de prêts en cours'), ['class' => 'error']);
 
-  protected function _tableColumns() {
-    return [$this->_('Emprunté par'),
-            $this->_('Support'),
-            $this->_('Vignette'),
-            $this->_('Titre'),
-            $this->_('Auteur'),
-            $this->_('Bibliothèque'),
-            $this->_('Retour prévu'),
-            $this->_('Informations'),
-            $this->_('Document réservé par d\'autres')];
+    return
+      $this->_renderStormLimit()
+      . $this->view->renderTable($description, $loans, ['class' => 'loans',
+                                                        'sorter' => true]);
   }
 
 
-  protected function renderLoans() {
-    $html = '';
-
-    foreach($this->_operations as $loan)
-      $html .= $this->renderLoan($loan);
-
-    return $this->_tag('tbody', $html);
+  protected function _getDescriptionClass() {
+    return new Class_Profil_Preferences_Loans();
   }
 
 
-  protected function renderLoan($loan) {
-
-    $tag_bookedbyothers = $this->_tag('td',
-                                      '',
-                                      $loan->getBookedByOthers()?
-                                      ['class' => 'checkedbox'] :
-                                      ['class' => 'uncheckedbox']);
-
-    return
-      $this->_tag('tr',
-                  $this->_tag('td',
-                              $loan->getUserFullName())
-                  . $this->_tag('td',
-                                $this->_renderDocTypeLabel($loan))
-                  . $this->_tag('td',
-                                $this->_renderThumbnail($loan, ['retour_abonne' => 'prets']))
-                  . $this->_tag('td',
-                                $this->_renderTitle($loan, ['retour_abonne' => 'prets']))
-                  . $this->_tag('td',
-                                $this->_renderAuthor($loan, ['retour_abonne' => 'prets']))
-                  . $this->_tag('td',
-                                $loan->getBibliotheque() )
-                  . $this->_tag('td',
-                                $loan->getDateRetour() . ' ' . $this->view->abonne_LoanAction($loan), ['class' => 'date_retour'])
-                  . $this->_tag('td',
-                                $loan->getType())
-                  .$tag_bookedbyothers,
-                  ($loan->isLate()) ? ['class' => 'pret_en_retard'] : []);
-
+  protected function _getDescription() {
+    $selected_columns = $this->_getDescriptionClass()
+      ->getTableCompositionOf(Class_Profil::getCurrentProfil())
+      ->getSelected();
+
+    if(!$selected_columns)
+      return;
+
+    $description = new Class_TableDescription('borrower_loans');
+
+    foreach($selected_columns as $column)
+      $description->addColumn($column->getLabel(),
+                              ['attribute' => '',
+                               'callback' => function($loan) use ($column)
+                                {
+                                  return $column->renderWith($this, $loan);
+                                },
+                               'options' => ['raw_params' => function($loan) use ($column)
+                                 {
+                                   return $column->getRawParams($loan);
+                                 },
+                                             'cell_params' => function($loan) use ($column)
+                                 {
+                                   return $column->getCellParams($loan);
+                                 }]]);
+
+    return $description;
   }
 }
\ No newline at end of file
diff --git a/library/ZendAfi/View/Helper/Abonne/LoansPNB.php b/library/ZendAfi/View/Helper/Abonne/LoansPNB.php
index 17d43955b2e..43ba3d3a4c7 100644
--- a/library/ZendAfi/View/Helper/Abonne/LoansPNB.php
+++ b/library/ZendAfi/View/Helper/Abonne/LoansPNB.php
@@ -21,43 +21,14 @@
 
 
 class ZendAfi_View_Helper_Abonne_LoansPNB extends ZendAfi_View_Helper_Abonne_Loans {
+  public $line_no = 0;
+
   public function abonne_LoansPNB($loans) {
     return parent::abonne_Loans($loans);
   }
 
 
-  protected function _tableColumns() {
-    return [$this->_('n°'),
-            $this->_('Titre'),
-            $this->_('Auteur'),
-            $this->_('Date d\'emprunt'),
-            $this->_('Retour prévu')];
-  }
-
-
-  protected function renderLoan($loan) {
-    $record_title = $loan->getTitre();
-
-    if ($record_id = $loan->getNoticeOPACId())
-      $record_title = $this->view->tagAnchor($this->view->url(
-                                                              ['controller' => 'recherche',
-                                                               'action' => 'viewnotice',
-                                                               'id' => $record_id,
-                                                               'retour_abonne' => 'prets'],
-                                                              null, true),
-                                             $loan->getTitre());
-    return
-      $this->_tag('tr',
-                  $this->_tag('td',
-                              $this->_line_no++)
-                  . $this->_tag('td',
-                                $record_title)
-                  . $this->_tag('td',
-                                $loan->getAuteur())
-                  . $this->_tag('td',
-                                date('d/m/Y', strtotime($loan->getIssueDate())))
-                  . $this->_tag('td',
-                                date('d/m/Y', strtotime($loan->getDateRetour())),
-                                ['class' => 'date_retour']));
+  protected function _getDescriptionClass() {
+    return new Class_Profil_Preferences_LoansPNB();
   }
 }
\ No newline at end of file
diff --git a/library/ZendAfi/View/Helper/Abonne/Operation.php b/library/ZendAfi/View/Helper/Abonne/Operation.php
index 2f0d7b08507..a550aa2d150 100644
--- a/library/ZendAfi/View/Helper/Abonne/Operation.php
+++ b/library/ZendAfi/View/Helper/Abonne/Operation.php
@@ -37,6 +37,11 @@ class ZendAfi_View_Helper_Abonne_Operation extends ZendAfi_View_Helper_BaseHelpe
   }
 
 
+  public function renderAuthor($operation, $attribs = []) {
+    return $this->_renderAuthor($operation, $attribs);
+  }
+
+
   protected function _renderTitle($operation, $attribs = []) {
     if($this->_isStormLimited())
       return $operation->getTitre();
@@ -49,6 +54,11 @@ class ZendAfi_View_Helper_Abonne_Operation extends ZendAfi_View_Helper_BaseHelpe
   }
 
 
+  public function renderTitle($operation, $attribs = []) {
+    return $this->_renderTitle($operation, $attribs);
+  }
+
+
   protected function _renderThumbnail($operation, $attribs = []) {
     if($this->_isStormLimited())
       return '';
@@ -62,11 +72,21 @@ class ZendAfi_View_Helper_Abonne_Operation extends ZendAfi_View_Helper_BaseHelpe
   }
 
 
+  public function renderThumbnail($operation, $attribs = []) {
+    return $this->_renderThumbnail($operation, $attribs);
+  }
+
+
   protected function _renderDocTypeLabel($operation) {
     return (string)$this->_getRecord($operation)->getTypeDocLabel();
   }
 
 
+  public function renderDocTypeLabel($operation) {
+    return $this->_renderDocTypeLabel($operation);
+  }
+
+
   protected function _getRecord($operation) {
     $record = new Class_Entity();
 
diff --git a/library/ZendAfi/View/Helper/RenderTable.php b/library/ZendAfi/View/Helper/RenderTable.php
index 8322e618596..26bd0d51e7f 100644
--- a/library/ZendAfi/View/Helper/RenderTable.php
+++ b/library/ZendAfi/View/Helper/RenderTable.php
@@ -42,19 +42,21 @@ class ZendAfi_View_Helper_RenderTable extends ZendAfi_View_Helper_BaseHelper {
 
     $options = array_merge(['pager' => false], $options);
 
-    $classes = 'models';
+    $classes = ['models'];
 
     if(isset($options['sorter']) && $options['sorter']) {
-      $classes .= ' tablesorter';
+      $classes [] = 'tablesorter';
       Class_ScriptLoader::getInstance()->loadTableSorter($options['pager']);
     }
 
+    $classes [] = isset($options['class']) ? $options['class'] : '';
+
     return
       $this->_tag('table',
                   $this->_head($description)
                   .$this->_body($description, $grouped_models),
                   ['id' => $description->getId(),
-                   'class' => $classes])
+                   'class' => implode(' ', $classes)])
       . $this->_renderPager($options);
   }
 
@@ -127,7 +129,7 @@ class ZendAfi_View_Helper_RenderTable_Header extends ZendAfi_View_Helper_BaseHel
   public function _renderColumn($column) {
     return $this->_tag('th',
                        $column->getLabel(),
-                       $column->getOptions() ? $column->getOptions() : null);
+                       $column->getOptionsParams());
   }
 }
 
@@ -170,17 +172,58 @@ class ZendAfi_View_Helper_RenderTable_Body extends ZendAfi_View_Helper_BaseHelpe
     return $this->_tag('tr',
                        implode('',
                                $this->_renderModelColumns($model)
-                               ->getArrayCopy()));
+                               ->getArrayCopy()),
+                       $this->_getRawParams($model));
   }
 
+
   protected function _renderModelColumns($model) {
     return $this
       ->_description
       ->columnsCollect(function($column) use ($model)
                        {
-                         return $this->view->renderTable_Cell($column, $model);
+                         return $this->view->renderTable_Cell($column, $model, $this);
                        });
   }
+
+
+  public function getCellParams($model) {
+    return $this->_buildParams($this
+                               ->_description
+                               ->columnsCollect(function($column) use ($model)
+                                                {
+                                                  return $column->getCellParams($model);
+                                                }));
+  }
+
+
+  protected function _buildParams($collection) {
+    $params = [];
+    foreach($collection->getArrayCopy() as $raw_params)
+      $params = array_merge($params, $this->_buildParam($raw_params, $params));
+
+    return $params;
+  }
+
+
+  protected function _buildParam($params_to_read, $params) {
+    foreach($params_to_read as $key => $value) {
+        if(isset($params[$key]))
+          $params[$key] .= $value;
+        $params[$key] = $value;
+      }
+    return $params;
+  }
+
+
+  protected function _getRawParams($model) {
+    return $this->_buildParams($this
+                               ->_description
+                               ->columnsCollect(function($column) use ($model)
+                                                {
+                                                  return $column->getRawParams($model);
+                                                }));
+  }
 }
 
 
@@ -188,11 +231,13 @@ class ZendAfi_View_Helper_RenderTable_Body extends ZendAfi_View_Helper_BaseHelpe
 class ZendAfi_View_Helper_RenderTable_Cell extends ZendAfi_View_Helper_BaseHelper {
   protected $_html;
 
-  public function renderTable_Cell($column, $model) {
+
+  public function renderTable_Cell($column, $model, $body) {
     $this->_html = '';
     $column->renderModelOn($model, $this);
     return $this->_tag('td',
-                       $this->_html);
+                       $this->_html,
+                       $body->getCellParams($model));
   }
 
 
diff --git a/tests/application/modules/opac/controllers/AbonneControllerPretsTest.php b/tests/application/modules/opac/controllers/AbonneControllerPretsTest.php
index 43678d38e1f..89a0e2f50d4 100644
--- a/tests/application/modules/opac/controllers/AbonneControllerPretsTest.php
+++ b/tests/application/modules/opac/controllers/AbonneControllerPretsTest.php
@@ -327,7 +327,7 @@ class AbonneControllerPretsListThreePretsTest extends AbonneControllerPretsListT
 
   /** @test */
   public function tableShouldHaveClassTableSorter() {
-    $this->assertXPath('//table[@class="tablesorter loans"]', $this->_response->getBody());
+    $this->assertXPath('//table[@class="models tablesorter loans"]', $this->_response->getBody());
   }
 
 
@@ -404,7 +404,7 @@ class AbonneControllerPretsListThreePretsTest extends AbonneControllerPretsListT
 
   /** @test */
   public function potterDueDateShouldBeTwentyNineOctober2022() {
-    $this->assertXPathContentContains("//tbody/tr[2][not(@class)]//td", '29/10/2022');
+    $this->assertNotXPathContentContains("//tbody/tr[2][contains(@class, 'pret_en_retard')]//td", '29/10/2022');
   }
 
 
diff --git a/tests/scenarios/HandleBranchcode/HandleBranchcodeTest.php b/tests/scenarios/HandleBranchcode/HandleBranchcodeTest.php
index ae5295445b3..c0523821894 100644
--- a/tests/scenarios/HandleBranchcode/HandleBranchcodeTest.php
+++ b/tests/scenarios/HandleBranchcode/HandleBranchcodeTest.php
@@ -153,7 +153,13 @@ class HandleBranchcodeDisplayLoanByOthersTest extends HandleBranchcodeTestCase {
     parent::setUp();
     $this->_referer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : null;
     $_SERVER['HTTP_REFERER'] = 'https://bokeh.org/opac/abonne/prets';
-    Class_AdminVar::set('ENABLE_USER_LOAN_SEARCH',true);
+    (new Class_Profil_Preferences())->setModulePref(Class_Profil::getCurrentProfil(),
+                                                    (new Class_Entity())
+                                                    ->setController('abonne')
+                                                    ->setAction('prets'),
+                                                    ['search_tool' => 1,
+                                                     'table_composition' => 'loaned_by;doctype;thumbnail;title;author;library;return_date;Onhold']);
+
   }
 
 
@@ -179,17 +185,15 @@ class HandleBranchcodeDisplayLoanByOthersTest extends HandleBranchcodeTestCase {
   public function onHoldShouldDisplayBookedByOther() {
     $this->borrower = $this->service->getEmprunteur($this->user);
     $this->dispatch('/opac/abonne/prets', true);
-    $this->assertXPathContentContains('//div', 'réservé par d\'autres', $this->_response->getBody());
+    $this->assertXPathContentContains('//th', 'Déjà réservé par d\'autres', $this->_response->getBody());
   }
 
 
   /** @test */
   public function informationsShouldNotBeDisplayed() {
-    $profil = Class_Profil::getCurrentProfil();
-
     $this->borrower = $this->service->getEmprunteur($this->user);
     $this->dispatch('/opac/abonne/prets', true);
-    $this->assertNotXPathContentContains('//div', 'Informations', $this->_response->getBody());
+    $this->assertNotXPathContentContains('//th', 'Informations', $this->_response->getBody());
   }
 
 
@@ -203,7 +207,7 @@ class HandleBranchcodeDisplayLoanByOthersTest extends HandleBranchcodeTestCase {
   /** @test */
   public function searchOnHoldShouldDisplayBookedByOther() {
     $this->dispatch('/opac/abonne/prets/onhold/yes', true);
-    $this->assertXPathContentContains('//div', 'réservé par d\'autres', $this->_response->getBody());
+    $this->assertXPathContentContains('//div', 'Déjà réservé par d\'autres', $this->_response->getBody());
   }
 
 
@@ -211,14 +215,14 @@ class HandleBranchcodeDisplayLoanByOthersTest extends HandleBranchcodeTestCase {
   public function postReturnDateShouldDisplayBookedByOther() {
     $this->dispatch('/opac/abonne/prets/start_issue_date/12%2F06%2F2010/end_loan_date//start_date_retour//end_date_retour//onhold//',true);
     $this->assertXPathContentContains('//td', 'Quel bazar, Léonard', $this->_response->getBody());
-      }
+  }
 
 
-    /** @test */
+  /** @test */
   public function dispatchOnHoldOnShouldDisplayLeonard() {
     $this->dispatch('/opac/abonne/prets/onhold/yes',true);
     $this->assertXPathContentContains('//td', 'Quel bazar, Léonard', $this->_response->getBody());
-      }
+  }
 
 
   /** @test */
@@ -253,7 +257,5 @@ class HandleBranchcodeDisplayLoanByOthersTest extends HandleBranchcodeTestCase {
     $this->assertXPath('//input[@name="end_issue_date"][@value="12/06/2019"]', $this->_response->getBody());
     $this->assertXPath('//input[@name="start_date_retour"][@value="20/06/2015"]', $this->_response->getBody());
     $this->assertXPath('//input[@name="end_date_retour"][@value="20/06/2019"]', $this->_response->getBody());
-
   }
-
 }
-- 
GitLab