Merge remote-tracking branch 'origin/master' into issue314_suggest_persons

This commit is contained in:
Julien Fastré 2021-11-29 13:45:37 +01:00
commit c64ab86f8e
41 changed files with 851 additions and 198 deletions

View File

@ -12,6 +12,7 @@ and this project adheres to
<!-- write down unreleased development here --> <!-- write down unreleased development here -->
* [person] suggest entities (person | thirdparty) when creating/editing the accompanying course (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/119) * [person] suggest entities (person | thirdparty) when creating/editing the accompanying course (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/119)
* [activity] add custom validation on the Activity class, based on what is required from the ActivityType (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/188)
* [main] translate multiselect messages when selecting/creating address * [main] translate multiselect messages when selecting/creating address
* [main] set the coordinates of the city when creating a new address OR choosing "pas d'adresse complète" * [main] set the coordinates of the city when creating a new address OR choosing "pas d'adresse complète"
* Use the user.label in accompanying course banner, instead of username; * Use the user.label in accompanying course banner, instead of username;
@ -21,7 +22,10 @@ and this project adheres to
* [activity] check ACL on activity list in person context * [activity] check ACL on activity list in person context
* [list for accompanying course in person] filter list using ACL * [list for accompanying course in person] filter list using ACL
* [validation] toasts are displayed for errors when modifying accompanying course (generalization required). * [validation] toasts are displayed for errors when modifying accompanying course (generalization required).
* [period] only the user can enable confidentiality
* add an endpoint for checking permissions. See https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/232 * 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
## Test releases ## Test releases

View File

@ -70,11 +70,6 @@ parameters:
count: 1 count: 1
path: src/Bundle/ChillPersonBundle/Serializer/Normalizer/MembersEditorNormalizer.php path: src/Bundle/ChillPersonBundle/Serializer/Normalizer/MembersEditorNormalizer.php
-
message: "#^Undefined variable\\: \\$value$#"
count: 1
path: src/Bundle/ChillPersonBundle/Validator/Constraints/AccompanyingPeriod/LocationValidityValidator.php
- -
message: "#^Undefined variable\\: \\$choiceSlug$#" message: "#^Undefined variable\\: \\$choiceSlug$#"
count: 1 count: 1

View File

@ -137,7 +137,7 @@ final class ActivityController extends AbstractController
static fn (ActivityReason $ar): int => $ar->getId() static fn (ActivityReason $ar): int => $ar->getId()
) )
->toArray(), ->toArray(),
'type_id' => $activity->getType()->getId(), 'type_id' => $activity->getActivityType()->getId(),
'duration' => $activity->getDurationTime() ? $activity->getDurationTime()->format('U') : null, 'duration' => $activity->getDurationTime() ? $activity->getDurationTime()->format('U') : null,
'date' => $activity->getDate()->format('Y-m-d'), 'date' => $activity->getDate()->format('Y-m-d'),
'attendee' => $activity->getAttendee(), 'attendee' => $activity->getAttendee(),
@ -194,7 +194,7 @@ final class ActivityController extends AbstractController
$form = $this->createForm(ActivityType::class, $entity, [ $form = $this->createForm(ActivityType::class, $entity, [
'center' => $entity->getCenter(), 'center' => $entity->getCenter(),
'role' => new Role('CHILL_ACTIVITY_UPDATE'), 'role' => new Role('CHILL_ACTIVITY_UPDATE'),
'activityType' => $entity->getType(), 'activityType' => $entity->getActivityType(),
'accompanyingPeriod' => $accompanyingPeriod, 'accompanyingPeriod' => $accompanyingPeriod,
])->handleRequest($request); ])->handleRequest($request);
@ -327,7 +327,7 @@ final class ActivityController extends AbstractController
$entity->setAccompanyingPeriod($accompanyingPeriod); $entity->setAccompanyingPeriod($accompanyingPeriod);
} }
$entity->setType($activityType); $entity->setActivityType($activityType);
$entity->setDate(new DateTime('now')); $entity->setDate(new DateTime('now'));
if ($request->query->has('activityData')) { if ($request->query->has('activityData')) {
@ -385,7 +385,7 @@ final class ActivityController extends AbstractController
$form = $this->createForm(ActivityType::class, $entity, [ $form = $this->createForm(ActivityType::class, $entity, [
'center' => $entity->getCenter(), 'center' => $entity->getCenter(),
'role' => new Role('CHILL_ACTIVITY_CREATE'), 'role' => new Role('CHILL_ACTIVITY_CREATE'),
'activityType' => $entity->getType(), 'activityType' => $entity->getActivityType(),
'accompanyingPeriod' => $accompanyingPeriod, 'accompanyingPeriod' => $accompanyingPeriod,
])->handleRequest($request); ])->handleRequest($request);
@ -408,7 +408,7 @@ final class ActivityController extends AbstractController
$activity_array = $this->serializer->normalize($entity, 'json', ['groups' => 'read']); $activity_array = $this->serializer->normalize($entity, 'json', ['groups' => 'read']);
$defaultLocationId = $this->getUser()->getCurrentLocation()->getId(); $defaultLocation = $this->getUser()->getCurrentLocation();
return $this->render($view, [ return $this->render($view, [
'person' => $person, 'person' => $person,
@ -416,7 +416,7 @@ final class ActivityController extends AbstractController
'entity' => $entity, 'entity' => $entity,
'form' => $form->createView(), 'form' => $form->createView(),
'activity_json' => $activity_array, 'activity_json' => $activity_array,
'default_location_id' => $defaultLocationId, 'default_location' => $defaultLocation,
]); ]);
} }

View File

@ -23,6 +23,7 @@ class AdminActivityTypeController extends CRUDController
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator) protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator)
{ {
/** @var \Doctrine\ORM\QueryBuilder $query */ /** @var \Doctrine\ORM\QueryBuilder $query */
return $query->orderBy('e.ordering', 'ASC'); return $query->orderBy('e.ordering', 'ASC')
->addOrderBy('e.id', 'ASC');
} }
} }

View File

@ -9,6 +9,7 @@
namespace Chill\ActivityBundle\Entity; namespace Chill\ActivityBundle\Entity;
use Chill\ActivityBundle\Validator\Constraints as ActivityValidator;
use Chill\DocStoreBundle\Entity\Document; use Chill\DocStoreBundle\Entity\Document;
use Chill\MainBundle\Entity\Center; use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable; use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
@ -41,6 +42,7 @@ use Symfony\Component\Serializer\Annotation\SerializedName;
* @DiscriminatorMap(typeProperty="type", mapping={ * @DiscriminatorMap(typeProperty="type", mapping={
* "activity": Activity::class * "activity": Activity::class
* }) * })
* @ActivityValidator\ActivityValidity
*/ */
/* /*
@ -202,8 +204,10 @@ class Activity implements HasCenterInterface, HasScopeInterface, AccompanyingPer
public function addPerson(?Person $person): self public function addPerson(?Person $person): self
{ {
if (null !== $person) { if (null !== $person) {
if (!$this->persons->contains($person)) {
$this->persons[] = $person; $this->persons[] = $person;
} }
}
return $this; return $this;
} }
@ -236,8 +240,10 @@ class Activity implements HasCenterInterface, HasScopeInterface, AccompanyingPer
public function addThirdParty(?ThirdParty $thirdParty): self public function addThirdParty(?ThirdParty $thirdParty): self
{ {
if (null !== $thirdParty) { if (null !== $thirdParty) {
if (!$this->thirdParties->contains($thirdParty)) {
$this->thirdParties[] = $thirdParty; $this->thirdParties[] = $thirdParty;
} }
}
return $this; return $this;
} }
@ -245,8 +251,10 @@ class Activity implements HasCenterInterface, HasScopeInterface, AccompanyingPer
public function addUser(?User $user): self public function addUser(?User $user): self
{ {
if (null !== $user) { if (null !== $user) {
if (!$this->users->contains($user)) {
$this->users[] = $user; $this->users[] = $user;
} }
}
return $this; return $this;
} }

View File

@ -12,6 +12,7 @@ namespace Chill\ActivityBundle\Entity;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use InvalidArgumentException; use InvalidArgumentException;
use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
/** /**
* Class ActivityType. * Class ActivityType.
@ -29,11 +30,13 @@ class ActivityType
public const FIELD_REQUIRED = 2; public const FIELD_REQUIRED = 2;
/** /**
* @deprecated not in use
* @ORM\Column(type="string", nullable=false, options={"default": ""}) * @ORM\Column(type="string", nullable=false, options={"default": ""})
*/ */
private string $accompanyingPeriodLabel = ''; private string $accompanyingPeriodLabel = '';
/** /**
* @deprecated not in use
* @ORM\Column(type="smallint", nullable=false, options={"default": 1}) * @ORM\Column(type="smallint", nullable=false, options={"default": 1})
*/ */
private int $accompanyingPeriodVisible = self::FIELD_INVISIBLE; private int $accompanyingPeriodVisible = self::FIELD_INVISIBLE;
@ -195,16 +198,21 @@ class ActivityType
/** /**
* @ORM\Column(type="smallint", nullable=false, options={"default": 1}) * @ORM\Column(type="smallint", nullable=false, options={"default": 1})
* @Assert\EqualTo(propertyPath="socialIssuesVisible", message="This parameter must be equal to social issue parameter")
*/ */
private int $socialActionsVisible = self::FIELD_INVISIBLE; private int $socialActionsVisible = self::FIELD_INVISIBLE;
/** /**
* @ORM\Column(type="string", nullable=false, options={"default": ""}) * @ORM\Column(type="string", nullable=false, options={"default": ""})
*
* @deprecated not in use
*/ */
private string $socialDataLabel = ''; private string $socialDataLabel = '';
/** /**
* @ORM\Column(type="smallint", nullable=false, options={"default": 1}) * @ORM\Column(type="smallint", nullable=false, options={"default": 1})
*
* @deprecated not in use
*/ */
private int $socialDataVisible = self::FIELD_INVISIBLE; private int $socialDataVisible = self::FIELD_INVISIBLE;
@ -260,16 +268,6 @@ class ActivityType
*/ */
private int $userVisible = self::FIELD_REQUIRED; private int $userVisible = self::FIELD_REQUIRED;
public function getAccompanyingPeriodLabel(): string
{
return $this->accompanyingPeriodLabel;
}
public function getAccompanyingPeriodVisible(): int
{
return $this->accompanyingPeriodVisible;
}
/** /**
* Get active * Get active
* return true if the type is active. * return true if the type is active.
@ -446,16 +444,6 @@ class ActivityType
return $this->socialActionsVisible; return $this->socialActionsVisible;
} }
public function getSocialDataLabel(): string
{
return $this->socialDataLabel;
}
public function getSocialDataVisible(): int
{
return $this->socialDataVisible;
}
public function getSocialIssuesLabel(): ?string public function getSocialIssuesLabel(): ?string
{ {
return $this->socialIssuesLabel; return $this->socialIssuesLabel;
@ -537,20 +525,6 @@ class ActivityType
return self::FIELD_INVISIBLE !== $this->{$property}; return self::FIELD_INVISIBLE !== $this->{$property};
} }
public function setAccompanyingPeriodLabel(string $accompanyingPeriodLabel): self
{
$this->accompanyingPeriodLabel = $accompanyingPeriodLabel;
return $this;
}
public function setAccompanyingPeriodVisible(int $accompanyingPeriodVisible): self
{
$this->accompanyingPeriodVisible = $accompanyingPeriodVisible;
return $this;
}
/** /**
* Set active * Set active
* set to true if the type is active. * set to true if the type is active.
@ -768,20 +742,6 @@ class ActivityType
return $this; return $this;
} }
public function setSocialDataLabel(string $socialDataLabel): self
{
$this->socialDataLabel = $socialDataLabel;
return $this;
}
public function setSocialDataVisible(int $socialDataVisible): self
{
$this->socialDataVisible = $socialDataVisible;
return $this;
}
public function setSocialIssuesLabel(string $socialIssuesLabel): self public function setSocialIssuesLabel(string $socialIssuesLabel): self
{ {
$this->socialIssuesLabel = $socialIssuesLabel; $this->socialIssuesLabel = $socialIssuesLabel;

View File

@ -56,7 +56,7 @@ class ActivityTypeType extends AbstractType
'persons', 'user', 'date', 'place', 'persons', 'persons', 'user', 'date', 'place', 'persons',
'thirdParties', 'durationTime', 'travelTime', 'attendee', 'thirdParties', 'durationTime', 'travelTime', 'attendee',
'reasons', 'comment', 'sentReceived', 'documents', 'reasons', 'comment', 'sentReceived', 'documents',
'emergency', 'accompanyingPeriod', 'socialData', 'users', 'emergency', 'socialIssues', 'socialActions', 'users',
]; ];
foreach ($fields as $field) { foreach ($fields as $field) {

View File

@ -1,7 +1,7 @@
<template> <template>
<concerned-groups></concerned-groups> <concerned-groups v-if="hasPerson"></concerned-groups>
<social-issues-acc></social-issues-acc> <social-issues-acc v-if="hasSocialIssues"></social-issues-acc>
<location></location> <location v-if="hasLocation"></location>
</template> </template>
<script> <script>
@ -11,6 +11,7 @@ import Location from './components/Location.vue';
export default { export default {
name: "App", name: "App",
props: ['hasSocialIssues', 'hasLocation', 'hasPerson'],
components: { components: {
ConcernedGroups, ConcernedGroups,
SocialIssuesAcc, SocialIssuesAcc,

View File

@ -1,4 +1,5 @@
import { getSocialIssues } from 'ChillPersonAssets/vuejs/AccompanyingCourse/api.js'; import { getSocialIssues } from 'ChillPersonAssets/vuejs/AccompanyingCourse/api.js';
import { fetchResults } from 'ChillMainAssets/lib/api/apiMethods';
/* /*
* Load socialActions by socialIssue (id) * Load socialActions by socialIssue (id)
@ -12,33 +13,20 @@ const getSocialActionByIssue = (id) => {
}); });
}; };
/* const getLocations = () => fetchResults('/api/1.0/main/location.json');
* Load Locations
*/ const getLocationTypes = () => fetchResults('/api/1.0/main/location-type.json');
const getLocations = () => {
const url = `/api/1.0/main/location.json`;
return fetch(url)
.then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
};
/* /*
* Load Location Types * Load Location Type by defaultFor
* @param {string} entity - can be "person" or "thirdparty"
*/ */
const getLocationTypes = () => { const getLocationTypeByDefaultFor = (entity) => {
const url = `/api/1.0/main/location-type.json`; return getLocationTypes().then(results =>
return fetch(url) results.filter(t => t.defaultFor === entity)[0]
.then(response => { );
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
}; };
/*
* Post a Location
*/
const postLocation = (body) => { const postLocation = (body) => {
const url = `/api/1.0/main/location.json`; const url = `/api/1.0/main/location.json`;
return fetch(url, { return fetch(url, {
@ -59,5 +47,6 @@ export {
getSocialActionByIssue, getSocialActionByIssue,
getLocations, getLocations,
getLocationTypes, getLocationTypes,
getLocationTypeByDefaultFor,
postLocation postLocation
}; };

View File

@ -2,10 +2,9 @@
<teleport to="#location"> <teleport to="#location">
<div class="mb-3 row"> <div class="mb-3 row">
<label class="col-form-label col-sm-4"> <label class="col-form-label col-sm-4">
{{ $t('activity.location') }} {{ $t("activity.location") }}
</label> </label>
<div class="col-sm-8"> <div class="col-sm-8">
<VueMultiselect <VueMultiselect
name="selectLocation" name="selectLocation"
id="selectLocation" id="selectLocation"
@ -17,7 +16,10 @@
:placeholder="$t('activity.choose_location')" :placeholder="$t('activity.choose_location')"
:custom-label="customLabel" :custom-label="customLabel"
:options="locations" :options="locations"
v-model="location"> group-values="locations"
group-label="locationGroup"
v-model="location"
>
</VueMultiselect> </VueMultiselect>
<new-location v-bind:locations="locations"></new-location> <new-location v-bind:locations="locations"></new-location>
@ -27,49 +29,146 @@
</template> </template>
<script> <script>
import { mapState } from "vuex"; import { mapState, mapGetters } from "vuex";
import VueMultiselect from 'vue-multiselect'; import VueMultiselect from "vue-multiselect";
import NewLocation from './Location/NewLocation.vue'; import NewLocation from "./Location/NewLocation.vue";
import { getLocations } from '../api.js'; import { getLocations, getLocationTypeByDefaultFor } from "../api.js";
export default { export default {
name: "Location", name: "Location",
components: { components: {
NewLocation, NewLocation,
VueMultiselect VueMultiselect,
}, },
data() { data() {
return { return {
locations: [] locations: [],
} };
}, },
computed: { computed: {
...mapState(['activity']), ...mapState(["activity"]),
...mapGetters(["suggestedEntities"]),
location: { location: {
get() { get() {
return this.activity.location; return this.activity.location;
}, },
set(value) { set(value) {
this.$store.dispatch('updateLocation', value); this.$store.dispatch("updateLocation", value);
} },
} },
}, },
mounted() { mounted() {
getLocations().then(response => new Promise(resolve => { getLocations().then(
console.log('getLocations', response); (results) => {
this.locations = response.results; getLocationTypeByDefaultFor('person').then(
if (window.default_location_id) { (personLocationType) => {
let location = this.locations.filter(l => l.id === window.default_location_id); if (personLocationType) {
this.$store.dispatch('updateLocation', location); const personLocation = this.makeAccompanyingPeriodLocation(personLocationType);
const concernedPersonsLocation =
this.makeConcernedPersonsLocation(personLocationType);
getLocationTypeByDefaultFor('thirdparty').then(
thirdpartyLocationType => {
const concernedThirdPartiesLocation =
this.makeConcernedThirdPartiesLocation(thirdpartyLocationType);
this.locations = [
{
locationGroup: 'Localisation du parcours',
locations: [personLocation]
},
{
locationGroup: 'Parties concernées',
locations: [...concernedPersonsLocation, ...concernedThirdPartiesLocation]
},
{
locationGroup: 'Autres localisations',
locations: results
} }
resolve(); ];
})) }
)
} else {
this.locations = [
{
locationGroup: 'Localisations',
locations: response.results
}
];
}
if (window.default_location_id) {
let location = this.locations.filter(
(l) => l.id === window.default_location_id
);
this.$store.dispatch("updateLocation", location);
}
}
)
})
}, },
methods: { methods: {
labelAccompanyingCourseLocation(value) {
return `${value.address.text} (${value.locationType.title.fr})`
},
customLabel(value) { customLabel(value) {
return `${value.locationType.title.fr} ${value.name ? value.name : ''}`; return value.locationType
? value.name
? value.name === '__AccompanyingCourseLocation__'
? this.labelAccompanyingCourseLocation(value)
: `${value.name} (${value.locationType.title.fr})`
: value.locationType.title.fr
: '';
},
makeConcernedPersonsLocation(locationType) {
let locations = [];
this.suggestedEntities.forEach(
(e) => {
if (e.type === 'person' && e.current_household_address !== null){
locations.push({
type: 'location',
id: -this.suggestedEntities.indexOf(e)*10,
onthefly: true,
name: e.text,
address: {
id: e.current_household_address.address_id,
},
locationType: locationType
});
} }
} }
} )
return locations;
},
makeConcernedThirdPartiesLocation(locationType) {
let locations = [];
this.suggestedEntities.forEach(
(e) => {
if (e.type === 'thirdparty' && e.address !== null){
locations.push({
type: 'location',
id: -this.suggestedEntities.indexOf(e)*10,
onthefly: true,
name: e.text,
address: { id: e.address.address_id },
locationType: locationType
});
}
}
)
return locations;
},
makeAccompanyingPeriodLocation(locationType) {
const accPeriodLocation = this.activity.accompanyingPeriod.location;
return {
type: 'location',
id: -1,
onthefly: true,
name: '__AccompanyingCourseLocation__',
address: {
id: accPeriodLocation.address_id,
text: `${accPeriodLocation.text} - ${accPeriodLocation.postcode.code} ${accPeriodLocation.postcode.name}`
},
locationType: locationType
}
}
},
};
</script> </script>

View File

@ -214,11 +214,9 @@ export default {
return cond; return cond;
}, },
getLocationTypesList() { getLocationTypesList() {
getLocationTypes().then(response => new Promise(resolve => { getLocationTypes().then(results => {
console.log('getLocationTypes', response); this.locationTypes = results.filter(t => t.availableForUsers === true);
this.locationTypes = response.results.filter(t => t.availableForUsers === true); })
resolve();
}))
}, },
openModal() { openModal() {
this.modal.showModal = true; this.modal.showModal = true;
@ -247,7 +245,6 @@ export default {
postLocation(body) postLocation(body)
.then( .then(
location => new Promise(resolve => { location => new Promise(resolve => {
console.log('postLocation', location);
this.locations.push(location); this.locations.push(location);
this.$store.dispatch('updateLocation', location); this.$store.dispatch('updateLocation', location);
resolve(); resolve();

View File

@ -7,8 +7,19 @@ import App from './App.vue';
const i18n = _createI18n(activityMessages); const i18n = _createI18n(activityMessages);
const hasSocialIssues = document.querySelector('#social-issues-acc') !== null;
const hasLocation = document.querySelector('#location') !== null;
const hasPerson = document.querySelector('#add-persons') !== null;
const app = createApp({ const app = createApp({
template: `<app></app>`, template: `<app :hasSocialIssues="hasSocialIssues", :hasLocation="hasLocation", :hasPerson="hasPerson"></app>`,
data() {
return {
hasSocialIssues,
hasLocation,
hasPerson,
};
}
}) })
.use(store) .use(store)
.use(i18n) .use(i18n)

View File

@ -1,5 +1,6 @@
import 'es6-promise/auto'; import 'es6-promise/auto';
import { createStore } from 'vuex'; import { createStore } from 'vuex';
import { postLocation } from './api';
const debug = process.env.NODE_ENV !== 'production'; const debug = process.env.NODE_ENV !== 'production';
//console.log('window.activity', window.activity); //console.log('window.activity', window.activity);
@ -27,7 +28,6 @@ const store = createStore({
}, },
getters: { getters: {
suggestedEntities(state) { suggestedEntities(state) {
console.log(state.activity);
if (typeof state.activity.accompanyingPeriod === "undefined") { if (typeof state.activity.accompanyingPeriod === "undefined") {
return []; return [];
} }
@ -303,7 +303,33 @@ const store = createStore({
let hiddenLocation = document.getElementById( let hiddenLocation = document.getElementById(
"chill_activitybundle_activity_location" "chill_activitybundle_activity_location"
); );
if (value.onthefly) {
const body = {
"type": "location",
"name": value.name === '__AccompanyingCourseLocation__' ? null : value.name,
"locationType": {
"id": value.locationType.id,
"type": "location-type"
}
};
if (value.address.id) {
Object.assign(body, {
"address": {
"id": value.address.id
},
})
}
postLocation(body)
.then(
location => hiddenLocation.value = location.id
).catch(
err => {
console.log(err.message);
}
);
} else {
hiddenLocation.value = value.id; hiddenLocation.value = value.id;
}
commit("updateLocation", value); commit("updateLocation", value);
}, },
}, },

View File

@ -28,7 +28,9 @@
{{ form_row(edit_form.socialActions) }} {{ form_row(edit_form.socialActions) }}
{% endif %} {% endif %}
{%- if edit_form.socialIssues is defined or edit_form.socialIssues is defined -%}
<div id="social-issues-acc"></div> <div id="social-issues-acc"></div>
{% endif %}
{%- if edit_form.reasons is defined -%} {%- if edit_form.reasons is defined -%}
{{ form_row(edit_form.reasons) }} {{ form_row(edit_form.reasons) }}
@ -46,9 +48,10 @@
{%- if edit_form.users is defined -%} {%- if edit_form.users is defined -%}
{{ form_widget(edit_form.users) }} {{ form_widget(edit_form.users) }}
{% endif %} {% endif %}
<div id="add-persons"></div>
{% endif %} {% endif %}
<div id="add-persons"></div>
<h2 class="chill-red">{{ 'Activity data'|trans }}</h2> <h2 class="chill-red">{{ 'Activity data'|trans }}</h2>

View File

@ -29,26 +29,30 @@
{{ form_row(form.socialActions) }} {{ form_row(form.socialActions) }}
{% endif %} {% endif %}
<div id="social-issues-acc"></div> {%- if edit_form.socialIssues is defined or edit_form.socialIssues is defined -%}
<div id="social-issues-acc"></div>
{% endif %}
{%- if form.reasons is defined -%} {%- if form.reasons is defined -%}
{{ form_row(form.reasons) }} {{ form_row(form.reasons) }}
{% endif %} {% endif %}
<h2 class="chill-red">{{ 'Concerned groups'|trans }}</h2> {%- if edit_form.persons is defined or edit_form.thirdParties is defined or edit_form.users is defined -%}
<h2 class="chill-red">{{ 'Concerned groups'|trans }}</h2>
{%- if form.persons is defined -%} {%- if form.persons is defined -%}
{{ form_widget(form.persons) }} {{ form_widget(form.persons) }}
{% endif %} {% endif %}
{%- if form.thirdParties is defined -%} {%- if form.thirdParties is defined -%}
{{ form_widget(form.thirdParties) }} {{ form_widget(form.thirdParties) }}
{% endif %} {% endif %}
{%- if form.users is defined -%} {%- if form.users is defined -%}
{{ form_widget(form.users) }} {{ form_widget(form.users) }}
{% endif %}
<div id="add-persons"></div>
{% endif %}
{% endif %} {% endif %}
<div id="add-persons"></div>
<h2 class="chill-red">{{ 'Activity data'|trans }}</h2> <h2 class="chill-red">{{ 'Activity data'|trans }}</h2>
{%- if form.date is defined -%} {%- if form.date is defined -%}

View File

@ -22,7 +22,7 @@
'{{ "You are going to leave a page with unsubmitted data. Are you sure you want to leave ?"|trans }}'); '{{ "You are going to leave a page with unsubmitted data. Are you sure you want to leave ?"|trans }}');
}); });
window.activity = {{ activity_json|json_encode|raw }}; window.activity = {{ activity_json|json_encode|raw }};
window.default_location_id = {{ default_location_id }}; {% if default_location is not null %}window.default_location_id = {{ default_location.id }}{% endif %};
</script> </script>
{{ encore_entry_script_tags('vue_activity') }} {{ encore_entry_script_tags('vue_activity') }}
{% endblock %} {% endblock %}

View File

@ -67,8 +67,8 @@
<dd> <dd>
{% if entity.location is not null %} {% if entity.location is not null %}
<p> <p>
<span>{{ entity.location.locationType.title|localize_translatable_string }}</span>
{{ entity.location.name }} {{ entity.location.name }}
<span> ({{ entity.location.locationType.title|localize_translatable_string }})</span>
</p> </p>
{{ entity.location.address|chill_entity_render_box }} {{ entity.location.address|chill_entity_render_box }}
{% else %} {% else %}

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.
*/
namespace Chill\ActivityBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* @Annotation
*/
class ActivityValidity extends Constraint
{
public const IS_REQUIRED_MESSAGE = ' is required';
public const ROOT_MESSAGE = 'For this type of activity, ';
public $noPersonsMessage = 'For this type of activity, you must add at least one person';
public $noThirdPartiesMessage = 'For this type of activity, you must add at least one third party';
public $noUsersMessage = 'For this type of activity, you must add at least one user';
public $socialActionsMessage = 'For this type of activity, you must add at least one social action';
public $socialIssuesMessage = 'For this type of activity, you must add at least one social issue';
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
public function makeIsRequiredMessage(string $property)
{
return self::ROOT_MESSAGE . $property . self::IS_REQUIRED_MESSAGE;
}
}

View File

@ -0,0 +1,126 @@
<?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.
*/
namespace Chill\ActivityBundle\Validator\Constraints;
use Chill\ActivityBundle\Entity\Activity;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Exception\UnexpectedValueException;
class ActivityValidityValidator extends ConstraintValidator
{
public function validate($activity, Constraint $constraint)
{
if (!$constraint instanceof ActivityValidity) {
throw new UnexpectedTypeException($constraint, ActivityValidity::class);
}
if (!$activity instanceof Activity) {
throw new UnexpectedValueException($activity, Activity::class);
}
if ($activity->getActivityType()->getPersonsVisible() === 2 && count($activity->getPersons()) === 0) {
$this->context
->buildViolation($constraint->noPersonsMessage)
->addViolation();
}
if ($activity->getActivityType()->getUsersVisible() === 2 && count($activity->getUsers()) === 0) {
$this->context
->buildViolation($constraint->noUsersMessage)
->addViolation();
}
if ($activity->getActivityType()->getThirdPartiesVisible() === 2 && count($activity->getThirdParties()) === 0) {
$this->context
->buildViolation($constraint->noThirdPartiesMessage)
->addViolation();
}
if ($activity->getActivityType()->getUserVisible() === 2 && null === $activity->getUser()) {
$this->context
->buildViolation($constraint->makeIsRequiredMessage('user'))
->addViolation();
}
if ($activity->getActivityType()->getDateVisible() === 2 && null === $activity->getDate()) {
$this->context
->buildViolation($constraint->makeIsRequiredMessage('date'))
->addViolation();
}
if ($activity->getActivityType()->getLocationVisible() === 2 && null === $activity->getLocation()) {
$this->context
->buildViolation($constraint->makeIsRequiredMessage('location'))
->addViolation();
}
if ($activity->getActivityType()->getDurationTimeVisible() === 2 && null === $activity->getDurationTime()) {
$this->context
->buildViolation($constraint->makeIsRequiredMessage('duration time'))
->addViolation();
}
if ($activity->getActivityType()->getTravelTimeVisible() === 2 && null === $activity->getTravelTime()) {
$this->context
->buildViolation($constraint->makeIsRequiredMessage('travel time'))
->addViolation();
}
if ($activity->getActivityType()->getAttendeeVisible() === 2 && null === $activity->getAttendee()) {
$this->context
->buildViolation($constraint->makeIsRequiredMessage('attendee'))
->addViolation();
}
if ($activity->getActivityType()->getReasonsVisible() === 2 && null === $activity->getReasons()) {
$this->context
->buildViolation($constraint->makeIsRequiredMessage('reasons'))
->addViolation();
}
if ($activity->getActivityType()->getCommentVisible() === 2 && null === $activity->getComment()) {
$this->context
->buildViolation($constraint->makeIsRequiredMessage('comment'))
->addViolation();
}
if ($activity->getActivityType()->getSentReceivedVisible() === 2 && null === $activity->getSentReceived()) {
$this->context
->buildViolation($constraint->makeIsRequiredMessage('sent/received'))
->addViolation();
}
if ($activity->getActivityType()->getDocumentsVisible() === 2 && null === $activity->getDocuments()) {
$this->context
->buildViolation($constraint->makeIsRequiredMessage('document'))
->addViolation();
}
if ($activity->getActivityType()->getEmergencyVisible() === 2 && null === $activity->getEmergency()) {
$this->context
->buildViolation($constraint->makeIsRequiredMessage('emergency'))
->addViolation();
}
if ($activity->getActivityType()->getSocialIssuesVisible() === 2 && $activity->getSocialIssues()->count() === 0) {
$this->context
->buildViolation($constraint->socialIssuesMessage)
->addViolation();
}
if ($activity->getActivityType()->getSocialActionsVisible() === 2 && $activity->getSocialActions()->count() === 0) {
$this->context
->buildViolation($constraint->socialActionsMessage)
->addViolation();
}
}
}

View File

@ -27,3 +27,8 @@ services:
Chill\ActivityBundle\Repository\: Chill\ActivityBundle\Repository\:
resource: '../Repository/' resource: '../Repository/'
Chill\ActivityBundle\Validator\Constraints\:
autowire: true
autoconfigure: true
resource: '../Validator/Constraints/'

View File

@ -139,34 +139,40 @@ ActivityReasonCategory is inactive and won't be proposed: La catégorie est inac
# activity type type admin # activity type type admin
ActivityType list: Types d'activités ActivityType list: Types d'activités
Create a new activity type: Créer un nouveau type d'activité Create a new activity type: Créer un nouveau type d'activité
Persons visible: Visibilté du champ Personnes Persons visible: Visibilité du champ Personnes
Persons label: Libellé du champ Personnes Persons label: Libellé du champ Personnes
User visible: Visibilté du champ Utilisateur User visible: Visibilité du champ Utilisateur
User label: Libellé du champ Utilisateur User label: Libellé du champ Utilisateur
Date visible: Visibilté du champ Date Date visible: Visibilité du champ Date
Date label: Libellé du champ Date Date label: Libellé du champ Date
Place visible: Visibilté du champ Lieu Place visible: Visibilité du champ Lieu
Place label: Libellé du champ Lieu Place label: Libellé du champ Lieu
Third parties visible: Visibilté du champ Tiers Third parties visible: Visibilité du champ Tiers
Third parties label: Libellé du champ Tiers Third parties label: Libellé du champ Tiers
Duration time visible: Visibilté du champ Durée Duration time visible: Visibilité du champ Durée
Duration time label: Libellé du champ Durée Duration time label: Libellé du champ Durée
Travel time visible: Visibilté du champ Durée de déplacement Travel time visible: Visibilité du champ Durée de déplacement
Travel time label: Libellé du champ Durée de déplacement Travel time label: Libellé du champ Durée de déplacement
Attendee visible: Visibilté du champ Présence de l'usager Attendee visible: Visibilité du champ Présence de l'usager
Attendee label: Libellé du champ Présence de l'usager Attendee label: Libellé du champ Présence de l'usager
Reasons visible: Visibilté du champ Sujet Reasons visible: Visibilité du champ Sujet
Reasons label: Libellé du champ Sujet Reasons label: Libellé du champ Sujet
Comment visible: Visibilté du champ Commentaire Comment visible: Visibilité du champ Commentaire
Comment label: Libellé du champ Commentaire Comment label: Libellé du champ Commentaire
Emergency visible: Visibilté du champ Urgent Emergency visible: Visibilité du champ Urgent
Emergency label: Libellé du champ Urgent Emergency label: Libellé du champ Urgent
Accompanying period visible: Visibilté du champ Période d'accompagnement Accompanying period visible: Visibilité du champ Période d'accompagnement
Accompanying period label: Libellé du champ Période d'accompagnement Accompanying period label: Libellé du champ Période d'accompagnement
Social data visible: Visibilté du champ Données sociales Social issues visible: Visibilité du champ Problématiques sociales
Social data label: Libellé du champ Données sociales Social issues label: Libellé du champ Problématiques sociales
Users visible: Visibilté du champ Utilisateurs Social actions visible: Visibilité du champ Action sociale
Social actions label: Libellé du champ Action sociale
Users visible: Visibilité du champ Utilisateurs
Users label: Libellé du champ Utilisateurs Users label: Libellé du champ Utilisateurs
Sent received visible: Visibilité du champ Entrant / Sortant
Sent received label: Libellé du champ Entrant / Sortant
Documents visible: Visibilité du champ Documents
Documents label: Libellé du champ Documents
# activity type category admin # activity type category admin
ActivityTypeCategory list: Liste des catégories des types d'activité ActivityTypeCategory list: Liste des catégories des types d'activité

View File

@ -1,2 +1,22 @@
The reasons's level should not be empty: Le niveau du sujet ne peut pas être vide The reasons's level should not be empty: Le niveau du sujet ne peut pas être vide
At least one reason must be choosen: Au moins un sujet doit être choisi At least one reason must be choosen: Au moins un sujet doit être choisi
For this type of activity, you must add at least one person: Pour ce type d'activité, vous devez ajouter au moins un usager
For this type of activity, you must add at least one user: Pour ce type d'activité, vous devez ajouter au moins un utilisateur
For this type of activity, you must add at least one third party: Pour ce type d'activité, vous devez ajouter au moins un tiers
For this type of activity, user is required: Pour ce type d'activité, l'utilisateur est requis
For this type of activity, date is required: Pour ce type d'activité, la date est requise
For this type of activity, location is required: Pour ce type d'activité, la localisation est requise
For this type of activity, attendee is required: Pour ce type d'activité, le champ "Présence de la personne" est requis
For this type of activity, duration time is required: Pour ce type d'activité, la durée est requise
For this type of activity, travel time is required: Pour ce type d'activité, la durée du trajet est requise
For this type of activity, reasons is required: Pour ce type d'activité, le champ "sujet" est requis
For this type of activity, comment is required: Pour ce type d'activité, un commentaire est requis
For this type of activity, sent/received is required: Pour ce type d'activité, le champ Entrant/Sortant est requis
For this type of activity, document is required: Pour ce type d'activité, un document est requis
For this type of activity, emergency is required: Pour ce type d'activité, le champ "Urgent" est requis
For this type of activity, accompanying period is required: Pour ce type d'activité, le parcours d'accompagnement est requis
For this type of activity, you must add at least one social issue: Pour ce type d'activité, vous devez ajouter au moins une problématique sociale
For this type of activity, you must add at least one social action: Pour ce type d'activité, vous devez indiquez au moins une action sociale
# admin
This parameter must be equal to social issue parameter: Ce paramètre doit être égal au paramètre "Visibilité du champs Problématiques sociales"

View File

@ -332,12 +332,12 @@ class CalendarController extends AbstractController
$personsId = array_map( $personsId = array_map(
static fn (Person $p): int => $p->getId(), static fn (Person $p): int => $p->getId(),
$entity->getPersons() $entity->getPersons()->toArray()
); );
$professionalsId = array_map( $professionalsId = array_map(
static fn (ThirdParty $thirdParty): ?int => $thirdParty->getId(), static fn (ThirdParty $thirdParty): ?int => $thirdParty->getId(),
$entity->getProfessionals() $entity->getProfessionals()->toArray()
); );
$durationTime = $entity->getEndDate()->diff($entity->getStartDate()); $durationTime = $entity->getEndDate()->diff($entity->getStartDate());

View File

@ -1,5 +1,6 @@
import 'es6-promise/auto'; import 'es6-promise/auto';
import { createStore } from 'vuex'; import { createStore } from 'vuex';
import { postLocation } from 'ChillActivityAssets/vuejs/Activity/api';
const debug = process.env.NODE_ENV !== 'production'; const debug = process.env.NODE_ENV !== 'production';
@ -33,7 +34,6 @@ const store = createStore({
}, },
getters: { getters: {
suggestedEntities(state) { suggestedEntities(state) {
console.log(state.activity)
if (typeof(state.activity.accompanyingPeriod) === 'undefined') { if (typeof(state.activity.accompanyingPeriod) === 'undefined') {
return []; return [];
} }
@ -189,8 +189,35 @@ const store = createStore({
updateLocation({ commit }, value) { updateLocation({ commit }, value) {
console.log('### action: updateLocation', value); console.log('### action: updateLocation', value);
let hiddenLocation = document.getElementById("chill_calendarbundle_calendar_location"); let hiddenLocation = document.getElementById("chill_calendarbundle_calendar_location");
if (value.onthefly) {
const body = {
"type": "location",
"name": value.name === '__AccompanyingCourseLocation__' ? null : value.name,
"locationType": {
"id": value.locationType.id,
"type": "location-type"
}
};
if (value.address.id) {
Object.assign(body, {
"address": {
"id": value.address.id
},
})
}
postLocation(body)
.then(
location => hiddenLocation.value = location.id
).catch(
err => {
console.log(err.message);
}
);
} else {
hiddenLocation.value = value.id; hiddenLocation.value = value.id;
commit('updateLocation', value); }
commit("updateLocation", value);
} }
} }

View File

@ -41,8 +41,8 @@
<dd> <dd>
{% if entity.location is not null %} {% if entity.location is not null %}
<p> <p>
<span>{{ entity.location.locationType.title|localize_translatable_string }}</span>
{{ entity.location.name }} {{ entity.location.name }}
<span> ({{ entity.location.locationType.title|localize_translatable_string }})</span>
</p> </p>
{{ entity.location.address|chill_entity_render_box }} {{ entity.location.address|chill_entity_render_box }}
{% else %} {% else %}

View File

@ -10,8 +10,6 @@
namespace Chill\MainBundle\Controller; namespace Chill\MainBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController; use Chill\MainBundle\CRUD\Controller\ApiController;
use DateInterval;
use DateTime;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
/** /**
@ -21,22 +19,11 @@ class LocationApiController extends ApiController
{ {
public function customizeQuery(string $action, Request $request, $query): void public function customizeQuery(string $action, Request $request, $query): void
{ {
$query->andWhere($query->expr()->orX( $query->andWhere(
$query->expr()->andX(
$query->expr()->eq('e.createdBy', ':user'),
$query->expr()->gte('e.createdAt', ':dateBefore')
),
$query->expr()->andX( $query->expr()->andX(
$query->expr()->eq('e.availableForUsers', "'TRUE'"), $query->expr()->eq('e.availableForUsers', "'TRUE'"),
$query->expr()->eq('e.active', "'TRUE'"), $query->expr()->eq('e.active', "'TRUE'"),
$query->expr()->isNotNull('e.name'),
$query->expr()->neq('e.name', ':emptyString'),
) )
)) );
->setParameters([
'user' => $this->getUser(),
'dateBefore' => (new DateTime())->sub(new DateInterval('P6M')),
'emptyString' => '',
]);
} }
} }

View File

@ -11,6 +11,7 @@ namespace Chill\MainBundle\Entity;
use Chill\MainBundle\Repository\LocationTypeRepository; use Chill\MainBundle\Repository\LocationTypeRepository;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Serializer\Annotation as Serializer; use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap; use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
@ -20,9 +21,14 @@ use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
* @DiscriminatorMap(typeProperty="type", mapping={ * @DiscriminatorMap(typeProperty="type", mapping={
* "location-type": LocationType::class * "location-type": LocationType::class
* }) * })
* @UniqueEntity({"defaultFor"})
*/ */
class LocationType class LocationType
{ {
public const DEFAULT_FOR_3PARTY = 'thirdparty';
public const DEFAULT_FOR_PERSON = 'person';
public const STATUS_NEVER = 'never'; public const STATUS_NEVER = 'never';
public const STATUS_OPTIONAL = 'optional'; public const STATUS_OPTIONAL = 'optional';
@ -53,6 +59,12 @@ class LocationType
*/ */
private string $contactData = self::STATUS_OPTIONAL; private string $contactData = self::STATUS_OPTIONAL;
/**
* @ORM\Column(type="string", nullable=true, length=32, unique=true)
* @Serializer\Groups({"read"})
*/
private ?string $defaultFor = null;
/** /**
* @ORM\Id * @ORM\Id
* @ORM\GeneratedValue * @ORM\GeneratedValue
@ -87,6 +99,11 @@ class LocationType
return $this->contactData; return $this->contactData;
} }
public function getDefaultFor(): ?string
{
return $this->defaultFor;
}
public function getId(): ?int public function getId(): ?int
{ {
return $this->id; return $this->id;
@ -125,6 +142,13 @@ class LocationType
return $this; return $this;
} }
public function setDefaultFor(?string $defaultFor): self
{
$this->defaultFor = $defaultFor;
return $this;
}
public function setTitle(array $title): self public function setTitle(array $title): self
{ {
$this->title = $title; $this->title = $title;

View File

@ -71,6 +71,18 @@ final class LocationTypeType extends AbstractType
], ],
'expanded' => true, 'expanded' => true,
] ]
)
->add(
'defaultFor',
ChoiceType::class,
[
'choices' => [
'none' => null,
'person' => LocationType::DEFAULT_FOR_PERSON,
'thirdparty' => LocationType::DEFAULT_FOR_3PARTY,
],
'expanded' => true,
]
); );
} }
} }

View File

@ -10,7 +10,6 @@
body: (body !== null) ? JSON.stringify(body) : null body: (body !== null) ? JSON.stringify(body) : null
}) })
.then(response => { .then(response => {
if (response.ok) { if (response.ok) {
return response.json(); return response.json();
} }

View File

@ -11,6 +11,7 @@
<th>{{ 'Address required'|trans }}</th> <th>{{ 'Address required'|trans }}</th>
<th>{{ 'Contact data'|trans }}</th> <th>{{ 'Contact data'|trans }}</th>
<th>{{ 'Active'|trans }}</th> <th>{{ 'Active'|trans }}</th>
<th>{{ 'Default for'|trans }}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -33,6 +34,7 @@
<i class="fa fa-square-o"></i> <i class="fa fa-square-o"></i>
{%- endif -%} {%- endif -%}
</td> </td>
<td>{{ entity.defaultFor|trans }}</td>
<td> <td>
<ul class="record_actions"> <ul class="record_actions">
<li> <li>

View File

@ -0,0 +1,38 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\Migrations\Main;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Add defaultFor to LocationType.
*/
final class Version20211123093355 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->addSql('DROP INDEX UNIQ_A459B5CADD3E4105');
$this->addSql('ALTER TABLE chill_main_location_type DROP defaultFor');
}
public function getDescription(): string
{
return 'Add defaultFor to LocationType';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_main_location_type ADD defaultFor VARCHAR(32) DEFAULT NULL');
$this->addSql('CREATE UNIQUE INDEX UNIQ_A459B5CADD3E4105 ON chill_main_location_type (defaultFor)');
}
}

View File

@ -212,6 +212,10 @@ Location type: Type de localisation
Phonenumber1: Numéro de téléphone Phonenumber1: Numéro de téléphone
Phonenumber2: Autre numéro de téléphone Phonenumber2: Autre numéro de téléphone
Configure location and location type: Configuration des localisations Configure location and location type: Configuration des localisations
Default for: Type de localisation par défaut pour
none: aucun
person: usager
thirdparty: tiers
# circles / scopes # circles / scopes
Choose the circle: Choisir le cercle Choose the circle: Choisir le cercle

View File

@ -249,6 +249,22 @@ 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"})
*/
public function toggleConfidentialApi(AccompanyingPeriod $accompanyingCourse, Request $request)
{
if ($request->getMethod() === 'POST') {
$this->denyAccessUnlessGranted(AccompanyingPeriodVoter::CONFIDENTIAL, $accompanyingCourse);
$accompanyingCourse->setConfidential(!$accompanyingCourse->isConfidential());
$this->getDoctrine()->getManager()->flush();
}
return $this->json($accompanyingCourse->isConfidential(), Response::HTTP_OK, [], ['groups' => ['read']]);
}
public function workApi($id, Request $request, string $_format): Response public function workApi($id, Request $request, string $_format): Response
{ {
return $this->addRemoveSomething( return $this->addRemoveSomething(

View File

@ -378,7 +378,6 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
Request::METHOD_DELETE => 'ALWAYS_FAILS', Request::METHOD_DELETE => 'ALWAYS_FAILS',
], ],
], ],
'confirm' => [ 'confirm' => [
'methods' => [ 'methods' => [
Request::METHOD_POST => true, Request::METHOD_POST => true,
@ -389,6 +388,16 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE, 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,
],
],
'findAccompanyingPeriodsByPerson' => [ 'findAccompanyingPeriodsByPerson' => [
'path' => '/by-person/{person_id}.{_format}', 'path' => '/by-person/{person_id}.{_format}',
'controller_action' => 'getAccompanyingPeriodsByPerson', 'controller_action' => 'getAccompanyingPeriodsByPerson',

View File

@ -49,6 +49,10 @@ use UnexpectedValueException;
* "accompanying_period": AccompanyingPeriod::class * "accompanying_period": AccompanyingPeriod::class
* }) * })
* @Assert\GroupSequenceProvider * @Assert\GroupSequenceProvider
* @Assert\Expression(
* "this.isConfidential and this.getUser === NULL",
* message="If the accompanying course is confirmed and confidential, a referrer must remain assigned."
* )
*/ */
class AccompanyingPeriod implements class AccompanyingPeriod implements
TrackCreationInterface, TrackCreationInterface,

View File

@ -21,6 +21,7 @@
<script> <script>
import { mapState } from 'vuex'; import { mapState } from 'vuex';
export default { export default {
name: "ToggleFlags", name: "ToggleFlags",
computed: { computed: {
@ -28,6 +29,7 @@ export default {
intensity: state => state.accompanyingCourse.intensity, intensity: state => state.accompanyingCourse.intensity,
emergency: state => state.accompanyingCourse.emergency, emergency: state => state.accompanyingCourse.emergency,
confidential: state => state.accompanyingCourse.confidential, confidential: state => state.accompanyingCourse.confidential,
permissions: state => state.permissions,
}), }),
isRegular() { isRegular() {
return (this.intensity === 'regular') ? true : false; return (this.intensity === 'regular') ? true : false;
@ -37,7 +39,7 @@ export default {
}, },
isConfidential() { isConfidential() {
return (this.confidential) ? true : false; return (this.confidential) ? true : false;
} },
}, },
methods: { methods: {
toggleIntensity() { toggleIntensity() {
@ -73,16 +75,22 @@ export default {
}); });
}, },
toggleConfidential() { toggleConfidential() {
this.$store.dispatch('toggleConfidential', (!this.isConfidential)) this.$store.dispatch('fetchPermissions').then(() => {
.catch(({name, violations}) => { 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') { if (name === 'ValidationException' || name === 'AccessException') {
violations.forEach((violation) => this.$toast.open({message: violation})); violations.forEach((violation) => this.$toast.open({message: violation}));
} else { } else {
this.$toast.open({message: 'An error occurred'}) this.$toast.open({message: 'An error occurred'})
} }
}); });
} },
} },
} }
</script> </script>

View File

@ -37,6 +37,7 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
referrersSuggested: [], referrersSuggested: [],
// all the users available // all the users available
users: [], users: [],
permissions: {}
}, },
getters: { getters: {
isParticipationValid(state) { isParticipationValid(state) {
@ -70,7 +71,14 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
return true; return true;
} }
return false; return false;
},
canTogglePermission(state) {
if (state.permissions.roles) {
return state.permissions.roles['CHILL_PERSON_ACCOMPANYING_PERIOD_TOGGLE_CONFIDENTIAL'];
} }
return false;
},
}, },
mutations: { mutations: {
catchError(state, error) { catchError(state, error) {
@ -201,6 +209,10 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
return u; return u;
}); });
}, },
setPermissions(state, permissions) {
state.permissions = permissions;
// console.log('permissions', state.permissions);
},
updateLocation(state, r) { updateLocation(state, r) {
//console.log('### mutation: set location attributes', r); //console.log('### mutation: set location attributes', r);
state.accompanyingCourse.locationStatus = r.locationStatus; state.accompanyingCourse.locationStatus = r.locationStatus;
@ -625,6 +637,33 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
let users = await getUsers(); let users = await getUsers();
commit('setUsers', users); commit('setUsers', users);
}, },
/**
* By adding more roles to body['roles'], more permissions can be checked.
*/
fetchPermissions({commit}) {
const url = '/api/1.0/main/permissions/info.json';
const body = {
"object": {
"type": "accompanying_period",
"id": id
},
"class": "Chill\\PersonBundle\\Entity\\AccompanyingPeriod",
"roles": [
"CHILL_PERSON_ACCOMPANYING_PERIOD_TOGGLE_CONFIDENTIAL"
]
}
return makeFetch('POST', url, body)
.then((response) => {
commit('setPermissions', response);
return Promise.resolve();
})
.catch((error) => {
commit('catchError', error);
throw error;
})
},
updateLocation({ commit, dispatch }, payload) { updateLocation({ commit, dispatch }, payload) {
//console.log('## action: updateLocation', payload.locationStatusTo); //console.log('## action: updateLocation', payload.locationStatusTo);
const url = `/api/1.0/person/accompanying-course/${payload.targetId}.json`; const url = `/api/1.0/person/accompanying-course/${payload.targetId}.json`;

View File

@ -31,6 +31,7 @@ class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleH
self::EDIT, self::EDIT,
self::DELETE, self::DELETE,
self::FULL, self::FULL,
self::TOGGLE_CONFIDENTIAL_ALL,
]; ];
public const CREATE = 'CHILL_PERSON_ACCOMPANYING_PERIOD_CREATE'; public const CREATE = 'CHILL_PERSON_ACCOMPANYING_PERIOD_CREATE';
@ -53,6 +54,13 @@ class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleH
*/ */
public const SEE_DETAILS = 'CHILL_PERSON_ACCOMPANYING_PERIOD_SEE_DETAILS'; public const SEE_DETAILS = 'CHILL_PERSON_ACCOMPANYING_PERIOD_SEE_DETAILS';
public const TOGGLE_CONFIDENTIAL = 'CHILL_PERSON_ACCOMPANYING_PERIOD_TOGGLE_CONFIDENTIAL';
/**
* Right to toggle confidentiality.
*/
public const TOGGLE_CONFIDENTIAL_ALL = 'CHILL_PERSON_ACCOMPANYING_PERIOD_TOGGLE_CONFIDENTIAL_ALL';
private Security $security; private Security $security;
private VoterHelperInterface $voterHelper; private VoterHelperInterface $voterHelper;
@ -65,7 +73,7 @@ class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleH
$this->voterHelper = $voterHelperFactory $this->voterHelper = $voterHelperFactory
->generate(self::class) ->generate(self::class)
->addCheckFor(null, [self::CREATE]) ->addCheckFor(null, [self::CREATE])
->addCheckFor(AccompanyingPeriod::class, self::ALL) ->addCheckFor(AccompanyingPeriod::class, [self::TOGGLE_CONFIDENTIAL, ...self::ALL])
->addCheckFor(Person::class, [self::SEE]) ->addCheckFor(Person::class, [self::SEE])
->build(); ->build();
} }
@ -113,6 +121,14 @@ class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleH
return false; return false;
} }
if (self::TOGGLE_CONFIDENTIAL === $attribute) {
if ($subject->getUser() === $token->getUser()) {
return true;
}
return $this->voterHelper->voteOnAttribute(self::TOGGLE_CONFIDENTIAL_ALL, $subject, $token);
}
// if confidential, only the referent can see it // if confidential, only the referent can see it
if ($subject->isConfidential()) { if ($subject->isConfidential()) {
return $token->getUser() === $subject->getUser(); return $token->getUser() === $subject->getUser();

View File

@ -0,0 +1,133 @@
<?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.
*/
namespace Chill\PersonBundle\Tests\AccompanyingPeriod;
use Chill\MainBundle\Entity\Center;
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
* @coversNothing
*/
class AccompanyingPeriodConfidentialTest extends WebTestCase
{
/**
* Setup before the first test of this class (see phpunit doc).
*/
public static function setUpBeforeClass()
{
static::bootKernel();
}
/**
* Setup before each test method (see phpunit doc).
*/
public function setUp()
{
$this->client = static::createClient([], [
'PHP_AUTH_USER' => 'fred',
'PHP_AUTH_PW' => 'password',
]);
}
public function dataGenerateRandomAccompanyingCourse()
{
$maxGenerated = 3;
$maxResults = $maxGenerated * 8;
static::bootKernel();
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$center = $em->getRepository(Center::class)
->findOneBy(['name' => 'Center A']);
$qb = $em->createQueryBuilder();
$personIds = $qb
->select('p.id')
->distinct(true)
->from(Person::class, 'p')
->join('p.accompanyingPeriodParticipations', 'participation')
->join('participation.accompanyingPeriod', 'ap')
->andWhere(
$qb->expr()->eq('ap.step', ':step')
)
->andWhere(
$qb->expr()->eq('ap.confidential', ':confidential')
)
->setParameter('step', AccompanyingPeriod::STEP_CONFIRMED)
->setParameter('confidential', true)
->setMaxResults($maxResults)
->getQuery()
->getScalarResult();
// create a random order
shuffle($personIds);
$nbGenerated = 0;
while ($nbGenerated < $maxGenerated) {
$id = array_pop($personIds)['id'];
$person = $em->getRepository(Person::class)
->find($id);
$periods = $person->getAccompanyingPeriods();
yield [array_pop($personIds)['id'], $periods[array_rand($periods)]->getId()];
++$nbGenerated;
}
}
/**
* @dataProvider dataGenerateRandomAccompanyingCourse
*/
public function testRemoveUserWhenConfidential(int $periodId)
{
$period = self::$container->get(AccompanyingPeriodRepository::class)
->find($periodId);
$em = static::$kernel->getContainer()->get('doctrine.orm.entity_manager');
$isConfidential = $period->isConfidential();
$step = $period->getStep();
$initialUser = $period->getUser();
$user = new stdClass();
$user->id = 0;
$user->type = 'user';
dump($user);
$this->client->request(
Request::METHOD_PATCH,
sprintf('/api/1.0/person/accompanying-course/%d.json', $periodId),
[], // parameters
[], // files
[], // server parameters
json_encode(['type' => 'accompanying_period', 'user' => $user])
);
$response = $this->client->getResponse();
// if ($isConfidential === true && $step === 'CONFIRMED') {
$this->assertEquals(422, $response->getStatusCode());
// }
$this->assertEquals(200, $response->getStatusCode());
$period = $em->getRepository(AccompanyingPeriod::class)
->find($periodId);
$this->assertEquals($user, $period->getUser());
// assign initial user again
$period->setUser($initialUser);
$em->flush();
}
}

View File

@ -32,7 +32,7 @@ class LocationValidityValidator extends ConstraintValidator
} }
if (!$period instanceof AccompanyingPeriod) { if (!$period instanceof AccompanyingPeriod) {
throw new UnexpectedValueException($value, AccompanyingPeriod::class); throw new UnexpectedValueException($period, AccompanyingPeriod::class);
} }
if ($period->getLocationStatus() === 'person') { if ($period->getLocationStatus() === 'person') {

View File

@ -1114,6 +1114,44 @@ paths:
400: 400:
description: "transition cannot be applyed" description: "transition cannot be applyed"
/1.0/person/accompanying-course/{id}/confidential.json:
post:
tags:
- person
summary: "Toggle confidentiality 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: "Confidentiality toggle"
required: true
content:
application/json:
schema:
type: object
properties:
type:
type: string
enum:
- "accompanying_period"
confidential:
type: boolean
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: /1.0/person/accompanying-course/by-person/{person_id}.json:
get: get:
tags: tags: