Merge remote-tracking branch 'origin/master' into 106_tasks_to_parcours

This commit is contained in:
2021-10-22 17:58:26 +02:00
19 changed files with 379 additions and 62 deletions

View File

@@ -0,0 +1,26 @@
<?php
namespace Chill\PersonBundle\AccompanyingPeriod\Suggestion;
use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
/**
* Basic implementation: this suggestion does not return any suggestion
*/
final class ReferralsSuggestion implements ReferralsSuggestionInterface
{
public function countReferralSuggested(AccompanyingPeriod $period, ?array $options = []): int
{
return 0;
}
/**
* @param AccompanyingPeriod $period
* @return array|User[]
*/
public function findReferralSuggested(AccompanyingPeriod $period, int $limit = 50, int $start = 0): array
{
return [];
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Chill\PersonBundle\AccompanyingPeriod\Suggestion;
use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
/**
* Process the suggestion of referral for a given accompanying period
*/
interface ReferralsSuggestionInterface
{
public function countReferralSuggested(AccompanyingPeriod $period, ?array $options = []): int;
/**
* @return array|User[]
*/
public function findReferralSuggested(AccompanyingPeriod $period, int $limit = 50, int $start = 0): array;
}

View File

@@ -3,12 +3,18 @@
namespace Chill\PersonBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Chill\MainBundle\Serializer\Model\Collection;
use Chill\PersonBundle\AccompanyingPeriod\Suggestion\ReferralsSuggestionInterface;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Chill\PersonBundle\Privacy\AccompanyingPeriodPrivacyEvent;
use Chill\PersonBundle\Entity\Person;
@@ -28,19 +34,23 @@ class AccompanyingCourseApiController extends ApiController
private Registry $registry;
private ReferralsSuggestionInterface $referralAvailable;
public function __construct(
EventDispatcherInterface $eventDispatcher,
ValidatorInterface $validator,
Registry $registry
Registry $registry,
ReferralsSuggestionInterface $referralAvailable
) {
$this->eventDispatcher = $eventDispatcher;
$this->validator = $validator;
$this->registry = $registry;
$this->referralAvailable = $referralAvailable;
}
public function confirmApi($id, Request $request, $_format): Response
{
/** @var AccompanyingPeriod $accompanyingPeriod */
/** @var AccompanyingPeriod $accompanyingPeriod */
$accompanyingPeriod = $this->getEntity('participation', $id, $request);
$this->checkACL('confirm', $request, $_format, $accompanyingPeriod);
@@ -58,10 +68,10 @@ $workflow = $this->registry->get($accompanyingPeriod);
'groups' => [ 'read' ]
]);
}
public function participationApi($id, Request $request, $_format)
{
/** @var AccompanyingPeriod $accompanyingPeriod */
/** @var AccompanyingPeriod $accompanyingPeriod */
$accompanyingPeriod = $this->getEntity('participation', $id, $request);
$person = $this->getSerializer()
->deserialize($request->getContent(), Person::class, $_format, []);
@@ -152,7 +162,7 @@ $workflow = $this->registry->get($accompanyingPeriod);
->deserialize($request->getContent(), $class, $_format, []);
} catch (RuntimeException $e) {
$exceptions[] = $e;
}
}
}
if ($requestor === null) {
throw new BadRequestException('Could not find any person or requestor', 0, $exceptions[0]);
@@ -187,4 +197,29 @@ $workflow = $this->registry->get($accompanyingPeriod);
return null;
}
/**
* @Route("/api/1.0/person/accompanying-course/{id}/referrers-suggested.{_format}",
* requirements={ "_format"="json"},
* name="chill_api_person_accompanying_period_referrers_suggested")
* @param AccompanyingPeriod $period
* @return JsonResponse
*/
public function suggestReferrals(AccompanyingPeriod $period, string $_format = 'json'): JsonResponse
{
$this->denyAccessUnlessGranted(AccompanyingPeriodVoter::EDIT, $period);
$total = $this->referralAvailable->countReferralSuggested($period);
$paginator = $this->getPaginatorFactory()->create($total);
if (0 < $total) {
$users = $this->referralAvailable->findReferralSuggested($period, $paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber());
} else {
$users = [];
}
return $this->json(new Collection($users, $paginator), Response::HTTP_OK,
[], [ AbstractNormalizer::GROUPS => [ 'read' ]]);
}
}

View File

@@ -1,3 +1,5 @@
import { fetchResults } from 'ChillMainAssets/lib/api/download.js';
/*
* Endpoint v.2 chill_api_single_accompanying_course__entity
* method GET/HEAD, get AccompanyingCourse Instance
@@ -168,11 +170,8 @@ const postSocialIssue = (id, body, method) => {
const getUsers = () => {
const url = `/api/1.0/main/user.json`;
return fetch(url)
.then(response => {
if (response.ok) { return response.json(); }
throw { msg: 'Error while retriving users.', sta: response.status, txt: response.statusText, err: new Error(), body: response.body };
});
return fetchResults(url);
};
const whoami = () => {
@@ -216,8 +215,6 @@ const addScope = (id, scope) => {
const removeScope = (id, scope) => {
const url = `/api/1.0/person/accompanying-course/${id}/scope.json`;
console.log(url);
console.log(scope);
return fetch(url, {
method: 'DELETE',
@@ -235,6 +232,12 @@ const removeScope = (id, scope) => {
});
};
const getReferrersSuggested = (course) => {
const url = `/api/1.0/person/accompanying-course/${course.id}/referrers-suggested.json`;
return fetchResults(url);
}
export {
getAccompanyingCourse,
patchAccompanyingCourse,
@@ -249,4 +252,5 @@ export {
postSocialIssue,
addScope,
removeScope,
getReferrersSuggested,
};

View File

@@ -15,9 +15,20 @@
v-bind:searchable="true"
v-bind:placeholder="$t('referrer.placeholder')"
v-model="value"
v-bind:options="options"
v-bind:options="users"
@select="updateReferrer">
</VueMultiselect>
<template v-if="referrersSuggested.length > 0">
<ul>
<li v-for="u in referrersSuggested" @click="updateReferrer(u)">
<user-render-box-badge :user="u"></user-render-box-badge>
</li>
</ul>
</template>
</div>
<div>
@@ -41,30 +52,29 @@
import VueMultiselect from 'vue-multiselect';
import { getUsers, whoami } from '../api';
import { mapState } from 'vuex';
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge";
export default {
name: "Referrer",
components: { VueMultiselect },
data() {
return {
options: []
}
components: {
UserRenderBoxBadge,
VueMultiselect,
},
computed: {
...mapState({
value: state => state.accompanyingCourse.user,
users: state => state.users,
referrersSuggested: state => {
return state.referrersSuggested.filter(u => {
if (null === state.accompanyingCourse.user) {
return true;
}
return state.accompanyingCourse.user.id !== u.id;
})
},
}),
},
mounted() {
this.getOptions();
},
methods: {
getOptions() {
getUsers().then(response => new Promise((resolve, reject) => {
this.options = response.results;
resolve();
}));
},
updateReferrer(value) {
//console.log('value', value);
this.$store.dispatch('updateReferrer', value);

View File

@@ -10,6 +10,8 @@ import { getAccompanyingCourse,
postSocialIssue,
addScope,
removeScope,
getReferrersSuggested,
getUsers,
} from '../api';
import { patchPerson } from "ChillPersonAssets/vuejs/_api/OnTheFly";
import { patchThirdparty } from "ChillThirdPartyAssets/vuejs/_api/OnTheFly";
@@ -38,6 +40,10 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
scopesAtStart: accompanyingCourse.scopes.map(scope => scope),
// the scope states at server side
scopesAtBackend: accompanyingCourse.scopes.map(scope => scope),
// the users which are available for referrer
referrersSuggested: [],
// all the users available
users: [],
},
getters: {
isParticipationValid(state) {
@@ -170,6 +176,16 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
//console.log('value', value);
state.accompanyingCourse.user = value;
},
setReferrersSuggested(state, users) {
state.referrersSuggested = users.map(u => {
if (state.accompanyingCourse.user !== null) {
if (state.accompanyingCourse.user.id === u.id) {
return state.accompanyingCourse.user;
}
}
return u;
});
},
confirmAccompanyingCourse(state, response) {
//console.log('### mutation: confirmAccompanyingCourse: response', response);
state.accompanyingCourse.step = response.step;
@@ -178,6 +194,16 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
//console.log('define location context');
state.addressContext = context;
},
setUsers(state, users) {
state.users = users.map(u => {
if (state.accompanyingCourse.user !== null) {
if (state.accompanyingCourse.user.id === u.id) {
return state.accompanyingCourse.user;
}
}
return u;
});
},
updateLocation(state, r) {
//console.log('### mutation: set location attributes', r);
state.accompanyingCourse.locationStatus = r.locationStatus;
@@ -272,6 +298,7 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
})).catch((error) => { commit('catchError', error) });
},
patchOnTheFly({ commit }, payload) {
// TODO should be into the dedicated component, no ? JF
console.log('## action: patch OnTheFly', payload);
let body = { type: payload.type };
if (payload.type === 'person') {
@@ -313,10 +340,12 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
},
toggleEmergency({ commit }, payload) {
patchAccompanyingCourse(id, { type: "accompanying_period", emergency: payload })
.then(course => new Promise((resolve, reject) => {
commit('toggleEmergency', course.emergency);
resolve();
})).catch((error) => { commit('catchError', error) });
.then(course => new Promise((resolve, reject) => {
commit('toggleEmergency', course.emergency);
return dispatch('setRefererresAvailable');
}))
.then(() => Promise.resolve())
.catch((error) => { commit('catchError', error) });
},
toggleConfidential({ commit }, payload) {
patchAccompanyingCourse(id, { type: "accompanying_period", confidential: payload })
@@ -387,11 +416,15 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
// check/uncheck in the UI. I do not know of to avoid it.
commit('setScopes', scopes);
return Promise.resolve();
}).then(() => {
return dispatch('fetchReferrersSuggested');
});
} else {
return dispatch('setScopes', state.scopesAtStart).then(() => {
commit('setScopes', scopes);
return Promise.resolve();
}).then(() => {
return dispatch('fetchReferrersSuggested');
});
}
},
@@ -434,7 +467,7 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
resolve();
})).catch((error) => { commit('catchError', error) });
},
updateSocialIssues({ state, commit }, { payload, body, method }) {
updateSocialIssues({ state, commit, dispatch }, { payload, body, method }) {
//console.log('## action: payload', { payload, body, method });
postSocialIssue(id, body, method)
.then(response => new Promise((resolve, reject) => {
@@ -445,6 +478,7 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
return getAccompanyingCourse(state.accompanyingCourse.id);
}).then(accompanying_course => {
commit('refreshSocialIssues', accompanying_course.socialIssues);
dispatch('fetchReferrersSuggested');
})
.catch((error) => { commit('catchError', error) });
},
@@ -462,7 +496,24 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
resolve();
})).catch((error) => { commit('catchError', error) });
},
updateLocation({ commit }, payload) {
async fetchReferrersSuggested({ state, commit}) {
let users = await getReferrersSuggested(state.accompanyingCourse);
commit('setReferrersSuggested', users);
if (
null === state.accompanyingCourse.user
&& !state.accompanyingCourse.confidential
&& !state.accompanyingCourse.step === 'DRAFT'
&& users.length === 1
) {
// set the user if unique
commit('updateReferrer', users[0]);
}
},
async fetchUsers({commit}) {
let users = await getUsers();
commit('setUsers', users);
},
updateLocation({ commit, dispatch }, payload) {
//console.log('## action: updateLocation', payload.locationStatusTo);
let body = { 'type': payload.target, 'id': payload.targetId };
let location = {};
@@ -484,6 +535,7 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
personLocation: accompanyingCourse.personLocation
});
resolve();
dispatch('fetchReferrersSuggested');
})).catch((error) => { commit('catchError', error) });
},
confirmAccompanyingCourse({ commit }) {
@@ -498,6 +550,9 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
}
}
});
store.dispatch('fetchReferrersSuggested');
store.dispatch('fetchUsers');
resolve(store);
}));

View File

@@ -325,6 +325,19 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
$this->period = $period;
}
/**
* @dataProvider dataGenerateRandomAccompanyingCourse
*/
public function testReferralAvailable(int $personId, int $periodId)
{
$this->client->request(
Request::METHOD_POST,
sprintf('/api/1.0/person/accompanying-course/%d/referrers-suggested.json', $periodId)
);
$this->assertEquals(200, $this->client->getResponse()->getStatusCode());
}
/**
*
* @dataProvider dataGenerateRandomAccompanyingCourse

View File

@@ -445,7 +445,7 @@ paths:
/1.0/person/accompanying-course/{id}.json:
get:
tags:
- person
- accompanying-course
summary: "Return the description for an accompanying course (accompanying period)"
parameters:
- name: id
@@ -532,7 +532,7 @@ paths:
/1.0/person/accompanying-course/{id}/requestor.json:
post:
tags:
- person
- accompanying-course
summary: "Add a requestor to the accompanying course"
parameters:
- name: id
@@ -574,7 +574,7 @@ paths:
description: "object with validation errors"
delete:
tags:
- person
- accompanying-course
summary: "Remove the requestor for the accompanying course"
parameters:
- name: id
@@ -598,7 +598,7 @@ paths:
/1.0/person/accompanying-course/{id}/participation.json:
post:
tags:
- person
- accompanying-course
summary: "Add a participant to the accompanying course"
parameters:
- name: id
@@ -627,7 +627,7 @@ paths:
description: "object with validation errors"
delete:
tags:
- person
- accompanying-course
summary: "Remove the participant for the accompanying course"
parameters:
- name: id
@@ -658,7 +658,7 @@ paths:
/1.0/person/accompanying-course/{id}/resource.json:
post:
tags:
- person
- accompanying-course
summary: "Add a resource to the accompanying course"
parameters:
- name: id
@@ -703,7 +703,7 @@ paths:
description: "object with validation errors"
delete:
tags:
- person
- accompanying-course
summary: "Remove the resource"
parameters:
- name: id
@@ -734,7 +734,7 @@ paths:
/1.0/person/accompanying-course/{id}/comment.json:
post:
tags:
- person
- accompanying-course
summary: "Add a comment to the accompanying course"
parameters:
- name: id
@@ -772,7 +772,7 @@ paths:
description: "object with validation errors"
delete:
tags:
- person
- accompanying-course
summary: "Remove the comment"
parameters:
- name: id
@@ -803,7 +803,7 @@ paths:
/1.0/person/accompanying-course/{id}/scope.json:
post:
tags:
- person
- accompanying-course
summary: "Add a scope to the accompanying course"
parameters:
- name: id
@@ -837,7 +837,7 @@ paths:
description: "object with validation errors"
delete:
tags:
- person
- accompanying-course
summary: "Remove the scope"
parameters:
- name: id
@@ -868,7 +868,7 @@ paths:
/1.0/person/accompanying-course/{id}/socialissue.json:
post:
tags:
- person
- accompanying-course
summary: "Add a social issue to the accompanying course"
parameters:
- name: id
@@ -902,7 +902,7 @@ paths:
description: "object with validation errors"
delete:
tags:
- person
- accompanying-course
summary: "Remove the social issue"
parameters:
- name: id
@@ -929,11 +929,31 @@ paths:
description: "OK"
422:
description: "object with validation errors"
/1.0/person/accompanying-course/{id}/referrers-suggested.json:
get:
tags:
- accompanying-course
summary: "get a list of available referral for a given accompanying cours"
parameters:
- name: id
in: path
required: true
description: The accompanying period's id
schema:
type: integer
format: integer
minimum: 1
responses:
401:
description: "Unauthorized"
404:
description: "Not found"
200:
description: "OK"
/1.0/person/accompanying-course/{id}/work.json:
post:
tags:
- person
- accompanying-course-work
summary: "Add a work (AccompanyingPeriodwork) to the accompanying course"
parameters:

View File

@@ -16,5 +16,5 @@ module.exports = function(encore, entries)
encore.addEntry('page_household_edit_metadata', __dirname + '/Resources/public/page/household_edit_metadata/index.js');
encore.addEntry('page_person', __dirname + '/Resources/public/page/person/index.js');
encore.addEntry('page_accompanying_course_index_person_locate', __dirname + '/Resources/public/page/accompanying_course_index/person_locate.js');
encore.addEntry('page_vis', __dirname + '/Resources/public/page/vis/index.js');
//encore.addEntry('page_vis', __dirname + '/Resources/public/page/vis/index.js');
};

View File

@@ -13,3 +13,10 @@ services:
entity: 'Chill\PersonBundle\Entity\AccompanyingPeriod'
lazy: true
method: preUpdateAccompanyingPeriod
Chill\PersonBundle\AccompanyingPeriod\Suggestion\ReferralsSuggestion:
autowire: true
autoconfigure: true
Chill\PersonBundle\AccompanyingPeriod\Suggestion\ReferralsSuggestionInterface: '@Chill\PersonBundle\AccompanyingPeriod\Suggestion\ReferralsSuggestion'

View File

@@ -41,10 +41,8 @@ services:
tags: ['controller.service_arguments']
Chill\PersonBundle\Controller\AccompanyingCourseApiController:
arguments:
$eventDispatcher: '@Symfony\Contracts\EventDispatcher\EventDispatcherInterface'
$validator: '@Symfony\Component\Validator\Validator\ValidatorInterface'
$registry: '@Symfony\Component\Workflow\Registry'
autoconfigure: true
autowire: true
tags: ['controller.service_arguments']
Chill\PersonBundle\Controller\PersonApiController: