From 54c2b92962bdc33ed23e05148ffc04d2b5e0fda0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 24 Jan 2022 10:09:57 +0000 Subject: [PATCH] Improve notifications --- CHANGELOG.md | 6 ++ .../Controller/ActivityController.php | 23 ++--- .../Resources/views/Activity/list.html.twig | 98 ++++++++++--------- .../ChillMainExtension.php | 1 + .../ChillMainBundle/Entity/Notification.php | 3 + .../Notification/Email/NotificationMailer.php | 10 +- .../Notification/NotificationPresence.php | 26 +++++ .../Templating/NotificationTwigExtension.php | 7 ++ .../NotificationTwigExtensionRuntime.php | 15 +++ .../Repository/NotificationRepository.php | 59 ++++++++--- .../views/Notification/_list_item.html.twig | 9 +- .../views/Notification/create.html.twig | 4 +- ...ension_counter_notifications_for.html.twig | 2 + ...extension_list_notifications_for.html.twig | 2 +- .../Service/Mailer/ChillMailer.php | 50 ++++++++++ .../config/services/mailer.yaml | 10 ++ .../migrations/Version20220120155303.php | 33 +++++++ .../translations/messages+intl-icu.fr.yaml | 16 +++ .../translations/messages.fr.yml | 1 + .../UserRefEventSubscriber.php} | 54 +++++++--- .../Entity/AccompanyingPeriod.php | 21 ++++ .../Form/CreationPersonType.php | 6 +- .../AccompanyingCourseWork/index.html.twig | 4 + .../views/AccompanyingPeriod/_list.html.twig | 4 + .../views/Person/list_with_period.html.twig | 4 + .../config/services/accompanyingPeriod.yaml | 14 ++- 26 files changed, 385 insertions(+), 97 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Resources/views/Notification/extension_counter_notifications_for.html.twig create mode 100644 src/Bundle/ChillMainBundle/Service/Mailer/ChillMailer.php create mode 100644 src/Bundle/ChillMainBundle/config/services/mailer.yaml create mode 100644 src/Bundle/ChillMainBundle/migrations/Version20220120155303.php rename src/Bundle/ChillPersonBundle/AccompanyingPeriod/{Workflow/WorkflowEventSubscriber.php => Events/UserRefEventSubscriber.php} (53%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 549276554..dc3a8da23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,12 @@ and this project adheres to * [person] name suggestions within create person form when person is created departing from a search input (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/377) +* [notification: formulaire création] descend la box avec la description dans le bas du formulaire +* [notification for activity]: fix link to activity +* [notification] add "URGENT" before accompanying course with emergency = true +* [notification] add a "read more" button on system notification +* [notification] add `[Chill]` in the subject of each notification, automatically +* [notification] add a counter for notification in activity list and accompanying period list, and search results * [parcours] bugfix if deathdate is not defined (eg. for a thirdparty) parcours is still displayed. Gave error before. ## Test releases diff --git a/src/Bundle/ChillActivityBundle/Controller/ActivityController.php b/src/Bundle/ChillActivityBundle/Controller/ActivityController.php index 6db1f6945..2947fda38 100644 --- a/src/Bundle/ChillActivityBundle/Controller/ActivityController.php +++ b/src/Bundle/ChillActivityBundle/Controller/ActivityController.php @@ -31,6 +31,7 @@ use DateTime; use Doctrine\ORM\EntityManagerInterface; use InvalidArgumentException; use Psr\Log\LoggerInterface; +use RuntimeException; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Form\Extension\Core\Type\SubmitType; @@ -38,8 +39,8 @@ use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Role\Role; -use Symfony\Component\Serializer\SerializerInterface; +use Symfony\Component\Serializer\SerializerInterface; use function array_key_exists; final class ActivityController extends AbstractController @@ -471,20 +472,21 @@ final class ActivityController extends AbstractController public function showAction(Request $request, int $id): Response { - $view = null; + $entity = $this->activityRepository->find($id); - [$person, $accompanyingPeriod] = $this->getEntity($request); + if (null === $entity) { + throw $this->createNotFoundException('Unable to find Activity entity.'); + } + + $accompanyingPeriod = $entity->getAccompanyingPeriod(); + $person = $entity->getPerson(); if ($accompanyingPeriod instanceof AccompanyingPeriod) { $view = 'ChillActivityBundle:Activity:showAccompanyingCourse.html.twig'; } elseif ($person instanceof Person) { $view = 'ChillActivityBundle:Activity:showPerson.html.twig'; - } - - $entity = $this->activityRepository->find($id); - - if (null === $entity) { - throw $this->createNotFoundException('Unable to find Activity entity.'); + } else { + throw new RuntimeException('the activity should be linked with a period or person'); } if (null !== $accompanyingPeriod) { @@ -493,8 +495,7 @@ final class ActivityController extends AbstractController $entity->personsNotAssociated = $entity->getPersonsNotAssociated(); } - // TODO revoir le Voter de Activity pour tenir compte qu'une activité peut appartenir a une période - // $this->denyAccessUnlessGranted('CHILL_ACTIVITY_SEE', $entity); + $this->denyAccessUnlessGranted(ActivityVoter::SEE, $entity); $deleteForm = $this->createDeleteForm($entity->getId(), $person, $accompanyingPeriod); diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/list.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/list.html.twig index a64142863..6b0639a27 100644 --- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/list.html.twig +++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/list.html.twig @@ -1,58 +1,64 @@ {% macro recordAction(activity, context = null, person_id = null, accompanying_course_id = null) %} - {% if no_action is not defined or no_action == false %} -
  • - {{ 'notification.Notify'|trans }} -
  • - {% endif %} - {% if context == 'person' and activity.accompanyingPeriod is not empty %} - {# - Disable person_id in following links, for redirect to accompanyingCourse context - #} - {% set person_id = null %} - {% set accompanying_course_id = activity.accompanyingPeriod.id %} -
  • - - - {{ 'Period number %number%'|trans({'%number%': accompanying_course_id}) }} - -
  • - {% endif %} -
  • - -
  • - {% if no_action is not defined or no_action == false %} - {% if is_granted('CHILL_ACTIVITY_UPDATE', activity) %} + {% if is_granted('CHILL_ACTIVITY_SEE_DETAILS', activity) %} + {% if no_action is not defined or no_action == false %} + {% set notif_counter = chill_count_notifications('Chill\\ActivityBundle\\Entity\\Activity', activity.id) %} + {% if notif_counter.total > 0 %} +
  • {{ chill_counter_notifications('Chill\\ActivityBundle\\Entity\\Activity', activity.id) }}
  • + {% endif %}
  • - + {{ 'notification.Notify'|trans }}
  • {% endif %} - {% if is_granted('CHILL_ACTIVITY_DELETE', activity) %} + {% if context == 'person' and activity.accompanyingPeriod is not empty %} + {# + Disable person_id in following links, for redirect to accompanyingCourse context + #} + {% set person_id = null %} + {% set accompanying_course_id = activity.accompanyingPeriod.id %}
  • - + class="btn btn-primary" + title="{{ 'See activity in accompanying course context'|trans }}"> + + {{ 'Period number %number%'|trans({'%number%': accompanying_course_id}) }} +
  • {% endif %} +
  • + +
  • + {% if no_action is not defined or no_action == false %} + {% if is_granted('CHILL_ACTIVITY_UPDATE', activity) %} +
  • + +
  • + {% endif %} + {% if is_granted('CHILL_ACTIVITY_DELETE', activity) %} +
  • + +
  • + {% endif %} + {% endif %} {% endif %} {% endmacro %} diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php index 772f4d8ee..3f0e2e65b 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php @@ -168,6 +168,7 @@ class ChillMainExtension extends Extension implements $loader->load('services/timeline.yaml'); $loader->load('services/search.yaml'); $loader->load('services/serializer.yaml'); + $loader->load('services/mailer.yaml'); $this->configureCruds($container, $config['cruds'], $config['apis'], $loader); } diff --git a/src/Bundle/ChillMainBundle/Entity/Notification.php b/src/Bundle/ChillMainBundle/Entity/Notification.php index 0624fff39..b2fe40c60 100644 --- a/src/Bundle/ChillMainBundle/Entity/Notification.php +++ b/src/Bundle/ChillMainBundle/Entity/Notification.php @@ -23,6 +23,9 @@ use Symfony\Component\Validator\Constraints as Assert; * @ORM\Entity * @ORM\Table( * name="chill_main_notification", + * indexes={ + * @ORM\Index(name="chill_main_notification_related_entity_idx", columns={"relatedentityclass", "relatedentityid"}) + * } * ) * @ORM\HasLifecycleCallbacks */ diff --git a/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php b/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php index 658764c26..e04a96af0 100644 --- a/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php +++ b/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php @@ -50,7 +50,7 @@ class NotificationMailer $email = new TemplatedEmail(); $email ->to($dest->getEmail()) - ->subject('Re: [Chill] ' . $comment->getNotification()->getTitle()) + ->subject('Re: ' . $comment->getNotification()->getTitle()) ->textTemplate('@ChillMain/Notification/email_notification_comment_persist.fr.md.twig') ->context([ 'comment' => $comment, @@ -79,11 +79,13 @@ class NotificationMailer continue; } + $email = new Email(); + $email + ->subject($notification->getTitle()); + if ($notification->isSystem()) { - $email = new Email(); $email - ->text($notification->getMessage()) - ->subject('[Chill] ' . $notification->getTitle()); + ->text($notification->getMessage()); } else { $email = new TemplatedEmail(); $email diff --git a/src/Bundle/ChillMainBundle/Notification/NotificationPresence.php b/src/Bundle/ChillMainBundle/Notification/NotificationPresence.php index 91bea3197..ed3a8cf66 100644 --- a/src/Bundle/ChillMainBundle/Notification/NotificationPresence.php +++ b/src/Bundle/ChillMainBundle/Notification/NotificationPresence.php @@ -15,12 +15,15 @@ use Chill\MainBundle\Entity\Notification; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Repository\NotificationRepository; use Symfony\Component\Security\Core\Security; +use function array_key_exists; /** * Helps to find if a notification exist for a given entity. */ class NotificationPresence { + private array $cache = []; + private NotificationRepository $notificationRepository; private Security $security; @@ -31,6 +34,29 @@ class NotificationPresence $this->notificationRepository = $notificationRepository; } + public function countNotificationsForClassAndEntity(string $relatedEntityClass, int $relatedEntityId): array + { + if (array_key_exists($relatedEntityClass, $this->cache) && array_key_exists($relatedEntityId, $this->cache[$relatedEntityClass])) { + return $this->cache[$relatedEntityClass][$relatedEntityId]; + } + + $user = $this->security->getUser(); + + if ($user instanceof User) { + $counter = $this->notificationRepository->countNotificationByRelatedEntityAndUserAssociated( + $relatedEntityClass, + $relatedEntityId, + $user + ); + + $this->cache[$relatedEntityClass][$relatedEntityId] = $counter; + + return $counter; + } + + return ['unread' => 0, 'read' => 0]; + } + /** * @return array|Notification[] */ diff --git a/src/Bundle/ChillMainBundle/Notification/Templating/NotificationTwigExtension.php b/src/Bundle/ChillMainBundle/Notification/Templating/NotificationTwigExtension.php index 115adf06b..eb017d912 100644 --- a/src/Bundle/ChillMainBundle/Notification/Templating/NotificationTwigExtension.php +++ b/src/Bundle/ChillMainBundle/Notification/Templating/NotificationTwigExtension.php @@ -23,6 +23,13 @@ class NotificationTwigExtension extends AbstractExtension 'needs_environment' => true, 'is_safe' => ['html'], ]), + new TwigFunction('chill_count_notifications', [NotificationTwigExtensionRuntime::class, 'countNotificationsFor'], [ + 'is_safe' => [], + ]), + new TwigFunction('chill_counter_notifications', [NotificationTwigExtensionRuntime::class, 'counterNotificationFor'], [ + 'needs_environment' => true, + 'is_safe' => ['html'], + ]), ]; } } diff --git a/src/Bundle/ChillMainBundle/Notification/Templating/NotificationTwigExtensionRuntime.php b/src/Bundle/ChillMainBundle/Notification/Templating/NotificationTwigExtensionRuntime.php index e35a39ac0..d5ec75699 100644 --- a/src/Bundle/ChillMainBundle/Notification/Templating/NotificationTwigExtensionRuntime.php +++ b/src/Bundle/ChillMainBundle/Notification/Templating/NotificationTwigExtensionRuntime.php @@ -24,6 +24,21 @@ class NotificationTwigExtensionRuntime implements RuntimeExtensionInterface $this->notificationPresence = $notificationPresence; } + public function counterNotificationFor(Environment $environment, string $relatedEntityClass, int $relatedEntityId, array $options = []): string + { + return $environment->render( + '@ChillMain/Notification/extension_counter_notifications_for.html.twig', + [ + 'counter' => $this->notificationPresence->countNotificationsForClassAndEntity($relatedEntityClass, $relatedEntityId), + ] + ); + } + + public function countNotificationsFor(string $relatedEntityClass, int $relatedEntityId, array $options = []): array + { + return $this->notificationPresence->countNotificationsForClassAndEntity($relatedEntityClass, $relatedEntityId); + } + public function listNotificationsFor(Environment $environment, string $relatedEntityClass, int $relatedEntityId, array $options = []): string { $notifications = $this->notificationPresence->getNotificationsForClassAndEntity($relatedEntityClass, $relatedEntityId); diff --git a/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php b/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php index 35ec64114..71966c973 100644 --- a/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php @@ -13,6 +13,7 @@ namespace Chill\MainBundle\Repository; use Chill\MainBundle\Entity\Notification; use Chill\MainBundle\Entity\User; +use Doctrine\DBAL\Statement; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; @@ -24,6 +25,8 @@ final class NotificationRepository implements ObjectRepository { private EntityManagerInterface $em; + private ?Statement $notificationByRelatedEntityAndUserAssociatedStatement = null; + private EntityRepository $repository; public function __construct(EntityManagerInterface $entityManager) @@ -48,6 +51,30 @@ final class NotificationRepository implements ObjectRepository ->getSingleScalarResult(); } + public function countNotificationByRelatedEntityAndUserAssociated(string $relatedEntityClass, int $relatedEntityId, User $user): array + { + if (null === $this->notificationByRelatedEntityAndUserAssociatedStatement) { + $sql = + 'SELECT + SUM((EXISTS (SELECT 1 AS c FROM chill_main_notification_addresses_unread cmnau WHERE user_id = 1812 and cmnau.notification_id = cmn.id))::int) AS unread, + SUM((cmn.sender_id = 1812)::int) AS sent, + COUNT(cmn.*) AS total + FROM chill_main_notification cmn + WHERE relatedentityclass = :relatedEntityClass AND relatedentityid = :relatedEntityId AND sender_id IS NOT NULL'; + $this->notificationByRelatedEntityAndUserAssociatedStatement = + $this->em->getConnection()->prepare($sql); + } + + $results = $this->notificationByRelatedEntityAndUserAssociatedStatement + ->executeQuery(['relatedEntityClass' => $relatedEntityClass, 'relatedEntityId' => $relatedEntityId]); + + $result = $results->fetchAssociative(); + + $results->free(); + + return $result; + } + public function countUnreadByUser(User $user): int { $sql = 'SELECT count(*) AS c FROM chill_main_notification_addresses_unread WHERE user_id = :userId'; @@ -153,11 +180,29 @@ final class NotificationRepository implements ObjectRepository * @return array|Notification[] */ public function findNotificationByRelatedEntityAndUserAssociated(string $relatedEntityClass, int $relatedEntityId, User $user): array + { + return + $this->buildQueryNotificationByRelatedEntityAndUserAssociated($relatedEntityClass, $relatedEntityId, $user) + ->select('n') + ->getQuery() + ->getResult(); + } + + public function findOneBy(array $criteria, ?array $orderBy = null): ?Notification + { + return $this->repository->findOneBy($criteria, $orderBy); + } + + public function getClassName() + { + return Notification::class; + } + + private function buildQueryNotificationByRelatedEntityAndUserAssociated(string $relatedEntityClass, int $relatedEntityId, User $user): QueryBuilder { $qb = $this->repository->createQueryBuilder('n'); $qb - ->select('n') ->where($qb->expr()->eq('n.relatedEntityClass', ':relatedEntityClass')) ->andWhere($qb->expr()->eq('n.relatedEntityId', ':relatedEntityId')) ->andWhere($qb->expr()->isNotNull('n.sender')) @@ -171,17 +216,7 @@ final class NotificationRepository implements ObjectRepository ->setParameter('relatedEntityId', $relatedEntityId) ->setParameter('user', $user); - return $qb->getQuery()->getResult(); - } - - public function findOneBy(array $criteria, ?array $orderBy = null): ?Notification - { - return $this->repository->findOneBy($criteria, $orderBy); - } - - public function getClassName() - { - return Notification::class; + return $qb; } private function queryByAddressee(User $addressee, bool $countQuery = false): QueryBuilder diff --git a/src/Bundle/ChillMainBundle/Resources/views/Notification/_list_item.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Notification/_list_item.html.twig index 91de10f99..391158a00 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Notification/_list_item.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Notification/_list_item.html.twig @@ -62,6 +62,7 @@ {{ c.notification.message|chill_markdown_to_html }} {% else %} {{ c.notification.message|u.truncate(250, '…', false)|chill_markdown_to_html }} +

    {{ 'Read more'|trans }}

    {% endif %} @@ -85,7 +86,13 @@ {% if is_granted('CHILL_MAIN_NOTIFICATION_SEE', c.notification) %}
  • + class="btn {% if not c.notification.isSystem %}btn-show change-icon{% else %}btn-misc{% endif %}" title="{{ 'notification.see_comments_thread'|trans }}"> + {% if not c.notification.isSystem() %} + + {% else %} + {{ 'Read more'|trans }} + {% endif %} +
  • {% endif %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/Notification/create.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Notification/create.html.twig index 8797c276a..ce5934e52 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Notification/create.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Notification/create.html.twig @@ -21,8 +21,6 @@ {{ form_row(form.title, { 'label': 'notification.subject'|trans }) }} {{ form_row(form.addressees, { 'label': 'notification.sent_to'|trans }) }} - {% include handler.template(notification) with handler.templateData(notification) %} -
    @@ -30,6 +28,8 @@
    + {% include handler.template(notification) with handler.templateData(notification) %} + {{ form_end(form) }}