mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
notification: update comment and api endpoint for marking as read/unread
This commit is contained in:
parent
9d638fe897
commit
8fe94bd117
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -125,7 +125,7 @@ class NotificationController extends AbstractController
|
|||||||
*/
|
*/
|
||||||
public function editAction(Notification $notification, Request $request): Response
|
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);
|
$form = $this->createForm(NotificationType::class, $notification);
|
||||||
|
|
||||||
@ -207,13 +207,44 @@ class NotificationController extends AbstractController
|
|||||||
*/
|
*/
|
||||||
public function showAction(Notification $notification, Request $request): Response
|
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 = $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) {
|
||||||
|
return $c->getId() === $commentId;
|
||||||
|
})->first();
|
||||||
|
|
||||||
|
if (false === $editedComment) {
|
||||||
|
throw $this->createNotFoundException("Comment with id {$commentId} does not exists nor belong to this notification");
|
||||||
|
}
|
||||||
|
|
||||||
|
$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);
|
$appendCommentForm->handleRequest($request);
|
||||||
|
|
||||||
if ($appendCommentForm->isSubmitted() && $appendCommentForm->isValid()) {
|
if ($appendCommentForm->isSubmitted() && $appendCommentForm->isValid()) {
|
||||||
|
$notification->addComment($appendComment);
|
||||||
$this->em->persist($appendComment);
|
$this->em->persist($appendComment);
|
||||||
$this->em->flush();
|
$this->em->flush();
|
||||||
|
|
||||||
@ -223,11 +254,14 @@ class NotificationController extends AbstractController
|
|||||||
'id' => $notification->getId(),
|
'id' => $notification->getId(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$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' => $appendCommentForm->createView(),
|
||||||
|
'editedCommentForm' => isset($editedCommentForm) ? $editedCommentForm->createView() : null,
|
||||||
|
'editedCommentId' => $commentId ?? null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// we mark the notification as read after having computed the response
|
// we mark the notification as read after having computed the response
|
||||||
|
@ -18,7 +18,9 @@
|
|||||||
|
|
||||||
{% if notification.comments|length > 0 %}
|
{% if notification.comments|length > 0 %}
|
||||||
{% for comment in notification.comments %}
|
{% for comment in notification.comments %}
|
||||||
|
{% if editedCommentForm is null or editedCommentId != comment.id %}
|
||||||
<div>
|
<div>
|
||||||
|
<a id="comment-{{ comment.id }}"></a>
|
||||||
<blockquote class="chill-user-quote">
|
<blockquote class="chill-user-quote">
|
||||||
{{ comment.content|chill_markdown_to_html }}
|
{{ comment.content|chill_markdown_to_html }}
|
||||||
</blockquote>
|
</blockquote>
|
||||||
@ -26,11 +28,30 @@
|
|||||||
{% if is_granted('CHILL_MAIN_NOTIFICATION_COMMENT_EDIT', comment) %}
|
{% if is_granted('CHILL_MAIN_NOTIFICATION_COMMENT_EDIT', comment) %}
|
||||||
<ul class="record_actions">
|
<ul class="record_actions">
|
||||||
<li>
|
<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>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</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 %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@ -38,6 +59,9 @@
|
|||||||
<div>
|
<div>
|
||||||
{{ form_start(appendCommentForm) }}
|
{{ form_start(appendCommentForm) }}
|
||||||
{{ form_widget(appendCommentForm) }}
|
{{ form_widget(appendCommentForm) }}
|
||||||
|
|
||||||
|
<input type="hidden" name="form" value="append" />
|
||||||
|
|
||||||
<ul class="record_actions">
|
<ul class="record_actions">
|
||||||
<li>
|
<li>
|
||||||
<button type="submit" class="btn btn-save">{{ 'notification.append_comment'|trans }}</button>
|
<button type="submit" class="btn btn-save">{{ 'notification.append_comment'|trans }}</button>
|
||||||
|
@ -22,9 +22,11 @@ final class NotificationVoter extends Voter
|
|||||||
{
|
{
|
||||||
public const COMMENT_EDIT = 'CHILL_MAIN_NOTIFICATION_COMMENT_EDIT';
|
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
|
protected function supports($attribute, $subject): bool
|
||||||
{
|
{
|
||||||
@ -45,10 +47,11 @@ final class NotificationVoter extends Voter
|
|||||||
|
|
||||||
if ($subject instanceof Notification) {
|
if ($subject instanceof Notification) {
|
||||||
switch ($attribute) {
|
switch ($attribute) {
|
||||||
case self::SEE:
|
case self::NOTIFICATION_SEE:
|
||||||
|
case self::NOTIFICATION_TOGGLE_READ_STATUS:
|
||||||
return $subject->getSender() === $user || $subject->getAddressees()->contains($user);
|
return $subject->getSender() === $user || $subject->getAddressees()->contains($user);
|
||||||
|
|
||||||
case self::UPDATE:
|
case self::NOTIFICATION_UPDATE:
|
||||||
return $subject->getSender() === $user;
|
return $subject->getSender() === $user;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
@ -733,4 +733,43 @@ paths:
|
|||||||
class: 'Chill\PersonBundle\Entity\AccompanyingPeriod'
|
class: 'Chill\PersonBundle\Entity\AccompanyingPeriod'
|
||||||
roles:
|
roles:
|
||||||
- 'CHILL_PERSON_ACCOMPANYING_PERIOD_SEE'
|
- '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"
|
||||||
|
|
||||||
|
|
||||||
|
@ -359,3 +359,6 @@ notification:
|
|||||||
Any notification sent: Aucune notification envoyée
|
Any notification sent: Aucune notification envoyée
|
||||||
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_updated: Commentaire mis à jour
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user