Merge branch '232_resources_comment' into 'master'

232 resources comment

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

View File

@@ -14,6 +14,7 @@ namespace Chill\PersonBundle\DependencyInjection;
use Chill\MainBundle\DependencyInjection\MissingBundleException;
use Chill\MainBundle\Security\Authorization\ChillExportVoter;
use Chill\PersonBundle\Doctrine\DQL\AddressPart;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodResourceVoter;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
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,
'name' => 'accompanying_period_origin',

View File

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

View File

@@ -14,8 +14,15 @@
}">
<template v-slot:record-actions>
<ul class="record_actions">
<li>
<write-comment
:resource="resource"
@updateComment="updateComment"
></write-comment>
</li>
<li>
<on-the-fly
:parent="parent"
:type="resource.resource.type"
:id="resource.resource.id"
action="show">
@@ -23,6 +30,7 @@
</li>
<li>
<on-the-fly
:parent="parent"
:type="resource.resource.type"
:id="resource.resource.id"
action="edit"
@@ -48,13 +56,19 @@
addId : false,
addEntity: true,
addInfo: false,
//addComment: true,
hLevel: 3
}">
<template v-slot:record-actions>
<ul class="record_actions">
<li>
<write-comment
:resource="resource"
@updateComment="updateComment"
></write-comment>
</li>
<li>
<on-the-fly
:parent="parent"
:type="resource.resource.type"
:id="resource.resource.id"
action="show">
@@ -62,6 +76,7 @@
</li>
<li>
<on-the-fly
:parent="parent"
:type="resource.resource.type"
:id="resource.resource.id"
action="edit"
@@ -85,6 +100,7 @@ import OnTheFly from 'ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue';
import ButtonLocation from '../ButtonLocation.vue';
import PersonRenderBox from 'ChillPersonAssets/vuejs/_components/Entity/PersonRenderBox.vue';
import ThirdPartyRenderBox from 'ChillThirdPartyAssets/vuejs/_components/Entity/ThirdPartyRenderBox.vue';
import WriteComment from './WriteComment';
export default {
name: 'ResourceItem',
@@ -92,11 +108,23 @@ export default {
OnTheFly,
ButtonLocation,
PersonRenderBox,
ThirdPartyRenderBox
ThirdPartyRenderBox,
WriteComment
},
props: ['resource'],
emits: ['remove'],
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() {
if ( this.resource.resource.current_household_address !== null ) {
return true;
@@ -116,6 +144,10 @@ export default {
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);
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) {
console.log('### mutation: updatePerson', payload);
let i = null;
@@ -360,6 +365,23 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
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
*/

View File

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

View File

@@ -4,11 +4,12 @@
{{ 'Resume Accompanying Course'|trans }}
{% endblock %}
{% macro insert_onthefly(type, entity) %}
{% macro insert_onthefly(type, entity, parent = null) %}
{% 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
buttonText: entity|chill_entity_render_string,
parent: parent
} %}
{% endmacro %}
@@ -55,6 +56,7 @@
</div>
{% endif %}
{% if accompanyingCourse.locationStatus != 'none' %}
<div class="mbloc col col-sm-6 col-lg-4">
<div class="location">
{% if accompanyingCourse.locationStatus == 'person' %}
@@ -63,12 +65,10 @@
{% elseif accompanyingCourse.locationStatus == 'address' %}
<h4>{{ 'This course has a temporarily location'|trans }}</h4>
{% endif %}
{% if accompanyingCourse.locationStatus != 'none' %}
{{ accompanyingCourse.location|chill_entity_render_box }}
{% endif %}
{{ accompanyingCourse.location|chill_entity_render_box }}
</div>
</div>
{% endif %}
{% if accompanyingCourse.pinnedComment is not empty %}
<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="resources">
<h4 class="item-key">{{ 'Resources'|trans }}</h4>
{% 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 %}
{{ _self.insert_onthefly('person', r.person) }}
{{ _self.insert_onthefly('person', r.person, parent) }}
{% elseif r.thirdParty is not null %}
{{ _self.insert_onthefly('thirdparty', r.thirdParty) }}
{{ _self.insert_onthefly('thirdparty', r.thirdParty, parent) }}
{% endif %}
{% endfor %}
</div>

View File

@@ -87,7 +87,15 @@
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
action: 'show', displayBadge: true,
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>
</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);
}
if (array_key_exists('comment', $data)) {
$resource->setComment($data['comment']);
}
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)
{
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:
type: thirdparty
id: 100
responses:
401:
description: "Unauthorized"
@@ -1234,6 +1233,54 @@ paths:
404:
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:
get:
tags:

View File

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