From 506c513fe5d76b50e7093e2e637883d856601d51 Mon Sep 17 00:00:00 2001
From: Patrick Barroca <pbarroca@afi-sa.fr>
Date: Fri, 8 Feb 2019 10:19:07 +0100
Subject: [PATCH] dev #79945 : openings duplicate and closure UI

---
 FEATURES/79945                                |  10 +
 VERSIONS_WIP/79945                            |   1 +
 library/Class/Ouverture.php                   |  38 +++-
 library/Class/Ouverture/Visitor.php           |   2 +-
 library/Class/TableDescription/Openings.php   |  99 +++++++++
 .../TableDescription/OpeningsLabelled.php     |  28 +++
 .../Controller/Plugin/Manager/Opening.php     |  29 ++-
 library/ZendAfi/Form/Admin/Ouverture.php      |  56 ++++-
 .../ZendAfi/View/Helper/LibraryOpenings.php   |   2 +-
 .../View/Helper/LibraryOpeningsAdmin.php      |  43 ++--
 .../View/Helper/RenderLibraryOpening.php      |   5 +-
 .../controllers/OuverturesControllerTest.php  | 201 +++++++++++++++++-
 .../View/Helper/RenderLibraryOpeningTest.php  | 106 +++++----
 13 files changed, 520 insertions(+), 100 deletions(-)
 create mode 100644 FEATURES/79945
 create mode 100644 VERSIONS_WIP/79945
 create mode 100644 library/Class/TableDescription/Openings.php
 create mode 100644 library/Class/TableDescription/OpeningsLabelled.php

diff --git a/FEATURES/79945 b/FEATURES/79945
new file mode 100644
index 00000000000..7827d4baef7
--- /dev/null
+++ b/FEATURES/79945
@@ -0,0 +1,10 @@
+        '79945' =>
+            ['Label' => $this->_('Gestion des ouvertures des bibliothèques'),
+             'Desc' => $this->_('Bokeh permet de définir des ouvertures / fermetures habituelles et limitées sur une période donnée.'),
+             'Image' => '',
+             'Video' => 'https://youtu.be/NWwEuxX7BpE',
+             'Category' => $this->_('Administration'),
+             'Right' => function($feature_description, $user) {return true;},
+             'Wiki' => 'http://wiki.bokeh-library-portal.org/index.php?title=Ouvertures_des_biblioth%C3%A8ques',
+             'Test' => '',
+             'Date' => '2019-02-07'],
\ No newline at end of file
diff --git a/VERSIONS_WIP/79945 b/VERSIONS_WIP/79945
new file mode 100644
index 00000000000..49509ce7012
--- /dev/null
+++ b/VERSIONS_WIP/79945
@@ -0,0 +1 @@
+ - ticket #79945 : Administration : Amélioration de la gestion des ouvertures de bibliothèques
\ No newline at end of file
diff --git a/library/Class/Ouverture.php b/library/Class/Ouverture.php
index 0bc1de2da90..f82aad082b2 100644
--- a/library/Class/Ouverture.php
+++ b/library/Class/Ouverture.php
@@ -21,7 +21,7 @@
 
 class OuvertureLoader extends Storm_Model_Loader {
   use Trait_Translator;
-
+  const CLOSED_VALUE = '00:00';
 
   public function compare($a, $b) {
     if ($a->getJourSemaine() && $b->getJourSemaine() && $a->getJourSemaine() > $b->getJourSemaine())
@@ -46,6 +46,11 @@ class OuvertureLoader extends Storm_Model_Loader {
   }
 
 
+  public function getClosedValue() {
+    return static::CLOSED_VALUE;
+  }
+
+
   public function getDays() {
     return [$this->_('Aucun'),
             $this->_('Lundi'),
@@ -58,6 +63,11 @@ class OuvertureLoader extends Storm_Model_Loader {
   }
 
 
+  public function isValueClosed($value) {
+    return $value == static::CLOSED_VALUE;
+  }
+
+
   public function humanDate($date) {
     return Class_Date::getHumanDate($date, 'dd/MM/yyyy');
   }
@@ -66,6 +76,12 @@ class OuvertureLoader extends Storm_Model_Loader {
   public function humanDateToDate($date) {
     return Class_Date::getHumanDate($date, 'yyyy-MM-dd');
   }
+
+
+  public function newClosedOn($day) {
+    return Class_Ouverture::newInstance(['horaires' => array_fill(0, 4, static::CLOSED_VALUE),
+                                         'jour' => $day]);
+  }
 }
 
 
@@ -249,14 +265,14 @@ class Class_Ouverture extends Storm_Model_Abstract {
 
   public function isExceptionalOpenOnDate($date) {
     return $this->isExceptional()
-      && (!$this->isClosure())
+      && (!$this->isClosed())
       && (date('Y-m-d', $date) == $this->getJour());
   }
 
 
   public function isExceptionalClosedOnDate($date) {
     return $this->isExceptional()
-      && $this->isClosure()
+      && $this->isClosed()
       && (date('Y-m-d', $date) == $this->getJour());
   }
 
@@ -279,7 +295,7 @@ class Class_Ouverture extends Storm_Model_Abstract {
     if (0 == $day_of_week = (int)date('w', $date))
       $day_of_week = 7;
 
-    return !$this->isClosure() && $this->getJourSemaine() == $day_of_week;
+    return !$this->isClosed() && $this->getJourSemaine() == $day_of_week;
   }
 
 
@@ -287,7 +303,7 @@ class Class_Ouverture extends Storm_Model_Abstract {
     if (0 == $day_of_week = (int)date('w', $date))
       $day_of_week = 7;
 
-    return $this->isClosure() && $this->getJourSemaine() == $day_of_week;
+    return $this->isClosed() && $this->getJourSemaine() == $day_of_week;
   }
 
 
@@ -307,11 +323,19 @@ class Class_Ouverture extends Storm_Model_Abstract {
   }
 
 
-  public function isClosure() {
+  public function isClosed() {
     foreach(['debut_matin', 'fin_matin', 'debut_apres_midi', 'fin_apres_midi'] as $field)
-      if ('00:00' != $this->callGetterByAttributeName($field))
+      if (!$this->getLoader()->isValueClosed($this->callGetterByAttributeName($field)))
         return false;
 
     return true;
   }
+
+
+  public function copy() {
+    $attributes = $this->_attributes;
+    unset($attributes['id']);
+
+    return (new static())->updateAttributes($attributes);
+  }
 }
\ No newline at end of file
diff --git a/library/Class/Ouverture/Visitor.php b/library/Class/Ouverture/Visitor.php
index 7c0d9eed46e..237c6f4cd21 100644
--- a/library/Class/Ouverture/Visitor.php
+++ b/library/Class/Ouverture/Visitor.php
@@ -59,7 +59,7 @@ class Class_Ouverture_Visitor {
     if (!$opening->hasValidityRange() && $opening->isRecurrent())
       return $this->_addDefault($opening);
 
-    if ($opening->isExceptional() && $opening->isClosure())
+    if ($opening->isExceptional() && $opening->isClosed())
       return $this->_addClosure($opening);
 
     if ($opening->isExceptional())
diff --git a/library/Class/TableDescription/Openings.php b/library/Class/TableDescription/Openings.php
new file mode 100644
index 00000000000..9dd982a22ec
--- /dev/null
+++ b/library/Class/TableDescription/Openings.php
@@ -0,0 +1,99 @@
+<?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 Class_TableDescription_Openings extends Class_TableDescription {
+  protected
+    $_view,
+    $_time_segment_callback;
+
+
+  public function init() {
+    $this
+      ->addColumn($this->_('Jour'),
+                  function($model) { return $this->_humanDateRange($model); })
+
+      ->addColumn($this->_('Matinée'),
+                  function($model)
+                  {
+                    return $this->_timeSegment($model->getDebutMatin(), $model->getFinMatin());
+                  })
+
+      ->addColumn($this->_('Après-midi'),
+                  function($model)
+                  {
+                    return $this->_timeSegment($model->getDebutApresMidi(), $model->getFinApresMidi());
+                  })
+
+      ->addRowAction(['url' => ['module' => 'admin',
+                                'controller' => 'ouvertures',
+                                'action' => 'edit',
+                                'id' => '%s'],
+                      'label' => $this->_('Modifier'),
+                      'icon' => 'edit'])
+
+      ->addRowAction(['url' => ['module' => 'admin',
+                                'controller' => 'ouvertures',
+                                'action' => 'duplicate',
+                                'id' => '%s'],
+                      'label' => $this->_('Dupliquer'),
+                      'icon' => 'copy',
+                      'anchorOptions' => ['onclick' => 'javascript:return confirm(\'' . $this->_('Êtes vous sûr de vouloir dupliquer cet élément ?') . '\');']])
+
+      ->addRowAction(['url' => ['module' => 'admin',
+                                'controller' => 'ouvertures',
+                                'action' => 'delete',
+                                'id' => '%s'],
+                      'label' => $this->_('Supprimer'),
+                      'icon' => 'delete',
+                      'anchorOptions' => ['onclick' => 'javascript:return confirm(\'' . $this->_('Êtes vous sûr de vouloir supprimer cet élément ?') . '\');']])
+      ;
+  }
+
+
+  public function setView($view) {
+    $this->_view = $view;
+    return $this;
+  }
+
+
+  public function setTimeSegmentCallback($callback) {
+    $this->_time_segment_callback = $callback;
+    return $this;
+  }
+
+
+  protected function _humanDateRange($model) {
+    return $this->_view
+      ? $this->_view->openingHumanDateRange($model)
+      : '';
+  }
+
+
+  protected function _timeSegment($start, $end) {
+    if (!isset($this->_time_segment_callback))
+      return '';
+
+    return ($segment = call_user_func($this->_time_segment_callback, $start, $end))
+      ? $segment
+      : $this->_('Fermé');
+  }
+}
diff --git a/library/Class/TableDescription/OpeningsLabelled.php b/library/Class/TableDescription/OpeningsLabelled.php
new file mode 100644
index 00000000000..2cb18e9c55d
--- /dev/null
+++ b/library/Class/TableDescription/OpeningsLabelled.php
@@ -0,0 +1,28 @@
+<?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 Class_TableDescription_OpeningsLabelled extends Class_TableDescription_Openings {
+  public function init() {
+    $this->addColumn($this->_('Libellé'), 'label');
+    parent::init();
+  }
+}
diff --git a/library/ZendAfi/Controller/Plugin/Manager/Opening.php b/library/ZendAfi/Controller/Plugin/Manager/Opening.php
index 58960dfd139..d599b3760ff 100644
--- a/library/ZendAfi/Controller/Plugin/Manager/Opening.php
+++ b/library/ZendAfi/Controller/Plugin/Manager/Opening.php
@@ -35,6 +35,13 @@ class ZendAfi_Controller_Plugin_Manager_Opening extends ZendAfi_Controller_Plugi
       ? $this->_getSQLDateFrom($post['validity_end'])
       : '';
 
+    if (isset($post['closed_am']) && $post['closed_am'] == 1)
+      $post['debut_matin'] = $post['fin_matin'] = Class_Ouverture::getClosedValue();
+
+    if (isset($post['closed_pm']) && $post['closed_pm'] == 1)
+      $post['debut_apres_midi'] = $post['fin_apres_midi'] = Class_Ouverture::getClosedValue();
+
+    unset($post['closed_am'], $post['closed_pm']);
     return $post;
   }
 
@@ -46,6 +53,27 @@ class ZendAfi_Controller_Plugin_Manager_Opening extends ZendAfi_Controller_Plugi
   }
 
 
+  public function duplicateAction() {
+    if (!$this->_canAdd()) {
+      $this->_helper->notify($this->_view->_('Vous n\'avez pas la permission "%s"',
+                                             $this->_getAddActionTitle()));
+      $this->_redirectToIndex();
+      return;
+    }
+
+    if (!$model = $this->_findModel()) {
+      $this->_helper
+        ->notify($this->_view->_('Impossible de dupliquer, la plage d\'ouverture est introuvable'));
+      $this->_redirectToIndex();
+      return;
+    }
+
+    $model->copy()->save();
+    $this->_helper->notify($this->_view->_('Plage d\'ouverture dupliquée'));
+    $this->_redirectToIndex();
+  }
+
+
   protected function _doBeforeSave($model) {
     return $model->setIdSite($this->_getParam('id_site', 0));
   }
@@ -66,4 +94,3 @@ class ZendAfi_Controller_Plugin_Manager_Opening extends ZendAfi_Controller_Plugi
     return $this;
   }
 }
-?>
\ No newline at end of file
diff --git a/library/ZendAfi/Form/Admin/Ouverture.php b/library/ZendAfi/Form/Admin/Ouverture.php
index 91b780850d3..88ccaad739d 100644
--- a/library/ZendAfi/Form/Admin/Ouverture.php
+++ b/library/ZendAfi/Form/Admin/Ouverture.php
@@ -26,7 +26,11 @@ class ZendAfi_Form_Admin_Ouverture extends ZendAfi_Form {
 
     Class_ScriptLoader::getInstance()
       ->addJqueryReady('formSelectToggleVisibilityForElement("#jour_semaine", "#fieldset-plage_ouverture tr:nth-child(3)", ["0"]);'
-                       . 'formSelectToggleVisibilityForElement("#jour_semaine", "#fieldset-plage_ouverture tr:nth-child(4)", ["1", "2", "3", "4", "5", "6", "7"]);');
+                       . 'formSelectToggleVisibilityForElement("#jour_semaine", "#fieldset-plage_ouverture tr:nth-child(4)", ["1", "2", "3", "4", "5", "6", "7"]);'
+                       . 'checkBoxToggleVisibilityForElement("#closed_am", $("#fieldset-plage_ouverture select[name$=\'_matin\']").parents("tr"), false);'
+                       . 'checkBoxToggleVisibilityForElement("#closed_pm", $("#fieldset-plage_ouverture select[name$=\'_apres_midi\']").parents("tr"), false);');
+
+    $possible_hours = Class_Multimedia_Location::getPossibleHours(5);
 
     $this
       ->addElement('text',
@@ -48,18 +52,33 @@ class ZendAfi_Form_Admin_Ouverture extends ZendAfi_Form {
                    'validity_range',
                    ['label' => $this->_('Restreindre la période d\'ouverture'),
                     'start' => ['name' => 'validity_start'],
-                    'end' => ['name' => 'validity_end']]);
+                    'end' => ['name' => 'validity_end']])
+
+      ->addElement('select',
+                   'debut_matin',
+                   ['label' => $this->_('Début matinée'),
+                    'multiOptions' => $possible_hours])
+
+      ->addElement('select',
+                   'fin_matin',
+                   ['label' => $this->_('Fin matinée'),
+                    'multiOptions' => $possible_hours])
+
+      ->addElement('checkbox', 'closed_am', ['label' => $this->_('Fermé matinée')])
 
-    $field_labels = ['debut_matin' => $this->_('Début matinée'),
-                     'fin_matin' => $this->_('Fin matinée'),
-                     'debut_apres_midi' =>  $this->_('Début après-midi'),
-                     'fin_apres_midi' => $this->_('Fin après-midi')];
+      ->addElement('select',
+                   'debut_apres_midi',
+                   ['label' => $this->_('Début après-midi'),
+                    'multiOptions' => $possible_hours])
 
-    foreach ($field_labels as $field => $label)
-      $this->addElement('select',
-                        $field,
-                        ['label' => $label,
-                         'multiOptions' => Class_Multimedia_Location::getPossibleHours(5)]);
+      ->addElement('select',
+                   'fin_apres_midi',
+                   ['label' => $this->_('Fin après-midi'),
+                    'multiOptions' => $possible_hours])
+
+      ->addElement('checkbox', 'closed_pm', ['label' => $this->_('Fermé après-midi')])
+
+      ;
 
     $this->addDisplayGroup($this->getElementsNames(),
                            'plage_ouverture',
@@ -90,6 +109,21 @@ class ZendAfi_Form_Admin_Ouverture extends ZendAfi_Form {
     $this->getElement('validity_range')
          ->setStartValue(isset($values['validity_start']) ? $values['validity_start'] : '')
          ->setEndValue(isset($values['validity_end']) ? $values['validity_end'] : '');
+
+
+    if (!isset($values['debut_matin']))
+      return parent::populate($values);
+
+    if ( Class_Ouverture::isValueClosed($values['debut_matin']) &&
+        Class_Ouverture::isValueClosed($values['fin_matin']) )
+      $this->getElement('closed_am')
+           ->setChecked(true);
+
+    if ( Class_Ouverture::isValueClosed($values['debut_apres_midi']) &&
+        Class_Ouverture::isValueClosed($values['fin_apres_midi']) )
+      $this->getElement('closed_pm')
+           ->setChecked(true);
+
     return parent::populate($values);
   }
 }
diff --git a/library/ZendAfi/View/Helper/LibraryOpenings.php b/library/ZendAfi/View/Helper/LibraryOpenings.php
index 93aeee6f3fe..3b9cb8bbff8 100644
--- a/library/ZendAfi/View/Helper/LibraryOpenings.php
+++ b/library/ZendAfi/View/Helper/LibraryOpenings.php
@@ -98,7 +98,7 @@ class ZendAfi_View_Helper_LibraryOpenings extends ZendAfi_View_Helper_BaseHelper
 
     foreach($openings as $opening) {
       $label = (!$label) ? $opening->getLabel() : $label;
-      $only_closures = $only_closures && $opening->isClosure();
+      $only_closures = $only_closures && $opening->isClosed();
       $content .= $this->_tag('li', $this->_renderOne($opening));
     }
 
diff --git a/library/ZendAfi/View/Helper/LibraryOpeningsAdmin.php b/library/ZendAfi/View/Helper/LibraryOpeningsAdmin.php
index 1dc9fd2f8e5..342c62553d1 100644
--- a/library/ZendAfi/View/Helper/LibraryOpeningsAdmin.php
+++ b/library/ZendAfi/View/Helper/LibraryOpeningsAdmin.php
@@ -72,14 +72,17 @@ class ZendAfi_View_Helper_LibraryOpeningsAdmin
     foreach($openings as $opening)
       $label = (!$label) ? $opening->getLabel() : $label;
 
+    $description = new Class_TableDescription_Openings('ouvertures');
     return $this->_renderAdminSection($closure($label),
-                                      $this->_renderTagModelTable($openings));
+                                      $this->_renderTagModelTable($openings, $description));
   }
 
 
   protected function _renderSectionLabelled($label, $openings) {
+    $description = (new Class_TableDescription_OpeningsLabelled('ouvertures'));
+
     return $this->_renderAdminSection($label,
-                                      $this->_renderTagModelTable($openings, [$this->_('Libellé')], ['label']));
+                                      $this->_renderTagModelTable($openings, $description));
   }
 
 
@@ -89,31 +92,15 @@ class ZendAfi_View_Helper_LibraryOpeningsAdmin
   }
 
 
-  protected function _renderTagModelTable($openings, $columns_labels = [], $columns_datas = []) {
-    $columns_labels = array_merge($columns_labels,
-                                  [$this->_('Jour'),
-                                   $this->_('Matin'),
-                                   '',
-                                   $this->_('Après-midi'),
-                                   '']);
-
-    $columns_datas = array_merge($columns_datas,
-                                 ['formatted_jour',
-                                  'debut_matin',
-                                  'fin_matin',
-                                  'debut_apres_midi',
-                                  'fin_apres_midi']);
-
-    return $this->view
-      ->tagModelTable($openings,
-                      $columns_labels,
-                      $columns_datas,
-                      [ ['action' => 'edit', 'content' => $this->view->boutonIco('type=edit')],
-                       ['action' => 'delete', 'content' => $this->view->boutonIco('type=del')] ],
-                      'ouvertures',
-                      null,
-                      ['formatted_jour' => function($opening) {
-                          return $this->view->openingHumanDateRange($opening);
-                        }]);
+  /**
+   * @param $openings collection
+   * @param $description Class_TableDescription
+   */
+  protected function _renderTagModelTable($openings, $description) {
+    $description
+      ->setView($this->view)
+      ->setTimeSegmentCallback(function($start, $end) { return $this->_renderTimeSegment($start, $end); });
+
+    return $this->view->renderTable($description, $openings);
   }
 }
\ No newline at end of file
diff --git a/library/ZendAfi/View/Helper/RenderLibraryOpening.php b/library/ZendAfi/View/Helper/RenderLibraryOpening.php
index daa2115a8fc..01622651cbf 100644
--- a/library/ZendAfi/View/Helper/RenderLibraryOpening.php
+++ b/library/ZendAfi/View/Helper/RenderLibraryOpening.php
@@ -33,7 +33,8 @@ class ZendAfi_View_Helper_RenderLibraryOpening extends ZendAfi_View_Helper_BaseH
 
 
   public function renderOuverturesForLibrary($library) {
-    if (!$ouverture = $this->getLibraryOuverture($library))
+    if ((!$ouverture = $this->getLibraryOuverture($library))
+        || $ouverture->isClosed())
       return $this->renderClosed($this->_('Fermé.')
                                  . $this->renderNextOuvertureForLibrary($library));
 
@@ -95,7 +96,7 @@ class ZendAfi_View_Helper_RenderLibraryOpening extends ZendAfi_View_Helper_BaseH
       $time = strtotime('+'.$i.' day', $this->getCurrentTime());
 
       if (($ouverture = $this->getLibraryOuvertureOnDate($library, $time))
-          && !$ouverture->isClosure())
+          && !$ouverture->isClosed())
         break;
     }
 
diff --git a/tests/application/modules/admin/controllers/OuverturesControllerTest.php b/tests/application/modules/admin/controllers/OuverturesControllerTest.php
index 586250c07cc..a9cfa65dc5c 100644
--- a/tests/application/modules/admin/controllers/OuverturesControllerTest.php
+++ b/tests/application/modules/admin/controllers/OuverturesControllerTest.php
@@ -126,10 +126,8 @@ class OuverturesControllerIndexActionSiteCranTest extends OuverturesControllerTe
 
   /** @test */
   public function ouvertureHoursShouldBeVisible() {
-    $this->assertXPathContentContains('//td', '08:00');
-    $this->assertXPathContentContains('//td', '12:00');
-    $this->assertXPathContentContains('//td', '13:30');
-    $this->assertXPathContentContains('//td', '17:00');
+    $this->assertXPathContentContains('//td', '08h00 - 12h00');
+    $this->assertXPathContentContains('//td', '13h30 - 17h00');
   }
 
 
@@ -370,18 +368,70 @@ class OuverturesControllerIndexActionSiteAnnecyTest extends OuverturesController
   }
 
 
+  /** @test */
+  public function pageShouldContainsButtonToCreateOuverture() {
+    $this->assertXPathContentContains('//button[contains(@data-url, "/ouvertures/add/id_site/3")]',
+                                      'Ajouter une plage d\'ouverture');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsButtonReturnToLibrariesIndex() {
+    $this->assertXPathContentContains('//button[contains(@data-url, "/bib")]',
+                                      'Retour à la liste des bibliothèques');
+  }
+
+
   /** @test */
   public function ouvertureAtHeightHalfShouldBeVisible() {
-    $this->assertXPathContentContains('//tbody//td[3]', '08:30');
-    $this->assertXPathContentContains('//tbody//td[4]', '12:00');
-    $this->assertXPathContentContains('//tbody//td[5]', '12:00');
-    $this->assertXPathContentContains('//tbody//td[6]', '17:00');
+    $this->assertXPathContentContains('//tbody//td', '08h30 - 12h00');
+    $this->assertXPathContentContains('//tbody//td', '12h00 - 17h00');
+  }
+
+
+  /** @test */
+  public function editActionShouldBePresent() {
+    $this->assertXPath('//a[contains(@href, "/ouvertures/edit/id_site/3/id/45")]');
+  }
+
+
+  /** @test */
+  public function deleteActionShouldBePresent() {
+    $this->assertXPath('//a[contains(@href, "/ouvertures/delete/id_site/3/id/45")]');
   }
 
 
-  /** @disabledtest */
-  function pageShouldContainsButtonToCreateOuverture() {
-    $this->assertXPathContentContains('//div[contains(@onclick, "ouvertures/add/id_site/3")]//td', 'Ajouter une plage d\'ouverture');
+  /** @test */
+  public function duplicateActionShouldBePresent() {
+    $this->assertXPath('//a[contains(@href, "/ouvertures/duplicate/id_site/3/id/45")]');
+  }
+}
+
+
+
+
+class OuverturesControllerDuplicateActionTest extends OuverturesControllerTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->dispatch('/admin/ouvertures/duplicate/id_site/3/id/45');
+  }
+
+
+  /** @test */
+  public function shouldRedirectToIndexOfAnnecyOpenings() {
+    $this->assertRedirectTo('/admin/ouvertures/index/id_site/3');
+  }
+
+
+  /** @test */
+  public function shouldNotifyDuplicationSuccess() {
+    $this->assertFlashMessengerContentContains('Plage d\'ouverture dupliquée');
+  }
+
+
+  /** @test */
+  public function annecyShouldHaveTwoOpenings() {
+    $this->assertEquals(2, Class_Ouverture::countBy(['id_site' => 3]));
   }
 }
 
@@ -513,6 +563,12 @@ class OuverturesControllerAddOuvertureCranTest extends OuverturesControllerTestC
   }
 
 
+  /** @test */
+  public function formShouldContainsCheckboxForClosedAm() {
+    $this->assertXPath('//form//input[@type="checkbox"][@name="closed_am"]');
+  }
+
+
   /** @test */
   public function titleShouldBeCranGevrierAjouteUnePlageDouverture() {
     $this->assertXPathContentContains('//h1', 'Cran-Gévrier : ajouter une plage d\'ouverture');
@@ -587,8 +643,10 @@ class OuverturesControllerPostAddOuvertureCranTest extends OuverturesControllerT
     parent::setUp();
     $this->postDispatch('/admin/ouvertures/add/id_site/3',  ['debut_matin' => '10:30',
                                                              'fin_matin' => '11:30',
+                                                             'closed_am' => 0,
                                                              'debut_apres_midi' => '14:00',
                                                              'fin_apres_midi' => '15:00',
+                                                             'closed_pm' => 0,
                                                              'jour_semaine' => Class_Ouverture::MARDI,
                                                              'jour' => '23/10/2012',
                                                              'validity_start' => '00/00/0000',
@@ -837,4 +895,125 @@ class OuverturesControllerPostValidityRangeErrorsTest extends OuverturesControll
     $this->assertXPathContentContains('//ul[@class="errors"]/li',
                                       'La date de début doit être plus récente que la date de fin');
   }
+}
+
+
+
+
+class OuverturesControllerPostClosedAMTest extends OuverturesControllerTestCase {
+  protected $_new_ouverture;
+
+  public function setUp() {
+    parent::setUp();
+    $this->postDispatch('/admin/ouvertures/add/id_site/3',  ['debut_matin' => '10:30',
+                                                             'fin_matin' => '11:30',
+                                                             'closed_am' => 1,
+                                                             'debut_apres_midi' => '14:00',
+                                                             'fin_apres_midi' => '15:00',
+                                                             'closed_pm' => 1,
+                                                             'jour_semaine' => Class_Ouverture::MARDI,
+                                                             'jour' => '23/10/2012',
+                                                             'validity_start' => '00/00/0000',
+                                                             'validity_end' => '']);
+    $this->_new_ouverture = Class_Ouverture::find(46);
+  }
+
+
+  /** @test */
+  public function debutMatinShouldBeSetZeroified() {
+    $this->assertEquals('00:00', Class_Ouverture::find(46)->getDebutMatin());
+  }
+
+
+  /** @test */
+  public function finMatinShouldBeSetZeroified() {
+    $this->assertEquals('00:00', Class_Ouverture::find(46)->getFinMatin());
+  }
+
+
+  /** @test */
+  public function debutApresMidiShouldBeSetZeroified() {
+    $this->assertEquals('00:00', Class_Ouverture::find(46)->getDebutApresMidi());
+  }
+
+
+  /** @test */
+  public function finApresMidiShouldBeSetZeroified() {
+    $this->assertEquals('00:00', Class_Ouverture::find(46)->getFinApresMidi());
+  }
+
+
+  /**
+   * @test
+   * @expectedException  Storm_Model_Exception
+   */
+  public function closedAMShouldNotStayInAttributes() {
+    Class_Ouverture::find(46)->hasClosedAm();
+  }
+
+
+  /**
+   * @test
+   * @expectedException  Storm_Model_Exception
+   */
+  public function closedPMShouldNotStayInAttributes() {
+    Class_Ouverture::find(46)->hasClosedPm();
+  }
+}
+
+
+
+class OuverturesControllerEditActionWithClosedAMTest extends OuverturesControllerTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->fixture('Class_Ouverture',
+                   ['id' => 456465,
+                    'validity_start' => '2017-12-01',
+                    'validity_end' => '2018-01-01',
+                    'debut_matin' => '00:00',
+                    'fin_matin' => '00:00',
+                    'debut_apres_midi' => '00:00',
+                    'fin_apres_midi' => '00:00',
+                    'label' => 'Hiver']);
+
+    $this->dispatch('/admin/ouvertures/edit/id/456465/id_site/3', true);
+  }
+
+  /** @test */
+  public function closedAMShouldBeChecked() {
+    $this->assertXPath('//input[@type="checkbox"][@name="closed_am"][@checked]');
+  }
+
+
+  /** @test */
+  public function closedPMShouldBeChecked() {
+    $this->assertXPath('//input[@type="checkbox"][@name="closed_pm"][@checked]');
+  }
+}
+
+
+
+
+class OuverturesControllerIndexActionWithClosedAMTest extends OuverturesControllerTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->fixture('Class_Ouverture',
+                   ['id' => 456465,
+                    'id_site' => 3,
+                    'validity_start' => '2017-12-01',
+                    'validity_end' => '2018-01-01',
+                    'debut_matin' => '00:00',
+                    'fin_matin' => '00:00',
+                    'debut_apres_midi' => '00:00',
+                    'fin_apres_midi' => '00:00',
+                    'label' => 'Hiver']);
+
+    $this->dispatch('/admin/ouvertures/index/id_site/3', true);
+  }
+
+
+  /** @test */
+  public function pageShouldContainsTwoClosedLabels() {
+    $this->assertXPathCount('//td[text()="Fermé"]', 2);
+  }
 }
\ No newline at end of file
diff --git a/tests/library/ZendAfi/View/Helper/RenderLibraryOpeningTest.php b/tests/library/ZendAfi/View/Helper/RenderLibraryOpeningTest.php
index bf9195a6ddd..9a192100f49 100644
--- a/tests/library/ZendAfi/View/Helper/RenderLibraryOpeningTest.php
+++ b/tests/library/ZendAfi/View/Helper/RenderLibraryOpeningTest.php
@@ -57,7 +57,7 @@ abstract class ZendAfi_View_Helper_RenderLibraryOpeningTestCase extends ViewHelp
 
   protected function _renderOpening($library) {
     $helper = new ZendAfi_View_Helper_RenderLibraryOpening();
-    $helper->setView(new ZendAfi_Controller_Action_Helper_View());
+    $helper->setView($this->view);
 
     return $helper->renderLibraryOpening($library);
   }
@@ -276,7 +276,6 @@ class ZendAfi_View_Helper_RenderLibraryOpeningOnMondayEveningTest extends ZendAf
   }
 
 
-
   /** @test */
   public function annecyShouldContainsLibraryIsClosedSince18() {
     $this->assertXPathContentContains($this->_renderOpening($this->annecy),
@@ -294,17 +293,13 @@ class ZendAfi_View_Helper_RenderLibraryOpeningOnMondayEveningTest extends ZendAf
 
   /** @test */
   public function withExceptionalCloseOnTuesdayMeythetShouldContainsLibraryWillOpenOnMondayAt10() {
-    $this->meythet->addOuverture($this->fixture('Class_Ouverture',
-                                                          ['id' => 348,
-                                                           'horaires' => ['00:00', '00:00', '00:00', '00:00'],
-                                                           'jour' => '2015-10-06']))
+    $this->meythet->addOuverture(Class_Ouverture::newClosedOn('2015-10-06'))
                   ->save();
 
     $this->assertXPathContentContains($this->_renderOpening($this->meythet),
                                       '//p',
                                       utf8_encode('Fermé depuis 18:00. Réouverture Lundi à 10:00'));
   }
-
 }
 
 
@@ -362,38 +357,39 @@ class ZendAfi_View_Helper_RenderLibraryOpeningOnTuesdayMorningTest extends ZendA
 
 
 
-class ZendAfi_View_Helper_RenderLibraryOpeningsOnValidityRangeTest extends ZendAfi_View_Helper_RenderLibraryOpeningTestCase {
+class ZendAfi_View_Helper_RenderLibraryOpeningsOnValidityRangeTest
+  extends ZendAfi_View_Helper_RenderLibraryOpeningTestCase {
+
   public function setUp() {
     parent::setUp();
-    $this->cran->addOuverture($start_end = $this->fixture('Class_Ouverture',
-                                                          ['id' => 348,
-                                                           'horaires' => ['00:00', '00:00', '14:00', '18:00'],
-                                                           'jour' => '',
-                                                           'validity_start' => '2015-09-01',
-                                                           'validity_end' => '2015-12-29',
-                                                           'jour_semaine' => Class_Ouverture::MARDI]))
-
-               ->addOuverture($end_only = $this->fixture('Class_Ouverture',
-                                                         ['id' => 349,
-                                                          'horaires' => ['00:00', '00:00', '15:00', '18:00'],
-                                                          'jour' => '',
-                                                          'validity_end' => '2015-06-30',
-                                                          'jour_semaine' => Class_Ouverture::MARDI]))
-
-               ->addOuverture($start_only = $this->fixture('Class_Ouverture',
-                                                           ['id' => 351,
-                                                            'horaires' => ['11:00', '12:00', '13:00', '18:00'],
-                                                            'jour' => '',
-                                                            'validity_start' => '2015-01-01',
-                                                            'jour_semaine' => Class_Ouverture::MARDI]))
-
-               ->addOuverture($start_next_year = $this->fixture('Class_Ouverture',
-                                                                ['id' => 350,
-                                                                 'horaires' => ['00:00', '00:00', '13:00', '18:00'],
-                                                                 'jour' => '',
-                                                                 'validity_start' => '2016-01-01',
-                                                                 'jour_semaine' => Class_Ouverture::MARDI]))
-
+    $this->cran->addOuverture($this->fixture('Class_Ouverture',
+                                             ['id' => 348,
+                                              'horaires' => ['00:00', '00:00', '14:00', '18:00'],
+                                              'jour' => '',
+                                              'validity_start' => '2015-09-01',
+                                              'validity_end' => '2015-12-29',
+                                              'jour_semaine' => Class_Ouverture::MARDI]))
+
+               ->addOuverture($this->fixture('Class_Ouverture',
+                                             ['id' => 349,
+                                              'horaires' => ['00:00', '00:00', '15:00', '18:00'],
+                                              'jour' => '',
+                                              'validity_end' => '2015-06-30',
+                                              'jour_semaine' => Class_Ouverture::MARDI]))
+
+               ->addOuverture($this->fixture('Class_Ouverture',
+                                             ['id' => 351,
+                                              'horaires' => ['11:00', '12:00', '13:00', '18:00'],
+                                              'jour' => '',
+                                              'validity_start' => '2015-01-01',
+                                              'jour_semaine' => Class_Ouverture::MARDI]))
+
+               ->addOuverture($this->fixture('Class_Ouverture',
+                                             ['id' => 350,
+                                              'horaires' => ['00:00', '00:00', '13:00', '18:00'],
+                                              'jour' => '',
+                                              'validity_start' => '2016-01-01',
+                                              'jour_semaine' => Class_Ouverture::MARDI]))
 
                ->assertSave();
 
@@ -457,4 +453,38 @@ class ZendAfi_View_Helper_RenderLibraryOpeningsOnValidityRangeTest extends ZendA
                                       '//p',
                                       utf8_encode('Réouverture Mardi à 15:00'));
   }
-}
\ No newline at end of file
+}
+
+
+
+class ZendAfi_View_Helper_RenderLibraryOpeningsWithCurrentlyClosedTest
+  extends ZendAfi_View_Helper_RenderLibraryOpeningTestCase {
+
+  public function setUp() {
+    parent::setUp();
+    $this->meythet = $this
+      ->fixture('Class_Bib',
+                ['id' => 4,
+                 'libelle' => 'Meythet',
+                 'closed_on_holidays' => false,
+                 'ouvertures' => [
+                                  Class_Ouverture::chaqueLundi('00:00', '00:00', '00:00', '00:00'),
+                                  Class_Ouverture::chaqueMardi('00:00', '00:00', '14:30', '16:30'),
+                                  Class_Ouverture::chaqueVendredi('09:00', '10:00', '00:00', '00:00'),
+                                  Class_Ouverture::newClosedOn('2019-02-25'),
+                 ]]);
+
+    Class_Ouverture::clearCache();
+    Class_Bib::clearCache();
+  }
+
+
+  /** @test */
+  public function on29_12_2015_SelectedOpeningShouldBeStartEnd() {
+    ZendAfi_View_Helper_RenderLibraryOpening::setTimeSource(new TimeSourceForTest('2019-02-25 11:45:23'));
+
+    $this->assertXPathContentContains($this->_renderOpening($this->meythet),
+                                      '//p',
+                                      utf8_encode('Fermé. Réouverture Mardi à 14:30'));
+  }
+}
-- 
GitLab