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 -->
* [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] 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;
@ -21,7 +22,10 @@ and this project adheres to
* [activity] check ACL on activity list in person context
* [list for accompanying course in person] filter list using ACL
* [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
* [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

View File

@ -70,11 +70,6 @@ parameters:
count: 1
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$#"
count: 1

View File

@ -137,7 +137,7 @@ final class ActivityController extends AbstractController
static fn (ActivityReason $ar): int => $ar->getId()
)
->toArray(),
'type_id' => $activity->getType()->getId(),
'type_id' => $activity->getActivityType()->getId(),
'duration' => $activity->getDurationTime() ? $activity->getDurationTime()->format('U') : null,
'date' => $activity->getDate()->format('Y-m-d'),
'attendee' => $activity->getAttendee(),
@ -194,7 +194,7 @@ final class ActivityController extends AbstractController
$form = $this->createForm(ActivityType::class, $entity, [
'center' => $entity->getCenter(),
'role' => new Role('CHILL_ACTIVITY_UPDATE'),
'activityType' => $entity->getType(),
'activityType' => $entity->getActivityType(),
'accompanyingPeriod' => $accompanyingPeriod,
])->handleRequest($request);
@ -327,7 +327,7 @@ final class ActivityController extends AbstractController
$entity->setAccompanyingPeriod($accompanyingPeriod);
}
$entity->setType($activityType);
$entity->setActivityType($activityType);
$entity->setDate(new DateTime('now'));
if ($request->query->has('activityData')) {
@ -385,7 +385,7 @@ final class ActivityController extends AbstractController
$form = $this->createForm(ActivityType::class, $entity, [
'center' => $entity->getCenter(),
'role' => new Role('CHILL_ACTIVITY_CREATE'),
'activityType' => $entity->getType(),
'activityType' => $entity->getActivityType(),
'accompanyingPeriod' => $accompanyingPeriod,
])->handleRequest($request);
@ -408,7 +408,7 @@ final class ActivityController extends AbstractController
$activity_array = $this->serializer->normalize($entity, 'json', ['groups' => 'read']);
$defaultLocationId = $this->getUser()->getCurrentLocation()->getId();
$defaultLocation = $this->getUser()->getCurrentLocation();
return $this->render($view, [
'person' => $person,
@ -416,7 +416,7 @@ final class ActivityController extends AbstractController
'entity' => $entity,
'form' => $form->createView(),
'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)
{
/** @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;
use Chill\ActivityBundle\Validator\Constraints as ActivityValidator;
use Chill\DocStoreBundle\Entity\Document;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
@ -41,6 +42,7 @@ use Symfony\Component\Serializer\Annotation\SerializedName;
* @DiscriminatorMap(typeProperty="type", mapping={
* "activity": Activity::class
* })
* @ActivityValidator\ActivityValidity
*/
/*
@ -202,8 +204,10 @@ class Activity implements HasCenterInterface, HasScopeInterface, AccompanyingPer
public function addPerson(?Person $person): self
{
if (null !== $person) {
if (!$this->persons->contains($person)) {
$this->persons[] = $person;
}
}
return $this;
}
@ -236,8 +240,10 @@ class Activity implements HasCenterInterface, HasScopeInterface, AccompanyingPer
public function addThirdParty(?ThirdParty $thirdParty): self
{
if (null !== $thirdParty) {
if (!$this->thirdParties->contains($thirdParty)) {
$this->thirdParties[] = $thirdParty;
}
}
return $this;
}
@ -245,8 +251,10 @@ class Activity implements HasCenterInterface, HasScopeInterface, AccompanyingPer
public function addUser(?User $user): self
{
if (null !== $user) {
if (!$this->users->contains($user)) {
$this->users[] = $user;
}
}
return $this;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -2,10 +2,9 @@
<teleport to="#location">
<div class="mb-3 row">
<label class="col-form-label col-sm-4">
{{ $t('activity.location') }}
{{ $t("activity.location") }}
</label>
<div class="col-sm-8">
<VueMultiselect
name="selectLocation"
id="selectLocation"
@ -17,7 +16,10 @@
:placeholder="$t('activity.choose_location')"
:custom-label="customLabel"
:options="locations"
v-model="location">
group-values="locations"
group-label="locationGroup"
v-model="location"
>
</VueMultiselect>
<new-location v-bind:locations="locations"></new-location>
@ -27,49 +29,146 @@
</template>
<script>
import { mapState } from "vuex";
import VueMultiselect from 'vue-multiselect';
import NewLocation from './Location/NewLocation.vue';
import { getLocations } from '../api.js';
import { mapState, mapGetters } from "vuex";
import VueMultiselect from "vue-multiselect";
import NewLocation from "./Location/NewLocation.vue";
import { getLocations, getLocationTypeByDefaultFor } from "../api.js";
export default {
name: "Location",
components: {
NewLocation,
VueMultiselect
VueMultiselect,
},
data() {
return {
locations: []
}
locations: [],
};
},
computed: {
...mapState(['activity']),
...mapState(["activity"]),
...mapGetters(["suggestedEntities"]),
location: {
get() {
return this.activity.location;
},
set(value) {
this.$store.dispatch('updateLocation', value);
}
}
this.$store.dispatch("updateLocation", value);
},
},
},
mounted() {
getLocations().then(response => new Promise(resolve => {
console.log('getLocations', response);
this.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);
getLocations().then(
(results) => {
getLocationTypeByDefaultFor('person').then(
(personLocationType) => {
if (personLocationType) {
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: {
labelAccompanyingCourseLocation(value) {
return `${value.address.text} (${value.locationType.title.fr})`
},
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>

View File

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

View File

@ -7,8 +7,19 @@ import App from './App.vue';
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({
template: `<app></app>`,
template: `<app :hasSocialIssues="hasSocialIssues", :hasLocation="hasLocation", :hasPerson="hasPerson"></app>`,
data() {
return {
hasSocialIssues,
hasLocation,
hasPerson,
};
}
})
.use(store)
.use(i18n)

View File

@ -1,5 +1,6 @@
import 'es6-promise/auto';
import { createStore } from 'vuex';
import { postLocation } from './api';
const debug = process.env.NODE_ENV !== 'production';
//console.log('window.activity', window.activity);
@ -27,7 +28,6 @@ const store = createStore({
},
getters: {
suggestedEntities(state) {
console.log(state.activity);
if (typeof state.activity.accompanyingPeriod === "undefined") {
return [];
}
@ -303,7 +303,33 @@ const store = createStore({
let hiddenLocation = document.getElementById(
"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;
}
commit("updateLocation", value);
},
},

View File

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

View File

@ -29,26 +29,30 @@
{{ form_row(form.socialActions) }}
{% 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 -%}
{{ form_row(form.reasons) }}
{% 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) }}
{% endif %}
{%- if form.thirdParties is defined -%}
{% endif %}
{%- if form.thirdParties is defined -%}
{{ form_widget(form.thirdParties) }}
{% endif %}
{%- if form.users is defined -%}
{% endif %}
{%- if form.users is defined -%}
{{ form_widget(form.users) }}
{% endif %}
<div id="add-persons"></div>
{% endif %}
{% endif %}
<div id="add-persons"></div>
<h2 class="chill-red">{{ 'Activity data'|trans }}</h2>
{%- 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 }}');
});
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>
{{ encore_entry_script_tags('vue_activity') }}
{% endblock %}

View File

@ -67,8 +67,8 @@
<dd>
{% if entity.location is not null %}
<p>
<span>{{ entity.location.locationType.title|localize_translatable_string }}</span>
{{ entity.location.name }}
<span> ({{ entity.location.locationType.title|localize_translatable_string }})</span>
</p>
{{ entity.location.address|chill_entity_render_box }}
{% 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\:
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
ActivityType list: Types d'activités
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
User visible: Visibilté du champ Utilisateur
User visible: Visibilité 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
Place visible: Visibilté du champ Lieu
Place visible: Visibilité 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
Duration time visible: Visibilté du champ Durée
Duration time visible: Visibilité 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
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
Reasons visible: Visibilté du champ Sujet
Reasons visible: Visibilité 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
Emergency visible: Visibilté du champ Urgent
Emergency visible: Visibilité 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
Social data visible: Visibilté du champ Données sociales
Social data label: Libellé du champ Données sociales
Users visible: Visibilté du champ Utilisateurs
Social issues visible: Visibilité du champ Problématiques sociales
Social issues label: Libellé du champ Problématiques sociales
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
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
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
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(
static fn (Person $p): int => $p->getId(),
$entity->getPersons()
$entity->getPersons()->toArray()
);
$professionalsId = array_map(
static fn (ThirdParty $thirdParty): ?int => $thirdParty->getId(),
$entity->getProfessionals()
$entity->getProfessionals()->toArray()
);
$durationTime = $entity->getEndDate()->diff($entity->getStartDate());

View File

@ -1,5 +1,6 @@
import 'es6-promise/auto';
import { createStore } from 'vuex';
import { postLocation } from 'ChillActivityAssets/vuejs/Activity/api';
const debug = process.env.NODE_ENV !== 'production';
@ -33,7 +34,6 @@ const store = createStore({
},
getters: {
suggestedEntities(state) {
console.log(state.activity)
if (typeof(state.activity.accompanyingPeriod) === 'undefined') {
return [];
}
@ -189,8 +189,35 @@ const store = createStore({
updateLocation({ commit }, value) {
console.log('### action: updateLocation', value);
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;
commit('updateLocation', value);
}
commit("updateLocation", value);
}
}

View File

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

View File

@ -10,8 +10,6 @@
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController;
use DateInterval;
use DateTime;
use Symfony\Component\HttpFoundation\Request;
/**
@ -21,22 +19,11 @@ class LocationApiController extends ApiController
{
public function customizeQuery(string $action, Request $request, $query): void
{
$query->andWhere($query->expr()->orX(
$query->expr()->andX(
$query->expr()->eq('e.createdBy', ':user'),
$query->expr()->gte('e.createdAt', ':dateBefore')
),
$query->andWhere(
$query->expr()->andX(
$query->expr()->eq('e.availableForUsers', "'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 Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
@ -20,9 +21,14 @@ use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
* @DiscriminatorMap(typeProperty="type", mapping={
* "location-type": LocationType::class
* })
* @UniqueEntity({"defaultFor"})
*/
class LocationType
{
public const DEFAULT_FOR_3PARTY = 'thirdparty';
public const DEFAULT_FOR_PERSON = 'person';
public const STATUS_NEVER = 'never';
public const STATUS_OPTIONAL = 'optional';
@ -53,6 +59,12 @@ class LocationType
*/
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\GeneratedValue
@ -87,6 +99,11 @@ class LocationType
return $this->contactData;
}
public function getDefaultFor(): ?string
{
return $this->defaultFor;
}
public function getId(): ?int
{
return $this->id;
@ -125,6 +142,13 @@ class LocationType
return $this;
}
public function setDefaultFor(?string $defaultFor): self
{
$this->defaultFor = $defaultFor;
return $this;
}
public function setTitle(array $title): self
{
$this->title = $title;

View File

@ -71,6 +71,18 @@ final class LocationTypeType extends AbstractType
],
'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
})
.then(response => {
if (response.ok) {
return response.json();
}

View File

@ -11,6 +11,7 @@
<th>{{ 'Address required'|trans }}</th>
<th>{{ 'Contact data'|trans }}</th>
<th>{{ 'Active'|trans }}</th>
<th>{{ 'Default for'|trans }}</th>
</tr>
</thead>
<tbody>
@ -33,6 +34,7 @@
<i class="fa fa-square-o"></i>
{%- endif -%}
</td>
<td>{{ entity.defaultFor|trans }}</td>
<td>
<ul class="record_actions">
<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
Phonenumber2: Autre numéro de téléphone
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
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
{
return $this->addRemoveSomething(

View File

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

View File

@ -49,6 +49,10 @@ use UnexpectedValueException;
* "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."
* )
*/
class AccompanyingPeriod implements
TrackCreationInterface,

View File

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

View File

@ -37,6 +37,7 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
referrersSuggested: [],
// all the users available
users: [],
permissions: {}
},
getters: {
isParticipationValid(state) {
@ -70,7 +71,14 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
return true;
}
return false;
},
canTogglePermission(state) {
if (state.permissions.roles) {
return state.permissions.roles['CHILL_PERSON_ACCOMPANYING_PERIOD_TOGGLE_CONFIDENTIAL'];
}
return false;
},
},
mutations: {
catchError(state, error) {
@ -201,6 +209,10 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
return u;
});
},
setPermissions(state, permissions) {
state.permissions = permissions;
// console.log('permissions', state.permissions);
},
updateLocation(state, r) {
//console.log('### mutation: set location attributes', r);
state.accompanyingCourse.locationStatus = r.locationStatus;
@ -625,6 +637,33 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
let users = await getUsers();
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) {
//console.log('## action: updateLocation', payload.locationStatusTo);
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::DELETE,
self::FULL,
self::TOGGLE_CONFIDENTIAL_ALL,
];
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 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 VoterHelperInterface $voterHelper;
@ -65,7 +73,7 @@ class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleH
$this->voterHelper = $voterHelperFactory
->generate(self::class)
->addCheckFor(null, [self::CREATE])
->addCheckFor(AccompanyingPeriod::class, self::ALL)
->addCheckFor(AccompanyingPeriod::class, [self::TOGGLE_CONFIDENTIAL, ...self::ALL])
->addCheckFor(Person::class, [self::SEE])
->build();
}
@ -113,6 +121,14 @@ class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleH
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 ($subject->isConfidential()) {
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) {
throw new UnexpectedValueException($value, AccompanyingPeriod::class);
throw new UnexpectedValueException($period, AccompanyingPeriod::class);
}
if ($period->getLocationStatus() === 'person') {

View File

@ -1114,6 +1114,44 @@ paths:
400:
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:
get:
tags: