Merge branch '232_resources_comment' into 'master'

232 resources comment

See merge request Chill-Projet/chill-bundles!276
This commit is contained in:
Mathieu Jaumotte 2022-01-13 11:05:15 +00:00
commit 6c71cb0e80
31 changed files with 502 additions and 71 deletions

View File

@ -53,6 +53,7 @@ and this project adheres to
* [notification] new notification interface, can be associated to AccompanyingCourse/Period, Activities. * [notification] new notification interface, can be associated to AccompanyingCourse/Period, Activities.
* List notifications, show, and comment in User section * List notifications, show, and comment in User section
* Notify button and contextual notification box on associated objects pages * 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 release 2021-12-14 ### test release 2021-12-14

View File

@ -66,7 +66,7 @@
{% include 'ChillActivityBundle:Activity:concernedGroups.html.twig' with { {% include 'ChillActivityBundle:Activity:concernedGroups.html.twig' with {
'context': context, 'context': context,
'with_display': 'wrap-list', 'render': 'wrap-list',
'entity': activity, 'entity': activity,
'badge_person': true 'badge_person': true
} %} } %}

View File

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

View File

@ -85,7 +85,7 @@
{% include 'ChillActivityBundle:Activity:concernedGroups.html.twig' with { {% include 'ChillActivityBundle:Activity:concernedGroups.html.twig' with {
'context': context, 'context': context,
'with_display': 'bloc', 'render': 'bloc',
'badge_person': 'true' 'badge_person': 'true'
} %} } %}

View File

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

View File

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

View File

@ -359,8 +359,8 @@ export default {
//console.log('validFrom', this.validFrom); //console.log('validFrom', this.validFrom);
//console.log('validTo', this.validTo); //console.log('validTo', this.validTo);
//console.log('useDatePane', this.useDatePane); //console.log('useDatePane', this.useDatePane);
//console.log('Mounted now !');
console.log('Mounted now !');
if (this.context.edit) { if (this.context.edit) {
console.log('getInitialAddress', this.context.addressId); console.log('getInitialAddress', this.context.addressId);
this.getInitialAddress(this.context.addressId); this.getInitialAddress(this.context.addressId);
@ -380,7 +380,7 @@ export default {
this.openEditPane(); this.openEditPane();
} else { } else {
this.flag.showPane = true; this.flag.showPane = true;
console.log('step0: open the Show Panel'); //console.log('step0: open the Show Panel');
} }
}, },
closeShowPane() { closeShowPane() {

View File

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

View File

@ -23,25 +23,37 @@
<template v-slot:body v-if="type === 'person'"> <template v-slot:body v-if="type === 'person'">
<on-the-fly-person <on-the-fly-person
v-bind:id="id" :id="id"
v-bind:type="type" :type="type"
v-bind:action="action" :action="action"
ref="castPerson"> ref="castPerson">
</on-the-fly-person> </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>
<template v-slot:body v-else-if="type === 'thirdparty'"> <template v-slot:body v-else-if="type === 'thirdparty'">
<on-the-fly-thirdparty <on-the-fly-thirdparty
v-bind:id="id" :id="id"
v-bind:type="type" :type="type"
v-bind:action="action" :action="action"
ref="castThirdparty"> ref="castThirdparty">
</on-the-fly-thirdparty> </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>
<template v-slot:body v-else> <template v-slot:body v-else>
<on-the-fly-create <on-the-fly-create
v-bind:action="action" :action="action"
ref="castNew"> ref="castNew">
</on-the-fly-create> </on-the-fly-create>
</template> </template>
@ -78,18 +90,25 @@ export default {
OnTheFlyThirdparty, OnTheFlyThirdparty,
OnTheFlyCreate OnTheFlyCreate
}, },
props: ['type', 'id', 'action', 'buttonText', 'displayBadge'], props: ['type', 'id', 'action', 'buttonText', 'displayBadge', 'parent'],
emits: ['saveFormOnTheFly'], emits: ['saveFormOnTheFly'],
data() { data() {
return { return {
modal: { modal: {
showModal: false, showModal: false,
modalDialogClass: "modal-dialog-scrollable modal-xl" modalDialogClass: "modal-dialog-scrollable modal-xl"
}, }
//action: this.action
} }
}, },
computed: { 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() { classAction() {
switch (this.action) { switch (this.action) {
case 'show': case 'show':
@ -145,8 +164,8 @@ export default {
}, },
methods: { methods: {
openModal() { openModal() {
console.log('## OPEN ON THE FLY MODAL'); //console.log('## OPEN ON THE FLY MODAL');
console.log('## type:', this.type, ', action:', this.action); //console.log('## type:', this.type, ', action:', this.action);
this.modal.showModal = true; this.modal.showModal = true;
this.$nextTick(function() { this.$nextTick(function() {
//this.$refs.search.focus(); //this.$refs.search.focus();

View File

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

View File

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

View File

@ -10,6 +10,7 @@
* action string 'show', 'edit', 'create' * action string 'show', 'edit', 'create'
* buttonText string * buttonText string
* displayBadge boolean (default: false) replace button by badge, need to define buttonText for content * 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" <span class="onthefly-container"
@ -31,6 +32,10 @@
data-display-badge="true" data-display-badge="true"
{% endif %} {% endif %}
{% if parent is defined %}
data-parent='{{ parent|json_encode }}'
{% endif %}
></span> ></span>
{{ encore_entry_script_tags('vue_onthefly') }} {{ encore_entry_script_tags('vue_onthefly') }}

View File

@ -6,7 +6,7 @@
<p class="message-confirm">{{ confirm_question }}</p> <p class="message-confirm">{{ confirm_question }}</p>
{% endif %} {% endif %}
{% if display_content is not empty %} {% if display_content is defined and display_content is not empty %}
{{ display_content|raw }} {{ display_content|raw }}
{% endif %} {% endif %}

View File

@ -14,6 +14,7 @@ namespace Chill\PersonBundle\DependencyInjection;
use Chill\MainBundle\DependencyInjection\MissingBundleException; use Chill\MainBundle\DependencyInjection\MissingBundleException;
use Chill\MainBundle\Security\Authorization\ChillExportVoter; use Chill\MainBundle\Security\Authorization\ChillExportVoter;
use Chill\PersonBundle\Doctrine\DQL\AddressPart; use Chill\PersonBundle\Doctrine\DQL\AddressPart;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodResourceVoter;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter; use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Chill\PersonBundle\Security\Authorization\PersonVoter; use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Exception; use Exception;
@ -413,6 +414,25 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
], ],
], ],
], ],
[
'class' => \Chill\PersonBundle\Entity\AccompanyingPeriod\Resource::class,
'name' => 'accompanying_period_resource',
'base_path' => '/api/1.0/person/accompanying-period/resource',
'base_role' => 'ROLE_USER',
'actions' => [
'_entity' => [
'methods' => [
Request::METHOD_GET => false,
Request::METHOD_PATCH => true,
Request::METHOD_HEAD => false,
Request::METHOD_DELETE => false,
],
'roles' => [
Request::METHOD_PATCH => AccompanyingPeriodResourceVoter::EDIT,
],
],
],
],
[ [
'class' => \Chill\PersonBundle\Entity\AccompanyingPeriod\Origin::class, 'class' => \Chill\PersonBundle\Entity\AccompanyingPeriod\Origin::class,
'name' => 'accompanying_period_origin', 'name' => 'accompanying_period_origin',

View File

@ -20,6 +20,8 @@ use Symfony\Component\Serializer\Annotation\Groups;
use UnexpectedValueException; use UnexpectedValueException;
/** /**
* **About denormalization**: this operation is operated by @see{AccompanyingPeriodResourdeNormalizer}.
*
* @ORM\Entity * @ORM\Entity
* @ORM\Table( * @ORM\Table(
* name="chill_person_accompanying_period_resource", * name="chill_person_accompanying_period_resource",
@ -44,9 +46,10 @@ class Resource
private ?AccompanyingPeriod $accompanyingPeriod = null; private ?AccompanyingPeriod $accompanyingPeriod = null;
/** /**
* @ORM\ManyToOne(targetEntity=Comment::class) * @ORM\Column(type="text", nullable=true)
* @Groups({"read"})
*/ */
private $comment; private ?string $comment = '';
/** /**
* @ORM\Id * @ORM\Id
@ -75,7 +78,7 @@ class Resource
return $this->accompanyingPeriod; return $this->accompanyingPeriod;
} }
public function getComment(): ?Comment public function getComment(): ?string
{ {
return $this->comment; return $this->comment;
} }
@ -92,7 +95,7 @@ class Resource
/** /**
* @return Person|ThirdParty * @return Person|ThirdParty
* @Groups({"read", "write"}) * @Groups({"read"})
*/ */
public function getResource() public function getResource()
{ {
@ -111,9 +114,9 @@ class Resource
return $this; return $this;
} }
public function setComment(?Comment $comment): self public function setComment(?string $comment = null): self
{ {
$this->comment = $comment; $this->comment = (string) $comment;
return $this; return $this;
} }

View File

@ -14,8 +14,15 @@
}"> }">
<template v-slot:record-actions> <template v-slot:record-actions>
<ul class="record_actions"> <ul class="record_actions">
<li>
<write-comment
:resource="resource"
@updateComment="updateComment"
></write-comment>
</li>
<li> <li>
<on-the-fly <on-the-fly
:parent="parent"
:type="resource.resource.type" :type="resource.resource.type"
:id="resource.resource.id" :id="resource.resource.id"
action="show"> action="show">
@ -23,6 +30,7 @@
</li> </li>
<li> <li>
<on-the-fly <on-the-fly
:parent="parent"
:type="resource.resource.type" :type="resource.resource.type"
:id="resource.resource.id" :id="resource.resource.id"
action="edit" action="edit"
@ -48,13 +56,19 @@
addId : false, addId : false,
addEntity: true, addEntity: true,
addInfo: false, addInfo: false,
//addComment: true,
hLevel: 3 hLevel: 3
}"> }">
<template v-slot:record-actions> <template v-slot:record-actions>
<ul class="record_actions"> <ul class="record_actions">
<li>
<write-comment
:resource="resource"
@updateComment="updateComment"
></write-comment>
</li>
<li> <li>
<on-the-fly <on-the-fly
:parent="parent"
:type="resource.resource.type" :type="resource.resource.type"
:id="resource.resource.id" :id="resource.resource.id"
action="show"> action="show">
@ -62,6 +76,7 @@
</li> </li>
<li> <li>
<on-the-fly <on-the-fly
:parent="parent"
:type="resource.resource.type" :type="resource.resource.type"
:id="resource.resource.id" :id="resource.resource.id"
action="edit" action="edit"
@ -85,6 +100,7 @@ import OnTheFly from 'ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue';
import ButtonLocation from '../ButtonLocation.vue'; import ButtonLocation from '../ButtonLocation.vue';
import PersonRenderBox from 'ChillPersonAssets/vuejs/_components/Entity/PersonRenderBox.vue'; import PersonRenderBox from 'ChillPersonAssets/vuejs/_components/Entity/PersonRenderBox.vue';
import ThirdPartyRenderBox from 'ChillThirdPartyAssets/vuejs/_components/Entity/ThirdPartyRenderBox.vue'; import ThirdPartyRenderBox from 'ChillThirdPartyAssets/vuejs/_components/Entity/ThirdPartyRenderBox.vue';
import WriteComment from './WriteComment';
export default { export default {
name: 'ResourceItem', name: 'ResourceItem',
@ -92,11 +108,23 @@ export default {
OnTheFly, OnTheFly,
ButtonLocation, ButtonLocation,
PersonRenderBox, PersonRenderBox,
ThirdPartyRenderBox ThirdPartyRenderBox,
WriteComment
}, },
props: ['resource'], props: ['resource'],
emits: ['remove'], emits: ['remove'],
computed: { computed: {
parent() {
return {
'type': this.resource.type,
'id': this.resource.id,
'comment': this.resource.comment,
'parent': {
'type': this.$store.state.accompanyingCourse.type,
'id': this.$store.state.accompanyingCourse.id
}
}
},
hasCurrentHouseholdAddress() { hasCurrentHouseholdAddress() {
if ( this.resource.resource.current_household_address !== null ) { if ( this.resource.resource.current_household_address !== null ) {
return true; return true;
@ -116,6 +144,10 @@ export default {
this.$toast.open({message: 'An error occurred'}) this.$toast.open({message: 'An error occurred'})
} }
}); });
},
updateComment(resource) {
console.log('updateComment', resource);
this.$store.commit('updateResource', resource);
} }
} }
} }

View File

@ -0,0 +1,116 @@
<template>
<a class="btn btn-sm btn-misc change-icon"
:title="$t('write_comment')"
@click="openModal"
><i class="fa fa-pencil-square-o"></i>
</a>
<teleport to="body">
<modal v-if="modal.showModal"
:modalDialogClass="modal.modalDialogClass"
@close="modal.showModal = false">
<template v-slot:header>
<h3 class="modal-title">{{ $t('write_comment_about', { 'r': resource.resource.text }) }}</h3>
</template>
<template v-slot:body>
<ckeditor
name="content"
v-bind:placeholder="$t('comment_placeholder')"
:editor="editor"
v-model="content"
tag-name="textarea">
</ckeditor>
</template>
<template v-slot:footer>
<a class="btn btn-save"
@click="saveAction">
{{ $t('action.save')}}
</a>
</template>
</modal>
</teleport>
</template>
<script>
import Modal from 'ChillMainAssets/vuejs/_components/Modal.vue';
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
import CKEditor from '@ckeditor/ckeditor5-vue';
import ClassicEditor from "ChillMainAssets/module/ckeditor5";
export default {
name: "WriteComment",
components: {
Modal,
ckeditor: CKEditor.component,
},
props: ['resource'],
emits: ['updateComment'],
data() {
return {
modal: {
showModal: false,
modalDialogClass: "modal-dialog-scrollable modal-xl"
},
editor: ClassicEditor,
formdata: {
content: this.resource.comment
}
}
},
i18n: {
messages: {
fr: {
write_comment: "Écrire un commentaire",
write_comment_about: "Écrire un commentaire à propos de {r}",
comment_placeholder: "Commencez à écrire..."
}
}
},
computed: {
content: {
set(value) {
this.formdata.content = value;
},
get() {
return this.formdata.content;
}
},
},
methods: {
openModal() {
//console.log('write comment for', this.resource.resource.type, this.resource.resource.id);
this.modal.showModal = true;
},
saveAction() {
//console.log('save comment', this.resource.id, this.formdata.content);
this.patchResource(this.resource.id, this.formdata.content);
},
patchResource(id, comment) {
let url = `/api/1.0/person/accompanying-period/resource/${id}.json`,
body = {
"type": "accompanying_period_resource",
"comment": comment
}
;
makeFetch('PATCH', url, body)
.then(r => {
let resource = {
'type': 'accompanying_period_resource',
'id': r.id,
'comment': r.comment,
'resource': r.resource
}
this.$emit('updateComment', resource);
this.modal.showModal = false;
}
)
}
}
}
</script>

View File

@ -124,6 +124,11 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
//console.log('### mutation: addResource', resource); //console.log('### mutation: addResource', resource);
state.accompanyingCourse.resources.push(resource); state.accompanyingCourse.resources.push(resource);
}, },
updateResource(state, payload) {
console.log('### mutation: updateResource', payload);
let i = state.accompanyingCourse.resources.findIndex(r => r.id === payload.id);
state.accompanyingCourse.resources[i] = payload;
},
updatePerson(state, payload) { updatePerson(state, payload) {
console.log('### mutation: updatePerson', payload); console.log('### mutation: updatePerson', payload);
let i = null; let i = null;
@ -360,6 +365,23 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
throw error; throw error;
}) })
}, },
patchResource({ commit }, payload) {
const body = { type: "accompanying_period_resource" };
body['resource'] = {
type: payload.result.type,
id: payload.result.id
};
const url = `/api/1.0/person/accompanying-course/resource/${id}.json`;
return makeFetch('PATCH', url, body)
.then((response) => {
commit('patchResource', response);
})
.catch((error) => {
commit('catchError', error);
throw error;
})
},
/** /**
* On The Fly * On The Fly
*/ */

View File

@ -187,7 +187,7 @@ export default {
getPerson(this.id) getPerson(this.id)
.then(person => new Promise((resolve, reject) => { .then(person => new Promise((resolve, reject) => {
this.person = person; this.person = person;
console.log('get person', this.person); //console.log('get person', this.person);
resolve(); resolve();
})); }));
}, },

View File

@ -4,11 +4,12 @@
{{ 'Resume Accompanying Course'|trans }} {{ 'Resume Accompanying Course'|trans }}
{% endblock %} {% endblock %}
{% macro insert_onthefly(type, entity) %} {% macro insert_onthefly(type, entity, parent = null) %}
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with { {% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
action: 'show', displayBadge: true, action: 'show', displayBadge: true,
targetEntity: { name: type, id: entity.id }, targetEntity: { name: type, id: entity.id },
buttonText: entity|chill_entity_render_string buttonText: entity|chill_entity_render_string,
parent: parent
} %} } %}
{% endmacro %} {% endmacro %}
@ -55,6 +56,7 @@
</div> </div>
{% endif %} {% endif %}
{% if accompanyingCourse.locationStatus != 'none' %}
<div class="mbloc col col-sm-6 col-lg-4"> <div class="mbloc col col-sm-6 col-lg-4">
<div class="location"> <div class="location">
{% if accompanyingCourse.locationStatus == 'person' %} {% if accompanyingCourse.locationStatus == 'person' %}
@ -63,12 +65,10 @@
{% elseif accompanyingCourse.locationStatus == 'address' %} {% elseif accompanyingCourse.locationStatus == 'address' %}
<h4>{{ 'This course has a temporarily location'|trans }}</h4> <h4>{{ 'This course has a temporarily location'|trans }}</h4>
{% endif %} {% endif %}
{{ accompanyingCourse.location|chill_entity_render_box }}
{% if accompanyingCourse.locationStatus != 'none' %}
{{ accompanyingCourse.location|chill_entity_render_box }}
{% endif %}
</div> </div>
</div> </div>
{% endif %}
{% if accompanyingCourse.pinnedComment is not empty %} {% if accompanyingCourse.pinnedComment is not empty %}
<div class="mbloc col col-sm-6 col-lg-8"> <div class="mbloc col col-sm-6 col-lg-8">
@ -107,11 +107,21 @@
<div class="mbloc col col-sm-6 col-lg-4"> <div class="mbloc col col-sm-6 col-lg-4">
<div class="resources"> <div class="resources">
<h4 class="item-key">{{ 'Resources'|trans }}</h4> <h4 class="item-key">{{ 'Resources'|trans }}</h4>
{% for r in accompanyingCourse.resources %} {% for r in accompanyingCourse.resources %}
{% set parent = {
'type': 'accompanying_period_resource',
'id': r.id,
'comment': r.comment,
'parent': {
'type': 'accompanying_period',
'id': accompanyingCourse.id
}
} %}
{% if r.person is not null %} {% if r.person is not null %}
{{ _self.insert_onthefly('person', r.person) }} {{ _self.insert_onthefly('person', r.person, parent) }}
{% elseif r.thirdParty is not null %} {% elseif r.thirdParty is not null %}
{{ _self.insert_onthefly('thirdparty', r.thirdParty) }} {{ _self.insert_onthefly('thirdparty', r.thirdParty, parent) }}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</div> </div>

View File

@ -87,7 +87,15 @@
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with { {% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
action: 'show', displayBadge: true, action: 'show', displayBadge: true,
targetEntity: { name: 'thirdparty', id: w.handlingThierParty.id }, targetEntity: { name: 'thirdparty', id: w.handlingThierParty.id },
buttonText: w.handlingThierParty|chill_entity_render_string buttonText: w.handlingThierParty|chill_entity_render_string,
parent: {
'type': 'accompanying_period_resource',
'id': r.id,
'comment': r.comment,
'parent': {
'type': 'accompanying_period',
'id': accompanyingCourse.id
}
} %} } %}
</span> </span>
</div> </div>

View File

@ -0,0 +1,51 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\PersonBundle\Security\Authorization;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Resource;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use UnexpectedValueException;
class AccompanyingPeriodResourceVoter extends Voter
{
public const EDIT = 'CHILL_PERSON_ACCOMPANYING_PERIOD_RESOURCE_EDIT';
private AccessDecisionManagerInterface $accessDecisionManager;
public function __construct(AccessDecisionManagerInterface $accessDecisionManager)
{
$this->accessDecisionManager = $accessDecisionManager;
}
protected function supports($attribute, $subject)
{
return $subject instanceof Resource && self::EDIT === $attribute;
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
/** @var \Chill\PersonBundle\Entity\AccompanyingPeriod\Resource $subject */
switch ($attribute) {
case self::EDIT:
return $this->accessDecisionManager->decide(
$token,
[AccompanyingPeriodVoter::EDIT],
$subject->getAccompanyingPeriod()
);
default:
throw new UnexpectedValueException("attribute not supported: {$attribute}");
}
}
}

View File

@ -84,9 +84,22 @@ class AccompanyingPeriodResourceNormalizer implements DenormalizerAwareInterface
$resource->setResource($res); $resource->setResource($res);
} }
if (array_key_exists('comment', $data)) {
$resource->setComment($data['comment']);
}
return $resource; return $resource;
} }
public function normalize($resource, $format = null, array $context = [])
{
return [
'type' => 'accompanying_period_resource',
'id' => $resource->getId(),
'comment' => $resource->getComment(),
];
}
public function supportsDenormalization($data, $type, $format = null) public function supportsDenormalization($data, $type, $format = null)
{ {
return Resource::class === $type; return Resource::class === $type;

View File

@ -0,0 +1,51 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Serializer\Normalizer;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Resource;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
/**
* @internal
* @coversNothing
*/
final class ResourceJsonNormalizerTest extends KernelTestCase
{
private DenormalizerInterface $denormalizer;
protected function setUp(): void
{
self::bootKernel();
$this->denormalizer = self::$container->get(DenormalizerInterface::class);
}
public function testDenormalize()
{
$resource = new Resource();
$context = [
AbstractNormalizer::GROUPS => ['write'],
AbstractNormalizer::OBJECT_TO_POPULATE => $resource,
];
$json = [
'comment' => 'bloup',
'type' => 'accompanying_period_resource',
];
$resource = $this->denormalizer->denormalize($json, Resource::class, 'json', $context);
$this->assertEquals('bloup', $resource->getComment());
}
}

View File

@ -752,7 +752,6 @@ paths:
resource: resource:
type: thirdparty type: thirdparty
id: 100 id: 100
responses: responses:
401: 401:
description: "Unauthorized" description: "Unauthorized"
@ -1234,6 +1233,54 @@ paths:
404: 404:
description: "Not found" description: "Not found"
/1.0/person/accompanying-period/resource/{id}.json:
patch:
tags:
- accompanying-course-resource
summary: "Alter the resource"
parameters:
- name: id
in: path
required: true
description: The resource's id
schema:
type: integer
format: integer
minimum: 1
requestBody:
description: "A resource"
required: true
content:
application/json:
schema:
type: object
properties:
type:
type: string
enum:
- "accompanying_period_resource"
#id:
# type: integer
comment:
type: string
required:
- type
examples:
Set the resource comment:
value:
type: accompanying_period_resource
#id: 0
comment: my judicious comment
responses:
401:
description: "Unauthorized"
404:
description: "Not found"
200:
description: "OK"
422:
description: "object with validation errors"
/1.0/person/household.json: /1.0/person/household.json:
get: get:
tags: tags:

View File

@ -1,6 +1,7 @@
services: services:
chill.person.security.authorization.person: chill.person.security.authorization.person:
autowire: true autowire: true
autoconfigure: true
class: Chill\PersonBundle\Security\Authorization\PersonVoter class: Chill\PersonBundle\Security\Authorization\PersonVoter
tags: tags:
- { name: security.voter } - { name: security.voter }
@ -8,11 +9,19 @@ services:
Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter: Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter:
autowire: true autowire: true
autoconfigure: true
tags: tags:
- { name: security.voter } - { name: security.voter }
- { name: chill.role } - { name: chill.role }
Chill\PersonBundle\Security\Authorization\AccompanyingPeriodCommentVoter: Chill\PersonBundle\Security\Authorization\AccompanyingPeriodCommentVoter:
autowire: true autowire: true
autoconfigure: true
tags:
- { name: security.voter }
Chill\PersonBundle\Security\Authorization\AccompanyingPeriodResourceVoter:
autowire: true
autoconfigure: true
tags: tags:
- { name: security.voter } - { name: security.voter }

View File

@ -0,0 +1,42 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\Migrations\Person;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20220104133334 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_person_accompanying_period_resource ADD comment_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE chill_person_accompanying_period_resource DROP comment');
$this->addSql('ALTER TABLE chill_person_accompanying_period_resource ADD CONSTRAINT fk_dc78989ff8697d13 FOREIGN KEY (comment_id) REFERENCES chill_person_accompanying_period_comment (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('CREATE INDEX idx_dc78989ff8697d13 ON chill_person_accompanying_period_resource (comment_id)');
}
public function getDescription(): string
{
return 'Replace accompanyingCourse Resources comment by a string';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_person_accompanying_period_resource DROP CONSTRAINT fk_dc78989ff8697d13');
$this->addSql('DROP INDEX idx_dc78989ff8697d13');
$this->addSql('ALTER TABLE chill_person_accompanying_period_resource ADD comment TEXT DEFAULT NULL');
$this->addSql('ALTER TABLE chill_person_accompanying_period_resource DROP comment_id');
}
}

View File

@ -133,7 +133,6 @@ class ThirdParty implements TrackCreationInterface, TrackUpdateInterface
/** /**
* @ORM\Column(name="comment", type="text", nullable=true) * @ORM\Column(name="comment", type="text", nullable=true)
* @Groups({"read"})
*/ */
private ?string $comment = null; private ?string $comment = null;

View File

@ -38,25 +38,9 @@
<i class="fa fa-li fa-hand-o-right"></i> <i class="fa fa-li fa-hand-o-right"></i>
<b class="me-2">{{ $t('child_of') }}</b> <b class="me-2">{{ $t('child_of') }}</b>
<span class="chill-entity badge-thirdparty">{{ thirdparty.parent.text }}</span> <span class="chill-entity badge-thirdparty">{{ thirdparty.parent.text }}</span>
<!-- console: [Vue warn]: Failed to resolve component: on-the-fly ...
<on-the-fly type="thirdparty" action="show" :id="thirdparty.parent.id"
:buttonText="thirdparty.parent.text" :displayBadge="'true' === 'true'"
></on-the-fly>
-->
</li> </li>
<!-- TODO hasChildren <!-- TODO hasChildren
<li v-if="hasChildren"> NB: we cannot call on-the-fly from RenderBox. See error message in previous version of this file.
<i class="fa fa-li fa-hand-o-right"></i>
<b class="me-2">{{ $t('children') }}</b>
<span v-for="child in thirdparty.activeChildren">
<on-the-fly type="thirdparty"
action="show"
:id="child.id"
:buttonText="child.text"
displayBadge="'true' === 'true'">
</on-the-fly>
</span>
</li>
--> -->
</ul> </ul>
<confidential v-if="thirdparty.contactDataAnonymous"> <confidential v-if="thirdparty.contactDataAnonymous">
@ -91,9 +75,6 @@
<a :href="'mailto: ' + thirdparty.email">{{ thirdparty.email }}</a> <a :href="'mailto: ' + thirdparty.email">{{ thirdparty.email }}</a>
</li> </li>
</ul> </ul>
<div v-if="options.addComment && !thirdparty.contactDataAnonymous && thirdparty.comment">
<blockquote class="chill-user-quote">{{ thirdparty.comment }}</blockquote>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -14,7 +14,6 @@
hLevel: 3, hLevel: 3,
addCenter: true, addCenter: true,
addNoData: true, addNoData: true,
addComment: true,
isMultiline: true isMultiline: true
}" }"
></third-party-render-box> ></third-party-render-box>
@ -169,7 +168,7 @@ export default {
getThirdparty(this.id).then(thirdparty => new Promise((resolve, reject) => { getThirdparty(this.id).then(thirdparty => new Promise((resolve, reject) => {
this.thirdparty = thirdparty; this.thirdparty = thirdparty;
this.thirdparty.kind = thirdparty.kind; this.thirdparty.kind = thirdparty.kind;
console.log('get thirdparty', thirdparty); //console.log('get thirdparty', thirdparty);
if (this.action !== 'show') { if (this.action !== 'show') {
if (thirdparty.address !== null) { if (thirdparty.address !== null) {
// bof! we force getInitialAddress because addressId not available when mounted // bof! we force getInitialAddress because addressId not available when mounted
@ -190,7 +189,7 @@ export default {
} }
}, },
mounted() { mounted() {
console.log('mounted', this.action); //console.log('mounted', this.action);
if (this.action !== 'create') { if (this.action !== 'create') {
this.loadData(); this.loadData();
} else { } else {

View File

@ -46,7 +46,6 @@ class ThirdPartyNormalizer implements NormalizerAwareInterface, NormalizerInterf
'parent' => $this->normalizer->normalize($thirdParty->getParent(), $format, $context), 'parent' => $this->normalizer->normalize($thirdParty->getParent(), $format, $context),
'civility' => $this->normalizer->normalize($thirdParty->getCivility(), $format, $context), 'civility' => $this->normalizer->normalize($thirdParty->getCivility(), $format, $context),
'contactDataAnonymous' => $thirdParty->isContactDataAnonymous(), 'contactDataAnonymous' => $thirdParty->isContactDataAnonymous(),
'comment' => $thirdParty->getComment(),
]; ];
} }