From 712e2a3e98360e8d89c4003ad6363be4cfc7a775 Mon Sep 17 00:00:00 2001
From: gloas <gloas@afi-sa.fr>
Date: Wed, 15 Apr 2020 12:34:19 +0200
Subject: [PATCH] wip on bootstrap

---
 .../opac/controllers/AbonneController.php     |  64 +++---
 .../opac/controllers/IndexController.php      |   5 +
 .../abonne/changer-mon-mot-de-passe.phtml     |   2 +
 .../opac/views/scripts/abonne/modifier.phtml  |   1 -
 library/Class/User/EditFormHelper.php         | 202 ++++++++++++++++++
 library/ZendAfi/View/Helper/Accueil/News.php  |  31 +--
 .../View/Helper/Admin/TagAddNewArticle.php    |  63 ++++++
 .../ZendAfi/View/Helper/TagEditArticle.php    |   2 +-
 .../Intonation/Assets/css/intonation.css      |  14 ++
 .../Library/AjaxPaginatedListHelper.php       |   2 +-
 .../Library/FormCustomizer/Abstract.php       |   6 +-
 .../templates/Intonation/Library/Settings.php |   4 +-
 .../Library/View/Wrapper/Article.php          |  10 +-
 .../Intonation/Library/View/Wrapper/Loan.php  |   6 +-
 .../Library/View/Wrapper/PNBLoan.php          |  17 +-
 .../Library/View/Wrapper/Record.php           | 116 ++++++++--
 .../User/RichContent/EditInformations.php     |   4 +-
 .../Wrapper/User/RichContent/EditPassword.php |  54 +++++
 .../Wrapper/User/RichContent/RateRecords.php  |   6 +-
 .../Library/Widget/Carousel/Article/View.php  |  12 +-
 .../Intonation/Library/Widget/Free/View.php   |  17 +-
 .../templates/Intonation/View/Abonne/Edit.php |   4 -
 .../Intonation/View/Abonne/EditPassword.php   |  57 +++++
 .../templates/Intonation/View/Heartbeat.php   |  34 +++
 library/templates/Intonation/View/Opac.php    |   2 +-
 .../Intonation/View/RenderTruncateList.php    |   3 +-
 .../Intonation/View/Search/HtmlCriteria.php   |  14 +-
 .../Intonation/View/Search/TextCriteria.php   |   9 +-
 .../Intonation/View/User/Informations.php     |  30 ++-
 .../Templates/MuscleTemplateTest.php          |  20 ++
 .../Templates/MyBibAppTemplateTest.php        |  88 ++++++++
 tests/scenarios/Templates/TemplatesTest.php   | 124 +++++++++--
 32 files changed, 875 insertions(+), 148 deletions(-)
 create mode 100644 application/modules/opac/views/scripts/abonne/changer-mon-mot-de-passe.phtml
 create mode 100644 library/Class/User/EditFormHelper.php
 create mode 100644 library/ZendAfi/View/Helper/Admin/TagAddNewArticle.php
 create mode 100644 library/templates/Intonation/Library/View/Wrapper/User/RichContent/EditPassword.php
 create mode 100644 library/templates/Intonation/View/Abonne/EditPassword.php
 create mode 100644 library/templates/Intonation/View/Heartbeat.php

diff --git a/application/modules/opac/controllers/AbonneController.php b/application/modules/opac/controllers/AbonneController.php
index f43062e9f3b..546c3f4f5d0 100644
--- a/application/modules/opac/controllers/AbonneController.php
+++ b/application/modules/opac/controllers/AbonneController.php
@@ -1819,46 +1819,6 @@ class AbonneController extends ZendAfi_Controller_Action {
   }
 
 
-  public function modifierAction() {
-    $form = $this->_userForm($this->_user);
-    $form->setAction($this->view->url());
-    $this->view->form = $form;
-
-    if (!$this->_request->isPost())
-      return ;
-
-    if (!$form->isValid($this->_request->getPost()))
-      return;
-
-    $fields_to_save = Class_AdminVar::getChampsFicheUtilisateur();
-    $attributes = [];
-
-    foreach($fields_to_save as $field)
-      $attributes[$field] = $this->_request->getParam($field);
-
-    $this->_user
-      ->updateAttributes($attributes);
-
-    $patron = $this->_user->getEmprunteur();
-    $patron->updateFromUser($this->_user);
-
-    try {
-      if ($this->_user->save()) {
-        $patron->ensureService($this->_user)->save();
-        $this->_helper->notify($this->_('Vos modifications ont bien été enregistrées'));
-        $this->_redirectClose('/abonne/informations');
-      }
-
-      $form->addDecorator('Errors');
-      foreach($this->_user->getErrors() as $error)
-        $form->addError($error);
-    } catch(Exception $e) {
-      $form->addError($e->getMessage());
-      $form->addDecorator('Errors');
-    }
-  }
-
-
   public function exporterLaSelectionAction() {
     $this->view->titre = $this->_('Exporter la sélection');
 
@@ -1953,4 +1913,28 @@ class AbonneController extends ZendAfi_Controller_Action {
 
     return $this->_helper->ajax($callback);
   }
+
+
+  public function modifierAction() {
+    if ((new Class_User_EditFormHelper($this->_user,
+                                       $this->view,
+                                       $this->_request))
+        ->beEditUser()
+        ->proceed()) {
+      $this->_helper->notify($this->_('Vos informations ont bien été modifiées.'), ['status' => 'success']);
+      $this->_redirectClose($this->view->absoluteUrl(['action' => 'informations']));
+    }
+  }
+
+
+  public function changerMonMotDePasseAction() {
+    if ((new Class_User_EditFormHelper($this->_user,
+                                       $this->view,
+                                       $this->_request))
+        ->beChangePassword()
+        ->proceed()) {
+      $this->_helper->notify($this->_('Votre mot de passe à bien été changé.'), ['status' => 'success']);
+      $this->_redirectClose($this->view->absoluteUrl(['action' => 'informations']));
+    }
+  }
 }
\ No newline at end of file
diff --git a/application/modules/opac/controllers/IndexController.php b/application/modules/opac/controllers/IndexController.php
index 6c2f723af29..3c79da43b1e 100644
--- a/application/modules/opac/controllers/IndexController.php
+++ b/application/modules/opac/controllers/IndexController.php
@@ -136,4 +136,9 @@ class IndexController extends ZendAfi_Controller_Action {
 
     return $this->_helper->ajax($callback);
   }
+
+
+  public function heartbeatAction() {
+    $this->getHelper('ViewRenderer')->setNoRender();
+  }
 }
\ No newline at end of file
diff --git a/application/modules/opac/views/scripts/abonne/changer-mon-mot-de-passe.phtml b/application/modules/opac/views/scripts/abonne/changer-mon-mot-de-passe.phtml
new file mode 100644
index 00000000000..f33f2fbba6a
--- /dev/null
+++ b/application/modules/opac/views/scripts/abonne/changer-mon-mot-de-passe.phtml
@@ -0,0 +1,2 @@
+<?php
+echo $this->abonne_EditPassword($this->user, $this->form);
diff --git a/application/modules/opac/views/scripts/abonne/modifier.phtml b/application/modules/opac/views/scripts/abonne/modifier.phtml
index 4bfc244568d..d906b46bf17 100644
--- a/application/modules/opac/views/scripts/abonne/modifier.phtml
+++ b/application/modules/opac/views/scripts/abonne/modifier.phtml
@@ -1,3 +1,2 @@
 <?php
 echo $this->abonne_Edit($this->user, $this->form);
-?>
diff --git a/library/Class/User/EditFormHelper.php b/library/Class/User/EditFormHelper.php
new file mode 100644
index 00000000000..317c9e281e2
--- /dev/null
+++ b/library/Class/User/EditFormHelper.php
@@ -0,0 +1,202 @@
+<?php
+/**
+ * Copyright (c) 2012-2020, 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_User_EditFormHelper {
+
+  use Trait_Translator;
+
+
+  protected
+    $_user,
+    $_view,
+    $_web_request,
+    $_form,
+    $_old_password,
+    $_callback;
+
+
+  public function __construct($user, $view, $request) {
+    $this->_user = $user;
+    $this->_view = $view;
+    $this->_web_request = $request;
+  }
+
+
+  public function beChangePassword() {
+    $this->_form = new ZendAfi_Form;
+
+    $this->_form
+
+      ->addElement('password',
+                   'current_password',
+                   ['label' => $this->_('Mot de passe actuel'),
+                    'required' => true,
+                    'allowEmpty' => false,
+                    'validators' => [(new Zend_Validate_Identical($this->_user->getPassword()))
+                                     ->setMessages(['missingToken' => $this->_('Vous devez confirmer votre mot de passe actuel'),
+                                                    'notSame' => $this->_('Le mot de passe ne correspondent pas à votre mot de passe actuel')])]])
+
+      ->addElement('password',
+                   'new_password',
+                   ['label' => $this->_('Nouveau mot de passe'),
+                    'required' => true,
+                    'allowEmpty' => false,
+                    'validators' => [(new Zend_Validate_StringLength(4, 24))]])
+
+      ->addElement('password',
+                   'confirm_new_password',
+                   ['label' => $this->_('Confirmer le nouveau mot de passe'),
+                    'required' => true,
+                    'allowEmpty' => false,
+                    'validators' => [(new Zend_Validate_Identical($this->_web_request->getParam('new_password')))
+                                     ->setMessages(['missingToken' => $this->_('Veuillez confirmer le nouveau mot de passe '),
+                                                    'notSame' => $this->_('Les mots de passe ne sont pas identifique')])]]);
+
+    $this->_form->addUniqDisplayGroup('change-password');
+
+    $this->_callback = function() {
+      $this->_old_password = $this->_user->getPassword();
+      $this->_user->setPassword($this->_web_request->getParam('new_password'));
+    };
+
+    return $this;
+  }
+
+
+  public function beEditUser() {
+    $this->_form = new ZendAfi_Form;
+
+    $textfields = ['nom' => $this->_('Nom'),
+                   'prenom' => $this->_('Prénom'),
+                   'pseudo' => $this->_('Pseudo'),
+                   'adresse' => $this->_('Adresse'),
+                   'code_postal' => $this->_('Code postal'),
+                   'ville' => $this->_('Ville'),
+                   'mail' => $this->_('E-Mail'),
+                   'telephone' => $this->_('Téléphone'),
+                   'mobile' => $this->_('Téléphone mobile')];
+
+    foreach($textfields as $field => $label) {
+
+      $element = $this->_form
+        ->createElement('text', $field)
+        ->setLabel($label)
+        ->setAttrib('size', 30);
+
+      $this->_form->addElement($element);
+    }
+
+    if ($mail_element = $this->_form->getElement('mail'))
+      $mail_element->addValidator(new ZendAfi_Validate_EmailAddress());
+
+    if ($phone_element = $this->_form->getElement('telephone'))
+      $phone_element->addValidator(new ZendAfi_Validate_PhoneNumber());
+
+    if ($mobile_element = $this->_form->getElement('mobile'))
+      $mobile_element->addValidator(new ZendAfi_Validate_PhoneNumber());
+
+    $this->_form->addDisplayGroup(['nom', 'prenom', 'pseudo'],
+                                  'identification',
+                                  ['legend' => 'Identification']);
+
+    $this->_form->addDisplayGroup(['adresse', 'code_postal', 'ville'],
+                                  'coordonnees',
+                                  ['legend' => 'Coordonnées']);
+
+    $this->_form->addDisplayGroup(['mail', 'telephone', 'mobile'],
+                                  'contact',
+                                  ['legend' => 'Contact']);
+
+    $fields_to_show = Class_AdminVar::getChampsFicheUtilisateur();
+    if (in_array('mode_contact', $fields_to_show)) {
+      $this->_form->addElement('radio',
+                               'mode_contact',
+                               ['label' => '',
+                                'multiOptions' => [Class_Users::MODE_CONTACT_LETTER => $this->_(' par courrier postal'),
+                                                   Class_Users::MODE_CONTACT_MAIL => $this->_(' par E-Mail'),
+                                                   Class_Users::MODE_CONTACT_SMS => $this->_(' par SMS')],
+                                'value' => $user->getModeContact()]);
+      $this->_form->addDisplayGroup(['mode_contact'], 'mode_de_contact', ['legend' => 'Recevoir mes notificactions de réservation et de rappel']);
+    }
+
+    $this->_form
+      ->populate($this->_user->toArray());
+
+    $this->_callback = function () {
+      $password = $this->_user->getPassword();
+
+      $this->_user
+      ->updateAttributes($this->_web_request->getPost())
+      ->setPassword($password);
+    };
+
+    return $this;
+  }
+
+
+
+  public function proceed() {
+    $this->_form
+      ->setAction($this->_view->url())
+      ->setMethod('POST')
+      ->setAttrib('autocomplete', 'off');
+
+    $this->_view->form = $this->_form;
+
+    if ( ! $this->_web_request->isPost())
+      return false;
+
+    if ( ! $this->_form->isValid($this->_web_request->getPost()))
+      return false;
+
+    $callback = $this->_callback;
+    $callback();
+
+    if (!$this->_user->isValid()) {
+      $this->_form->addDecorator('Errors');
+      foreach($this->_user->getErrors() as $error)
+        $this->_form->addError($error);
+      return false;
+    }
+
+    return $this->_updateUser();
+  }
+
+
+  protected function _updateUser() {
+    $patron = $this->_user->getEmprunteur();
+    $patron->updateFromUser($this->_user);
+    $patron->setPreviousPassword($this->_old_password);
+
+    try {
+      $patron
+        ->ensureService($this->_user)
+        ->save();
+    } catch(Exception $e) {
+      $this->_form->addError($e->getMessage());
+      $this->_form->addDecorator('Errors');
+      return false;
+    }
+
+    return $this->_user->save();
+  }
+}
\ No newline at end of file
diff --git a/library/ZendAfi/View/Helper/Accueil/News.php b/library/ZendAfi/View/Helper/Accueil/News.php
index 0c01fb25c78..4d920e42c28 100644
--- a/library/ZendAfi/View/Helper/Accueil/News.php
+++ b/library/ZendAfi/View/Helper/Accueil/News.php
@@ -95,36 +95,7 @@ class ZendAfi_View_Helper_Accueil_News extends ZendAfi_View_Helper_Accueil_Base
 
 
   protected function addNewsAddButton() {
-    if (!$this->_articles || !Class_Users::isCurrentUserCanAccesBackend())
-      return '';
-
-    $category = $this->getFirstArticleCategorie($this->_articles);
-    if (!$category
-        || !Class_Users::getIdentity()
-        ->hasAnyPermissionOn($category, [Class_Permission::createArticle(),
-                                         Class_Permission::createArticleCategory()]))
-      return '';
-
-    return $this->view
-      ->tagAnchor($this->view->url(['module' => 'admin',
-                                    'controller' => 'cms',
-                                    'action' => 'add',
-                                    'id_module' => $this->id_module,
-                                    'id_cat' => $category->getId()]),
-                  Class_Admin_Skin::current()
-                  ->renderActionIconOn('add', $this->view,
-                                       ['title' => $this->_('Ajouter un nouvel article')]),
-                  ['data-popup' => 'true']);
-  }
-
-
-  protected function getFirstArticleCategorie($articles) {
-    if (!$articles || !is_array($articles)) return '';
-
-    foreach($articles as $article) {
-      if($category = $article->getCategorie())
-        return $category;
-    }
+    return $this->view->Admin_TagAddNewArticle($this->id_module, $this->_articles);
   }
 
 
diff --git a/library/ZendAfi/View/Helper/Admin/TagAddNewArticle.php b/library/ZendAfi/View/Helper/Admin/TagAddNewArticle.php
new file mode 100644
index 00000000000..45610fdfcc4
--- /dev/null
+++ b/library/ZendAfi/View/Helper/Admin/TagAddNewArticle.php
@@ -0,0 +1,63 @@
+<?php
+/**
+ * Copyright (c) 2012-2020, 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 ZendAfi_View_Helper_Admin_TagAddNewArticle extends ZendAfi_View_Helper_BaseHelper {
+  public function Admin_TagAddNewArticle($id_module, $articles) {
+    if (!$articles)
+      return '';
+
+    $articles = array_filter($articles);
+
+    if (empty($articles))
+      return '';
+
+    if (!Class_Users::isCurrentUserCanAccesBackend())
+      return '';
+
+    if (!$category = $this->getFirstArticleCategorie($articles))
+      return '';
+
+    if (!Class_Users::getIdentity()->hasAnyPermissionOn($category, [Class_Permission::createArticle(),
+                                                                    Class_Permission::createArticleCategory()]))
+      return '';
+
+    return
+      $this->view
+      ->tagAnchor($this->view->url(['module' => 'admin',
+                                    'controller' => 'cms',
+                                    'action' => 'add',
+                                    'id_module' => $id_module,
+                                    'id_cat' => $category->getId()]),
+                  Class_Admin_Skin::current()
+                  ->renderActionIconOn('add', $this->view,
+                                       ['title' => $this->_('Ajouter un nouvel article')]),
+                  ['data-popup' => 'true']);
+  }
+
+
+  protected function getFirstArticleCategorie($articles) {
+    foreach($articles as $article) {
+      if($category = $article->getCategorie())
+        return $category;
+    }
+  }
+}
diff --git a/library/ZendAfi/View/Helper/TagEditArticle.php b/library/ZendAfi/View/Helper/TagEditArticle.php
index e9d4d4a8d42..6a5405556f6 100644
--- a/library/ZendAfi/View/Helper/TagEditArticle.php
+++ b/library/ZendAfi/View/Helper/TagEditArticle.php
@@ -58,7 +58,7 @@ class ZendAfi_View_Helper_TagEditArticle extends ZendAfi_View_Helper_BaseHelper
 
   protected function _renderEdit() {
     return $this
-      ->_renderActionLink('edit', $this->_('Modifier l\'article'),
+      ->_renderActionLink('edit', $this->_('Modifier l\'article %s', $this->_article->getTitre()),
                           ['module' => 'admin',
                            'controller' => 'cms',
                            'action' => 'edit',
diff --git a/library/templates/Intonation/Assets/css/intonation.css b/library/templates/Intonation/Assets/css/intonation.css
index d538990a27a..c2db570cdb0 100644
--- a/library/templates/Intonation/Assets/css/intonation.css
+++ b/library/templates/Intonation/Assets/css/intonation.css
@@ -781,4 +781,18 @@ input[id^="select_record"] + * {
 
 .dropdown-menu select {
     min-width: 1px;
+}
+
+.only_visible_in_viewnotice {
+    display: none;
+}
+
+.recherche_viewnotice .only_visible_in_viewnotice {
+    display: block;
+}
+
+.badge.only_visible_in_viewnotice .text-truncate {
+    width: auto;
+    max-width: none;
+    white-space: normal;
 }
\ No newline at end of file
diff --git a/library/templates/Intonation/Library/AjaxPaginatedListHelper.php b/library/templates/Intonation/Library/AjaxPaginatedListHelper.php
index 408451fb894..c1e5214d987 100644
--- a/library/templates/Intonation/Library/AjaxPaginatedListHelper.php
+++ b/library/templates/Intonation/Library/AjaxPaginatedListHelper.php
@@ -148,7 +148,7 @@ class Intonation_Library_AjaxPaginatedListHelper {
                                  $element
                                    ->setView($this->_view)
                                    ->inJsSearch();
-                                 return false !== strpos($element->getContentForJSSearch(), $term);
+                                 return false !== strpos(strtolower($element->getContentForJSSearch()), $term);
                                });
   }
 
diff --git a/library/templates/Intonation/Library/FormCustomizer/Abstract.php b/library/templates/Intonation/Library/FormCustomizer/Abstract.php
index 9d65774d505..461553e84eb 100644
--- a/library/templates/Intonation/Library/FormCustomizer/Abstract.php
+++ b/library/templates/Intonation/Library/FormCustomizer/Abstract.php
@@ -118,7 +118,7 @@ class Intonation_Library_FormCustomizer_Abstract {
 
       ->addElement('select',
                    $this->_template->withNameSpace('width_medium'),
-                   ['label' => $this->_('Largeur à partir de la taille moyennne ( entre 768px et 991px )'),
+                   ['label' => $this->_('Largeur à partir de la taille moyenne ( entre 768px et 991px )'),
                     'value' => Intonation_Library_Constants::WIDTH_AUTO,
                     'multiOptions' => $this->_getWidthsOptions()])
 
@@ -148,7 +148,7 @@ class Intonation_Library_FormCustomizer_Abstract {
 
       ->addElement('select',
                    $this->_template->withNameSpace('order_medium'),
-                   ['label' => $this->_('Ordre à partir de la taille moyennne ( entre 768px et 991px )'),
+                   ['label' => $this->_('Ordre à partir de la taille moyenne ( entre 768px et 991px )'),
                     'value' => Intonation_Library_Constants::WIDTH_AUTO,
                     'multiOptions' => $this->_getWidthsOptions()])
 
@@ -178,7 +178,7 @@ class Intonation_Library_FormCustomizer_Abstract {
 
       ->addElement('select',
                    $this->_template->withNameSpace('offset_medium'),
-                   ['label' => $this->_('Décalage à droite à partir de la taille moyennne ( entre 768px et 991px )'),
+                   ['label' => $this->_('Décalage à droite à partir de la taille moyenne ( entre 768px et 991px )'),
                     'value' => Intonation_Library_Constants::WIDTH_AUTO,
                     'multiOptions' => $this->_getOffsetOptions()])
 
diff --git a/library/templates/Intonation/Library/Settings.php b/library/templates/Intonation/Library/Settings.php
index 89c07bf444a..44983d538e4 100644
--- a/library/templates/Intonation/Library/Settings.php
+++ b/library/templates/Intonation/Library/Settings.php
@@ -169,13 +169,14 @@ class Intonation_Library_Settings extends Intonation_System_Abstract {
                                                   'like' => 'class fas fa-heart text-danger',
                                                   'dislike' => 'class far fa-heart',
                                                   'suggest' => 'class fas fa-hand-holding-heart',
-                                                  'subscription' => 'class fas fa-euro-sign',
+                                                  'subscription' => 'class fas fa-receipt',
                                                   'card-number' => 'class fas fa-barcode',
                                                   'read-document' => 'class far fa-arrow-alt-circle-right',
                                                   'read-review' => 'class far fa-comment-dots',
                                                   'team' => 'class fas fa-users',
                                                   'agenda' => 'class fas fa-calendar-alt',
                                                   'ical' => 'class far fa-calendar-plus',
+                                                  'novelty' => 'class far fa-sun',
 
                           ],
 
@@ -201,6 +202,7 @@ class Intonation_Library_Settings extends Intonation_System_Abstract {
                                                 'rename' => 'class fas fa-edit',
                                                 'more' => 'class fas fa-ellipsis-h',
                                                 'refresh' => 'class fas fa-sync-alt',
+                                                'lock' => 'class fas fa-unlock-alt',
 
                                                 'email' => 'class fas fa-at',
                                                 'phone' => 'class fas fa-phone',
diff --git a/library/templates/Intonation/Library/View/Wrapper/Article.php b/library/templates/Intonation/Library/View/Wrapper/Article.php
index d52456d32ac..d727a02a505 100644
--- a/library/templates/Intonation/Library/View/Wrapper/Article.php
+++ b/library/templates/Intonation/Library/View/Wrapper/Article.php
@@ -217,11 +217,11 @@ class Intonation_Library_View_Wrapper_Article extends Intonation_Library_View_Wr
                                                  'Title' => $this->_('Ajouter l\'événement %s dans mon calendrier',
                                                                      $this->_model->getLibelle())]);
 
-    $actions [] = $this->_view->div(['class' => 'print'],
-                                   $this->_view->tagPrintLink((new Class_Entity())
-                                                              ->setController('cms')
-                                                              ->setModels([$this->_model])
-                                                              ->setStrategy('Article_List')));
+    if ($print_link = $this->_view->tagPrintLink((new Class_Entity())
+                                                 ->setController('cms')
+                                                 ->setModels([$this->_model])
+                                                 ->setStrategy('Article_List')))
+      $actions [] = $this->_view->div(['class' => 'print'], $print_link);
 
     $actions [] = $this->_view->reseauxSociaux($this->_model);
 
diff --git a/library/templates/Intonation/Library/View/Wrapper/Loan.php b/library/templates/Intonation/Library/View/Wrapper/Loan.php
index 1ee5c4f6461..b685b0959e2 100644
--- a/library/templates/Intonation/Library/View/Wrapper/Loan.php
+++ b/library/templates/Intonation/Library/View/Wrapper/Loan.php
@@ -217,12 +217,12 @@ class Intonation_Library_View_Wrapper_Loan extends Intonation_Library_View_Wrapp
     if ($this->_record)
       return $this->_record;
 
-    if (!$this->_model->getNoticeOPAC())
+    if (!$db_record = $this->_model->getNoticeOPAC())
       return $this->_record = null;
 
-    return $this->_record = $wrapper = (new Intonation_Library_View_Wrapper_Record)
+    return $this->_record = (new Intonation_Library_View_Wrapper_Record)
       ->setView($this->_view)
-      ->setModel($this->_model->getNoticeOPAC());
+      ->setModel($db_record);
   }
 
 
diff --git a/library/templates/Intonation/Library/View/Wrapper/PNBLoan.php b/library/templates/Intonation/Library/View/Wrapper/PNBLoan.php
index b89ce891280..bf304e56c1d 100644
--- a/library/templates/Intonation/Library/View/Wrapper/PNBLoan.php
+++ b/library/templates/Intonation/Library/View/Wrapper/PNBLoan.php
@@ -101,15 +101,12 @@ class Intonation_Library_View_Wrapper_PNBLoan extends Intonation_Library_View_Wr
   }
 
 
-  protected function _getRecord() {
-    if ($this->_record)
-      return $this->_record;
-
-    if (!$this->_model->getNoticeOPAC())
-      return $this->_record = null;
-
-    return $this->_record = $wrapper = (new Intonation_Library_View_Wrapper_Record)
-      ->setView($this->_view)
-      ->setModel($this->_model->getNoticeOPAC());
+  public function getContentForJSSearch() {
+    return $this->_in_js_search
+      ? $this->_view->dNone(implode(' ', [$this->getMainTitle(),
+                                          $this->getSecondaryTitle(),
+                                          $this->getDescription(),
+                                          '%s']))
+      : '';
   }
 }
diff --git a/library/templates/Intonation/Library/View/Wrapper/Record.php b/library/templates/Intonation/Library/View/Wrapper/Record.php
index 0eff9817b65..9e9f0e25bd3 100644
--- a/library/templates/Intonation/Library/View/Wrapper/Record.php
+++ b/library/templates/Intonation/Library/View/Wrapper/Record.php
@@ -129,7 +129,13 @@ class Intonation_Library_View_Wrapper_Record extends Intonation_Library_View_Wra
 
 
   public function getBadges() {
-    $badges = [
+    $badges = [((new Intonation_Library_Badge)
+                ->setTag('div')
+                ->setClass(' pb-1 mb-1 px-0 card-title only_visible_in_viewnotice')
+                ->setText($this->_getFullTitle())
+                ->setTitle($this->_('Compléments de titre pour le document %s',
+                                    $this->_model->getTitrePrincipal(' ')))),
+
                ((new Intonation_Library_Badge)
                 ->setTag('a')
                 ->setClass('warning fs_1em record_doctype')
@@ -166,6 +172,18 @@ class Intonation_Library_View_Wrapper_Record extends Intonation_Library_View_Wra
                                       $this->_model->getTitrePrincipal(' '),
                                       $this->_model->getAnnee())));
 
+    $badges [] = ((new Intonation_Library_Badge)
+                  ->setClass('secondary record_novelty')
+                  ->setImage(($this->_model->isNouveaute()
+                              ? (Class_Template::current()
+                                 ->getIco($this->_view,
+                                          'novelty',
+                                          'library'))
+                              : ''))
+                  ->setText($this->_model->isNouveaute() ? $this->_('Nouveauté') : '')
+                  ->setTitle($this->_('Le document %s est nouveau dans votre bibliotèque',
+                                      $this->_model->getTitrePrincipal(' '))));
+
     if ($count = $this->_model->numberOfAvisBibliothecaire()) {
       $badges [] = ((new Intonation_Library_Badge)
                     ->setTag('span')
@@ -210,20 +228,81 @@ class Intonation_Library_View_Wrapper_Record extends Intonation_Library_View_Wra
                     ->setTitle($facet->getTitle())
                     ->setUrl($facet->getUrlForLink()));
 
-    if ($this->_model->hasSerie()) {
-      $serie = $this->_model->getClefChapeau() . '-' . $this->_model->getTypeDoc();
+    $badges = $this->_addSerieBadges($badges);
 
-      $label = Class_Codification::getLibelleForSerie($this->_model);
-      $badges [] = (new Intonation_Library_Badge)
-        ->setTag('a')
-        ->setUrl($this->_view->url((new Class_CriteresRecherche)->getNewUrlCriteresSerie($serie), null, true))
-        ->setClass('white record_serie')
-        ->setText($label)
-        ->setTitle($this->_view->_('Lancer une recherche pour %s', lcfirst($label)));
-    }
+    return $this->_view->renderBadges($badges);
+  }
 
 
-    return $this->_view->renderBadges($badges);
+  protected function _addSerieBadges($badges) {
+    if ( ! $this->_model->hasSerie())
+      return $badges;
+
+    $serie = $this->_model->getClefChapeau() . '-' . $this->_model->getTypeDoc();
+
+    $label = Class_Codification::getLibelleForSerie($this->_model);
+    $badges [] = (new Intonation_Library_Badge)
+      ->setTag('a')
+      ->setUrl($this->_view->url((new Class_CriteresRecherche)->getNewUrlCriteresSerie($serie), null, true))
+      ->setClass('white record_serie')
+      ->setText($label)
+      ->setTitle($this->_view->_('%s dans la recherche.', $label));
+
+    if ( ! $user = Class_Users::getIdentity())
+      return $badges;
+
+    if ( ! $same_serie_records = $this->_model->getNoticesMemeSeries())
+      return $badges;
+
+    if ( ! $readed_selection = Class_PanierNotice::findFirstBy(['libelle' => 'Déjà lu',
+                                                                'id_user' => $user->getId()]))
+      return $badges;
+
+    if ( ! $readed_records = $readed_selection->getNoticesAsArray())
+      return $badges;
+
+    $readed_records_in_series =
+      array_filter($readed_records, function ($record)
+                   {
+                     return $record->hasSerie();
+                   });
+
+    if ( ! $readed_records_in_series)
+      return $badges;
+
+    $readed_in_serie = array_uintersect($readed_records_in_series,
+                                   $same_serie_records,
+                                   function($record_1, $record_2)
+                                   {
+                                     if ($record_1->getId() == $record_2->getId())
+                                       return 0;
+
+                                     if ($record_1->getId() > $record_2->getId())
+                                       return 1;
+
+                                     return -1;
+                                   });
+
+    if ( ! $readed_in_serie)
+      return $badges;
+
+    $total_readed_in_serie = count($readed_in_serie);
+    $total_to_read = count($same_serie_records);
+
+    $badges [] = (new Intonation_Library_Badge)
+      ->setClass('white record_serie')
+      ->setImage(Class_Template::current()->getIco($this->_view,
+                                                   'readed',
+                                                   'library'))
+      ->setText($this->_('%d sur %d',
+                         $total_readed_in_serie,
+                         $total_to_read))
+      ->setTitle($this->_view->_('Vous avez lu %d sur %d des tômes de la série %s',
+                                 $total_readed_in_serie,
+                                 $total_to_read,
+                                 ucfirst(strtolower($this->_model->getClefChapeau()))));
+
+    return $badges;
   }
 
 
@@ -444,4 +523,17 @@ class Intonation_Library_View_Wrapper_Record extends Intonation_Library_View_Wra
     $this->_allow_XSL = $bool;
     return $this;
   }
+
+
+  protected function _getFullTitle() {
+    $main = $this->getMainTitle();
+    $titles = $this->_model->getChampNotice(Class_Codification::CODE_TITRE);
+
+    $titles = array_filter($titles, function($title) use ($main)
+                           {
+                             return false === strpos($main, $title);
+                           });
+
+    return implode(BR, $titles);
+  }
 }
\ No newline at end of file
diff --git a/library/templates/Intonation/Library/View/Wrapper/User/RichContent/EditInformations.php b/library/templates/Intonation/Library/View/Wrapper/User/RichContent/EditInformations.php
index 3e44a896d48..10a17b2d4ed 100644
--- a/library/templates/Intonation/Library/View/Wrapper/User/RichContent/EditInformations.php
+++ b/library/templates/Intonation/Library/View/Wrapper/User/RichContent/EditInformations.php
@@ -20,7 +20,7 @@
  */
 
 
-class Intonation_Library_View_Wrapper_User_RichContent_EditInformations extends Intonation_Library_View_Wrapper_User_RichContent_Settings {
+class Intonation_Library_View_Wrapper_User_RichContent_EditInformations extends Intonation_Library_View_Wrapper_User_RichContent_Informations {
 
   public function getTitle() {
     return $this->_('Modifier mes informations');
@@ -44,7 +44,7 @@ class Intonation_Library_View_Wrapper_User_RichContent_EditInformations extends
 
   public function getNavUrl() {
     return ['controller' => 'abonne',
-            'action' => 'modifier'];
+            'action' => 'informations'];
   }
 
 
diff --git a/library/templates/Intonation/Library/View/Wrapper/User/RichContent/EditPassword.php b/library/templates/Intonation/Library/View/Wrapper/User/RichContent/EditPassword.php
new file mode 100644
index 00000000000..4ef8cc89b4c
--- /dev/null
+++ b/library/templates/Intonation/Library/View/Wrapper/User/RichContent/EditPassword.php
@@ -0,0 +1,54 @@
+<?php
+/**
+ * Copyright (c) 2012-2019, 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 Intonation_Library_View_Wrapper_User_RichContent_EditPassword extends Intonation_Library_View_Wrapper_User_RichContent_Informations {
+
+  public function getTitle() {
+    return $this->_('Changer mon mot de passe');
+  }
+
+
+  public function getDBTitle() {
+    return $this->getTitle();
+  }
+
+
+  public function getContent() {
+    return $this->_content;
+  }
+
+
+  public function getClass() {
+    return 'form_user_password';
+  }
+
+
+  public function getNavUrl() {
+    return ['controller' => 'abonne',
+            'action' => 'informations'];
+  }
+
+
+  public function getNavTitle() {
+    return $this->_('Changer le mot de passe de mon compte');
+  }
+}
diff --git a/library/templates/Intonation/Library/View/Wrapper/User/RichContent/RateRecords.php b/library/templates/Intonation/Library/View/Wrapper/User/RichContent/RateRecords.php
index 102f80c61e5..0660d811271 100644
--- a/library/templates/Intonation/Library/View/Wrapper/User/RichContent/RateRecords.php
+++ b/library/templates/Intonation/Library/View/Wrapper/User/RichContent/RateRecords.php
@@ -112,10 +112,14 @@ class Intonation_Library_View_Wrapper_User_RichContent_RateRecords extends Inton
 
   protected function _removeRated($records, $user) {
     $unrated_records = [];
-    foreach ($records as $record)
+    foreach ($records as $record) {
+      if (! $record)
+        continue;
+
       if (!Class_AvisNotice::findFirstBy(['id_user' => $user->getId(),
                                           'clef_oeuvre' => $record->getClefOeuvre()]))
           $unrated_records [] = $record;
+    }
 
     return $unrated_records;
   }
diff --git a/library/templates/Intonation/Library/Widget/Carousel/Article/View.php b/library/templates/Intonation/Library/Widget/Carousel/Article/View.php
index 1937379e385..cb202e118b5 100644
--- a/library/templates/Intonation/Library/Widget/Carousel/Article/View.php
+++ b/library/templates/Intonation/Library/Widget/Carousel/Article/View.php
@@ -22,11 +22,14 @@
 
 class Intonation_Library_Widget_Carousel_Article_View extends Intonation_Library_Widget_Carousel_View {
 
+  protected $_articles;
+
+
   protected function _findElements() {
     $loader = Class_Article::getLoader();
     $this->preferences['size'] = 100;
-    $articles_to_show = $loader->getArticlesByPreferences($this->preferences);
-    return $articles_to_show;
+    $this->_articles = $loader->getArticlesByPreferences($this->preferences);
+    return $this->_articles;
   }
 
 
@@ -48,4 +51,9 @@ class Intonation_Library_Widget_Carousel_Article_View extends Intonation_Library
   protected function _emptyListMessage() {
     return $this->view->tag('p', $this->_('Aucun article'));
   }
+
+
+  protected function _extendedActions() {
+    return [function() { return $this->view->Admin_TagAddNewArticle($this->id_module, $this->_articles); }];
+  }
 }
\ No newline at end of file
diff --git a/library/templates/Intonation/Library/Widget/Free/View.php b/library/templates/Intonation/Library/Widget/Free/View.php
index fb8c82dcf77..d04f2656b74 100644
--- a/library/templates/Intonation/Library/Widget/Free/View.php
+++ b/library/templates/Intonation/Library/Widget/Free/View.php
@@ -22,6 +22,9 @@
 
 class Intonation_Library_Widget_Free_View extends Zendafi_View_Helper_Accueil_Base {
 
+  protected $_article;
+
+
   public function getHtml() {
     $this->titre = $this->_settings->getTitre();
     $this->contenu = $this->_getHTML();
@@ -39,6 +42,18 @@ class Intonation_Library_Widget_Free_View extends Zendafi_View_Helper_Accueil_Ba
     if (!$article = array_shift($articles))
       return '';
 
-    return $this->view->article_ReplaceWidgets($article->getFullContent());
+    $this->_article = $article;
+
+    if ($edit_link = $this->view->admin_TagEdit($article))
+      $edit_link = $this->view->div(['class' => 'position_absolute'],
+                                    $edit_link);
+
+    return $this->view->article_ReplaceWidgets($article->getFullContent())
+      . $edit_link;
+  }
+
+
+  protected function _extendedActions() {
+    return [function() { return $this->view->Admin_TagAddNewArticle($this->id_module, [$this->_article]); }];
   }
 }
\ No newline at end of file
diff --git a/library/templates/Intonation/View/Abonne/Edit.php b/library/templates/Intonation/View/Abonne/Edit.php
index 58f9c7677ee..7bcfca1937c 100644
--- a/library/templates/Intonation/View/Abonne/Edit.php
+++ b/library/templates/Intonation/View/Abonne/Edit.php
@@ -22,10 +22,6 @@
 
 class Intonation_View_Abonne_Edit extends Intonation_View_Abonne {
   public function abonne_Edit($user) {
-    $this->view->form->removeElement('subscriptions');
-    $this->view->form->removeElement('password');
-    $this->view->form->removeElement('confirm_password');
-
     $html = $this->abonne($user);
     $this->view->titre = $this->_('%s : Modifier mes informations',
                                   $this->view->titre);
diff --git a/library/templates/Intonation/View/Abonne/EditPassword.php b/library/templates/Intonation/View/Abonne/EditPassword.php
new file mode 100644
index 00000000000..9397a2eff20
--- /dev/null
+++ b/library/templates/Intonation/View/Abonne/EditPassword.php
@@ -0,0 +1,57 @@
+<?php
+/**
+ * Copyright (c) 2012-2019, 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 Intonation_View_Abonne_EditPassword extends Intonation_View_Abonne {
+  public function abonne_EditPassword($user) {
+    $html = $this->abonne($user);
+    $this->view->titre = $this->_('%s : Changer mon mot de passe',
+                                  $this->view->titre);
+    return $html;
+  }
+
+
+  protected function _hookOn($rich_content) {
+    $sections = $rich_content->getSections();
+
+    $content = $this->view->renderForm($this->view->form)
+      . $this->view->tagAction((new Intonation_Library_Link())
+                                ->setText($this->_('J\'ai oublié mon mot de passe'))
+                                ->setUrl($this->view->url(['controller' => 'auth',
+                                                           'action' => 'lostpass']))
+                                ->setClass('btn btn-sm btn-secondary')
+                                ->setTitle($this->_('Réinitialisé mon mot de passe')));
+
+
+    $sections [Intonation_Library_View_Wrapper_User_RichContent::INFO] = (new Intonation_Library_View_Wrapper_User_RichContent_EditPassword)
+      ->setModel($this->view->user)
+      ->setView($this->view)
+      ->setContent($content)
+      ->beActive()
+      ->beVisible();
+
+    $rich_content->setSections($sections);
+  }
+
+
+  protected function _showSections($sections) {
+  }
+}
diff --git a/library/templates/Intonation/View/Heartbeat.php b/library/templates/Intonation/View/Heartbeat.php
new file mode 100644
index 00000000000..e1a4edcf70b
--- /dev/null
+++ b/library/templates/Intonation/View/Heartbeat.php
@@ -0,0 +1,34 @@
+<?php
+/**
+ * Copyright (c) 2012-2014, 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 Intonation_View_Heartbeat extends ZendAfi_View_Helper_BaseHelper {
+  public function heartbeat() {
+    return (Class_Users::isCurrentUserCanAccesBackend() || ZendAfi_Auth_Others::getInstance()->isSuperAdminLogged())
+      ? $this->_tag('script',
+                    sprintf('(function heartbeat() {$.get("%s", function() {setTimeout(heartbeat, 60000);}); })();',
+                            $this->view->absoluteUrl(['module' => 'opac',
+                                                      'controller' => 'index',
+                                                      'action' => 'heartbeat',
+                                                      'id_profil' => Class_Profil::getCurrentProfil()->getId()], null, true)))
+      : '';
+  }
+}
\ No newline at end of file
diff --git a/library/templates/Intonation/View/Opac.php b/library/templates/Intonation/View/Opac.php
index 62ed3797d1d..46c26b1eab6 100644
--- a/library/templates/Intonation/View/Opac.php
+++ b/library/templates/Intonation/View/Opac.php
@@ -181,8 +181,8 @@ class Intonation_View_Opac extends ZendAfi_View_Helper_BaseHelper {
 
                 $this->_tag('title', $this->view->getTitre()),
 
-                $head_scripts->styleSheetsHTML(),
                 $script_loader->styleSheetsHTML(),
+                $head_scripts->styleSheetsHTML(),
                 $head_scripts->javaScriptsHTML(),
                 $script_loader->javaScriptsHTML(),
                 $this->view->heartbeat(),
diff --git a/library/templates/Intonation/View/RenderTruncateList.php b/library/templates/Intonation/View/RenderTruncateList.php
index 839c869e02a..197762018d5 100644
--- a/library/templates/Intonation/View/RenderTruncateList.php
+++ b/library/templates/Intonation/View/RenderTruncateList.php
@@ -151,8 +151,7 @@ class Intonation_View_RenderTruncateList extends ZendAfi_View_Helper_BaseHelper
   protected function _ajaxifyList($collection) {
     $helper = (new Intonation_Library_AjaxPaginatedListHelper)
       ->setView($this->view)
-      ->setCollection($collection)
-      ->setCurrentPage(1);
+      ->setCollection($collection);
 
     return $this->view->renderAjaxPaginatedList($helper);
   }
diff --git a/library/templates/Intonation/View/Search/HtmlCriteria.php b/library/templates/Intonation/View/Search/HtmlCriteria.php
index 0766aef2fb5..548e734d0c9 100644
--- a/library/templates/Intonation/View/Search/HtmlCriteria.php
+++ b/library/templates/Intonation/View/Search/HtmlCriteria.php
@@ -50,7 +50,9 @@ class Intonation_View_Search_HtmlCriteria extends ZendAfi_View_Helper_TagCritere
 
 
   public function getSuppressionImgUrlForLibelle($label, $url) {
-    unset($url['page']);
+    if (isset($url['page']))
+      unset($url['page']);
+
     return $this->view
       ->tagAction(new Intonation_Library_Link(['Url' => $this->view->url($url, null, true),
                                                'Image' => Class_Template::current()->getIco($this->view, 'clean', 'utils'),
@@ -77,6 +79,16 @@ class Intonation_View_Search_HtmlCriteria extends ZendAfi_View_Helper_TagCritere
   }
 
 
+  public function visitAnnexe($annexe) {
+    $text = $this->_('Site: %s',
+                     Class_Codification::getInstance()->getLibelleFacette('Y' . $annexe));
+
+    $url = $this->_criteres_recherche->getUrlCriteresWithoutElement('annexe');
+
+    return $this->htmlAppend($this->getSuppressionImgUrlForLibelle($text, $url));
+  }
+
+
   public function htmlAppend($text, $attribs = []) {
     if (empty($attribs))
       $attribs = ['class' => 'd-inline text-left align-items-center'];
diff --git a/library/templates/Intonation/View/Search/TextCriteria.php b/library/templates/Intonation/View/Search/TextCriteria.php
index 0242cead7c5..41763d9083c 100644
--- a/library/templates/Intonation/View/Search/TextCriteria.php
+++ b/library/templates/Intonation/View/Search/TextCriteria.php
@@ -38,12 +38,19 @@ class Intonation_View_Search_TextCriteria extends Intonation_View_Search_HtmlCri
   }
 
 
+  public function visitAnnexe($annexe) {
+    return $this
+      ->htmlAppend($this->view->_('Site: %s', Class_Codification::getInstance()->getLibelleFacette('Y' . $annexe)));
+  }
+
+
   public  function getSuppressionImgUrlForLibelle($label, $url) {
     return $label;
   }
 
 
   public function htmlAppend($text, $attribs = []) {
-    return $this->_html .= ', ' . $text;
+    $this->_html .= ', ' . $text;
+    return $this;
   }
 }
\ No newline at end of file
diff --git a/library/templates/Intonation/View/User/Informations.php b/library/templates/Intonation/View/User/Informations.php
index ec4f8d136d5..62cb26b9f71 100644
--- a/library/templates/Intonation/View/User/Informations.php
+++ b/library/templates/Intonation/View/User/Informations.php
@@ -31,18 +31,19 @@ class Intonation_View_User_Informations extends ZendAfi_View_Helper_BaseHelper {
     $map = [$this->_('Nom') => $user->getNom(),
             $this->_('Prénom') => $user->getPrenom(),
             $this->_('Pseudo') => $user->getPseudo(),
-            $this->_('Date de naissance') => (new DateTime($user->getNaissance()))->format($this->_('d / m / Y')),
 
-            $this->_('Numéro de carte') => $user->getIdabon(),
-            $this->_('Bibliothèque') => $this->_getLibrary($user),
+            $this->_('Adresse') => $user->getAdresse(),
+            $this->_('Code postal') => $user->getCodePostal(),
+            $this->_('Ville') => $user->getVille(),
 
             $this->_('Courriel') => $user->getMail(),
             $this->_('Numéro de téléphone') => $user->getTelephone(),
             $this->_('Numéro de téléphone mobile') => $user->getMobile(),
-            $this->_('Adresse') => $user->getAdresse(),
-            $this->_('Ville') => $user->getVille(),
-            $this->_('Code postal') => $user->getCodePostal(),
 
+            $this->_('Date de naissance') => (new DateTime($user->getNaissance()))->format($this->_('d / m / Y')),
+
+            $this->_('Numéro de carte') => $user->getIdabon(),
+            $this->_('Bibliothèque') => $this->_getLibrary($user),
     ];
 
     $html = [];
@@ -53,18 +54,27 @@ class Intonation_View_User_Informations extends ZendAfi_View_Helper_BaseHelper {
 
     $edit_link =
       new Intonation_Library_Link(['Url' => $this->view->url(['controller' => 'abonne',
-                                                              'action' => 'modifier'], null, true),
+                                                              'action' => 'modifier']),
                                    'Image' => Class_Template::current()->getIco($this->view, 'edit', 'utils'),
                                    'Text' => $this->_('Modifier mes informations'),
                                    'Title' => $this->_('Modifier les informations me concernant'),
                                    'Class' => 'btn btn-sm btn-success',
-                                   'InlineText' => 1,
-                                   'Popup' => true]);
+                                   'InlineText' => 1]);
+
+    $edit_password =
+      new Intonation_Library_Link(['Url' => $this->view->url(['controller' => 'abonne',
+                                                              'action' => 'changer-mon-mot-de-passe']),
+                                   'Image' => Class_Template::current()->getIco($this->view, 'lock', 'utils'),
+                                   'Text' => $this->_('Changer mon mot de passe'),
+                                   'Title' => $this->_('Changer le mot de passe de mon compte.'),
+                                   'Class' => 'btn btn-sm btn-secondary ml-3',
+                                   'InlineText' => 1]);
 
     return
       $this->view->div(['class' => 'card no_border'],
                        $this->view->div(['class' => 'card-header pl-2 pb-0'],
-                                          $this->view->tagAction($edit_link))
+                                          $this->view->tagAction($edit_link)
+                                        . $this->view->tagAction($edit_password))
                        . $this->view->div(['class' => 'card-body pl-0 pt-2'],
                                         $this->_tag('dl', implode($html))));
   }
diff --git a/tests/scenarios/Templates/MuscleTemplateTest.php b/tests/scenarios/Templates/MuscleTemplateTest.php
index 61eda30cc18..b01e10e5b0d 100644
--- a/tests/scenarios/Templates/MuscleTemplateTest.php
+++ b/tests/scenarios/Templates/MuscleTemplateTest.php
@@ -65,6 +65,19 @@ class MuscleTemplateProfilePatcherTest extends MuscleTemplateTestCase {
 
   public function setUp() {
     parent::setUp();
+
+    $this->fixture('Class_ArticleCategorie',
+                   ['id' => 34,
+                    'libelle' => 'TESTING',
+                   ]);
+
+    $this->fixture('Class_Article',
+                   ['id' => 5,
+                    'titre' => 'Un super article de test',
+                    'contenu' => 'Un super contenu d\'article pour les testes',
+                    'id_cat' => 34
+                   ]);
+
     $this->dispatch('/opac/index/index');
   }
 
@@ -77,6 +90,7 @@ class MuscleTemplateProfilePatcherTest extends MuscleTemplateTestCase {
             ['admin_tools'],
             ['scroll'],
             ['library'],
+            ['news'],
             ['critiques'],
     ];
   }
@@ -95,6 +109,12 @@ class MuscleTemplateProfilePatcherTest extends MuscleTemplateTestCase {
   public function editTemplateMuscleShouldBePresent() {
     $this->assertXPathContentContains('//a[contains(@href, "admin/template/edit/template/MUSCLE")]', 'Configuration du thème');
   }
+
+
+  /** @test */
+  public function addNewArticleInWidgetShouldBePresent() {
+    $this->assertXPathContentContains('//a[contains(@href, "admin/cms/add")]', 'Ajouter un nouvel article');
+  }
 }
 
 
diff --git a/tests/scenarios/Templates/MyBibAppTemplateTest.php b/tests/scenarios/Templates/MyBibAppTemplateTest.php
index 9e75841dc6d..f496a20c7a8 100644
--- a/tests/scenarios/Templates/MyBibAppTemplateTest.php
+++ b/tests/scenarios/Templates/MyBibAppTemplateTest.php
@@ -320,4 +320,92 @@ class MyBibAppTemplatePostDispatchOauthWithUserAgentTest extends MyBibAppTemplat
 
     $this->assertRedirectTo('bokeh://authorize#token=nonos');
   }
+}
+
+
+
+
+class MyBibAppTemplateReadedInSerieTest extends MyBibAppTemplateTestCase {
+
+
+  /** @test */
+  public function badgeReadedShouldContains1on2() {
+    $profile_id =
+      (new MyBibApp_Template)
+      ->tryOn($this->fixture('Class_Profil',
+                             ['id' => 23]));
+
+    Class_Profil::setCurrentProfil(Class_Profil::find($profile_id));
+
+    $logged_user = Class_Users::getIdentity();
+    $logged_user_id = $logged_user->getId();
+
+    Class_Template::current()->upgradeUser($logged_user);
+
+    $un_pour_tous =
+      $this->fixture('Class_Notice',
+                     ['id' => 12,
+                      'clef_chapeau' => 'TROLLS DE TROY',
+                      'clef_alpha' => 'TROLLS DE TROY UN POUR TOUS',
+                      'tome_alpha' => 1,
+                      'type_doc' => Class_TypeDoc::LIVRE])
+           ->set_subfield('461', 't', 'TROLLS DE TROY');
+
+    $enquete_pour_les_troyens =
+      $this->fixture('Class_Notice',
+                     ['id' => 15,
+                      'clef_chapeau' => 'TROLLS DE TROY',
+                      'clef_alpha' => 'TROLLS DE TROY ENQUETE POUR LES TROYENS',
+                      'tome_alpha' => 2,
+                      'type_doc' => Class_TypeDoc::LIVRE])
+           ->set_subfield('461', 't', 'TROLLS DE TROY');
+
+    $readed_selection =
+      Class_PanierNotice::findFirstBy(['libelle' => 'Déjà lu',
+                                       'id_user' => $logged_user->getId()]);
+
+    $readed_selection->addNotice($un_pour_tous);
+
+    $result = $this->mock();
+
+    $result
+      ->whenCalled('setDuration')
+      ->answers($result)
+
+      ->whenCalled('setSettings')
+      ->answers($result)
+
+      ->whenCalled('getSettings')
+      ->answers(['facettes' => ''])
+
+      ->whenCalled('fetchFacetsAndTags')
+      ->answers(['facettes' => ''])
+
+      ->whenCalled('getCriteresRecherche')
+      ->answers((new Intonation_Library_Search_Criteria)->setParams(['expressionRecherche' => 'trolls',
+                                                                     'liste_format' => 4]))
+
+      ->whenCalled('getRecordsCount')
+      ->answers(2)
+
+      ->whenCalled('isError')
+      ->answers(false)
+
+      ->whenCalled('fetchRecords')
+      ->answers([$un_pour_tous,
+                 $enquete_pour_les_troyens])
+
+      ->whenCalled('fetchAllRecordsIds')
+      ->answers([12, 15]);
+
+    $engine = $this
+      ->mock()
+      ->whenCalled('lancerRecherche')
+      ->answers($result);
+
+    Class_MoteurRecherche::setInstance($engine);
+
+    $this->dispatch('/recherche/simple/id_profil/24/expressionRecherche/trolls');
+    $this->assertXPathContentContains('//span[contains(@class, "record_serie")]', '1 sur 2');
+  }
 }
\ No newline at end of file
diff --git a/tests/scenarios/Templates/TemplatesTest.php b/tests/scenarios/Templates/TemplatesTest.php
index 03bf00ec929..db29ddd6d54 100644
--- a/tests/scenarios/Templates/TemplatesTest.php
+++ b/tests/scenarios/Templates/TemplatesTest.php
@@ -1994,6 +1994,7 @@ class TemplatesViewRecordTest extends TemplatesIntonationTestCase {
     $this->fixture('Class_Notice',
                    ['id' => 456,
                     'type_doc' => 1,
+                    'date_creation' => date('Y-m-d', Class_Notice::getTimeSource()->time()),
                     'titre_principal' => 'Psycho',
                     'clef_oeuvre' => 'PSYKO',
                     'facettes' => 'G13 M12']);
@@ -2024,6 +2025,12 @@ class TemplatesViewRecordTest extends TemplatesIntonationTestCase {
   }
 
 
+  /** @test */
+  public function shouldBeANovelty() {
+    $this->assertXPathContentContains('//span', 'Nouveauté');
+  }
+
+
   /** @test */
   public function ratingShouldBeDisplayed() {
     $this->assertXPathContentContains('//div', 'Lost héighway');
@@ -3136,6 +3143,33 @@ abstract class TemplatesIntonationAccountTestCase extends TemplatesIntonationTes
                                   'contenu' => 'test de newsletter: http://monurl.newsletter.fr. http://mon-domain.org']);
 
     $current_user->setNewsletters([$newsletter]);
+
+    Class_Loan_Pnb::setTimeSource(new TimeSourceForTest('2017-11-14 10:23:10'));
+    Class_AdminVar::set('DILICOM_PNB_ENABLE_HOLDS', 1);
+
+    $http = $this->mock()->whenCalled('open_url')->answers(null);
+    Class_WebService_BibNumerique_Dilicom_Hub::setDefaultHttpClient($http);
+
+    $pnb_album = $this->fixture('Class_Album',
+                                ['id' => 543,
+                                 'titre' => 'Dr House',
+                                 'visible' => 1,
+                                 'status' => 3,
+                                 'id_origine' => 'Dilicom-3663608260879']);
+
+    $pnb_album->index();
+
+    $this->fixture('Class_Loan_Pnb',
+                   ['id' => 3,
+                    'record_origin_id' => 'Dilicom-3663608260879',
+                    'subscriber_id' => '',
+                    'album' => $pnb_album,
+                    'user_id' => $current_user->getId(),
+                    'expected_return_date' => '2017-12-13 13:57:33',
+                    'loan_date' => '2017-11-13 13:57:33',
+                    'loan_link' => 'https://pnb-dilicom.centprod.com/v2//XXXXXXXX.do',
+                    'order_line_id' => '584837a045ce56ef0a072a8b',]);
+
   }
 }
 
@@ -3180,8 +3214,8 @@ class TemplatesIntonationDispatchAccountTest extends TemplatesIntonationAccountT
 
 
   /** @test */
-  public function paulShouldHaveBadgeLoansCount2() {
-    $this->assertXPathContentContains('//div//a[contains(@class, "badge")]', '2 prêt(s) en cours');
+  public function paulShouldHaveBadgeLoansCount3() {
+    $this->assertXPathContentContains('//div//a[contains(@class, "badge")]', '3 prêt(s) en cours');
   }
 
 
@@ -3285,20 +3319,24 @@ class TemplatesDispatchAbonneLoansTest extends TemplatesIntonationAccountTestCas
 
 
   /** @test */
-  public function loanAliceShouldContainsLoan() {
+  public function shouldContainsLoanAlice() {
     $this->dispatch('/opac/abonne/ajax-loans/id_profil/72');
     $this->assertXPathContentContains('//div', 'Alice');
   }
 
 
   /** @test */
-  public function shouldInputWithKey() {
-    Storm_Cache::beVolatile();
+  public function shouldContainsLoanDrHouse() {
+    $this->dispatch('/opac/abonne/ajax-loans/id_profil/72');
+    $this->assertXPathContentContains('//div', 'Dr House');
+  }
 
-    $cards = new Class_User_Cards(Class_Users::getIdentity());
 
+  /** @test */
+  public function page2ShouldContainsSearchInputWithMd5Key() {
+    Storm_Cache::beVolatile();
+    $cards = new Class_User_Cards(Class_Users::getIdentity());
     $loans = $cards->getLoansWithOutPNB([]);
-
     $loans = array_map(function($loan)
                        {
                          return (new Intonation_Library_View_Wrapper_Loan)
@@ -3312,7 +3350,6 @@ class TemplatesDispatchAbonneLoansTest extends TemplatesIntonationAccountTestCas
       ->setCollection($collection)
       ->setRendering('cardifyHorizontal');
 
-
     $id = $helper->getId();
 
     $this->dispatch('/opac/index/ajax-paginated-list/id/' . $id . '/page/2/render/ajax/id_profil/72');
@@ -3321,16 +3358,16 @@ class TemplatesDispatchAbonneLoansTest extends TemplatesIntonationAccountTestCas
 
 
   /** @test */
-  public function potterShouldBeFound() {
+  public function loansShouldContainsDrHouse() {
     Storm_Cache::beVolatile();
 
     $cards = new Class_User_Cards(Class_Users::getIdentity());
 
-    $loans = $cards->getLoansWithOutPNB([]);
+    $loans = $cards->getPNBLoans([]);
 
     $loans = array_map(function($loan)
                        {
-                         return (new Intonation_Library_View_Wrapper_Loan)
+                         return (new Intonation_Library_View_Wrapper_PNBLoan)
                            ->setModel($loan)
                            ->setView($this->view);
                        }, $loans->getArrayCopy());
@@ -3344,8 +3381,8 @@ class TemplatesDispatchAbonneLoansTest extends TemplatesIntonationAccountTestCas
 
     $id = $helper->getId();
 
-    $this->dispatch('/opac/index/ajax-paginated-list/id/' . $id . '/page/1/id_profil/72/search/potter/size/10');
-    $this->assertXPathContentContains('//span', '1');
+    $this->dispatch('/opac/index/ajax-paginated-list/id/' . $id . '/page/1/id_profil/72/search/house/size/10');
+    $this->assertXPathContentContains('//div//a', 'Dr House');
   }
 }
 
@@ -3425,21 +3462,76 @@ class TemplatesDispatchAbonneSuivreUneRechercheTest extends TemplatesIntonationA
 
 
 class TemplatesIntonationDispatchAccountEditTest extends TemplatesIntonationAccountTestCase {
+  /** @test */
+  public function editAccountShouldBePresent() {
+    $this->dispatch('/opac/abonne/fiche/id_profil/72');
+    $this->assertXPath('//a[contains(@href, "/abonne/modifier/id_profil/72")]');
+  }
+
+
+  /** @test */
+  public function editPasswordShouldBePresent() {
+    $this->dispatch('/opac/abonne/fiche/id_profil/72');
+    $this->assertXPath('//a[contains(@href, "/abonne/changer-mon-mot-de-passe/id_profil/72")]');
+  }
+
+
   /** @test */
   public function editAccountShouldContainsFormToAbonneModifier() {
     $this->dispatch('/opac/abonne/modifier/id_profil/72');
-    $this->assertXPath('//form[contains(@action, "/abonne/modifier")]');
+    $this->assertXPath('//form[contains(@action, "/abonne/modifier/id_profil/72")]');
+  }
+
+
+  /** @test */
+  public function editPasswordShouldContainsFormToAbonneChangerMonMotDePasse() {
+    $this->dispatch('/opac/abonne/changer-mon-mot-de-passe/id_profil/72');
+    $this->assertXPath('//form[contains(@action, "/abonne/changer-mon-mot-de-passe/id_profil/72")]');
+  }
+
+
+  /** @test */
+  public function editPasswordShouldContainsLinkToAuthLostpass() {
+    $this->dispatch('/opac/abonne/changer-mon-mot-de-passe/id_profil/72');
+    $this->assertXPath('//div//a[contains(@href, "/auth/lostpass/id_profil/72")]');
   }
 
 
   /** @test */
   public function editAccountPostNameDevShouldUpdateUser() {
     Class_AdminVar::newInstanceWithId('CHAMPS_FICHE_UTILISATEUR',
-                                      ['valeur' => 'nom']);
+                                      ['valeur' => 'pseudo;nom;password']);
 
     $this->postDispatch('/opac/abonne/modifier/id_profil/72', ['nom' => 'Dev']);
     $this->assertEquals('Dev', Class_Users::getIdentity()->getNom());
   }
+
+
+  /** @test */
+  public function changePasswordPost123ShouldRenderToSmallPassword() {
+    $this->postDispatch('/opac/abonne/changer-mon-mot-de-passe/id_profil/72', ['current_password' => 'test',
+                                                                               'new_password' => '123',
+                                                                               'confirm_new_password' => '123']);
+    $this->assertXPathContentContains('//div', 'contient moins de 4 caract');
+  }
+
+
+  /** @test */
+  public function changePasswordPost123456ShouldUpdatePasswordUser() {
+    $this->postDispatch('/opac/abonne/changer-mon-mot-de-passe/id_profil/72', ['current_password' => 'test',
+                                                                               'new_password' => '123456',
+                                                                               'confirm_new_password' => '123456']);
+    $this->assertEquals('123456', Class_Users::getIdentity()->getPassword());
+  }
+
+
+  /** @test */
+  public function changePasswordPostEmptyFieldsShouldRenderEmptyPassword() {
+    $this->postDispatch('/opac/abonne/changer-mon-mot-de-passe/id_profil/72', ['current_password' => '',
+                                                                               'new_password' => '',
+                                                                               'confirm_new_password' => '']);
+    $this->assertXPathContentContains('//div', 'Une valeur est requise');
+  }
 }
 
 
@@ -4057,7 +4149,7 @@ class TemplatesDispatchEditFreeWidgetTest extends TemplatesIntonationTestCase {
 class TemplatesDispatchIntonationWithFreeTest extends TemplatesIntonationTestCase {
 
   /** @test */
-  public function facebookImageShouldBePresent() {
+  public function boiteLibreShouldBePresent() {
     $this->dispatch('/opac/index/index/id_profil/72', true);
     $this->assertXPathContentContains('//div', 'Boite libre');
   }
-- 
GitLab