mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-07-01 22:46:13 +00:00
Merge branch 'master' into 232_resources_comment
This commit is contained in:
commit
b23161fa1d
@ -40,6 +40,9 @@ and this project adheres to
|
|||||||
* address reference: add index for refid
|
* address reference: add index for refid
|
||||||
* [accompanyingCourse_work] fix styles conflicts + fix bug with remove goal (remove goals one at a time)
|
* [accompanyingCourse_work] fix styles conflicts + fix bug with remove goal (remove goals one at a time)
|
||||||
* [accompanyingCourse] improve masonry on resume page, add origin
|
* [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
|
## Test releases
|
||||||
|
|
||||||
|
@ -33,7 +33,8 @@
|
|||||||
"symfony/form": "^4.4",
|
"symfony/form": "^4.4",
|
||||||
"symfony/framework-bundle": "^4.4",
|
"symfony/framework-bundle": "^4.4",
|
||||||
"symfony/intl": "^4.4",
|
"symfony/intl": "^4.4",
|
||||||
"symfony/mime": "^4.4",
|
"symfony/mailer": "^5.4",
|
||||||
|
"symfony/mime": "^5.4",
|
||||||
"symfony/monolog-bundle": "^3.5",
|
"symfony/monolog-bundle": "^3.5",
|
||||||
"symfony/security-bundle": "^4.4",
|
"symfony/security-bundle": "^4.4",
|
||||||
"symfony/serializer": "^5.3",
|
"symfony/serializer": "^5.3",
|
||||||
@ -47,7 +48,12 @@
|
|||||||
"symfony/yaml": "^4.4",
|
"symfony/yaml": "^4.4",
|
||||||
"twig/extra-bundle": "^3.0",
|
"twig/extra-bundle": "^3.0",
|
||||||
"twig/intl-extra": "^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": {
|
"require-dev": {
|
||||||
"doctrine/doctrine-fixtures-bundle": "^3.3",
|
"doctrine/doctrine-fixtures-bundle": "^3.3",
|
||||||
@ -65,8 +71,17 @@
|
|||||||
"symfony/var-dumper": "^4.4",
|
"symfony/var-dumper": "^4.4",
|
||||||
"symfony/web-profiler-bundle": "^4.4"
|
"symfony/web-profiler-bundle": "^4.4"
|
||||||
},
|
},
|
||||||
"conflict": {
|
"config": {
|
||||||
"symfony/symfony": "*"
|
"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": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
<?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\ActivityBundle\Notification;
|
||||||
|
|
||||||
|
use Chill\ActivityBundle\Entity\Activity;
|
||||||
|
use Chill\ActivityBundle\Repository\ActivityRepository;
|
||||||
|
use Chill\MainBundle\Entity\Notification;
|
||||||
|
use Chill\MainBundle\Notification\NotificationHandlerInterface;
|
||||||
|
|
||||||
|
final class ActivityNotificationHandler implements NotificationHandlerInterface
|
||||||
|
{
|
||||||
|
private ActivityRepository $activityRepository;
|
||||||
|
|
||||||
|
public function __construct(ActivityRepository $activityRepository)
|
||||||
|
{
|
||||||
|
$this->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;
|
||||||
|
}
|
||||||
|
}
|
@ -1,33 +0,0 @@
|
|||||||
<?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\ActivityBundle\Notification;
|
|
||||||
|
|
||||||
use Chill\ActivityBundle\Entity\Activity;
|
|
||||||
use Chill\MainBundle\Entity\Notification;
|
|
||||||
|
|
||||||
final class ActivityNotificationRenderer
|
|
||||||
{
|
|
||||||
public function getTemplate()
|
|
||||||
{
|
|
||||||
return '@ChillActivity/Activity/showInNotification.html.twig';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getTemplateData(Notification $notification)
|
|
||||||
{
|
|
||||||
return ['notification' => $notification];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function supports(Notification $notification, array $options = []): bool
|
|
||||||
{
|
|
||||||
return $notification->getRelatedEntityClass() === Activity::class;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,151 @@
|
|||||||
|
{% set t = activity.type %}
|
||||||
|
<div class="item-bloc activity-item{% if itemBlocClass is defined %} {{ itemBlocClass }}{% endif %}">
|
||||||
|
|
||||||
|
<div class="item-row">
|
||||||
|
<div class="wrap-list">
|
||||||
|
<div class="wl-row">
|
||||||
|
<div class="wl-col title">
|
||||||
|
{% if activity.date %}
|
||||||
|
<p class="date-label">
|
||||||
|
{{ activity.date|format_date('short') }}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="wl-col list">
|
||||||
|
<h2 class="badge-title">
|
||||||
|
<span class="title_label"></span>
|
||||||
|
<span class="title_action">
|
||||||
|
{{ activity.type.name | localize_translatable_string }}
|
||||||
|
|
||||||
|
{% if activity.emergency %}
|
||||||
|
<span class="badge bg-danger rounded-pill fs-6 float-end">{{ 'Emergency'|trans|upper }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item-row column separator">
|
||||||
|
<div class="wrap-list">
|
||||||
|
{% if activity.location and t.locationVisible %}
|
||||||
|
<div class="wl-row">
|
||||||
|
<div class="wl-col title"><h3>{{ 'location'|trans }}</h3></div>
|
||||||
|
<div class="wl-col list">
|
||||||
|
<p class="wl-item">
|
||||||
|
<span>{{ activity.location.locationType.title|localize_translatable_string }}</span>
|
||||||
|
{{ activity.location.name }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if activity.sentReceived is not empty and t.sentReceivedVisible %}
|
||||||
|
<div class="wl-row">
|
||||||
|
<div class="wl-col title"><h3>{{ 'Sent received'|trans }}</h3></div>
|
||||||
|
<div class="wl-col list">
|
||||||
|
<p class="wl-item">
|
||||||
|
{{ activity.sentReceived|capitalize|trans }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if activity.user and t.userVisible %}
|
||||||
|
<div class="wl-row">
|
||||||
|
<div class="wl-col title"><h3>{{ 'Referrer'|trans }}</h3></div>
|
||||||
|
<div class="wl-col list">
|
||||||
|
<p class="wl-item">
|
||||||
|
{{ activity.user.usernameCanonical|chill_entity_render_string|capitalize }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% include 'ChillActivityBundle:Activity:concernedGroups.html.twig' with {
|
||||||
|
'context': context,
|
||||||
|
'with_display': 'wrap-list',
|
||||||
|
'entity': activity,
|
||||||
|
'badge_person': true
|
||||||
|
} %}
|
||||||
|
|
||||||
|
<div class="wrap-list">
|
||||||
|
{%- if activity.reasons is not empty and t.reasonsVisible -%}
|
||||||
|
<div class="wl-row">
|
||||||
|
<div class="wl-col title">
|
||||||
|
<h3>{{ 'Reasons'|trans }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="wl-col list">
|
||||||
|
{% for r in activity.reasons %}
|
||||||
|
<p class="wl-item reasons">
|
||||||
|
{{ r|chill_entity_render_box }}
|
||||||
|
</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{%- if activity.socialIssues is not empty and t.socialIssuesVisible -%}
|
||||||
|
<div class="wl-row">
|
||||||
|
<div class="wl-col title">
|
||||||
|
<h3>{{ 'Social issues'|trans }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="wl-col list">
|
||||||
|
{% for r in activity.socialIssues %}
|
||||||
|
<p class="wl-item social-issues">
|
||||||
|
{{ r|chill_entity_render_box }}
|
||||||
|
</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{%- if activity.socialActions is not empty and t.socialActionsVisible -%}
|
||||||
|
<div class="wl-row">
|
||||||
|
<div class="wl-col title">
|
||||||
|
<h3>{{ 'Social actions'|trans }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="wl-col list">
|
||||||
|
{% for r in activity.socialActions %}
|
||||||
|
<p class="wl-item social-actions">
|
||||||
|
{{ r|chill_entity_render_box }}
|
||||||
|
</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if activity.comment.comment is not empty and is_granted('CHILL_ACTIVITY_SEE_DETAILS', activity) %}
|
||||||
|
<div class="wl-row">
|
||||||
|
<div class="wl-col title">
|
||||||
|
<h3>{{ 'Comment'|trans }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="wl-col list">
|
||||||
|
{{ activity.comment|chill_entity_render_box({
|
||||||
|
'disable_markdown': false,
|
||||||
|
'limit_lines': 3,
|
||||||
|
'metadata': false
|
||||||
|
}) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{# Only if ACL SEE_DETAILS AND/OR only on template SHOW ??
|
||||||
|
durationTime
|
||||||
|
travelTime
|
||||||
|
comment
|
||||||
|
documents
|
||||||
|
attendee
|
||||||
|
#}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item-row separator">
|
||||||
|
<ul class="record_actions">
|
||||||
|
{{ recordAction }}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
@ -1,159 +1,12 @@
|
|||||||
<div class="context-{{ context }}">
|
{% macro recordAction(activity, context = null, person_id = null, accompanying_course_id = null) %}
|
||||||
|
{% if no_action is not defined or no_action == false %}
|
||||||
{% if activities|length == 0 %}
|
<li>
|
||||||
<p class="chill-no-data-statement">
|
<a class="btn btn-notify" href="{{ chill_path_add_return_path('chill_main_notification_create', {
|
||||||
{{ "There isn't any activities."|trans }}
|
'entityClass': 'Chill\\ActivityBundle\\Entity\\Activity',
|
||||||
</p>
|
'entityId': activity.id
|
||||||
|
}) }}">{{ 'notification.Notify'|trans }}</a>
|
||||||
{% else %}
|
</li>
|
||||||
<div class="flex-table activity-list">
|
|
||||||
{% for activity in activities %}
|
|
||||||
{% set t = activity.type %}
|
|
||||||
<div class="item-bloc">
|
|
||||||
|
|
||||||
<div class="item-row">
|
|
||||||
<div class="wrap-list">
|
|
||||||
<div class="wl-row">
|
|
||||||
<div class="wl-col title">
|
|
||||||
{% if activity.date %}
|
|
||||||
<p class="date-label">
|
|
||||||
{{ activity.date|format_date('short') }}
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
|
||||||
<div class="wl-col list">
|
|
||||||
<h2 class="badge-title">
|
|
||||||
<span class="title_label"></span>
|
|
||||||
<span class="title_action">
|
|
||||||
{{ activity.type.name | localize_translatable_string }}
|
|
||||||
|
|
||||||
{% if activity.emergency %}
|
|
||||||
<span class="badge bg-danger rounded-pill fs-6 float-end">{{ 'Emergency'|trans|upper }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="item-row column separator">
|
|
||||||
<div class="wrap-list">
|
|
||||||
{% if activity.location and t.locationVisible %}
|
|
||||||
<div class="wl-row">
|
|
||||||
<div class="wl-col title"><h3>{{ 'location'|trans }}</h3></div>
|
|
||||||
<div class="wl-col list">
|
|
||||||
<p class="wl-item">
|
|
||||||
<span>{{ activity.location.locationType.title|localize_translatable_string }}</span>
|
|
||||||
{{ activity.location.name }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if activity.sentReceived is not empty and t.sentReceivedVisible %}
|
|
||||||
<div class="wl-row">
|
|
||||||
<div class="wl-col title"><h3>{{ 'Sent received'|trans }}</h3></div>
|
|
||||||
<div class="wl-col list">
|
|
||||||
<p class="wl-item">
|
|
||||||
{{ activity.sentReceived|capitalize|trans }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if activity.user and t.userVisible %}
|
|
||||||
<div class="wl-row">
|
|
||||||
<div class="wl-col title"><h3>{{ 'Referrer'|trans }}</h3></div>
|
|
||||||
<div class="wl-col list">
|
|
||||||
<p class="wl-item">
|
|
||||||
{{ activity.user.usernameCanonical|chill_entity_render_string|capitalize }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% include 'ChillActivityBundle:Activity:concernedGroups.html.twig' with {
|
|
||||||
'context': context,
|
|
||||||
'with_display': 'wrap-list',
|
|
||||||
'entity': activity,
|
|
||||||
'badge_person': true
|
|
||||||
} %}
|
|
||||||
|
|
||||||
<div class="wrap-list">
|
|
||||||
{%- if activity.reasons is not empty and t.reasonsVisible -%}
|
|
||||||
<div class="wl-row">
|
|
||||||
<div class="wl-col title">
|
|
||||||
<h3>{{ 'Reasons'|trans }}</h3>
|
|
||||||
</div>
|
|
||||||
<div class="wl-col list">
|
|
||||||
{% for r in activity.reasons %}
|
|
||||||
<p class="wl-item reasons">
|
|
||||||
{{ r|chill_entity_render_box }}
|
|
||||||
</p>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{%- if activity.socialIssues is not empty and t.socialIssuesVisible -%}
|
|
||||||
<div class="wl-row">
|
|
||||||
<div class="wl-col title">
|
|
||||||
<h3>{{ 'Social issues'|trans }}</h3>
|
|
||||||
</div>
|
|
||||||
<div class="wl-col list">
|
|
||||||
{% for r in activity.socialIssues %}
|
|
||||||
<p class="wl-item social-issues">
|
|
||||||
{{ r|chill_entity_render_box }}
|
|
||||||
</p>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{%- if activity.socialActions is not empty and t.socialActionsVisible -%}
|
|
||||||
<div class="wl-row">
|
|
||||||
<div class="wl-col title">
|
|
||||||
<h3>{{ 'Social actions'|trans }}</h3>
|
|
||||||
</div>
|
|
||||||
<div class="wl-col list">
|
|
||||||
{% for r in activity.socialActions %}
|
|
||||||
<p class="wl-item social-actions">
|
|
||||||
{{ r|chill_entity_render_box }}
|
|
||||||
</p>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if activity.comment.comment is not empty and is_granted('CHILL_ACTIVITY_SEE_DETAILS', activity) %}
|
|
||||||
<div class="wl-row">
|
|
||||||
<div class="wl-col title">
|
|
||||||
<h3>{{ 'Comment'|trans }}</h3>
|
|
||||||
</div>
|
|
||||||
<div class="wl-col list">
|
|
||||||
{{ activity.comment|chill_entity_render_box({
|
|
||||||
'disable_markdown': false,
|
|
||||||
'limit_lines': 3,
|
|
||||||
'metadata': false
|
|
||||||
}) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{# Only if ACL SEE_DETAILS AND/OR only on template SHOW ??
|
|
||||||
durationTime
|
|
||||||
travelTime
|
|
||||||
comment
|
|
||||||
documents
|
|
||||||
attendee
|
|
||||||
#}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="item-row separator">
|
|
||||||
<ul class="record_actions">
|
|
||||||
{% if context == 'person' and activity.accompanyingPeriod is not empty %}
|
{% if context == 'person' and activity.accompanyingPeriod is not empty %}
|
||||||
{#
|
{#
|
||||||
Disable person_id in following links, for redirect to accompanyingCourse context
|
Disable person_id in following links, for redirect to accompanyingCourse context
|
||||||
@ -201,10 +54,22 @@
|
|||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
{% endmacro %}
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
<div class="context-{{ context }}">
|
||||||
|
|
||||||
|
{% if activities|length == 0 %}
|
||||||
|
<p class="chill-no-data-statement">
|
||||||
|
{{ "There isn't any activities."|trans }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
<div class="flex-table activity-list">
|
||||||
|
{% for activity in activities %}
|
||||||
|
{% include 'ChillActivityBundle:Activity:_list_item.html.twig' with {
|
||||||
|
'context': context,
|
||||||
|
'recordAction': _self.recordAction(activity, context, person_id, accompanying_course_id)
|
||||||
|
} %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -4,6 +4,17 @@
|
|||||||
|
|
||||||
{% block title %}{{ 'Activity list' |trans }}{% endblock title %}
|
{% 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 %}
|
{% block content %}
|
||||||
|
|
||||||
{% set person_id = null %}
|
{% set person_id = null %}
|
||||||
|
@ -20,6 +20,16 @@
|
|||||||
|
|
||||||
{% block title %}{{ 'Activity list' |trans }}{% endblock title %}
|
{% 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 %}
|
{% block personcontent %}
|
||||||
|
|
||||||
{% set person_id = null %}
|
{% set person_id = null %}
|
||||||
|
@ -212,9 +212,3 @@
|
|||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
<script>
|
|
||||||
import ShowPane from "../../../../ChillMainBundle/Resources/public/vuejs/Address/components/ShowPane";
|
|
||||||
export default {
|
|
||||||
components: {ShowPane}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
@ -4,6 +4,16 @@
|
|||||||
|
|
||||||
{% block title 'Show the activity'|trans %}
|
{% 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 %}
|
{% import 'ChillActivityBundle:ActivityReason:macro.html.twig' as m %}
|
||||||
|
|
||||||
{% block content -%}
|
{% block content -%}
|
||||||
@ -11,3 +21,21 @@
|
|||||||
{% include 'ChillActivityBundle:Activity:show.html.twig' with {'context': 'accompanyingCourse'} %}
|
{% include 'ChillActivityBundle:Activity:show.html.twig' with {'context': 'accompanyingCourse'} %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
||||||
|
{% block block_post_menu %}
|
||||||
|
<div class="post-menu pt-4">
|
||||||
|
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<a class="btn btn-primary" href="{{ chill_path_add_return_path('chill_main_notification_create', {'entityClass': 'Chill\\ActivityBundle\\Entity\\Activity', 'entityId': entity.id}) }}">
|
||||||
|
<i class="fa fa-paper-plane fa-fw"></i>
|
||||||
|
{{ 'notification.Notify'|trans }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% set notifications = chill_list_notifications('Chill\\ActivityBundle\\Entity\\Activity', entity.id) %}
|
||||||
|
{% if notifications is not empty %}
|
||||||
|
{{ notifications|raw }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
@ -1,2 +1,27 @@
|
|||||||
|
{% macro recordAction(activity) %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ path('chill_activity_activity_show', {'id': activity.id }) }}"
|
||||||
|
class="btn btn-show" title="{{ 'Show the activity'|trans }}"></a>
|
||||||
|
</li>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
<a href="{{ path('chill_activity_activity_show', {'id': notification.relatedEntityId }) }}">Go to Activity</a>
|
{% if activity is not null %}
|
||||||
|
<div class="flex-table">
|
||||||
|
{% 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 %}
|
||||||
|
<div class="alert alert-warning border-warning border-1">
|
||||||
|
{{ 'This is the minimal activity data'|trans ~ ': ' ~ activity.id }}<br>
|
||||||
|
{{ 'you are not allowed to see it details'|trans }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-warning border-warning border-1">
|
||||||
|
{{ 'You get notified of an activity which does not exists any more'|trans }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
@ -4,6 +4,16 @@
|
|||||||
|
|
||||||
{% block title 'Show the activity'|trans %}
|
{% 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 %}
|
{% import 'ChillActivityBundle:ActivityReason:macro.html.twig' as m %}
|
||||||
|
|
||||||
{% block personcontent -%}
|
{% block personcontent -%}
|
||||||
@ -11,3 +21,21 @@
|
|||||||
{% include 'ChillActivityBundle:Activity:show.html.twig' with {'context': 'person'} %}
|
{% include 'ChillActivityBundle:Activity:show.html.twig' with {'context': 'person'} %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock personcontent %}
|
{% endblock personcontent %}
|
||||||
|
|
||||||
|
{% block block_post_menu %}
|
||||||
|
<div class="post-menu pt-4">
|
||||||
|
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<a class="btn btn-primary" href="{{ chill_path_add_return_path('chill_main_notification_create', {'entityClass': 'Chill\\ActivityBundle\\Entity\\Activity', 'entityId': entity.id}) }}">
|
||||||
|
<i class="fa fa-paper-plane fa-fw"></i>
|
||||||
|
{{ 'notification.Notify'|trans }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% set notifications = chill_list_notifications('Chill\\ActivityBundle\\Entity\\Activity', entity.id) %}
|
||||||
|
{% if notifications is not empty %}
|
||||||
|
{{ notifications|raw }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
@ -224,3 +224,7 @@ Aggregate by activity reason: Aggréger par sujet de l'activité
|
|||||||
Last activities: Les dernières activités
|
Last activities: Les dernières activités
|
||||||
|
|
||||||
See activity in accompanying course context: Voir l'activité dans le contexte du parcours d'accompagnement
|
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°
|
||||||
|
@ -26,7 +26,7 @@ class CollectionDocGenNormalizer implements ContextAwareNormalizerInterface, Nor
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Collection $object
|
* @param Collection $object
|
||||||
* @param null|string $format
|
* @param string|null $format
|
||||||
*
|
*
|
||||||
* @return array|ArrayObject|bool|float|int|string|void|null
|
* @return array|ArrayObject|bool|float|int|string|void|null
|
||||||
*/
|
*/
|
||||||
|
@ -66,7 +66,7 @@ class DocGenObjectNormalizer implements NormalizerAwareInterface, NormalizerInte
|
|||||||
if (!$this->classMetadataFactory->hasMetadataFor($classMetadataKey)) {
|
if (!$this->classMetadataFactory->hasMetadataFor($classMetadataKey)) {
|
||||||
throw new LogicException(sprintf(
|
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',
|
'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,
|
$format,
|
||||||
implode(', ', ($context['groups'] ?? []))
|
implode(', ', ($context['groups'] ?? []))
|
||||||
));
|
));
|
||||||
|
@ -22,6 +22,7 @@ use Chill\MainBundle\DependencyInjection\CompilerPass\TimelineCompilerClass;
|
|||||||
use Chill\MainBundle\DependencyInjection\CompilerPass\WidgetsCompilerPass;
|
use Chill\MainBundle\DependencyInjection\CompilerPass\WidgetsCompilerPass;
|
||||||
use Chill\MainBundle\DependencyInjection\ConfigConsistencyCompilerPass;
|
use Chill\MainBundle\DependencyInjection\ConfigConsistencyCompilerPass;
|
||||||
use Chill\MainBundle\DependencyInjection\RoleProvidersCompilerPass;
|
use Chill\MainBundle\DependencyInjection\RoleProvidersCompilerPass;
|
||||||
|
use Chill\MainBundle\Notification\NotificationHandlerInterface;
|
||||||
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
|
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
|
||||||
use Chill\MainBundle\Search\SearchApiInterface;
|
use Chill\MainBundle\Search\SearchApiInterface;
|
||||||
use Chill\MainBundle\Security\ProvideRoleInterface;
|
use Chill\MainBundle\Security\ProvideRoleInterface;
|
||||||
@ -29,6 +30,7 @@ use Chill\MainBundle\Security\Resolver\CenterResolverInterface;
|
|||||||
use Chill\MainBundle\Security\Resolver\ScopeResolverInterface;
|
use Chill\MainBundle\Security\Resolver\ScopeResolverInterface;
|
||||||
use Chill\MainBundle\Templating\Entity\ChillEntityRenderInterface;
|
use Chill\MainBundle\Templating\Entity\ChillEntityRenderInterface;
|
||||||
use Chill\MainBundle\Templating\Entity\CompilerPass as RenderEntityCompilerPass;
|
use Chill\MainBundle\Templating\Entity\CompilerPass as RenderEntityCompilerPass;
|
||||||
|
use Chill\MainBundle\Templating\UI\NotificationCounterInterface;
|
||||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||||
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
||||||
|
|
||||||
@ -50,6 +52,10 @@ class ChillMainBundle extends Bundle
|
|||||||
->addTag('chill.render_entity');
|
->addTag('chill.render_entity');
|
||||||
$container->registerForAutoconfiguration(SearchApiInterface::class)
|
$container->registerForAutoconfiguration(SearchApiInterface::class)
|
||||||
->addTag('chill.search_api_provider');
|
->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 SearchableServicesCompilerPass());
|
||||||
$container->addCompilerPass(new ConfigConsistencyCompilerPass());
|
$container->addCompilerPass(new ConfigConsistencyCompilerPass());
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -11,59 +11,292 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\MainBundle\Controller;
|
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\Pagination\PaginatorFactory;
|
||||||
use Chill\MainBundle\Repository\NotificationRepository;
|
use Chill\MainBundle\Repository\NotificationRepository;
|
||||||
|
use Chill\MainBundle\Security\Authorization\NotificationVoter;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
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\Routing\Annotation\Route;
|
||||||
use Symfony\Component\Security\Core\Security;
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Route("/{_locale}/notification")
|
* @Route("/{_locale}/notification")
|
||||||
*/
|
*/
|
||||||
class NotificationController extends AbstractController
|
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->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(
|
public function createAction(Request $request): Response
|
||||||
NotificationRepository $notificationRepository,
|
{
|
||||||
NotificationRenderer $notificationRenderer,
|
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
|
||||||
PaginatorFactory $paginatorFactory
|
|
||||||
) {
|
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();
|
$currentUser = $this->security->getUser();
|
||||||
|
|
||||||
$notificationsNbr = $notificationRepository->countAllForAttendee(($currentUser));
|
$notificationsNbr = $this->notificationRepository->countAllForAttendee(($currentUser));
|
||||||
$paginator = $paginatorFactory->create($notificationsNbr);
|
$paginator = $this->paginatorFactory->create($notificationsNbr);
|
||||||
|
|
||||||
$notifications = $notificationRepository->findAllForAttendee(
|
$notifications = $this->notificationRepository->findAllForAttendee(
|
||||||
$currentUser,
|
$currentUser,
|
||||||
$limit = $paginator->getItemsPerPage(),
|
$limit = $paginator->getItemsPerPage(),
|
||||||
$offset = $paginator->getCurrentPage()->getFirstItemNumber()
|
$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 = [];
|
$templateData = [];
|
||||||
|
|
||||||
foreach ($notifications as $notification) {
|
foreach ($notifications as $notification) {
|
||||||
$data = [
|
$templateData[] = [
|
||||||
'template' => $notificationRenderer->getTemplate($notification),
|
'template' => $this->notificationHandlerManager->getTemplate($notification),
|
||||||
'template_data' => $notificationRenderer->getTemplateData($notification),
|
'template_data' => $this->notificationHandlerManager->getTemplateData($notification),
|
||||||
'notification' => $notification,
|
'notification' => $notification,
|
||||||
];
|
];
|
||||||
$templateData[] = $data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->render('@ChillMain/Notification/show.html.twig', [
|
return $templateData;
|
||||||
'datas' => $templateData,
|
|
||||||
'notifications' => $notifications,
|
|
||||||
'paginator' => $paginator,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,9 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\MainBundle\Entity;
|
namespace Chill\MainBundle\Entity;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
|
use DateTimeInterface;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
@ -20,19 +22,27 @@ use Doctrine\ORM\Mapping as ORM;
|
|||||||
* @ORM\Entity
|
* @ORM\Entity
|
||||||
* @ORM\Table(
|
* @ORM\Table(
|
||||||
* name="chill_main_notification",
|
* 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\ManyToMany(targetEntity=User::class)
|
||||||
* @ORM\JoinTable(name="chill_main_notification_addresses_user")
|
* @ORM\JoinTable(name="chill_main_notification_addresses_user")
|
||||||
*/
|
*/
|
||||||
private Collection $addressees;
|
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")
|
* @ORM\Column(type="datetime_immutable")
|
||||||
*/
|
*/
|
||||||
@ -43,43 +53,84 @@ class Notification
|
|||||||
* @ORM\GeneratedValue
|
* @ORM\GeneratedValue
|
||||||
* @ORM\Column(type="integer")
|
* @ORM\Column(type="integer")
|
||||||
*/
|
*/
|
||||||
private int $id;
|
private ?int $id = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ORM\Column(type="text")
|
* @ORM\Column(type="text")
|
||||||
*/
|
*/
|
||||||
private string $message;
|
private string $message = '';
|
||||||
|
|
||||||
/**
|
|
||||||
* @ORM\Column(type="json")
|
|
||||||
*/
|
|
||||||
private array $read;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ORM\Column(type="string", length=255)
|
* @ORM\Column(type="string", length=255)
|
||||||
*/
|
*/
|
||||||
private string $relatedEntityClass;
|
private string $relatedEntityClass = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ORM\Column(type="integer")
|
* @ORM\Column(type="integer")
|
||||||
*/
|
*/
|
||||||
private int $relatedEntityId;
|
private int $relatedEntityId;
|
||||||
|
|
||||||
|
private array $removedAddresses = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ORM\ManyToOne(targetEntity=User::class)
|
* @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()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->addressees = new ArrayCollection();
|
$this->addressees = new ArrayCollection();
|
||||||
|
$this->unreadBy = new ArrayCollection();
|
||||||
|
$this->comments = new ArrayCollection();
|
||||||
|
$this->setDate(new DateTimeImmutable());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function addAddressee(User $addressee): self
|
public function addAddressee(User $addressee): self
|
||||||
{
|
{
|
||||||
if (!$this->addressees->contains($addressee)) {
|
if (!$this->addressees->contains($addressee)) {
|
||||||
$this->addressees[] = $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;
|
return $this;
|
||||||
@ -90,9 +141,19 @@ class Notification
|
|||||||
*/
|
*/
|
||||||
public function getAddressees(): Collection
|
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;
|
return $this->addressees;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getComments(): Collection
|
||||||
|
{
|
||||||
|
return $this->comments;
|
||||||
|
}
|
||||||
|
|
||||||
public function getDate(): ?DateTimeImmutable
|
public function getDate(): ?DateTimeImmutable
|
||||||
{
|
{
|
||||||
return $this->date;
|
return $this->date;
|
||||||
@ -108,11 +169,6 @@ class Notification
|
|||||||
return $this->message;
|
return $this->message;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getRead(): array
|
|
||||||
{
|
|
||||||
return $this->read;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getRelatedEntityClass(): ?string
|
public function getRelatedEntityClass(): ?string
|
||||||
{
|
{
|
||||||
return $this->relatedEntityClass;
|
return $this->relatedEntityClass;
|
||||||
@ -128,9 +184,97 @@ class Notification
|
|||||||
return $this->sender;
|
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
|
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;
|
return $this;
|
||||||
}
|
}
|
||||||
@ -149,13 +293,6 @@ class Notification
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setRead(array $read): self
|
|
||||||
{
|
|
||||||
$this->read = $read;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setRelatedEntityClass(string $relatedEntityClass): self
|
public function setRelatedEntityClass(string $relatedEntityClass): self
|
||||||
{
|
{
|
||||||
$this->relatedEntityClass = $relatedEntityClass;
|
$this->relatedEntityClass = $relatedEntityClass;
|
||||||
@ -176,4 +313,25 @@ class Notification
|
|||||||
|
|
||||||
return $this;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
191
src/Bundle/ChillMainBundle/Entity/NotificationComment.php
Normal file
191
src/Bundle/ChillMainBundle/Entity/NotificationComment.php
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
<?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\Entity;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
|
||||||
|
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use Doctrine\ORM\Event\LifecycleEventArgs;
|
||||||
|
use Doctrine\ORM\Event\PreFlushEventArgs;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\Entity
|
||||||
|
* @ORM\Table("chill_main_notification_comment")
|
||||||
|
* @ORM\HasLifecycleCallbacks
|
||||||
|
*/
|
||||||
|
class NotificationComment implements TrackCreationInterface, TrackUpdateInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @ORM\Column(type="text")
|
||||||
|
*/
|
||||||
|
private string $content = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\Column(type="datetime_immutable", nullable=true)
|
||||||
|
*/
|
||||||
|
private ?DateTimeImmutable $createdAt = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\ManyToOne(targetEntity=User::class)
|
||||||
|
* @ORM\JoinColumn(nullable=true)
|
||||||
|
*/
|
||||||
|
private ?User $createdBy = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\Id
|
||||||
|
* @ORM\GeneratedValue
|
||||||
|
* @ORM\Column(type="integer")
|
||||||
|
*/
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\ManyToOne(targetEntity=Notification::class, inversedBy="comments")
|
||||||
|
* @ORM\JoinColumn(nullable=false)
|
||||||
|
*/
|
||||||
|
private ?Notification $notification = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal variable which detect if the comment is just persisted.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
private bool $recentlyPersisted = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO typo in property (hotfixed).
|
||||||
|
*
|
||||||
|
* @ORM\Column(type="datetime_immutable", nullable=true)
|
||||||
|
*/
|
||||||
|
private ?DateTimeImmutable $updateAt = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\ManyToOne(targetEntity=User::class)
|
||||||
|
* @ORM\JoinColumn(nullable=true)
|
||||||
|
*/
|
||||||
|
private ?User $updatedBy = null;
|
||||||
|
|
||||||
|
public function getContent(): string
|
||||||
|
{
|
||||||
|
return $this->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;
|
||||||
|
}
|
||||||
|
}
|
26
src/Bundle/ChillMainBundle/Form/NotificationCommentType.php
Normal file
26
src/Bundle/ChillMainBundle/Form/NotificationCommentType.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?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\Form;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Form\Type\ChillTextareaType;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
|
||||||
|
class NotificationCommentType extends AbstractType
|
||||||
|
{
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
|
{
|
||||||
|
$builder->add('content', ChillTextareaType::class, [
|
||||||
|
'required' => false,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
43
src/Bundle/ChillMainBundle/Form/NotificationType.php
Normal file
43
src/Bundle/ChillMainBundle/Form/NotificationType.php
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<?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\Form;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\Notification;
|
||||||
|
use Chill\MainBundle\Form\Type\ChillTextareaType;
|
||||||
|
use Chill\MainBundle\Form\Type\PickUserDynamicType;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
class NotificationType extends AbstractType
|
||||||
|
{
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
|
{
|
||||||
|
$builder
|
||||||
|
->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);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,81 @@
|
|||||||
|
<?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\Form\Type\DataTransformer;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Symfony\Component\Form\DataTransformerInterface;
|
||||||
|
use Symfony\Component\Form\Exception\TransformationFailedException;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||||
|
use Symfony\Component\Serializer\SerializerInterface;
|
||||||
|
use function array_key_exists;
|
||||||
|
|
||||||
|
class UserToJsonTransformer implements DataTransformerInterface
|
||||||
|
{
|
||||||
|
private DenormalizerInterface $denormalizer;
|
||||||
|
|
||||||
|
private bool $multiple;
|
||||||
|
|
||||||
|
private SerializerInterface $serializer;
|
||||||
|
|
||||||
|
public function __construct(DenormalizerInterface $denormalizer, SerializerInterface $serializer, bool $multiple)
|
||||||
|
{
|
||||||
|
$this->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']],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
63
src/Bundle/ChillMainBundle/Form/Type/PickUserDynamicType.php
Normal file
63
src/Bundle/ChillMainBundle/Form/Type/PickUserDynamicType.php
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<?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\Form\Type;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Chill\MainBundle\Form\Type\DataTransformer\UserToJsonTransformer;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\Form\FormInterface;
|
||||||
|
use Symfony\Component\Form\FormView;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||||
|
use Symfony\Component\Serializer\SerializerInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pick user dymically, using vuejs module "AddPerson".
|
||||||
|
*/
|
||||||
|
class PickUserDynamicType extends AbstractType
|
||||||
|
{
|
||||||
|
private DenormalizerInterface $denormalizer;
|
||||||
|
|
||||||
|
private SerializerInterface $serializer;
|
||||||
|
|
||||||
|
public function __construct(DenormalizerInterface $denormalizer, SerializerInterface $serializer)
|
||||||
|
{
|
||||||
|
$this->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';
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Notification\Counter;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\Notification;
|
||||||
|
use Chill\MainBundle\Entity\NotificationComment;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Chill\MainBundle\Repository\NotificationRepository;
|
||||||
|
use Chill\MainBundle\Templating\UI\NotificationCounterInterface;
|
||||||
|
use Doctrine\ORM\Event\LifecycleEventArgs;
|
||||||
|
use Doctrine\ORM\Event\PreFlushEventArgs;
|
||||||
|
use Psr\Cache\CacheItemPoolInterface;
|
||||||
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
|
|
||||||
|
final class NotificationByUserCounter implements NotificationCounterInterface
|
||||||
|
{
|
||||||
|
private CacheItemPoolInterface $cacheItemPool;
|
||||||
|
|
||||||
|
private NotificationRepository $notificationRepository;
|
||||||
|
|
||||||
|
public function __construct(CacheItemPoolInterface $cacheItemPool, NotificationRepository $notificationRepository)
|
||||||
|
{
|
||||||
|
$this->cacheItemPool = $cacheItemPool;
|
||||||
|
$this->notificationRepository = $notificationRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addNotification(UserInterface $u): int
|
||||||
|
{
|
||||||
|
if (!$u instanceof User) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->countUnreadByUser($u);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function countUnreadByUser(User $user): int
|
||||||
|
{
|
||||||
|
$key = self::generateCacheKeyUnreadNotificationByUser($user);
|
||||||
|
|
||||||
|
$item = $this->cacheItemPool->getItem($key);
|
||||||
|
|
||||||
|
if ($item->isHit()) {
|
||||||
|
return $item->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
$unreads = $this->notificationRepository->countUnreadByUser($user);
|
||||||
|
|
||||||
|
$item
|
||||||
|
->set($unreads)
|
||||||
|
// keep in cache for 15 minutes
|
||||||
|
->expiresAfter(60 * 15);
|
||||||
|
$this->cacheItemPool->save($item);
|
||||||
|
|
||||||
|
return $unreads;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function generateCacheKeyUnreadNotificationByUser(User $user): string
|
||||||
|
{
|
||||||
|
return 'chill_main_notif_unread_by_' . $user->getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onEditNotificationComment(NotificationComment $notificationComment, LifecycleEventArgs $eventArgs): void
|
||||||
|
{
|
||||||
|
$this->resetCacheForNotification($notificationComment->getNotification());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onPreFlushNotification(Notification $notification, PreFlushEventArgs $eventArgs): void
|
||||||
|
{
|
||||||
|
$this->resetCacheForNotification($notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resetCacheForNotification(Notification $notification): void
|
||||||
|
{
|
||||||
|
$keys = [];
|
||||||
|
|
||||||
|
if (null !== $notification->getSender()) {
|
||||||
|
$keys[] = self::generateCacheKeyUnreadNotificationByUser($notification->getSender());
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($notification->getAddressees() as $addressee) {
|
||||||
|
$keys[] = self::generateCacheKeyUnreadNotificationByUser($addressee);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->cacheItemPool->deleteItems($keys);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,110 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Notification\Email;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\Notification;
|
||||||
|
use Chill\MainBundle\Entity\NotificationComment;
|
||||||
|
use Doctrine\ORM\Event\LifecycleEventArgs;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||||
|
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||||
|
use Symfony\Component\Mailer\MailerInterface;
|
||||||
|
use Symfony\Component\Mime\Email;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
|
class NotificationMailer
|
||||||
|
{
|
||||||
|
private LoggerInterface $logger;
|
||||||
|
|
||||||
|
private MailerInterface $mailer;
|
||||||
|
|
||||||
|
private TranslatorInterface $translator;
|
||||||
|
|
||||||
|
public function __construct(MailerInterface $mailer, LoggerInterface $logger, TranslatorInterface $translator)
|
||||||
|
{
|
||||||
|
$this->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(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Notification\Exception;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
class NotificationHandlerNotFound extends RuntimeException
|
||||||
|
{
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Notification;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\Notification;
|
||||||
|
|
||||||
|
interface NotificationHandlerInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Return the template path (twig file).
|
||||||
|
*/
|
||||||
|
public function getTemplate(Notification $notification, array $options = []): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an array which will be passed as data for the template.
|
||||||
|
*/
|
||||||
|
public function getTemplateData(Notification $notification, array $options = []): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if the handler supports the handling for this notification.
|
||||||
|
*/
|
||||||
|
public function supports(Notification $notification, array $options = []): bool;
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Notification;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\Notification;
|
||||||
|
use Chill\MainBundle\Notification\Exception\NotificationHandlerNotFound;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
|
||||||
|
final class NotificationHandlerManager
|
||||||
|
{
|
||||||
|
private EntityManagerInterface $em;
|
||||||
|
|
||||||
|
private iterable $handlers;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
iterable $handlers,
|
||||||
|
EntityManagerInterface $em
|
||||||
|
) {
|
||||||
|
$this->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);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Notification;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\Notification;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Chill\MainBundle\Repository\NotificationRepository;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helps to find if a notification exist for a given entity.
|
||||||
|
*/
|
||||||
|
class NotificationPresence
|
||||||
|
{
|
||||||
|
private NotificationRepository $notificationRepository;
|
||||||
|
|
||||||
|
private Security $security;
|
||||||
|
|
||||||
|
public function __construct(Security $security, NotificationRepository $notificationRepository)
|
||||||
|
{
|
||||||
|
$this->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 [];
|
||||||
|
}
|
||||||
|
}
|
@ -1,54 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Chill\MainBundle\Notification;
|
|
||||||
|
|
||||||
use Chill\ActivityBundle\Notification\ActivityNotificationRenderer;
|
|
||||||
use Chill\MainBundle\Entity\Notification;
|
|
||||||
use Chill\PersonBundle\Notification\AccompanyingPeriodNotificationRenderer;
|
|
||||||
use Exception;
|
|
||||||
|
|
||||||
final class NotificationRenderer
|
|
||||||
{
|
|
||||||
private array $renderers;
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
AccompanyingPeriodNotificationRenderer $accompanyingPeriodNotificationRenderer,
|
|
||||||
ActivityNotificationRenderer $activityNotificationRenderer
|
|
||||||
) {
|
|
||||||
// TODO configure automatically
|
|
||||||
// TODO CREER UNE INTERFACE POUR ETRE SUR QUE LES RENDERERS SONT OK
|
|
||||||
|
|
||||||
$this->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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Notification\Templating;
|
||||||
|
|
||||||
|
use Twig\Extension\AbstractExtension;
|
||||||
|
use Twig\TwigFunction;
|
||||||
|
|
||||||
|
class NotificationTwigExtension extends AbstractExtension
|
||||||
|
{
|
||||||
|
public function getFunctions()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new TwigFunction('chill_list_notifications', [NotificationTwigExtensionRuntime::class, 'listNotificationsFor'], [
|
||||||
|
'needs_environment' => true,
|
||||||
|
'is_safe' => ['html'],
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Notification\Templating;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Notification\NotificationPresence;
|
||||||
|
use Twig\Environment;
|
||||||
|
use Twig\Extension\RuntimeExtensionInterface;
|
||||||
|
|
||||||
|
class NotificationTwigExtensionRuntime implements RuntimeExtensionInterface
|
||||||
|
{
|
||||||
|
private NotificationPresence $notificationPresence;
|
||||||
|
|
||||||
|
public function __construct(NotificationPresence $notificationPresence)
|
||||||
|
{
|
||||||
|
$this->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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
@ -13,25 +13,76 @@ namespace Chill\MainBundle\Repository;
|
|||||||
|
|
||||||
use Chill\MainBundle\Entity\Notification;
|
use Chill\MainBundle\Entity\Notification;
|
||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Doctrine\DBAL\Types\Types;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Doctrine\ORM\EntityRepository;
|
use Doctrine\ORM\EntityRepository;
|
||||||
use Doctrine\ORM\Query;
|
use Doctrine\ORM\Query;
|
||||||
|
use Doctrine\ORM\QueryBuilder;
|
||||||
use Doctrine\Persistence\ObjectRepository;
|
use Doctrine\Persistence\ObjectRepository;
|
||||||
|
|
||||||
final class NotificationRepository implements ObjectRepository
|
final class NotificationRepository implements ObjectRepository
|
||||||
{
|
{
|
||||||
|
private EntityManagerInterface $em;
|
||||||
|
|
||||||
private EntityRepository $repository;
|
private EntityRepository $repository;
|
||||||
|
|
||||||
public function __construct(EntityManagerInterface $entityManager)
|
public function __construct(EntityManagerInterface $entityManager)
|
||||||
{
|
{
|
||||||
|
$this->em = $entityManager;
|
||||||
$this->repository = $entityManager->getRepository(Notification::class);
|
$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
|
public function find($id, $lockMode = null, $lockVersion = null): ?Notification
|
||||||
@ -53,9 +104,9 @@ final class NotificationRepository implements ObjectRepository
|
|||||||
*
|
*
|
||||||
* @return Notification[]
|
* @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) {
|
if ($limit) {
|
||||||
$query = $query->setMaxResults($limit);
|
$query = $query->setMaxResults($limit);
|
||||||
@ -65,7 +116,26 @@ final class NotificationRepository implements ObjectRepository
|
|||||||
$query = $query->setFirstResult($offset);
|
$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 $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
|
public function findOneBy(array $criteria, ?array $orderBy = null): ?Notification
|
||||||
{
|
{
|
||||||
return $this->repository->findOneBy($criteria, $orderBy);
|
return $this->repository->findOneBy($criteria, $orderBy);
|
||||||
@ -89,22 +184,25 @@ final class NotificationRepository implements ObjectRepository
|
|||||||
return Notification::class;
|
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');
|
$qb = $this->repository->createQueryBuilder('n');
|
||||||
|
|
||||||
$select = 'n';
|
|
||||||
|
|
||||||
if ($countQuery) {
|
|
||||||
$select = 'count(n)';
|
|
||||||
}
|
|
||||||
|
|
||||||
$qb
|
$qb
|
||||||
->select($select)
|
->where($qb->expr()->isMemberOf(':addressee', 'n.addressees'))
|
||||||
->join('n.addressees', 'a')
|
|
||||||
->where('a = :addressee')
|
|
||||||
->setParameter('addressee', $addressee);
|
->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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,8 @@
|
|||||||
// Chill flex responsive table/block presentation
|
// Chill flex responsive table/block presentation
|
||||||
@import './scss/flex_table';
|
@import './scss/flex_table';
|
||||||
|
|
||||||
|
// Specific templates
|
||||||
|
@import './scss/notification';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* BASE LAYOUT POSITION
|
* BASE LAYOUT POSITION
|
||||||
|
@ -20,6 +20,7 @@ $chill-theme-buttons: (
|
|||||||
"misc": $gray-300,
|
"misc": $gray-300,
|
||||||
"cancel": $gray-300,
|
"cancel": $gray-300,
|
||||||
"choose": $gray-300,
|
"choose": $gray-300,
|
||||||
|
"notify": $gray-300,
|
||||||
"unlink": $chill-red,
|
"unlink": $chill-red,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -73,6 +74,7 @@ $chill-theme-buttons: (
|
|||||||
&.btn-delete::before,
|
&.btn-delete::before,
|
||||||
&.btn-remove::before,
|
&.btn-remove::before,
|
||||||
&.btn-choose::before,
|
&.btn-choose::before,
|
||||||
|
&.btn-notify::before,
|
||||||
&.btn-cancel::before {
|
&.btn-cancel::before {
|
||||||
font: normal normal normal 14px/1 ForkAwesome;
|
font: normal normal normal 14px/1 ForkAwesome;
|
||||||
margin-right: 0.5em;
|
margin-right: 0.5em;
|
||||||
@ -98,6 +100,7 @@ $chill-theme-buttons: (
|
|||||||
&.btn-cancel::before { content: "\f060"; } // fa-arrow-left
|
&.btn-cancel::before { content: "\f060"; } // fa-arrow-left
|
||||||
&.btn-choose::before { content: "\f00c"; } // fa-check // f046 fa-check-square-o
|
&.btn-choose::before { content: "\f00c"; } // fa-check // f046 fa-check-square-o
|
||||||
&.btn-unlink::before { content: "\f127"; } // fa-chain-broken
|
&.btn-unlink::before { content: "\f127"; } // fa-chain-broken
|
||||||
|
&.btn-notify::before { content: "\f1d8"; } // fa-paper-plane
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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: '<notification-read-toggle ' +
|
||||||
|
':notificationId="notificationId" ' +
|
||||||
|
':buttonClass="buttonClass" ' +
|
||||||
|
':buttonNoText="buttonNoText" ' +
|
||||||
|
':showUrl="showUrl" ' +
|
||||||
|
':isRead="isRead"' +
|
||||||
|
'@markRead="onMarkRead" @markUnread="onMarkUnread"' +
|
||||||
|
'></notification-read-toggle>',
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
})
|
@ -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: '<pick-entity ' +
|
||||||
|
':multiple="multiple" ' +
|
||||||
|
':types="types" ' +
|
||||||
|
':picked="picked" ' +
|
||||||
|
':uniqid="uniqid" ' +
|
||||||
|
'@addNewEntity="addNewEntity" ' +
|
||||||
|
'@removeEntity="removeEntity"></pick-entity>',
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,89 @@
|
|||||||
|
<template>
|
||||||
|
<ul class="list-suggest remove-items">
|
||||||
|
<li v-for="p in picked" @click="removeEntity(p)" :key="p.type+p.id">
|
||||||
|
<span class="chill_denomination">{{ p.text }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<ul class="record_actions">
|
||||||
|
<li>
|
||||||
|
<AddPersons
|
||||||
|
:options="addPersonsOptions"
|
||||||
|
:key="uniqid"
|
||||||
|
:buttonTitle="translatedListOfTypes"
|
||||||
|
:modalTitle="translatedListOfTypes"
|
||||||
|
ref="addPersons"
|
||||||
|
@addNewPersons="addNewEntity"
|
||||||
|
>
|
||||||
|
</AddPersons>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import AddPersons from "ChillPersonAssets/vuejs/_components/AddPersons.vue";
|
||||||
|
import { appMessages } from "./i18n";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "PickEntity",
|
||||||
|
props: {
|
||||||
|
multiple: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
types: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
picked: {
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
uniqid: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['addNewEntity', 'removeEntity'],
|
||||||
|
components: {
|
||||||
|
AddPersons,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
key: ''
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
addPersonsOptions() {
|
||||||
|
return {
|
||||||
|
uniq: !this.multiple,
|
||||||
|
type: this.types,
|
||||||
|
priority: null,
|
||||||
|
button: {
|
||||||
|
size: 'btn-sm',
|
||||||
|
class: 'btn-submit',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
translatedListOfTypes() {
|
||||||
|
let trans = [];
|
||||||
|
this.types.forEach(t => {
|
||||||
|
trans.push(appMessages.fr.pick_entity[t].toLowerCase());
|
||||||
|
})
|
||||||
|
return appMessages.fr.pick_entity.modal_title + trans.join(', ');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
addNewEntity({ selected, modal }) {
|
||||||
|
selected.forEach((item) => {
|
||||||
|
this.$emit('addNewEntity', item.result);
|
||||||
|
}, this
|
||||||
|
);
|
||||||
|
this.$refs.addPersons.resetSearch(); // to cast child method
|
||||||
|
modal.showModal = false;
|
||||||
|
},
|
||||||
|
removeEntity(entity) {
|
||||||
|
console.log('remove entity', entity);
|
||||||
|
this.$emit('removeEntity', entity);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
@ -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 };
|
@ -0,0 +1,112 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="{'btn-group btn-group-sm float-end': isButtonGroup }"
|
||||||
|
role="group" aria-label="Notification actions">
|
||||||
|
|
||||||
|
<button v-if="isRead"
|
||||||
|
class="btn"
|
||||||
|
:class="overrideClass"
|
||||||
|
type="button"
|
||||||
|
:title="$t('markAsUnread')"
|
||||||
|
@click="markAsUnread"
|
||||||
|
>
|
||||||
|
<i class="fa fa-sm fa-envelope-o"></i>
|
||||||
|
<span v-if="!buttonNoText" class="ps-2">
|
||||||
|
{{ $t('markAsUnread') }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button v-if="!isRead"
|
||||||
|
class="btn"
|
||||||
|
:class="overrideClass"
|
||||||
|
type="button"
|
||||||
|
:title="$t('markAsRead')"
|
||||||
|
@click="markAsRead"
|
||||||
|
>
|
||||||
|
<i class="fa fa-sm fa-envelope-open-o"></i>
|
||||||
|
<span v-if="!buttonNoText" class="ps-2">
|
||||||
|
{{ $t('markAsRead') }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<a v-if="isButtonGroup"
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-primary"
|
||||||
|
:href="showUrl"
|
||||||
|
:title="$t('action.show')"
|
||||||
|
>
|
||||||
|
<i class="fa fa-sm fa-eye"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { makeFetch } from 'ChillMainAssets/lib/api/apiMethods.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "NotificationReadToggle",
|
||||||
|
props: {
|
||||||
|
isRead: {
|
||||||
|
required: true,
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
notificationId: {
|
||||||
|
required: true,
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
// Optional
|
||||||
|
buttonClass: {
|
||||||
|
required: false,
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
buttonNoText: {
|
||||||
|
required: false,
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
showUrl: {
|
||||||
|
required: false,
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['markRead', 'markUnread'],
|
||||||
|
computed: {
|
||||||
|
/// [Option] override default button appearance (btn-misc)
|
||||||
|
overrideClass() {
|
||||||
|
return this.buttonClass ? this.buttonClass : 'btn-misc'
|
||||||
|
},
|
||||||
|
/// [Option] don't display text on button
|
||||||
|
buttonHideText() {
|
||||||
|
return this.buttonNoText;
|
||||||
|
},
|
||||||
|
/// [Option] showUrl is href for show page second button.
|
||||||
|
// When passed, the component return a button-group with 2 buttons.
|
||||||
|
isButtonGroup() {
|
||||||
|
return !!this.showUrl
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
markAsUnread() {
|
||||||
|
makeFetch('POST', `/api/1.0/main/notification/${this.notificationId}/mark/unread`, []).then(response => {
|
||||||
|
this.$emit('markRead', { notificationId: this.notificationId });
|
||||||
|
})
|
||||||
|
},
|
||||||
|
markAsRead() {
|
||||||
|
makeFetch('POST', `/api/1.0/main/notification/${this.notificationId}/mark/read`, []).then(response => {
|
||||||
|
this.$emit('markUnread', { notificationId: this.notificationId });
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
i18n: {
|
||||||
|
messages: {
|
||||||
|
fr: {
|
||||||
|
markAsUnread: 'Marquer comme non-lu',
|
||||||
|
markAsRead: 'Marquer comme lu'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
|
||||||
|
</style>
|
@ -215,3 +215,8 @@
|
|||||||
{{ form_widget(form.center) }}
|
{{ form_widget(form.center) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block pick_user_dynamic_widget %}
|
||||||
|
<input type="hidden" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value }}" {% endif %} data-input-uniqid="{{ form.vars['uniqid'] }}"/>
|
||||||
|
<div data-module="pick-dynamic" data-types="{{ form.vars['types']|json_encode }}" data-multiple="{{ form.vars['multiple'] }}" data-uniqid="{{ form.vars['uniqid'] }}"></div>
|
||||||
|
{% endblock %}
|
||||||
|
@ -0,0 +1,88 @@
|
|||||||
|
<div class="item-bloc {% if not notification.isReadBy(app.user) %}unread{% else %}read{% endif %}">
|
||||||
|
<div class="item-row title">
|
||||||
|
<h2 class="notification-title">
|
||||||
|
<a href="{{ chill_path_add_return_path('chill_main_notification_show', {'id': notification.id}) }}">
|
||||||
|
{{ notification.title }}
|
||||||
|
</a>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="item-row mt-2 header">
|
||||||
|
|
||||||
|
<div class="item-col">
|
||||||
|
<ul class="small_in_title">
|
||||||
|
{% if step is not defined or step == 'inbox' %}
|
||||||
|
<li class="notification-from">
|
||||||
|
<span class="item-key">
|
||||||
|
<abbr title="{{ 'notification.received_from'|trans }}">
|
||||||
|
{{ 'notification.from'|trans }} :
|
||||||
|
</abbr>
|
||||||
|
</span>
|
||||||
|
{% if not notification.isSystem %}
|
||||||
|
<span class="badge-user">
|
||||||
|
{{ notification.sender|chill_entity_render_string }}
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge-user system">{{ 'notification.is_system'|trans }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if notification.addressees|length > 0 %}
|
||||||
|
<li class="notification-to">
|
||||||
|
<span class="item-key">
|
||||||
|
<abbr title="{{ 'notification.sent_to'|trans }}">
|
||||||
|
{{ 'notification.to'|trans }} :
|
||||||
|
</abbr>
|
||||||
|
</span>
|
||||||
|
{% for a in notification.addressees %}
|
||||||
|
<span class="badge-user">
|
||||||
|
{{ a|chill_entity_render_string }}
|
||||||
|
</span>
|
||||||
|
{% endfor %}
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item-col">
|
||||||
|
{{ notification.date|format_datetime('long', 'short') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="item-row separator">
|
||||||
|
<div class="mx-3 flex-grow-1">
|
||||||
|
{% include data.template with data.template_data %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="item-row row">
|
||||||
|
<div>
|
||||||
|
<blockquote class="chill-user-quote">
|
||||||
|
{{ notification.message|u.truncate(250, '…', false)|chill_markdown_to_html }}
|
||||||
|
</blockquote>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if action_button is not defined or action_button != 'false' %}
|
||||||
|
<div class="item-row separator">
|
||||||
|
<ul class="record_actions">
|
||||||
|
<li>
|
||||||
|
{# Vue component #}
|
||||||
|
<span class="notification_toggle_read_status"
|
||||||
|
data-notification-id="{{ notification.id }}"
|
||||||
|
data-notification-current-is-read="{{ notification.isReadBy(app.user) }}"
|
||||||
|
></span>
|
||||||
|
</li>
|
||||||
|
{% if is_granted('CHILL_MAIN_NOTIFICATION_UPDATE', notification) %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ chill_path_add_return_path('chill_main_notification_edit', {'id': notification.id}) }}"
|
||||||
|
class="btn btn-edit" title="{{ 'Edit'|trans }}"></a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if is_granted('CHILL_MAIN_NOTIFICATION_SEE', notification) %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ chill_path_add_return_path('chill_main_notification_show', {'id': notification.id}) }}"
|
||||||
|
class="btn btn-show" title="{{ 'Show'|trans }}"></a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
@ -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 %}
|
||||||
|
<div class="col-8 notification notification-new">
|
||||||
|
<h1 class="mb-5">{{ block('title') }}</h1>
|
||||||
|
|
||||||
|
{{ 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) %}
|
||||||
|
|
||||||
|
<div class="mb-3 row">
|
||||||
|
<label class="col-form-label col-sm-4" for="notification_message">{{ form_label(form.message) }}</label>
|
||||||
|
<div class="col-12">
|
||||||
|
{{ form_widget(form.message) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ form_end(form) }}
|
||||||
|
|
||||||
|
<ul class="record_actions sticky-form-buttons">
|
||||||
|
<li class="cancel">
|
||||||
|
<a href="{{ chill_return_path_or('chill_main_homepage') }}" class="btn btn-cancel">{{ 'Cancel'|trans|chill_return_path_label }}</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button type="submit" form="notification" class="btn btn-save change-icon">
|
||||||
|
<i class="fa fa-paper-plane fa-fw"></i> {{ 'notification.Send'|trans }}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -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 %}
|
||||||
|
<div class="col-8 notification notification-edit">
|
||||||
|
<h1 class="mb-5">{{ block('title') }}</h1>
|
||||||
|
|
||||||
|
{{ 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) %}
|
||||||
|
|
||||||
|
<div class="mb-3 row">
|
||||||
|
<label class="col-form-label col-sm-4" for="notification_message">{{ form_label(form.message) }}</label>
|
||||||
|
<div class="col-12">
|
||||||
|
{{ form_widget(form.message) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ form_end(form) }}
|
||||||
|
|
||||||
|
<ul class="record_actions sticky-form-buttons">
|
||||||
|
<li class="cancel">
|
||||||
|
<a href="{{ chill_return_path_or('chill_main_homepage') }}" class="btn btn-cancel">{{ 'Cancel'|trans|chill_return_path_label }}</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button type="submit" form="notification" class="btn btn-save">{{ 'Save'|trans }}</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -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
|
@ -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
|
@ -0,0 +1,45 @@
|
|||||||
|
<div class="list-group my-2 notification notification-box">
|
||||||
|
<div class="list-group-item">
|
||||||
|
<h4>{{ 'notification.Sent'|trans }}</h4>
|
||||||
|
</div>
|
||||||
|
{# TODO pagination or limit #}
|
||||||
|
{% for notification in notifications %}
|
||||||
|
<div class="list-group-item {% if notification.isReadBy(app.user) %}read{% else %}unread{% endif %}">
|
||||||
|
|
||||||
|
{% if not notification.isSystem %}
|
||||||
|
{% if notification.sender == app.user %}
|
||||||
|
|
||||||
|
<h6 class="notification-title">
|
||||||
|
<abbr title="{{ 'Le ' ~ notification.date|format_date('long') ~ '\n' ~ notification.title }}">
|
||||||
|
{{ notification.date|format_datetime('short','short') }}
|
||||||
|
</abbr>
|
||||||
|
{# Vue component #}
|
||||||
|
<span class="notification_toggle_read_status"
|
||||||
|
data-notification-id="{{ notification.id }}"
|
||||||
|
data-notification-current-is-read="{{ notification.isReadBy(app.user) }}"
|
||||||
|
data-show-button-url="{{ chill_path_add_return_path('chill_main_notification_show', {'id': notification.id}) }}"
|
||||||
|
data-button-class="btn-outline-primary"
|
||||||
|
data-button-text="false"
|
||||||
|
></span>
|
||||||
|
</h6>
|
||||||
|
|
||||||
|
{% if notification.addressees|length > 0 %}
|
||||||
|
<abbr title="{{ 'notification.sent_to'|trans }}">{{ 'notification.to'|trans }}:</abbr>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for a in notification.addressees %}
|
||||||
|
<span class="badge-user">
|
||||||
|
{{ a|chill_entity_render_string }}
|
||||||
|
</span>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
<div>{{ 'notification.you were notified by %sender%'|trans({'%sender%': notification.sender|chill_entity_render_string }) }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<div>{{ 'notification.you were notified by system'|trans }}</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
@ -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 %}
|
||||||
|
<div class="col-10 notification notification-list">
|
||||||
|
<h1>{{ block('title') }}</h1>
|
||||||
|
|
||||||
|
<ul class="nav nav-pills justify-content-center">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if step == 'inbox' %}active{% endif %}" href="{{ path('chill_main_notification_my') }}">
|
||||||
|
{{ 'notification.Notifications received'|trans }}
|
||||||
|
{% if unreads['inbox'] > 0 %}
|
||||||
|
<span class="badge rounded-pill bg-danger">
|
||||||
|
{{ unreads['inbox'] }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if step == 'sent' %}active{% endif %}" href="{{ path('chill_main_notification_sent') }}">
|
||||||
|
{{ 'notification.Notifications sent'|trans }}
|
||||||
|
{% if unreads['sent'] > 0 %}
|
||||||
|
<span class="badge rounded-pill bg-danger">
|
||||||
|
{{ unreads['sent'] }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{% if datas|length == 0 %}
|
||||||
|
{% if step == 'inbox' %}
|
||||||
|
<p class="chill-no-data-statement">{{ 'notification.Any notification received'|trans }}</p>
|
||||||
|
{% else %}
|
||||||
|
<p class="chill-no-data-statement">{{ 'notification.Any notification sent'|trans }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<div class="flex-table">
|
||||||
|
{% for data in datas %}
|
||||||
|
{% set notification = data.notification %}
|
||||||
|
{% include 'ChillMainBundle:Notification:_list_item.html.twig' %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
@ -1,42 +1,121 @@
|
|||||||
{% extends "@ChillMain/layout.html.twig" %}
|
{% 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) %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ chill_path_forward_return_path('chill_main_notification_show', {
|
||||||
|
'_fragment': 'comment-' ~ comment.id,
|
||||||
|
'edit': comment.id,
|
||||||
|
'id': comment.notification.id
|
||||||
|
}) }}" class="btn btn-edit" title="{{ 'Edit'|trans }}"
|
||||||
|
></a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="container content">
|
<div class="col-10 notification notification-show">
|
||||||
<div class="grid-8 centered">
|
|
||||||
<h1>{{ "Notifications list" | trans }}</h1>
|
|
||||||
<!-- TODO : UNREAD & READ -->
|
|
||||||
|
|
||||||
{%for data in datas %}
|
<h1>{{ 'notification.Notification'|trans }}</h1>
|
||||||
{% set notification = data.notification %}
|
|
||||||
|
|
||||||
<dl class="chill_view_data">
|
<div class="flex-table">
|
||||||
<dt class="inline">{{ 'Message'|trans }}</dt>
|
{% include 'ChillMainBundle:Notification:_list_item.html.twig' with {
|
||||||
<dd>{{ notification.message }}</dd>
|
'data': {
|
||||||
</dl>
|
'template': handler.getTemplate(notification),
|
||||||
|
'template_data': handler.getTemplateData(notification)
|
||||||
|
},
|
||||||
|
'action_button': 'false'
|
||||||
|
} %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<dl class="chill_view_data">
|
<div class="notification-comment-list my-5">
|
||||||
<dt class="inline">{{ 'Date'|trans }}</dt>
|
<h2 class="chill-blue">{{ 'notification.comments_list'|trans }}</h2>
|
||||||
<dd>{{ notification.date | date('long') }}</dd>
|
|
||||||
</dl>
|
|
||||||
|
|
||||||
|
{% if notification.comments|length > 0 %}
|
||||||
|
<div class="flex-table">
|
||||||
|
{% for comment in notification.comments %}
|
||||||
|
|
||||||
<dl class="chill_view_data">
|
{% if editedCommentForm is null or editedCommentId != comment.id %}
|
||||||
<dt class="inline">{{ 'Sender'|trans }}</dt>
|
{{ m.show_comment(comment, {
|
||||||
<dd>{{ notification.sender }}</dd>
|
'recordAction': _self.recordAction(comment)
|
||||||
</dl>
|
}) }}
|
||||||
|
{% else %}
|
||||||
|
<div class="item-bloc">
|
||||||
|
<div class="item-row row">
|
||||||
|
<a id="comment-{{ comment.id }}"></a>
|
||||||
|
|
||||||
<dl class="chill_view_data">
|
{{ form_start(editedCommentForm) }}
|
||||||
<dt class="inline">{{ 'Addressees'|trans }}</dt>
|
{{ form_errors(editedCommentForm) }}
|
||||||
<dd>{{ notification.addressees |join(', ') }}</dd>
|
{{ form_widget(editedCommentForm.content) }}
|
||||||
</dl>
|
<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">
|
||||||
|
{{ 'cancel'|trans }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button class="btn btn-save" type="submit">{{ 'Save'|trans }}</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{{ form_end(editedCommentForm) }}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<dl class="chill_view_data">
|
|
||||||
<dt class="inline">{{ 'Entity'|trans }}</dt>
|
|
||||||
<dd>
|
|
||||||
{% include data.template with data.template_data %}
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="new-comment my-5">
|
||||||
|
<h2 class="chill-blue">{{ 'Write a new comment'|trans }}</h2>
|
||||||
|
|
||||||
|
{{ form_start(appendCommentForm) }}
|
||||||
|
{{ form_errors(appendCommentForm) }}
|
||||||
|
{{ form_widget(appendCommentForm.content) }}
|
||||||
|
<input type="hidden" name="form" value="append" />
|
||||||
|
<ul class="record_actions">
|
||||||
|
<li>
|
||||||
|
<button class="btn btn-create" type="submit">{{ 'notification.append_comment'|trans }}</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{{ form_end(appendCommentForm) }}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="record_actions sticky-form-buttons">
|
||||||
|
<li class="cancel">
|
||||||
|
<a href="{{ chill_return_path_or('chill_main_notification_my') }}" class="btn btn-cancel">
|
||||||
|
{{ 'Cancel'|trans|chill_return_path_label }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{# Vue component #}
|
||||||
|
<span class="notification_toggle_read_status"
|
||||||
|
data-notification-id="{{ notification.id }}"
|
||||||
|
data-notification-current-is-read="1"
|
||||||
|
></span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
@ -106,6 +106,6 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% block js%}<!-- nothing added to js -->{% endblock %}
|
{% block js %}<!-- nothing added to js -->{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -12,16 +12,27 @@ declare(strict_types=1);
|
|||||||
namespace Chill\MainBundle\Routing\MenuBuilder;
|
namespace Chill\MainBundle\Routing\MenuBuilder;
|
||||||
|
|
||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Chill\MainBundle\Notification\Counter\NotificationByUserCounter;
|
||||||
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
|
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
|
||||||
use Symfony\Component\Security\Core\Security;
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
class UserMenuBuilder implements LocalMenuBuilderInterface
|
class UserMenuBuilder implements LocalMenuBuilderInterface
|
||||||
{
|
{
|
||||||
|
private NotificationByUserCounter $notificationByUserCounter;
|
||||||
|
|
||||||
private Security $security;
|
private Security $security;
|
||||||
|
|
||||||
public function __construct(Security $security)
|
private TranslatorInterface $translator;
|
||||||
{
|
|
||||||
|
public function __construct(
|
||||||
|
NotificationByUserCounter $notificationByUserCounter,
|
||||||
|
Security $security,
|
||||||
|
TranslatorInterface $translator
|
||||||
|
) {
|
||||||
|
$this->notificationByUserCounter = $notificationByUserCounter;
|
||||||
$this->security = $security;
|
$this->security = $security;
|
||||||
|
$this->translator = $translator;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function buildMenu($menuId, \Knp\Menu\MenuItem $menu, array $parameters)
|
public function buildMenu($menuId, \Knp\Menu\MenuItem $menu, array $parameters)
|
||||||
@ -44,6 +55,20 @@ class UserMenuBuilder implements LocalMenuBuilderInterface
|
|||||||
'order' => -9999999,
|
'order' => -9999999,
|
||||||
'icon' => 'map-marker',
|
'icon' => 'map-marker',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$nbNotifications = $this->notificationByUserCounter->countUnreadByUser($user);
|
||||||
|
|
||||||
|
$menu
|
||||||
|
->addChild(
|
||||||
|
$this->translator->trans('notification.My notifications with counter', ['nb' => $nbNotifications]),
|
||||||
|
['route' => 'chill_main_notification_my']
|
||||||
|
)
|
||||||
|
->setExtras([
|
||||||
|
'order' => 600,
|
||||||
|
'icon' => 'envelope',
|
||||||
|
'counter' => $nbNotifications,
|
||||||
|
]);
|
||||||
|
|
||||||
$menu
|
$menu
|
||||||
->addChild(
|
->addChild(
|
||||||
'Change password',
|
'Change password',
|
||||||
|
@ -0,0 +1,89 @@
|
|||||||
|
<?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\Security\Authorization;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\Notification;
|
||||||
|
use Chill\MainBundle\Entity\NotificationComment;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||||
|
use UnexpectedValueException;
|
||||||
|
|
||||||
|
final class NotificationVoter extends Voter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Allow to add a comment on a notification.
|
||||||
|
*
|
||||||
|
* May apply on both @see{NotificationComment::class} and @see{Notification::class}.
|
||||||
|
*/
|
||||||
|
public const COMMENT_ADD = 'CHILL_MAIN_NOTIFICATION_COMMENT_ADD';
|
||||||
|
|
||||||
|
public const COMMENT_EDIT = 'CHILL_MAIN_NOTIFICATION_COMMENT_EDIT';
|
||||||
|
|
||||||
|
public const NOTIFICATION_SEE = 'CHILL_MAIN_NOTIFICATION_SEE';
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
return $subject instanceof Notification || $subject instanceof NotificationComment;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $attribute
|
||||||
|
* @param mixed $subject
|
||||||
|
*/
|
||||||
|
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
|
||||||
|
{
|
||||||
|
$user = $token->getUser();
|
||||||
|
|
||||||
|
if (!$user instanceof User) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($subject instanceof Notification) {
|
||||||
|
switch ($attribute) {
|
||||||
|
case self::COMMENT_ADD:
|
||||||
|
return false === $subject->isSystem() && (
|
||||||
|
$subject->getAddressees()->contains($user) || $subject->getSender() === $user
|
||||||
|
);
|
||||||
|
|
||||||
|
case self::NOTIFICATION_SEE:
|
||||||
|
case self::NOTIFICATION_TOGGLE_READ_STATUS:
|
||||||
|
return $subject->getSender() === $user || $subject->getAddressees()->contains($user);
|
||||||
|
|
||||||
|
case self::NOTIFICATION_UPDATE:
|
||||||
|
return $subject->getSender() === $user && false === $subject->isSystem();
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new UnexpectedValueException("this subject {$attribute} is not implemented");
|
||||||
|
}
|
||||||
|
} elseif ($subject instanceof NotificationComment) {
|
||||||
|
switch ($attribute) {
|
||||||
|
case self::COMMENT_ADD:
|
||||||
|
return false === $subject->getNotification()->isSystem() && (
|
||||||
|
$subject->getNotification()->getAddressees()->contains($user) || $subject->getNotification()->getSender() === $user
|
||||||
|
);
|
||||||
|
|
||||||
|
case self::COMMENT_EDIT:
|
||||||
|
return $subject->getCreatedBy() === $user && false === $subject->getNotification()->isSystem();
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new UnexpectedValueException("this subject {$attribute} is not implemented");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new UnexpectedValueException();
|
||||||
|
}
|
||||||
|
}
|
@ -56,7 +56,7 @@ class AddressNormalizer implements ContextAwareNormalizerInterface, NormalizerAw
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Address $address
|
* @param Address $address
|
||||||
* @param null|string $format
|
* @param string|null $format
|
||||||
*/
|
*/
|
||||||
public function normalize($address, $format = null, array $context = [])
|
public function normalize($address, $format = null, array $context = [])
|
||||||
{
|
{
|
||||||
|
@ -22,7 +22,7 @@ class CollectionNormalizer implements NormalizerAwareInterface, NormalizerInterf
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Collection $collection
|
* @param Collection $collection
|
||||||
* @param null|string $format
|
* @param string|null $format
|
||||||
*/
|
*/
|
||||||
public function normalize($collection, $format = null, array $context = [])
|
public function normalize($collection, $format = null, array $context = [])
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,98 @@
|
|||||||
|
<?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 DateTimeImmutable;
|
||||||
|
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)
|
||||||
|
->setUpdatedAt(new DateTimeImmutable());
|
||||||
|
$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));
|
||||||
|
}
|
||||||
|
}
|
123
src/Bundle/ChillMainBundle/Tests/Entity/NotificationTest.php
Normal file
123
src/Bundle/ChillMainBundle/Tests/Entity/NotificationTest.php
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
<?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 Entity;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\Notification;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Chill\MainBundle\Repository\UserRepository;
|
||||||
|
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||||
|
use function count;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
final class NotificationTest extends KernelTestCase
|
||||||
|
{
|
||||||
|
private array $toDelete = [];
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
self::bootKernel();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 generateNotificationData()
|
||||||
|
{
|
||||||
|
self::bootKernel();
|
||||||
|
$userRepository = self::$container->get(UserRepository::class);
|
||||||
|
|
||||||
|
$senderId = $userRepository
|
||||||
|
->findOneBy(['username' => 'center b_social'])
|
||||||
|
->getId();
|
||||||
|
|
||||||
|
$addressesIds = [];
|
||||||
|
$addressesIds[] = $userRepository
|
||||||
|
->findOneBy(['username' => 'center b_direction'])
|
||||||
|
->getId();
|
||||||
|
|
||||||
|
yield [
|
||||||
|
$senderId,
|
||||||
|
$addressesIds,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAddAddresseeStoreAnUread()
|
||||||
|
{
|
||||||
|
$notification = new Notification();
|
||||||
|
$notification->addAddressee($user1 = new User());
|
||||||
|
$notification->addAddressee($user2 = new User());
|
||||||
|
$notification->getAddressees()->add($user3 = new User());
|
||||||
|
$notification->getAddressees()->add($user4 = new User());
|
||||||
|
|
||||||
|
$this->assertCount(4, $notification->getAddressees());
|
||||||
|
|
||||||
|
// launch listener
|
||||||
|
$notification->registerUnread();
|
||||||
|
$this->assertCount(4, $notification->getUnreadBy());
|
||||||
|
$this->assertContains($user1, $notification->getUnreadBy()->toArray());
|
||||||
|
$this->assertContains($user2, $notification->getUnreadBy()->toArray());
|
||||||
|
$this->assertContains($user3, $notification->getUnreadBy()->toArray());
|
||||||
|
|
||||||
|
$notification->markAsReadBy($user1);
|
||||||
|
|
||||||
|
$this->assertCount(3, $notification->getUnreadBy());
|
||||||
|
$this->assertNotContains($user1, $notification->getUnreadBy()->toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider generateNotificationData
|
||||||
|
*/
|
||||||
|
public function testPrePersistComputeUnread(int $senderId, array $addressesIds)
|
||||||
|
{
|
||||||
|
$em = self::$container->get(EntityManagerInterface::class);
|
||||||
|
$notification = new Notification();
|
||||||
|
$notification
|
||||||
|
->setSender($em->find(User::class, $senderId))
|
||||||
|
->setRelatedEntityId(0)
|
||||||
|
->setRelatedEntityClass(AccompanyingPeriod::class)
|
||||||
|
->setMessage('Fake message');
|
||||||
|
|
||||||
|
foreach ($addressesIds as $addresseeId) {
|
||||||
|
$notification
|
||||||
|
->getAddressees()->add($em->find(User::class, $addresseeId));
|
||||||
|
}
|
||||||
|
|
||||||
|
$em->persist($notification);
|
||||||
|
$em->flush();
|
||||||
|
$em->refresh($notification);
|
||||||
|
|
||||||
|
$this->toDelete[] = [Notification::class, $notification->getId()];
|
||||||
|
|
||||||
|
$this->assertEquals($senderId, $notification->getSender()->getId());
|
||||||
|
$this->assertCount(count($addressesIds), $notification->getUnreadBy());
|
||||||
|
|
||||||
|
$unreadIds = $notification->getUnreadBy()->map(static function (User $u) { return $u->getId(); });
|
||||||
|
|
||||||
|
foreach ($addressesIds as $addresseeId) {
|
||||||
|
$this->assertContains($addresseeId, $unreadIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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"
|
||||||
|
|
||||||
|
|
||||||
|
@ -61,10 +61,11 @@ module.exports = function(encore, entries)
|
|||||||
encore.addEntry('mod_ckeditor5', __dirname + '/Resources/public/module/ckeditor5/index.js');
|
encore.addEntry('mod_ckeditor5', __dirname + '/Resources/public/module/ckeditor5/index.js');
|
||||||
encore.addEntry('mod_disablebuttons', __dirname + '/Resources/public/module/disable-buttons/index.js');
|
encore.addEntry('mod_disablebuttons', __dirname + '/Resources/public/module/disable-buttons/index.js');
|
||||||
encore.addEntry('mod_blur', __dirname + '/Resources/public/module/blur/index.js');
|
encore.addEntry('mod_blur', __dirname + '/Resources/public/module/blur/index.js');
|
||||||
|
|
||||||
encore.addEntry('mod_input_address', __dirname + '/Resources/public/vuejs/Address/mod_input_address_index.js');
|
encore.addEntry('mod_input_address', __dirname + '/Resources/public/vuejs/Address/mod_input_address_index.js');
|
||||||
|
encore.addEntry('mod_notification_toggle_read_status', __dirname + '/Resources/public/module/notification/toggle_read.js');
|
||||||
|
encore.addEntry('mod_pickentity_type', __dirname + '/Resources/public/module/pick-entity/index.js');
|
||||||
|
|
||||||
// Vue entrypoints
|
// Vue entrypoints
|
||||||
encore.addEntry('vue_address', __dirname + '/Resources/public/vuejs/Address/index.js');
|
encore.addEntry('vue_address', __dirname + '/Resources/public/vuejs/Address/index.js');
|
||||||
encore.addEntry('vue_onthefly', __dirname + '/Resources/public/vuejs/OnTheFly/index.js');
|
encore.addEntry('vue_onthefly', __dirname + '/Resources/public/vuejs/OnTheFly/index.js');
|
||||||
|
|
||||||
};
|
};
|
||||||
|
@ -18,8 +18,8 @@ services:
|
|||||||
|
|
||||||
Chill\MainBundle\Form\Type\:
|
Chill\MainBundle\Form\Type\:
|
||||||
resource: '../Form/Type'
|
resource: '../Form/Type'
|
||||||
tags:
|
autoconfigure: true
|
||||||
- { name: form.type }
|
autowire: true
|
||||||
|
|
||||||
Chill\MainBundle\Doctrine\Event\:
|
Chill\MainBundle\Doctrine\Event\:
|
||||||
resource: '../Doctrine/Event/'
|
resource: '../Doctrine/Event/'
|
||||||
|
@ -13,4 +13,58 @@ services:
|
|||||||
$translator: '@Symfony\Component\Translation\TranslatorInterface'
|
$translator: '@Symfony\Component\Translation\TranslatorInterface'
|
||||||
$routeParameters: '%chill_main.notifications%'
|
$routeParameters: '%chill_main.notifications%'
|
||||||
|
|
||||||
Chill\MainBundle\Notification\NotificationRenderer: ~
|
Chill\MainBundle\Notification\NotificationHandlerManager:
|
||||||
|
arguments:
|
||||||
|
$handlers: !tagged_iterator chill_main.notification_handler
|
||||||
|
|
||||||
|
Chill\MainBundle\Notification\NotificationPresence: ~
|
||||||
|
|
||||||
|
Chill\MainBundle\Notification\Templating\NotificationTwigExtension: ~
|
||||||
|
|
||||||
|
Chill\MainBundle\Notification\Templating\NotificationTwigExtensionRuntime: ~
|
||||||
|
|
||||||
|
Chill\MainBundle\Notification\Counter\NotificationByUserCounter:
|
||||||
|
autoconfigure: true
|
||||||
|
autowire: true
|
||||||
|
tags:
|
||||||
|
-
|
||||||
|
name: 'doctrine.orm.entity_listener'
|
||||||
|
event: 'preFlush'
|
||||||
|
entity: 'Chill\MainBundle\Entity\Notification'
|
||||||
|
# set the 'lazy' option to TRUE to only instantiate listeners when they are used
|
||||||
|
lazy: true
|
||||||
|
method: 'onPreFlushNotification'
|
||||||
|
|
||||||
|
-
|
||||||
|
name: 'doctrine.orm.entity_listener'
|
||||||
|
event: 'postUpdate'
|
||||||
|
entity: 'Chill\MainBundle\Entity\NotificationComment'
|
||||||
|
# set the 'lazy' option to TRUE to only instantiate listeners when they are used
|
||||||
|
lazy: true
|
||||||
|
method: 'onEditNotificationComment'
|
||||||
|
-
|
||||||
|
name: 'doctrine.orm.entity_listener'
|
||||||
|
event: 'postPersist'
|
||||||
|
entity: 'Chill\MainBundle\Entity\NotificationComment'
|
||||||
|
# set the 'lazy' option to TRUE to only instantiate listeners when they are used
|
||||||
|
lazy: true
|
||||||
|
method: 'onEditNotificationComment'
|
||||||
|
|
||||||
|
Chill\MainBundle\Notification\Email\NotificationMailer:
|
||||||
|
autowire: true
|
||||||
|
autoconfigure: true
|
||||||
|
tags:
|
||||||
|
-
|
||||||
|
name: 'doctrine.orm.entity_listener'
|
||||||
|
event: 'postPersist'
|
||||||
|
entity: 'Chill\MainBundle\Entity\Notification'
|
||||||
|
# set the 'lazy' option to TRUE to only instantiate listeners when they are used
|
||||||
|
lazy: true
|
||||||
|
method: 'postPersistNotification'
|
||||||
|
-
|
||||||
|
name: 'doctrine.orm.entity_listener'
|
||||||
|
event: 'postPersist'
|
||||||
|
entity: 'Chill\MainBundle\Entity\NotificationComment'
|
||||||
|
# set the 'lazy' option to TRUE to only instantiate listeners when they are used
|
||||||
|
lazy: true
|
||||||
|
method: 'postPersistComment'
|
||||||
|
@ -24,6 +24,8 @@ services:
|
|||||||
|
|
||||||
Chill\MainBundle\Security\Authorization\DefaultVoterHelperFactory: ~
|
Chill\MainBundle\Security\Authorization\DefaultVoterHelperFactory: ~
|
||||||
|
|
||||||
|
Chill\MainBundle\Security\Authorization\NotificationVoter: ~
|
||||||
|
|
||||||
Chill\MainBundle\Security\Authorization\VoterHelperFactoryInterface: '@Chill\MainBundle\Security\Authorization\DefaultVoterHelperFactory'
|
Chill\MainBundle\Security\Authorization\VoterHelperFactoryInterface: '@Chill\MainBundle\Security\Authorization\DefaultVoterHelperFactory'
|
||||||
|
|
||||||
chill.main.security.authorization.helper:
|
chill.main.security.authorization.helper:
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
<?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\Migrations\Main;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20211225231532 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('DROP TABLE chill_main_notification_addresses_unread');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_notification ADD read JSONB DEFAULT \'[]\'');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Store notification readed by user in a specific table';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('CREATE TABLE chill_main_notification_addresses_unread (notification_id INT NOT NULL, user_id INT NOT NULL, PRIMARY KEY(notification_id, user_id))');
|
||||||
|
$this->addSql('CREATE INDEX IDX_154A075FEF1A9D84 ON chill_main_notification_addresses_unread (notification_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_154A075FA76ED395 ON chill_main_notification_addresses_unread (user_id)');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_notification_addresses_unread ADD CONSTRAINT FK_154A075FEF1A9D84 FOREIGN KEY (notification_id) REFERENCES chill_main_notification (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_notification_addresses_unread ADD CONSTRAINT FK_154A075FA76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_notification DROP read');
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
<?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\Migrations\Main;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20211228183221 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('create unique index uniq_5bdc8067567988b4440f6072
|
||||||
|
on chill_main_notification (relatedentityclass, relatedentityid)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'remove unique index which prevent to notify twice an entity';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('DROP INDEX uniq_5bdc8067567988b4440f6072');
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
<?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\Migrations\Main;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20211228215919 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('DROP SEQUENCE chill_main_notification_comment_id_seq CASCADE');
|
||||||
|
$this->addSql('DROP TABLE chill_main_notification_comment');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Notifications: add comment on notifications';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('CREATE SEQUENCE chill_main_notification_comment_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
|
||||||
|
$this->addSql('CREATE TABLE chill_main_notification_comment (id INT NOT NULL, notification_id INT NOT NULL, content TEXT NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updateAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, createdBy_id INT DEFAULT NULL, updatedBy_id INT DEFAULT NULL, PRIMARY KEY(id))');
|
||||||
|
$this->addSql('CREATE INDEX IDX_983BD2CFEF1A9D84 ON chill_main_notification_comment (notification_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_983BD2CF3174800F ON chill_main_notification_comment (createdBy_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_983BD2CF65FF1AEC ON chill_main_notification_comment (updatedBy_id)');
|
||||||
|
$this->addSql('COMMENT ON COLUMN chill_main_notification_comment.createdAt IS \'(DC2Type:datetime_immutable)\'');
|
||||||
|
$this->addSql('COMMENT ON COLUMN chill_main_notification_comment.updateAt IS \'(DC2Type:datetime_immutable)\'');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_notification_comment ADD CONSTRAINT FK_983BD2CFEF1A9D84 FOREIGN KEY (notification_id) REFERENCES chill_main_notification (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_notification_comment ADD CONSTRAINT FK_983BD2CF3174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_notification_comment ADD CONSTRAINT FK_983BD2CF65FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
<?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\Migrations\Main;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20211229140308 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE chill_main_notification DROP CONSTRAINT FK_5BDC806765FF1AEC');
|
||||||
|
$this->addSql('DROP INDEX IDX_5BDC806765FF1AEC');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_notification DROP updatedAt');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_notification DROP updatedBy_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Notification: add updated tracking information';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE chill_main_notification ADD updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
|
||||||
|
$this->addSql('UPDATE chill_main_notification SET updatedAt="date"');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_notification ADD updatedBy_id INT DEFAULT NULL');
|
||||||
|
$this->addSql('COMMENT ON COLUMN chill_main_notification.updatedAt IS \'(DC2Type:datetime_immutable)\'');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_notification ADD CONSTRAINT FK_5BDC806765FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('CREATE INDEX IDX_5BDC806765FF1AEC ON chill_main_notification (updatedBy_id)');
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
<?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\Migrations\Main;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20211230003532 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE chill_main_notification DROP title');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_notification ALTER sender_id SET NOT NULL');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_notification ALTER updatedAt DROP NOT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add title and allow system notification (sender is null)';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE chill_main_notification ADD title TEXT DEFAULT \'\' NOT NULL');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_notification ALTER sender_id DROP NOT NULL');
|
||||||
|
$this->addSql('UPDATE chill_main_notification set updatedat="date"');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_notification ALTER updatedat SET NOT NULL');
|
||||||
|
}
|
||||||
|
}
|
@ -4,3 +4,12 @@ years_old: >-
|
|||||||
many {# ans}
|
many {# ans}
|
||||||
other {# ans}
|
other {# ans}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
notification:
|
||||||
|
My notifications with counter: >-
|
||||||
|
{nb, plural,
|
||||||
|
=0 {Mes notifications}
|
||||||
|
one {Une notification}
|
||||||
|
few {# notifications}
|
||||||
|
other {# notifications}
|
||||||
|
}
|
||||||
|
@ -351,3 +351,33 @@ By: Par
|
|||||||
For: Pour
|
For: Pour
|
||||||
Created for: Créé pour
|
Created for: Créé pour
|
||||||
Created by: Créé par
|
Created by: Créé par
|
||||||
|
|
||||||
|
notification:
|
||||||
|
Notification: Notification
|
||||||
|
My own notifications: Mes notifications
|
||||||
|
Notify: Envoyer une notification
|
||||||
|
Send: Envoyer
|
||||||
|
Edit notification: Modifier une notification
|
||||||
|
Notification created: Notification envoyée
|
||||||
|
Notification updated: La notification a été mise à jour
|
||||||
|
Any notification received: Aucune notification reçue
|
||||||
|
Any notification sent: Aucune notification envoyée
|
||||||
|
Notifications received: Notifications reçues
|
||||||
|
Notifications sent: Notifications envoyées
|
||||||
|
comment_appended: Commentaire ajouté
|
||||||
|
append_comment: Ajouter un commentaire
|
||||||
|
comment_updated: Commentaire mis à jour
|
||||||
|
comments_list: Fil de commentaires
|
||||||
|
show notification from %sender%: Voir la notification de %sender%
|
||||||
|
is_unread: Non-lue
|
||||||
|
is_system: notification automatique
|
||||||
|
list: Notifications
|
||||||
|
Sent: Envoyé
|
||||||
|
to: À
|
||||||
|
sent_to: Destinataire(s)
|
||||||
|
from: De
|
||||||
|
received_from: Expéditeur
|
||||||
|
you were notified by %sender%: Vous avez été notifié par %sender%
|
||||||
|
you were notified by system: Vous avez été notifié automatiquement
|
||||||
|
subject: Objet
|
||||||
|
|
||||||
|
@ -0,0 +1,77 @@
|
|||||||
|
<?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\PersonBundle\AccompanyingPeriod\Workflow;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\Notification;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
use Symfony\Component\Templating\EngineInterface;
|
||||||
|
use Symfony\Component\Workflow\Event\EnteredEvent;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
|
class WorkflowEventSubscriber implements EventSubscriberInterface
|
||||||
|
{
|
||||||
|
private EntityManagerInterface $em;
|
||||||
|
|
||||||
|
private EngineInterface $engine;
|
||||||
|
|
||||||
|
private Security $security;
|
||||||
|
|
||||||
|
private TranslatorInterface $translator;
|
||||||
|
|
||||||
|
public function __construct(Security $security, TranslatorInterface $translator, EngineInterface $engine, EntityManagerInterface $em)
|
||||||
|
{
|
||||||
|
$this->security = $security;
|
||||||
|
$this->translator = $translator;
|
||||||
|
$this->engine = $engine;
|
||||||
|
$this->em = $em;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getSubscribedEvents()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'workflow.accompanying_period_lifecycle.entered' => [
|
||||||
|
'onStateEntered',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onStateEntered(EnteredEvent $enteredEvent): void
|
||||||
|
{
|
||||||
|
if ($enteredEvent->getMarking()->has(AccompanyingPeriod::STEP_CONFIRMED)) {
|
||||||
|
$this->onPeriodConfirmed($enteredEvent->getSubject());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function onPeriodConfirmed(AccompanyingPeriod $period)
|
||||||
|
{
|
||||||
|
if ($period->getUser() instanceof User
|
||||||
|
&& $period->getUser() !== $this->security->getUser()) {
|
||||||
|
$notification = new Notification();
|
||||||
|
$notification
|
||||||
|
->setRelatedEntityId($period->getId())
|
||||||
|
->setRelatedEntityClass(AccompanyingPeriod::class)
|
||||||
|
->setTitle($this->translator->trans('period_notification.period_designated_subject'))
|
||||||
|
->setMessage($this->engine->render(
|
||||||
|
'@ChillPerson/Notification/accompanying_course_designation.md.twig',
|
||||||
|
[
|
||||||
|
'accompanyingCourse' => $period,
|
||||||
|
]
|
||||||
|
))
|
||||||
|
->addAddressee($period->getUser());
|
||||||
|
$this->em->persist($notification);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
<?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\PersonBundle\Notification;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\Notification;
|
||||||
|
use Chill\MainBundle\Notification\NotificationHandlerInterface;
|
||||||
|
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||||
|
use Chill\PersonBundle\Repository\AccompanyingPeriodRepository;
|
||||||
|
|
||||||
|
final class AccompanyingPeriodNotificationHandler implements NotificationHandlerInterface
|
||||||
|
{
|
||||||
|
private AccompanyingPeriodRepository $accompanyingPeriodRepository;
|
||||||
|
|
||||||
|
public function __construct(AccompanyingPeriodRepository $accompanyingPeriodRepository)
|
||||||
|
{
|
||||||
|
$this->accompanyingPeriodRepository = $accompanyingPeriodRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTemplate(Notification $notification, array $options = []): string
|
||||||
|
{
|
||||||
|
return 'ChillPersonBundle:AccompanyingPeriod:showInNotification.html.twig';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTemplateData(Notification $notification, array $options = []): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'notification' => $notification,
|
||||||
|
'period' => $this->accompanyingPeriodRepository->find($notification->getRelatedEntityId()),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supports(Notification $notification, array $options = []): bool
|
||||||
|
{
|
||||||
|
return $notification->getRelatedEntityClass() === AccompanyingPeriod::class;
|
||||||
|
}
|
||||||
|
}
|
@ -1,33 +0,0 @@
|
|||||||
<?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\PersonBundle\Notification;
|
|
||||||
|
|
||||||
use Chill\MainBundle\Entity\Notification;
|
|
||||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
|
||||||
|
|
||||||
final class AccompanyingPeriodNotificationRenderer
|
|
||||||
{
|
|
||||||
public function getTemplate()
|
|
||||||
{
|
|
||||||
return 'ChillPersonBundle:AccompanyingPeriod:showInNotification.html.twig';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getTemplateData(Notification $notification)
|
|
||||||
{
|
|
||||||
return ['notification' => $notification];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function supports(Notification $notification)
|
|
||||||
{
|
|
||||||
return $notification->getRelatedEntityClass() === AccompanyingPeriod::class;
|
|
||||||
}
|
|
||||||
}
|
|
@ -259,9 +259,9 @@ abbr.referrer { // still used ?
|
|||||||
div#dashboards {
|
div#dashboards {
|
||||||
div.mbloc {
|
div.mbloc {
|
||||||
& > div:not(.warnings) {
|
& > div:not(.warnings) {
|
||||||
border: 1px solid $chill-light-gray;
|
//border: 1px solid $chill-light-gray;
|
||||||
|
//border-radius: 0.35rem;
|
||||||
background-color: $chill-llight-gray;
|
background-color: $chill-llight-gray;
|
||||||
border-radius: 0.35rem;
|
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
& > div.warnings .alert {
|
& > div.warnings .alert {
|
||||||
|
@ -20,6 +20,11 @@ span.badge-thirdparty {
|
|||||||
|
|
||||||
span.badge-user {
|
span.badge-user {
|
||||||
border-bottom-width: 1px;
|
border-bottom-width: 1px;
|
||||||
|
&.system {
|
||||||
|
background-color: $chill-llight-gray;
|
||||||
|
font-style: italic;
|
||||||
|
color: $chill-gray;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
span.badge-person {
|
span.badge-person {
|
||||||
border-bottom-color: $chill-green;
|
border-bottom-color: $chill-green;
|
||||||
|
@ -81,6 +81,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if form is not null %}
|
{% if form is not null %}
|
||||||
<div class="new-comment my-5">
|
<div class="new-comment my-5">
|
||||||
<h2 class="chill-blue">{{ 'Write a new comment'|trans }}</h2>
|
<h2 class="chill-blue">{{ 'Write a new comment'|trans }}</h2>
|
||||||
|
@ -13,14 +13,19 @@
|
|||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
{{ parent() }}
|
{{ parent() }}
|
||||||
{{ encore_entry_script_tags('page_accompanying_course_index_person_locate') }}
|
{{ encore_entry_script_tags('page_accompanying_course_index_person_locate') }}
|
||||||
{{ encore_entry_script_tags('page_accompanying_course_index_masonry') }}
|
{{ encore_entry_script_tags('page_accompanying_course_index_masonry') }}
|
||||||
|
{{ encore_entry_script_tags('mod_notification_toggle_read_status') }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
{{ parent() }}
|
||||||
|
{{ encore_entry_link_tags('mod_notification_toggle_read_status') }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="accompanyingcourse-resume">
|
<div class="accompanyingcourse-resume">
|
||||||
|
|
||||||
<div id="dashboards" class="row g-3" data-masonry='{"percentPosition": true }'>
|
<div id="dashboards" class="row g-3" data-masonry='{"percentPosition": true }'>
|
||||||
|
|
||||||
{% if 'DRAFT' == accompanyingCourse.step %}
|
{% if 'DRAFT' == accompanyingCourse.step %}
|
||||||
@ -186,3 +191,18 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block block_post_menu %}
|
||||||
|
<div class="post-menu pt-4">
|
||||||
|
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<a class="btn btn-primary" href="{{ chill_path_add_return_path('chill_main_notification_create', {'entityClass': 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod', 'entityId': accompanyingCourse.id}) }}">
|
||||||
|
<i class="fa fa-paper-plane fa-fw"></i>
|
||||||
|
{{ 'notification.Notify'|trans }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ chill_list_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod', accompanyingCourse.id) }}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
@ -1,145 +1,29 @@
|
|||||||
{% block content %}
|
{% if person is defined %}
|
||||||
<div class="flex-table accompanyingcourse-list">
|
{% set contextEntity = { 'type': 'person', 'entity': person } %}{% endif %}
|
||||||
{% for accompanying_period in accompanying_periods %}
|
{% if household is defined %}
|
||||||
<div class="item-bloc">
|
{% set contextEntity = { 'type': 'household', 'entity': household } %}{% endif %}
|
||||||
<div class="item-row">
|
|
||||||
<div class="wrap-header">
|
|
||||||
<div class="wh-row">
|
|
||||||
<div class="wh-col">
|
|
||||||
<span class="h3">
|
|
||||||
<i class="fa fa-fw fa-random"></i>
|
|
||||||
<b>{{ accompanying_period.id }}</b>
|
|
||||||
</span>
|
|
||||||
{% if accompanying_period.emergency %}
|
|
||||||
<span class="badge rounded-pill bg-danger">{{- 'Emergency'|trans|upper -}}</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if accompanying_period.confidential %}
|
|
||||||
<span class="badge rounded-pill bg-danger">{{- 'Confidential'|trans|upper -}}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="wh-col">
|
|
||||||
{% if accompanying_period.step == 'DRAFT' %}
|
|
||||||
<span class="badge bg-secondary">{{- 'Draft'|trans|upper -}}</span>
|
|
||||||
{% elseif accompanying_period.step == 'CONFIRMED' %}
|
|
||||||
<span class="badge bg-primary">{{- 'Confirmed'|trans|upper -}}</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-primary">{{- 'Closed'|trans|upper -}}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="wh-row">
|
|
||||||
<div class="wh-col">
|
|
||||||
{% if accompanying_period.closingDate == null %}
|
|
||||||
{{ 'accompanying_period.dates_from_%opening_date%'|trans({ '%opening_date%': accompanying_period.openingDate|format_date('long') } ) }}
|
|
||||||
{% else %}
|
|
||||||
{{ 'accompanying_period.dates_from_%opening_date%_to_%closing_date%'|trans({
|
|
||||||
'%opening_date%': accompanying_period.openingDate|format_date('long'),
|
|
||||||
'%closing_date%': accompanying_period.closingDate|format_date('long')}
|
|
||||||
) }}
|
|
||||||
{% if accompanying_period.isOpen == false %}
|
|
||||||
<dl class="chill_view_data">
|
|
||||||
<dt>{{ 'Closing motive'|trans }} :</dt>
|
|
||||||
<dd>{{ accompanying_period.closingMotive|chill_entity_render_box }}</dd>
|
|
||||||
</dl>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="wh-col">
|
|
||||||
{% if chill_accompanying_periods.fields.user == 'visible' %}
|
|
||||||
{% if accompanying_period.user %}
|
|
||||||
<abbr class="referrer" title="{{ 'Referrer'|trans }}">ref:</abbr>
|
|
||||||
{{ accompanying_period.user.username|chill_entity_render_box }}
|
|
||||||
{% else %}
|
|
||||||
<span class="chill-no-data-statement">{{ 'No accompanying user'|trans }}</span>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
{% macro recordAction(period, contextEntity) %}
|
||||||
<div class="item-row separator">
|
|
||||||
<div class="wrap-list">
|
|
||||||
{% if accompanying_period.requestorPerson is not null or accompanying_period.requestorThirdParty is not null %}
|
|
||||||
<div class="wl-row">
|
|
||||||
<div class="wl-col title"><h3>{{ 'Requestor'|trans({'gender': null }) }}</h3></div>
|
|
||||||
<div class="wl-col list">
|
|
||||||
{% if accompanying_period.requestorPerson is not null %}
|
|
||||||
<span class="wl-item">
|
|
||||||
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
|
|
||||||
action: 'show', displayBadge: true,
|
|
||||||
targetEntity: { name: 'person', id: accompanying_period.requestorPerson.id },
|
|
||||||
buttonText: accompanying_period.requestorPerson|chill_entity_render_string
|
|
||||||
} %}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if accompanying_period.requestorThirdParty is not null %}
|
|
||||||
<span class="wl-item">
|
|
||||||
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
|
|
||||||
action: 'show', displayBadge: true,
|
|
||||||
targetEntity: { name: 'thirdparty', id: accompanying_period.requestorThirdParty.id },
|
|
||||||
buttonText: accompanying_period.requestorThirdParty|chill_entity_render_string
|
|
||||||
} %}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if accompanying_period.participations.count > 0 %}
|
|
||||||
<div class="wl-row">
|
|
||||||
<div class="wl-col title"><h3>{{ 'Participants'|trans }}</h3></div>
|
|
||||||
<div class="wl-col list">
|
|
||||||
{% for p in accompanying_period.getCurrentParticipations %}
|
|
||||||
<span class="wl-item">
|
|
||||||
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
|
|
||||||
action: 'show', displayBadge: true,
|
|
||||||
targetEntity: { name: 'person', id: p.person.id },
|
|
||||||
buttonText: p.person|chill_entity_render_string
|
|
||||||
} %}
|
|
||||||
</span>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if accompanying_period.socialIssues.count > 0 %}
|
|
||||||
<div class="wl-row">
|
|
||||||
<div class="wl-col title"><h3>{{ 'Social issues'|trans }}</h3></div>
|
|
||||||
<div class="wl-col list">
|
|
||||||
{% for si in accompanying_period.socialIssues %}
|
|
||||||
<p class="wl-item">
|
|
||||||
{{ si|chill_entity_render_box }}
|
|
||||||
</p>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="item-row separator">
|
|
||||||
|
|
||||||
<ul class="record_actions">
|
|
||||||
{# TODO if enable_accompanying_course_with_multiple_persons is true ... #}
|
{# TODO if enable_accompanying_course_with_multiple_persons is true ... #}
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ path('chill_person_accompanying_course_index', { 'accompanying_period_id': accompanying_period.id }) }}"
|
<a href="{{ path('chill_person_accompanying_course_index', { 'accompanying_period_id': period.id }) }}"
|
||||||
class="btn btn-show" title="{{ 'See accompanying period'|trans }}">{# {{ 'See this period'|trans }} #}</a>
|
class="btn btn-show" title="{{ 'See accompanying period'|trans }}">{# {{ 'See this period'|trans }} #}</a>
|
||||||
</li>
|
</li>
|
||||||
{% if accompanying_period.step == 'DRAFT' %}
|
{% if period.step == 'DRAFT' and contextEntity.type == 'person' %}
|
||||||
|
{% set person = contextEntity.entity %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ path('chill_person_accompanying_course_delete', { 'accompanying_period_id': accompanying_period.id, 'person_id' : person.id }) }}"
|
<a href="{{ path('chill_person_accompanying_course_delete', { 'accompanying_period_id': period.id, 'person_id' : person.id }) }}"
|
||||||
class="btn btn-delete" title="{{ 'Delete accompanying period'|trans }}">{# {{ 'Delete this period'|trans }} #}</a>
|
class="btn btn-delete" title="{{ 'Delete accompanying period'|trans }}">{# {{ 'Delete this period'|trans }} #}</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- if new accompanying course, this is not necessary
|
{# DISABLED if new accompanying course, this is not necessary
|
||||||
{% if person is defined %}
|
{% if person is defined %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ path('chill_person_accompanying_period_update', {'person_id' : person.id, 'period_id' : accompanying_period.id } ) }}"
|
<a href="{{ path('chill_person_accompanying_period_update', {'person_id' : person.id, 'period_id' : period.id } ) }}"
|
||||||
class="btn btn-update" title="{{ 'Edit accompanying period'|trans }}"></a>
|
class="btn btn-update" title="{{ 'Edit accompanying period'|trans }}"></a>
|
||||||
</li>
|
</li>
|
||||||
{% if accompanying_period.isOpen == true %}
|
{% if period.isOpen == true %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ path('chill_person_accompanying_period_close', {'person_id' : person.id}) }}"
|
<a href="{{ path('chill_person_accompanying_period_close', {'person_id' : person.id}) }}"
|
||||||
class="btn btn-update change-icon">
|
class="btn btn-update change-icon">
|
||||||
@ -148,9 +32,9 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if accompanying_period.canBeReOpened(person) == true %}
|
{% if period.canBeReOpened(person) == true %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ path('chill_person_accompanying_period_re_open', {'person_id' : person.id, 'period_id' : accompanying_period.id } ) }}"
|
<a href="{{ path('chill_person_accompanying_period_re_open', {'person_id' : person.id, 'period_id' : period.id } ) }}"
|
||||||
class="btn btn-create change-icon">
|
class="btn btn-create change-icon">
|
||||||
<i class="fa fa-fw fa-unlock" aria-hidden="true"></i>
|
<i class="fa fa-fw fa-unlock" aria-hidden="true"></i>
|
||||||
{{'Re-open accompanying period'|trans }}
|
{{'Re-open accompanying period'|trans }}
|
||||||
@ -158,13 +42,19 @@
|
|||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% elseif household is defined %}
|
{% elseif household is defined %}
|
||||||
{# TODO buttons specific for household ? #}
|
TODO buttons specific for household ?
|
||||||
{% endif %}
|
{% endif %}
|
||||||
-->
|
#}
|
||||||
</ul>
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="flex-table accompanyingcourse-list">
|
||||||
|
{% for period in accompanying_periods %}
|
||||||
|
|
||||||
|
{% include 'ChillPersonBundle:AccompanyingPeriod:_list_item.html.twig' with {
|
||||||
|
'recordAction': _self.recordAction(period, contextEntity)
|
||||||
|
} %}
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
@ -0,0 +1,121 @@
|
|||||||
|
<div class="item-bloc accompanying-period-item{% if itemBlocClass is defined %} {{ itemBlocClass }}{% endif %}">
|
||||||
|
<div class="item-row">
|
||||||
|
<div class="wrap-header">
|
||||||
|
<div class="wh-row">
|
||||||
|
<div class="wh-col">
|
||||||
|
<span class="h3">
|
||||||
|
<i class="fa fa-fw fa-random"></i>
|
||||||
|
<b>{{ period.id }}</b>
|
||||||
|
</span>
|
||||||
|
{% if period.emergency %}
|
||||||
|
<span class="badge rounded-pill bg-danger">{{- 'Emergency'|trans|upper -}}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if period.confidential %}
|
||||||
|
<span class="badge rounded-pill bg-danger">{{- 'Confidential'|trans|upper -}}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="wh-col">
|
||||||
|
{% if period.step == 'DRAFT' %}
|
||||||
|
<span class="badge bg-secondary">{{- 'Draft'|trans|upper -}}</span>
|
||||||
|
{% elseif period.step == 'CONFIRMED' %}
|
||||||
|
<span class="badge bg-primary">{{- 'Confirmed'|trans|upper -}}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-primary">{{- 'Closed'|trans|upper -}}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="wh-row">
|
||||||
|
<div class="wh-col">
|
||||||
|
{% if period.closingDate == null %}
|
||||||
|
{{ 'accompanying_period.dates_from_%opening_date%'|trans({ '%opening_date%': period.openingDate|format_date('long') } ) }}
|
||||||
|
{% else %}
|
||||||
|
{{ 'accompanying_period.dates_from_%opening_date%_to_%closing_date%'|trans({
|
||||||
|
'%opening_date%': period.openingDate|format_date('long'),
|
||||||
|
'%closing_date%': period.closingDate|format_date('long')}
|
||||||
|
) }}
|
||||||
|
{% if period.isOpen == false %}
|
||||||
|
<dl class="chill_view_data">
|
||||||
|
<dt>{{ 'Closing motive'|trans }} :</dt>
|
||||||
|
<dd>{{ period.closingMotive|chill_entity_render_box }}</dd>
|
||||||
|
</dl>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="wh-col">
|
||||||
|
{% if chill_accompanying_periods.fields.user == 'visible' %}
|
||||||
|
{% if period.user %}
|
||||||
|
<abbr class="referrer" title="{{ 'Referrer'|trans }}">{{ 'Referrer'|trans }}:</abbr>
|
||||||
|
{{ period.user.username|chill_entity_render_box }}
|
||||||
|
{% else %}
|
||||||
|
<span class="chill-no-data-statement">{{ 'No accompanying user'|trans }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="item-row separator">
|
||||||
|
<div class="wrap-list">
|
||||||
|
{% if period.requestorPerson is not null or period.requestorThirdParty is not null %}
|
||||||
|
<div class="wl-row">
|
||||||
|
<div class="wl-col title"><h3>{{ 'Requestor'|trans({'gender': null }) }}</h3></div>
|
||||||
|
<div class="wl-col list">
|
||||||
|
{% if period.requestorPerson is not null %}
|
||||||
|
<span class="wl-item">
|
||||||
|
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
|
||||||
|
action: 'show', displayBadge: true,
|
||||||
|
targetEntity: { name: 'person', id: period.requestorPerson.id },
|
||||||
|
buttonText: period.requestorPerson|chill_entity_render_string
|
||||||
|
} %}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if period.requestorThirdParty is not null %}
|
||||||
|
<span class="wl-item">
|
||||||
|
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
|
||||||
|
action: 'show', displayBadge: true,
|
||||||
|
targetEntity: { name: 'thirdparty', id: period.requestorThirdParty.id },
|
||||||
|
buttonText: period.requestorThirdParty|chill_entity_render_string
|
||||||
|
} %}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if period.participations.count > 0 %}
|
||||||
|
<div class="wl-row">
|
||||||
|
<div class="wl-col title"><h3>{{ 'Participants'|trans }}</h3></div>
|
||||||
|
<div class="wl-col list">
|
||||||
|
{% for p in period.getCurrentParticipations %}
|
||||||
|
<span class="wl-item">
|
||||||
|
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
|
||||||
|
action: 'show', displayBadge: true,
|
||||||
|
targetEntity: { name: 'person', id: p.person.id },
|
||||||
|
buttonText: p.person|chill_entity_render_string
|
||||||
|
} %}
|
||||||
|
</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if period.socialIssues.count > 0 %}
|
||||||
|
<div class="wl-row">
|
||||||
|
<div class="wl-col title"><h3>{{ 'Social issues'|trans }}</h3></div>
|
||||||
|
<div class="wl-col list">
|
||||||
|
{% for si in period.socialIssues %}
|
||||||
|
<p class="wl-item">
|
||||||
|
{{ si|chill_entity_render_box }}
|
||||||
|
</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if recordAction is defined %}
|
||||||
|
<div class="item-row separator">
|
||||||
|
<ul class="record_actions">
|
||||||
|
{{ recordAction }}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
@ -1,3 +1,26 @@
|
|||||||
<a href="{{ path('chill_person_accompanying_course_index', {'accompanying_period_id': notification.relatedEntityId }) }}">
|
{% macro recordAction(period) %}
|
||||||
Go to Acc. period.
|
<li>
|
||||||
</a>
|
<a href="{{ path('chill_person_accompanying_course_index', { 'accompanying_period_id': period }) }}"
|
||||||
|
class="btn btn-show" title="{{ 'See accompanying period'|trans }}"></a>
|
||||||
|
</li>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% if period is not null %}
|
||||||
|
<div class="flex-table">
|
||||||
|
{% if is_granted('CHILL_PERSON_ACCOMPANYING_PERIOD_SEE', period) %}
|
||||||
|
{% include 'ChillPersonBundle:AccompanyingPeriod:_list_item.html.twig' with {
|
||||||
|
'recordAction': _self.recordAction(notification.relatedEntityId),
|
||||||
|
'itemBlocClass': 'bg-chill-llight-gray'
|
||||||
|
} %}
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-warning border-warning border-1">
|
||||||
|
{{ 'This is the minimal period details'|trans ~ ': ' ~ period.id }}<br>
|
||||||
|
{{ 'You are getting a notification for a period you are not allowed to see'|trans }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-warning border-warning border-1">
|
||||||
|
{{ 'You are getting a notification for a period which does not exists any more'|trans }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
{% block title 'household.Household summary'|trans %}
|
{% block title 'household.Household summary'|trans %}
|
||||||
|
|
||||||
{% block block_post_menu %}
|
{% block block_post_menu %}
|
||||||
<div class="block-post-menu"></div>
|
<div class="post-menu"></div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
{{ 'period_notification.You are designated to a new period'|trans }}
|
||||||
|
|
||||||
|
{{ 'period_notification.See it online'|trans }}:
|
||||||
|
|
||||||
|
{{ absolute_url(path('chill_person_accompanying_course_index', {'accompanying_period_id': accompanyingCourse.id}, false)) }}
|
||||||
|
|
||||||
|
{{ 'period_notification.Persons are'|trans }}:
|
||||||
|
|
||||||
|
{% for p in accompanyingCourse.getCurrentParticipations %}
|
||||||
|
* {{ p.person|chill_entity_render_string }}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{{ 'period_notification.Social issues are'|trans }}: {% for s in accompanyingCourse.socialIssues %}{{ s|chill_entity_render_string }}{% if not loop.last %}, {% endif %}{% endfor %}.
|
||||||
|
|
@ -38,7 +38,7 @@
|
|||||||
}) }}
|
}) }}
|
||||||
|
|
||||||
{% block block_post_menu %}
|
{% block block_post_menu %}
|
||||||
<div class="block-post-menu">
|
<div class="post-menu">
|
||||||
{{ chill_delegated_block('person_post_vertical_menu', { 'person': person } ) }}
|
{{ chill_delegated_block('person_post_vertical_menu', { 'person': person } ) }}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -98,7 +98,7 @@ class AccompanyingPeriodDocGenNormalizer implements ContextAwareNormalizerInterf
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param AccompanyingPeriod|null $period
|
* @param AccompanyingPeriod|null $period
|
||||||
* @param null|string $format
|
* @param string|null $format
|
||||||
*/
|
*/
|
||||||
public function normalize($period, $format = null, array $context = [])
|
public function normalize($period, $format = null, array $context = [])
|
||||||
{
|
{
|
||||||
|
@ -21,7 +21,7 @@ final class AccompanyingPeriodOriginNormalizer implements NormalizerInterface
|
|||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @param Origin $origin
|
* @param Origin $origin
|
||||||
* @param null|string $format
|
* @param string|null $format
|
||||||
*/
|
*/
|
||||||
public function normalize($origin, $format = null, array $context = [])
|
public function normalize($origin, $format = null, array $context = [])
|
||||||
{
|
{
|
||||||
|
@ -21,7 +21,7 @@ class AccompanyingPeriodParticipationNormalizer implements NormalizerAwareInterf
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param AccompanyingPeriodParticipation $participation
|
* @param AccompanyingPeriodParticipation $participation
|
||||||
* @param null|string $format
|
* @param string|null $format
|
||||||
*/
|
*/
|
||||||
public function normalize($participation, $format = null, array $context = [])
|
public function normalize($participation, $format = null, array $context = [])
|
||||||
{
|
{
|
||||||
|
@ -168,7 +168,7 @@ class PersonJsonNormalizer implements
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Person $person
|
* @param Person $person
|
||||||
* @param null|string $format
|
* @param string|null $format
|
||||||
*/
|
*/
|
||||||
public function normalize($person, $format = null, array $context = [])
|
public function normalize($person, $format = null, array $context = [])
|
||||||
{
|
{
|
||||||
|
@ -31,7 +31,7 @@ class RelationshipDocGenNormalizer implements ContextAwareNormalizerInterface, N
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Relationship $relation
|
* @param Relationship $relation
|
||||||
* @param null|string $format
|
* @param string|null $format
|
||||||
*/
|
*/
|
||||||
public function normalize($relation, $format = null, array $context = [])
|
public function normalize($relation, $format = null, array $context = [])
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
<?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 AccompanyingPeriod\Workflow;
|
||||||
|
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
final class WorkflowEventSubscriberTest extends KernelTestCase
|
||||||
|
{
|
||||||
|
}
|
@ -20,3 +20,9 @@ services:
|
|||||||
|
|
||||||
Chill\PersonBundle\AccompanyingPeriod\Suggestion\ReferralsSuggestionInterface: '@Chill\PersonBundle\AccompanyingPeriod\Suggestion\ReferralsSuggestion'
|
Chill\PersonBundle\AccompanyingPeriod\Suggestion\ReferralsSuggestionInterface: '@Chill\PersonBundle\AccompanyingPeriod\Suggestion\ReferralsSuggestion'
|
||||||
|
|
||||||
|
Chill\PersonBundle\AccompanyingPeriod\Workflow\:
|
||||||
|
resource: './../../AccompanyingPeriod/Workflow'
|
||||||
|
autowire: true
|
||||||
|
autoconfigure: true
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
services:
|
services:
|
||||||
Chill\PersonBundle\Notification\AccompanyingPeriodNotificationRenderer:
|
Chill\PersonBundle\Notification\AccompanyingPeriodNotificationHandler:
|
||||||
autowire: true
|
autowire: true
|
||||||
|
autoconfigure: true
|
||||||
|
@ -487,3 +487,14 @@ docgen:
|
|||||||
A basic context for accompanying period: Contexte pour les parcours
|
A basic context for accompanying period: Contexte pour les parcours
|
||||||
A context for accompanying period work: Contexte pour les actions d'accompagnement
|
A context for accompanying period work: Contexte pour les actions d'accompagnement
|
||||||
A context for accompanying period work evaluation: Contexte pour les évaluations dans les actions d'accompagnement
|
A context for accompanying period work evaluation: Contexte pour les évaluations dans les actions d'accompagnement
|
||||||
|
|
||||||
|
period_notification:
|
||||||
|
period_designated_subject: Vous êtes référent d'un parcours d'accompagnement
|
||||||
|
You are designated to a new period: Vous avez été désigné référent d'un parcours d'accompagnement.
|
||||||
|
Persons are: Les usagers concernés sont les suivants
|
||||||
|
Social issues are: Les problématiques sociales renseignées sont les suivantes
|
||||||
|
See it online: Visualisez le parcours en ligne
|
||||||
|
|
||||||
|
You are getting a notification for a period which does not exists any more: Cette notification ne correspond pas à une période d'accompagnement valide.
|
||||||
|
You are getting a notification for a period you are not allowed to see: La notification fait référence à une période d'accompagnement à laquelle vous n'avez pas accès.
|
||||||
|
This is the minimal period details: Période d'accompagnement n°
|
||||||
|
@ -6,8 +6,7 @@ services:
|
|||||||
- { name: 'twig.extension' }
|
- { name: 'twig.extension' }
|
||||||
|
|
||||||
Chill\TaskBundle\Templating\UI\CountNotificationTask:
|
Chill\TaskBundle\Templating\UI\CountNotificationTask:
|
||||||
|
autoconfigure: true
|
||||||
arguments:
|
arguments:
|
||||||
$singleTaskRepository: '@Chill\TaskBundle\Repository\SingleTaskRepository'
|
$singleTaskRepository: '@Chill\TaskBundle\Repository\SingleTaskRepository'
|
||||||
$cachePool: '@cache.user_data'
|
$cachePool: '@cache.user_data'
|
||||||
tags:
|
|
||||||
- { name: chill.count_notification.user }
|
|
||||||
|
@ -30,7 +30,7 @@ class ThirdPartyNormalizer implements NormalizerAwareInterface, NormalizerInterf
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param ThirdParty $thirdParty
|
* @param ThirdParty $thirdParty
|
||||||
* @param null|string $format
|
* @param string|null $format
|
||||||
*/
|
*/
|
||||||
public function normalize($thirdParty, $format = null, array $context = [])
|
public function normalize($thirdParty, $format = null, array $context = [])
|
||||||
{
|
{
|
||||||
|
@ -1 +1 @@
|
|||||||
Subproject commit bd95d3c96a437757b7e8f35cdfd30da9aeac1a01
|
Subproject commit 7a58754daf4a82f3b5419b2fb17a2ea42f7f9e7a
|
Loading…
x
Reference in New Issue
Block a user