Commit 83fda506 authored by Laurent's avatar Laurent
Browse files

dev#98799 : PNB Holds

parent 9f144713
Pipeline #8662 passed with stage
in 41 minutes and 45 seconds
'98799' =>
['Label' => $this->_('Système de réservation des documents PNB'),
'Desc' => $this->_('Les abonnés peuvent réserver des documents PNB si ceux-ci ne sont pas disponibles.'),
'Image' => '',
'Video' => 'https://youtu.be/vHZh4WF9jpc',
'Category' => $this->_('Ressources numériques'),
'Right' => function($feature_description, $user) {return true;},
'Wiki' => 'http://wiki.bokeh-library-portal.org/index.php?title=Configuration_du_PNB_Dilicom#R.C3.A9servations_de_documents',
'Test' => '',
'Date' => '2019-10-17'],
\ No newline at end of file
- ticket #98799 : Système de réservation des documents PNB
\ No newline at end of file
......@@ -75,6 +75,15 @@ class AbonneController extends ZendAfi_Controller_Action {
}
public function deletePnbHoldAction() {
$hold = Class_Hold_Pnb::find($this->_getParam('id'));
$hold->delete();
$this->_helper->notify($this->_('Votre réservation du document %s a bien été supprimée.',
$hold->getTitle()));
$this->_redirectToReferer();
}
public function inscrireSessionAction() {
$this->_redirectToReferer();
......
......@@ -315,6 +315,29 @@ class BibNumeriqueController extends ZendAfi_Controller_Action {
}
public function holdBookAction(){
$album = Class_Album::find($this->_getParam('id'));
$status= (new Class_WebService_BibNumerique_Dilicom_Hub())
->isAlbumHoldableBy($album, $this->_user);
if ($status->returnMessage) {
$this->_helper->notify($status->returnMessage[0]);
return $this->_redirectToReferer();
}
$hold = new Class_Hold_Pnb();
$hold->setRecordOriginId($album->getIdOrigine())
->setUser($this->_user)
->save();
$this->_helper->notify($this->_('Réservation enregistrée : %s', $hold->getTitle()));
$this->_redirectToReferer();
}
public function downloadLoanBookAjaxAction() {
if ($this->_userShouldBeRedirect())
return;
......
<?php
$adapter = Zend_Db_Table_Abstract::getDefaultAdapter();
$adapter->query('CREATE TABLE if not exists `hold_pnb` ( '
. 'id int(11) unsigned not null auto_increment,'
. 'record_origin_id varchar(255) not null,'
. 'user_id int(11),'
. 'subscriber_id varchar(255) not null,'
. 'hold_date datetime,'
. 'expiration_date datetime,'
. 'primary key (id),'
. 'key (`user_id`),'
. 'key (`subscriber_id`),'
. 'key (`record_origin_id`)'
. ') engine=MyISAM default charset=utf8');
......@@ -57,7 +57,8 @@ $bokeh
->setupDevOptions()
->setupHTTPClient()
->setupLanguage()
->setupCustomFields();
->setupCustomFields()
->setupMail();
if (!session_id()) //if in unit tests
$bokeh->setupSession();
......
......@@ -155,7 +155,10 @@ class Class_AdminVarLoader extends Storm_Model_Loader {
protected function _getAlbumVars() {
return $this->_getStaticAlbumVars() + $this->_getDynamicAlbumVars();
return
$this->_getStaticAlbumVars()
+ $this->_getDynamicAlbumVars()
+ $this->_getDilicomPnbVars();
}
......@@ -178,6 +181,32 @@ class Class_AdminVarLoader extends Storm_Model_Loader {
}
protected function _getDilicomPnbVars() {
return [
'DILICOM_PNB' => Class_AdminVar_Meta::newOnOff($this->_('Activation du PNB Dilicom')),
'DILICOM_PNB_GLN_COLLECTIVITE' => Class_AdminVar_Meta::newDefault($this->_('Gln de la collectivité, il est fourni par Dilicom.')),
'DILICOM_PNB_PWD_COLLECTIVITE' => Class_AdminVar_Meta::newDefault($this->_('Mot de passe de la collectivité, il est fourni par Dilicom.')),
'DILICOM_PNB_GLN_CONTRACTOR' => Class_AdminVar_Meta::newDefault($this->_('Contracteur du PNB Dilicom')),
'DILICOM_PNB_SERVER_URL' => Class_AdminVar_Meta::newDefault($this->_('Url du serveur PNB Dilicom')),
'DILICOM_PNB_IP_ADRESSES' => Class_AdminVar_Meta::newMultiInput($this->_('Liste des adresses IP publiques autorisées pour la consultation des documents'),
['validate' => 'ZendAfi_Validate_Dilicom_IpAdresses']),
'DILICOM_PNB_FTP_SERVER' => Class_AdminVar_Meta::newDefault($this->_('Serveur FTP de diffusion des offres PNB Dilicom')),
'DILICOM_PNB_FTP_USER' => Class_AdminVar_Meta::newDefault($this->_('Utilisateur FTP de diffusion des offres PNB Dilicom')),
'DILICOM_PNB_FTP_PASS' => Class_AdminVar_Meta::newDefault($this->_('Mot de passe FTP de diffusion des offres PNB Dilicom')),
'DILICOM_PNB_MAX_LOAN_DURATION' => Class_AdminVar_Meta::newDefault($this->_('Durée maximale (en jours) d\'un prêt PNB Dilicom')),
'DILICOM_PNB_LOAN_COUNT_LIMIT' => Class_AdminVar_Meta::newDefault($this->_('Nombre de prêts simultanés maximum pour un livre PNB Dilicom')),
'DILICOM_PNB_MAX_LOAN_PER_USER' => Class_AdminVar_Meta::newDefault($this->_('Nombre de prêts simultanés maximum pour un abonné PNB Dilicom (par défaut 3)'), ['value' => 3]),
'DILICOM_PNB_LOAN_WARNING_MESSAGE' => Class_AdminVar_Meta::newEditor($this->_('Message d\'avertissement affiché sur la popup d\'emprunt'),
['value' => $this->_('Votre compte sera mis à jour dans un délai de 15 minutes après le retour anticipé du document.')]),
'DILICOM_PNB_ENABLE_HOLDS' => Class_AdminVar_Meta::newOnOff($this->_('Activation de la gestion des réservations de documents PNB Dilicom')),
'DILICOM_PNB_HOLD_MAX_AVAILABILITY' => Class_AdminVar_Meta::newDefault($this->_('Nombre de jours de disponibilité de la réservation (par défaut 8)'), ['value' => 8]),
'DILICOM_PNB_HOLD_AVAILABLE_MAIL' => Class_AdminVar_Meta::newEditor($this->_('Email de notification de disponibilité de reservation numérique'), ['value'=>'Bonjour {user_name},<p>Le document <a href="{record_url}">{record_title}</a> vous est réservé pour emprunt jusqu\'au {hold_expiration_date}.</p>']),
'DILICOM_PNB_RECORD_MAX_HOLD_COUNT' => Class_AdminVar_Meta::newDefault($this->_('Nombre maximum de réservations par document numérique'), ['value' => 3]),
'DILICOM_PNB_PATRON_MAX_HOLD_COUNT' => Class_AdminVar_Meta::newDefault($this->_('Nombre maximum de réservations par utilisateur'), ['value' => 2])];
}
protected function _getStaticAlbumVars() {
return ['ALBUMS_LIST_MODE' => Class_AdminVar_Meta::newOnOff($this->_('Le gestionnaire de contenu affiche les albums sous forme de liste paginée au lieu de d\'une arborescence. Cet affichage est adapté lorsque le nombre d\'albums devient trop important')),
......@@ -209,21 +238,6 @@ class Class_AdminVarLoader extends Storm_Model_Loader {
'VODECLIC_ID' => Class_AdminVar_Meta::newDefault($this->_('Identifiant partenaire Vodeclic'))->bePrivate(),
'VODECLIC_BIB_ID' => Class_AdminVar_Meta::newDefault($this->_('Identifiant code bibliothèque Vodeclic'))->bePrivate(),
'DILICOM_PNB' => Class_AdminVar_Meta::newOnOff($this->_('Activation du PNB Dilicom')),
'DILICOM_PNB_GLN_COLLECTIVITE' => Class_AdminVar_Meta::newDefault($this->_('Gln de la collectivité, il est fourni par Dilicom.')),
'DILICOM_PNB_PWD_COLLECTIVITE' => Class_AdminVar_Meta::newDefault($this->_('Mot de passe de la collectivité, il est fourni par Dilicom.')),
'DILICOM_PNB_GLN_CONTRACTOR' => Class_AdminVar_Meta::newDefault($this->_('Contracteur du PNB Dilicom')),
'DILICOM_PNB_SERVER_URL' => Class_AdminVar_Meta::newDefault($this->_('Url du serveur PNB Dilicom')),
'DILICOM_PNB_IP_ADRESSES' => Class_AdminVar_Meta::newMultiInput($this->_('Liste des adresses IP publiques autorisées pour la consultation des documents'),
['validate' => 'ZendAfi_Validate_Dilicom_IpAdresses']),
'DILICOM_PNB_FTP_SERVER' => Class_AdminVar_Meta::newDefault($this->_('Serveur FTP de diffusion des offres PNB Dilicom')),
'DILICOM_PNB_FTP_USER' => Class_AdminVar_Meta::newDefault($this->_('Utilisateur FTP de diffusion des offres PNB Dilicom')),
'DILICOM_PNB_FTP_PASS' => Class_AdminVar_Meta::newDefault($this->_('Mot de passe FTP de diffusion des offres PNB Dilicom')),
'DILICOM_PNB_MAX_LOAN_DURATION' => Class_AdminVar_Meta::newDefault($this->_('Durée maximale (en jours) d\'un prêt PNB Dilicom')),
'DILICOM_PNB_LOAN_COUNT_LIMIT' => Class_AdminVar_Meta::newDefault($this->_('Nombre de prêts simultanés maximum pour un livre PNB Dilicom')),
'DILICOM_PNB_MAX_LOAN_PER_USER' => Class_AdminVar_Meta::newDefault($this->_('Nombre de prêts simultanés maximum pour un abonné PNB Dilicom (par défaut 3)'), ['value' => 3]),
'DILICOM_PNB_LOAN_WARNING_MESSAGE' => Class_AdminVar_Meta::newEditor($this->_('Message d\'avertissement affiché sur la popup d\'emprunt'),
['value' => $this->_('Votre compte sera mis à jour dans un délai de 15 minutes après le retour anticipé du document.')]),
'MYCOW_EID' => Class_AdminVar_Meta::newDefault($this->_('Clé d\'identification MyCOW.EU pour le portail. Cette clé doit être fournie par MyCOW.EU. Elle active la ressource numérique dans le portail.'))->bePrivate(),
'CITEDELAMUSIQUE' => Class_AdminVar_Meta::newDefault($this->_('Adresse du serveur OAI Cité de la Musique'))->bePrivate(),
'CITEDELAMUSIQUE_ID' => Class_AdminVar_Meta::newDefault($this->_('Identifiant d\'accès au serveur OAI Cité de la Musique'))->bePrivate(),
......
......@@ -148,7 +148,7 @@ class Class_Album_UsageConstraint extends Storm_Model_Abstract {
public function numberOfSimultaneousLoansRemaning() {
return $this->findItemDoIfNone(
function($item) {
return $this->getLoanAllowedNumberOfUsers() - abs($item->getLoanCount());
return $this->getLoanAllowedNumberOfUsers() - max(0, $item->getLoanCount());
},
false );
}
......
......@@ -65,6 +65,8 @@ class Class_Batch_Dilicom extends Class_Batch_Abstract {
protected function _getDefaultJobs() {
return [new Class_Batch_DilicomJobOnix($this),
new Class_Batch_DilicomJobEndedLoans($this),
new Class_Batch_DilicomJobUnindexExpiredOrders($this)];
new Class_Batch_DilicomJobUnindexExpiredOrders($this),
new Class_Batch_DilicomJobProcessHolds($this)
];
}
}
\ No newline at end of file
<?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 Class_Batch_DilicomJobProcessHolds extends Class_Batch_Job {
public function getLabel() {
return $this->_('Dilicom Job : Traitement des Réservations PNB');
}
public function run() {
$logger = $this->getLogger();
$logger->log($this->_('Traitement des réservations PNB'));
if (!$this->isEnabled()) {
$logger->log($this->_('Traitement des réservations PNB Dilicom désactivé'));
return;
}
$this->_allocateHolds($logger);
$this->_deleteExpiredHolds($logger);
}
protected function _allocateHolds($logger) {
$hub = new Class_WebService_BibNumerique_Dilicom_Hub();
$holds_pending = (new Storm_Model_Collection(Class_Hold_Pnb::findAllPending()))
->select(function($hold) { return $hold->hasAlbum(); });
$holds_pending->eachDo(function ($hold) use ($hub, $logger)
{
$hold->setLogger($logger);
$allocable = $hub->isHoldAllocable($hold);
if ($allocable->returnMessage)
return $logger->log($this->_('Allocation de la réservation sur %s(%s) pour %s(%s) impossible : %s',
$hold->getTitle(),
$hold->getId(),
$hold->getUserFullName(),
$hold->getUserId(),
$allocable->returnMessage[0]),
'debug');
$hold->allocateHold();
$logger->log($this->_('Réservation de %s(%s) pour %s(%s) allouée',
$hold->getTitle(),
$hold->getId(),
$hold->getUserFullName(),
$hold->getUserId()),
'debug');
});
$logger->log($this->_plural('Aucune réservation traitée',
'1 ressource inspectée',
'%d ressources inspectées',
$holds_pending->count()));
return $this;
}
protected function _deleteExpiredHolds($logger) {
Class_Hold_Pnb::findAllExpired()
->eachDo(function($hold) use ($logger)
{
$hold->delete();
$logger
->log($this->_('Réservation(%s) de %s(%s) expirée(%s) sur %s(%s) supprimée',
$hold->getId(),
$hold->getUserFullName(),
$hold->getUserId(),
$hold->getExpirationDate(),
$hold->getTitle(),
$hold->getRecordOriginId()),
'debug');
});
return $this;
}
}
\ No newline at end of file
<?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_Hold_PnbLoader extends Storm_Model_Loader {
public function findAllOngoingOfUser($user) {
if (!$user)
return [];
return array_unique(
array_merge(
Class_Hold_Pnb::findAllBy(['user_id' => $user->getId(),
'order' => 'hold_date']),
Class_Hold_Pnb::findAllBy(['subscriber_id' => $user->getIdabon(),
'order' => 'hold_date'])));
}
public function findAllOngoingOfUserAndAlbum($user, $album) {
return (new Storm_Model_Collection(Class_Hold_Pnb::findAllOngoingOfUser($user)))
->select(function($hold) use ($album)
{
return $hold->getRecordOriginId() == $album->getIdOrigine();
});
}
public function findAllByAlbum($album) {
return new Storm_Model_Collection(Class_Hold_Pnb::findAllBy(['record_origin_id' => $album->getIdOrigine(),
'order' => 'hold_date']));
}
public function countPendingByAlbum($album){
return Class_Hold_Pnb::findAllByAlbum($album)->select('isPending')->count();
}
public function countByAllocatedOn($album) {
return $this->findAllByAlbum($album)->select('isAllocated')->count();
}
public function findAllPending() {
return Class_Hold_Pnb::findAllBy(['expiration_date'=> null,
'order' => 'hold_date'
]);
}
public function findAllPendingOrAllocatedForAlbum($album) {
return Class_Hold_Pnb::findAllByAlbum($album)->reject('isExpired');
}
public function findAllExpired() {
return (new Storm_Model_Collection(Class_Hold_Pnb::findAllBy(['expiration_date not' => null])))
->select('isExpired');
}
public function deleteByLoan($loan) {
Class_Hold_Pnb::findAllOngoingOfUserAndAlbum($loan->getUser(), $loan->getAlbum())
->eachDo('delete');
}
}
class Class_Hold_Pnb extends Storm_Model_Abstract {
use Trait_TimeSource, Trait_Translator, Trait_Logger;
protected
$_table_name = 'hold_pnb',
$_loader_class = 'Class_Hold_PnbLoader',
$_belongs_to = ['user' => ['model'=>'Class_Users',
'referenced_in'=>'user_id']],
$_default_attribute_values = ['expiration_date' => null,
'user_id' => null,
'subscriber_id' => ''];
public function getNextExpirationDate(){
$days = (int)Class_AdminVar::getValueOrDefault('DILICOM_PNB_HOLD_MAX_AVAILABILITY');
$timestamp = strtotime($this->getCurrentDateTime() . ' +' . $days . ' day');
return date('Y-m-d',$timestamp).' 23:59:59';
}
public function allocateHold(){
$this->setExpirationDate($this->getNextExpirationDate())
->save();
$this
->getLogger()
->log($this->_('Allocation de la réservation(%s) sur %s(%s) à %s(%s)',
$this->getId(),
$this->getTitle(),
$this->getRecordOriginId(),
$this->getUserFullName(),
$this->getUserId()));
$this->_notifyHolder();
}
public function _notifyHolder() {
$expiration_date = strftime("%A %d %B %Y",
strtotime($this->getExpirationDate()));
$subject = $this->_('%s disponible pour emprunt jusqu\'au %s',
$this->getTitle(),
$expiration_date);
$mail = new Class_MailHtml();
$status = $mail->sendMail($subject,
Class_AdminVar::getValueOrDefault('DILICOM_PNB_HOLD_AVAILABLE_MAIL'),
$this->getUser()->getMail(),
[
'user_name' => $this->getUserFullName(),
'record_url' => $this->getRecordAbsoluteUrl(),
'record_title' => $this->getTitle(),
'hold_expiration_date' => $expiration_date
]);
$this->getLogger()
->log(sprintf('[MAIL]%s %s : %s',
$status ? ('[' . $status . ']') : '',
$this->getUser()->getMail(),
$subject),
'debug');
return $this;
}
public function setUser($user) {
parent::_set('user', $user);
return $this->setSubscriberId($user->getIdabon());
}
public function getUser() {
$user = parent::_get('user');
return $user
? $user
: Class_Users::findFirstBy(['idabon' => $this->getSubscriberId()]);
}
public function getUserId() {
return ($user = $this->getUser())
? $user->getId()
: null;
}
public function getUserFullName() {
return ($user = $this->getUser())
? $user->getNomComplet()
: '';
}
public function getAlbum() {
return Class_Album::findFirstBy(['id_origine' => $this->getRecordOriginId()]);
}
public function getTitle() {
return ($album = $this->getAlbum())
? $album->getTitre()
: '';
}
public function getMainAuthor() {
return ($album = $this->getAlbum())
? $album->getMainAuthorName()
: '';
}
public function getRecord() {
return ($album = $this->getAlbum())
? $album->getNotice()
: null;
}
public function getRecordAbsoluteUrl() {
return ($record = $this->getRecord())
? $record->getAbsoluteUrl()
: '';
}
public function getOrder() {
if ($this->isExpired())
return null;
$holds = $this->getLoader()
->findAllPendingOrAllocatedForAlbum($this->getAlbum())
->getArrayCopy();
return array_search($this, $holds, true) + 1;
}
public function isPending(){
return !($this->isAllocated() || $this->isExpired()) ;
}
public function isExpired() {
return $this->getExpirationDate() && ($this->getExpirationDate() < $this->getCurrentDateTime());
}
public function isAllocated() {
return $this->getExpirationDate() > $this->getCurrentDateTime();
}
public function beforeSave() {
if ($this->isNew())
$this->setHoldDate($this->getCurrentDateTime());
}
}
......@@ -82,7 +82,7 @@ class Class_Mail {
return (true === $statut)
? ''
: $error_message;
: $statut;
}
......
......@@ -26,6 +26,11 @@ class Class_TableDescription_PNBItems extends Class_TableDescription {
->addColumn($this->_('Titre'), 'title')
->addColumn($this->_('Nombre de prêts'), 'quantity_on_total')
->addColumn($this->_('Nombre de prêts simultanés'), 'live_quantity')
->addColumn($this->_('Nombre de réservations'),
function($model)
{
return Class_Hold_Pnb::findAllByAlbum($model->getAlbum())->count();
})
->addColumn($this->_('Durée de prêt en jours'), 'duration')
->addColumn($this->_('Nombre de jours restant sur la licence'), 'license_expiration')
->addColumn($this->_('Date de commande'), 'order_date')
......
......@@ -1291,7 +1291,10 @@ class Class_Users extends Storm_Model_Abstract {
* @return int
*/
public function getNbReservations() {
return $this->getEmprunteur()->getNbReservations();
return $this->getEmprunteur()->getNbReservations()
+ (new Storm_Model_Collection(Class_Hold_Pnb::findAllOngoingOfUser($this)))
->reject('isExpired')
->count();
}
......
......@@ -91,6 +91,12 @@ class Class_WebService_BibNumerique_Dilicom_Hub extends Class_WebService_Abstrac
if (!$item = $this->_getFirstLoanableItemOrLast($album))
return $this->_error($this->_('La consultation du document est impossible.'));
return $this->_placeLoanOnItemForUser($item, $user);
}
protected function _placeLoanOnItemForUser($item, $user) {
$album = $item->getAlbum();
$loan = Class_Loan_Pnb::newInstance(['subscriber_id' => $user->getIdabon(),
'user_id' => $user->getId(),
'loan_date' => $this->_startDate(),
......@@ -129,8 +135,10 @@ class Class_WebService_BibNumerique_Dilicom_Hub extends Class_WebService_Abstrac
return $this->_error($this->_('Emprunt impossible. Le service "loanBook" a renvoyé une erreur : "%s"', implode(',', $error)));
}
if (isset($content->link) && ($link = $content->link) && isset($link->url) && ($url = $link->url))
$loan->setLoanLink($url)->save();
if (isset($content->link) && ($link = $content->link) && isset($link->url) && ($url = $link->url)) {
$loan->setLoanLink($url)->save();
Class_Hold_Pnb::deleteByLoan($loan);
}
$this->updateStatus($album);
return $content;
}
......@@ -188,6 +196,53 @@ class Class_WebService_BibNumerique_Dilicom_Hub extends Class_WebService_Abstrac
}
public function isAlbumHoldableBy($album, $user) {
if (!Class_AdminVar::isModuleEnabled('DILICOM_PNB_ENABLE_HOLDS'))
return $this->_error($this->_('Les réservations sont désactivées.'));
if (!$user)
return $this->_alert('Réservation impossible. Utilisateur non connecté');