Merge branch 'master' of gitlab.com:Chill-Projet/chill-bundles

This commit is contained in:
Mathieu Jaumotte 2022-03-01 16:14:00 +01:00
commit 7b17dc692e
19 changed files with 211 additions and 81 deletions

View File

@ -11,6 +11,7 @@ and this project adheres to
## Unreleased
<!-- write down unreleased development here -->
* [parcours] Toggle emergency/intensity only by referrer (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/442)
* [docstore] Add an API entrypoint for StoredObject (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/466)
* [person] Add the possibility of uploading existing documents to AccPeriodWorkEvaluationDocument (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/466)
* [person] Add title to AccPeriodWorkEvaluationDocument (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/466)
@ -48,10 +49,9 @@ and this project adheres to
* [thirdparty_contact]: in search results the 'qualité' is displayed (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/465)
* [bug]: fix confidential toggle of address in thirdpartyrenderbox (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/460)
## Test releases
* Creation of PickCivilityType, and implementation in PersonType and ThirdpartyType
* [renderbox]: Fix display of address (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/462)
* [renderbox]: Add email in personRenderBox, this was not yet displayed.
### test release 2022-02-14
@ -68,6 +68,7 @@ and this project adheres to
* [parcours]: Mes parcours brouillon added to user menu (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/440)
* [Documents]: List view adapted to display more information (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/414)
* [person]: style fix in parcours listing per person. (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/432)
* [parcours]: Only the referrer can toggle the intensity of the parcours (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/442)
* [household]: display address of current household (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/415)
* ajoute un ordre dans les localisation (api)
* [pick entity]: fix translations in modal (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/419)
@ -316,6 +317,7 @@ and this project adheres to
* add an endpoint for checking permissions. See https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/232
* [activity] for a new activity: suggest and create on-the-fly locations based on the accompanying course location + location of the suggested parties
* [calendar] for a new rdv: suggest and create on-the-fly locations based on the accompanying course location + location of the suggested parties
* [period] Validation added when period is confidential and confirmed -> user cannot be null.
## Test releases

BIN
composer Executable file

Binary file not shown.

View File

@ -72,7 +72,7 @@ section.chill-entity {
}
}
p {
// display: inline-block;
display: inline-block;
margin: 0 0 0 1.5em;
text-indent: -1.5em;

View File

@ -1,7 +1,7 @@
/**
* Generic api method that can be adapted to any fetch request
*/
const makeFetch = (method, url, body) => {
const makeFetch = (method, url, body) => {
return fetch(url, {
method: method,
headers: {
@ -11,19 +11,20 @@
})
.then(response => {
if (response.ok) {
console.log('200 error')
return response.json();
}
if (response.status === 422) {
console.log('422 error')
return response.json().then(response => {
throw ValidationException(response)
});
}
if (response.status === 403) {
return response.json().then(() => {
throw AccessException();
});
console.log('403 error')
throw AccessException(response);
}
throw {
@ -88,14 +89,13 @@ const ValidationException = (response) => {
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);
return error;
}
const AccessException = () => {
const AccessException = (response) => {
const error = {};
error.name = 'AccessException';
error.violations = ['You are no longer permitted to perform this action'];
error.violations = ['You are not allowed to perform this action'];
return error;
}

View File

@ -322,19 +322,39 @@ final class AccompanyingCourseApiController extends ApiController
/**
* @Route("/api/1.0/person/accompanying-course/{id}/confidential.json", name="chill_api_person_accompanying_period_confidential")
* @ParamConverter("accompanyingCourse", options={"id": "id"})
*
* @param mixed $id
*/
public function toggleConfidentialApi(AccompanyingPeriod $accompanyingCourse, Request $request)
public function toggleConfidentialApi(AccompanyingPeriod $accompanyingCourse, $id, Request $request)
{
if ($request->getMethod() === 'POST') {
$this->denyAccessUnlessGranted(AccompanyingPeriodVoter::TOGGLE_CONFIDENTIAL, $accompanyingCourse);
$accompanyingCourse->setConfidential(!$accompanyingCourse->isConfidential());
$this->getDoctrine()->getManager()->flush();
}
return $this->json($accompanyingCourse->isConfidential(), Response::HTTP_OK, [], ['groups' => ['read']]);
}
/**
* @Route("/api/1.0/person/accompanying-course/{id}/intensity.json", name="chill_api_person_accompanying_period_intensity")
* @ParamConverter("accompanyingCourse", options={"id": "id"})
*/
public function toggleIntensityApi(AccompanyingPeriod $accompanyingCourse, Request $request)
{
if ($request->getMethod() === 'POST') {
$this->denyAccessUnlessGranted(AccompanyingPeriodVoter::TOGGLE_INTENSITY, $accompanyingCourse);
$status = $accompanyingCourse->getIntensity() === 'regular' ? 'occasional' : 'regular';
$accompanyingCourse->setIntensity($status);
$this->getDoctrine()->getManager()->flush();
}
return $this->json($accompanyingCourse->getIntensity(), Response::HTTP_OK, [], ['groups' => ['read']]);
}
public function workApi($id, Request $request, string $_format): Response
{
return $this->addRemoveSomething(

View File

@ -395,16 +395,16 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
],
],
'confidential' => [
'methods' => [
Request::METHOD_POST => true,
Request::METHOD_GET => true,
],
'controller_action' => 'toggleConfidentialApi',
'roles' => [
Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::TOGGLE_CONFIDENTIAL,
],
],
// 'confidential' => [
// 'methods' => [
// Request::METHOD_POST => true,
// Request::METHOD_GET => true,
// ],
// 'controller_action' => 'toggleConfidentialApi',
// 'roles' => [
// Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::TOGGLE_CONFIDENTIAL,
// ],
// ],
'findAccompanyingPeriodsByPerson' => [
'path' => '/by-person/{person_id}.{_format}',
'controller_action' => 'getAccompanyingPeriodsByPerson',

View File

@ -30,6 +30,8 @@ use Chill\PersonBundle\Entity\AccompanyingPeriod\Resource;
use Chill\PersonBundle\Entity\AccompanyingPeriod\UserHistory;
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
use Chill\PersonBundle\Validator\Constraints\AccompanyingPeriod\AccompanyingPeriodValidity;
use Chill\PersonBundle\Validator\Constraints\AccompanyingPeriod\ConfidentialCourseMustHaveReferrer;
use Chill\PersonBundle\Validator\Constraints\AccompanyingPeriod\LocationValidity;
use Chill\PersonBundle\Validator\Constraints\AccompanyingPeriod\ParticipationOverlap;
use Chill\PersonBundle\Validator\Constraints\AccompanyingPeriod\ResourceDuplicateCheck;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
@ -62,12 +64,9 @@ use const SORT_REGULAR;
* "accompanying_period": AccompanyingPeriod::class
* })
* @Assert\GroupSequenceProvider
* @Assert\Expression(
* "this.isConfidential and this.getUser === NULL",
* message="If the accompanying course is confirmed and confidential, a referrer must remain assigned."
* )
*
* @AccompanyingPeriodValidity(groups={AccompanyingPeriod::STEP_DRAFT, AccompanyingPeriod::STEP_CONFIRMED})
* @LocationValidity(groups={AccompanyingPeriod::STEP_DRAFT, AccompanyingPeriod::STEP_CONFIRMED})
* @ConfidentialCourseMustHaveReferrer(groups={AccompanyingPeriod::STEP_DRAFT, AccompanyingPeriod::STEP_CONFIRMED})
*/
class AccompanyingPeriod implements
GroupSequenceProviderInterface,
@ -201,7 +200,7 @@ class AccompanyingPeriod implements
/**
* @var string
* @ORM\Column(type="string", nullable=true)
* @Groups({"read", "write"})
* @Groups({"read"})
* @Assert\NotBlank(groups={AccompanyingPeriod::STEP_CONFIRMED})
*/
private $intensity = self::INTENSITY_OCCASIONAL;
@ -1000,7 +999,7 @@ class AccompanyingPeriod implements
}
/**
* Validation function.
* Validation functions.
*/
public function isDateConsistent(ExecutionContextInterface $context)
{

View File

@ -58,7 +58,7 @@ export default {
this.$store.dispatch('toggleIntensity', value)
.catch(({name, violations}) => {
if (name === 'ValidationException' || name === 'AccessException') {
violations.forEach((violation) => this.$toast.open({message: violation}));
this.$toast.open({message: this.$t('Only the referrer can toggle the intensity of an accompanying course')})
} else {
this.$toast.open({message: 'An error occurred'})
}
@ -75,20 +75,15 @@ export default {
});
},
toggleConfidential() {
this.$store.dispatch('fetchPermissions').then(() => {
if (!this.$store.getters.canTogglePermission) {
this.$toast.open({message: "Seul le référent peut modifier la confidentialité"});
return Promise.resolve();
} else {
return this.$store.dispatch('toggleConfidential', (!this.isConfidential));
}
}).catch(({name, violations}) => {
if (name === 'ValidationException' || name === 'AccessException') {
violations.forEach((violation) => this.$toast.open({message: violation}));
} else {
this.$toast.open({message: 'An error occurred'})
}
});
this.$store.dispatch('toggleConfidential')
.catch(({name, violations}) => {
console.log(name);
if (name === 'ValidationException' || name === 'AccessException') {
this.$toast.open({message: this.$t('Only the referrer can toggle the confidentiality of an accompanying course')})
} else {
this.$toast.open({message: 'An error occurred'})
}
});
},
},
}

View File

@ -46,6 +46,12 @@ if (root === 'banner') {
})
.use(store)
.use(i18n)
.use(VueToast, {
position: "bottom-right",
type: "error",
duration: 5000,
dismissible: true
})
.component('banner', Banner)
.mount('#banner-accompanying-course');
});

View File

@ -167,6 +167,8 @@ const appMessages = {
'Error while retriving users.': "Erreur du serveur lors du chargement de la liste des travailleurs.",
'Error while getting whoami.': "Erreur du serveur lors de la requête 'qui suis-je ?'",
'Error while retriving origin\'s list.': "Erreur du serveur lors du chargement de la liste des origines de la demande.",
'Only the referrer can toggle the intensity of an accompanying course': "Seul le référent peut modifier l'intensité d'un parcours.",
'Only the referrer can toggle the confidentiality of an accompanying course': "Seul le référent peut modifier la confidentialité d'un parcours."
}
};

View File

@ -420,24 +420,24 @@ let initPromise = (root) => Promise.all([getScopesPromise(root), accompanyingCou
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;
})
.then((response) => {
commit('patchResource', response);
})
.catch((error) => {
commit('catchError', error);
throw error;
})
},
/**
* Update accompanying course intensity/emergency/confidentiality
*/
toggleIntensity({ commit }, payload) {
const url = `/api/1.0/person/accompanying-course/${id}.json`
const url = `/api/1.0/person/accompanying-course/${id}/intensity.json`
const body = { type: "accompanying_period", 'intensity': payload }
return makeFetch('PATCH', url, body)
return makeFetch('POST', url, body)
.then((response) => {
commit('toggleIntensity', response.intensity);
commit('toggleIntensity', response);
})
.catch((error) => {
@ -459,14 +459,18 @@ let initPromise = (root) => Promise.all([getScopesPromise(root), accompanyingCou
})
},
toggleConfidential({ commit }, payload) {
const url = `/api/1.0/person/accompanying-course/${id}.json`
const url = `/api/1.0/person/accompanying-course/${id}/confidential.json`
const body = { type: "accompanying_period", confidential: payload }
return makeFetch('PATCH', url, body)
console.log('url', url, 'body', body);
return makeFetch('POST', url, body)
.then((response) => {
commit('toggleConfidential', response.confidential);
console.log('response', response);
commit('toggleConfidential', response);
})
.catch((error) => {
console.log('error', error)
commit('catchError', error);
throw error;
})
@ -737,10 +741,10 @@ let initPromise = (root) => Promise.all([getScopesPromise(root), accompanyingCou
"object": {
"type": "accompanying_period",
"id": id
},
"class": "Chill\\PersonBundle\\Entity\\AccompanyingPeriod",
"roles": [
"CHILL_PERSON_ACCOMPANYING_PERIOD_TOGGLE_CONFIDENTIAL"
},
"class": "Chill\\PersonBundle\\Entity\\AccompanyingPeriod",
"roles": [
"CHILL_PERSON_ACCOMPANYING_PERIOD_TOGGLE_CONFIDENTIAL"
]
}

View File

@ -113,15 +113,6 @@
<p class="chill-no-data-statement">{{ $t('renderbox.no_data') }}</p>
</li>
<li v-if="person.email">
<i class="fa fa-li fa-envelope-o"></i>
<a :href="'mailto: ' + person.email">{{ person.email }}</a>
</li>
<li v-else-if="options.addNoData">
<i class="fa fa-li fa-envelope-o"></i>
<p class="chill-no-data-statement">{{ $t('renderbox.no_data') }}</p>
</li>
<li v-if="person.centers !== undefined && person.centers.length > 0 && options.addCenter">
<i class="fa fa-li fa-long-arrow-right"></i>
<template v-for="c in person.centers">{{ c.name }}</template>

View File

@ -33,6 +33,7 @@ class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleH
self::DELETE,
self::FULL,
self::TOGGLE_CONFIDENTIAL_ALL,
self::TOGGLE_INTENSITY,
];
public const CREATE = 'CHILL_PERSON_ACCOMPANYING_PERIOD_CREATE';
@ -62,6 +63,11 @@ class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleH
*/
public const TOGGLE_CONFIDENTIAL_ALL = 'CHILL_PERSON_ACCOMPANYING_PERIOD_TOGGLE_CONFIDENTIAL_ALL';
/**
* Right to toggle urgency of parcours.
*/
public const TOGGLE_INTENSITY = 'CHILL_PERSON_ACCOMPANYING_PERIOD_TOGGLE_INTENSITY';
private Security $security;
private VoterHelperInterface $voterHelper;
@ -125,11 +131,20 @@ class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleH
}
if (self::TOGGLE_CONFIDENTIAL === $attribute) {
if ($subject->getUser() === $token->getUser()) {
if (null !== $subject->getUser() && ($subject->getUser() === $token->getUser())) {
return true;
}
return $this->voterHelper->voteOnAttribute(self::TOGGLE_CONFIDENTIAL_ALL, $subject, $token);
return false;
// return $this->voterHelper->voteOnAttribute(self::TOGGLE_CONFIDENTIAL_ALL, $subject, $token);
}
if (self::TOGGLE_INTENSITY === $attribute) {
if (null !== $subject->getUser() && ($subject->getUser() === $token->getUser())) {
return true;
}
return false;
}
// if confidential, only the referent can see it

View File

@ -13,9 +13,7 @@ namespace Chill\PersonBundle\Tests\AccompanyingPeriod;
use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\HttpFoundation\Request;
/**
* @internal
@ -42,7 +40,7 @@ final class AccompanyingPeriodConfidentialTest extends WebTestCase
]);
}
public function dataGenerateRandomAccompanyingCourse()
public function testConfidentialInvalid()
{
// Disabling this dataprovider to avoid having errors while running the test.
return yield from [];
@ -88,10 +86,7 @@ final class AccompanyingPeriodConfidentialTest extends WebTestCase
}
}
/**
* @dataProvider dataGenerateRandomAccompanyingCourse
*/
public function testRemoveUserWhenConfidential(int $periodId)
public function testConfidentialValid()
{
$this->markTestIncomplete(
'Marked as incomplete because of a problem in the dataprovider, at line 81.'
@ -101,8 +96,7 @@ final class AccompanyingPeriodConfidentialTest extends WebTestCase
->find($periodId);
$em = self::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$isConfidential = $period->isConfidential();
$step = $period->getStep();
$violations = self::$validator->validate($period, null, ['confirmed']);
$initialUser = $period->getUser();

View File

@ -15,6 +15,7 @@ use Chill\ActivityBundle\Repository\ActivityRepository;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
use Chill\PersonBundle\Templating\Entity\SocialIssueRender;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
@ -28,10 +29,13 @@ class AccompanyingPeriodValidityValidator extends ConstraintValidator
private SocialIssueRender $socialIssueRender;
public function __construct(ActivityRepository $activityRepository, SocialIssueRender $socialIssueRender)
private TokenStorageInterface $token;
public function __construct(ActivityRepository $activityRepository, SocialIssueRender $socialIssueRender, TokenStorageInterface $token)
{
$this->activityRepository = $activityRepository;
$this->socialIssueRender = $socialIssueRender;
$this->token = $token;
}
public function validate($period, Constraint $constraint)
@ -44,6 +48,7 @@ class AccompanyingPeriodValidityValidator extends ConstraintValidator
throw new UnexpectedValueException($period, AccompanyingPeriod::class);
}
/** Check if a social issue can be deleted (is not linked to an action or activity within the parcours) */
$socialIssues = [];
$activities = $this->activityRepository->findBy(['accompanyingPeriod' => $period]);

View File

@ -0,0 +1,27 @@
<?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\Validator\Constraints\AccompanyingPeriod;
use Symfony\Component\Validator\Constraint;
/**
* @Annotation
*/
class ConfidentialCourseMustHaveReferrer extends Constraint
{
public string $message = 'A confidential parcours must have a referrer';
public function getTargets()
{
return [self::CLASS_CONSTRAINT];
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace Chill\PersonBundle\Validator\Constraints\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
class ConfidentialCourseMustHaveReferrerValidator extends ConstraintValidator
{
public function validate($value, Constraint $constraint)
{
if (!$value instanceof AccompanyingPeriod) {
throw new UnexpectedTypeException($value, AccompanyingPeriod::class);
}
if (!$constraint instanceof ConfidentialCourseMustHaveReferrer) {
throw new UnexpectedTypeException($constraint, ConfidentialCourseMustHaveReferrer::class);
}
if ($value->isConfidential() && null === $value->getUser()) {
$this->context
->buildViolation($constraint->message)
->atPath('user')
->addViolation();
}
}
}

View File

@ -1177,6 +1177,44 @@ paths:
422:
description: "object with validation errors"
/1.0/person/accompanying-course/{id}/intensity.json:
post:
tags:
- person
summary: "Toggle intensity status of accompanying course"
parameters:
- name: id
in: path
required: true
description: The accompanying period's id
schema:
type: integer
format: integer
minimum: 1
requestBody:
description: "Intensity toggle"
required: true
content:
application/json:
schema:
type: object
properties:
type:
type: string
enum:
- "accompanying_period"
intensity:
type: string
responses:
401:
description: "Unauthorized"
404:
description: "Not found"
200:
description: "OK"
422:
description: "object with validation errors"
/1.0/person/accompanying-course/by-person/{person_id}.json:
get:
tags:

View File

@ -20,6 +20,7 @@ Two addresses has the same validFrom date: La date de validité est identique à
The firstname cannot be empty: Le prénom ne peut pas être vide
The lastname cannot be empty: Le nom de famille ne peut pas être vide
The gender must be set: Le genre doit être renseigné
You are not allowed to perform this action: Vous n'avez pas le droit de changer cette valeur.
#export list
You must select at least one element: Vous devez sélectionner au moins un élément
@ -51,6 +52,8 @@ household_membership:
A course must contains at least one social issue: 'Un parcours doit être associé à au moins une problématique sociale'
A course must be associated to at least one scope: 'Un parcours doit être associé à au moins un service'
The social %name% issue cannot be deleted because it is associated with an activity or an action: 'La problématique sociale "%name%" ne peut pas être supprimée car elle est associée à une activité ou une action'
A confidential parcours must have a referrer: 'Un parcours confidentiel doit avoir un référent'
Only the referrer can change the confidentiality of a parcours: 'Seul le référent peut modifier la confidentialité'
# resource
You must associate at least one entity: Associez un usager, un tiers ou indiquez une description libre