diff --git a/CHANGELOG.md b/CHANGELOG.md
index c24f76df4..a0612475a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -40,6 +40,9 @@ and this project adheres to
* address reference: add index for refid
* [accompanyingCourse_work] fix styles conflicts + fix bug with remove goal (remove goals one at a time)
* [accompanyingCourse] improve masonry on resume page, add origin
+* [notification] new notification interface, can be associated to AccompanyingCourse/Period, Activities.
+ * List notifications, show, and comment in User section
+ * Notify button and contextual notification box on associated objects pages
## Test releases
diff --git a/composer.json b/composer.json
index 4a93a9f41..d3766bb65 100644
--- a/composer.json
+++ b/composer.json
@@ -33,7 +33,8 @@
"symfony/form": "^4.4",
"symfony/framework-bundle": "^4.4",
"symfony/intl": "^4.4",
- "symfony/mime": "^4.4",
+ "symfony/mailer": "^5.4",
+ "symfony/mime": "^5.4",
"symfony/monolog-bundle": "^3.5",
"symfony/security-bundle": "^4.4",
"symfony/serializer": "^5.3",
@@ -47,7 +48,12 @@
"symfony/yaml": "^4.4",
"twig/extra-bundle": "^3.0",
"twig/intl-extra": "^3.0",
- "twig/markdown-extra": "^3.3"
+ "twig/markdown-extra": "^3.3",
+ "twig/string-extra": "^3.3",
+ "twig/twig": "^3.0"
+ },
+ "conflict": {
+ "symfony/symfony": "*"
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.3",
@@ -65,8 +71,17 @@
"symfony/var-dumper": "^4.4",
"symfony/web-profiler-bundle": "^4.4"
},
- "conflict": {
- "symfony/symfony": "*"
+ "config": {
+ "bin-dir": "bin",
+ "optimize-autoloader": true,
+ "sort-packages": true,
+ "vendor-dir": "tests/app/vendor",
+ "allow-plugins": {
+ "composer/package-versions-deprecated": true,
+ "phpstan/extension-installer": true,
+ "ergebnis/composer-normalize": true,
+ "phpro/grumphp": true
+ }
},
"autoload": {
"psr-4": {
diff --git a/src/Bundle/ChillActivityBundle/Notification/ActivityNotificationHandler.php b/src/Bundle/ChillActivityBundle/Notification/ActivityNotificationHandler.php
new file mode 100644
index 000000000..f391de1ff
--- /dev/null
+++ b/src/Bundle/ChillActivityBundle/Notification/ActivityNotificationHandler.php
@@ -0,0 +1,45 @@
+activityRepository = $activityRepository;
+ }
+
+ public function getTemplate(Notification $notification, array $options = []): string
+ {
+ return '@ChillActivity/Activity/showInNotification.html.twig';
+ }
+
+ public function getTemplateData(Notification $notification, array $options = []): array
+ {
+ return [
+ 'notification' => $notification,
+ 'activity' => $this->activityRepository->find($notification->getRelatedEntityId()),
+ ];
+ }
+
+ public function supports(Notification $notification, array $options = []): bool
+ {
+ return $notification->getRelatedEntityClass() === Activity::class;
+ }
+}
diff --git a/src/Bundle/ChillActivityBundle/Notification/ActivityNotificationRenderer.php b/src/Bundle/ChillActivityBundle/Notification/ActivityNotificationRenderer.php
deleted file mode 100644
index f7f19e3f1..000000000
--- a/src/Bundle/ChillActivityBundle/Notification/ActivityNotificationRenderer.php
+++ /dev/null
@@ -1,33 +0,0 @@
- $notification];
- }
-
- public function supports(Notification $notification, array $options = []): bool
- {
- return $notification->getRelatedEntityClass() === Activity::class;
- }
-}
diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/_list_item.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/_list_item.html.twig
new file mode 100644
index 000000000..7b46ffb67
--- /dev/null
+++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/_list_item.html.twig
@@ -0,0 +1,151 @@
+{% set t = activity.type %}
+
+
+
+
+
+
+ {% if activity.date %}
+
+ {{ activity.date|format_date('short') }}
+
+ {% endif %}
+
+
+
+
+
+ {{ activity.type.name | localize_translatable_string }}
+
+ {% if activity.emergency %}
+ {{ 'Emergency'|trans|upper }}
+ {% endif %}
+
+
+
+
+
+
+
+
+
+ {% if activity.location and t.locationVisible %}
+
+
{{ 'location'|trans }}
+
+
+ {{ activity.location.locationType.title|localize_translatable_string }}
+ {{ activity.location.name }}
+
+
+
+ {% endif %}
+
+ {% if activity.sentReceived is not empty and t.sentReceivedVisible %}
+
+
{{ 'Sent received'|trans }}
+
+
+ {{ activity.sentReceived|capitalize|trans }}
+
+
+
+ {% endif %}
+
+ {% if activity.user and t.userVisible %}
+
+
{{ 'Referrer'|trans }}
+
+
+ {{ activity.user.usernameCanonical|chill_entity_render_string|capitalize }}
+
+
+
+ {% endif %}
+
+
+ {% include 'ChillActivityBundle:Activity:concernedGroups.html.twig' with {
+ 'context': context,
+ 'with_display': 'wrap-list',
+ 'entity': activity,
+ 'badge_person': true
+ } %}
+
+
+ {%- if activity.reasons is not empty and t.reasonsVisible -%}
+
+
+
{{ 'Reasons'|trans }}
+
+
+ {% for r in activity.reasons %}
+
+ {{ r|chill_entity_render_box }}
+
+ {% endfor %}
+
+
+ {% endif %}
+
+ {%- if activity.socialIssues is not empty and t.socialIssuesVisible -%}
+
+
+
{{ 'Social issues'|trans }}
+
+
+ {% for r in activity.socialIssues %}
+
+ {{ r|chill_entity_render_box }}
+
+ {% endfor %}
+
+
+ {% endif %}
+
+ {%- if activity.socialActions is not empty and t.socialActionsVisible -%}
+
+
+
{{ 'Social actions'|trans }}
+
+
+ {% for r in activity.socialActions %}
+
+ {{ r|chill_entity_render_box }}
+
+ {% endfor %}
+
+
+ {% endif %}
+
+ {% if activity.comment.comment is not empty and is_granted('CHILL_ACTIVITY_SEE_DETAILS', activity) %}
+
+
+
{{ 'Comment'|trans }}
+
+
+ {{ activity.comment|chill_entity_render_box({
+ 'disable_markdown': false,
+ 'limit_lines': 3,
+ 'metadata': false
+ }) }}
+
+
+ {% endif %}
+
+ {# Only if ACL SEE_DETAILS AND/OR only on template SHOW ??
+ durationTime
+ travelTime
+ comment
+ documents
+ attendee
+ #}
+
+
+
+
+
+
diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/list.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/list.html.twig
index 3f49e67cc..a64142863 100644
--- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/list.html.twig
+++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/list.html.twig
@@ -1,3 +1,61 @@
+{% 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) %}
+
+
+
+ {% endif %}
+ {% if is_granted('CHILL_ACTIVITY_DELETE', activity) %}
+
+
+
+ {% endif %}
+ {% endif %}
+{% endmacro %}
+
{% if activities|length == 0 %}
@@ -8,203 +66,10 @@
{% else %}
{% for activity in activities %}
- {% set t = activity.type %}
-
-
-
-
-
-
- {% if activity.date %}
-
- {{ activity.date|format_date('short') }}
-
- {% endif %}
-
-
-
-
-
- {{ activity.type.name | localize_translatable_string }}
-
- {% if activity.emergency %}
- {{ 'Emergency'|trans|upper }}
- {% endif %}
-
-
-
-
-
-
-
-
-
- {% if activity.location and t.locationVisible %}
-
-
{{ 'location'|trans }}
-
-
- {{ activity.location.locationType.title|localize_translatable_string }}
- {{ activity.location.name }}
-
-
-
- {% endif %}
-
- {% if activity.sentReceived is not empty and t.sentReceivedVisible %}
-
-
{{ 'Sent received'|trans }}
-
-
- {{ activity.sentReceived|capitalize|trans }}
-
-
-
- {% endif %}
-
- {% if activity.user and t.userVisible %}
-
-
{{ 'Referrer'|trans }}
-
-
- {{ activity.user.usernameCanonical|chill_entity_render_string|capitalize }}
-
-
-
- {% endif %}
-
-
- {% include 'ChillActivityBundle:Activity:concernedGroups.html.twig' with {
- 'context': context,
- 'with_display': 'wrap-list',
- 'entity': activity,
- 'badge_person': true
- } %}
-
-
- {%- if activity.reasons is not empty and t.reasonsVisible -%}
-
-
-
{{ 'Reasons'|trans }}
-
-
- {% for r in activity.reasons %}
-
- {{ r|chill_entity_render_box }}
-
- {% endfor %}
-
-
- {% endif %}
-
- {%- if activity.socialIssues is not empty and t.socialIssuesVisible -%}
-
-
-
{{ 'Social issues'|trans }}
-
-
- {% for r in activity.socialIssues %}
-
- {{ r|chill_entity_render_box }}
-
- {% endfor %}
-
-
- {% endif %}
-
- {%- if activity.socialActions is not empty and t.socialActionsVisible -%}
-
-
-
{{ 'Social actions'|trans }}
-
-
- {% for r in activity.socialActions %}
-
- {{ r|chill_entity_render_box }}
-
- {% endfor %}
-
-
- {% endif %}
-
- {% if activity.comment.comment is not empty and is_granted('CHILL_ACTIVITY_SEE_DETAILS', activity) %}
-
-
-
{{ 'Comment'|trans }}
-
-
- {{ activity.comment|chill_entity_render_box({
- 'disable_markdown': false,
- 'limit_lines': 3,
- 'metadata': false
- }) }}
-
-
- {% endif %}
-
- {# Only if ACL SEE_DETAILS AND/OR only on template SHOW ??
- durationTime
- travelTime
- comment
- documents
- attendee
- #}
-
-
-
-
-
- {% 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) %}
-
-
-
- {% endif %}
- {% if is_granted('CHILL_ACTIVITY_DELETE', activity) %}
-
-
-
- {% endif %}
- {% endif %}
-
-
-
-
+ {% include 'ChillActivityBundle:Activity:_list_item.html.twig' with {
+ 'context': context,
+ 'recordAction': _self.recordAction(activity, context, person_id, accompanying_course_id)
+ } %}
{% endfor %}
{% endif %}
diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/listAccompanyingCourse.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/listAccompanyingCourse.html.twig
index 431a06ba6..a666f183d 100644
--- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/listAccompanyingCourse.html.twig
+++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/listAccompanyingCourse.html.twig
@@ -4,6 +4,17 @@
{% block title %}{{ 'Activity list' |trans }}{% endblock title %}
+
+{% block js %}
+ {{ parent() }}
+ {{ encore_entry_script_tags('mod_notification_toggle_read_status') }}
+{% endblock %}
+
+{% block css %}
+ {{ parent() }}
+ {{ encore_entry_link_tags('mod_notification_toggle_read_status') }}
+{% endblock %}
+
{% block content %}
{% set person_id = null %}
diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/listPerson.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/listPerson.html.twig
index 158bd0e1b..ac4266f8c 100644
--- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/listPerson.html.twig
+++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/listPerson.html.twig
@@ -20,6 +20,16 @@
{% block title %}{{ 'Activity list' |trans }}{% endblock title %}
+{% block js %}
+ {{ parent() }}
+ {{ encore_entry_script_tags('mod_notification_toggle_read_status') }}
+{% endblock %}
+
+{% block css %}
+ {{ parent() }}
+ {{ encore_entry_link_tags('mod_notification_toggle_read_status') }}
+{% endblock %}
+
{% block personcontent %}
{% set person_id = null %}
diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/show.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/show.html.twig
index cb5b135b3..9faacd430 100644
--- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/show.html.twig
+++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/show.html.twig
@@ -198,8 +198,8 @@
{% if is_granted('CHILL_ACTIVITY_UPDATE', entity) %}
-
-
+
+
{{ 'Edit'|trans }}
@@ -212,9 +212,3 @@
{% endif %}
-
diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/showAccompanyingCourse.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/showAccompanyingCourse.html.twig
index b74b315a3..c7fa71f6d 100644
--- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/showAccompanyingCourse.html.twig
+++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/showAccompanyingCourse.html.twig
@@ -4,6 +4,16 @@
{% block title 'Show the activity'|trans %}
+{% block js %}
+ {{ parent() }}
+ {{ encore_entry_script_tags('mod_notification_toggle_read_status') }}
+{% endblock %}
+
+{% block css %}
+ {{ parent() }}
+ {{ encore_entry_link_tags('mod_notification_toggle_read_status') }}
+{% endblock %}
+
{% import 'ChillActivityBundle:ActivityReason:macro.html.twig' as m %}
{% block content -%}
@@ -11,3 +21,21 @@
{% include 'ChillActivityBundle:Activity:show.html.twig' with {'context': 'accompanyingCourse'} %}
{% endblock content %}
+
+{% block block_post_menu %}
+
+{% endblock %}
diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/showInNotification.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/showInNotification.html.twig
index 79badfe26..721538271 100644
--- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/showInNotification.html.twig
+++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/showInNotification.html.twig
@@ -1,2 +1,27 @@
+{% macro recordAction(activity) %}
+
+
+
+{% endmacro %}
-Go to Activity
+{% if activity is not null %}
+
+ {% if is_granted('CHILL_ACTIVITY_SEE', activity) %}
+ {% include 'ChillActivityBundle:Activity:_list_item.html.twig' with {
+ 'recordAction': _self.recordAction(activity),
+ 'context': 'accompanyingCourse',
+ 'itemBlocClass': 'bg-chill-llight-gray'
+ } %}
+ {% else %}
+
+ {{ 'This is the minimal activity data'|trans ~ ': ' ~ activity.id }}
+ {{ 'you are not allowed to see it details'|trans }}
+
+ {% endif %}
+
+{% else %}
+
+ {{ 'You get notified of an activity which does not exists any more'|trans }}
+
+{% endif %}
diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/showPerson.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/showPerson.html.twig
index 5e92f8fa2..fa6cf2ea9 100644
--- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/showPerson.html.twig
+++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/showPerson.html.twig
@@ -4,6 +4,16 @@
{% block title 'Show the activity'|trans %}
+{% block js %}
+ {{ parent() }}
+ {{ encore_entry_script_tags('mod_notification_toggle_read_status') }}
+{% endblock %}
+
+{% block css %}
+ {{ parent() }}
+ {{ encore_entry_link_tags('mod_notification_toggle_read_status') }}
+{% endblock %}
+
{% import 'ChillActivityBundle:ActivityReason:macro.html.twig' as m %}
{% block personcontent -%}
@@ -11,3 +21,21 @@
{% include 'ChillActivityBundle:Activity:show.html.twig' with {'context': 'person'} %}
{% endblock personcontent %}
+
+{% block block_post_menu %}
+
+{% endblock %}
diff --git a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml
index 00715d283..4a21603ca 100644
--- a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml
+++ b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml
@@ -224,3 +224,7 @@ Aggregate by activity reason: Aggréger par sujet de l'activité
Last activities: Les dernières activités
See activity in accompanying course context: Voir l'activité dans le contexte du parcours d'accompagnement
+
+You get notified of an activity which does not exists any more: Cette notification ne correspond pas à une activité valide.
+you are not allowed to see it details: La notification fait référence à une activité à laquelle vous n'avez pas accès.
+This is the minimal activity data: Activité n°
diff --git a/src/Bundle/ChillDocGeneratorBundle/Serializer/Normalizer/CollectionDocGenNormalizer.php b/src/Bundle/ChillDocGeneratorBundle/Serializer/Normalizer/CollectionDocGenNormalizer.php
index 55774dd83..cd01a003a 100644
--- a/src/Bundle/ChillDocGeneratorBundle/Serializer/Normalizer/CollectionDocGenNormalizer.php
+++ b/src/Bundle/ChillDocGeneratorBundle/Serializer/Normalizer/CollectionDocGenNormalizer.php
@@ -26,7 +26,7 @@ class CollectionDocGenNormalizer implements ContextAwareNormalizerInterface, Nor
/**
* @param Collection $object
- * @param null|string $format
+ * @param string|null $format
*
* @return array|ArrayObject|bool|float|int|string|void|null
*/
diff --git a/src/Bundle/ChillDocGeneratorBundle/Serializer/Normalizer/DocGenObjectNormalizer.php b/src/Bundle/ChillDocGeneratorBundle/Serializer/Normalizer/DocGenObjectNormalizer.php
index 0feea511d..6851c7225 100644
--- a/src/Bundle/ChillDocGeneratorBundle/Serializer/Normalizer/DocGenObjectNormalizer.php
+++ b/src/Bundle/ChillDocGeneratorBundle/Serializer/Normalizer/DocGenObjectNormalizer.php
@@ -66,7 +66,7 @@ class DocGenObjectNormalizer implements NormalizerAwareInterface, NormalizerInte
if (!$this->classMetadataFactory->hasMetadataFor($classMetadataKey)) {
throw new LogicException(sprintf(
'This object does not have metadata: %s. Add groups on this entity to allow to serialize with the format %s and groups %s',
- is_object($object) ? get_class($object) : '(todo' /*$context['docgen:expects'],*/,
+ is_object($object) ? get_class($object) : '(todo' /*$context['docgen:expects'],*/ ,
$format,
implode(', ', ($context['groups'] ?? []))
));
diff --git a/src/Bundle/ChillMainBundle/ChillMainBundle.php b/src/Bundle/ChillMainBundle/ChillMainBundle.php
index 2ff7ef862..7a4e563bb 100644
--- a/src/Bundle/ChillMainBundle/ChillMainBundle.php
+++ b/src/Bundle/ChillMainBundle/ChillMainBundle.php
@@ -22,6 +22,7 @@ use Chill\MainBundle\DependencyInjection\CompilerPass\TimelineCompilerClass;
use Chill\MainBundle\DependencyInjection\CompilerPass\WidgetsCompilerPass;
use Chill\MainBundle\DependencyInjection\ConfigConsistencyCompilerPass;
use Chill\MainBundle\DependencyInjection\RoleProvidersCompilerPass;
+use Chill\MainBundle\Notification\NotificationHandlerInterface;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\MainBundle\Search\SearchApiInterface;
use Chill\MainBundle\Security\ProvideRoleInterface;
@@ -29,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;
@@ -50,6 +52,10 @@ class ChillMainBundle extends Bundle
->addTag('chill.render_entity');
$container->registerForAutoconfiguration(SearchApiInterface::class)
->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/NotificationApiController.php b/src/Bundle/ChillMainBundle/Controller/NotificationApiController.php
new file mode 100644
index 000000000..8751aa50a
--- /dev/null
+++ b/src/Bundle/ChillMainBundle/Controller/NotificationApiController.php
@@ -0,0 +1,87 @@
+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);
+ }
+}
diff --git a/src/Bundle/ChillMainBundle/Controller/NotificationController.php b/src/Bundle/ChillMainBundle/Controller/NotificationController.php
index 4eed79740..52f58ed68 100644
--- a/src/Bundle/ChillMainBundle/Controller/NotificationController.php
+++ b/src/Bundle/ChillMainBundle/Controller/NotificationController.php
@@ -11,59 +11,292 @@ declare(strict_types=1);
namespace Chill\MainBundle\Controller;
-use Chill\MainBundle\Notification\NotificationRenderer;
+use Chill\MainBundle\Entity\Notification;
+use Chill\MainBundle\Entity\NotificationComment;
+use Chill\MainBundle\Entity\User;
+use Chill\MainBundle\Form\NotificationCommentType;
+use Chill\MainBundle\Form\NotificationType;
+use Chill\MainBundle\Notification\Exception\NotificationHandlerNotFound;
+use Chill\MainBundle\Notification\NotificationHandlerManager;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Repository\NotificationRepository;
+use Chill\MainBundle\Security\Authorization\NotificationVoter;
+use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
+use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @Route("/{_locale}/notification")
*/
class NotificationController extends AbstractController
{
- private $security;
+ private EntityManagerInterface $em;
- public function __construct(Security $security)
- {
+ private NotificationHandlerManager $notificationHandlerManager;
+
+ private NotificationRepository $notificationRepository;
+
+ private PaginatorFactory $paginatorFactory;
+
+ private Security $security;
+
+ private TranslatorInterface $translator;
+
+ public function __construct(
+ EntityManagerInterface $em,
+ Security $security,
+ NotificationRepository $notificationRepository,
+ NotificationHandlerManager $notificationHandlerManager,
+ PaginatorFactory $paginatorFactory,
+ TranslatorInterface $translator
+ ) {
+ $this->em = $em;
$this->security = $security;
+ $this->notificationRepository = $notificationRepository;
+ $this->notificationHandlerManager = $notificationHandlerManager;
+ $this->paginatorFactory = $paginatorFactory;
+ $this->translator = $translator;
}
/**
- * @Route("/show", name="chill_main_notification_show")
+ * @Route("/create", name="chill_main_notification_create")
*/
- public function showAction(
- NotificationRepository $notificationRepository,
- NotificationRenderer $notificationRenderer,
- PaginatorFactory $paginatorFactory
- ) {
+ public function createAction(Request $request): Response
+ {
+ $this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
+
+ if (!$this->security->getUser() instanceof User) {
+ throw new AccessDeniedHttpException('You must be authenticated and a user to create a notification');
+ }
+
+ if (!$request->query->has('entityClass')) {
+ throw new BadRequestHttpException('Missing entityClass parameter');
+ }
+
+ if (!$request->query->has('entityId')) {
+ throw new BadRequestHttpException('missing entityId parameter');
+ }
+
+ $notification = new Notification();
+ $notification
+ ->setRelatedEntityClass($request->query->get('entityClass'))
+ ->setRelatedEntityId($request->query->getInt('entityId'))
+ ->setSender($this->security->getUser());
+
+ try {
+ $handler = $this->notificationHandlerManager->getHandler($notification);
+ } catch (NotificationHandlerNotFound $e) {
+ throw new BadRequestHttpException('no handler for this notification');
+ }
+
+ $form = $this->createForm(NotificationType::class, $notification);
+
+ $form->handleRequest($request);
+
+ if ($form->isSubmitted() && $form->isValid()) {
+ $this->em->persist($notification);
+ $this->em->flush();
+
+ $this->addFlash('success', $this->translator->trans('notification.Notification created'));
+
+ if ($request->query->has('returnPath')) {
+ return new RedirectResponse($request->query->get('returnPath'));
+ }
+
+ return $this->redirectToRoute('chill_main_homepage');
+ }
+
+ return $this->render('@ChillMain/Notification/create.html.twig', [
+ 'form' => $form->createView(),
+ 'handler' => $handler,
+ 'notification' => $notification,
+ ]);
+ }
+
+ /**
+ * @Route("/{id}/edit", name="chill_main_notification_edit")
+ */
+ public function editAction(Notification $notification, Request $request): Response
+ {
+ $this->denyAccessUnlessGranted(NotificationVoter::NOTIFICATION_UPDATE, $notification);
+
+ $form = $this->createForm(NotificationType::class, $notification);
+
+ $form->handleRequest($request);
+
+ if ($form->isSubmitted() && $form->isValid()) {
+ $this->em->flush();
+
+ $this->addFlash('success', $this->translator->trans('notification.Notification updated'));
+
+ if ($request->query->has('returnPath')) {
+ return new RedirectResponse($request->query->get('returnPath'));
+ }
+
+ return $this->redirectToRoute('chill_main_notification_my');
+ }
+
+ return $this->render('@ChillMain/Notification/edit.html.twig', [
+ 'form' => $form->createView(),
+ 'handler' => $this->notificationHandlerManager->getHandler($notification),
+ 'notification' => $notification,
+ ]);
+ }
+
+ /**
+ * @Route("/inbox", name="chill_main_notification_my")
+ */
+ public function inboxAction(): Response
+ {
+ $this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
$currentUser = $this->security->getUser();
- $notificationsNbr = $notificationRepository->countAllForAttendee(($currentUser));
- $paginator = $paginatorFactory->create($notificationsNbr);
+ $notificationsNbr = $this->notificationRepository->countAllForAttendee(($currentUser));
+ $paginator = $this->paginatorFactory->create($notificationsNbr);
- $notifications = $notificationRepository->findAllForAttendee(
+ $notifications = $this->notificationRepository->findAllForAttendee(
$currentUser,
$limit = $paginator->getItemsPerPage(),
$offset = $paginator->getCurrentPage()->getFirstItemNumber()
);
+ return $this->render('@ChillMain/Notification/list.html.twig', [
+ 'datas' => $this->itemsForTemplate($notifications),
+ 'notifications' => $notifications,
+ 'paginator' => $paginator,
+ 'step' => 'inbox',
+ 'unreads' => $this->countUnread(),
+ ]);
+ }
+
+ /**
+ * @Route("/sent", name="chill_main_notification_sent")
+ */
+ public function sentAction(): Response
+ {
+ $this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
+ $currentUser = $this->security->getUser();
+
+ $notificationsNbr = $this->notificationRepository->countAllForSender($currentUser);
+ $paginator = $this->paginatorFactory->create($notificationsNbr);
+
+ $notifications = $this->notificationRepository->findAllForSender(
+ $currentUser,
+ $limit = $paginator->getItemsPerPage(),
+ $offset = $paginator->getCurrentPage()->getFirstItemNumber()
+ );
+
+ return $this->render('@ChillMain/Notification/list.html.twig', [
+ 'datas' => $this->itemsForTemplate($notifications),
+ 'notifications' => $notifications,
+ 'paginator' => $paginator,
+ 'step' => 'sent',
+ 'unreads' => $this->countUnread(),
+ ]);
+ }
+
+ /**
+ * @Route("/{id}/show", name="chill_main_notification_show")
+ */
+ public function showAction(Notification $notification, Request $request): Response
+ {
+ $this->denyAccessUnlessGranted(NotificationVoter::NOTIFICATION_SEE, $notification);
+
+ 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");
+ }
+
+ $this->denyAccessUnlessGranted(NotificationVoter::COMMENT_EDIT, $editedComment);
+
+ $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 ($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')) {
+ $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' => isset($appendCommentForm) ? $appendCommentForm->createView() : null,
+ 'editedCommentForm' => isset($editedCommentForm) ? $editedCommentForm->createView() : null,
+ 'editedCommentId' => $commentId ?? null,
+ ]);
+
+ // we mark the notification as read after having computed the response
+ if ($this->getUser() instanceof User && !$notification->isReadBy($this->getUser())) {
+ $notification->markAsReadBy($this->getUser());
+ $this->em->flush();
+ }
+
+ return $response;
+ }
+
+ private function countUnread(): array
+ {
+ return [
+ 'sent' => $this->notificationRepository->countUnreadByUserWhereSender($this->security->getUser()),
+ 'inbox' => $this->notificationRepository->countUnreadByUserWhereAddressee($this->security->getUser()),
+ ];
+ }
+
+ private function itemsForTemplate(array $notifications): array
+ {
$templateData = [];
foreach ($notifications as $notification) {
- $data = [
- 'template' => $notificationRenderer->getTemplate($notification),
- 'template_data' => $notificationRenderer->getTemplateData($notification),
+ $templateData[] = [
+ 'template' => $this->notificationHandlerManager->getTemplate($notification),
+ 'template_data' => $this->notificationHandlerManager->getTemplateData($notification),
'notification' => $notification,
];
- $templateData[] = $data;
}
- return $this->render('@ChillMain/Notification/show.html.twig', [
- 'datas' => $templateData,
- 'notifications' => $notifications,
- 'paginator' => $paginator,
- ]);
+ return $templateData;
}
}
diff --git a/src/Bundle/ChillMainBundle/Entity/Notification.php b/src/Bundle/ChillMainBundle/Entity/Notification.php
index cba4f33e5..e566a8d78 100644
--- a/src/Bundle/ChillMainBundle/Entity/Notification.php
+++ b/src/Bundle/ChillMainBundle/Entity/Notification.php
@@ -11,7 +11,9 @@ declare(strict_types=1);
namespace Chill\MainBundle\Entity;
+use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use DateTimeImmutable;
+use DateTimeInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
@@ -20,19 +22,27 @@ use Doctrine\ORM\Mapping as ORM;
* @ORM\Entity
* @ORM\Table(
* name="chill_main_notification",
- * uniqueConstraints={
- * @ORM\UniqueConstraint(columns={"relatedEntityClass", "relatedEntityId"})
- * }
* )
+ * @ORM\HasLifecycleCallbacks
*/
-class Notification
+class Notification implements TrackUpdateInterface
{
+ private array $addedAddresses = [];
+
/**
* @ORM\ManyToMany(targetEntity=User::class)
* @ORM\JoinTable(name="chill_main_notification_addresses_user")
*/
private Collection $addressees;
+ private ?ArrayCollection $addressesOnLoad = null;
+
+ /**
+ * @ORM\OneToMany(targetEntity=NotificationComment::class, mappedBy="notification", orphanRemoval=true)
+ * @ORM\OrderBy({"createdAt": "ASC"})
+ */
+ private Collection $comments;
+
/**
* @ORM\Column(type="datetime_immutable")
*/
@@ -43,43 +53,84 @@ class Notification
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
- private int $id;
+ private ?int $id = null;
/**
* @ORM\Column(type="text")
*/
- private string $message;
-
- /**
- * @ORM\Column(type="json")
- */
- private array $read;
+ private string $message = '';
/**
* @ORM\Column(type="string", length=255)
*/
- private string $relatedEntityClass;
+ private string $relatedEntityClass = '';
/**
* @ORM\Column(type="integer")
*/
private int $relatedEntityId;
+ private array $removedAddresses = [];
+
/**
* @ORM\ManyToOne(targetEntity=User::class)
- * @ORM\JoinColumn(nullable=false)
+ * @ORM\JoinColumn(nullable=true)
*/
- private User $sender;
+ private ?User $sender = null;
+
+ /**
+ * @ORM\Column(type="text", options={"default": ""})
+ */
+ private string $title = '';
+
+ /**
+ * @ORM\ManyToMany(targetEntity=User::class)
+ * @ORM\JoinTable(name="chill_main_notification_addresses_unread")
+ */
+ private Collection $unreadBy;
+
+ /**
+ * @ORM\Column(type="datetime_immutable")
+ */
+ private ?DateTimeImmutable $updatedAt;
+
+ /**
+ * @ORM\ManyToOne(targetEntity=User::class)
+ */
+ private ?User $updatedBy;
public function __construct()
{
$this->addressees = new ArrayCollection();
+ $this->unreadBy = new ArrayCollection();
+ $this->comments = new ArrayCollection();
+ $this->setDate(new DateTimeImmutable());
}
public function addAddressee(User $addressee): self
{
if (!$this->addressees->contains($addressee)) {
$this->addressees[] = $addressee;
+ $this->addedAddresses[] = $addressee;
+ }
+
+ return $this;
+ }
+
+ public function addComment(NotificationComment $comment): self
+ {
+ if (!$this->comments->contains($comment)) {
+ $this->comments[] = $comment;
+ $comment->setNotification($this);
+ }
+
+ return $this;
+ }
+
+ public function addUnreadBy(User $user): self
+ {
+ if (!$this->unreadBy->contains($user)) {
+ $this->unreadBy[] = $user;
}
return $this;
@@ -90,9 +141,19 @@ class Notification
*/
public function getAddressees(): Collection
{
+ // keep a copy to compute changes later
+ if (null === $this->addressesOnLoad) {
+ $this->addressesOnLoad = new ArrayCollection($this->addressees->toArray());
+ }
+
return $this->addressees;
}
+ public function getComments(): Collection
+ {
+ return $this->comments;
+ }
+
public function getDate(): ?DateTimeImmutable
{
return $this->date;
@@ -108,11 +169,6 @@ class Notification
return $this->message;
}
- public function getRead(): array
- {
- return $this->read;
- }
-
public function getRelatedEntityClass(): ?string
{
return $this->relatedEntityClass;
@@ -128,9 +184,97 @@ class Notification
return $this->sender;
}
+ public function getTitle(): string
+ {
+ return $this->title;
+ }
+
+ public function getUnreadBy(): Collection
+ {
+ return $this->unreadBy;
+ }
+
+ public function getUpdatedAt(): ?DateTimeImmutable
+ {
+ return $this->updatedAt;
+ }
+
+ public function getUpdatedBy(): ?User
+ {
+ return $this->updatedBy;
+ }
+
+ public function isReadBy(User $user): bool
+ {
+ return !$this->unreadBy->contains($user);
+ }
+
+ public function isSystem(): bool
+ {
+ return null === $this->sender;
+ }
+
+ public function markAsReadBy(User $user): self
+ {
+ return $this->removeUnreadBy($user);
+ }
+
+ public function markAsUnreadBy(User $user): self
+ {
+ return $this->addUnreadBy($user);
+ }
+
+ /**
+ * @ORM\PreFlush
+ */
+ public function registerUnread()
+ {
+ foreach ($this->addedAddresses as $addressee) {
+ $this->addUnreadBy($addressee);
+ }
+
+ foreach ($this->removedAddresses as $addressee) {
+ $this->removeAddressee($addressee);
+ }
+
+ if (null !== $this->addressesOnLoad) {
+ foreach ($this->addressees as $existingAddresse) {
+ if (!$this->addressesOnLoad->contains($existingAddresse)) {
+ $this->addUnreadBy($existingAddresse);
+ }
+ }
+
+ foreach ($this->addressesOnLoad as $onLoadAddressee) {
+ if (!$this->addressees->contains($onLoadAddressee)) {
+ $this->removeUnreadBy($onLoadAddressee);
+ }
+ }
+ }
+
+ $this->removedAddresses = [];
+ $this->addedAddresses = [];
+ $this->addressesOnLoad = null;
+ }
+
public function removeAddressee(User $addressee): self
{
- $this->addressees->removeElement($addressee);
+ if ($this->addressees->removeElement($addressee)) {
+ $this->removedAddresses[] = $addressee;
+ }
+
+ return $this;
+ }
+
+ public function removeComment(NotificationComment $comment): self
+ {
+ $this->comments->removeElement($comment);
+
+ return $this;
+ }
+
+ public function removeUnreadBy(User $user): self
+ {
+ $this->unreadBy->removeElement($user);
return $this;
}
@@ -149,13 +293,6 @@ class Notification
return $this;
}
- public function setRead(array $read): self
- {
- $this->read = $read;
-
- return $this;
- }
-
public function setRelatedEntityClass(string $relatedEntityClass): self
{
$this->relatedEntityClass = $relatedEntityClass;
@@ -176,4 +313,25 @@ class Notification
return $this;
}
+
+ public function setTitle(string $title): Notification
+ {
+ $this->title = $title;
+
+ return $this;
+ }
+
+ public function setUpdatedAt(DateTimeInterface $datetime): self
+ {
+ $this->updatedAt = $datetime;
+
+ return $this;
+ }
+
+ public function setUpdatedBy(User $user): self
+ {
+ $this->updatedBy = $user;
+
+ return $this;
+ }
}
diff --git a/src/Bundle/ChillMainBundle/Entity/NotificationComment.php b/src/Bundle/ChillMainBundle/Entity/NotificationComment.php
new file mode 100644
index 000000000..01911c729
--- /dev/null
+++ b/src/Bundle/ChillMainBundle/Entity/NotificationComment.php
@@ -0,0 +1,191 @@
+content;
+ }
+
+ public function getCreatedAt(): ?DateTimeImmutable
+ {
+ return $this->createdAt;
+ }
+
+ public function getCreatedBy(): ?User
+ {
+ return $this->createdBy;
+ }
+
+ public function getId(): ?int
+ {
+ return $this->id;
+ }
+
+ public function getNotification(): ?Notification
+ {
+ return $this->notification;
+ }
+
+ public function getUpdatedAt(): ?DateTimeImmutable
+ {
+ return $this->updateAt;
+ }
+
+ public function getUpdatedBy(): ?User
+ {
+ return $this->updatedBy;
+ }
+
+ /**
+ * @ORM\PreFlush
+ */
+ public function onFlushMarkNotificationAsUnread(PreFlushEventArgs $eventArgs): void
+ {
+ if ($this->recentlyPersisted) {
+ foreach ($this->getNotification()->getAddressees() as $addressee) {
+ if ($this->getCreatedBy() !== $addressee) {
+ $this->getNotification()->markAsUnreadBy($addressee);
+ }
+ }
+
+ if ($this->getNotification()->getSender() !== $this->getCreatedBy()) {
+ $this->getNotification()->markAsUnreadBy($this->getNotification()->getSender());
+ }
+ }
+ }
+
+ /**
+ * @ORM\PrePersist
+ */
+ public function onPrePersist(LifecycleEventArgs $eventArgs): void
+ {
+ $this->recentlyPersisted = true;
+ }
+
+ public function setContent(string $content): self
+ {
+ $this->content = $content;
+
+ return $this;
+ }
+
+ public function setCreatedAt(DateTimeInterface $datetime): self
+ {
+ $this->createdAt = $datetime;
+
+ return $this;
+ }
+
+ public function setCreatedBy(User $user): self
+ {
+ $this->createdBy = $user;
+
+ return $this;
+ }
+
+ /**
+ * @internal use Notification::addComment
+ */
+ public function setNotification(?Notification $notification): self
+ {
+ $this->notification = $notification;
+
+ return $this;
+ }
+
+ /**
+ * @deprecated use @see{self::setUpdatedAt} instead
+ */
+ public function setUpdateAt(?DateTimeImmutable $updateAt): self
+ {
+ return $this->setUpdatedAt($updateAt);
+ }
+
+ public function setUpdatedAt(DateTimeInterface $datetime): self
+ {
+ $this->updateAt = $datetime;
+
+ return $this;
+ }
+
+ public function setUpdatedBy(User $user): self
+ {
+ $this->updatedBy = $user;
+
+ return $this;
+ }
+}
diff --git a/src/Bundle/ChillMainBundle/Form/NotificationCommentType.php b/src/Bundle/ChillMainBundle/Form/NotificationCommentType.php
new file mode 100644
index 000000000..7aa3a6cb1
--- /dev/null
+++ b/src/Bundle/ChillMainBundle/Form/NotificationCommentType.php
@@ -0,0 +1,26 @@
+add('content', ChillTextareaType::class, [
+ 'required' => false,
+ ]);
+ }
+}
diff --git a/src/Bundle/ChillMainBundle/Form/NotificationType.php b/src/Bundle/ChillMainBundle/Form/NotificationType.php
new file mode 100644
index 000000000..b24513524
--- /dev/null
+++ b/src/Bundle/ChillMainBundle/Form/NotificationType.php
@@ -0,0 +1,43 @@
+add('title', TextType::class, [
+ 'label' => 'Title',
+ 'required' => true,
+ ])
+ ->add('addressees', PickUserDynamicType::class, [
+ 'multiple' => true,
+ ])
+ ->add('message', ChillTextareaType::class, [
+ 'required' => false,
+ ]);
+ }
+
+ public function configureOptions(OptionsResolver $resolver)
+ {
+ $resolver->setDefault('class', Notification::class);
+ }
+}
diff --git a/src/Bundle/ChillMainBundle/Form/Type/DataTransformer/UserToJsonTransformer.php b/src/Bundle/ChillMainBundle/Form/Type/DataTransformer/UserToJsonTransformer.php
new file mode 100644
index 000000000..df670f891
--- /dev/null
+++ b/src/Bundle/ChillMainBundle/Form/Type/DataTransformer/UserToJsonTransformer.php
@@ -0,0 +1,81 @@
+denormalizer = $denormalizer;
+ $this->serializer = $serializer;
+ $this->multiple = $multiple;
+ }
+
+ public function reverseTransform($value)
+ {
+ if ($this->multiple) {
+ return array_map(
+ function ($item) { return $this->denormalizeOne($item); },
+ json_decode($value, true)
+ );
+ }
+
+ return $this->denormalizeOne(json_decode($value, true));
+ }
+
+ /**
+ * @param User|User[] $value
+ */
+ public function transform($value): string
+ {
+ if (null === $value) {
+ return $this->multiple ? 'null' : '[]';
+ }
+
+ return $this->serializer->serialize($value, 'json', [
+ AbstractNormalizer::GROUPS => ['read'],
+ ]);
+ }
+
+ private function denormalizeOne(array $item): User
+ {
+ if (!array_key_exists('type', $item)) {
+ throw new TransformationFailedException('the key "type" is missing on element');
+ }
+
+ if (!array_key_exists('id', $item)) {
+ throw new TransformationFailedException('the key "id" is missing on element');
+ }
+
+ return
+ $this->denormalizer->denormalize(
+ ['type' => $item['type'], 'id' => $item['id']],
+ User::class,
+ 'json',
+ [AbstractNormalizer::GROUPS => ['read']],
+ );
+ }
+}
diff --git a/src/Bundle/ChillMainBundle/Form/Type/PickUserDynamicType.php b/src/Bundle/ChillMainBundle/Form/Type/PickUserDynamicType.php
new file mode 100644
index 000000000..52798608e
--- /dev/null
+++ b/src/Bundle/ChillMainBundle/Form/Type/PickUserDynamicType.php
@@ -0,0 +1,63 @@
+denormalizer = $denormalizer;
+ $this->serializer = $serializer;
+ }
+
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ $builder->addViewTransformer(new UserToJsonTransformer($this->denormalizer, $this->serializer, $options['multiple']));
+ }
+
+ public function buildView(FormView $view, FormInterface $form, array $options)
+ {
+ $view->vars['multiple'] = $options['multiple'];
+ $view->vars['types'] = ['user'];
+ $view->vars['uniqid'] = uniqid('pick_user_dyn');
+ }
+
+ public function configureOptions(OptionsResolver $resolver)
+ {
+ $resolver
+ ->setDefault('multiple', false)
+ ->setAllowedTypes('multiple', ['bool'])
+ ->setDefault('compound', false);
+ }
+
+ public function getBlockPrefix()
+ {
+ return 'pick_user_dynamic';
+ }
+}
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/Notification/Email/NotificationMailer.php b/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php
new file mode 100644
index 000000000..658764c26
--- /dev/null
+++ b/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php
@@ -0,0 +1,110 @@
+mailer = $mailer;
+ $this->logger = $logger;
+ $this->translator = $translator;
+ }
+
+ public function postPersistComment(NotificationComment $comment, LifecycleEventArgs $eventArgs): void
+ {
+ foreach (
+ array_merge(
+ $comment->getNotification()->getAddressees()->toArray(),
+ [$comment->getNotification()->getSender()]
+ ) as $dest
+ ) {
+ if (null === $dest->getEmail() || $comment->getCreatedBy() !== $dest) {
+ continue;
+ }
+ $email = new TemplatedEmail();
+ $email
+ ->to($dest->getEmail())
+ ->subject('Re: [Chill] ' . $comment->getNotification()->getTitle())
+ ->textTemplate('@ChillMain/Notification/email_notification_comment_persist.fr.md.twig')
+ ->context([
+ 'comment' => $comment,
+ 'dest' => $dest,
+ ]);
+
+ try {
+ $this->mailer->send($email);
+ } catch (TransportExceptionInterface $e) {
+ $this->logger->warning('[NotificationMailer] could not send an email notification about comment', [
+ 'to' => $dest->getEmail(),
+ 'error_message' => $e->getMessage(),
+ 'error_trace' => $e->getTraceAsString(),
+ ]);
+ }
+ }
+ }
+
+ /**
+ * Send a email after a notification is persisted.
+ */
+ public function postPersistNotification(Notification $notification, LifecycleEventArgs $eventArgs): void
+ {
+ foreach ($notification->getAddressees() as $addressee) {
+ if (null === $addressee->getEmail()) {
+ continue;
+ }
+
+ if ($notification->isSystem()) {
+ $email = new Email();
+ $email
+ ->text($notification->getMessage())
+ ->subject('[Chill] ' . $notification->getTitle());
+ } else {
+ $email = new TemplatedEmail();
+ $email
+ ->textTemplate('@ChillMain/Notification/email_non_system_notification_content.fr.md.twig')
+ ->context([
+ 'notification' => $notification,
+ 'dest' => $addressee,
+ ]);
+ }
+
+ $email->to($addressee->getEmail());
+
+ try {
+ $this->mailer->send($email);
+ } catch (TransportExceptionInterface $e) {
+ $this->logger->warning('[NotificationMailer] could not send an email notification', [
+ 'to' => $addressee->getEmail(),
+ 'error_message' => $e->getMessage(),
+ 'error_trace' => $e->getTraceAsString(),
+ ]);
+ }
+ }
+ }
+}
diff --git a/src/Bundle/ChillMainBundle/Notification/Exception/NotificationHandlerNotFound.php b/src/Bundle/ChillMainBundle/Notification/Exception/NotificationHandlerNotFound.php
new file mode 100644
index 000000000..00add5f4f
--- /dev/null
+++ b/src/Bundle/ChillMainBundle/Notification/Exception/NotificationHandlerNotFound.php
@@ -0,0 +1,18 @@
+handlers = $handlers;
+ $this->em = $em;
+ }
+
+ /**
+ * @throw NotificationHandlerNotFound if handler is not found
+ */
+ public function getHandler(Notification $notification, array $options = []): NotificationHandlerInterface
+ {
+ foreach ($this->handlers as $renderer) {
+ if ($renderer->supports($notification, $options)) {
+ return $renderer;
+ }
+ }
+
+ throw new NotificationHandlerNotFound();
+ }
+
+ public function getTemplate(Notification $notification, array $options = []): string
+ {
+ return $this->getHandler($notification, $options)->getTemplate($notification, $options);
+ }
+
+ public function getTemplateData(Notification $notification, array $options = []): array
+ {
+ return $this->getHandler($notification, $options)->getTemplateData($notification, $options);
+ }
+}
diff --git a/src/Bundle/ChillMainBundle/Notification/NotificationPresence.php b/src/Bundle/ChillMainBundle/Notification/NotificationPresence.php
new file mode 100644
index 000000000..91bea3197
--- /dev/null
+++ b/src/Bundle/ChillMainBundle/Notification/NotificationPresence.php
@@ -0,0 +1,51 @@
+security = $security;
+ $this->notificationRepository = $notificationRepository;
+ }
+
+ /**
+ * @return array|Notification[]
+ */
+ public function getNotificationsForClassAndEntity(string $relatedEntityClass, int $relatedEntityId): array
+ {
+ $user = $this->security->getUser();
+
+ if ($user instanceof User) {
+ return $this->notificationRepository->findNotificationByRelatedEntityAndUserAssociated(
+ $relatedEntityClass,
+ $relatedEntityId,
+ $user
+ );
+ }
+
+ return [];
+ }
+}
diff --git a/src/Bundle/ChillMainBundle/Notification/NotificationRenderer.php b/src/Bundle/ChillMainBundle/Notification/NotificationRenderer.php
deleted file mode 100644
index 048dfc02f..000000000
--- a/src/Bundle/ChillMainBundle/Notification/NotificationRenderer.php
+++ /dev/null
@@ -1,54 +0,0 @@
-renderers[] = $accompanyingPeriodNotificationRenderer;
- $this->renderers[] = $activityNotificationRenderer;
- }
-
- public function getTemplate(Notification $notification)
- {
- return $this->getRenderer($notification)->getTemplate();
- }
-
- public function getTemplateData(Notification $notification)
- {
- return $this->getRenderer($notification)->getTemplateData($notification);
- }
-
- private function getRenderer(Notification $notification)
- {
- foreach ($this->renderers as $renderer) {
- if ($renderer->supports($notification)) {
- return $renderer;
- }
- }
-
- throw new Exception('No renderer for ' . $notification);
- }
-}
diff --git a/src/Bundle/ChillMainBundle/Notification/Templating/NotificationTwigExtension.php b/src/Bundle/ChillMainBundle/Notification/Templating/NotificationTwigExtension.php
new file mode 100644
index 000000000..115adf06b
--- /dev/null
+++ b/src/Bundle/ChillMainBundle/Notification/Templating/NotificationTwigExtension.php
@@ -0,0 +1,28 @@
+ true,
+ 'is_safe' => ['html'],
+ ]),
+ ];
+ }
+}
diff --git a/src/Bundle/ChillMainBundle/Notification/Templating/NotificationTwigExtensionRuntime.php b/src/Bundle/ChillMainBundle/Notification/Templating/NotificationTwigExtensionRuntime.php
new file mode 100644
index 000000000..e35a39ac0
--- /dev/null
+++ b/src/Bundle/ChillMainBundle/Notification/Templating/NotificationTwigExtensionRuntime.php
@@ -0,0 +1,39 @@
+notificationPresence = $notificationPresence;
+ }
+
+ public function listNotificationsFor(Environment $environment, string $relatedEntityClass, int $relatedEntityId, array $options = []): string
+ {
+ $notifications = $this->notificationPresence->getNotificationsForClassAndEntity($relatedEntityClass, $relatedEntityId);
+
+ if ([] === $notifications) {
+ return '';
+ }
+
+ return $environment->render('@ChillMain/Notification/extension_list_notifications_for.html.twig', [
+ 'notifications' => $notifications,
+ ]);
+ }
+}
diff --git a/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php b/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php
index 4af19550c..35ec64114 100644
--- a/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php
+++ b/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php
@@ -13,25 +13,76 @@ namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Entity\User;
+use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query;
+use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectRepository;
final class NotificationRepository implements ObjectRepository
{
+ private EntityManagerInterface $em;
+
private EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager)
{
+ $this->em = $entityManager;
$this->repository = $entityManager->getRepository(Notification::class);
}
- public function countAllForAttendee(User $addressee): int // TODO passer à attendees avec S
+ public function countAllForAttendee(User $addressee): int
{
- $query = $this->queryAllForAttendee($addressee, $countQuery = true);
+ return $this->queryByAddressee($addressee)
+ ->select('count(n)')
+ ->getQuery()
+ ->getSingleScalarResult();
+ }
- return $query->getSingleScalarResult();
+ public function countAllForSender(User $sender): int
+ {
+ return $this->queryBySender($sender)
+ ->select('count(n)')
+ ->getQuery()
+ ->getSingleScalarResult();
+ }
+
+ public function countUnreadByUser(User $user): int
+ {
+ $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)
+ ->setParameter('userId', $user->getId());
+
+ return $nq->getSingleScalarResult();
+ }
+
+ public function countUnreadByUserWhereAddressee(User $user): int
+ {
+ $qb = $this->repository->createQueryBuilder('n');
+ $qb
+ ->select('count(n)')
+ ->where($qb->expr()->isMemberOf(':user', 'n.addressees'))
+ ->andWhere($qb->expr()->isMemberOf(':user', 'n.unreadBy'))
+ ->setParameter('user', $user);
+
+ return $qb->getQuery()->getSingleScalarResult();
+ }
+
+ public function countUnreadByUserWhereSender(User $user): int
+ {
+ $qb = $this->repository->createQueryBuilder('n');
+ $qb
+ ->select('count(n)')
+ ->where($qb->expr()->eq('n.sender', ':user'))
+ ->andWhere($qb->expr()->isMemberOf(':user', 'n.unreadBy'))
+ ->setParameter('user', $user);
+
+ return $qb->getQuery()->getSingleScalarResult();
}
public function find($id, $lockMode = null, $lockVersion = null): ?Notification
@@ -53,9 +104,9 @@ final class NotificationRepository implements ObjectRepository
*
* @return Notification[]
*/
- public function findAllForAttendee(User $addressee, $limit = null, $offset = null): array // TODO passer à attendees avec S
+ public function findAllForAttendee(User $addressee, $limit = null, $offset = null): array
{
- $query = $this->queryAllForAttendee($addressee);
+ $query = $this->queryByAddressee($addressee)->select('n');
if ($limit) {
$query = $query->setMaxResults($limit);
@@ -65,7 +116,26 @@ final class NotificationRepository implements ObjectRepository
$query = $query->setFirstResult($offset);
}
- return $query->getResult();
+ $query->addOrderBy('n.date', 'DESC');
+
+ return $query->getQuery()->getResult();
+ }
+
+ public function findAllForSender(User $sender, $limit = null, $offset = null): array
+ {
+ $query = $this->queryBySender($sender)->select('n');
+
+ if ($limit) {
+ $query = $query->setMaxResults($limit);
+ }
+
+ if ($offset) {
+ $query = $query->setFirstResult($offset);
+ }
+
+ $query->addOrderBy('n.date', 'DESC');
+
+ return $query->getQuery()->getResult();
}
/**
@@ -79,6 +149,31 @@ final class NotificationRepository implements ObjectRepository
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
+ /**
+ * @return array|Notification[]
+ */
+ public function findNotificationByRelatedEntityAndUserAssociated(string $relatedEntityClass, int $relatedEntityId, User $user): array
+ {
+ $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'))
+ ->andWhere(
+ $qb->expr()->orX(
+ $qb->expr()->isMemberOf(':user', 'n.addressees'),
+ $qb->expr()->eq('n.sender', ':user')
+ )
+ )
+ ->setParameter('relatedEntityClass', $relatedEntityClass)
+ ->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);
@@ -89,22 +184,25 @@ final class NotificationRepository implements ObjectRepository
return Notification::class;
}
- private function queryAllForAttendee(User $addressee, bool $countQuery = false): Query
+ private function queryByAddressee(User $addressee, bool $countQuery = false): QueryBuilder
{
$qb = $this->repository->createQueryBuilder('n');
- $select = 'n';
-
- if ($countQuery) {
- $select = 'count(n)';
- }
-
$qb
- ->select($select)
- ->join('n.addressees', 'a')
- ->where('a = :addressee')
+ ->where($qb->expr()->isMemberOf(':addressee', 'n.addressees'))
->setParameter('addressee', $addressee);
- return $qb->getQuery();
+ return $qb;
+ }
+
+ private function queryBySender(User $sender): QueryBuilder
+ {
+ $qb = $this->repository->createQueryBuilder('n');
+
+ $qb
+ ->where($qb->expr()->eq('n.sender', ':sender'))
+ ->setParameter('sender', $sender);
+
+ return $qb;
}
}
diff --git a/src/Bundle/ChillMainBundle/Resources/public/chill/chillmain.scss b/src/Bundle/ChillMainBundle/Resources/public/chill/chillmain.scss
index e7f613bc8..a570b645a 100644
--- a/src/Bundle/ChillMainBundle/Resources/public/chill/chillmain.scss
+++ b/src/Bundle/ChillMainBundle/Resources/public/chill/chillmain.scss
@@ -25,6 +25,8 @@
// Chill flex responsive table/block presentation
@import './scss/flex_table';
+// Specific templates
+@import './scss/notification';
/*
* BASE LAYOUT POSITION
diff --git a/src/Bundle/ChillMainBundle/Resources/public/chill/scss/buttons.scss b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/buttons.scss
index 14c22685b..86e2f2299 100644
--- a/src/Bundle/ChillMainBundle/Resources/public/chill/scss/buttons.scss
+++ b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/buttons.scss
@@ -20,6 +20,7 @@ $chill-theme-buttons: (
"misc": $gray-300,
"cancel": $gray-300,
"choose": $gray-300,
+ "notify": $gray-300,
"unlink": $chill-red,
);
@@ -73,6 +74,7 @@ $chill-theme-buttons: (
&.btn-delete::before,
&.btn-remove::before,
&.btn-choose::before,
+ &.btn-notify::before,
&.btn-cancel::before {
font: normal normal normal 14px/1 ForkAwesome;
margin-right: 0.5em;
@@ -98,6 +100,7 @@ $chill-theme-buttons: (
&.btn-cancel::before { content: "\f060"; } // fa-arrow-left
&.btn-choose::before { content: "\f00c"; } // fa-check // f046 fa-check-square-o
&.btn-unlink::before { content: "\f127"; } // fa-chain-broken
+ &.btn-notify::before { content: "\f1d8"; } // fa-paper-plane
}
diff --git a/src/Bundle/ChillMainBundle/Resources/public/chill/scss/notification.scss b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/notification.scss
new file mode 100644
index 000000000..23993299d
--- /dev/null
+++ b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/notification.scss
@@ -0,0 +1,62 @@
+div.notification {
+ h2.notification-title,
+ h6.notification-title {
+ a {
+ text-decoration: none;
+ }
+ &::before {
+ font-family: "ForkAwesome";
+ font-size: 80%;
+ margin-right: 0.3em;
+ }
+ }
+ div.read {
+ h2.notification-title,
+ h6.notification-title {
+ font-weight: 500;
+ &::before {
+ content: "\f2b7"; //envelope-open-o
+ }
+ }
+ }
+ div.unread {
+ h2.notification-title,
+ h6.notification-title {
+ &::before {
+ content: "\f003"; //envelope-o
+ }
+ }
+ }
+}
+
+/*
+ * Notifications List
+*/
+
+div.notification-list,
+div.notification-show {
+ div.item-bloc {
+ div.item-row.header {
+
+ div.item-col {
+ &:first-child {
+ flex-grow: 1;
+
+ }
+ &:last-child {
+ flex-grow: 0;
+ }
+ }
+
+ ul.small_in_title {
+ list-style-type: circle;
+ li {
+ span.item-key {
+ display: inline-block;
+ width: 3em;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/Bundle/ChillMainBundle/Resources/public/module/notification/toggle_read.js b/src/Bundle/ChillMainBundle/Resources/public/module/notification/toggle_read.js
new file mode 100644
index 000000000..82a118f77
--- /dev/null
+++ b/src/Bundle/ChillMainBundle/Resources/public/module/notification/toggle_read.js
@@ -0,0 +1,43 @@
+import {createApp} from "vue";
+import NotificationReadToggle from "ChillMainAssets/vuejs/_components/Notification/NotificationReadToggle.vue";
+import { _createI18n } from "ChillMainAssets/vuejs/_js/i18n";
+
+const i18n = _createI18n({});
+
+window.addEventListener('DOMContentLoaded', function (e) {
+ document.querySelectorAll('.notification_toggle_read_status')
+ .forEach(function (el) {
+ createApp({
+ template: ' ',
+ components: {
+ NotificationReadToggle,
+ },
+ data() {
+ return {
+ notificationId: +el.dataset.notificationId,
+ buttonClass: el.dataset.buttonClass,
+ buttonNoText: 'false' === el.dataset.buttonText,
+ showUrl: el.dataset.showButtonUrl,
+ isRead: 1 === +el.dataset.notificationCurrentIsRead,
+ }
+ },
+ methods: {
+ onMarkRead() {
+ this.isRead = false;
+ },
+ onMarkUnread() {
+ this.isRead = true;
+ },
+ }
+ })
+ .use(i18n)
+ .mount(el);
+ });
+})
diff --git a/src/Bundle/ChillMainBundle/Resources/public/module/pick-entity/index.js b/src/Bundle/ChillMainBundle/Resources/public/module/pick-entity/index.js
new file mode 100644
index 000000000..83a890cd9
--- /dev/null
+++ b/src/Bundle/ChillMainBundle/Resources/public/module/pick-entity/index.js
@@ -0,0 +1,69 @@
+import { createApp } from 'vue';
+import PickEntity from 'ChillMainAssets/vuejs/PickEntity/PickEntity.vue';
+import { _createI18n } from 'ChillMainAssets/vuejs/_js/i18n';
+import { appMessages } from 'ChillMainAssets/vuejs/PickEntity/i18n';
+
+const i18n = _createI18n(appMessages);
+
+window.addEventListener('DOMContentLoaded', function(e) {
+
+ let apps = document.querySelectorAll('[data-module="pick-dynamic"]');
+
+ apps.forEach(function(el) {
+
+ const
+ isMultiple = parseInt(el.dataset.multiple) === 1,
+ input = document.querySelector('[data-input-uniqid="'+ el.dataset.uniqid +'"]'),
+ picked = isMultiple ? JSON.parse(input.value) : [JSON.parse(input.value)];
+
+ createApp({
+ template: ' ',
+ components: {
+ PickEntity,
+ },
+ data() {
+ return {
+ multiple: isMultiple,
+ types: JSON.parse(el.dataset.types),
+ picked,
+ uniqid: el.dataset.uniqid,
+ }
+ },
+ methods: {
+ addNewEntity(entity) {
+ console.log('addNewEntity', entity);
+ if (this.multiple) {
+ console.log('adding multiple');
+ if (!this.picked.some(el => {
+ return el.type === entity.type && el.id === entity.id;
+ })) {
+ this.picked.push(entity);
+ input.value = JSON.stringify(this.picked);
+ }
+ } else {
+ if (!this.picked.some(el => {
+ return el.type === entity.type && el.id === entity.id;
+ })) {
+ this.picked.splice(0, this.picked.length);
+ this.picked.push(entity);
+ input.value = JSON.stringify(this.picked[0]);
+ }
+ }
+ },
+ removeEntity(entity) {
+ console.log('removeEntity', entity);
+ this.picked = this.picked.filter(e => !(e.type === entity.type && e.id === entity.id));
+ input.value = JSON.stringify(this.picked);
+ },
+ }
+ })
+ .use(i18n)
+ .mount(el);
+ });
+});
diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/PickEntity/PickEntity.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/PickEntity/PickEntity.vue
new file mode 100644
index 000000000..43f3ebf94
--- /dev/null
+++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/PickEntity/PickEntity.vue
@@ -0,0 +1,89 @@
+
+
+
+
+
+
diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/PickEntity/i18n.js b/src/Bundle/ChillMainBundle/Resources/public/vuejs/PickEntity/i18n.js
new file mode 100644
index 000000000..2c4432218
--- /dev/null
+++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/PickEntity/i18n.js
@@ -0,0 +1,17 @@
+import { personMessages } from 'ChillPersonAssets/vuejs/_js/i18n';
+
+const appMessages = {
+ fr: {
+ pick_entity: {
+ add: 'Ajouter',
+ modal_title: 'Ajouter des ',
+ user: 'Utilisateurs',
+ person: 'Usagers',
+ thirdparty: 'Tiers',
+ }
+ }
+}
+
+Object.assign(appMessages.fr, personMessages.fr);
+
+export { appMessages };
diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/Notification/NotificationReadToggle.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/Notification/NotificationReadToggle.vue
new file mode 100644
index 000000000..f1b246626
--- /dev/null
+++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/Notification/NotificationReadToggle.vue
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+ {{ $t('markAsUnread') }}
+
+
+
+
+
+
+ {{ $t('markAsRead') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Bundle/ChillMainBundle/Resources/views/Form/fields.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Form/fields.html.twig
index 1426841bb..25f1363c3 100644
--- a/src/Bundle/ChillMainBundle/Resources/views/Form/fields.html.twig
+++ b/src/Bundle/ChillMainBundle/Resources/views/Form/fields.html.twig
@@ -215,3 +215,8 @@
{{ form_widget(form.center) }}
{% endif %}
{% endblock %}
+
+{% block pick_user_dynamic_widget %}
+
+
+{% endblock %}
diff --git a/src/Bundle/ChillMainBundle/Resources/views/Notification/_list_item.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Notification/_list_item.html.twig
new file mode 100644
index 000000000..8123373f2
--- /dev/null
+++ b/src/Bundle/ChillMainBundle/Resources/views/Notification/_list_item.html.twig
@@ -0,0 +1,88 @@
+
+
+
+
+
+ {% include data.template with data.template_data %}
+
+
+
+
+
+ {{ notification.message|u.truncate(250, '…', false)|chill_markdown_to_html }}
+
+
+
+ {% if action_button is not defined or action_button != 'false' %}
+
+
+
+ {# Vue component #}
+
+
+ {% if is_granted('CHILL_MAIN_NOTIFICATION_UPDATE', notification) %}
+
+
+
+ {% endif %}
+ {% if is_granted('CHILL_MAIN_NOTIFICATION_SEE', notification) %}
+
+
+
+ {% endif %}
+
+
+ {% endif %}
+
diff --git a/src/Bundle/ChillMainBundle/Resources/views/Notification/create.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Notification/create.html.twig
new file mode 100644
index 000000000..8797c276a
--- /dev/null
+++ b/src/Bundle/ChillMainBundle/Resources/views/Notification/create.html.twig
@@ -0,0 +1,46 @@
+{% extends "@ChillMain/layout.html.twig" %}
+
+{% block title 'notification.Notify'|trans %}
+
+{% block js %}
+ {{ parent() }}
+ {{ encore_entry_script_tags('mod_pickentity_type') }}
+{% endblock %}
+
+{% block css %}
+ {{ parent() }}
+ {{ encore_entry_link_tags('mod_pickentity_type') }}
+{% endblock %}
+
+{% block content %}
+
+
{{ block('title') }}
+
+ {{ form_start(form, { 'attr': { 'id': 'notification' }}) }}
+
+ {{ 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) %}
+
+
+
{{ form_label(form.message) }}
+
+ {{ form_widget(form.message) }}
+
+
+
+ {{ form_end(form) }}
+
+
+
+{% endblock %}
diff --git a/src/Bundle/ChillMainBundle/Resources/views/Notification/edit.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Notification/edit.html.twig
new file mode 100644
index 000000000..b51cc4dab
--- /dev/null
+++ b/src/Bundle/ChillMainBundle/Resources/views/Notification/edit.html.twig
@@ -0,0 +1,44 @@
+{% extends "@ChillMain/layout.html.twig" %}
+
+{% block title 'notification.Edit notification'|trans %}
+
+{% block js %}
+ {{ parent() }}
+ {{ encore_entry_script_tags('mod_pickentity_type') }}
+{% endblock %}
+
+{% block css %}
+ {{ parent() }}
+ {{ encore_entry_link_tags('mod_pickentity_type') }}
+{% endblock %}
+
+{% block content %}
+
+
{{ block('title') }}
+
+ {{ form_start(form, { 'attr': { 'id': 'notification' }}) }}
+
+ {{ 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) %}
+
+
+
{{ form_label(form.message) }}
+
+ {{ form_widget(form.message) }}
+
+
+
+ {{ form_end(form) }}
+
+
+
+{% endblock %}
diff --git a/src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content.fr.md.twig b/src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content.fr.md.twig
new file mode 100644
index 000000000..62e41860b
--- /dev/null
+++ b/src/Bundle/ChillMainBundle/Resources/views/Notification/email_non_system_notification_content.fr.md.twig
@@ -0,0 +1,20 @@
+{{ dest.label }},
+
+{{ notification.sender.label }} a créé une notification pour vous:
+
+> {{ notification.title }}
+>
+>
+{%- for line in notification.message|split("\n") %}
+> {{ line }}
+{%- if not loop.last %}
+>
+{%- endif %}
+{%- endfor %}
+
+Vous pouvez visualiser la notification et y répondre ici:
+
+{{ absolute_url(path('chill_main_notification_show', {'_locale': 'fr', 'id': notification.id }, false)) }}
+
+--
+Le logiciel Chill
diff --git a/src/Bundle/ChillMainBundle/Resources/views/Notification/email_notification_comment_persist.fr.md.twig b/src/Bundle/ChillMainBundle/Resources/views/Notification/email_notification_comment_persist.fr.md.twig
new file mode 100644
index 000000000..b1244da39
--- /dev/null
+++ b/src/Bundle/ChillMainBundle/Resources/views/Notification/email_notification_comment_persist.fr.md.twig
@@ -0,0 +1,19 @@
+{{ dest.label }},
+
+{{ comment.createdBy.label }} a créé un commentaire sur la notification "{{ comment.notification.title }}".
+
+Commentaire:
+
+{% for line in comment.content|split("\n") %}
+> {{ line }}
+{%- if not loop.last %}
+>
+{%- endif %}
+{%- endfor %}
+
+Vous pouvez visualiser la notification et y répondre ici:
+
+{{ absolute_url(path('chill_main_notification_show', {'_locale': 'fr', 'id': comment.notification.id }, false)) }}
+
+--
+Le logiciel Chill
diff --git a/src/Bundle/ChillMainBundle/Resources/views/Notification/extension_list_notifications_for.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Notification/extension_list_notifications_for.html.twig
new file mode 100644
index 000000000..31267e135
--- /dev/null
+++ b/src/Bundle/ChillMainBundle/Resources/views/Notification/extension_list_notifications_for.html.twig
@@ -0,0 +1,45 @@
+
+
+
{{ 'notification.Sent'|trans }}
+
+ {# TODO pagination or limit #}
+ {% for notification in notifications %}
+
+
+ {% if not notification.isSystem %}
+ {% if notification.sender == app.user %}
+
+
+
+ {{ notification.date|format_datetime('short','short') }}
+
+ {# Vue component #}
+
+
+
+ {% if notification.addressees|length > 0 %}
+
{{ 'notification.to'|trans }}:
+ {% endif %}
+
+ {% for a in notification.addressees %}
+
+ {{ a|chill_entity_render_string }}
+
+ {% endfor %}
+
+ {% else %}
+
{{ 'notification.you were notified by %sender%'|trans({'%sender%': notification.sender|chill_entity_render_string }) }}
+ {% endif %}
+ {% else %}
+
{{ 'notification.you were notified by system'|trans }}
+ {% endif %}
+
+
+ {% endfor %}
+
diff --git a/src/Bundle/ChillMainBundle/Resources/views/Notification/list.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Notification/list.html.twig
new file mode 100644
index 000000000..cd0cc5b67
--- /dev/null
+++ b/src/Bundle/ChillMainBundle/Resources/views/Notification/list.html.twig
@@ -0,0 +1,57 @@
+{% extends "@ChillMain/layout.html.twig" %}
+
+{% block title 'notification.My own notifications'|trans %}
+
+{% block js %}
+ {{ parent() }}
+ {{ encore_entry_script_tags('mod_notification_toggle_read_status') }}
+{% endblock %}
+
+{% block css %}
+ {{ parent() }}
+ {{ encore_entry_link_tags('mod_notification_toggle_read_status') }}
+{% endblock %}
+
+{% block content %}
+
+
{{ block('title') }}
+
+
+
+ {% if datas|length == 0 %}
+ {% if step == 'inbox' %}
+
{{ 'notification.Any notification received'|trans }}
+ {% else %}
+
{{ 'notification.Any notification sent'|trans }}
+ {% endif %}
+ {% else %}
+
+ {% for data in datas %}
+ {% set notification = data.notification %}
+ {% include 'ChillMainBundle:Notification:_list_item.html.twig' %}
+ {% endfor %}
+
+ {% endif %}
+
+{% endblock content %}
diff --git a/src/Bundle/ChillMainBundle/Resources/views/Notification/show.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Notification/show.html.twig
index 34aeee049..2d3b27728 100644
--- a/src/Bundle/ChillMainBundle/Resources/views/Notification/show.html.twig
+++ b/src/Bundle/ChillMainBundle/Resources/views/Notification/show.html.twig
@@ -1,42 +1,121 @@
{% extends "@ChillMain/layout.html.twig" %}
+{% block title 'notification.show notification from %sender%'|trans(
+ { '%sender%': notification.sender|chill_entity_render_string }
+) ~ ' ' ~ notification.title %}
+
+{% block js %}
+ {{ parent() }}
+ {{ encore_entry_script_tags('mod_notification_toggle_read_status') }}
+{% endblock %}
+
+{% block css %}
+ {{ parent() }}
+ {{ encore_entry_link_tags('mod_notification_toggle_read_status') }}
+{% endblock %}
+
+{% import '@ChillPerson/AccompanyingCourse/Comment/macro_showItem.html.twig' as m %}
+
+{% macro recordAction(comment) %}
+ {% if is_granted('CHILL_MAIN_NOTIFICATION_COMMENT_EDIT', comment) %}
+
+
+
+ {% endif %}
+{% endmacro %}
+
{% block content %}
-
-
-
{{ "Notifications list" | trans }}
-
+
- {%for data in datas %}
- {% set notification = data.notification %}
+
{{ 'notification.Notification'|trans }}
-
- {{ 'Message'|trans }}
- {{ notification.message }}
-
+
+ {% include 'ChillMainBundle:Notification:_list_item.html.twig' with {
+ 'data': {
+ 'template': handler.getTemplate(notification),
+ 'template_data': handler.getTemplateData(notification)
+ },
+ 'action_button': 'false'
+ } %}
+
-
- {{ 'Date'|trans }}
- {{ notification.date | date('long') }}
-
+
+
+
+
{% endblock content %}
diff --git a/src/Bundle/ChillMainBundle/Resources/views/layout.html.twig b/src/Bundle/ChillMainBundle/Resources/views/layout.html.twig
index 07b7970d8..a123ce405 100644
--- a/src/Bundle/ChillMainBundle/Resources/views/layout.html.twig
+++ b/src/Bundle/ChillMainBundle/Resources/views/layout.html.twig
@@ -106,6 +106,6 @@
});
- {% block js%}{% endblock %}
+ {% block js %}{% endblock %}
{{ 'notification.comments_list'|trans }}
+ {% if notification.comments|length > 0 %} +-- {{ 'Sender'|trans }}
- - {{ notification.sender }}
-
+ {% if editedCommentForm is null or editedCommentId != comment.id %} + {{ m.show_comment(comment, { + 'recordAction': _self.recordAction(comment) + }) }} + {% else %} +-- {{ 'Addressees'|trans }}
- - {{ notification.addressees |join(', ') }}
-
+ {{ form_start(editedCommentForm) }} + {{ form_errors(editedCommentForm) }} + {{ form_widget(editedCommentForm.content) }} + ++-
+
+ {{ 'cancel'|trans }}
+
+
+ -
+
+
+
+ {{ form_end(editedCommentForm) }} + +{{ 'Write a new comment'|trans }}
+ + {{ form_start(appendCommentForm) }} + {{ form_errors(appendCommentForm) }} + {{ form_widget(appendCommentForm.content) }} + ++-
+
+
+
+ {{ form_end(appendCommentForm) }} --- {{ 'Entity'|trans }}
- -
- {% include data.template with data.template_data %}
-
-
- {% endfor %}