Commit 3f18c55d authored by Patrick Barroca's avatar Patrick Barroca 🐧 Committed by Laurent

dev #109790 Implements Drive checkout of holds.

parent da546c47
Pipeline #10147 passed with stage
in 43 minutes and 43 seconds
'109790' =>
['Label' => $this->_('Retrait des documents sur rendez-vous (Drive)'),
'Desc' => $this->_('Les abonnés peuvent prendre rendez-vous pour retirer leurs documents à des horaires pré-définies'),
'Image' => '',
'Video' => '',
'Category' => $this->_('Circulation'),
'Right' => function($feature_description, $user) {return true;},
'Wiki' => 'http://wiki.bokeh-library-portal.org/index.php?title=Cat%C3%A9gorie:Drive',
'Test' => '',
'Date' => '2020-05-04'],
\ No newline at end of file
- ticket #109790 : Retrait des documents sur rendez-vous (Drive)
\ No newline at end of file
<?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 Admin_DriveCheckoutController extends ZendAfi_Controller_Action {
use Trait_TimeSource;
public function indexAction() {
$this->view->titre = $this->view->_('Drive : rendez-vous');
$this->view->libraries = Class_Bib::findAllBy(['enable_drive' => true,
'order' => 'libelle']);
$this->view->disabled_libraries = Class_Bib::findAllBy(['enable_drive' => false,
'order' => 'libelle']);
}
public function listAction() {
$this->view->library = Class_Bib::find($this->_getParam('id_bib'));
$this->view->date = $this->_getParam('date', $this->getCurrentDate());
$this->view->titre = $this->view->_('Drive : rendez-vous : %s, %s',
$this->view->library->getLibelle(),
strftime('%d %B %Y', strtotime($this->view->date)));
$this->view->checkouts = Class_DriveCheckout::findAllBy(['role' => 'library',
'model' => $this->view->library,
'left(start_at,10)' => $this->view->date,
'order' => 'start_at']);
$this->view->table_description = new Class_TableDescription_DriveCheckout_ListWithActions('checkouts');
}
public function deleteAction() {
$checkout = Class_DriveCheckout::find($this->_getParam('id'));
$checkout->delete();
$this->_helper->notify($this->_('Rendez-vous pour %s supprimé',
$checkout->getNomComplet()));
$this->_redirect(sprintf('/admin/drive-checkout/list/id_bib/%s/date/%s',
$checkout->getLibraryId(),
substr($checkout->getStartAt(), 0, 10)));
}
public function icalAction() {
if (!$checkout = Class_DriveCheckout::find($this->_getParam('id'))) {
$this->getHelper('ViewRenderer')->setNoRender();
$this->_helper->notify($this->_('Impossible d\'importer un retrait inconnu.'));
$this->_redirectToIndex();
return;
}
$content = (new Class_ICal_DriveCheckout($checkout))
->renderCalendar(Class_Profil::getCurrentProfil());
$this->_helper->ical('calendar.ics', $content);
}
public function listHoldsAction() {
$this->view->checkout = Class_DriveCheckout::find($this->_getParam('id'));
$this->view->titre = sprintf('%s, %s, %s, %s',
$this->view->checkout->getNomComplet(),
$this->view->checkout->getIdAbon(),
strftime('%d %B %H:%M',
strtotime($this->view->checkout->getStartAt())),
$this->view->checkout->getLibraryLabel());
}
public function listAllHoldsAction() {
$this->view->user = Class_Users::find($this->_getParam('id_user'));
foreach($this->view->user->getReservations() as $hold)
$hold->getExemplaireOPAC($this->view->user);//cache initialization
$this->view->titre = $this->_('Réservations pour %s, %s',
$this->view->user->getNomComplet(),
$this->view->user->getIdabon());
}
public function listCsvAction() {
$this->listAction();
$filename = implode(' ', [$this->view->date,
$this->view->library->getLibelle(),
$this->view->_('rendez-vous')]) . '.csv';
$this
->_helper
->csv($filename,
$this->view->renderCsv(new Class_TableDescription_DriveCheckout_List('checkouts'),
$this->view->checkouts));
}
public function itemsCsvAction() {
$library = Class_Bib::find($this->_getParam('id_bib'));
$date = $this->_getParam('date', $this->getCurrentDate());
$checkouts = Class_DriveCheckout::findAllBy(['role' => 'library',
'model' => $library,
'left(start_at,10)' => $date,
'order' => 'start_at']);
$holds = (new Storm_Collection($checkouts))
->injectInto(new Storm_Collection(),
function($holds, $checkout)
{
$holds->addAll($checkout->getLibraryHolds());
return $holds;
});
$filename = implode(' ', [$date,
$library->getLibelle(),
$this->view->_('documents')]) . '.csv';
$this
->_helper
->csv($filename,
$this->view->renderCsv(new Class_TableDescription_DriveCheckout_HoldsWithCheckouts('holds'),
$holds->getArrayCopy()));
}
public function planAction() {
$user = Class_Users::find($this->_getParam('id_user'));
$plan = new Class_DriveCheckout_Plan($this->_request->getParams(), $user);
$plan->doNotFilterLibraries();
if (!$plan->isValid()) {
$this->getHelper('ViewRenderer')->setNoRender();
$this->_helper->notify($plan->getLastMessage());
$this->_redirect($this->view->url($plan->fallbackUrl()));
return;
}
if ($this->_request->isPost()
&& ($checkout = $plan->persist())) {
$this->getHelper('ViewRenderer')->setNoRender();
$this->_helper->notify($this->view->_('Retrait planifié pour %s, le %s, %s',
$checkout->getNomComplet(),
strftime('%d %B %H:%M', strtotime($checkout->getStartAt())),
$checkout->getLibraryLabel()));
$this->_redirect(sprintf('/admin/drive-checkout/list/id_bib/%s/date/%s',
$checkout->getLibraryId(),
substr($checkout->getStartAt(), 0, 10)));
return;
}
$this->view->titre = $this->view->_('Planifier un retrait pour %s',
$user->getNomComplet());
$this->view->plan = $plan;
$this->view->user = $user;
}
}
......@@ -20,7 +20,7 @@
*/
class Admin_OuverturesController extends ZendAfi_Controller_Action {
protected $_library, $_is_multimedia = false;
protected $_library, $_is_used_for = null;
public function getPlugins() {
return ['ZendAfi_Controller_Plugin_ResourceDefinition_Opening',
......@@ -29,13 +29,15 @@ class Admin_OuverturesController extends ZendAfi_Controller_Action {
public function init() {
$this->_used_for = (int)$this->_getParam('used_for', null);
if ((!$this->_library = $this->_getLibrary())
|| ($this->_getParam('multimedia') && !Class_AdminVar::isMultimediaEnabled())) {
|| (($this->_used_for === Class_Ouverture::USED_FOR_MULTIMEDIA)
&& !Class_AdminVar::isMultimediaEnabled())) {
$this->_redirect('/admin/bib');
return;
}
$this->_is_multimedia = $this->_isMultimedia();
parent::init();
}
......@@ -45,11 +47,6 @@ class Admin_OuverturesController extends ZendAfi_Controller_Action {
}
protected function _isMultimedia() {
return null !== $this->_getParam('multimedia');
}
public function acceptVisitor($visitor) {
parent::acceptVisitor($visitor);
$visitor
......@@ -57,10 +54,10 @@ class Admin_OuverturesController extends ZendAfi_Controller_Action {
{
return $this->_getLibrary();
})
->visitIsMultimedia(function()
{
return $this->_isMultimedia();
});
->visitUsedFor(function()
{
return (int)$this->_getParam('used_for');
});
return $this;
}
......@@ -76,7 +73,7 @@ class Admin_OuverturesController extends ZendAfi_Controller_Action {
if ($this->_response->isRedirect())
return;
$this->view->multimedia = $this->_is_multimedia;
$this->view->used_for = $this->_used_for;
$this->view->model_name = 'library';
$this->view->library = $this->_library;
......
<?php
$render_libraries = function($libraries)
{
return $this->tagUlLi(
array_map(
function($library)
{
return $this->tagAnchor(['action' => 'list',
'id_bib' => $library->getId()],
$library->getLibelle());
},
$libraries));
};
echo $render_libraries($this->libraries);
echo $this->tag('h3', $this->_('Drive désactivé'));
echo $render_libraries($this->disabled_libraries);
?>
<?php
echo $this->renderTable((new Class_TableDescription('holds'))
->addColumn($this->_('Bibliothèque'), function($hold) { return $hold->getBibliotheque(); })
->addColumn($this->_('Code-barres'), function($hold) { return $hold->getCodeBarre(); })
->addColumn($this->_('Etat'), function($hold) { return $hold->getEtat(); })
->addColumn($this->_('Titre'), function($hold) { return $hold->getTitre(); }),
$this->user->getReservations());
<?php
echo $this->renderTable(new Class_TableDescription_DriveCheckout_Holds('holds'),
$this->checkout->getLibraryHolds());
echo $this->tagAnchor($this->url(['module' => 'admin',
'controller' => 'drive-checkout',
'action' => 'list-all-holds',
'id_user' => $this->checkout->getUser()->getId()],
null,
true),
$this->_('Voir toutes les réservations de %s',
$this->checkout->getNomComplet()),
['data-popup' => 'true']);
<?php
$skin = Class_Admin_Skin::current();
echo $this->Button((new Class_Entity())
->setUrl($this->url(['action' => 'items-csv']))
->setText($this->_('Exporter les documents (.csv)'))
->setImage($this->tagImg($skin->getIconUrl('actions', 'test'),
['style' => 'filter: invert();']))
->setAttribs(['style' => 'float:right']));
echo $this->Button((new Class_Entity())
->setUrl($this->url(['action' => 'list-csv']))
->setText($this->_('Exporter les rendez-vous (.csv)'))
->setImage($this->tagImg($skin->getIconUrl('actions', 'test'),
['style' => 'filter: invert();']))
->setAttribs(['style' => 'float:right']));
echo $this->tag('label', $this->_('Date'), ['for' => 'date', 'style' => 'margin-right: 5px']);
$date_url = $this->url(['date' => null]) . '/date/';
echo $this->formDate('date',
$this->date,
['onchange' => 'window.location=\'' . $date_url . '\' + this.value']);
echo $this->renderTable($this->table_description, $this->checkouts);
<?php
echo $this->tagAnchor($this->url(['module' => 'admin',
'controller' => 'drive-checkout',
'action' => 'list-all-holds',
'id_user' => $this->user->getId()],
null,
true),
$this->_('Voir toutes les réservations de %s',
$this->user->getNomComplet()),
['data-popup' => 'true']);
echo $this->driveCheckoutPlan($this->plan);
<?php
echo $this->renderForm($this->form);
if ($this->used_for === Class_Ouverture::USED_FOR_LIBRARY)
echo $this->renderForm($this->form);
$button_text = $this->_('Ajouter une plage d\'ouverture');
if ($this->used_for === Class_Ouverture::USED_FOR_MULTIMEDIA)
$button_text = $this->_('Ajouter une plage horaire de réservation multimédia');
if ($this->used_for === Class_Ouverture::USED_FOR_DRIVE)
$button_text = $this->_('Ajouter une plage d\'ouverture du drive');
echo $this->button_New(
(new Class_Entity())->setText($this->multimedia
? $this->_('Ajouter une plage horaire de réservation multimedia')
: $this->_('Ajouter une plage d\'ouverture'))
(new Class_Entity())->setText( $button_text)
->setUrl($this->url(['action' => 'add',
'id' => null])));
echo $this->button_Back(
......@@ -13,4 +20,4 @@ echo $this->button_Back(
'controller' => 'bib',
'action' => 'index'], null, true)));
echo $this->libraryOpeningsAdmin($this->library, $this->multimedia);
echo $this->libraryOpeningsAdmin($this->library, $this->used_for);
<?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 DriveCheckoutController extends ZendAfi_Controller_Action {
protected $_user = null;
public function init() {
parent::init();
$this->_user = $this->view->user = Class_Users::getIdentity();
}
public function planAction() {
if (!$this->_isActive())
return;
$plan = new Class_DriveCheckout_Plan($this->_request->getParams(), $this->_user);
if (!$plan->isValid()) {
$this->getHelper('ViewRenderer')->setNoRender();
$this->_helper->notify($plan->getLastMessage());
$this->_redirect($this->view->url($plan->fallbackUrl()));
return;
}
if ($this->_request->isPost()
&& ($checkout = $plan->persist())) {
$this->getHelper('ViewRenderer')->setNoRender();
$this->_helper->notify($this->_('Le retrait de vos documents de %s est planifié pour %s.',
$checkout->getLibraryLabel(),
$checkout->getDateTimeLabel()));
$this->_redirect($this->view->url($plan->resetUrl()));
return;
}
$this->view->plan = $plan;
}
public function deleteAction() {
if (!$this->_isActive())
return;
$this->getHelper('ViewRenderer')->setNoRender();
$message = $this->_('Impossible de supprimer un retrait inconnu.');
if ($checkout = Class_DriveCheckout::findFor($this->_getParam('id'), $this->_user)) {
$checkout->delete();
$message = $this->_('Le retrait de vos documents de %s planifié pour %s a été supprimé.',
$checkout->getLibraryLabel(),
$checkout->getDateTimeLabel());
}
$this->_helper->notify($message);
$this->_redirect('/opac/drive-checkout/plan');
}
public function icalAction() {
if (!$this->_isActive())
return;
if (!$checkout = Class_DriveCheckout::findFor($this->_getParam('id'), $this->_user)) {
$this->getHelper('ViewRenderer')->setNoRender();
$this->_helper->notify($this->_('Impossible d\'importer un retrait inconnu.'));
$this->_redirect('/opac/drive-checkout/plan');
return;
}
$content = (new Class_ICal_DriveCheckout($checkout))
->renderCalendar(Class_Profil::getCurrentProfil());
$this->_helper->ical('calendar.ics', $content);
}
protected function _isActive() {
if (Class_AdminVar::isDriveCheckoutEnabled())
return true;
$this->getHelper('ViewRenderer')->setNoRender();
$this->_helper->notify($this->_('La planification du retrait des réservations est désactivée.'));
$this->_redirect($this->view->url([], null, true));
return false;
}
}
<?php
echo $this->driveCheckoutPlan($this->plan);
<?php
$adapter = Zend_Db_Table::getDefaultAdapter();
try {
$adapter->query('create table if not exists `drive_checkout` ('
. 'id int(11) unsigned not null auto_increment,'
. 'user_id int(11) not null,'
. 'library_id int(11) not null,'
. 'start_at datetime not null,'
. 'primary key (id),'
. 'key user_id (user_id),'
. 'key library_id (library_id),'
. 'key start_at (start_at)'
. ') engine=MyISAM default charset=utf8');
} catch (Exception $e) {
}
\ No newline at end of file
<?php
$adapter = Zend_Db_Table::getDefaultAdapter();
try {
$adapter->query('alter table ouvertures change multimedia used_for int(11) default 0');
}
catch (Exception $e){}
try {
$adapter->query('alter table ouvertures add index used_for (used_for)');
}
catch (Exception $e){}
try {
$adapter->query('alter table bib_c_site add enable_drive tinyint(1) default 0');
}
catch (Exception $e){}
try {
$adapter->query('alter table ouvertures add column max_per_period_matin int(11) default null,
add column max_per_period_apres_midi int(11) default null;');
}
catch (Exception $e){}
\ No newline at end of file
......@@ -137,6 +137,7 @@ class Class_AdminVarLoader extends Storm_Model_Loader {
'usergroup-agenda' => $this->_getRendezVousVars(),
'templating' => $this->_getTemplatingVars(),
'identity-providers' => $this->_getIdentityProvidersVars(),
'drive-checkout' => $this->_getDriveCheckoutVars()
];
}
......@@ -550,6 +551,17 @@ class Class_AdminVarLoader extends Storm_Model_Loader {
}
protected function _getDriveCheckoutVars() {
return
['ENABLE_DRIVE_CHECKOUT' => Class_AdminVar_Meta::newOnOff($this->_('Activer la prise de rendez-vous pour récupérer des réservations (Drive)')),
'DRIVE_TEMPLATE_NEW_RDV_SUBJECT' => Class_AdminVar_Meta::newDefault($this->_('Sujet des courriels de confirmation de création de rendez-vous drive.'),
['value'=> 'Confirmation de rendez-vous à {library.libelle}']),
'DRIVE_TEMPLATE_NEW_RDV_CONTENT' => Class_AdminVar_Meta::newEditor($this->_('Modèle utilisé pour les courriels de confirmation de création de rendez-vous drive.'),
['value'=> '<p>Bonjour {user.nom_complet},</p> <p>nous vous confirmons votre rendez-vous <strong> &agrave; {library.libelle} {rendez_vous.date_time_label}</strong>.</p>']),
];
}
public function allVarsValues() {
if (null !== static::$_all_vars_values)
return static::$_all_vars_values;
......@@ -1131,6 +1143,11 @@ class Class_AdminVarLoader extends Storm_Model_Loader {
public function isIdentityProvidersEnabled() {
return Class_AdminVar::isModuleEnabled('ENABLE_IDENTITY_PROVIDERS');
}
public function isDriveCheckoutEnabled() {
return Class_AdminVar::isModuleEnabled('ENABLE_DRIVE_CHECKOUT');
}
}
......
......@@ -273,7 +273,7 @@ class Class_Bib extends Storm_Model_Abstract {
'ouvertures_multimedia' => ['model' => 'Class_Ouverture',
'role' => 'bib',
'scope' => ['multimedia' => 1],
'scope' => ['used_for' => Class_Ouverture::USED_FOR_MULTIMEDIA],
'order' => ['jour',
'validity_end desc',
'validity_start desc',
......@@ -917,19 +917,62 @@ class Class_Bib extends Storm_Model_Abstract {
}
public function getOuvertureOnDate($time) {
if($this->hasHoraire())
public function isDriveEnabled() {
return !$this->isAttributeEmpty('enable_drive') && 1 == (int)$this->getEnableDrive();
}
public function hasDriveOuvertures() {
return $this->isDriveEnabled() && Class_Ouverture::hasDriveFor($this);
}
/**
* @param $date DateTime
* @return Class_Ouverture or null
*/
public function getDriveOuvertureOnDate($date) {
return $this->isDriveEnabled() && $date
? $this->getOuvertureOnDate($date->getTimeStamp(), Class_Ouverture::USED_FOR_DRIVE)
: null;
}
public function hasOuverturesFor($used_for) {
return null !== Class_Ouverture::findFirstBy(['id_site' => $this->getId(),
'used_for' => $used_for]);
}
public function getOuvertureOnDate($time, $used_for = Class_Ouverture::USED_FOR_LIBRARY) {
if ($this->hasHoraire())
return null;
if ($ouverture = Class_Ouverture::findFirstBy(['jour' => date('Y-m-d', $time),
'id_site' => $this->getId()]))
'id_site' => $this->getId(),
'used_for' => $used_for]))
return $ouverture;
if ($this->isClosedOnHolidays() && Class_Date_Holiday::isHoliday($time))
return null;
$day_of_week = Class_Date::dayOfWeek($time);
$openings = new Storm_Model_Collection($this->getOuvertures());
$openings = new Storm_Model_Collection(
Class_Ouverture::findAllBy
(
[
'id_site' => $this->getId(),
'used_for'=> $used_for,
'order' => [
'jour',
'validity_end desc',
'validity_start desc',
'jour_semaine',
'debut_matin'
],
]
)
);
$blessed = $openings->select('hasValidityRange')
->detect(
......
<?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.
*