From f7ca61f36b40fbe3072ce6c1a94ff3bfe4cc8cc3 Mon Sep 17 00:00:00 2001 From: jygaulier Date: Wed, 16 Sep 2020 10:28:17 +0200 Subject: [PATCH] - add : command bin/console validation:remind (experimental) - removed : internal handling of "VALIDATION_REMINDER" event - fixed : duration formatting --- bin/console | 3 + lib/Alchemy/Phrasea/Command/Command.php | 19 +- .../SendValidationRemindersCommand.php | 351 ++++++++++++++++++ .../Controller/Root/LoginController.php | 4 + .../Event/Subscriber/ValidationSubscriber.php | 7 +- .../ValidationParticipantRepository.php | 19 +- .../Phrasea/Controller/Root/LoginTest.php | 2 + 7 files changed, 390 insertions(+), 15 deletions(-) create mode 100644 lib/Alchemy/Phrasea/Command/SendValidationRemindersCommand.php diff --git a/bin/console b/bin/console index 744c7c7959..cbcb1b4a74 100755 --- a/bin/console +++ b/bin/console @@ -40,6 +40,7 @@ use Alchemy\Phrasea\Command\Plugin\AddPlugin; use Alchemy\Phrasea\Command\Plugin\RemovePlugin; use Alchemy\Phrasea\Command\CheckConfig; use Alchemy\Phrasea\Command\SearchEngine\MappingUpdateCommand; +use Alchemy\Phrasea\Command\SendValidationRemindersCommand; use Alchemy\Phrasea\Command\Setup\H264ConfigurationDumper; use Alchemy\Phrasea\Command\Setup\H264MappingGenerator; use Alchemy\Phrasea\Command\Setup\XSendFileConfigurationDumper; @@ -169,6 +170,8 @@ $cli->command(new WorkerExecuteCommand()); $cli->command(new WorkerRunServiceCommand()); $cli->command(new WorkerShowConfigCommand()); +$cli->command(new SendValidationRemindersCommand()); + $cli->loadPlugins(); $cli->run(); diff --git a/lib/Alchemy/Phrasea/Command/Command.php b/lib/Alchemy/Phrasea/Command/Command.php index e7acfabd44..9ce36b2824 100644 --- a/lib/Alchemy/Phrasea/Command/Command.php +++ b/lib/Alchemy/Phrasea/Command/Command.php @@ -114,14 +114,17 @@ abstract class Command extends SymfoCommand implements CommandInterface */ public function getFormattedDuration($seconds) { - $duration = ceil($seconds) . ' seconds'; - - if ($duration > 60) { - $duration = round($duration / 60, 1) . ' minutes'; - } elseif ($duration > 3600) { - $duration = round($duration / (60 * 60), 1) . ' hours'; - } elseif ($duration > (24 * 60 * 60)) { - $duration = round($duration / (24 * 60 * 60), 1) . ' days'; + if ($seconds > (24 * 3600)) { + $duration = round($seconds / (24 * 3600), 1) . ' days'; + } + elseif ($seconds > 3600) { + $duration = round($seconds / (3600), 1) . ' hours'; + } + elseif ($seconds > 60) { + $duration = round($seconds / 60, 1) . ' minutes'; + } + else { + $duration = ceil($seconds) . ' seconds'; } return $duration; diff --git a/lib/Alchemy/Phrasea/Command/SendValidationRemindersCommand.php b/lib/Alchemy/Phrasea/Command/SendValidationRemindersCommand.php new file mode 100644 index 0000000000..1ef28bfbc3 --- /dev/null +++ b/lib/Alchemy/Phrasea/Command/SendValidationRemindersCommand.php @@ -0,0 +1,351 @@ +setDescription('Send validation reminders. (experimental)'); + $this->addOption('dry',null, InputOption::VALUE_NONE,'dry run, list but don\'t act'); + $this->addOption('now', null,InputArgument::OPTIONAL, 'fake today'); + $this->addOption('days', null,InputArgument::OPTIONAL, 'overwrite validation-reminder-days'); + } + + + /** + * sanity check the cmd line options + * + * @return bool + */ + protected function sanitizeArgs() + { + $r = true; + + // --dry + $this->dry = $this->input->getOption('dry') ? true : false; + + // --now + if(($v = $this->input->getOption('now')) !== null) { + try { + $this->now = new DateTime($v); + } + catch (Exception $e) { + $this->output->writeln(sprintf('bad --date "%s"', $v)); + $r = false; + } + } + else { + $this->now = new DateTime(); + } + + // --days + if(($v = $this->input->getOption('days')) !== null) { + if(($this->days = (int)$v) <= 0) { + $this->output->writeln(sprintf('--days must be > 0 (bad value "%s")', $v)); + $r = false; + } + } + else { + $this->days = (int)$this->getConf()->get(['registry', 'actions', 'validation-reminder-days']); + } + + return $r; + } + + protected function doExecute(InputInterface $input, OutputInterface $output) + { + $this->input = $input; + $this->output = $output; + + $this->setDelivererLocator(new LazyLocator($this->container, 'notification.deliverer')); + + if(!$this->sanitizeArgs()) { + return -1; + } + + $date_to = clone($this->now); + $interval = sprintf('P%dD', $this->days); + try { + $date_to->add(new DateInterval($interval)); + } + catch(Exception $e) { + $this->output->writeln(sprintf('Bad interval "%s" ?', $interval)); + return -1; + } + + if($this->dry) { + $this->output->writeln('dry mode : emails will NOT be sent'); + } + + $output->writeln(sprintf('from "%s" to "%s" (days=%d), ', $this->now->format(self::DATE_FMT), $date_to->format(self::DATE_FMT), $this->days)); + + $fmt = ' participant: %-11s user: %-10s %s token: %-10s '; + //$output->writeln(sprintf($fmt, 'session', 'basket', 'participant', 'user', 'token', 'email')); + + $last_session = null; + foreach ($this->getValidationParticipantRepository()->findNotConfirmedAndNotRemindedParticipantsByExpireDate($date_to, $this->now) as $participant) { + $validationSession = $participant->getSession(); + $basket = $validationSession->getBasket(); + + // change session ? display header + if($validationSession->getId() !== $last_session) { + try { + $basket_name = $basket->getName(); + } + catch(Exception $e) { + // basket not found ? + $basket_name = '?'; + } + + try { + $initiator_email = $validationSession->getInitiator()->getEmail(); + } + catch(Exception $e) { + // initiator user not found ? + $initiator_email = '?'; + } + + $output->writeln(''); + $output->writeln(sprintf('session_id: %s (created %s by "%s", expire %s), basket_id: %s ("%s")', + $validationSession->getId(), + $validationSession->getCreated()->format(self::DATE_FMT), + $initiator_email, + $validationSession->getExpires()->format(self::DATE_FMT), + $basket->getId(), + $basket_name + )); + + $last_session = $validationSession->getId(); + } + + // now display participant + $can_send = true; + + // fu..ing doctrine : we can get user id if user does not exists ! we must try to hydrate to get an exception ! + $user = $participant->getUser(); // always ok ! + try { + $str_email = $user->getEmail(); // force to hydrate + } + catch (Exception $e) { + $str_email = 'user not found'; + $can_send = false; + } + + // find the token if exists + // nb : a validation may have not generated tokens if forcing auth was required upon creation + $token = null; + try { + $token = $this->getTokenRepository()->findValidationToken($basket, $user); + if($token) { + $str_token = sprintf('%s (cre. %s, exp. %s)', + $this->dotdot($token->getValue(), 10), + $token->getCreated()->format(self::DATE_FMT), + $token->getExpiration()->format(self::DATE_FMT) + ); + } + else { + $str_token = '(no token))'; // token not found + } + } + catch (Exception $e) { + // not unique token ? should not happen + $str_token = sprintf('%s', $e->getMessage()); + $can_send = false; + } + + $output->writeln(sprintf($fmt, + $this->dotdot($participant->getId(), 10), + $this->dotdot($user->getId(), 10), + $this->dotdot($str_email, 30, 'left', '"', '"'), + $str_token + ) + ); + + if(!$can_send) { + continue; + } + + if(!is_null($token)) { + $url = $this->container->url('lightbox_validation', ['basket' => $basket->getId(), 'LOG' => $token->getValue()]); + } + else { + $url = $this->container->url('lightbox_validation', ['basket' => $basket->getId()]); + } + + // $this->dispatch(PhraseaEvents::VALIDATION_REMINDER, new ValidationEvent($participant, $basket, $url)); + $this->doRemind($participant, $basket, $url); + } + + $this->getEntityManager()->flush(); + + return 0; + } + + /** + * format a string to a specified length + * + * @param string $s + * @param int $l + * @param string $align 'left' or 'right' + * @param string $pfx prefix to add, e.g '("' + * @param string $sfx suffix to add, e.g '")' + * @return string + */ + private function dotdot($s, $l, $align='left', $pfx='', $sfx='') + { + $l -= (strlen($pfx) + strlen($sfx)); + if(strlen($s) > $l) { + $s = $pfx . substr($s, 0, $l-1) . "\xE2\x80\xA6" . $sfx; + } + else { + $spc = str_repeat(' ', $l-strlen($s)); + $s = $align=='left' ? ($pfx . $s . $sfx . $spc) : ($spc . $pfx . $s . $sfx); + } + + return $s; + } + + private function doRemind(ValidationParticipant $participant, Basket $basket, $url) + { + $params = [ + 'from' => $basket->getValidation()->getInitiator()->getId(), + 'to' => $participant->getUser()->getId(), + 'ssel_id' => $basket->getId(), + 'url' => $url, + ]; + + $datas = json_encode($params); + + $mailed = false; + + $user_from = $basket->getValidation()->getInitiator(); + $user_to = $participant->getUser(); + + if ($this->shouldSendNotificationFor($participant->getUser(), 'eventsmanager_notify_validationreminder')) { + $readyToSend = false; + $title = $receiver = $emitter = null; + try { + $title = $basket->getName(); + + $receiver = Receiver::fromUser($user_to); + $emitter = Emitter::fromUser($user_from); + + $readyToSend = true; + } + catch (Exception $e) { + // no-op + } + + if ($readyToSend) { + $this->output->writeln(sprintf(' -> remind "%s" from "%s" to "%s"', $title, $receiver->getEmail(), $emitter->getEmail())); + if(!$this->dry) { + // for real + $mail = MailInfoValidationReminder::create($this->container, $receiver, $emitter); + $mail->setButtonUrl($params['url']); + $mail->setTitle($title); + + $this->deliver($mail); + $mailed = true; + + $participant->setReminded(new DateTime('now')); + $this->getEntityManager()->persist($participant); + } + } + } + + return $this->container['events-manager']->notify($params['to'], 'eventsmanager_notify_validationreminder', $datas, $mailed); + } + + + + /** + * @return EntityManagerInterface + */ + private function getEntityManager() + { + return $this->container['orm.em']; + } + + /** + * @return PropertyAccess + */ + protected function getConf() + { + return $this->container['conf']; + } + + /** + * @return ValidationParticipantRepository + */ + private function getValidationParticipantRepository() + { + return $this->container['repo.validation-participants']; + } + + /** + * @return TokenRepository + */ + private function getTokenRepository() + { + return $this->container['repo.tokens']; + } + + /** + * @param User $user + * @param $type + * @return mixed + */ + protected function shouldSendNotificationFor(User $user, $type) + { + return $this->container['settings']->getUserNotificationSetting($user, $type); + } +} \ No newline at end of file diff --git a/lib/Alchemy/Phrasea/Controller/Root/LoginController.php b/lib/Alchemy/Phrasea/Controller/Root/LoginController.php index ac9e517848..862b5ffd84 100644 --- a/lib/Alchemy/Phrasea/Controller/Root/LoginController.php +++ b/lib/Alchemy/Phrasea/Controller/Root/LoginController.php @@ -597,6 +597,9 @@ class LoginController extends Controller $date = new DateTime('+' . (int) $this->getConf()->get(['registry', 'actions', 'validation-reminder-days']) . ' days'); $manager = $this->getEntityManager(); + /* + * PHRAS-3214_validation-tokens-refacto : This code is moved to console command "SendValidationRemindersCommand.php" + * foreach ($this->getValidationParticipantRepository()->findNotConfirmedAndNotRemindedParticipantsByExpireDate($date) as $participant) { $validationSession = $participant->getSession(); $basket = $validationSession->getBasket(); @@ -625,6 +628,7 @@ class LoginController extends Controller } $manager->flush(); + */ $session = $this->getAuthenticator()->openAccount($user); diff --git a/lib/Alchemy/Phrasea/Core/Event/Subscriber/ValidationSubscriber.php b/lib/Alchemy/Phrasea/Core/Event/Subscriber/ValidationSubscriber.php index 232b03b8d0..9f788c115b 100644 --- a/lib/Alchemy/Phrasea/Core/Event/Subscriber/ValidationSubscriber.php +++ b/lib/Alchemy/Phrasea/Core/Event/Subscriber/ValidationSubscriber.php @@ -108,6 +108,9 @@ class ValidationSubscriber extends AbstractNotificationSubscriber return $this->app['events-manager']->notify($params['to'], 'eventsmanager_notify_validationdone', $datas, $mailed); } + /* + * PHRAS-3214_validation-tokens-refacto : This code is moved to console command "SendValidationRemindersCommand.php" + * public function onRemind(ValidationEvent $event) { $params = [ @@ -150,13 +153,15 @@ class ValidationSubscriber extends AbstractNotificationSubscriber return $this->app['events-manager']->notify($params['to'], 'eventsmanager_notify_validationreminder', $datas, $mailed); } + */ public static function getSubscribedEvents() { return [ PhraseaEvents::VALIDATION_CREATE => 'onCreate', PhraseaEvents::VALIDATION_DONE => 'onFinish', - PhraseaEvents::VALIDATION_REMINDER => 'onRemind', + // PHRAS-3214_validation-tokens-refacto : This code is moved to console command "SendValidationRemindersCommand.php" + // PhraseaEvents::VALIDATION_REMINDER => 'onRemind', ]; } } diff --git a/lib/Alchemy/Phrasea/Model/Repositories/ValidationParticipantRepository.php b/lib/Alchemy/Phrasea/Model/Repositories/ValidationParticipantRepository.php index 6374df2953..4fd55a75de 100644 --- a/lib/Alchemy/Phrasea/Model/Repositories/ValidationParticipantRepository.php +++ b/lib/Alchemy/Phrasea/Model/Repositories/ValidationParticipantRepository.php @@ -12,6 +12,7 @@ namespace Alchemy\Phrasea\Model\Repositories; use Alchemy\Phrasea\Model\Entities\ValidationParticipant; +use DateTime; use Doctrine\ORM\EntityRepository; use Doctrine\DBAL\Types\Type; @@ -21,10 +22,12 @@ class ValidationParticipantRepository extends EntityRepository /** * Retrieve all not reminded participants where the validation has not expired * - * @param $expireDate The expiration Date + * @param $expireDate DateTime The expiration Date + * @param $today DateTime fake "today" to allow to get past/future events + * (used by SendValidationRemindersCommand.php to debug with --dry) * @return ValidationParticipant[] */ - public function findNotConfirmedAndNotRemindedParticipantsByExpireDate(\DateTime $expireDate) + public function findNotConfirmedAndNotRemindedParticipantsByExpireDate(DateTime $expireDate, DateTime $today=null) { $dql = ' SELECT p, s @@ -33,10 +36,14 @@ class ValidationParticipantRepository extends EntityRepository JOIN s.basket b WHERE p.is_confirmed = 0 AND p.reminded IS NULL - AND s.expires < :date AND s.expires > CURRENT_TIMESTAMP()'; + AND s.expires < :date AND s.expires > ' . ($today===null ? 'CURRENT_TIMESTAMP()' : ':today'); - return $this->_em->createQuery($dql) - ->setParameter('date', $expireDate, Type::DATETIME) - ->getResult(); + $q = $this->_em->createQuery($dql) + ->setParameter('date', $expireDate, Type::DATETIME); + if($today !== null) { + $q->setParameter('today', $today, Type::DATETIME); + } + + return $q->getResult(); } } diff --git a/tests/Alchemy/Tests/Phrasea/Controller/Root/LoginTest.php b/tests/Alchemy/Tests/Phrasea/Controller/Root/LoginTest.php index b17d31de7c..39bb699910 100644 --- a/tests/Alchemy/Tests/Phrasea/Controller/Root/LoginTest.php +++ b/tests/Alchemy/Tests/Phrasea/Controller/Root/LoginTest.php @@ -2076,9 +2076,11 @@ class LoginTest extends \PhraseanetAuthenticatedWebTestCase ->disableOriginalConstructor() ->getMock(); + /* $repo->expects($participants ? $this->once() : $this->never()) ->method('findNotConfirmedAndNotRemindedParticipantsByExpireDate') ->will($this->returnValue([])); + */ $app['repo.validation-participants'] = $repo; }