Notification: add a counter for notifications

This commit is contained in:
Julien Fastré 2022-01-04 16:44:01 +01:00
parent 5bb5468198
commit 3a207b2c5d
12 changed files with 211 additions and 29 deletions

View File

@ -30,6 +30,7 @@ use Chill\MainBundle\Security\Resolver\CenterResolverInterface;
use Chill\MainBundle\Security\Resolver\ScopeResolverInterface; use Chill\MainBundle\Security\Resolver\ScopeResolverInterface;
use Chill\MainBundle\Templating\Entity\ChillEntityRenderInterface; use Chill\MainBundle\Templating\Entity\ChillEntityRenderInterface;
use Chill\MainBundle\Templating\Entity\CompilerPass as RenderEntityCompilerPass; use Chill\MainBundle\Templating\Entity\CompilerPass as RenderEntityCompilerPass;
use Chill\MainBundle\Templating\UI\NotificationCounterInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\HttpKernel\Bundle\Bundle;
@ -53,6 +54,8 @@ class ChillMainBundle extends Bundle
->addTag('chill.search_api_provider'); ->addTag('chill.search_api_provider');
$container->registerForAutoconfiguration(NotificationHandlerInterface::class) $container->registerForAutoconfiguration(NotificationHandlerInterface::class)
->addTag('chill_main.notification_handler'); ->addTag('chill_main.notification_handler');
$container->registerForAutoconfiguration(NotificationCounterInterface::class)
->addTag('chill.count_notification.user');
$container->addCompilerPass(new SearchableServicesCompilerPass()); $container->addCompilerPass(new SearchableServicesCompilerPass());
$container->addCompilerPass(new ConfigConsistencyCompilerPass()); $container->addCompilerPass(new ConfigConsistencyCompilerPass());

View File

@ -209,9 +209,6 @@ class NotificationController extends AbstractController
{ {
$this->denyAccessUnlessGranted(NotificationVoter::NOTIFICATION_SEE, $notification); $this->denyAccessUnlessGranted(NotificationVoter::NOTIFICATION_SEE, $notification);
$appendComment = new NotificationComment();
$appendCommentForm = $this->createForm(NotificationCommentType::class, $appendComment);
if ($request->query->has('edit')) { if ($request->query->has('edit')) {
$commentId = $request->query->getInt('edit'); $commentId = $request->query->getInt('edit');
$editedComment = $notification->getComments()->filter(static function (NotificationComment $c) use ($commentId) { $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"); 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); $editedCommentForm = $this->createForm(NotificationCommentType::class, $editedComment);
if (Request::METHOD_POST === $request->getMethod() && 'edit' === $request->request->get('form')) { if (Request::METHOD_POST === $request->getMethod() && 'edit' === $request->request->get('form')) {
@ -240,6 +239,10 @@ class NotificationController extends AbstractController
} }
} }
if ($this->isGranted(NotificationVoter::COMMENT_ADD, $notification)) {
$appendComment = new NotificationComment();
$appendCommentForm = $this->createForm(NotificationCommentType::class, $appendComment);
if (Request::METHOD_POST === $request->getMethod() && 'append' === $request->request->get('form')) { if (Request::METHOD_POST === $request->getMethod() && 'append' === $request->request->get('form')) {
$appendCommentForm->handleRequest($request); $appendCommentForm->handleRequest($request);
@ -255,11 +258,12 @@ class NotificationController extends AbstractController
]); ]);
} }
} }
}
$response = $this->render('@ChillMain/Notification/show.html.twig', [ $response = $this->render('@ChillMain/Notification/show.html.twig', [
'notification' => $notification, 'notification' => $notification,
'handler' => $this->notificationHandlerManager->getHandler($notification), 'handler' => $this->notificationHandlerManager->getHandler($notification),
'appendCommentForm' => $appendCommentForm->createView(), 'appendCommentForm' => isset($appendCommentForm) ? $appendCommentForm->createView() : null,
'editedCommentForm' => isset($editedCommentForm) ? $editedCommentForm->createView() : null, 'editedCommentForm' => isset($editedCommentForm) ? $editedCommentForm->createView() : null,
'editedCommentId' => $commentId ?? null, 'editedCommentId' => $commentId ?? null,
]); ]);

View File

@ -0,0 +1,95 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\MainBundle\Notification\Counter;
use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Entity\NotificationComment;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Repository\NotificationRepository;
use Chill\MainBundle\Templating\UI\NotificationCounterInterface;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\PreFlushEventArgs;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Security\Core\User\UserInterface;
final class NotificationByUserCounter implements NotificationCounterInterface
{
private CacheItemPoolInterface $cacheItemPool;
private NotificationRepository $notificationRepository;
public function __construct(CacheItemPoolInterface $cacheItemPool, NotificationRepository $notificationRepository)
{
$this->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);
}
}

View File

@ -50,12 +50,13 @@ final class NotificationRepository implements ObjectRepository
public function countUnreadByUser(User $user): int 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 = new Query\ResultSetMapping();
$rsm->addScalarResult('c', 'c', Types::INTEGER); $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(); return $nq->getSingleScalarResult();
} }

View File

@ -60,15 +60,29 @@
{% if not notification.isReadBy(app.user) %} {% if not notification.isReadBy(app.user) %}
<div class="badge bg-danger">{{ 'notification.is_unread'|trans }}</div> <div class="badge bg-danger">{{ 'notification.is_unread'|trans }}</div>
{% endif %} {% endif %}
{% if notification.isSystem %}
<div class="badge bg-chill-green">{{ 'notification.is_system'|trans }}</div>
{% endif %}
</h2> </h2>
</div> </div>
<div class="item-row"> <div class="item-row">
<div class="item-col"> <div class="item-col">
{% if step == 'inbox' %} {% if step == 'inbox' and not notification.isSystem %}
{{ 'notification.from'|trans }}: {{ notification.sender|chill_entity_render_string }} {{ 'notification.from'|trans }}: {{ notification.sender|chill_entity_render_string }}
{% else %}
&nbsp;
{% endif %} {% endif %}
</div> </div>
<div class="item-col">{{ 'notification.adressees'|trans }}{% for a in notification.addressees %}{{ a|chill_entity_render_string }}{% if not loop.last %}, {% endif %}{% endfor %}</div> <div class="item-col">{{ 'notification.adressees'|trans }}
<ul>
{% for a in notification.addressees %}
<li>
{{ a|chill_entity_render_string }}
</li>
{% endfor %}
</ul>
</div>
<div class="item-col">{{ notification.date|format_datetime('long', 'short') }}</div> <div class="item-col">{{ notification.date|format_datetime('long', 'short') }}</div>
</div> </div>
<div class="item-row"> <div class="item-row">

View File

@ -65,7 +65,7 @@
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% if appendCommentForm is defined %} {% if appendCommentForm is not null %}
<div> <div>
{{ form_start(appendCommentForm) }} {{ form_start(appendCommentForm) }}
{{ form_widget(appendCommentForm) }} {{ form_widget(appendCommentForm) }}

View File

@ -12,18 +12,25 @@ declare(strict_types=1);
namespace Chill\MainBundle\Routing\MenuBuilder; namespace Chill\MainBundle\Routing\MenuBuilder;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Notification\Counter\NotificationByUserCounter;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface; use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\Security;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
class UserMenuBuilder implements LocalMenuBuilderInterface class UserMenuBuilder implements LocalMenuBuilderInterface
{ {
private NotificationByUserCounter $notificationByUserCounter;
private Security $security; private Security $security;
private TranslatorInterface $translator; 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->security = $security;
$this->translator = $translator; $this->translator = $translator;
} }
@ -49,14 +56,17 @@ class UserMenuBuilder implements LocalMenuBuilderInterface
'icon' => 'map-marker', 'icon' => 'map-marker',
]); ]);
$nbNotifications = $this->notificationByUserCounter->countUnreadByUser($user);
$menu $menu
->addChild( ->addChild(
$this->translator->trans('My notifications'), $this->translator->trans('notification.My notifications with counter', ['nb' => $nbNotifications]),
['route' => 'chill_main_notification_my'] ['route' => 'chill_main_notification_my']
) )
->setExtras([ ->setExtras([
'order' => 600, 'order' => 600,
'icon' => 'envelope', 'icon' => 'envelope',
'counter' => $nbNotifications,
]); ]);
$menu $menu

View File

@ -20,6 +20,13 @@ use UnexpectedValueException;
final class NotificationVoter extends Voter 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 COMMENT_EDIT = 'CHILL_MAIN_NOTIFICATION_COMMENT_EDIT';
public const NOTIFICATION_SEE = 'CHILL_MAIN_NOTIFICATION_SEE'; public const NOTIFICATION_SEE = 'CHILL_MAIN_NOTIFICATION_SEE';
@ -47,20 +54,30 @@ final class NotificationVoter extends Voter
if ($subject instanceof Notification) { if ($subject instanceof Notification) {
switch ($attribute) { switch ($attribute) {
case self::COMMENT_ADD:
return false === $subject->isSystem() && (
$subject->getAddressees()->contains($user) || $subject->getSender() === $user
);
case self::NOTIFICATION_SEE: case self::NOTIFICATION_SEE:
case self::NOTIFICATION_TOGGLE_READ_STATUS: case self::NOTIFICATION_TOGGLE_READ_STATUS:
return $subject->getSender() === $user || $subject->getAddressees()->contains($user); return $subject->getSender() === $user || $subject->getAddressees()->contains($user);
case self::NOTIFICATION_UPDATE: case self::NOTIFICATION_UPDATE:
return $subject->getSender() === $user; return $subject->getSender() === $user && false === $subject->isSystem();
default: default:
throw new UnexpectedValueException("this subject {$attribute} is not implemented"); throw new UnexpectedValueException("this subject {$attribute} is not implemented");
} }
} elseif ($subject instanceof NotificationComment) { } elseif ($subject instanceof NotificationComment) {
switch ($attribute) { switch ($attribute) {
case self::COMMENT_ADD:
return false === $subject->getNotification()->isSystem() && (
$subject->getNotification()->getAddressees()->contains($user) || $subject->getNotification()->getSender() === $user
);
case self::COMMENT_EDIT: case self::COMMENT_EDIT:
return $subject->getCreatedBy() === $user; return $subject->getCreatedBy() === $user && false === $subject->getNotification()->isSystem();
default: default:
throw new UnexpectedValueException("this subject {$attribute} is not implemented"); throw new UnexpectedValueException("this subject {$attribute} is not implemented");

View File

@ -22,3 +22,30 @@ services:
Chill\MainBundle\Notification\Templating\NotificationTwigExtension: ~ Chill\MainBundle\Notification\Templating\NotificationTwigExtension: ~
Chill\MainBundle\Notification\Templating\NotificationTwigExtensionRuntime: ~ 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'

View File

@ -4,3 +4,12 @@ years_old: >-
many {# ans} many {# ans}
other {# ans} other {# ans}
} }
notification:
My notifications with counter: >-
{nb, plural,
=0 {Mes notifications}
one {Une notification}
few {# notifications}
other {# notifications}
}

View File

@ -360,6 +360,9 @@ notification:
Notifications received: Notifications reçues Notifications received: Notifications reçues
Notifications sent: Notification envoyées Notifications sent: Notification envoyées
comment_appended: Commentaire ajouté comment_appended: Commentaire ajouté
append_comment: Ajouter un commentaire
comment_updated: Commentaire mis à jour comment_updated: Commentaire mis à jour
is_unread: Non-lue is_unread: Non-lue
is_system: Notification automatique
list: Notifications

View File

@ -6,8 +6,7 @@ services:
- { name: 'twig.extension' } - { name: 'twig.extension' }
Chill\TaskBundle\Templating\UI\CountNotificationTask: Chill\TaskBundle\Templating\UI\CountNotificationTask:
autoconfigure: true
arguments: arguments:
$singleTaskRepository: '@Chill\TaskBundle\Repository\SingleTaskRepository' $singleTaskRepository: '@Chill\TaskBundle\Repository\SingleTaskRepository'
$cachePool: '@cache.user_data' $cachePool: '@cache.user_data'
tags:
- { name: chill.count_notification.user }