diff --git a/src/Bundle/ChillMainBundle/ChillMainBundle.php b/src/Bundle/ChillMainBundle/ChillMainBundle.php index 0416edf76..7a4e563bb 100644 --- a/src/Bundle/ChillMainBundle/ChillMainBundle.php +++ b/src/Bundle/ChillMainBundle/ChillMainBundle.php @@ -30,6 +30,7 @@ use Chill\MainBundle\Security\Resolver\CenterResolverInterface; use Chill\MainBundle\Security\Resolver\ScopeResolverInterface; use Chill\MainBundle\Templating\Entity\ChillEntityRenderInterface; use Chill\MainBundle\Templating\Entity\CompilerPass as RenderEntityCompilerPass; +use Chill\MainBundle\Templating\UI\NotificationCounterInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; @@ -53,6 +54,8 @@ class ChillMainBundle extends Bundle ->addTag('chill.search_api_provider'); $container->registerForAutoconfiguration(NotificationHandlerInterface::class) ->addTag('chill_main.notification_handler'); + $container->registerForAutoconfiguration(NotificationCounterInterface::class) + ->addTag('chill.count_notification.user'); $container->addCompilerPass(new SearchableServicesCompilerPass()); $container->addCompilerPass(new ConfigConsistencyCompilerPass()); diff --git a/src/Bundle/ChillMainBundle/Controller/NotificationController.php b/src/Bundle/ChillMainBundle/Controller/NotificationController.php index dddaf306f..52f58ed68 100644 --- a/src/Bundle/ChillMainBundle/Controller/NotificationController.php +++ b/src/Bundle/ChillMainBundle/Controller/NotificationController.php @@ -209,9 +209,6 @@ class NotificationController extends AbstractController { $this->denyAccessUnlessGranted(NotificationVoter::NOTIFICATION_SEE, $notification); - $appendComment = new NotificationComment(); - $appendCommentForm = $this->createForm(NotificationCommentType::class, $appendComment); - if ($request->query->has('edit')) { $commentId = $request->query->getInt('edit'); $editedComment = $notification->getComments()->filter(static function (NotificationComment $c) use ($commentId) { @@ -222,6 +219,8 @@ class NotificationController extends AbstractController throw $this->createNotFoundException("Comment with id {$commentId} does not exists nor belong to this notification"); } + $this->denyAccessUnlessGranted(NotificationVoter::COMMENT_EDIT, $editedComment); + $editedCommentForm = $this->createForm(NotificationCommentType::class, $editedComment); if (Request::METHOD_POST === $request->getMethod() && 'edit' === $request->request->get('form')) { @@ -240,26 +239,31 @@ class NotificationController extends AbstractController } } - if (Request::METHOD_POST === $request->getMethod() && 'append' === $request->request->get('form')) { - $appendCommentForm->handleRequest($request); + if ($this->isGranted(NotificationVoter::COMMENT_ADD, $notification)) { + $appendComment = new NotificationComment(); + $appendCommentForm = $this->createForm(NotificationCommentType::class, $appendComment); - if ($appendCommentForm->isSubmitted() && $appendCommentForm->isValid()) { - $notification->addComment($appendComment); - $this->em->persist($appendComment); - $this->em->flush(); + if (Request::METHOD_POST === $request->getMethod() && 'append' === $request->request->get('form')) { + $appendCommentForm->handleRequest($request); - $this->addFlash('success', $this->translator->trans('notification.comment_appended')); + if ($appendCommentForm->isSubmitted() && $appendCommentForm->isValid()) { + $notification->addComment($appendComment); + $this->em->persist($appendComment); + $this->em->flush(); - return $this->redirectToRoute('chill_main_notification_show', [ - 'id' => $notification->getId(), - ]); + $this->addFlash('success', $this->translator->trans('notification.comment_appended')); + + return $this->redirectToRoute('chill_main_notification_show', [ + 'id' => $notification->getId(), + ]); + } } } $response = $this->render('@ChillMain/Notification/show.html.twig', [ 'notification' => $notification, 'handler' => $this->notificationHandlerManager->getHandler($notification), - 'appendCommentForm' => $appendCommentForm->createView(), + 'appendCommentForm' => isset($appendCommentForm) ? $appendCommentForm->createView() : null, 'editedCommentForm' => isset($editedCommentForm) ? $editedCommentForm->createView() : null, 'editedCommentId' => $commentId ?? null, ]); diff --git a/src/Bundle/ChillMainBundle/Notification/Counter/NotificationByUserCounter.php b/src/Bundle/ChillMainBundle/Notification/Counter/NotificationByUserCounter.php new file mode 100644 index 000000000..8dfd2df8a --- /dev/null +++ b/src/Bundle/ChillMainBundle/Notification/Counter/NotificationByUserCounter.php @@ -0,0 +1,95 @@ +cacheItemPool = $cacheItemPool; + $this->notificationRepository = $notificationRepository; + } + + public function addNotification(UserInterface $u): int + { + if (!$u instanceof User) { + return 0; + } + + return $this->countUnreadByUser($u); + } + + public function countUnreadByUser(User $user): int + { + $key = self::generateCacheKeyUnreadNotificationByUser($user); + + $item = $this->cacheItemPool->getItem($key); + + if ($item->isHit()) { + return $item->get(); + } + + $unreads = $this->notificationRepository->countUnreadByUser($user); + + $item + ->set($unreads) + // keep in cache for 15 minutes + ->expiresAfter(60 * 15); + $this->cacheItemPool->save($item); + + return $unreads; + } + + public static function generateCacheKeyUnreadNotificationByUser(User $user): string + { + return 'chill_main_notif_unread_by_' . $user->getId(); + } + + public function onEditNotificationComment(NotificationComment $notificationComment, LifecycleEventArgs $eventArgs): void + { + $this->resetCacheForNotification($notificationComment->getNotification()); + } + + public function onPreFlushNotification(Notification $notification, PreFlushEventArgs $eventArgs): void + { + $this->resetCacheForNotification($notification); + } + + private function resetCacheForNotification(Notification $notification): void + { + $keys = []; + + if (null !== $notification->getSender()) { + $keys[] = self::generateCacheKeyUnreadNotificationByUser($notification->getSender()); + } + + foreach ($notification->getAddressees() as $addressee) { + $keys[] = self::generateCacheKeyUnreadNotificationByUser($addressee); + } + + $this->cacheItemPool->deleteItems($keys); + } +} diff --git a/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php b/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php index 68c6b8be6..ce9e3b9e9 100644 --- a/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php @@ -50,12 +50,13 @@ final class NotificationRepository implements ObjectRepository public function countUnreadByUser(User $user): int { - $sql = 'SELECT count(*) AS c FROM chill_main_notification_addresses_unread WHERE user_id = ?'; + $sql = 'SELECT count(*) AS c FROM chill_main_notification_addresses_unread WHERE user_id = :userId'; $rsm = new Query\ResultSetMapping(); $rsm->addScalarResult('c', 'c', Types::INTEGER); - $nq = $this->em->createNativeQuery($sql, $rsm); + $nq = $this->em->createNativeQuery($sql, $rsm) + ->setParameter('userId', $user->getId()); return $nq->getSingleScalarResult(); } diff --git a/src/Bundle/ChillMainBundle/Resources/views/Notification/list.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Notification/list.html.twig index 836883846..0c0a75312 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Notification/list.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Notification/list.html.twig @@ -60,15 +60,29 @@ {% if not notification.isReadBy(app.user) %}
{{ 'notification.is_unread'|trans }}
{% endif %} + {% if notification.isSystem %} +
{{ 'notification.is_system'|trans }}
+ {% endif %} +
- {% if step == 'inbox' %} - {{ 'notification.from'|trans }}: {{ notification.sender|chill_entity_render_string }} + {% if step == 'inbox' and not notification.isSystem %} + {{ 'notification.from'|trans }}: {{ notification.sender|chill_entity_render_string }} + {% else %} +   {% endif %}
-
{{ 'notification.adressees'|trans }}{% for a in notification.addressees %}{{ a|chill_entity_render_string }}{% if not loop.last %}, {% endif %}{% endfor %}
+
{{ 'notification.adressees'|trans }} + +
{{ notification.date|format_datetime('long', 'short') }}
diff --git a/src/Bundle/ChillMainBundle/Resources/views/Notification/show.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Notification/show.html.twig index 96357233a..271def828 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Notification/show.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Notification/show.html.twig @@ -65,7 +65,7 @@ {% endfor %} {% endif %} - {% if appendCommentForm is defined %} + {% if appendCommentForm is not null %}
{{ form_start(appendCommentForm) }} {{ form_widget(appendCommentForm) }} diff --git a/src/Bundle/ChillMainBundle/Routing/MenuBuilder/UserMenuBuilder.php b/src/Bundle/ChillMainBundle/Routing/MenuBuilder/UserMenuBuilder.php index 74a8ff31d..ae2eb1f2c 100644 --- a/src/Bundle/ChillMainBundle/Routing/MenuBuilder/UserMenuBuilder.php +++ b/src/Bundle/ChillMainBundle/Routing/MenuBuilder/UserMenuBuilder.php @@ -12,18 +12,25 @@ declare(strict_types=1); namespace Chill\MainBundle\Routing\MenuBuilder; use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Notification\Counter\NotificationByUserCounter; use Chill\MainBundle\Routing\LocalMenuBuilderInterface; use Symfony\Component\Security\Core\Security; use Symfony\Contracts\Translation\TranslatorInterface; class UserMenuBuilder implements LocalMenuBuilderInterface { + private NotificationByUserCounter $notificationByUserCounter; + private Security $security; private TranslatorInterface $translator; - public function __construct(Security $security, TranslatorInterface $translator) - { + public function __construct( + NotificationByUserCounter $notificationByUserCounter, + Security $security, + TranslatorInterface $translator + ) { + $this->notificationByUserCounter = $notificationByUserCounter; $this->security = $security; $this->translator = $translator; } @@ -49,14 +56,17 @@ class UserMenuBuilder implements LocalMenuBuilderInterface 'icon' => 'map-marker', ]); + $nbNotifications = $this->notificationByUserCounter->countUnreadByUser($user); + $menu ->addChild( - $this->translator->trans('My notifications'), + $this->translator->trans('notification.My notifications with counter', ['nb' => $nbNotifications]), ['route' => 'chill_main_notification_my'] ) ->setExtras([ 'order' => 600, 'icon' => 'envelope', + 'counter' => $nbNotifications, ]); $menu diff --git a/src/Bundle/ChillMainBundle/Security/Authorization/NotificationVoter.php b/src/Bundle/ChillMainBundle/Security/Authorization/NotificationVoter.php index d0b283c24..0a6c239f4 100644 --- a/src/Bundle/ChillMainBundle/Security/Authorization/NotificationVoter.php +++ b/src/Bundle/ChillMainBundle/Security/Authorization/NotificationVoter.php @@ -20,6 +20,13 @@ use UnexpectedValueException; final class NotificationVoter extends Voter { + /** + * Allow to add a comment on a notification. + * + * May apply on both @see{NotificationComment::class} and @see{Notification::class}. + */ + public const COMMENT_ADD = 'CHILL_MAIN_NOTIFICATION_COMMENT_ADD'; + public const COMMENT_EDIT = 'CHILL_MAIN_NOTIFICATION_COMMENT_EDIT'; public const NOTIFICATION_SEE = 'CHILL_MAIN_NOTIFICATION_SEE'; @@ -47,20 +54,30 @@ final class NotificationVoter extends Voter if ($subject instanceof Notification) { switch ($attribute) { + case self::COMMENT_ADD: + return false === $subject->isSystem() && ( + $subject->getAddressees()->contains($user) || $subject->getSender() === $user + ); + case self::NOTIFICATION_SEE: case self::NOTIFICATION_TOGGLE_READ_STATUS: return $subject->getSender() === $user || $subject->getAddressees()->contains($user); case self::NOTIFICATION_UPDATE: - return $subject->getSender() === $user; + return $subject->getSender() === $user && false === $subject->isSystem(); default: throw new UnexpectedValueException("this subject {$attribute} is not implemented"); } } elseif ($subject instanceof NotificationComment) { switch ($attribute) { + case self::COMMENT_ADD: + return false === $subject->getNotification()->isSystem() && ( + $subject->getNotification()->getAddressees()->contains($user) || $subject->getNotification()->getSender() === $user + ); + case self::COMMENT_EDIT: - return $subject->getCreatedBy() === $user; + return $subject->getCreatedBy() === $user && false === $subject->getNotification()->isSystem(); default: throw new UnexpectedValueException("this subject {$attribute} is not implemented"); diff --git a/src/Bundle/ChillMainBundle/config/services/notification.yaml b/src/Bundle/ChillMainBundle/config/services/notification.yaml index 4ec90de41..f98561bd9 100644 --- a/src/Bundle/ChillMainBundle/config/services/notification.yaml +++ b/src/Bundle/ChillMainBundle/config/services/notification.yaml @@ -22,3 +22,30 @@ services: Chill\MainBundle\Notification\Templating\NotificationTwigExtension: ~ Chill\MainBundle\Notification\Templating\NotificationTwigExtensionRuntime: ~ + + Chill\MainBundle\Notification\Counter\NotificationByUserCounter: + autoconfigure: true + autowire: true + tags: + - + name: 'doctrine.orm.entity_listener' + event: 'preFlush' + entity: 'Chill\MainBundle\Entity\Notification' + # set the 'lazy' option to TRUE to only instantiate listeners when they are used + lazy: true + method: 'onPreFlushNotification' + + - + name: 'doctrine.orm.entity_listener' + event: 'postUpdate' + entity: 'Chill\MainBundle\Entity\NotificationComment' + # set the 'lazy' option to TRUE to only instantiate listeners when they are used + lazy: true + method: 'onEditNotificationComment' + - + name: 'doctrine.orm.entity_listener' + event: 'postPersist' + entity: 'Chill\MainBundle\Entity\NotificationComment' + # set the 'lazy' option to TRUE to only instantiate listeners when they are used + lazy: true + method: 'onEditNotificationComment' diff --git a/src/Bundle/ChillMainBundle/translations/messages+intl-icu.fr.yaml b/src/Bundle/ChillMainBundle/translations/messages+intl-icu.fr.yaml index d5dedbb9f..4f25b46da 100644 --- a/src/Bundle/ChillMainBundle/translations/messages+intl-icu.fr.yaml +++ b/src/Bundle/ChillMainBundle/translations/messages+intl-icu.fr.yaml @@ -1,6 +1,15 @@ years_old: >- - {age, plural, + {age, plural, one {# an} many {# ans} other {# ans} } + +notification: + My notifications with counter: >- + {nb, plural, + =0 {Mes notifications} + one {Une notification} + few {# notifications} + other {# notifications} + } diff --git a/src/Bundle/ChillMainBundle/translations/messages.fr.yml b/src/Bundle/ChillMainBundle/translations/messages.fr.yml index 0bd15b74b..7efda04d1 100644 --- a/src/Bundle/ChillMainBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillMainBundle/translations/messages.fr.yml @@ -360,6 +360,9 @@ notification: Notifications received: Notifications reçues Notifications sent: Notification envoyées comment_appended: Commentaire ajouté + append_comment: Ajouter un commentaire comment_updated: Commentaire mis à jour is_unread: Non-lue + is_system: Notification automatique + list: Notifications diff --git a/src/Bundle/ChillTaskBundle/config/services/templating.yaml b/src/Bundle/ChillTaskBundle/config/services/templating.yaml index 401bef6d5..0fb98cc19 100644 --- a/src/Bundle/ChillTaskBundle/config/services/templating.yaml +++ b/src/Bundle/ChillTaskBundle/config/services/templating.yaml @@ -4,10 +4,9 @@ services: $taskWorkflowManager: '@Chill\TaskBundle\Workflow\TaskWorkflowManager' tags: - { name: 'twig.extension' } - + Chill\TaskBundle\Templating\UI\CountNotificationTask: + autoconfigure: true arguments: $singleTaskRepository: '@Chill\TaskBundle\Repository\SingleTaskRepository' $cachePool: '@cache.user_data' - tags: - - { name: chill.count_notification.user }