Compare commits

..

11 Commits

204 changed files with 1489 additions and 5972 deletions

View File

@@ -3,3 +3,4 @@
# Run tests from root to adapt your own environment
KERNEL_CLASS='App\Kernel'
APP_SECRET='$ecretf0rt3st'
DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres?serverVersion=12&charset=utf8

View File

@@ -20,8 +20,6 @@ variables:
# Configure postgres environment variables (https://hub.docker.com/r/_/postgres/)
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
# configure database access
DATABASE_URL: postgresql://postgres:postgres@db:5432/postgres?serverVersion=12&charset=utf8
# fetch the chill-app using git submodules
GIT_SUBMODULE_STRATEGY: recursive
REDIS_HOST: redis

View File

@@ -11,19 +11,6 @@ and this project adheres to
## Unreleased
<!-- write down unreleased development here -->
* vuejs: add validation on required fields for AddPerson, Address and Location components
* vuejs: treat 422 validation errors in locations and AddPerson components
## Test releases
### test release 2022-01-12
* fix thirdparty normalizer on telephone field: https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/322
### test release 2022-01-11
* vuejs: translate in French all multiselect widgets
* [address] define address lines according postal standards for France and Belgium (default) and change AddressRender, chill_entity_render_box and AddressRenderBox.vue
* [household] change translations (champs-libres/departement-de-la-vendee/accent-suivi-developpement#109)
* [household] add address i18n in household component (champs-libres/departement-de-la-vendee/accent-suivi-developpement#158)
* [household] add on the fly i18n in household component
@@ -33,9 +20,6 @@ and this project adheres to
* [household] household member editor: remove markNoAddress button (champs-libres/departement-de-la-vendee/accent-suivi-developpement#109)
* [person]: ordering fields in add person (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/61)
* [person]: Add email and alt names in add person (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/61)
* [accompanyingCourse] Add a delete action and delete buttons to delete a accompanying course when step = DRAFT (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/64)
* [accompanyingCourse] Add a administrative location in the accompanying course, set the user current location as default, allow to select a location in a select field and do not allow to confirm the accompanying course if location is empty.
* [accompanyingCourse] Add the administrative location in the available variables for document generation
* AddAddress: optimize loading: wait for the user finish typing;
* UserPicker: fix bug with deprecated role
* docgen: add base context + tests
@@ -51,10 +35,8 @@ and this project adheres to
* address reference: add index for refid
* [accompanyingCourse_work] fix styles conflicts + fix bug with remove goal (remove goals one at a time)
* [accompanyingCourse] improve masonry on resume page, add origin
* [notification] new notification interface, can be associated to AccompanyingCourse/Period, Activities.
* List notifications, show, and comment in User section
* Notify button and contextual notification box on associated objects pages
* [accompanyingCourse] add a comment for each resource associated. A modal allow to save comment. Comment is displayed in on-the-fly show modal of the accompanyingCourse context (edit page + resume page).
## Test releases
### test release 2021-12-14

View File

@@ -33,8 +33,7 @@
"symfony/form": "^4.4",
"symfony/framework-bundle": "^4.4",
"symfony/intl": "^4.4",
"symfony/mailer": "^5.4",
"symfony/mime": "^5.4",
"symfony/mime": "^4.4",
"symfony/monolog-bundle": "^3.5",
"symfony/security-bundle": "^4.4",
"symfony/serializer": "^5.3",
@@ -48,19 +47,13 @@
"symfony/yaml": "^4.4",
"twig/extra-bundle": "^3.0",
"twig/intl-extra": "^3.0",
"twig/markdown-extra": "^3.3",
"twig/string-extra": "^3.3",
"twig/twig": "^3.0"
},
"conflict": {
"symfony/symfony": "*"
"twig/markdown-extra": "^3.3"
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.3",
"drupol/php-conventions": "^5",
"fakerphp/faker": "^1.13",
"nelmio/alice": "^3.8",
"phpspec/prophecy-phpunit": "^2.0",
"phpstan/phpstan-strict-rules": "^1.0",
"phpunit/phpunit": ">= 7.5",
"symfony/debug-bundle": "^5.1",
@@ -71,17 +64,8 @@
"symfony/var-dumper": "^4.4",
"symfony/web-profiler-bundle": "^4.4"
},
"config": {
"bin-dir": "bin",
"optimize-autoloader": true,
"sort-packages": true,
"vendor-dir": "tests/app/vendor",
"allow-plugins": {
"composer/package-versions-deprecated": true,
"phpstan/extension-installer": true,
"ergebnis/composer-normalize": true,
"phpro/grumphp": true
}
"conflict": {
"symfony/symfony": "*"
},
"autoload": {
"psr-4": {

View File

@@ -315,6 +315,11 @@ parameters:
count: 1
path: src/Bundle/ChillMainBundle/Security/PasswordRecover/TokenManager.php
-
message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#"
count: 3
path: src/Bundle/ChillMainBundle/Templating/Entity/AddressRender.php
-
message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#"
count: 1

View File

@@ -6,6 +6,7 @@
backupGlobals="false"
colors="true"
bootstrap="tests/app/tests/bootstrap.php"
stopOnFailure="true"
>
<php>
<ini name="error_reporting" value="-1" />

View File

@@ -1,45 +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\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;
}
}

View File

@@ -0,0 +1,33 @@
<?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;
}
}

View File

@@ -11,7 +11,7 @@ import Location from './components/Location.vue';
export default {
name: "App",
props: ['hasSocialIssues', 'hasLocation', 'hasPerson'],
props: ['hasSocialIssues', 'hasLocation', 'hasPerson'],
components: {
ConcernedGroups,
SocialIssuesAcc,

View File

@@ -12,7 +12,7 @@
</div>
<div v-if="getContext === 'accompanyingCourse' && suggestedEntities.length > 0">
<ul class="list-suggest add-items inline">
<li v-for="(p, i) in suggestedEntities" @click="addSuggestedEntity(p)" :key="`suggestedEntities-${i}`">
<li v-for="p in suggestedEntities" @click="addSuggestedEntity(p)">
<span>{{ p.text }}</span>
</li>
</ul>

View File

@@ -15,16 +15,14 @@
:searchable="true"
:placeholder="$t('activity.choose_location')"
:custom-label="customLabel"
:select-label="$t('multiselect.select_label')"
:deselect-label="$t('multiselect.deselect_label')"
:selected-label="$t('multiselect.selected_label')"
:options="availableLocations"
group-values="locations"
group-label="locationGroup"
v-model="location"
>
</VueMultiselect>
<new-location v-bind:availableLocations="availableLocations"></new-location>
<new-location v-bind:locations="locations"></new-location>
</div>
</div>
</teleport>
@@ -34,6 +32,7 @@
import { mapState, mapGetters } from "vuex";
import VueMultiselect from "vue-multiselect";
import NewLocation from "./Location/NewLocation.vue";
import { getLocations, getLocationTypeByDefaultFor, getUserCurrentLocation } from "../api.js";
export default {
name: "Location",

View File

@@ -18,6 +18,15 @@
</template>
<template v-slot:body>
<form>
<div class="form-floating mb-3">
<p v-if="errors.length">
<b>{{ $t('activity.errors') }}</b>
<ul>
<li v-for="error in errors" :key="error">{{ error }}</li>
</ul>
</p>
</div>
<div class="form-floating mb-3">
<select class="form-select form-select-lg" id="type" required v-model="selectType">
<option selected disabled value="">{{ $t('activity.choose_location_type') }}</option>
@@ -53,12 +62,6 @@
<input class="form-control form-control-lg" id="email" v-model="inputEmail" placeholder />
<label for="email">{{ $t('activity.location_fields.email') }}</label>
</div>
<div class="alert alert-warning" v-if="errors.length">
<ul>
<li v-for="(e, i) in errors" :key="i">{{ e }}</li>
</ul>
</div>
</form>
</template>
<template v-slot:footer>
@@ -78,8 +81,7 @@
import Modal from 'ChillMainAssets/vuejs/_components/Modal.vue';
import AddAddress from "ChillMainAssets/vuejs/Address/components/AddAddress.vue";
import { mapState } from "vuex";
import { getLocationTypes } from "../../api";
import { makeFetch } from 'ChillMainAssets/lib/api/apiMethods';
import { getLocationTypes, postLocation } from "../../api";
export default {
name: "NewLocation",
@@ -87,7 +89,7 @@ export default {
Modal,
AddAddress,
},
props: ['availableLocations'],
props: ['locations'],
data() {
return {
errors: [],
@@ -221,6 +223,7 @@ export default {
},
saveNewLocation() {
if (this.checkForm()) {
console.log('saveNewLocation', this.selected);
let body = {
type: 'location',
name: this.selected.name,
@@ -239,28 +242,23 @@ export default {
}
});
}
makeFetch('POST', '/api/1.0/main/location.json', body)
.then(response => {
this.$store.dispatch('addAvailableLocationGroup', {
locationGroup: 'Localisations nouvellement créées',
locations: [response]
});
this.$store.dispatch('updateLocation', response);
this.modal.showModal = false;
})
.catch((error) => {
if (error.name === 'ValidationException') {
for (let v of error.violations) {
this.errors.push(v);
}
} else {
this.errors.push('An error occurred');
postLocation(body)
.then(
location => new Promise(resolve => {
this.locations.push(location);
this.$store.dispatch('updateLocation', location);
resolve();
this.modal.showModal = false;
})
).catch(
err => {
this.errors.push(err.message);
}
})
);
};
},
submitNewAddress(payload) {
console.log('submitNewAddress', payload);
this.selected.addressId = payload.addressId;
this.addAddress.context.addressId = payload.addressId;
this.addAddress.context.edit = true;

View File

@@ -9,9 +9,9 @@
<check-social-issue
v-for="issue in socialIssuesList"
:key="issue.id"
:issue="issue"
:selection="socialIssuesSelected"
v-bind:key="issue.id"
v-bind:issue="issue"
v-bind:selection="socialIssuesSelected"
@updateSelected="updateIssuesSelected">
</check-social-issue>
@@ -21,18 +21,18 @@
label="text"
track-by="id"
open-direction="bottom"
:close-on-select="true"
:preserve-search="false"
:reset-after="true"
:hide-selected="true"
:taggable="false"
:multiple="false"
:searchable="true"
:allow-empty="true"
:show-labels="false"
:loading="issueIsLoading"
:placeholder="$t('activity.choose_other_social_issue')"
:options="socialIssuesOther"
v-bind:close-on-select="true"
v-bind:preserve-search="false"
v-bind:reset-after="true"
v-bind:hide-selected="true"
v-bind:taggable="false"
v-bind:multiple="false"
v-bind:searchable="true"
v-bind:allow-empty="true"
v-bind:show-labels="false"
v-bind:loading="issueIsLoading"
v-bind:placeholder="$t('activity.choose_other_social_issue')"
v-bind:options="socialIssuesOther"
@select="addIssueInList">
</VueMultiselect>
</div>
@@ -58,9 +58,9 @@
<check-social-action
v-if="socialIssuesSelected.length || socialActionsSelected.length"
v-for="action in socialActionsList"
:key="action.id"
:action="action"
:selection="socialActionsSelected"
v-bind:key="action.id"
v-bind:action="action"
v-bind:selection="socialActionsSelected"
@updateSelected="updateActionsSelected">
</check-social-action>
</template>

View File

@@ -1,5 +1,4 @@
import { personMessages } from 'ChillPersonAssets/vuejs/_js/i18n'
import { multiSelectMessages } from 'ChillMainAssets/vuejs/_js/i18n'
const activityMessages = {
fr: {
@@ -34,11 +33,12 @@ const activityMessages = {
},
create_address: 'Créer une adresse',
edit_address: "Modifier l'adresse"
}
}
}
Object.assign(activityMessages.fr, personMessages.fr, multiSelectMessages.fr);
Object.assign(activityMessages.fr, personMessages.fr);
export {
activityMessages

View File

@@ -12,11 +12,7 @@ const hasLocation = document.querySelector('#location') !== null;
const hasPerson = document.querySelector('#add-persons') !== null;
const app = createApp({
template: `<app
:hasSocialIssues="hasSocialIssues"
:hasLocation="hasLocation"
:hasPerson="hasPerson"
></app>`,
template: `<app :hasSocialIssues="hasSocialIssues", :hasLocation="hasLocation", :hasPerson="hasPerson"></app>`,
data() {
return {
hasSocialIssues,

View File

@@ -240,9 +240,6 @@ const store = createStore({
});
commit("updateActionsSelected", payload);
},
addAvailableLocationGroup({ commit }, payload) {
commit("addAvailableLocationGroup", payload);
},
addPersonsInvolved({ commit }, payload) {
//console.log('### action addPersonsInvolved', payload.result.type);
switch (payload.result.type) {

View File

@@ -1,151 +0,0 @@
{% 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">
{{ activity.location.name }}
<span>({{ activity.location.locationType.title|localize_translatable_string }})</span>
</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|chill_entity_render_string|capitalize }}
</p>
</div>
</div>
{% endif %}
</div>
{% include 'ChillActivityBundle:Activity:concernedGroups.html.twig' with {
'context': context,
'render': '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>

View File

@@ -3,12 +3,11 @@
{{ path(pathname, parms) }}
{% endmacro %}
{% macro insert_onthefly(type, entity, parent = null) %}
{% macro insert_onthefly(type, entity) %}
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
action: 'show', displayBadge: true,
targetEntity: { name: type, id: entity.id },
buttonText: entity|chill_entity_render_string,
parent: parent
buttonText: entity|chill_entity_render_string
} %}
{% endmacro %}
@@ -60,7 +59,7 @@
}]) %}
{% endif %}
{% if (render == 'bloc') %}
{% if (with_display == 'bloc') %}
<div class="{{ context }} flex-bloc concerned-groups">
{% for bloc in blocks %}
@@ -91,7 +90,7 @@
</div>
{% endif %}
{% if (render == 'row') %}
{% if (with_display == 'row') %}
<div class="concerned-groups">
{% for bloc in blocks %}
<div class="group">
@@ -116,7 +115,7 @@
</div>
{% endif %}
{% if (render == 'wrap-list') %}
{% if (with_display == 'wrap-list') %}
<div class="concerned-groups wrap-list">
{% for bloc in blocks %}
<div class="wl-row">

View File

@@ -1,61 +1,3 @@
{% macro recordAction(activity, context = null, person_id = null, accompanying_course_id = null) %}
{% if no_action is not defined or no_action == false %}
<li>
<a class="btn btn-notify" href="{{ chill_path_add_return_path('chill_main_notification_create', {
'entityClass': 'Chill\\ActivityBundle\\Entity\\Activity',
'entityId': activity.id
}) }}">{{ 'notification.Notify'|trans }}</a>
</li>
{% endif %}
{% if context == 'person' and activity.accompanyingPeriod is not empty %}
{#
Disable person_id in following links, for redirect to accompanyingCourse context
#}
{% set person_id = null %}
{% set accompanying_course_id = activity.accompanyingPeriod.id %}
<li>
<a href="{{ chill_path_add_return_path('chill_activity_activity_list',{
'accompanying_period_id': accompanying_course_id
}) }}"
class="btn btn-primary"
title="{{ 'See activity in accompanying course context'|trans }}">
<i class="fa fa-random fa-fw"></i>
{{ 'Period number %number%'|trans({'%number%': accompanying_course_id}) }}
</a>
</li>
{% endif %}
<li>
<a href="{{ path('chill_activity_activity_show', {'id': activity.id,
'person_id': person_id,
'accompanying_period_id': accompanying_course_id
}) }}"
class="btn btn-show"
title="{{ 'Show'|trans }}"></a>
</li>
{% if no_action is not defined or no_action == false %}
{% if is_granted('CHILL_ACTIVITY_UPDATE', activity) %}
<li>
<a href="{{ path('chill_activity_activity_edit', {'id': activity.id,
'person_id': person_id,
'accompanying_period_id': accompanying_course_id
}) }}"
class="btn btn-update"
title="{{ 'Edit'|trans }}"></a>
</li>
{% endif %}
{% if is_granted('CHILL_ACTIVITY_DELETE', activity) %}
<li>
<a href="{{ path('chill_activity_activity_delete', {'id': activity.id,
'person_id': person_id,
'accompanying_period_id': accompanying_course_id
}) }}"
class="btn btn-delete"
title="{{ 'Delete'|trans }}"></a>
</li>
{% endif %}
{% endif %}
{% endmacro %}
<div class="context-{{ context }}">
{% if activities|length == 0 %}
@@ -66,10 +8,203 @@
{% 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)
} %}
{% 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 %}
</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 %}
{#
Disable person_id in following links, for redirect to accompanyingCourse context
#}
{% set person_id = null %}
{% set accompanying_course_id = activity.accompanyingPeriod.id %}
<li>
<a href="{{ chill_path_add_return_path('chill_activity_activity_list',{
'accompanying_period_id': accompanying_course_id
}) }}"
class="btn btn-primary"
title="{{ 'See activity in accompanying course context'|trans }}">
<i class="fa fa-random fa-fw"></i>
{{ 'Period number %number%'|trans({'%number%': accompanying_course_id}) }}
</a>
</li>
{% endif %}
<li>
<a href="{{ path('chill_activity_activity_show', {'id': activity.id,
'person_id': person_id,
'accompanying_period_id': accompanying_course_id
}) }}"
class="btn btn-show"
title="{{ 'Show'|trans }}"></a>
</li>
{% if no_action is not defined or no_action == false %}
{% if is_granted('CHILL_ACTIVITY_UPDATE', activity) %}
<li>
<a href="{{ path('chill_activity_activity_edit', {'id': activity.id,
'person_id': person_id,
'accompanying_period_id': accompanying_course_id
}) }}"
class="btn btn-update"
title="{{ 'Edit'|trans }}"></a>
</li>
{% endif %}
{% if is_granted('CHILL_ACTIVITY_DELETE', activity) %}
<li>
<a href="{{ path('chill_activity_activity_delete', {'id': activity.id,
'person_id': person_id,
'accompanying_period_id': accompanying_course_id
}) }}"
class="btn btn-delete"
title="{{ 'Delete'|trans }}"></a>
</li>
{% endif %}
{% endif %}
</ul>
</div>
</div>
{% endfor %}
</div>
{% endif %}

View File

@@ -4,17 +4,6 @@
{% block title %}{{ 'Activity list' |trans }}{% endblock title %}
{% block js %}
{{ parent() }}
{{ encore_entry_script_tags('mod_notification_toggle_read_status') }}
{% endblock %}
{% block css %}
{{ parent() }}
{{ encore_entry_link_tags('mod_notification_toggle_read_status') }}
{% endblock %}
{% block content %}
{% set person_id = null %}

View File

@@ -20,16 +20,6 @@
{% block title %}{{ 'Activity list' |trans }}{% endblock title %}
{% block js %}
{{ parent() }}
{{ encore_entry_script_tags('mod_notification_toggle_read_status') }}
{% endblock %}
{% block css %}
{{ parent() }}
{{ encore_entry_link_tags('mod_notification_toggle_read_status') }}
{% endblock %}
{% block personcontent %}
{% set person_id = null %}

View File

@@ -34,7 +34,7 @@
<div class="item-row separator">
<dl class="chill_view_data">
<dt class="inline">{{ 'Referrer'|trans|capitalize }}</dt>
<dd>{{ entity.user|chill_entity_render_box }}</dd>
<dd>{{ entity.user }}</dd>
{%- if entity.scope -%}
<dt class="inline">{{ 'Scope'|trans }}</dt>
@@ -85,7 +85,7 @@
{% include 'ChillActivityBundle:Activity:concernedGroups.html.twig' with {
'context': context,
'render': 'bloc',
'with_display': 'bloc',
'badge_person': 'true'
} %}
@@ -103,8 +103,8 @@
<dd>
{% if entity.location is not null %}
<p>
<span>{{ entity.location.locationType.title|localize_translatable_string }}</span>
{{ entity.location.name }}
<span>({{ entity.location.locationType.title|localize_translatable_string }})</span>
</p>
<div class="ms-3">{{ entity.location.address|chill_entity_render_box }}</div>
{% else %}
@@ -198,8 +198,8 @@
</a>
</li>
{% if is_granted('CHILL_ACTIVITY_UPDATE', entity) %}
<li>
<a class="btn btn-update" href="{{ path('chill_activity_activity_edit', { 'id': entity.id, 'person_id': person_id, 'accompanying_period_id': accompanying_course_id }) }}">
<li>
<a class="btn btn-update" href="{{ path('chill_activity_activity_edit', { 'id': entity.id, 'person_id': person_id, 'accompanying_period_id': accompanying_course_id }) }}">
{{ 'Edit'|trans }}
</a>
</li>
@@ -212,3 +212,9 @@
</li>
{% endif %}
</ul>
<script>
import ShowPane from "../../../../ChillMainBundle/Resources/public/vuejs/Address/components/ShowPane";
export default {
components: {ShowPane}
}
</script>

View File

@@ -4,16 +4,6 @@
{% block title 'Show the activity'|trans %}
{% block js %}
{{ parent() }}
{{ encore_entry_script_tags('mod_notification_toggle_read_status') }}
{% endblock %}
{% block css %}
{{ parent() }}
{{ encore_entry_link_tags('mod_notification_toggle_read_status') }}
{% endblock %}
{% import 'ChillActivityBundle:ActivityReason:macro.html.twig' as m %}
{% block content -%}
@@ -21,21 +11,3 @@
{% include 'ChillActivityBundle:Activity:show.html.twig' with {'context': 'accompanyingCourse'} %}
</div>
{% 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 %}

View File

@@ -1,27 +1,2 @@
{% 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 %}
{% 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 %}
<a href="{{ path('chill_activity_activity_show', {'id': notification.relatedEntityId }) }}">Go to Activity</a>

View File

@@ -4,16 +4,6 @@
{% block title 'Show the activity'|trans %}
{% block js %}
{{ parent() }}
{{ encore_entry_script_tags('mod_notification_toggle_read_status') }}
{% endblock %}
{% block css %}
{{ parent() }}
{{ encore_entry_link_tags('mod_notification_toggle_read_status') }}
{% endblock %}
{% import 'ChillActivityBundle:ActivityReason:macro.html.twig' as m %}
{% block personcontent -%}
@@ -21,21 +11,3 @@
{% include 'ChillActivityBundle:Activity:show.html.twig' with {'context': 'person'} %}
</div>
{% 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 %}

View File

@@ -224,7 +224,3 @@ Aggregate by activity reason: Aggréger par sujet de l'activité
Last activities: Les dernières activités
See activity in accompanying course context: Voir l'activité dans le contexte du parcours d'accompagnement
You get notified of an activity which does not exists any more: Cette notification ne correspond pas à une activité valide.
you are not allowed to see it details: La notification fait référence à une activité à laquelle vous n'avez pas accès.
This is the minimal activity data: Activité n°

View File

@@ -13,9 +13,6 @@
:close-on-select="false"
:allow-empty="true"
:model-value="value"
:select-label="$t('multiselect.select_label')"
:deselect-label="$t('multiselect.deselect_label')"
:selected-label="$t('multiselect.selected_label')"
@select="selectUsers"
@remove="unSelectUsers"
@close="coloriseSelectedValues"

View File

@@ -1,17 +1,13 @@
import { multiSelectMessages } from 'ChillMainAssets/vuejs/_js/i18n'
const calendarUserSelectorMessages = {
fr: {
choose_your_calendar_user: "Afficher les plages de disponibilités",
select_user: "Sélectionnez des calendriers",
show_my_calendar: "Afficher mon calendrier",
show_weekends: "Afficher les week-ends"
}
};
fr: {
choose_your_calendar_user: "Afficher les plages de disponibilités",
select_user: "Sélectionnez des calendriers",
show_my_calendar: "Afficher mon calendrier",
show_weekends: "Afficher les week-ends"
}
};
Object.assign(calendarUserSelectorMessages.fr, multiSelectMessages.fr);
export {
calendarUserSelectorMessages
};
export {
calendarUserSelectorMessages
};

View File

@@ -94,7 +94,7 @@
<div class="item-col">
{% include 'ChillActivityBundle:Activity:concernedGroups.html.twig' with {
'context': accompanyingCourse,
'render': 'row',
'with_display': 'row',
'entity': calendar
} %}
</div>

View File

@@ -6,7 +6,7 @@
</dl>
<h2 class="chill-red">{{ 'Concerned groups'|trans }}</h2>
{% include 'ChillActivityBundle:Activity:concernedGroups.html.twig' with {'context': context, 'render': 'bloc' } %}
{% include 'ChillActivityBundle:Activity:concernedGroups.html.twig' with {'context': context, 'with_display': 'bloc' } %}
<h2 class="chill-red">{{ 'Calendar data'|trans }}</h2>
@@ -108,13 +108,13 @@
{# TODO
{% if is_granted('CHILL_ACTIVITY_DELETE', entity) %}
#}
<li>
<a href="{{ path('chill_calendar_calendar_delete', { 'id': entity.id, 'accompanying_period_id': accompanying_course_id, 'user_id': user_id } ) }}" class="btn btn-delete">
{{ 'Delete'|trans }}
</a>
</li>
{#
{% endif %}
#}

View File

@@ -276,6 +276,7 @@ final class DocGeneratorTemplateController extends AbstractController
fwrite($templateResource, $dataDecrypted);
rewind($templateResource);
}
$datas = $context->getData($template, $entity, $contextGenerationData);
try {

View File

@@ -46,6 +46,7 @@ class RelatorioDriver implements DriverInterface
'template' => new DataPart($template, $templateName ?? uniqid('template_'), $resourceType),
];
$form = new FormDataPart($formFields);
dump(json_encode($data));
try {
$response = $this->relatorioClient->request('POST', $this->url, [

View File

@@ -23,7 +23,11 @@
<input type="hidden" name="entityClassName" value="{{ contextManager.getContextByKey(entity.context).entityClass|e('html_attr') }}" />
<input type="text" name="entityId" />
<button type="submit" class="btn btn-mini btn-misc"><i class="fa fa-cog"></i>{{ 'docgen.test generate'|trans }}</button>
<ul class="record_actions">
<li>
<button type="submit" class="btn btn-mini btn-neutral">{{ 'docgen.test generate'|trans }}</button>
</li>
</ul>
</form>
</td>
<td>

View File

@@ -11,11 +11,9 @@ declare(strict_types=1);
namespace Chill\DocGeneratorBundle\Serializer\Helper;
use Symfony\Component\Serializer\Mapping\ClassMetadata;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use function array_merge;
use function is_array;
class NormalizeNullValueHelper
{
@@ -32,7 +30,7 @@ class NormalizeNullValueHelper
$this->discriminatorValue = $discriminatorValue;
}
public function normalize(array $attributes, string $format = 'docgen', ?array $context = [], ?ClassMetadata $classMetadata = null)
public function normalize(array $attributes, string $format = 'docgen', ?array $context = [])
{
$data = [];
$data['isNull'] = true;
@@ -60,7 +58,7 @@ class NormalizeNullValueHelper
default:
$data[$key] = $this->normalizer->normalize(null, $format, array_merge(
$this->getContextForAttribute($key, $context, $classMetadata),
$context,
['docgen:expects' => $class]
));
@@ -71,25 +69,4 @@ class NormalizeNullValueHelper
return $data;
}
private function getContextForAttribute(string $key, array $initialContext, ?ClassMetadata $classMetadata): array
{
if (null === $classMetadata) {
return $initialContext;
}
$attributeMetadata = $classMetadata->getAttributesMetadata()[$key] ?? null;
if (null !== $attributeMetadata) {
/** @var \Symfony\Component\Serializer\Mapping\AttributeMetadata $attributeMetadata */
$initialContext = array_merge(
$initialContext,
$attributeMetadata->getNormalizationContextForGroups(
is_array($initialContext['groups']) ? $initialContext['groups'] : [$initialContext['groups']]
)
);
}
return $initialContext;
}
}

View File

@@ -26,7 +26,7 @@ class CollectionDocGenNormalizer implements ContextAwareNormalizerInterface, Nor
/**
* @param Collection $object
* @param string|null $format
* @param null|string $format
*
* @return array|ArrayObject|bool|float|int|string|void|null
*/

View File

@@ -66,7 +66,7 @@ class DocGenObjectNormalizer implements NormalizerAwareInterface, NormalizerInte
if (!$this->classMetadataFactory->hasMetadataFor($classMetadataKey)) {
throw new LogicException(sprintf(
'This object does not have metadata: %s. Add groups on this entity to allow to serialize with the format %s and groups %s',
is_object($object) ? get_class($object) : '(todo' /*$context['docgen:expects'],*/ ,
is_object($object) ? get_class($object) : '(todo' /*$context['docgen:expects'],*/,
$format,
implode(', ', ($context['groups'] ?? []))
));
@@ -196,7 +196,7 @@ class DocGenObjectNormalizer implements NormalizerAwareInterface, NormalizerInte
$normalizer = new NormalizeNullValueHelper($this->normalizer, $typeKey, $typeValue);
return $normalizer->normalize($keys, $format, $context, $metadata);
return $normalizer->normalize($keys, $format, $context);
}
/**
@@ -260,13 +260,9 @@ class DocGenObjectNormalizer implements NormalizerAwareInterface, NormalizerInte
/** @var AttributeMetadata $attribute */
$value = $this->propertyAccess->getValue($object, $attribute->getName());
$key = $attribute->getSerializedName() ?? $attribute->getName();
$objectContext = array_merge(
$context,
$attribute->getNormalizationContextForGroups(
is_array($context['groups']) ? $context['groups'] : [$context['groups']]
)
);
$isTranslatable = $objectContext['is-translatable'] ?? false;
$isTranslatable = $attribute->getNormalizationContextForGroups(
is_array($context['groups']) ? $context['groups'] : [$context['groups']]
)['is-translatable'] ?? false;
if ($isTranslatable) {
$data[$key] = $this->translatableStringHelper
@@ -277,7 +273,7 @@ class DocGenObjectNormalizer implements NormalizerAwareInterface, NormalizerInte
foreach ($value as $k => $v) {
$arr[$k] =
$this->normalizer->normalize($v, $format, array_merge(
$objectContext,
$context,
$attribute->getNormalizationContextForGroups($expectedGroups)
));
}
@@ -285,11 +281,11 @@ class DocGenObjectNormalizer implements NormalizerAwareInterface, NormalizerInte
} elseif (is_object($value)) {
$data[$key] =
$this->normalizer->normalize($value, $format, array_merge(
$objectContext,
$context,
$attribute->getNormalizationContextForGroups($expectedGroups)
));
} elseif (null === $value) {
$data[$key] = $this->normalizeNullOutputValue($format, $objectContext, $attribute, $reflection);
$data[$key] = $this->normalizeNullOutputValue($format, $context, $attribute, $reflection);
} else {
$data[$key] = $value;
}

View File

@@ -14,7 +14,6 @@ namespace Chill\DocGeneratorBundle\tests\Serializer\Normalizer;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
@@ -34,49 +33,6 @@ final class DocGenObjectNormalizerTest extends KernelTestCase
$this->normalizer = self::$container->get(NormalizerInterface::class);
}
public function testChangeContextOnAttribute()
{
$object = new TestableParentClass();
$actual = $this->normalizer->normalize(
$object,
'docgen',
['groups' => 'docgen:read']
);
$this->assertIsArray($actual);
$this->assertArrayHasKey('child', $actual);
$this->assertIsArray($actual['child']);
$this->assertArrayHasKey('foo', $actual['child']);
$this->assertEquals('bar', $actual['child']['foo']);
$this->assertArrayNotHasKey('baz', $actual['child']);
// test with child = null
$object->child = null;
$actual = $this->normalizer->normalize(
$object,
'docgen',
['groups' => 'docgen:read']
);
$this->assertIsArray($actual);
$this->assertArrayHasKey('child', $actual);
$this->assertIsArray($actual['child']);
$this->assertArrayHasKey('foo', $actual['child']);
$this->assertEquals('', $actual['child']['foo']);
$this->assertArrayNotHasKey('baz', $actual['child']);
$actual = $this->normalizer->normalize(
null,
'docgen',
['groups' => 'docgen:read', 'docgen:expects' => TestableParentClass::class],
);
$this->assertIsArray($actual);
$this->assertArrayHasKey('child', $actual);
$this->assertIsArray($actual['child']);
$this->assertArrayHasKey('foo', $actual['child']);
$this->assertEquals('', $actual['child']['foo']);
$this->assertArrayNotHasKey('baz', $actual['child']);
}
public function testNormalizationBasic()
{
$scope = new Scope();
@@ -143,30 +99,3 @@ final class DocGenObjectNormalizerTest extends KernelTestCase
$this->assertEquals($expected, $normalized, 'test normalization fo an user with null center');
}
}
class TestableParentClass
{
/**
* @Serializer\Groups("docgen:read")
* @Serializer\Context(normalizationContext={"groups": "docgen:read:foo"}, groups={"docgen:read"})
*/
public ?TestableChildClass $child;
public function __construct()
{
$this->child = new TestableChildClass();
}
}
class TestableChildClass
{
/**
* @Serializer\Groups("docgen:read")
*/
public string $baz = 'bloup';
/**
* @Serializer\Groups("docgen:read:foo")
*/
public string $foo = 'bar';
}

View File

@@ -13,7 +13,6 @@ namespace Chill\DocGeneratorBundle\tests\Service\Context;
use Chill\DocGeneratorBundle\Service\Context\BaseContextData;
use Chill\MainBundle\Entity\User;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
@@ -24,8 +23,6 @@ use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
*/
final class BaseContextDataTest extends KernelTestCase
{
use ProphecyTrait;
protected function setUp(): void
{
parent::setUp();

View File

@@ -7,7 +7,6 @@ docgen:
Context: Contexte
New template: Nouveau gabarit
Edit template: Modifier gabarit
test generate: Tester la génération
With context: 'Avec le contexte :'

View File

@@ -22,7 +22,6 @@ use Chill\MainBundle\DependencyInjection\CompilerPass\TimelineCompilerClass;
use Chill\MainBundle\DependencyInjection\CompilerPass\WidgetsCompilerPass;
use Chill\MainBundle\DependencyInjection\ConfigConsistencyCompilerPass;
use Chill\MainBundle\DependencyInjection\RoleProvidersCompilerPass;
use Chill\MainBundle\Notification\NotificationHandlerInterface;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\MainBundle\Search\SearchApiInterface;
use Chill\MainBundle\Security\ProvideRoleInterface;
@@ -30,7 +29,6 @@ use Chill\MainBundle\Security\Resolver\CenterResolverInterface;
use Chill\MainBundle\Security\Resolver\ScopeResolverInterface;
use Chill\MainBundle\Templating\Entity\ChillEntityRenderInterface;
use Chill\MainBundle\Templating\Entity\CompilerPass as RenderEntityCompilerPass;
use Chill\MainBundle\Templating\UI\NotificationCounterInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
@@ -52,10 +50,6 @@ class ChillMainBundle extends Bundle
->addTag('chill.render_entity');
$container->registerForAutoconfiguration(SearchApiInterface::class)
->addTag('chill.search_api_provider');
$container->registerForAutoconfiguration(NotificationHandlerInterface::class)
->addTag('chill_main.notification_handler');
$container->registerForAutoconfiguration(NotificationCounterInterface::class)
->addTag('chill.count_notification.user');
$container->addCompilerPass(new SearchableServicesCompilerPass());
$container->addCompilerPass(new ConfigConsistencyCompilerPass());

View File

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

View File

@@ -11,292 +11,59 @@ declare(strict_types=1);
namespace Chill\MainBundle\Controller;
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\Notification\NotificationRenderer;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Repository\NotificationRepository;
use Chill\MainBundle\Security\Authorization\NotificationVoter;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @Route("/{_locale}/notification")
*/
class NotificationController extends AbstractController
{
private EntityManagerInterface $em;
private $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;
public function __construct(Security $security)
{
$this->security = $security;
$this->notificationRepository = $notificationRepository;
$this->notificationHandlerManager = $notificationHandlerManager;
$this->paginatorFactory = $paginatorFactory;
$this->translator = $translator;
}
/**
* @Route("/create", name="chill_main_notification_create")
* @Route("/show", name="chill_main_notification_show")
*/
public function createAction(Request $request): Response
{
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
if (!$this->security->getUser() instanceof User) {
throw new AccessDeniedHttpException('You must be authenticated and a user to create a notification');
}
if (!$request->query->has('entityClass')) {
throw new BadRequestHttpException('Missing entityClass parameter');
}
if (!$request->query->has('entityId')) {
throw new BadRequestHttpException('missing entityId parameter');
}
$notification = new Notification();
$notification
->setRelatedEntityClass($request->query->get('entityClass'))
->setRelatedEntityId($request->query->getInt('entityId'))
->setSender($this->security->getUser());
try {
$handler = $this->notificationHandlerManager->getHandler($notification);
} catch (NotificationHandlerNotFound $e) {
throw new BadRequestHttpException('no handler for this notification');
}
$form = $this->createForm(NotificationType::class, $notification);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->em->persist($notification);
$this->em->flush();
$this->addFlash('success', $this->translator->trans('notification.Notification created'));
if ($request->query->has('returnPath')) {
return new RedirectResponse($request->query->get('returnPath'));
}
return $this->redirectToRoute('chill_main_homepage');
}
return $this->render('@ChillMain/Notification/create.html.twig', [
'form' => $form->createView(),
'handler' => $handler,
'notification' => $notification,
]);
}
/**
* @Route("/{id}/edit", name="chill_main_notification_edit")
*/
public function editAction(Notification $notification, Request $request): Response
{
$this->denyAccessUnlessGranted(NotificationVoter::NOTIFICATION_UPDATE, $notification);
$form = $this->createForm(NotificationType::class, $notification);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->em->flush();
$this->addFlash('success', $this->translator->trans('notification.Notification updated'));
if ($request->query->has('returnPath')) {
return new RedirectResponse($request->query->get('returnPath'));
}
return $this->redirectToRoute('chill_main_notification_my');
}
return $this->render('@ChillMain/Notification/edit.html.twig', [
'form' => $form->createView(),
'handler' => $this->notificationHandlerManager->getHandler($notification),
'notification' => $notification,
]);
}
/**
* @Route("/inbox", name="chill_main_notification_my")
*/
public function inboxAction(): Response
{
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
public function showAction(
NotificationRepository $notificationRepository,
NotificationRenderer $notificationRenderer,
PaginatorFactory $paginatorFactory
) {
$currentUser = $this->security->getUser();
$notificationsNbr = $this->notificationRepository->countAllForAttendee(($currentUser));
$paginator = $this->paginatorFactory->create($notificationsNbr);
$notificationsNbr = $notificationRepository->countAllForAttendee(($currentUser));
$paginator = $paginatorFactory->create($notificationsNbr);
$notifications = $this->notificationRepository->findAllForAttendee(
$notifications = $notificationRepository->findAllForAttendee(
$currentUser,
$limit = $paginator->getItemsPerPage(),
$offset = $paginator->getCurrentPage()->getFirstItemNumber()
);
return $this->render('@ChillMain/Notification/list.html.twig', [
'datas' => $this->itemsForTemplate($notifications),
'notifications' => $notifications,
'paginator' => $paginator,
'step' => 'inbox',
'unreads' => $this->countUnread(),
]);
}
/**
* @Route("/sent", name="chill_main_notification_sent")
*/
public function sentAction(): Response
{
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
$currentUser = $this->security->getUser();
$notificationsNbr = $this->notificationRepository->countAllForSender($currentUser);
$paginator = $this->paginatorFactory->create($notificationsNbr);
$notifications = $this->notificationRepository->findAllForSender(
$currentUser,
$limit = $paginator->getItemsPerPage(),
$offset = $paginator->getCurrentPage()->getFirstItemNumber()
);
return $this->render('@ChillMain/Notification/list.html.twig', [
'datas' => $this->itemsForTemplate($notifications),
'notifications' => $notifications,
'paginator' => $paginator,
'step' => 'sent',
'unreads' => $this->countUnread(),
]);
}
/**
* @Route("/{id}/show", name="chill_main_notification_show")
*/
public function showAction(Notification $notification, Request $request): Response
{
$this->denyAccessUnlessGranted(NotificationVoter::NOTIFICATION_SEE, $notification);
if ($request->query->has('edit')) {
$commentId = $request->query->getInt('edit');
$editedComment = $notification->getComments()->filter(static function (NotificationComment $c) use ($commentId) {
return $c->getId() === $commentId;
})->first();
if (false === $editedComment) {
throw $this->createNotFoundException("Comment with id {$commentId} does not exists nor belong to this notification");
}
$this->denyAccessUnlessGranted(NotificationVoter::COMMENT_EDIT, $editedComment);
$editedCommentForm = $this->createForm(NotificationCommentType::class, $editedComment);
if (Request::METHOD_POST === $request->getMethod() && 'edit' === $request->request->get('form')) {
$editedCommentForm->handleRequest($request);
if ($editedCommentForm->isSubmitted() && $editedCommentForm->isValid()) {
$this->em->flush();
$this->addFlash('success', $this->translator->trans('notification.comment_updated'));
return $this->redirectToRoute('chill_main_notification_show', [
'id' => $notification->getId(),
'_fragment' => 'comment-' . $commentId,
]);
}
}
}
if ($this->isGranted(NotificationVoter::COMMENT_ADD, $notification)) {
$appendComment = new NotificationComment();
$appendCommentForm = $this->createForm(NotificationCommentType::class, $appendComment);
if (Request::METHOD_POST === $request->getMethod() && 'append' === $request->request->get('form')) {
$appendCommentForm->handleRequest($request);
if ($appendCommentForm->isSubmitted() && $appendCommentForm->isValid()) {
$notification->addComment($appendComment);
$this->em->persist($appendComment);
$this->em->flush();
$this->addFlash('success', $this->translator->trans('notification.comment_appended'));
return $this->redirectToRoute('chill_main_notification_show', [
'id' => $notification->getId(),
]);
}
}
}
$response = $this->render('@ChillMain/Notification/show.html.twig', [
'notification' => $notification,
'handler' => $this->notificationHandlerManager->getHandler($notification),
'appendCommentForm' => isset($appendCommentForm) ? $appendCommentForm->createView() : null,
'editedCommentForm' => isset($editedCommentForm) ? $editedCommentForm->createView() : null,
'editedCommentId' => $commentId ?? null,
]);
// we mark the notification as read after having computed the response
if ($this->getUser() instanceof User && !$notification->isReadBy($this->getUser())) {
$notification->markAsReadBy($this->getUser());
$this->em->flush();
}
return $response;
}
private function countUnread(): array
{
return [
'sent' => $this->notificationRepository->countUnreadByUserWhereSender($this->security->getUser()),
'inbox' => $this->notificationRepository->countUnreadByUserWhereAddressee($this->security->getUser()),
];
}
private function itemsForTemplate(array $notifications): array
{
$templateData = [];
foreach ($notifications as $notification) {
$templateData[] = [
'template' => $this->notificationHandlerManager->getTemplate($notification),
'template_data' => $this->notificationHandlerManager->getTemplateData($notification),
$data = [
'template' => $notificationRenderer->getTemplate($notification),
'template_data' => $notificationRenderer->getTemplateData($notification),
'notification' => $notification,
];
$templateData[] = $data;
}
return $templateData;
return $this->render('@ChillMain/Notification/show.html.twig', [
'datas' => $templateData,
'notifications' => $notifications,
'paginator' => $paginator,
]);
}
}

View File

@@ -11,9 +11,7 @@ declare(strict_types=1);
namespace Chill\MainBundle\Entity;
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use DateTimeImmutable;
use DateTimeInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
@@ -22,27 +20,19 @@ use Doctrine\ORM\Mapping as ORM;
* @ORM\Entity
* @ORM\Table(
* name="chill_main_notification",
* uniqueConstraints={
* @ORM\UniqueConstraint(columns={"relatedEntityClass", "relatedEntityId"})
* }
* )
* @ORM\HasLifecycleCallbacks
*/
class Notification implements TrackUpdateInterface
class Notification
{
private array $addedAddresses = [];
/**
* @ORM\ManyToMany(targetEntity=User::class)
* @ORM\JoinTable(name="chill_main_notification_addresses_user")
*/
private Collection $addressees;
private ?ArrayCollection $addressesOnLoad = null;
/**
* @ORM\OneToMany(targetEntity=NotificationComment::class, mappedBy="notification", orphanRemoval=true)
* @ORM\OrderBy({"createdAt": "ASC"})
*/
private Collection $comments;
/**
* @ORM\Column(type="datetime_immutable")
*/
@@ -53,84 +43,43 @@ class Notification implements TrackUpdateInterface
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private ?int $id = null;
private int $id;
/**
* @ORM\Column(type="text")
*/
private string $message = '';
private string $message;
/**
* @ORM\Column(type="json")
*/
private array $read;
/**
* @ORM\Column(type="string", length=255)
*/
private string $relatedEntityClass = '';
private string $relatedEntityClass;
/**
* @ORM\Column(type="integer")
*/
private int $relatedEntityId;
private array $removedAddresses = [];
/**
* @ORM\ManyToOne(targetEntity=User::class)
* @ORM\JoinColumn(nullable=true)
* @ORM\JoinColumn(nullable=false)
*/
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;
private User $sender;
public function __construct()
{
$this->addressees = new ArrayCollection();
$this->unreadBy = new ArrayCollection();
$this->comments = new ArrayCollection();
$this->setDate(new DateTimeImmutable());
}
public function addAddressee(User $addressee): self
{
if (!$this->addressees->contains($addressee)) {
$this->addressees[] = $addressee;
$this->addedAddresses[] = $addressee;
}
return $this;
}
public function addComment(NotificationComment $comment): self
{
if (!$this->comments->contains($comment)) {
$this->comments[] = $comment;
$comment->setNotification($this);
}
return $this;
}
public function addUnreadBy(User $user): self
{
if (!$this->unreadBy->contains($user)) {
$this->unreadBy[] = $user;
}
return $this;
@@ -141,19 +90,9 @@ class Notification implements TrackUpdateInterface
*/
public function getAddressees(): Collection
{
// keep a copy to compute changes later
if (null === $this->addressesOnLoad) {
$this->addressesOnLoad = new ArrayCollection($this->addressees->toArray());
}
return $this->addressees;
}
public function getComments(): Collection
{
return $this->comments;
}
public function getDate(): ?DateTimeImmutable
{
return $this->date;
@@ -169,6 +108,11 @@ class Notification implements TrackUpdateInterface
return $this->message;
}
public function getRead(): array
{
return $this->read;
}
public function getRelatedEntityClass(): ?string
{
return $this->relatedEntityClass;
@@ -184,97 +128,9 @@ class Notification implements TrackUpdateInterface
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
{
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);
$this->addressees->removeElement($addressee);
return $this;
}
@@ -293,6 +149,13 @@ class Notification implements TrackUpdateInterface
return $this;
}
public function setRead(array $read): self
{
$this->read = $read;
return $this;
}
public function setRelatedEntityClass(string $relatedEntityClass): self
{
$this->relatedEntityClass = $relatedEntityClass;
@@ -313,25 +176,4 @@ class Notification implements TrackUpdateInterface
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;
}
}

View File

@@ -1,191 +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\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;
}
}

View File

@@ -1,26 +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\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,
]);
}
}

View File

@@ -1,43 +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\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);
}
}

View File

@@ -1,81 +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\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']],
);
}
}

View File

@@ -1,63 +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\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';
}
}

View File

@@ -1,95 +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\Counter;
use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Entity\NotificationComment;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Repository\NotificationRepository;
use Chill\MainBundle\Templating\UI\NotificationCounterInterface;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\PreFlushEventArgs;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Security\Core\User\UserInterface;
final class NotificationByUserCounter implements NotificationCounterInterface
{
private CacheItemPoolInterface $cacheItemPool;
private NotificationRepository $notificationRepository;
public function __construct(CacheItemPoolInterface $cacheItemPool, NotificationRepository $notificationRepository)
{
$this->cacheItemPool = $cacheItemPool;
$this->notificationRepository = $notificationRepository;
}
public function addNotification(UserInterface $u): int
{
if (!$u instanceof User) {
return 0;
}
return $this->countUnreadByUser($u);
}
public function countUnreadByUser(User $user): int
{
$key = self::generateCacheKeyUnreadNotificationByUser($user);
$item = $this->cacheItemPool->getItem($key);
if ($item->isHit()) {
return $item->get();
}
$unreads = $this->notificationRepository->countUnreadByUser($user);
$item
->set($unreads)
// keep in cache for 15 minutes
->expiresAfter(60 * 15);
$this->cacheItemPool->save($item);
return $unreads;
}
public static function generateCacheKeyUnreadNotificationByUser(User $user): string
{
return 'chill_main_notif_unread_by_' . $user->getId();
}
public function onEditNotificationComment(NotificationComment $notificationComment, LifecycleEventArgs $eventArgs): void
{
$this->resetCacheForNotification($notificationComment->getNotification());
}
public function onPreFlushNotification(Notification $notification, PreFlushEventArgs $eventArgs): void
{
$this->resetCacheForNotification($notification);
}
private function resetCacheForNotification(Notification $notification): void
{
$keys = [];
if (null !== $notification->getSender()) {
$keys[] = self::generateCacheKeyUnreadNotificationByUser($notification->getSender());
}
foreach ($notification->getAddressees() as $addressee) {
$keys[] = self::generateCacheKeyUnreadNotificationByUser($addressee);
}
$this->cacheItemPool->deleteItems($keys);
}
}

View File

@@ -1,110 +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\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(),
]);
}
}
}
}

View File

@@ -1,18 +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\Exception;
use RuntimeException;
class NotificationHandlerNotFound extends RuntimeException
{
}

View File

@@ -1,32 +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\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;
}

View File

@@ -1,55 +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\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);
}
}

View File

@@ -1,51 +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\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 [];
}
}

View File

@@ -0,0 +1,54 @@
<?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);
}
}

View File

@@ -1,28 +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\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'],
]),
];
}
}

View File

@@ -1,39 +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\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,
]);
}
}

View File

@@ -13,76 +13,25 @@ namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Entity\User;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectRepository;
final class NotificationRepository implements ObjectRepository
{
private EntityManagerInterface $em;
private EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager)
{
$this->em = $entityManager;
$this->repository = $entityManager->getRepository(Notification::class);
}
public function countAllForAttendee(User $addressee): int
public function countAllForAttendee(User $addressee): int // TODO passer à attendees avec S
{
return $this->queryByAddressee($addressee)
->select('count(n)')
->getQuery()
->getSingleScalarResult();
}
$query = $this->queryAllForAttendee($addressee, $countQuery = true);
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();
return $query->getSingleScalarResult();
}
public function find($id, $lockMode = null, $lockVersion = null): ?Notification
@@ -104,9 +53,9 @@ final class NotificationRepository implements ObjectRepository
*
* @return Notification[]
*/
public function findAllForAttendee(User $addressee, $limit = null, $offset = null): array
public function findAllForAttendee(User $addressee, $limit = null, $offset = null): array // TODO passer à attendees avec S
{
$query = $this->queryByAddressee($addressee)->select('n');
$query = $this->queryAllForAttendee($addressee);
if ($limit) {
$query = $query->setMaxResults($limit);
@@ -116,26 +65,7 @@ final class NotificationRepository implements ObjectRepository
$query = $query->setFirstResult($offset);
}
$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();
return $query->getResult();
}
/**
@@ -149,31 +79,6 @@ final class NotificationRepository implements ObjectRepository
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
/**
* @return array|Notification[]
*/
public function findNotificationByRelatedEntityAndUserAssociated(string $relatedEntityClass, int $relatedEntityId, User $user): array
{
$qb = $this->repository->createQueryBuilder('n');
$qb
->select('n')
->where($qb->expr()->eq('n.relatedEntityClass', ':relatedEntityClass'))
->andWhere($qb->expr()->eq('n.relatedEntityId', ':relatedEntityId'))
->andWhere($qb->expr()->isNotNull('n.sender'))
->andWhere(
$qb->expr()->orX(
$qb->expr()->isMemberOf(':user', 'n.addressees'),
$qb->expr()->eq('n.sender', ':user')
)
)
->setParameter('relatedEntityClass', $relatedEntityClass)
->setParameter('relatedEntityId', $relatedEntityId)
->setParameter('user', $user);
return $qb->getQuery()->getResult();
}
public function findOneBy(array $criteria, ?array $orderBy = null): ?Notification
{
return $this->repository->findOneBy($criteria, $orderBy);
@@ -184,25 +89,22 @@ final class NotificationRepository implements ObjectRepository
return Notification::class;
}
private function queryByAddressee(User $addressee, bool $countQuery = false): QueryBuilder
private function queryAllForAttendee(User $addressee, bool $countQuery = false): Query
{
$qb = $this->repository->createQueryBuilder('n');
$select = 'n';
if ($countQuery) {
$select = 'count(n)';
}
$qb
->where($qb->expr()->isMemberOf(':addressee', 'n.addressees'))
->select($select)
->join('n.addressees', 'a')
->where('a = :addressee')
->setParameter('addressee', $addressee);
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;
return $qb->getQuery();
}
}

View File

@@ -25,8 +25,6 @@
// Chill flex responsive table/block presentation
@import './scss/flex_table';
// Specific templates
@import './scss/notification';
/*
* BASE LAYOUT POSITION
@@ -418,8 +416,3 @@ span.item-key {
background-color: #0000000a;
//text-decoration: dotted underline;
}
// increase toast message z-index (above all modals)
div.v-toast {
z-index: 10000!important;
}

View File

@@ -20,7 +20,6 @@ $chill-theme-buttons: (
"misc": $gray-300,
"cancel": $gray-300,
"choose": $gray-300,
"notify": $gray-300,
"unlink": $chill-red,
);
@@ -74,7 +73,6 @@ $chill-theme-buttons: (
&.btn-delete::before,
&.btn-remove::before,
&.btn-choose::before,
&.btn-notify::before,
&.btn-cancel::before {
font: normal normal normal 14px/1 ForkAwesome;
margin-right: 0.5em;
@@ -100,7 +98,6 @@ $chill-theme-buttons: (
&.btn-cancel::before { content: "\f060"; } // fa-arrow-left
&.btn-choose::before { content: "\f00c"; } // fa-check // f046 fa-check-square-o
&.btn-unlink::before { content: "\f127"; } // fa-chain-broken
&.btn-notify::before { content: "\f1d8"; } // fa-paper-plane
}

View File

@@ -1,62 +0,0 @@
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;
}
}
}
}
}
}

View File

@@ -85,9 +85,7 @@ const fetchScopes = () => {
const ValidationException = (response) => {
const error = {};
error.name = 'ValidationException';
error.violations = response.violations.map((violation) => `${violation.title}: ${violation.propertyPath}`);
error.titles = response.violations.map((violation) => violation.title);
error.propertyPaths = response.violations.map((violation) => violation.propertyPath);
error.violations = response.violations.map((violation) => `${violation.title}`);
return error;
}

View File

@@ -1,43 +0,0 @@
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);
});
})

View File

@@ -1,69 +0,0 @@
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);
});
});

View File

@@ -98,8 +98,6 @@
v-bind:defaultz="this.defaultz"
v-bind:entity="this.entity"
v-bind:flag="this.flag"
v-bind:errors="this.errors"
v-bind:checkErrors="this.checkErrors"
@getCities="getCities"
@getReferenceAddresses="getReferenceAddresses">
</edit-pane>
@@ -125,8 +123,6 @@
v-bind:defaultz="this.defaultz"
v-bind:entity="this.entity"
v-bind:flag="this.flag"
v-bind:errors="this.errors"
v-bind:checkErrors="this.checkErrors"
v-bind:insideModal="false"
@getCities="getCities"
@getReferenceAddresses="getReferenceAddresses">
@@ -260,10 +256,8 @@ export default {
editPane: false,
datePane: false,
loading: false,
success: false,
dirty: false
success: false
},
errors: [],
defaultz: {
button: {
text: { create: 'add_an_address_title', edit: 'edit_address' },
@@ -365,8 +359,8 @@ export default {
//console.log('validFrom', this.validFrom);
//console.log('validTo', this.validTo);
//console.log('useDatePane', this.useDatePane);
//console.log('Mounted now !');
console.log('Mounted now !');
if (this.context.edit) {
console.log('getInitialAddress', this.context.addressId);
this.getInitialAddress(this.context.addressId);
@@ -386,7 +380,7 @@ export default {
this.openEditPane();
} else {
this.flag.showPane = true;
//console.log('step0: open the Show Panel');
console.log('step0: open the Show Panel');
}
},
closeShowPane() {
@@ -535,23 +529,6 @@ export default {
});
},
checkErrors() {
this.errors = [];
if (this.flag.dirty) {
if (this.entity.selected.country === null) {
this.errors.push("Un pays doit être sélectionné.");
}
if (Object.keys(this.entity.selected.city).length === 0) {
this.errors.push("Une ville doit être sélectionnée.");
}
if (!this.entity.selected.isNoAddress) {
if (this.entity.selected.address.street === null || this.entity.selected.address.streetNumber === null) {
this.errors.push("Une adresse doit être sélectionnée.");
}
}
}
},
/*
* Make form ready for new changes
*/

View File

@@ -6,13 +6,11 @@
v-model="value"
:placeholder="$t('select_address')"
:tag-placeholder="$t('create_address')"
:select-label="$t('multiselect.select_label')"
:select-label="$t('press_enter_to_select')"
:deselect-label="$t('create_address')"
:selected-label="$t('multiselect.selected_label')"
@search-change="listenInputSearch"
ref="addressSelector"
@select="selectAddress"
@remove="remove"
name="field"
track-by="id"
label="value"
@@ -57,7 +55,7 @@ import { searchReferenceAddresses, fetchReferenceAddresses } from '../../api.js'
export default {
name: 'AddressSelection',
components: { VueMultiselect },
props: ['entity', 'context', 'updateMapCenter', 'flag', 'checkErrors'],
props: ['entity', 'context', 'updateMapCenter'],
data() {
return {
value: this.context.edit ? this.entity.address.addressReference : null,
@@ -110,13 +108,6 @@ export default {
this.entity.selected.address.streetNumber = value.streetNumber;
this.entity.selected.writeNew.address = false;
this.updateMapCenter(value.point);
this.flag.dirty = true;
this.checkErrors();
},
remove() {
this.flag.dirty = true;
this.entity.selected.address = {};
this.checkErrors();
},
listenInputSearch(query) {
//console.log('listenInputSearch', query, this.isAddressSelectorOpen);
@@ -157,8 +148,6 @@ export default {
this.entity.selected.address.street = addr.street;
this.entity.selected.address.streetNumber = addr.number;
this.entity.selected.writeNew.address = true;
this.flag.dirty = true;
this.checkErrors();
}
},
splitAddress(address) {

View File

@@ -7,15 +7,13 @@
@search-change="listenInputSearch"
ref="citySelector"
@select="selectCity"
@remove="remove"
name="field"
track-by="id"
label="value"
:custom-label="transName"
:placeholder="$t('select_city')"
:select-label="$t('multiselect.select_label')"
:select-label="$t('press_enter_to_select')"
:deselect-label="$t('create_postal_code')"
:selected-label="$t('multiselect.selected_label')"
:taggable="true"
:multiple="false"
@tag="addPostcode"
@@ -56,12 +54,12 @@ import { searchCities, fetchCities } from '../../api.js';
export default {
name: 'CitySelection',
components: { VueMultiselect },
props: ['entity', 'context', 'focusOnAddress', 'updateMapCenter', 'flag', 'checkErrors'],
props: ['entity', 'context', 'focusOnAddress', 'updateMapCenter'],
emits: ['getReferenceAddresses'],
data() {
return {
value: this.context.edit ? this.entity.address.postcode : null,
isLoading: false,
isLoading: false
}
},
computed: {
@@ -124,13 +122,6 @@ export default {
if (value.center) {
this.updateMapCenter(value.center);
}
this.flag.dirty = true;
this.checkErrors();
},
remove() {
this.flag.dirty = true;
this.entity.selected.city = {};
this.checkErrors();
},
listenInputSearch(query) {
if (query.length > 2) {

View File

@@ -5,16 +5,13 @@
id="countrySelect"
label="name"
track-by="id"
:custom-label="transName"
:placeholder="$t('select_country')"
:options="sortedCountries"
v-bind:custom-label="transName"
v-bind:placeholder="$t('select_country')"
v-bind:options="sortedCountries"
v-model="value"
:select-label="$t('multiselect.select_label')"
:deselect-label="$t('multiselect.deselect_label')"
:selected-label="$t('multiselect.selected_label')"
@select="selectCountry"
@remove="remove"
>
:select-label="$t('press_enter_to_select')"
:deselect-label="$t('press_enter_to_remove')"
@select="selectCountry">
</VueMultiselect>
</div>
</template>
@@ -25,7 +22,7 @@ import VueMultiselect from 'vue-multiselect';
export default {
name: 'CountrySelection',
components: { VueMultiselect },
props: ['context', 'entity', 'flag', 'checkErrors'],
props: ['context', 'entity'],
emits: ['getCities'],
data() {
return {
@@ -36,13 +33,14 @@ export default {
},
computed: {
sortedCountries() {
//console.log('sorted countries');
const countries = this.entity.loaded.countries;
let sortedCountries = [];
sortedCountries.push(...countries.filter(c => c.countryCode === 'FR'))
sortedCountries.push(...countries.filter(c => c.countryCode === 'BE'))
sortedCountries.push(...countries.filter(c => c.countryCode !== 'FR').filter(c => c.countryCode !== 'BE'))
return sortedCountries;
},
}
},
mounted() {
this.init();
@@ -51,7 +49,6 @@ export default {
init() {
if (this.value !== undefined) {
this.selectCountry(this.value);
this.flag.dirty = false;
}
},
selectCountryByCode(countryCode) {
@@ -64,13 +61,7 @@ export default {
//console.log('select country', value);
this.entity.selected.country = value;
this.$emit('getCities', value);
this.checkErrors();
},
remove() {
this.flag.dirty = true;
this.entity.selected.country = null;
this.checkErrors();
},
}
}
};

View File

@@ -7,12 +7,6 @@
<span class="sr-only">Loading...</span>
</div>
<div v-if="errors.length" class="alert alert-warning" >
<ul>
<li v-for="(e, i) in errors" :key="i">{{ e }}</li>
</ul>
</div>
<h4 class="h3">{{ $t('select_an_address_title') }}</h4>
<div class="row my-3">
<div class="col-lg-6">
@@ -31,8 +25,6 @@
<country-selection
v-bind:context="context"
v-bind:entity="entity"
v-bind:flag="flag"
v-bind:checkErrors="checkErrors"
@getCities="$emit('getCities', selected.country)">
</country-selection>
@@ -41,17 +33,13 @@
v-bind:context="context"
v-bind:focusOnAddress="focusOnAddress"
v-bind:updateMapCenter="updateMapCenter"
v-bind:flag="flag"
v-bind:checkErrors="checkErrors"
@getReferenceAddresses="$emit('getReferenceAddresses', selected.city)">
</city-selection>
<address-selection v-if="!isNoAddress"
v-bind:entity="entity"
v-bind:context="context"
v-bind:updateMapCenter="updateMapCenter"
v-bind:flag="flag"
v-bind:checkErrors="checkErrors">
v-bind:updateMapCenter="updateMapCenter">
</address-selection>
</div>
@@ -111,9 +99,7 @@ export default {
'flag',
'entity',
'errorMsg',
'insideModal',
'errors',
'checkErrors',
'insideModal'
],
emits: ['getCities', 'getReferenceAddresses'],
data() {
@@ -142,7 +128,7 @@ export default {
get() {
return this.entity.selected.isNoAddress;
}
},
}
},
methods: {
focusOnAddress() {

View File

@@ -10,7 +10,7 @@
<h4 class="h3">{{ $t('address_suggestions') }}</h4>
<div class="flex-table AddressSuggestionList">
<div v-for="(a, i) in context.suggestions" class="item-bloc" :key="`suggestions-${i}`">
<div v-for="a in context.suggestions" class="item-bloc">
<div class="float-button bottom">
<div class="box">
<div class="action">

View File

@@ -1,7 +1,7 @@
import { multiSelectMessages } from 'ChillMainAssets/vuejs/_js/i18n'
const addressMessages = {
fr: {
press_enter_to_select: 'Appuyer sur Entrée pour sélectionner',
press_enter_to_remove: 'Appuyer sur Entrée pour désélectionner',
add_an_address_title: 'Créer une adresse',
edit_an_address_title: 'Modifier une adresse',
create_a_new_address: 'Créer une nouvelle adresse',
@@ -48,8 +48,6 @@ const addressMessages = {
}
};
Object.assign(addressMessages.fr, multiSelectMessages.fr);
export {
addressMessages
};

View File

@@ -5,7 +5,6 @@
:action="context.action"
:buttonText="options.buttonText"
:displayBadge="options.displayBadge === 'true'"
:parent="options.parent"
@saveFormOnTheFly="saveFormOnTheFly">
</on-the-fly>
</template>

View File

@@ -23,37 +23,25 @@
<template v-slot:body v-if="type === 'person'">
<on-the-fly-person
:id="id"
:type="type"
:action="action"
v-bind:id="id"
v-bind:type="type"
v-bind:action="action"
ref="castPerson">
</on-the-fly-person>
<div v-if="hasResourceComment">
<h3>{{ $t('onthefly.resource_comment_title') }}</h3>
<blockquote class="chill-user-quote">
{{ parent.comment }}
</blockquote>
</div>
</template>
<template v-slot:body v-else-if="type === 'thirdparty'">
<on-the-fly-thirdparty
:id="id"
:type="type"
:action="action"
v-bind:id="id"
v-bind:type="type"
v-bind:action="action"
ref="castThirdparty">
</on-the-fly-thirdparty>
<div v-if="hasResourceComment">
<h3>{{ $t('onthefly.resource_comment_title') }}</h3>
<blockquote class="chill-user-quote">
{{ parent.comment }}
</blockquote>
</div>
</template>
<template v-slot:body v-else>
<on-the-fly-create
:action="action"
v-bind:action="action"
ref="castNew">
</on-the-fly-create>
</template>
@@ -90,25 +78,18 @@ export default {
OnTheFlyThirdparty,
OnTheFlyCreate
},
props: ['type', 'id', 'action', 'buttonText', 'displayBadge', 'parent', 'canCloseModal'],
props: ['type', 'id', 'action', 'buttonText', 'displayBadge'],
emits: ['saveFormOnTheFly'],
data() {
return {
modal: {
showModal: false,
modalDialogClass: "modal-dialog-scrollable modal-xl"
}
},
//action: this.action
}
},
computed: {
hasResourceComment() {
//console.log('hasResourceComment', this.parent);
return (typeof this.parent !== 'undefined' && this.parent !== null)
&& this.action === 'show'
&& this.parent.type === 'accompanying_period_resource'
&& (this.parent.comment !== null && this.parent.comment !== '')
;
},
classAction() {
switch (this.action) {
case 'show':
@@ -162,23 +143,10 @@ export default {
return 'entity-' + this.type + ' badge-' + this.type;
}
},
watch: {
canCloseModal: {
handler: function(val, oldVal) {
if (val) {
this.closeModal();
}
},
deep: true
}
},
methods: {
closeModal() {
this.modal.showModal = false;
},
openModal() {
//console.log('## OPEN ON THE FLY MODAL');
//console.log('## type:', this.type, ', action:', this.action);
console.log('## OPEN ON THE FLY MODAL');
console.log('## type:', this.type, ', action:', this.action);
this.modal.showModal = true;
this.$nextTick(function() {
//this.$refs.search.focus();
@@ -213,6 +181,8 @@ export default {
// pass datas to parent
this.$emit('saveFormOnTheFly', { type: type, data: data });
this.modal.showModal = false;
},
buildLocation(id, type) {
if (type === 'person') {

View File

@@ -17,7 +17,6 @@ const ontheflyMessages = {
person: "un nouvel usager",
thirdparty: "un nouveau tiers professionnel"
},
resource_comment_title: "Un commentaire est associé à cet interlocuteur"
}
}
}

View File

@@ -21,8 +21,7 @@ containers.forEach((container) => {
},
options: {
buttonText: container.dataset.buttonText || null,
displayBadge: container.dataset.displayBadge || false,
parent: JSON.parse(container.dataset.parent) || null,
displayBadge: container.dataset.displayBadge || false
}
}
}

View File

@@ -1,89 +0,0 @@
<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>

View File

@@ -1,17 +0,0 @@
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 };

View File

@@ -1,112 +0,0 @@
<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>

View File

@@ -84,17 +84,3 @@ const _createI18n = (appMessages) => {
};
export { _createI18n }
export const multiSelectMessages = {
fr: {
multiselect: {
placeholder: 'Choisir',
tag_placeholder: 'Créer un nouvel élément',
select_label: 'Appuyer sur "Entrée" pour sélectionner',
deselect_label: 'Appuyer sur "Entrée" pour désélectionner',
select_group_label: 'Appuyer sur "Entrée" pour sélectionner ce groupe',
deselect_group_label: 'Appuyer sur "Entrée" pour désélectionner ce groupe',
selected_label: 'Sélectionné'
}
}
};

View File

@@ -9,7 +9,7 @@
* with_delimiter bool add a delimiter between fragments
* has_no_address bool
* multiline bool multiline display
* extended_infos bool add extra informations (step, floor, etc.) DEPRECATED
* extended_infos bool add extra informations (step, floor, etc.)
#}
@@ -33,9 +33,25 @@
{{ 'address.consider homeless'|trans }}
</span>
{% else %}
<span class="address{% if options['multiline'] %} multiline{% endif %}{% if options['with_delimiter'] %} delimiter{% endif %}">
{% if options['extended_infos'] %}
<span class="address{% if options['multiline'] %} multiline{% endif %}{% if options['with_delimiter'] %} delimiter{% endif %}">
{{ _self.raw(lines) }}
</span>
</span>
{% else %}
<span class="address{% if options['multiline'] %} multiline{% endif %}{% if options['with_delimiter'] %} delimiter{% endif %}">
{% if address.street is not empty %}
<p>{{ streetLine }}</p>
{% endif %}
{% if address.postCode is not empty %}
<p class="postcode">
<span class="code">{{ address.postCode.code }}</span>
<span class="name">{{ address.postCode.name }}</span>
<span class="name">{{ address.distribution }}</span>
</p>
<p class="country">{{ address.postCode.country.name|localize_translatable_string }}</p>
{% endif %}
</span>
{% endif %}
{% endif %}
{{ _self.validity(address, options) }}
{% endmacro %}
@@ -87,12 +103,12 @@
<span class="name">{{ address.distribution }}</span>
</p>
<p class="country">{{ address.postCode.country.name|localize_translatable_string }}</p>
</div>
</div>
{% endif %}
<div class="noaddress">
{{ 'address.consider homeless'|trans }}
</div>
{% else %}
<div class="address{% if options['multiline'] %} multiline{% endif %}{% if options['with_delimiter'] %} delimiter{% endif %}">
{% if options['with_picto'] %}

View File

@@ -215,8 +215,3 @@
{{ form_widget(form.center) }}
{% endif %}
{% 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 %}

View File

@@ -1,88 +0,0 @@
<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>

View File

@@ -1,46 +0,0 @@
{% 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 %}

View File

@@ -1,44 +0,0 @@
{% 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 %}

View File

@@ -1,20 +0,0 @@
{{ 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

View File

@@ -1,19 +0,0 @@
{{ 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

View File

@@ -1,45 +0,0 @@
<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>

View File

@@ -1,57 +0,0 @@
{% 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 %}

View File

@@ -1,121 +1,42 @@
{% 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 %}
<div class="col-10 notification notification-show">
<div id="container content">
<div class="grid-8 centered">
<h1>{{ "Notifications list" | trans }}</h1>
<!-- TODO : UNREAD & READ -->
<h1>{{ 'notification.Notification'|trans }}</h1>
{%for data in datas %}
{% set notification = data.notification %}
<div class="flex-table">
{% include 'ChillMainBundle:Notification:_list_item.html.twig' with {
'data': {
'template': handler.getTemplate(notification),
'template_data': handler.getTemplateData(notification)
},
'action_button': 'false'
} %}
</div>
<dl class="chill_view_data">
<dt class="inline">{{ 'Message'|trans }}</dt>
<dd>{{ notification.message }}</dd>
</dl>
<div class="notification-comment-list my-5">
<h2 class="chill-blue">{{ 'notification.comments_list'|trans }}</h2>
<dl class="chill_view_data">
<dt class="inline">{{ 'Date'|trans }}</dt>
<dd>{{ notification.date | date('long') }}</dd>
</dl>
{% if notification.comments|length > 0 %}
<div class="flex-table">
{% for comment in notification.comments %}
{% if editedCommentForm is null or editedCommentId != comment.id %}
{{ m.show_comment(comment, {
'recordAction': _self.recordAction(comment)
}) }}
{% else %}
<div class="item-bloc">
<div class="item-row row">
<a id="comment-{{ comment.id }}"></a>
<dl class="chill_view_data">
<dt class="inline">{{ 'Sender'|trans }}</dt>
<dd>{{ notification.sender }}</dd>
</dl>
{{ form_start(editedCommentForm) }}
{{ form_errors(editedCommentForm) }}
{{ form_widget(editedCommentForm.content) }}
<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 %}
{% endfor %}
</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) }}
<dl class="chill_view_data">
<dt class="inline">{{ 'Addressees'|trans }}</dt>
<dd>{{ notification.addressees |join(', ') }}</dd>
</dl>
<dl class="chill_view_data">
<dt class="inline">{{ 'Entity'|trans }}</dt>
<dd>
{% include data.template with data.template_data %}
</dd>
</dl>
{% endfor %}
</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 %}

View File

@@ -10,7 +10,6 @@
* action string 'show', 'edit', 'create'
* buttonText string
* displayBadge boolean (default: false) replace button by badge, need to define buttonText for content
* parent object (optional) pass parent context of the targetEntity (used for course resource comment)
#}
<span class="onthefly-container"
@@ -32,10 +31,6 @@
data-display-badge="true"
{% endif %}
{% if parent is defined %}
data-parent='{{ parent|json_encode }}'
{% endif %}
></span>
{{ encore_entry_script_tags('vue_onthefly') }}

View File

@@ -6,20 +6,14 @@
<p class="message-confirm">{{ confirm_question }}</p>
{% endif %}
{% if display_content is defined and display_content is not empty %}
{{ display_content|raw }}
{% endif %}
{{ form_start(form) }}
<ul class="record_actions sticky-form-buttons">
{% if cancel_route is defined %}
<li class="cancel">
<a href="{{ chill_path_forward_return_path(cancel_route, cancel_parameters|default( { } ) ) }}" class="btn btn-cancel">
{{ 'Cancel'|trans }}
</a>
</li>
{% endif %}
<li class="cancel">
<a href="{{ path(cancel_route, cancel_parameters|default( { } ) ) }}" class="btn btn-cancel">
{{ 'Cancel'|trans }}
</a>
</li>
<li>
{{ form_widget(form.submit, { 'attr' : { 'class' : "btn btn-delete" } } ) }}
</li>

View File

@@ -106,6 +106,6 @@
});
</script>
{% block js %}<!-- nothing added to js -->{% endblock %}
{% block js%}<!-- nothing added to js -->{% endblock %}
</body>
</html>

View File

@@ -12,27 +12,16 @@ declare(strict_types=1);
namespace Chill\MainBundle\Routing\MenuBuilder;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Notification\Counter\NotificationByUserCounter;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Contracts\Translation\TranslatorInterface;
class UserMenuBuilder implements LocalMenuBuilderInterface
{
private NotificationByUserCounter $notificationByUserCounter;
private Security $security;
private TranslatorInterface $translator;
public function __construct(
NotificationByUserCounter $notificationByUserCounter,
Security $security,
TranslatorInterface $translator
) {
$this->notificationByUserCounter = $notificationByUserCounter;
public function __construct(Security $security)
{
$this->security = $security;
$this->translator = $translator;
}
public function buildMenu($menuId, \Knp\Menu\MenuItem $menu, array $parameters)
@@ -55,20 +44,6 @@ class UserMenuBuilder implements LocalMenuBuilderInterface
'order' => -9999999,
'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
->addChild(
'Change password',

View File

@@ -1,89 +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\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();
}
}

View File

@@ -56,11 +56,12 @@ class AddressNormalizer implements ContextAwareNormalizerInterface, NormalizerAw
/**
* @param Address $address
* @param string|null $format
* @param null|string $format
*/
public function normalize($address, $format = null, array $context = [])
{
if ($address instanceof Address) {
$data = [
'address_id' => $address->getId(),
'text' => $address->isNoAddress() ? null : $this->addressRender->renderStreetLine($address, []),

View File

@@ -22,7 +22,7 @@ class CollectionNormalizer implements NormalizerAwareInterface, NormalizerInterf
/**
* @param Collection $collection
* @param string|null $format
* @param null|string $format
*/
public function normalize($collection, $format = null, array $context = [])
{

View File

@@ -12,7 +12,6 @@ declare(strict_types=1);
namespace Chill\MainBundle\Serializer\Normalizer;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Location;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserJob;
@@ -56,21 +55,16 @@ class UserNormalizer implements ContextAwareNormalizerInterface, NormalizerAware
$context,
['docgen:expects' => Center::class, 'groups' => 'docgen:read']
);
$locationContext = array_merge(
$context,
['docgen:expects' => Location::class, 'groups' => 'dogen:read']
);
if (null === $user && 'docgen' === $format) {
return array_merge(self::NULL_USER, [
'user_job' => $this->normalizer->normalize(null, $format, $userJobContext),
'main_center' => $this->normalizer->normalize(null, $format, $centerContext),
'main_scope' => $this->normalizer->normalize(null, $format, $scopeContext),
'current_location' => $this->normalizer->normalize(null, $format, $locationContext),
]);
}
$data = [
return [
'type' => 'user',
'id' => $user->getId(),
'username' => $user->getUsername(),
@@ -81,12 +75,6 @@ class UserNormalizer implements ContextAwareNormalizerInterface, NormalizerAware
'main_center' => $this->normalizer->normalize($user->getMainCenter(), $format, $centerContext),
'main_scope' => $this->normalizer->normalize($user->getMainScope(), $format, $scopeContext),
];
if ('docgen' === $format) {
$data['current_location'] = $this->normalizer->normalize($user->getCurrentLocation(), $format, $locationContext);
}
return $data;
}
public function supportsNormalization($data, $format = null, array $context = []): bool

View File

@@ -14,6 +14,7 @@ namespace Chill\MainBundle\Templating\Entity;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Symfony\Component\Templating\EngineInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use function array_merge;
use function strtr;
@@ -27,18 +28,21 @@ class AddressRender implements ChillEntityRenderInterface
'with_delimiter' => false,
'has_no_address' => false,
'multiline' => true,
/* deprecated */
'extended_infos' => false,
];
private EngineInterface $templating;
private TranslatableStringHelper $translatableStringHelper;
protected ParameterBagInterface $parameterBag;
public function __construct(EngineInterface $templating, TranslatableStringHelper $translatableStringHelper)
{
public function __construct(
EngineInterface $templating,
TranslatableStringHelper $translatableStringHelper,
ParameterBagInterface $parameterBag
) {
$this->templating = $templating;
$this->translatableStringHelper = $translatableStringHelper;
$this->parameterBag = $parameterBag;
}
/**
@@ -68,7 +72,6 @@ class AddressRender implements ChillEntityRenderInterface
public function renderLines($addr): array
{
$lines = [];
if (null !== $addr->getPostCode()) {
if ($addr->getPostCode()->getCountry()->getCountryCode() === 'FR') {
$lines[] = $this->renderIntraBuildingLine($addr);
@@ -85,19 +88,32 @@ class AddressRender implements ChillEntityRenderInterface
$lines[] = $this->renderCountryLine($addr);
}
}
return array_values(array_filter($lines, static fn ($l) => null !== $l));
return array_values(array_filter($lines, fn ($l) => null !== $l));
}
public function renderStreetLine(Address $addr): ?string
/**
* @param Address addr
* @param mixed $addr
*/
public function renderString($addr, array $options): string
{
if (null !== $addr->getStreet() && $addr->getStreet() !== '') {
return implode(' - ', $this->renderLines($addr));
}
public function supports($entity, array $options): bool
{
return $entity instanceof Address;
}
public function renderStreetLine($addr): ?string
{
if (!empty($addr->getStreet())) {
$street = $addr->getStreet();
} else {
$street = '';
}
if (null !== $addr->getStreetNumber() && $addr->getStreetNumber() !== '') {
if (!empty($addr->getStreetNumber())) {
$streetNumber = $addr->getStreetNumber();
} else {
$streetNumber = '';
@@ -118,30 +134,18 @@ class AddressRender implements ChillEntityRenderInterface
return $res;
}
/**
* @param Address addr
* @param mixed $addr
*/
public function renderString($addr, array $options): string
private function renderBuildingLine($addr): ?string
{
return implode(' - ', $this->renderLines($addr));
}
public function supports($entity, array $options): bool
{
return $entity instanceof Address;
}
private function renderBuildingLine(Address $addr): ?string
{
if (null !== $addr->getBuildingName() && $addr->getBuildingName() !== '') {
if (!empty($addr->getBuildingName())) {
$building = $addr->getBuildingName();
} else {
$building = '';
}
$intraBuilding = $this->renderIntraBuildingLine($addr);
if (null === $intraBuilding) {
if (!empty($this->renderIntraBuildingLine($addr))) {
$intraBuilding = $this->renderIntraBuildingLine($addr);
} else {
$intraBuilding = '';
}
@@ -162,29 +166,36 @@ class AddressRender implements ChillEntityRenderInterface
private function renderCityLine($addr): string
{
if (null !== $addr->getPostcode()) {
if (!empty($addr->getPostcode())) {
$res = strtr('{postcode} {label}', [
'{postcode}' => $addr->getPostcode()->getCode(),
'{label}' => $addr->getPostcode()->getName(),
]);
}
if (null !== $addr->getPostCode()->getCountry()->getCountryCode()) {
if ($addr->getPostCode()->getCountry()->getCountryCode() === 'FR') {
if ($addr->getDistribution()) {
$res = $res . ' ' . $addr->getDistribution();
}
if (null !== $addr->getPostCode()->getCountry()->getCountryCode()) {
if ($addr->getPostCode()->getCountry()->getCountryCode() === 'FR') {
if ($addr->getDistribution()) {
$res = $res . ' ' . $addr->getDistribution();
}
}
}
return $res ?? '';
return $res;
}
private function renderCountryLine($addr): ?string
{
return $this->translatableStringHelper->localize(
$addr->getPostCode()->getCountry()->getName()
);
$preferredCountries = $this->parameterBag->get('chill_main.available_countries');
if (in_array($addr->getPostCode()->getCountry()->getCountryCode(), $preferredCountries)) {
$res = null;
} else {
$res = $this->translatableStringHelper->localize(
$addr->getPostCode()->getCountry()->getName()
);
}
return $res;
}
private function renderDeliveryLine($addr): ?string
@@ -213,11 +224,10 @@ class AddressRender implements ChillEntityRenderInterface
}
$res = implode(' - ', $arr);
if ('' === $res) {
$res = null;
}
return $res;
}
}

View File

@@ -1,98 +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 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));
}
}

View File

@@ -1,125 +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 Entity;
use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Repository\UserRepository;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use DateTimeImmutable;
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)
->setUpdatedAt(new DateTimeImmutable())
->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);
}
}
}

Some files were not shown because too many files have changed in this diff Show More