notification: update comment and api endpoint for marking as read/unread

This commit is contained in:
Julien Fastré 2021-12-29 17:36:14 +01:00
parent 9d638fe897
commit 8fe94bd117
7 changed files with 302 additions and 16 deletions

View File

@ -0,0 +1,87 @@
<?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\Controller;
use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Security\Authorization\NotificationVoter;
use Doctrine\ORM\EntityManagerInterface;
use RuntimeException;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Security;
use UnexpectedValueException;
/**
* @Route("/api/1.0/main/notification")
*/
class NotificationApiController
{
private EntityManagerInterface $entityManager;
private Security $security;
public function __construct(EntityManagerInterface $entityManager, Security $security)
{
$this->entityManager = $entityManager;
$this->security = $security;
}
/**
* @Route("/{id}/mark/read", name="chill_api_main_notification_mark_read", methods={"POST"})
*/
public function markAsRead(Notification $notification): JsonResponse
{
return $this->markAs('read', $notification);
}
/**
* @Route("/{id}/mark/unread", name="chill_api_main_notification_mark_unread", methods={"POST"})
*/
public function markAsUnread(Notification $notification): JsonResponse
{
return $this->markAs('unread', $notification);
}
private function markAs(string $target, Notification $notification): JsonResponse
{
if (!$this->security->isGranted(NotificationVoter::NOTIFICATION_TOGGLE_READ_STATUS, $notification)) {
throw new AccessDeniedException('Not allowed to toggle read status of notification');
}
$user = $this->security->getUser();
if (!$user instanceof User) {
throw new RuntimeException('not possible to mark as read by this user');
}
switch ($target) {
case 'read':
$notification->markAsReadBy($user);
break;
case 'unread':
$notification->markAsUnreadBy($user);
break;
default:
throw new UnexpectedValueException("target not supported: {$target}");
}
$this->entityManager->flush();
return new JsonResponse(null, JsonResponse::HTTP_ACCEPTED, [], false);
}
}

View File

@ -125,7 +125,7 @@ class NotificationController extends AbstractController
*/
public function editAction(Notification $notification, Request $request): Response
{
$this->denyAccessUnlessGranted(NotificationVoter::UPDATE, $notification);
$this->denyAccessUnlessGranted(NotificationVoter::NOTIFICATION_UPDATE, $notification);
$form = $this->createForm(NotificationType::class, $notification);
@ -207,27 +207,61 @@ class NotificationController extends AbstractController
*/
public function showAction(Notification $notification, Request $request): Response
{
$this->denyAccessUnlessGranted(NotificationVoter::SEE, $notification);
$this->denyAccessUnlessGranted(NotificationVoter::NOTIFICATION_SEE, $notification);
$notification->addComment($appendComment = new NotificationComment());
$appendComment = new NotificationComment();
$appendCommentForm = $this->createForm(NotificationCommentType::class, $appendComment);
$appendCommentForm->handleRequest($request);
if ($appendCommentForm->isSubmitted() && $appendCommentForm->isValid()) {
$this->em->persist($appendComment);
$this->em->flush();
if ($request->query->has('edit')) {
$commentId = $request->query->getInt('edit');
$editedComment = $notification->getComments()->filter(static function (NotificationComment $c) use ($commentId) {
return $c->getId() === $commentId;
})->first();
$this->addFlash('success', $this->translator->trans('notification.comment_appended'));
if (false === $editedComment) {
throw $this->createNotFoundException("Comment with id {$commentId} does not exists nor belong to this notification");
}
return $this->redirectToRoute('chill_main_notification_show', [
'id' => $notification->getId(),
]);
$editedCommentForm = $this->createForm(NotificationCommentType::class, $editedComment);
if (Request::METHOD_POST === $request->getMethod() && 'edit' === $request->request->get('form')) {
$editedCommentForm->handleRequest($request);
if ($editedCommentForm->isSubmitted() && $editedCommentForm->isValid()) {
$this->em->flush();
$this->addFlash('success', $this->translator->trans('notification.comment_updated'));
return $this->redirectToRoute('chill_main_notification_show', [
'id' => $notification->getId(),
'_fragment' => 'comment-' . $commentId,
]);
}
}
}
if (Request::METHOD_POST === $request->getMethod() && 'append' === $request->request->get('form')) {
$appendCommentForm->handleRequest($request);
if ($appendCommentForm->isSubmitted() && $appendCommentForm->isValid()) {
$notification->addComment($appendComment);
$this->em->persist($appendComment);
$this->em->flush();
$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(),
'editedCommentForm' => isset($editedCommentForm) ? $editedCommentForm->createView() : null,
'editedCommentId' => $commentId ?? null,
]);
// we mark the notification as read after having computed the response

View File

@ -18,7 +18,9 @@
{% if notification.comments|length > 0 %}
{% for comment in notification.comments %}
{% if editedCommentForm is null or editedCommentId != comment.id %}
<div>
<a id="comment-{{ comment.id }}"></a>
<blockquote class="chill-user-quote">
{{ comment.content|chill_markdown_to_html }}
</blockquote>
@ -26,11 +28,30 @@
{% if is_granted('CHILL_MAIN_NOTIFICATION_COMMENT_EDIT', comment) %}
<ul class="record_actions">
<li>
<a href="#" class="btn btn-edit"></a>
<a href="{{ chill_path_forward_return_path('chill_main_notification_show', { '_fragment': 'comment-'~comment.id, 'edit': comment.id, 'id': notification.id }) }}" class="btn btn-edit"></a>
</li>
</ul>
{% endif %}
</div>
{% else %}
<div>
<a id="comment-{{ comment.id }}"></a>
{{ form_start(editedCommentForm) }}
{{ form_widget(editedCommentForm) }}
<input type="hidden" name="form" value="edit" />
<ul class="record_actions">
<li class="cancel">
<a href="{{ chill_path_forward_return_path('chill_main_notification_show', { '_fragment': 'comment-'~comment.id, 'id': notification.id }) }}" class="btn btn-cancel"></a>
</li>
<li>
<button type="submit" class="btn btn-save"></button>
</li>
</ul>
{{ form_end(editedCommentForm) }}
</div>
{% endif %}
{% endfor %}
{% endif %}
@ -38,6 +59,9 @@
<div>
{{ form_start(appendCommentForm) }}
{{ form_widget(appendCommentForm) }}
<input type="hidden" name="form" value="append" />
<ul class="record_actions">
<li>
<button type="submit" class="btn btn-save">{{ 'notification.append_comment'|trans }}</button>

View File

@ -22,9 +22,11 @@ final class NotificationVoter extends Voter
{
public const COMMENT_EDIT = 'CHILL_MAIN_NOTIFICATION_COMMENT_EDIT';
public const SEE = 'CHILL_MAIN_NOTIFICATION_SEE';
public const NOTIFICATION_SEE = 'CHILL_MAIN_NOTIFICATION_SEE';
public const UPDATE = 'CHILL_MAIN_NOTIFICATION_UPDATE';
public const NOTIFICATION_TOGGLE_READ_STATUS = 'CHILL_MAIIN_NOTIFICATION_TOGGLE_READ_STATUS';
public const NOTIFICATION_UPDATE = 'CHILL_MAIN_NOTIFICATION_UPDATE';
protected function supports($attribute, $subject): bool
{
@ -45,10 +47,11 @@ final class NotificationVoter extends Voter
if ($subject instanceof Notification) {
switch ($attribute) {
case self::SEE:
case self::NOTIFICATION_SEE:
case self::NOTIFICATION_TOGGLE_READ_STATUS:
return $subject->getSender() === $user || $subject->getAddressees()->contains($user);
case self::UPDATE:
case self::NOTIFICATION_UPDATE:
return $subject->getSender() === $user;
default:

View File

@ -0,0 +1,96 @@
<?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 Controller;
use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Repository\UserRepository;
use Chill\MainBundle\Test\PrepareClientTrait;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
/**
* @internal
* @coversNothing
*/
final class NotificationApiControllerTest extends WebTestCase
{
use PrepareClientTrait;
private array $toDelete = [];
protected function tearDown(): void
{
$em = self::$container->get(EntityManagerInterface::class);
foreach ($this->toDelete as [$className, $id]) {
$object = $em->find($className, $id);
$em->remove($object);
}
$em->flush();
}
public function generateDataMarkAsRead()
{
self::bootKernel();
$em = self::$container->get(EntityManagerInterface::class);
$userRepository = self::$container->get(UserRepository::class);
$userA = $userRepository->findOneBy(['username' => 'center a_social']);
$userB = $userRepository->findOneBy(['username' => 'center b_social']);
$notification = new Notification();
$notification
->setMessage('Test generated')
->setRelatedEntityClass(AccompanyingPeriod::class)
->setRelatedEntityId(0)
->setSender($userB)
->addAddressee($userA);
$em->persist($notification);
$em->refresh($notification);
$em->flush();
$this->toDelete[] = [Notification::class, $notification->getId()];
yield [$notification->getId()];
}
/**
* @dataProvider generateDataMarkAsRead
*/
public function testMarkAsReadOrUnRead(int $notificationId)
{
$client = $this->getClientAuthenticated();
$client->request('POST', "/api/1.0/main/notification/{$notificationId}/mark/read");
$this->assertResponseIsSuccessful('test marking as read');
$em = self::$container->get(EntityManagerInterface::class);
/** @var Notification $notification */
$notification = $em->find(Notification::class, $notificationId);
$user = self::$container->get(UserRepository::class)->findOneBy(['username' => 'center a_social']);
$this->assertTrue($notification->isReadBy($user));
$client->request('POST', "/api/1.0/main/notification/{$notificationId}/mark/unread");
$this->assertResponseIsSuccessful('test marking as unread');
$notification = $em->find(Notification::class, $notificationId);
$user = $em->find(User::class, $user->getId());
$em->refresh($notification);
$em->refresh($user);
$this->assertFalse($notification->isReadBy($user));
}
}

View File

@ -733,4 +733,43 @@ paths:
class: 'Chill\PersonBundle\Entity\AccompanyingPeriod'
roles:
- 'CHILL_PERSON_ACCOMPANYING_PERIOD_SEE'
/1.0/main/notification/{id}/mark/read:
post:
tags:
- notification
summary: mark a notification as read
parameters:
- name: id
in: path
required: true
description: The notification id
schema:
type: integer
format: integer
minimum: 1
responses:
202:
description: "accepted"
403:
description: "unauthorized"
/1.0/main/notification/{id}/mark/unread:
post:
tags:
- notification
summary: mark a notification as unread
parameters:
- name: id
in: path
required: true
description: The notification id
schema:
type: integer
format: integer
minimum: 1
responses:
202:
description: "accepted"
403:
description: "unauthorized"

View File

@ -359,3 +359,6 @@ notification:
Any notification sent: Aucune notification envoyée
Notifications received: Notifications reçues
Notifications sent: Notification envoyées
comment_appended: Commentaire ajouté
comment_updated: Commentaire mis à jour