mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
Merge branch 'issue297_confidential_toggle' into 'master'
confidential toggle rights See merge request Chill-Projet/chill-bundles!227
This commit is contained in:
commit
58119b3de0
@ -20,6 +20,7 @@ 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
|
* [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
|
* [calendar] for a new rdv: suggest and create on-the-fly locations based on the accompanying course location + location of the suggested parties
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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(
|
||||||
|
@ -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',
|
||||||
|
@ -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,
|
||||||
|
@ -48,6 +48,7 @@ const whoami = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
whoami,
|
whoami,
|
||||||
getSocialIssues,
|
getSocialIssues,
|
||||||
|
@ -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,15 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
toggleConfidential() {
|
toggleConfidential() {
|
||||||
this.$store.dispatch('toggleConfidential', (!this.isConfidential))
|
this.$store.dispatch('fetchPermissions').then(() => {
|
||||||
.catch(({name, violations}) => {
|
if (!this.$store.getters.canTogglePermission) {
|
||||||
if (name === 'ValidationException' || name === 'AccessException') {
|
this.$toast.open({message: "Seul le référent peut modifier la confidentialité"});
|
||||||
violations.forEach((violation) => this.$toast.open({message: violation}));
|
} else {
|
||||||
} else {
|
this.$store.dispatch('toggleConfidential', (!this.isConfidential));
|
||||||
this.$toast.open({message: 'An error occurred'})
|
}
|
||||||
}
|
});
|
||||||
});
|
},
|
||||||
}
|
},
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -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`;
|
||||||
@ -642,12 +681,12 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
|
|||||||
Object.assign(body, location);
|
Object.assign(body, location);
|
||||||
makeFetch('PATCH', url, body)
|
makeFetch('PATCH', url, body)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
commit('updateLocation', {
|
commit('updateLocation', {
|
||||||
location: response.location,
|
location: response.location,
|
||||||
locationStatus: response.locationStatus,
|
locationStatus: response.locationStatus,
|
||||||
personLocation: response.personLocation
|
personLocation: response.personLocation
|
||||||
});
|
});
|
||||||
dispatch('fetchReferrersSuggested');
|
dispatch('fetchReferrersSuggested');
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
commit('catchError', error);
|
commit('catchError', error);
|
||||||
|
@ -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();
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -1113,6 +1113,44 @@ paths:
|
|||||||
description: "OK"
|
description: "OK"
|
||||||
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:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user