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

This commit is contained in:
2021-11-22 17:14:11 +01:00
118 changed files with 3939 additions and 1367 deletions

View File

@@ -298,16 +298,19 @@ final class ActivityController extends AbstractController
$activity_array = $this->serializer->normalize($entity, 'json', ['groups' => 'read']);
$defaultLocationId = $this->getUser()->getCurrentLocation()->getId();
return $this->render($view, [
'person' => $person,
'accompanyingCourse' => $accompanyingPeriod,
'entity' => $entity,
'form' => $form->createView(),
'activity_json' => $activity_array
'activity_json' => $activity_array,
'default_location_id' => $defaultLocationId
]);
}
public function showAction(Request $request, $id): Response
public function showAction(Request $request, int $id): Response
{
$view = null;
@@ -361,7 +364,7 @@ final class ActivityController extends AbstractController
/**
* Displays a form to edit an existing Activity entity.
*/
public function editAction($id, Request $request): Response
public function editAction(int $id, Request $request): Response
{
$view = null;

View File

@@ -36,6 +36,8 @@ use Chill\MainBundle\DataFixtures\ORM\LoadScopes;
class LoadActivity extends AbstractFixture implements OrderedFixtureInterface
{
use \Symfony\Component\DependencyInjection\ContainerAwareTrait;
/**
* @var \Faker\Generator
*/
@@ -80,15 +82,10 @@ class LoadActivity extends AbstractFixture implements OrderedFixtureInterface
*
* @return \Chill\ActivityBundle\Entity\ActivityReason
*/
private function getRandomActivityReason(array $excludingIds)
private function getRandomActivityReason()
{
$reasonRef = LoadActivityReason::$references[array_rand(LoadActivityReason::$references)];
if (in_array($this->getReference($reasonRef)->getId(), $excludingIds, true)) {
// we have a reason which should be excluded. Find another...
return $this->getRandomActivityReason($excludingIds);
}
return $this->getReference($reasonRef);
}
@@ -103,7 +100,7 @@ class LoadActivity extends AbstractFixture implements OrderedFixtureInterface
return $this->getReference($userRef);
}
public function newRandomActivity($person)
public function newRandomActivity($person): ?Activity
{
$activity = (new Activity())
->setUser($this->getRandomUser())
@@ -116,11 +113,13 @@ class LoadActivity extends AbstractFixture implements OrderedFixtureInterface
// ->setAttendee($this->faker->boolean())
$usedId = array();
for ($i = 0; $i < rand(0, 4); $i++) {
$reason = $this->getRandomActivityReason($usedId);
$usedId[] = $reason->getId();
$activity->addReason($reason);
$reason = $this->getRandomActivityReason();
if (null !== $reason) {
$activity->addReason($reason);
} else {
return null;
}
}
return $activity;
@@ -137,7 +136,9 @@ class LoadActivity extends AbstractFixture implements OrderedFixtureInterface
for ($i = 0; $i < $activityNbr; $i ++) {
$activity = $this->newRandomActivity($person);
$manager->persist($activity);
if (null !== $activity) {
$manager->persist($activity);
}
}
}
$manager->flush();

View File

@@ -22,6 +22,7 @@ use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
use Symfony\Component\Serializer\Annotation\SerializedName;
/**
* Class Activity
@@ -102,8 +103,11 @@ class Activity implements HasCenterInterface, HasScopeInterface, AccompanyingPer
/**
* @ORM\ManyToOne(targetEntity="Chill\ActivityBundle\Entity\ActivityType")
* @Groups({"read"})
* @SerializedName("activityType")
* @ORM\JoinColumn(name="type_id")
*/
private ActivityType $type;
private ActivityType $activityType;
/**
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\Scope")
@@ -309,19 +313,34 @@ class Activity implements HasCenterInterface, HasScopeInterface, AccompanyingPer
return $this;
}
public function setType(ActivityType $type): self
public function setActivityType(ActivityType $activityType): self
{
$this->type = $type;
$this->activityType = $activityType;
return $this;
}
public function getActivityType(): ActivityType
{
return $this->activityType;
}
/**
* @deprecated
*/
public function setType(ActivityType $activityType): self
{
$this->activityType = $activityType;
return $this;
}
/**
* @deprecated
*/
public function getType(): ActivityType
{
return $this->type;
return $this->activityType;
}
public function setScope(Scope $scope): self

View File

@@ -21,6 +21,7 @@
namespace Chill\ActivityBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* Class ActivityType
@@ -45,11 +46,13 @@ class ActivityType
/**
* @ORM\Column(type="json")
* @Groups({"read"})
*/
private array $name = [];
/**
* @ORM\Column(type="boolean")
* @Groups({"read"})
*/
private bool $active = true;
@@ -100,6 +103,7 @@ class ActivityType
/**
* @ORM\Column(type="smallint", nullable=false, options={"default"=1})
* @Groups({"read"})
*/
private int $personsVisible = self::FIELD_OPTIONAL;
@@ -110,6 +114,7 @@ class ActivityType
/**
* @ORM\Column(type="smallint", nullable=false, options={"default"=1})
* @Groups({"read"})
*/
private int $thirdPartiesVisible = self::FIELD_INVISIBLE;
@@ -190,6 +195,7 @@ class ActivityType
/**
* @ORM\Column(type="smallint", nullable=false, options={"default"=1})
* @Groups({"read"})
*/
private int $usersVisible = self::FIELD_OPTIONAL;

View File

@@ -1,5 +1,5 @@
<template>
<teleport to="#add-persons">
<teleport to="#add-persons" v-if="isComponentVisible">
<div class="flex-bloc concerned-groups" :class="getContext">
<persons-bloc
@@ -9,11 +9,13 @@
v-bind:setPersonsInBloc="setPersonsInBloc">
</persons-bloc>
</div>
<div v-if="getContext === 'accompanyingCourse' && filterSuggestedPersons.length > 0">
<ul>
<li v-for="p in filterSuggestedPersons" @click="addNewPerson(p)">
{{ p.text }}
<div v-if="getContext === 'accompanyingCourse' && suggestedEntities.length > 0">
<ul class="list-unstyled">
<li v-for="p in suggestedEntities" @click="addSuggestedEntity(p)">
<span class="badge bg-primary" style="cursor: pointer;">
<i class="fa fa-plus fa-fw text-success"></i>
{{ p.text }}
</span>
</li>
</ul>
</div>
@@ -22,7 +24,7 @@
buttonTitle="activity.add_persons"
modalTitle="activity.add_persons"
v-bind:key="addPersons.key"
v-bind:options="addPersons.options"
v-bind:options="addPersonsOptions"
@addNewPersons="addNewPersons"
ref="addPersons">
</add-persons>
@@ -52,38 +54,35 @@ export default {
{ key: 'personsAssociated',
title: 'activity.bloc_persons_associated',
persons: [],
included: false
included: window.activity ? window.activity.activityType.personsVisible !== 0 : true
},
{ key: 'personsNotAssociated',
title: 'activity.bloc_persons_not_associated',
persons: [],
included: false
included: window.activity ? window.activity.activityType.personsVisible !== 0 : true
},
{ key: 'thirdparty',
title: 'activity.bloc_thirdparty',
persons: [],
included: true
included: window.activity ? window.activity.activityType.thirdPartiesVisible !== 0 : true
},
{ key: 'users',
title: 'activity.bloc_users',
persons: [],
included: true
included: window.activity ? window.activity.activityType.usersVisible !== 0 : true
},
],
addPersons: {
key: 'activity',
options: {
type: ['person', 'thirdparty', 'user'],
priority: null,
uniq: false,
button: {
size: 'btn-sm'
}
}
key: 'activity'
}
}
},
computed: {
isComponentVisible() {
return window.activity
? (window.activity.activityType.personsVisible !== 0 || window.activity.activityType.thirdPartiesVisible !== 0 || window.activity.activityType.usersVisible !== 0)
: true
},
...mapState({
persons: state => state.activity.persons,
thirdParties: state => state.activity.thirdParties,
@@ -91,14 +90,38 @@ export default {
accompanyingCourse: state => state.activity.accompanyingPeriod
}),
...mapGetters([
'filterSuggestedPersons'
'suggestedEntities'
]),
getContext() {
return (this.accompanyingCourse) ? "accompanyingCourse" : "person";
},
contextPersonsBlocs() {
return this.personsBlocs.filter(bloc => bloc.included !== false);
}
},
addPersonsOptions() {
let optionsType = [];
if (window.activity) {
if (window.activity.activityType.personsVisible !== 0) {
optionsType.push('person')
}
if (window.activity.activityType.thirdPartiesVisible !== 0) {
optionsType.push('thirdparty')
}
if (window.activity.activityType.usersVisible !== 0) {
optionsType.push('user')
}
} else {
optionsType = ['person', 'thirdparty', 'user'];
}
return {
type: optionsType,
priority: null,
uniq: false,
button: {
size: 'btn-sm'
}
}
},
},
mounted() {
this.setPersonsInBloc();
@@ -168,7 +191,7 @@ export default {
},
addNewPersons({ selected, modal }) {
console.log('@@@ CLICK button addNewPersons', selected);
selected.forEach(function(item) {
selected.forEach((item) => {
this.$store.dispatch('addPersonsInvolved', item);
}, this
);
@@ -176,7 +199,7 @@ export default {
modal.showModal = false;
this.setPersonsInBloc();
},
addNewPerson(person) {
addSuggestedEntity(person) {
this.$store.dispatch('addPersonsInvolved', { result: person, type: 'person' });
this.setPersonsInBloc();
},

View File

@@ -4,7 +4,7 @@
<span class="chill_denomination">
{{ textCutted }}
</span>
<a class="fa fa-fw fa-times"
<a class="fa fa-fw fa-times text-danger text-decoration-none"
@click.prevent="$emit('remove', person)">
</a>
</span>

View File

@@ -55,16 +55,17 @@ export default {
}
},
mounted() {
this.getLocationsList();
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);
}
resolve();
}))
},
methods: {
getLocationsList() {
getLocations().then(response => new Promise(resolve => {
console.log('getLocations', response);
this.locations = response.results;
resolve();
}))
},
customLabel(value) {
return `${value.locationType.title.fr} ${value.name ? value.name : ''}`;
}

View File

@@ -19,172 +19,294 @@ const removeIdFromValue = (string, id) => {
};
const store = createStore({
strict: debug,
state: {
activity: window.activity,
socialIssuesOther: [],
socialActionsList: [],
},
getters: {
filterSuggestedPersons(state) {
if (typeof(state.activity.accompanyingPeriod) === 'undefined') {
return [];
}
let existingPersonIds = state.activity.persons.map(p => p.id);
return state.activity.accompanyingPeriod.participations.filter(p => p.endDate === null)
.map(p => p.person)
.filter(p => !existingPersonIds.includes(p.id))
}
},
mutations: {
// SocialIssueAcc
addIssueInList(state, issue) {
//console.log('add issue list', issue.id);
state.activity.accompanyingPeriod.socialIssues.push(issue);
},
addIssueSelected(state, issue) {
//console.log('add issue selected', issue.id);
state.activity.socialIssues.push(issue);
},
updateIssuesSelected(state, issues) {
//console.log('update issues selected', issues);
state.activity.socialIssues = issues;
},
updateIssuesOther(state, payload) {
//console.log('update issues other');
state.socialIssuesOther = payload;
},
removeIssueInOther(state, issue) {
//console.log('remove issue other', issue.id);
state.socialIssuesOther = state.socialIssuesOther.filter(i => i.id !== issue.id);
},
resetActionsList(state) {
//console.log('reset list actions');
state.socialActionsList = [];
},
addActionInList(state, action) {
//console.log('add action list', action.id);
state.socialActionsList.push(action);
},
updateActionsSelected(state, actions) {
//console.log('update actions selected', actions);
state.activity.socialActions = actions;
},
filterList(state, list) {
const filterList = (list) => {
// remove duplicates entries
list = list.filter((value, index) => list.findIndex(array => array.id === value.id) === index);
// alpha sort
list.sort((a,b) => (a.text > b.text) ? 1 : ((b.text > a.text) ? -1 : 0));
return list;
};
if (list === 'issues') {
state.activity.accompanyingPeriod.socialIssues = filterList(state.activity.accompanyingPeriod.socialIssues);
}
if (list === 'actions') {
state.socialActionsList = filterList(state.socialActionsList);
}
strict: debug,
state: {
activity: window.activity,
socialIssuesOther: [],
socialActionsList: [],
},
getters: {
suggestedEntities(state) {
console.log(state.activity);
if (typeof state.activity.accompanyingPeriod === "undefined") {
return [];
}
const allEntities = [
...store.getters.suggestedPersons,
...store.getters.suggestedRequestor,
...store.getters.suggestedUser,
...store.getters.suggestedResources,
];
const uniqueIds = [
...new Set(allEntities.map((i) => `${i.type}-${i.id}`)),
];
return Array.from(
uniqueIds,
(id) => allEntities.filter((r) => `${r.type}-${r.id}` === id)[0]
);
},
suggestedPersons(state) {
const existingPersonIds = state.activity.persons.map((p) => p.id);
return state.activity.activityType.personsVisible === 0
? []
: state.activity.accompanyingPeriod.participations
.filter((p) => p.endDate === null)
.map((p) => p.person)
.filter((p) => !existingPersonIds.includes(p.id));
},
suggestedRequestor(state) {
if (state.activity.accompanyingPeriod.requestor === null) {
return [];
}
const existingPersonIds = state.activity.persons.map((p) => p.id);
const existingThirdPartyIds = state.activity.thirdParties.map(
(p) => p.id
);
// ConcernedGroups
addPersonsInvolved(state, payload) {
//console.log('### mutation addPersonsInvolved', payload.result.type);
switch (payload.result.type) {
case 'person':
state.activity.persons.push(payload.result);
break;
case 'thirdparty':
state.activity.thirdParties.push(payload.result);
break;
case 'user':
state.activity.users.push(payload.result);
break;
};
return [state.activity.accompanyingPeriod.requestor].filter(
(r) =>
(r.type === "person" &&
!existingPersonIds.includes(r.id) &&
state.activity.activityType.personsVisible !== 0) ||
(r.type === "thirdparty" &&
!existingThirdPartyIds.includes(r.id) &&
state.activity.activityType.thirdPartiesVisible !== 0)
);
},
suggestedUser(state) {
const existingUserIds = state.activity.users.map((p) => p.id);
return state.activity.activityType.usersVisible === 0
? []
: [state.activity.accompanyingPeriod.user].filter(
(u) => u !== null && !existingUserIds.includes(u.id)
);
},
suggestedResources(state) {
const resources = state.activity.accompanyingPeriod.resources;
const existingPersonIds = state.activity.persons.map((p) => p.id);
const existingThirdPartyIds = state.activity.thirdParties.map(
(p) => p.id
);
return state.activity.accompanyingPeriod.resources
.map((r) => r.resource)
.filter(
(r) =>
(r.type === "person" &&
!existingPersonIds.includes(r.id) &&
state.activity.activityType.personsVisible !== 0) ||
(r.type === "thirdparty" &&
!existingThirdPartyIds.includes(r.id) &&
state.activity.activityType.thirdPartiesVisible !== 0)
);
},
},
removePersonInvolved(state, payload) {
//console.log('### mutation removePersonInvolved', payload.type);
switch (payload.type) {
case 'person':
state.activity.persons = state.activity.persons.filter(person => person !== payload);
break;
case 'thirdparty':
state.activity.thirdParties = state.activity.thirdParties.filter(thirdparty => thirdparty !== payload);
break;
case 'user':
state.activity.users = state.activity.users.filter(user => user !== payload);
break;
};
mutations: {
// SocialIssueAcc
addIssueInList(state, issue) {
//console.log('add issue list', issue.id);
state.activity.accompanyingPeriod.socialIssues.push(issue);
},
addIssueSelected(state, issue) {
//console.log('add issue selected', issue.id);
state.activity.socialIssues.push(issue);
},
updateIssuesSelected(state, issues) {
//console.log('update issues selected', issues);
state.activity.socialIssues = issues;
},
updateIssuesOther(state, payload) {
//console.log('update issues other');
state.socialIssuesOther = payload;
},
removeIssueInOther(state, issue) {
//console.log('remove issue other', issue.id);
state.socialIssuesOther = state.socialIssuesOther.filter(
(i) => i.id !== issue.id
);
},
resetActionsList(state) {
//console.log('reset list actions');
state.socialActionsList = [];
},
addActionInList(state, action) {
//console.log('add action list', action.id);
state.socialActionsList.push(action);
},
updateActionsSelected(state, actions) {
//console.log('update actions selected', actions);
state.activity.socialActions = actions;
},
filterList(state, list) {
const filterList = (list) => {
// remove duplicates entries
list = list.filter(
(value, index) =>
list.findIndex((array) => array.id === value.id) ===
index
);
// alpha sort
list.sort((a, b) =>
a.text > b.text ? 1 : b.text > a.text ? -1 : 0
);
return list;
};
if (list === "issues") {
state.activity.accompanyingPeriod.socialIssues = filterList(
state.activity.accompanyingPeriod.socialIssues
);
}
if (list === "actions") {
state.socialActionsList = filterList(state.socialActionsList);
}
},
// ConcernedGroups
addPersonsInvolved(state, payload) {
console.log("### mutation addPersonsInvolved", payload);
switch (payload.result.type) {
case "person":
state.activity.persons.push(payload.result);
break;
case "thirdparty":
state.activity.thirdParties.push(payload.result);
break;
case "user":
state.activity.users.push(payload.result);
break;
}
},
removePersonInvolved(state, payload) {
//console.log('### mutation removePersonInvolved', payload.type);
switch (payload.type) {
case "person":
state.activity.persons = state.activity.persons.filter(
(person) => person !== payload
);
break;
case "thirdparty":
state.activity.thirdParties =
state.activity.thirdParties.filter(
(thirdparty) => thirdparty !== payload
);
break;
case "user":
state.activity.users = state.activity.users.filter(
(user) => user !== payload
);
break;
}
},
updateLocation(state, value) {
console.log("### mutation: updateLocation", value);
state.activity.location = value;
},
},
updateLocation(state, value) {
console.log('### mutation: updateLocation', value);
state.activity.location = value;
}
},
actions: {
addIssueSelected({ commit }, issue) {
let aSocialIssues = document.getElementById("chill_activitybundle_activity_socialIssues");
aSocialIssues.value = addIdToValue(aSocialIssues.value, issue.id);
commit('addIssueSelected', issue);
actions: {
addIssueSelected({ commit }, issue) {
let aSocialIssues = document.getElementById(
"chill_activitybundle_activity_socialIssues"
);
aSocialIssues.value = addIdToValue(aSocialIssues.value, issue.id);
commit("addIssueSelected", issue);
},
updateIssuesSelected({ commit }, payload) {
let aSocialIssues = document.getElementById(
"chill_activitybundle_activity_socialIssues"
);
aSocialIssues.value = "";
payload.forEach((item) => {
aSocialIssues.value = addIdToValue(
aSocialIssues.value,
item.id
);
});
commit("updateIssuesSelected", payload);
},
updateActionsSelected({ commit }, payload) {
let aSocialActions = document.getElementById(
"chill_activitybundle_activity_socialActions"
);
aSocialActions.value = "";
payload.forEach((item) => {
aSocialActions.value = addIdToValue(
aSocialActions.value,
item.id
);
});
commit("updateActionsSelected", payload);
},
addPersonsInvolved({ commit }, payload) {
//console.log('### action addPersonsInvolved', payload.result.type);
switch (payload.result.type) {
case "person":
let aPersons = document.getElementById(
"chill_activitybundle_activity_persons"
);
aPersons.value = addIdToValue(
aPersons.value,
payload.result.id
);
break;
case "thirdparty":
let aThirdParties = document.getElementById(
"chill_activitybundle_activity_thirdParties"
);
aThirdParties.value = addIdToValue(
aThirdParties.value,
payload.result.id
);
break;
case "user":
let aUsers = document.getElementById(
"chill_activitybundle_activity_users"
);
aUsers.value = addIdToValue(
aUsers.value,
payload.result.id
);
break;
}
commit("addPersonsInvolved", payload);
},
removePersonInvolved({ commit }, payload) {
//console.log('### action removePersonInvolved', payload);
switch (payload.type) {
case "person":
let aPersons = document.getElementById(
"chill_activitybundle_activity_persons"
);
aPersons.value = removeIdFromValue(
aPersons.value,
payload.id
);
break;
case "thirdparty":
let aThirdParties = document.getElementById(
"chill_activitybundle_activity_thirdParties"
);
aThirdParties.value = removeIdFromValue(
aThirdParties.value,
payload.id
);
break;
case "user":
let aUsers = document.getElementById(
"chill_activitybundle_activity_users"
);
aUsers.value = removeIdFromValue(aUsers.value, payload.id);
break;
}
commit("removePersonInvolved", payload);
},
updateLocation({ commit }, value) {
console.log("### action: updateLocation", value);
let hiddenLocation = document.getElementById(
"chill_activitybundle_activity_location"
);
hiddenLocation.value = value.id;
commit("updateLocation", value);
},
},
updateIssuesSelected({ commit }, payload) {
let aSocialIssues = document.getElementById("chill_activitybundle_activity_socialIssues");
aSocialIssues.value = '';
payload.forEach(item => {
aSocialIssues.value = addIdToValue(aSocialIssues.value, item.id);
});
commit('updateIssuesSelected', payload);
},
updateActionsSelected({ commit }, payload) {
let aSocialActions = document.getElementById("chill_activitybundle_activity_socialActions");
aSocialActions.value = '';
payload.forEach(item => {
aSocialActions.value = addIdToValue(aSocialActions.value, item.id);
});
commit('updateActionsSelected', payload);
},
addPersonsInvolved({ commit }, payload) {
//console.log('### action addPersonsInvolved', payload.result.type);
switch (payload.result.type) {
case 'person':
let aPersons = document.getElementById("chill_activitybundle_activity_persons");
aPersons.value = addIdToValue(aPersons.value, payload.result.id);
break;
case 'thirdparty':
let aThirdParties = document.getElementById("chill_activitybundle_activity_thirdParties");
aThirdParties.value = addIdToValue(aThirdParties.value, payload.result.id);
break;
case 'user':
let aUsers = document.getElementById("chill_activitybundle_activity_users");
aUsers.value = addIdToValue(aUsers.value, payload.result.id);
break;
};
commit('addPersonsInvolved', payload);
},
removePersonInvolved({ commit }, payload) {
//console.log('### action removePersonInvolved', payload);
switch (payload.type) {
case 'person':
let aPersons = document.getElementById("chill_activitybundle_activity_persons");
aPersons.value = removeIdFromValue(aPersons.value, payload.id);
break;
case 'thirdparty':
let aThirdParties = document.getElementById("chill_activitybundle_activity_thirdParties");
aThirdParties.value = removeIdFromValue(aThirdParties.value, payload.id);
break;
case 'user':
let aUsers = document.getElementById("chill_activitybundle_activity_users");
aUsers.value = removeIdFromValue(aUsers.value, payload.id);
break;
};
commit('removePersonInvolved', payload);
},
updateLocation({ commit }, value) {
console.log('### action: updateLocation', value);
let hiddenLocation = document.getElementById("chill_activitybundle_activity_location");
hiddenLocation.value = value.id;
commit('updateLocation', value);
}
}
});
export default store;

View File

@@ -27,14 +27,16 @@
{{ activity.type.name | localize_translatable_string }}
<ul class="small_in_title">
{% if activity.location and t.locationVisible %}
<li>
<abbr title="{{ 'location'|trans }}">{{ 'location'|trans ~ ': ' }}</abbr>
{# TODO {% if activity.location %}{{ activity.location }}{% endif %} #}
Domicile de l'usager
<span class="item-key">{{ 'location'|trans ~ ': ' }}</span>
<span>{{ activity.location.locationType.title|localize_translatable_string }}</span>
{{ activity.location.name }}
</li>
{% endif %}
{% if activity.user and t.userVisible %}
<li>
<abbr title="{{ 'Referrer'|trans }}">{{ 'Referrer'|trans ~ ': ' }}</abbr>
<span class="item-key">{{ 'Referrer'|trans ~ ': ' }}</span>
{{ activity.user.usernameCanonical }}
</li>
{% endif %}

View File

@@ -12,12 +12,11 @@
},
{ 'title': 'Third parties'|trans,
'items': entity.thirdParties,
'path' : 'chill_3party_3party_show',
'key' : 'thirdparty_id'
'path' : 'chill_crud_3party_3party_view',
'key' : 'id'
},
{ 'title': 'Users concerned'|trans,
'items': entity.users,
'path' : 'admin_user_show',
'key' : 'id'
},
] %}
@@ -35,8 +34,8 @@
},
{ 'title': 'Third parties'|trans,
'items': entity.thirdParties,
'path' : 'chill_3party_3party_show',
'key' : 'thirdparty_id'
'path' : 'chill_crud_3party_3party_view',
'key' : 'id'
},
{ 'title': 'Users concerned'|trans,
'items': entity.users,

View File

@@ -34,16 +34,18 @@
{{ form_row(edit_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 edit_form.persons is defined -%}
{{ form_widget(edit_form.persons) }}
{% endif %}
{%- if edit_form.thirdParties is defined -%}
{{ form_widget(edit_form.thirdParties) }}
{% endif %}
{%- if edit_form.users is defined -%}
{{ form_widget(edit_form.users) }}
{%- if edit_form.persons is defined -%}
{{ form_widget(edit_form.persons) }}
{% endif %}
{%- if edit_form.thirdParties is defined -%}
{{ form_widget(edit_form.thirdParties) }}
{% endif %}
{%- if edit_form.users is defined -%}
{{ form_widget(edit_form.users) }}
{% endif %}
{% endif %}
<div id="add-persons"></div>

View File

@@ -22,6 +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 }};
</script>
{{ encore_entry_script_tags('vue_activity') }}
{% endblock %}

View File

@@ -55,7 +55,7 @@
{% endif %}
<h2 class="chill-red">{{ 'Concerned groups'|trans }}</h2>
{% include 'ChillActivityBundle:Activity:concernedGroups.html.twig' with {'context': context, 'with_display': 'bloc' } %}
{% include 'ChillActivityBundle:Activity:concernedGroups.html.twig' with {'context': context, 'with_display': 'bloc', 'badge_person': 'true' } %}
<h2 class="chill-red">{{ 'Activity data'|trans }}</h2>

View File

@@ -1,4 +1,2 @@
{{ dump(notification) }}
<a href="{{ path('chill_activity_activity_show', {'id': notification.relatedEntityId }) }}">Go to Activity</a>

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Chill\Migrations\Activity;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20211119173555 extends AbstractMigration
{
public function getDescription(): string
{
return 'remove comment on deprecated json_array type';
}
public function up(Schema $schema): void
{
$columns = [
'activitytype.name',
'activitytypecategory.name'
];
foreach ($columns as $col) {
$this->addSql("COMMENT ON COLUMN $col IS NULL");
}
}
public function down(Schema $schema): void
{
$this->throwIrreversibleMigrationException();
}
}

View File

@@ -168,7 +168,7 @@ class CalendarController extends AbstractController
* Show a calendar item
* @Route("/{_locale}/calendar/calendar/{id}/show", name="chill_calendar_calendar_show")
*/
public function showAction(Request $request, $id): Response
public function showAction(Request $request, int $id): Response
{
$view = null;
$em = $this->getDoctrine()->getManager();
@@ -241,7 +241,7 @@ class CalendarController extends AbstractController
* Edit a calendar item
* @Route("/{_locale}/calendar/calendar/{id}/edit", name="chill_calendar_calendar_edit")
*/
public function editAction($id, Request $request): Response
public function editAction(int $id, Request $request): Response
{
$view = null;
$em = $this->getDoctrine()->getManager();
@@ -302,7 +302,7 @@ class CalendarController extends AbstractController
* Delete a calendar item
* @Route("/{_locale}/calendar/{id}/delete", name="chill_calendar_calendar_delete")
*/
public function deleteAction(Request $request, $id)
public function deleteAction(Request $request, int $id)
{
$view = null;
$em = $this->getDoctrine()->getManager();

View File

@@ -51,6 +51,7 @@ class Calendar
/**
* @ORM\ManyToOne(targetEntity="Chill\PersonBundle\Entity\AccompanyingPeriod")
* @Groups({"read"})
*/
private AccompanyingPeriod $accompanyingPeriod;
@@ -110,8 +111,6 @@ class Calendar
*/
private ?\DateTimeImmutable $endDate = null;
//TODO Lieu
/**
* @ORM\Column(type="string", length=255)
*/

View File

@@ -194,24 +194,24 @@ class CalendarType extends AbstractType
))
;
// $builder->add('invites', HiddenType::class);
// $builder->get('invites')
// ->addModelTransformer(new CallbackTransformer(
// function (iterable $usersAsIterable): string {
// $userIds = [];
// foreach ($usersAsIterable as $value) {
// $userIds[] = $value->getId();
// }
// return implode(',', $userIds);
// },
// function (?string $usersAsString): array {
// return array_map(
// fn(string $id): ?Invite => $this->om->getRepository(Invite::class)->findOneBy(['id' => (int) $id]),
// explode(',', $usersAsString)
// );
// }
// ))
// ;
$builder->add('invites', HiddenType::class);
$builder->get('invites')
->addModelTransformer(new CallbackTransformer(
function (iterable $usersAsIterable): string {
$userIds = [];
foreach ($usersAsIterable as $value) {
$userIds[] = $value->getId();
}
return implode(',', $userIds);
},
function (?string $usersAsString): array {
return array_map(
fn(string $id): ?Invite => $this->om->getRepository(Invite::class)->findOneBy(['id' => (int) $id]),
explode(',', $usersAsString)
);
}
))
;
}/**

View File

@@ -28,9 +28,59 @@ const mapEntity = (entity) => {
const store = createStore({
strict: debug,
state: {
activity: mapEntity(window.entity),
activity: mapEntity(window.entity), // activity is the calendar entity actually
currentEvent: null
},
getters: {
suggestedEntities(state) {
console.log(state.activity)
if (typeof(state.activity.accompanyingPeriod) === 'undefined') {
return [];
}
const allEntities = [
...store.getters.suggestedPersons,
...store.getters.suggestedRequestor,
...store.getters.suggestedUser,
...store.getters.suggestedResources
];
const uniqueIds = [...new Set(allEntities.map(i => `${i.type}-${i.id}`))];
return Array.from(uniqueIds, id => allEntities.filter(r => `${r.type}-${r.id}` === id)[0]);
},
suggestedPersons(state) {
const existingPersonIds = state.activity.persons.map(p => p.id);
return state.activity.accompanyingPeriod.participations
.filter(p => p.endDate === null)
.map(p => p.person)
.filter(p => !existingPersonIds.includes(p.id))
},
suggestedRequestor(state) {
const existingPersonIds = state.activity.persons.map(p => p.id);
const existingThirdPartyIds = state.activity.thirdParties.map(p => p.id);
return [state.activity.accompanyingPeriod.requestor]
.filter(r =>
(r.type === 'person' && !existingPersonIds.includes(r.id)) ||
(r.type === 'thirdparty' && !existingThirdPartyIds.includes(r.id))
);
},
suggestedUser(state) {
const existingUserIds = state.activity.users.map(p => p.id);
return [state.activity.accompanyingPeriod.user]
.filter(
u => !existingUserIds.includes(u.id)
);
},
suggestedResources(state) {
const resources = state.activity.accompanyingPeriod.resources;
const existingPersonIds = state.activity.persons.map(p => p.id);
const existingThirdPartyIds = state.activity.thirdParties.map(p => p.id);
return state.activity.accompanyingPeriod.resources
.map(r => r.resource)
.filter(r =>
(r.type === 'person' && !existingPersonIds.includes(r.id)) ||
(r.type === 'thirdparty' && !existingThirdPartyIds.includes(r.id))
);
}
},
mutations: {
// ConcernedGroups

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Chill\Migrations\Calendar;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20211119173557 extends AbstractMigration
{
public function getDescription(): string
{
return 'remove comment on deprecated json_array type';
}
public function up(Schema $schema): void
{
$columns = [
'chill_calendar.cancel_reason.name',
'chill_calendar.invite.status',
];
foreach ($columns as $col) {
$this->addSql("COMMENT ON COLUMN $col IS NULL");
}
}
public function down(Schema $schema): void
{
$this->throwIrreversibleMigrationException();
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace Chill\DocGeneratorBundle\Serializer\Encoder;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
class DocGenEncoder implements \Symfony\Component\Serializer\Encoder\EncoderInterface
{
/**
* @inheritDoc
*/
public function encode($data, string $format, array $context = [])
{
if (!$this->isAssociative($data)) {
throw new UnexpectedValueException("Only associative arrays are allowed; lists are not allowed");
}
$result = [];
$this->recusiveEncoding($data, $result, '');
return $result;
}
private function recusiveEncoding(array $data, array &$result, $path)
{
if ($this->isAssociative($data)) {
foreach ($data as $key => $value) {
if (\is_array($value)) {
$this->recusiveEncoding($value, $result, $this->canonicalizeKey($path, $key));
} else {
$result[$this->canonicalizeKey($path, $key)] = $value;
}
}
} else {
foreach ($data as $elem) {
if (!$this->isAssociative($elem)) {
throw new UnexpectedValueException(sprintf("Embedded loops are not allowed. See data under %s path", $path));
}
$sub = [];
$this->recusiveEncoding($elem, $sub, '');
$result[$path][] = $sub;
}
}
}
private function canonicalizeKey(string $path, string $key): string
{
return $path === '' ? $key : $path.'_'.$key;
}
private function isAssociative(array $data)
{
$keys = \array_keys($data);
return $keys !== \array_keys($keys);
}
/**
* @inheritDoc
*/
public function supportsEncoding(string $format)
{
return $format === 'docgen';
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Chill\DocGeneratorBundle\Serializer\Helper;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class NormalizeNullValueHelper
{
private NormalizerInterface $normalizer;
public function __construct(NormalizerInterface $normalizer)
{
$this->normalizer = $normalizer;
}
public function normalize(array $attributes, string $format = 'docgen', ?array $context = [])
{
$data = [];
foreach ($attributes as $key => $class) {
if (is_numeric($key)) {
$data[$class] = '';
} else {
switch ($class) {
case 'array':
case 'bool':
case 'double':
case 'float':
case 'int':
case 'resource':
case 'string':
case 'null':
$data[$key] = '';
break;
default:
$data[$key] = $this->normalizer->normalize(null, $format, \array_merge(
$context,
['docgen:expects' => $class]
));
break;
}
}
}
return $data;
}
}

View File

@@ -0,0 +1,177 @@
<?php
namespace Chill\DocGeneratorBundle\Serializer\Normalizer;
use Chill\DocGeneratorBundle\Serializer\Helper\NormalizeNullValueHelper;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\Serializer\Exception\ExceptionInterface;
use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\Mapping\AttributeMetadata;
use Symfony\Component\Serializer\Mapping\ClassMetadata;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class DocGenObjectNormalizer implements NormalizerInterface, NormalizerAwareInterface
{
use NormalizerAwareTrait;
private ClassMetadataFactoryInterface $classMetadataFactory;
private PropertyAccessor $propertyAccess;
public function __construct(ClassMetadataFactoryInterface $classMetadataFactory)
{
$this->classMetadataFactory = $classMetadataFactory;
$this->propertyAccess = PropertyAccess::createPropertyAccessor();
}
/**
* @inheritDoc
*/
public function normalize($object, string $format = null, array $context = [])
{
$classMetadataKey = $object ?? $context['docgen:expects'];
if (!$this->classMetadataFactory->hasMetadataFor($classMetadataKey)) {
throw new LogicException(sprintf("This object does not have metadata: %s. Add groups on this entity to allow to serialize with the format %s and groups %s", is_object($object) ? get_class($object) : $context['docgen:expects'], $format, \implode(', ', $context['groups'])));
}
$metadata = $this->classMetadataFactory->getMetadataFor($classMetadataKey);
$expectedGroups = \array_key_exists(AbstractNormalizer::GROUPS, $context) ?
\is_array($context[AbstractNormalizer::GROUPS]) ? $context[AbstractNormalizer::GROUPS] : [$context[AbstractNormalizer::GROUPS]]
: [];
$attributes = \array_filter(
$metadata->getAttributesMetadata(),
function (AttributeMetadata $a) use ($expectedGroups) {
foreach ($a->getGroups() as $g) {
if (\in_array($g, $expectedGroups, true)) {
return true;
}
}
return false;
});
if (null === $object) {
return $this->normalizeNullData($format, $context, $metadata, $attributes);
}
return $this->normalizeObject($object, $format, $context, $expectedGroups, $metadata, $attributes);
}
/**
* @param string $format
* @param array $context
* @param array $expectedGroups
* @param ClassMetadata $metadata
* @param array|AttributeMetadata[] $attributes
*/
private function normalizeNullData(string $format, array $context, ClassMetadata $metadata, array $attributes): array
{
$keys = [];
foreach ($attributes as $attribute) {
$key = $attribute->getSerializedName() ?? $attribute->getName();
$keys[$key] = $this->getExpectedType($attribute, $metadata->getReflectionClass());
}
$normalizer = new NormalizeNullValueHelper($this->normalizer);
return $normalizer->normalize($keys, $format, $context);
}
/**
* @param $object
* @param $format
* @param array $context
* @param array $expectedGroups
* @param ClassMetadata $metadata
* @param array|AttributeMetadata[] $attributes
* @return array
* @throws ExceptionInterface
*/
private function normalizeObject($object, $format, array $context, array $expectedGroups, ClassMetadata $metadata, array $attributes)
{
$data = [];
$reflection = $metadata->getReflectionClass();
foreach ($attributes as $attribute) {
/** @var AttributeMetadata $attribute */
$value = $this->propertyAccess->getValue($object, $attribute->getName());
$key = $attribute->getSerializedName() ?? $attribute->getName();
if (is_object($value)) {
$data[$key] =
$this->normalizer->normalize($value, $format, \array_merge(
$context, $attribute->getNormalizationContextForGroups($expectedGroups)
));
} elseif (null === $value) {
$data[$key] = $this->normalizeNullOutputValue($format, $context, $attribute, $reflection);
} else {
$data[$key] = (string) $value;
}
}
return $data;
}
private function getExpectedType(AttributeMetadata $attribute, \ReflectionClass $reflection): string
{
// we have to get the expected content
if ($reflection->hasProperty($attribute->getName())) {
$type = $reflection->getProperty($attribute->getName())->getType();
} elseif ($reflection->hasMethod($attribute->getName())) {
$type = $reflection->getMethod($attribute->getName())->getReturnType();
} else {
throw new \LogicException(sprintf(
"Could not determine how the content is determined for the attribute %s. Add attribute property only on property or method", $attribute->getName()
));
}
if (null === $type) {
throw new \LogicException(sprintf(
"Could not determine the type for this attribute: %s. Add a return type to the method or property declaration", $attribute->getName()
));
}
return $type->getName();
}
/**
*/
private function normalizeNullOutputValue($format, array $context, AttributeMetadata $attribute, \ReflectionClass $reflection)
{
$type = $this->getExpectedType($attribute, $reflection);
switch ($type) {
case 'array':
case 'bool':
case 'double':
case 'float':
case 'int':
case 'resource':
case 'string':
return '';
default:
return $this->normalizer->normalize(
null,
$format,
\array_merge(
$context,
['docgen:expects' => $type]
)
);
}
}
/**
* @inheritDoc
*/
public function supportsNormalization($data, string $format = null): bool
{
return $format === 'docgen' && (is_object($data) || null === $data);
}
}

View File

@@ -8,3 +8,10 @@ services:
autowire: true
autoconfigure: true
resource: '../Repository/'
Chill\DocGeneratorBundle\Serializer\Normalizer\:
autowire: true
autoconfigure: true
resource: '../Serializer/Normalizer/'
tags:
- { name: 'serializer.normalizer', priority: -152 }

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Chill\Migrations\DocGenerator;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20211119173556 extends AbstractMigration
{
public function getDescription(): string
{
return 'remove comment on deprecated json_array type';
}
public function up(Schema $schema): void
{
$columns = [
'chill_docgen_template.name'
];
foreach ($columns as $col) {
$this->addSql("COMMENT ON COLUMN $col IS NULL");
}
}
public function down(Schema $schema): void
{
$this->throwIrreversibleMigrationException();
}
}

View File

@@ -0,0 +1,113 @@
<?php
namespace Chill\DocGeneratorBundle\Tests\Serializer\Encoder;
use Chill\DocGeneratorBundle\Serializer\Encoder\DocGenEncoder;
use Doctrine\ORM\EntityRepository;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
class DocGenEncoderTest extends TestCase
{
private DocGenEncoder $encoder;
protected function setUp()
{
parent::setUp();
$this->encoder = new DocGenEncoder();
}
/**
* @dataProvider generateEncodeData
*/
public function testEncode($expected, $data, string $msg)
{
$generated = $this->encoder->encode($data, 'docgen');
$this->assertEquals($expected, $generated, $msg);
}
public function testEmbeddedLoopsThrowsException()
{
$this->expectException(UnexpectedValueException::class);
$data = [
'data' => [
['item' => 'one'],
[
'embedded' => [
[
['subitem' => 'two'],
['subitem' => 'three']
]
]
],
]
];
$this->encoder->encode($data, 'docgen');
}
public function generateEncodeData()
{
yield [ ['tests' => 'ok'], ['tests' => 'ok'], "A simple test with a simple array"];
yield [
// expected:
['item_subitem' => 'value'],
// data:
['item' => ['subitem' => 'value']],
"A test with multidimensional array"
];
yield [
// expected:
[ 'data' => [['item' => 'one'], ['item' => 'two']] ],
// data:
[ 'data' => [['item' => 'one'], ['item' => 'two']] ],
"a list of items"
];
yield [
// expected:
[ 'data' => [['item_subitem' => 'alpha'], ['item' => 'two']] ],
// data:
[ 'data' => [['item' => ['subitem' => 'alpha']], ['item' => 'two'] ] ],
"a list of items with multidimensional array inside item"
];
yield [
// expected:
[
'persons' => [
[
'firstname' => 'Jonathan',
'lastname' => 'Dupont',
'dateOfBirth_long' => '16 juin 1981',
'dateOfBirth_short' => '16/06/1981',
'father_firstname' => 'Marcel',
'father_lastname' => 'Dupont',
'father_dateOfBirth_long' => '10 novembre 1953',
'father_dateOfBirth_short' => '10/11/1953'
],
]
],
// data:
[
'persons' => [
[
'firstname' => 'Jonathan',
'lastname' => 'Dupont',
'dateOfBirth' => [ 'long' => '16 juin 1981', 'short' => '16/06/1981'],
'father' => [
'firstname' => 'Marcel',
'lastname' => 'Dupont',
'dateOfBirth' => ['long' => '10 novembre 1953', 'short' => '10/11/1953']
]
],
]
],
"a longer list, with near real data inside and embedded associative arrays"
];
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace Chill\DocGeneratorBundle\tests\Serializer\Normalizer;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\User;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class DocGenObjectNormalizerTest extends KernelTestCase
{
private NormalizerInterface $normalizer;
protected function setUp()
{
parent::setUp();
self::bootKernel();
$this->normalizer = self::$container->get(NormalizerInterface::class);
}
public function testNormalizationBasic()
{
$user = new User();
$user->setUsername('User Test');
$user->setMainCenter($center = new Center());
$center->setName('test');
$normalized = $this->normalizer->normalize($user, 'docgen', [ AbstractNormalizer::GROUPS => ['docgen:read']]);
$expected = [
'label' => 'User Test',
'email' => '',
'mainCenter' => [
'name' => 'test'
]
];
$this->assertEquals($expected, $normalized, "test normalization fo an user");
}
public function testNormalizeWithNullValueEmbedded()
{
$user = new User();
$user->setUsername('User Test');
$normalized = $this->normalizer->normalize($user, 'docgen', [ AbstractNormalizer::GROUPS => ['docgen:read']]);
$expected = [
'label' => 'User Test',
'email' => '',
'mainCenter' => [
'name' => ''
]
];
$this->assertEquals($expected, $normalized, "test normalization fo an user with null center");
}
public function testNormalizeNullObjectWithObjectEmbedded()
{
$normalized = $this->normalizer->normalize(null, 'docgen', [
AbstractNormalizer::GROUPS => ['docgen:read'],
'docgen:expects' => User::class,
]);
$expected = [
'label' => '',
'email' => '',
'mainCenter' => [
'name' => ''
]
];
$this->assertEquals($expected, $normalized, "test normalization for a null user");
}
}

View File

@@ -1,44 +1,19 @@
<?php
/*
* Copyright (C) 2018 Champs-Libres SCRLFS
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Chill\DocStoreBundle\Security\Authorization;
use App\Security\Authorization\VoterHelperFactory;
use Chill\MainBundle\Security\Authorization\AbstractChillVoter;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Security\Authorization\VoterHelperFactoryInterface;
use Chill\MainBundle\Security\Authorization\VoterHelperInterface;
use Chill\MainBundle\Security\ProvideRoleHierarchyInterface;
use Chill\DocStoreBundle\Entity\PersonDocument;
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher;
use Chill\PersonBundle\Entity\Person;
use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Role\Role;
use Psr\Log\LoggerInterface;
use Symfony\Component\Security\Core\Security;
/**
*
*/
class PersonDocumentVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface
{
const CREATE = 'CHILL_PERSON_DOCUMENT_CREATE';

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Chill\Migrations\DocStore;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20211119173558 extends AbstractMigration
{
public function getDescription(): string
{
return 'remove comment on deprecated json_array type';
}
public function up(Schema $schema): void
{
$columns = [
'chill_doc.document_category.name',
'chill_doc.stored_object.key',
'chill_doc.stored_object.iv',
'chill_doc.stored_object.datas',
];
foreach ($columns as $col) {
$this->addSql("COMMENT ON COLUMN $col IS NULL");
}
}
public function down(Schema $schema): void
{
$this->throwIrreversibleMigrationException();
}
}

View File

@@ -3,6 +3,7 @@
namespace Chill\MainBundle;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\MainBundle\Search\SearchApiInterface;
use Chill\MainBundle\Search\SearchInterface;
use Chill\MainBundle\Security\Authorization\ChillVoterInterface;
use Chill\MainBundle\Security\ProvideRoleInterface;
@@ -41,6 +42,8 @@ class ChillMainBundle extends Bundle
->addTag('chill_main.scope_resolver');
$container->registerForAutoconfiguration(ChillEntityRenderInterface::class)
->addTag('chill.render_entity');
$container->registerForAutoconfiguration(SearchApiInterface::class)
->addTag('chill.search_api_provider');
$container->addCompilerPass(new SearchableServicesCompilerPass());
$container->addCompilerPass(new ConfigConsistencyCompilerPass());

View File

@@ -18,6 +18,7 @@ use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Form\UserType;
use Chill\MainBundle\Entity\GroupCenter;
use Chill\MainBundle\Form\Type\ComposedGroupCenterType;
use Chill\MainBundle\Form\UserCurrentLocationType;
use Chill\MainBundle\Form\UserPasswordType;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
@@ -87,7 +88,7 @@ class UserController extends CRUDController
[
'add_groupcenter_form' => $this->createAddLinkGroupCenterForm($entity, $request)->createView(),
'delete_groupcenter_form' => array_map(
function(\Symfony\Component\Form\Form $form) {
function (\Symfony\Component\Form\Form $form) {
return $form->createView();
},
iterator_to_array($this->getDeleteLinkGroupCenterByUser($entity, $request), true)
@@ -164,7 +165,7 @@ class UserController extends CRUDController
}
$groupCenter = $em->getRepository('ChillMainBundle:GroupCenter')
->find($gcid);
->find($gcid);
if (!$groupCenter) {
throw $this->createNotFoundException('Unable to find groupCenter entity');
@@ -181,10 +182,9 @@ class UserController extends CRUDController
$em->flush();
$this->addFlash('success', $this->get('translator')
->trans('The permissions where removed.'));
->trans('The permissions where removed.'));
return $this->redirect($this->generateUrl('chill_crud_admin_user_edit', array('id' => $uid)));
}
/**
@@ -206,24 +206,26 @@ class UserController extends CRUDController
if ($form->isValid()) {
$groupCenter = $this->getPersistedGroupCenter(
$form[self::FORM_GROUP_CENTER_COMPOSED]->getData());
$form[self::FORM_GROUP_CENTER_COMPOSED]->getData()
);
$user->addGroupCenter($groupCenter);
if ($this->validator->validate($user)->count() === 0) {
$em->flush();
$this->addFlash('success', $this->get('translator')->trans('The '
$this->addFlash('success', $this->get('translator')->trans('The '
. 'permissions have been successfully added to the user'));
$returnPathParams = $request->query->has('returnPath') ?
['returnPath' => $request->query->get('returnPath')] : [];
return $this->redirect($this->generateUrl('chill_crud_admin_user_edit',
\array_merge(['id' => $uid], $returnPathParams)));
$returnPathParams = $request->query->has('returnPath') ?
['returnPath' => $request->query->get('returnPath')] : [];
return $this->redirect($this->generateUrl(
'chill_crud_admin_user_edit',
\array_merge(['id' => $uid], $returnPathParams)
));
}
foreach($this->validator->validate($user) as $error) {
foreach ($this->validator->validate($user) as $error) {
$this->addFlash('error', $error->getMessage());
}
}
@@ -233,7 +235,7 @@ class UserController extends CRUDController
'edit_form' => $this->createEditForm($user)->createView(),
'add_groupcenter_form' => $this->createAddLinkGroupCenterForm($user, $request)->createView(),
'delete_groupcenter_form' => array_map(
static fn(Form $form) => $form->createView(),
static fn (Form $form) => $form->createView(),
iterator_to_array($this->getDeleteLinkGroupCenterByUser($user, $request), true)
)
]);
@@ -244,10 +246,10 @@ class UserController extends CRUDController
$em = $this->getDoctrine()->getManager();
$groupCenterManaged = $em->getRepository('ChillMainBundle:GroupCenter')
->findOneBy(array(
'center' => $groupCenter->getCenter(),
'permissionsGroup' => $groupCenter->getPermissionsGroup()
));
->findOneBy(array(
'center' => $groupCenter->getCenter(),
'permissionsGroup' => $groupCenter->getPermissionsGroup()
));
if (!$groupCenterManaged) {
$em->persist($groupCenter);
@@ -267,8 +269,10 @@ class UserController extends CRUDController
$returnPathParams = $request->query->has('returnPath') ? ['returnPath' => $request->query->get('returnPath')] : [];
return $this->createFormBuilder()
->setAction($this->generateUrl('admin_user_delete_groupcenter',
array_merge($returnPathParams, ['uid' => $user->getId(), 'gcid' => $groupCenter->getId()])))
->setAction($this->generateUrl(
'admin_user_delete_groupcenter',
array_merge($returnPathParams, ['uid' => $user->getId(), 'gcid' => $groupCenter->getId()])
))
->setMethod('DELETE')
->add('submit', SubmitType::class, array('label' => 'Delete'))
->getForm();
@@ -282,8 +286,10 @@ class UserController extends CRUDController
$returnPathParams = $request->query->has('returnPath') ? ['returnPath' => $request->query->get('returnPath')] : [];
return $this->createFormBuilder()
->setAction($this->generateUrl('admin_user_add_groupcenter',
array_merge($returnPathParams, ['uid' => $user->getId()])))
->setAction($this->generateUrl(
'admin_user_add_groupcenter',
array_merge($returnPathParams, ['uid' => $user->getId()])
))
->setMethod('POST')
->add(self::FORM_GROUP_CENTER_COMPOSED, ComposedGroupCenterType::class)
->add('submit', SubmitType::class, array('label' => 'Add a new groupCenter'))
@@ -296,4 +302,36 @@ class UserController extends CRUDController
yield $groupCenter->getId() => $this->createDeleteLinkGroupCenterForm($user, $groupCenter, $request);
}
}
/**
* Displays a form to edit the user current location.
*
* @Route("/{_locale}/main/user/current-location/edit", name="chill_main_user_currentlocation_edit")
*/
public function editCurrentLocationAction(Request $request)
{
$user = $this->getUser();
$form = $this->createForm(UserCurrentLocationType::class, $user)
->add('submit', SubmitType::class, ['label' => 'Save'])
->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$currentLocation = $form->get('currentLocation')->getData();
$user->setCurrentLocation($currentLocation);
$this->getDoctrine()->getManager()->flush();
$this->addFlash('success', $this->get('translator')->trans('Current location successfully updated'));
return $this->redirect(
$request->query->has('returnPath') ? $request->query->get('returnPath') :
$this->generateUrl('chill_main_homepage')
);
}
return $this->render('@ChillMain/User/edit_current_location.html.twig', [
'entity' => $user,
'edit_form' => $form->createView()
]);
}
}

View File

@@ -1,19 +1,19 @@
<?php
/*
*
*
* Copyright (C) 2014, Champs Libres Cooperative SCRLFS, <http://www.champs-libres.coop>
*
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
@@ -23,12 +23,11 @@ namespace Chill\MainBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Symfony\Component\Serializer\Annotation as Serializer;
/**
* @ORM\Entity
* @ORM\Table(name="centers")
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class Center implements HasCenterInterface
{
@@ -46,9 +45,10 @@ class Center implements HasCenterInterface
* @var string
*
* @ORM\Column(type="string", length=255)
* @Serializer\Groups({"docgen:read"})
*/
private $name;
private string $name = '';
/**
* @var Collection
*
@@ -58,8 +58,8 @@ class Center implements HasCenterInterface
* )
*/
private $groupCenters;
/**
* Center constructor.
*/
@@ -67,7 +67,7 @@ class Center implements HasCenterInterface
{
$this->groupCenters = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* @return string
*/
@@ -75,7 +75,7 @@ class Center implements HasCenterInterface
{
return $this->name;
}
/**
* @param $name
* @return $this
@@ -85,7 +85,7 @@ class Center implements HasCenterInterface
$this->name = $name;
return $this;
}
/**
* @return int
*/
@@ -93,7 +93,7 @@ class Center implements HasCenterInterface
{
return $this->id;
}
/**
* @return ArrayCollection|Collection
*/
@@ -101,7 +101,7 @@ class Center implements HasCenterInterface
{
return $this->groupCenters;
}
/**
* @param GroupCenter $groupCenter
* @return $this
@@ -111,7 +111,7 @@ class Center implements HasCenterInterface
$this->groupCenters->add($groupCenter);
return $this;
}
/**
* @return string
*/
@@ -119,7 +119,7 @@ class Center implements HasCenterInterface
{
return $this->getName();
}
/**
* @return $this|Center
*/

View File

@@ -6,9 +6,10 @@ use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ArrayCollection;
use Chill\MainBundle\Entity\UserJob;
use Chill\MainBundle\Entity\Location;
use Symfony\Component\Security\Core\User\AdvancedUserInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
use Symfony\Component\Serializer\Annotation as Serializer;
/**
* User
@@ -16,7 +17,7 @@ use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
* @ORM\Entity
* @ORM\Table(name="users")
* @ORM\Cache(usage="NONSTRICT_READ_WRITE", region="acl_cache_region")
* @DiscriminatorMap(typeProperty="type", mapping={
* @Serializer\DiscriminatorMap(typeProperty="type", mapping={
* "user"=User::class
* })
*/
@@ -51,6 +52,7 @@ class User implements AdvancedUserInterface {
/**
* @ORM\Column(type="string", length=200)
* @Serializer\Groups({"docgen:read"})
*/
private string $label = '';
@@ -58,8 +60,9 @@ class User implements AdvancedUserInterface {
* @var string
*
* @ORM\Column(type="string", length=150, nullable=true)
* @Serializer\Groups({"docgen:read"})
*/
private $email;
private ?string $email = null;
/**
* @var string
@@ -123,6 +126,7 @@ class User implements AdvancedUserInterface {
/**
* @var Center|null
* @ORM\ManyToOne(targetEntity=Center::class)
* @Serializer\Groups({"docgen:read"})
*/
private ?Center $mainCenter = null;
@@ -138,6 +142,12 @@ class User implements AdvancedUserInterface {
*/
private ?UserJob $userJob = null;
/**
* @var Location|null
* @ORM\ManyToOne(targetEntity=Location::class)
*/
private ?Location $currentLocation = null;
/**
* User constructor.
*/
@@ -485,4 +495,22 @@ class User implements AdvancedUserInterface {
$this->userJob = $userJob;
return $this;
}
/**
* @return Location|null
*/
public function getCurrentLocation(): ?Location
{
return $this->currentLocation;
}
/**
* @param Location|null $location
* @return User
*/
public function setCurrentLocation(?Location $currentLocation): User
{
$this->currentLocation = $currentLocation;
return $this;
}
}

View File

@@ -15,15 +15,15 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
final class LocationFormType extends AbstractType
{
// private TranslatableStringHelper $translatableStringHelper;
private TranslatableStringHelper $translatableStringHelper;
// /**
// * @param TranslatableStringHelper $translatableStringHelper
// */
// public function __construct(TranslatableStringHelper $translatableStringHelper)
// {
// $this->translatableStringHelper = $translatableStringHelper;
// }
/**
* @param TranslatableStringHelper $translatableStringHelper
*/
public function __construct(TranslatableStringHelper $translatableStringHelper)
{
$this->translatableStringHelper = $translatableStringHelper;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
@@ -38,8 +38,7 @@ final class LocationFormType extends AbstractType
];
},
'choice_label' => function (EntityLocationType $entity) {
//return $this->translatableStringHelper->localize($entity->getTitle()); //TODO not working. Cannot pass smthg in the constructor
return $entity->getTitle()['fr'];
return $this->translatableStringHelper->localize($entity->getTitle());
},
])
->add('name', TextType::class)

View File

@@ -0,0 +1,42 @@
<?php
namespace Chill\MainBundle\Form;
use Chill\MainBundle\Entity\Location;
use Chill\MainBundle\Repository\LocationRepository;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
class UserCurrentLocationType extends AbstractType
{
private LocationRepository $locationRepository;
private TranslatableStringHelper $translatableStringHelper;
/**
* @param TranslatableStringHelper $translatableStringHelper
*/
public function __construct(TranslatableStringHelper $translatableStringHelper, LocationRepository $locationRepository)
{
$this->translatableStringHelper = $translatableStringHelper;
$this->locationRepository = $locationRepository;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('currentLocation', EntityType::class, [
'class' => Location::class,
'choices' => $this->locationRepository->findByPublicLocations(),
'choice_label' => function (Location $entity) {
return $entity->getName() ?
$entity->getName().' ('.$this->translatableStringHelper->localize($entity->getLocationType()->getTitle()).')' :
$this->translatableStringHelper->localize($entity->getLocationType()->getTitle());
},
'placeholder' => 'Pick a location',
'required' => false,
]);
}
}

View File

@@ -18,4 +18,14 @@ class LocationRepository extends ServiceEntityRepository
{
parent::__construct($registry, Location::class);
}
/**
* fetch locations which are selectable for users
*
* @return Location[]
*/
public function findByPublicLocations()
{
return $this->findBy(['active' => true, 'availableForUsers' => true]);
}
}

View File

@@ -402,3 +402,11 @@ input.belgian_national_number {
&.daily_counter {}
&.control_digit {}
}
// replace abbr
span.item-key {
font-variant: all-small-caps;
font-size: 90%;
background-color: #0000000a;
//text-decoration: dotted underline;
}

View File

@@ -8,3 +8,43 @@ require('./bootstrap.scss');
import Dropdown from 'bootstrap/js/src/dropdown';
import Modal from 'bootstrap/js/dist/modal';
import Collapse from 'bootstrap/js/src/collapse';
import Carousel from 'bootstrap/js/src/carousel';
//
// ACHeaderSlider is a small slider used in banner of AccompanyingCourse Section
// Initialize options, and show/hide controls in first/last slides
//
let ACHeaderSlider = document.querySelector('#ACHeaderSlider');
if (ACHeaderSlider) {
let controlPrev = ACHeaderSlider.querySelector('button[data-bs-slide="prev"]'),
controlNext = ACHeaderSlider.querySelector('button[data-bs-slide="next"]'),
length = ACHeaderSlider.querySelectorAll('.carousel-item').length,
last = length-1,
carousel = new Carousel(ACHeaderSlider, {
interval: false,
wrap: false,
ride: false,
keyboard: false,
touch: true
})
;
document.addEventListener('DOMContentLoaded', (e) => {
controlNext.classList.remove('visually-hidden');
});
ACHeaderSlider.addEventListener('slid.bs.carousel', (e) => {
//console.log('from slide', e.direction, e.relatedTarget, e.from, e.to );
switch (e.to) {
case 0:
controlPrev.classList.add('visually-hidden');
controlNext.classList.remove('visually-hidden');
break;
case last:
controlPrev.classList.remove('visually-hidden');
controlNext.classList.add('visually-hidden');
break;
default:
controlPrev.classList.remove('visually-hidden');
controlNext.classList.remove('visually-hidden');
}
})
}

View File

@@ -0,0 +1,21 @@
{% extends 'ChillMainBundle::layout.html.twig' %}
{% block title %}{{ 'Edit my current location'|trans }}{% endblock %}
{% block content -%}
<div class="col-md-10 col-xxl">
<h1>{{ 'Edit my current location'|trans }}</h1>
{{ form_start(edit_form) }}
{{ form_row(edit_form.currentLocation) }}
<ul class="record_actions sticky-form-buttons">
<li>
{{ form_widget(edit_form.submit, { 'attr': { 'class': 'btn btn-edit' } } ) }}
</li>
</ul>
{{ form_end(edit_form) }}
</div>
{% endblock %}

View File

@@ -5,7 +5,7 @@
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ installation.name }} - {% block title %}{% endblock %}</title>
<title>{{ installation.name }} - {% block title %}{{ 'Homepage'|trans }}{% endblock %}</title>
<link rel="shortcut icon" href="{{ asset('build/images/favicon.ico') }}" type="image/x-icon">
{{ encore_entry_link_tags('mod_bootstrap') }}
@@ -68,6 +68,9 @@
<button type="submit" class="btn btn-lg btn-warning mt-3">
<i class="fa fa-fw fa-search"></i> {{ 'Search'|trans }}
</button>
<a class="btn btn-lg btn-misc mt-3" href="{{ path('chill_main_advanced_search_list') }}">
<i class="fa fa-fw fa-search"></i> {{ 'Advanced search'|trans }}
</a>
</center>
</form>
</div>

View File

@@ -20,46 +20,57 @@ namespace Chill\MainBundle\Routing\MenuBuilder;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\MainBundle\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Security;
/**
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class UserMenuBuilder implements LocalMenuBuilderInterface
{
/**
*
* @var TokenStorageInterface
*/
protected $tokenStorage;
public function __construct(TokenStorageInterface $tokenStorage)
private Security $security;
public function __construct(Security $security)
{
$this->tokenStorage = $tokenStorage;
$this->security = $security;
}
public function buildMenu($menuId, \Knp\Menu\MenuItem $menu, array $parameters)
{
if ($this->tokenStorage->getToken()->getUser() instanceof User) {
$user = $this->security->getUser();
if ($user instanceof User) {
if (null !== $user->getCurrentLocation()) {
$locationTextMenu = $user->getCurrentLocation()->getName();
} else {
$locationTextMenu = 'Set a location';
}
$menu
->addChild(
$locationTextMenu,
['route' => 'chill_main_user_currentlocation_edit']
)
->setExtras([
'order' => -9999999,
'icon' => 'map-marker'
]);
$menu
->addChild(
'Change password',
[ 'route' => 'change_my_password']
['route' => 'change_my_password']
)
->setExtras([
'order' => 99999999998
]);
}
$menu
->addChild(
'Logout',
'Logout',
[
'route' => 'logout'
])
]
)
->setExtras([
'order'=> 99999999999,
'order' => 99999999999,
'icon' => 'power-off'
]);
}

View File

@@ -26,10 +26,10 @@ use Chill\MainBundle\Search\ParsingException;
/**
* This class implements abstract search with most common responses.
*
*
* you should use this abstract class instead of SearchInterface : if the signature of
* search interface change, the generic method will be implemented here.
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*
*/
@@ -37,7 +37,7 @@ abstract class AbstractSearch implements SearchInterface
{
/**
* parse string expected to be a date and transform to a DateTime object
*
*
* @param type $string
* @return \DateTime
* @throws ParsingException if the date is not parseable
@@ -51,14 +51,14 @@ abstract class AbstractSearch implements SearchInterface
. 'not parsable', 0, $ex);
throw $exception;
}
}
/**
* recompose a pattern, retaining only supported terms
*
*
* the outputted string should be used to show users their search
*
*
* @param array $terms
* @param array $supportedTerms
* @param string $domain if your domain is NULL, you should set NULL. You should set used domain instead
@@ -67,35 +67,35 @@ abstract class AbstractSearch implements SearchInterface
protected function recomposePattern(array $terms, array $supportedTerms, $domain = NULL)
{
$recomposed = '';
if ($domain !== NULL)
{
$recomposed .= '@'.$domain.' ';
}
foreach ($supportedTerms as $term) {
if (array_key_exists($term, $terms) && $term !== '_default') {
$recomposed .= ' '.$term.':';
$containsSpace = \strpos($terms[$term], " ") !== false;
if ($containsSpace) {
$recomposed .= "(";
$recomposed .= '"';
}
$recomposed .= (mb_stristr(' ', $terms[$term]) === FALSE) ? $terms[$term] : '('.$terms[$term].')';
if ($containsSpace) {
$recomposed .= ")";
$recomposed .= '"';
}
}
}
if ($terms['_default'] !== '') {
$recomposed .= ' '.$terms['_default'];
}
//strip first character if empty
if (mb_strcut($recomposed, 0, 1) === ' '){
$recomposed = mb_strcut($recomposed, 1);
}
return $recomposed;
}
}
}

View File

@@ -20,19 +20,15 @@ class SearchApi
private EntityManagerInterface $em;
private PaginatorFactory $paginator;
private array $providers = [];
private iterable $providers = [];
public function __construct(
EntityManagerInterface $em,
SearchPersonApiProvider $searchPerson,
ThirdPartyApiSearch $thirdPartyApiSearch,
SearchUserApiProvider $searchUser,
iterable $providers,
PaginatorFactory $paginator
) {
$this->em = $em;
$this->providers[] = $searchPerson;
$this->providers[] = $thirdPartyApiSearch;
$this->providers[] = $searchUser;
$this->providers = $providers;
$this->paginator = $paginator;
}
@@ -68,10 +64,15 @@ class SearchApi
private function findProviders(string $pattern, array $types, array $parameters): array
{
return \array_filter(
$this->providers,
fn($p) => $p->supportsTypes($pattern, $types, $parameters)
);
$providers = [];
foreach ($this->providers as $provider) {
if ($provider->supportsTypes($pattern, $types, $parameters)) {
$providers[] = $provider;
}
}
return $providers;
}
private function countItems($providers, $types, $parameters): int
@@ -82,12 +83,12 @@ class SearchApi
$countNq = $this->em->createNativeQuery($countQuery, $rsmCount);
$countNq->setParameters($parameters);
return $countNq->getSingleScalarResult();
return (int) $countNq->getSingleScalarResult();
}
private function buildCountQuery(array $queries, $types, $parameters)
{
$query = "SELECT COUNT(*) AS count FROM ({union_unordered}) AS sq";
$query = "SELECT SUM(c) AS count FROM ({union_unordered}) AS sq";
$unions = [];
$parameters = [];
@@ -141,17 +142,20 @@ class SearchApi
private function prepareProviders(array $rawResults)
{
$metadatas = [];
$providers = [];
foreach ($rawResults as $r) {
foreach ($this->providers as $k => $p) {
if ($p->supportsResult($r['key'], $r['metadata'])) {
$metadatas[$k][] = $r['metadata'];
$providers[$k] = $p;
break;
}
}
}
foreach ($metadatas as $k => $m) {
$this->providers[$k]->prepare($m);
$providers[$k]->prepare($m);
}
}

View File

@@ -4,6 +4,8 @@ namespace Chill\MainBundle\Search;
class SearchApiQuery
{
private array $select = [];
private array $selectParams = [];
private ?string $selectKey = null;
private array $selectKeyParams = [];
private ?string $jsonbMetadata = null;
@@ -15,6 +17,38 @@ class SearchApiQuery
private array $whereClauses = [];
private array $whereClausesParams = [];
public function addSelectClause(string $select, array $params = []): self
{
$this->select[] = $select;
$this->selectParams = [...$this->selectParams, ...$params];
return $this;
}
public function resetSelectClause(): self
{
$this->select = [];
$this->selectParams = [];
$this->selectKey = null;
$this->selectKeyParams = [];
$this->jsonbMetadata = null;
$this->jsonbMetadataParams = [];
$this->pertinence = null;
$this->pertinenceParams = [];
return $this;
}
public function getSelectClauses(): array
{
return $this->select;
}
public function getSelectParams(): array
{
return $this->selectParams;
}
public function setSelectKey(string $selectKey, array $params = []): self
{
$this->selectKey = $selectKey;
@@ -47,6 +81,16 @@ class SearchApiQuery
return $this;
}
public function getFromClause(): string
{
return $this->fromClause;
}
public function getFromParams(): array
{
return $this->fromClauseParams;
}
/**
* Set the where clause and replace all existing ones.
*
@@ -54,7 +98,7 @@ class SearchApiQuery
public function setWhereClauses(string $whereClause, array $params = []): self
{
$this->whereClauses = [$whereClause];
$this->whereClausesParams = [$params];
$this->whereClausesParams = $params;
return $this;
}
@@ -71,11 +115,53 @@ class SearchApiQuery
public function andWhereClause(string $whereClause, array $params = []): self
{
$this->whereClauses[] = $whereClause;
$this->whereClausesParams[] = $params;
\array_push($this->whereClausesParams, ...$params);
return $this;
}
private function buildSelectParams(bool $count = false): array
{
if ($count) {
return [];
}
$args = $this->getSelectParams();
if (null !== $this->selectKey) {
$args = [...$args, ...$this->selectKeyParams];
}
if (null !== $this->jsonbMetadata) {
$args = [...$args, ...$this->jsonbMetadataParams];
}
if (null !== $this->pertinence) {
$args = [...$args, ...$this->pertinenceParams];
}
return $args;
}
private function buildSelectClause(bool $countOnly = false): string
{
if ($countOnly) {
return 'count(*) AS c';
}
$selects = $this->getSelectClauses();
if (null !== $this->selectKey) {
$selects[] = \strtr("'{key}' AS key", [ '{key}' => $this->selectKey ]);
}
if (null !== $this->jsonbMetadata) {
$selects[] = \strtr('{metadata} AS metadata', [ '{metadata}' => $this->jsonbMetadata]);
}
if (null !== $this->pertinence) {
$selects[] = \strtr('{pertinence} AS pertinence', [ '{pertinence}' => $this->pertinence]);
}
return \implode(', ', $selects);
}
public function buildQuery(bool $countOnly = false): string
{
$isMultiple = count($this->whereClauses);
@@ -87,19 +173,8 @@ class SearchApiQuery
($isMultiple ? ')' : '')
;
if (!$countOnly) {
$select = \strtr("
'{key}' AS key,
{metadata} AS metadata,
{pertinence} AS pertinence
", [
'{key}' => $this->selectKey,
'{metadata}' => $this->jsonbMetadata,
'{pertinence}' => $this->pertinence,
]);
} else {
$select = "1 AS c";
}
$select = $this->buildSelectClause($countOnly);
return \strtr("SELECT
{select}
@@ -116,18 +191,16 @@ class SearchApiQuery
public function buildParameters(bool $countOnly = false): array
{
if (!$countOnly) {
return \array_merge(
$this->selectKeyParams,
$this->jsonbMetadataParams,
$this->pertinenceParams,
$this->fromClauseParams,
\array_merge([], ...$this->whereClausesParams),
);
return [
...$this->buildSelectParams($countOnly),
...$this->fromClauseParams,
...$this->whereClausesParams,
];
} else {
return \array_merge(
$this->fromClauseParams,
\array_merge([], ...$this->whereClausesParams),
);
return [
...$this->fromClauseParams,
...$this->whereClausesParams,
];
}
}
}

View File

@@ -10,10 +10,10 @@ use Chill\MainBundle\Search\HasAdvancedSearchFormInterface;
* installed into the app.
* the service is callable from the container with
* $container->get('chill_main.search_provider')
*
* the syntax for search string is :
* - domain, which begin with `@`. Example: `@person`. Restrict the search to some
* entities. It may exists multiple search provider for the same domain (example:
*
* the syntax for search string is :
* - domain, which begin with `@`. Example: `@person`. Restrict the search to some
* entities. It may exists multiple search provider for the same domain (example:
* a search provider for people which performs regular search, and suggestion search
* with phonetical algorithms
* - terms, which are the terms of the search. There are terms with argument (example :
@@ -25,17 +25,17 @@ class SearchProvider
{
/**
*
*
* @var SearchInterface[]
*/
private $searchServices = array();
/**
*
* @var HasAdvancedSearchForm[]
*/
private $hasAdvancedFormSearchServices = array();
/*
* return search services in an array, ordered by
* the order key (defined in service definition)
@@ -59,7 +59,7 @@ class SearchProvider
return $this->searchServices;
}
public function getHasAdvancedFormSearchServices()
{
//sort the array
@@ -75,7 +75,7 @@ class SearchProvider
/**
* parse the search string to extract domain and terms
*
*
* @param string $pattern
* @return string[] an array where the keys are _domain, _default (residual terms) or term
*/
@@ -95,9 +95,9 @@ class SearchProvider
/**
* Extract the domain of the subject
*
*
* The domain begins with `@`. Example: `@person`, `@report`, ....
*
*
* @param type $subject
* @return string
* @throws ParsingException
@@ -121,14 +121,15 @@ class SearchProvider
private function extractTerms(&$subject)
{
$terms = array();
preg_match_all('/([a-z\-]+):([\w\-]+|\([^\(\r\n]+\))/', $subject, $matches);
$matches = [];
preg_match_all('/([a-z\-]+):(([^"][\S\-]+)|"[^"]*")/', $subject, $matches);
foreach ($matches[2] as $key => $match) {
//remove from search pattern
$this->mustBeExtracted[] = $matches[0][$key];
//strip parenthesis
if (mb_substr($match, 0, 1) === '(' &&
mb_substr($match, mb_strlen($match) - 1) === ')') {
if (mb_substr($match, 0, 1) === '"' &&
mb_substr($match, mb_strlen($match) - 1) === '"') {
$match = trim(mb_substr($match, 1, mb_strlen($match) - 2));
}
$terms[$matches[1][$key]] = $match;
@@ -139,14 +140,14 @@ class SearchProvider
/**
* store string which must be extracted to find default arguments
*
*
* @var string[]
*/
private $mustBeExtracted = array();
/**
* extract default (residual) arguments
*
*
* @param string $subject
* @return string
*/
@@ -158,7 +159,7 @@ class SearchProvider
/**
* search through services which supports domain and give
* results as an array of resultsfrom different SearchInterface
*
*
* @param string $pattern
* @param number $start
* @param number $limit
@@ -167,25 +168,25 @@ class SearchProvider
* @return array of results from different SearchInterface
* @throws UnknowSearchDomainException if the domain is unknow
*/
public function getSearchResults($pattern, $start = 0, $limit = 50,
public function getSearchResults($pattern, $start = 0, $limit = 50,
array $options = array(), $format = 'html')
{
$terms = $this->parse($pattern);
$results = array();
//sort searchServices by order
$sortedSearchServices = array();
foreach($this->searchServices as $service) {
$sortedSearchServices[$service->getOrder()] = $service;
}
if ($terms['_domain'] !== NULL) {
foreach ($sortedSearchServices as $service) {
if ($service->supports($terms['_domain'], $format)) {
$results[] = $service->renderResult($terms, $start, $limit, $options);
}
}
if (count($results) === 0) {
throw new UnknowSearchDomainException($terms['_domain']);
}
@@ -196,24 +197,24 @@ class SearchProvider
}
}
}
//sort array
ksort($results);
return $results;
}
public function getResultByName($pattern, $name, $start = 0, $limit = 50,
array $options = array(), $format = 'html')
array $options = array(), $format = 'html')
{
$terms = $this->parse($pattern);
$search = $this->getByName($name);
if ($terms['_domain'] !== NULL && !$search->supports($terms['_domain'], $format))
{
throw new ParsingException("The domain is not supported for the search name");
}
return $search->renderResult($terms, $start, $limit, $options, $format);
}
@@ -232,16 +233,16 @@ class SearchProvider
throw new UnknowSearchNameException($name);
}
}
/**
* return searchservice with an advanced form, defined in service
* return searchservice with an advanced form, defined in service
* definition.
*
*
* @param string $name
* @return HasAdvancedSearchForm
* @throws UnknowSearchNameException
*/
public function getHasAdvancedFormByName($name)
public function getHasAdvancedFormByName($name)
{
if (\array_key_exists($name, $this->hasAdvancedFormSearchServices)) {
return $this->hasAdvancedFormSearchServices[$name];
@@ -253,7 +254,7 @@ class SearchProvider
public function addSearchService(SearchInterface $service, $name)
{
$this->searchServices[$name] = $service;
if ($service instanceof HasAdvancedSearchFormInterface) {
$this->hasAdvancedFormSearchServices[$name] = $service;
}
@@ -477,7 +478,7 @@ class SearchProvider
$string = strtr($string, $chars);
} /* remove from wordpress: we use only utf 8
* else {
// Assume ISO-8859-1 if not UTF-8
$chars['in'] = chr(128) . chr(131) . chr(138) . chr(142) . chr(154) . chr(158)
. chr(159) . chr(162) . chr(165) . chr(181) . chr(192) . chr(193) . chr(194)

View File

@@ -0,0 +1,36 @@
<?php
namespace Chill\MainBundle\Search\Utils;
use \DateTimeImmutable;
class ExtractDateFromPattern
{
private const DATE_PATTERN = [
["([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))", 'Y-m-d'], // 1981-05-12
["((0[1-9]|[12]\d|3[01])\/(0[1-9]|1[0-2])\/([12]\d{3}))", 'd/m/Y'], // 15/12/1980
["((0[1-9]|[12]\d|3[01])-(0[1-9]|1[0-2])-([12]\d{3}))", 'd-m-Y'], // 15/12/1980
];
public function extractDates(string $subject): SearchExtractionResult
{
$dates = [];
$filteredSubject = $subject;
foreach (self::DATE_PATTERN as [$pattern, $format]) {
$matches = [];
\preg_match_all($pattern, $filteredSubject, $matches);
foreach ($matches[0] as $match) {
$date = DateTimeImmutable::createFromFormat($format, $match);
if (false !== $date) {
$dates[] = $date;
// filter string: remove what is found
$filteredSubject = \trim(\strtr($filteredSubject, [$match => ""]));
}
}
}
return new SearchExtractionResult($filteredSubject, $dates);
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Chill\MainBundle\Search\Utils;
class ExtractPhonenumberFromPattern
{
private const PATTERN = "([\+]{0,1}[0-9\ ]{5,})";
public function extractPhonenumber(string $subject): SearchExtractionResult
{
$matches = [];
\preg_match(self::PATTERN, $subject,$matches);
if (0 < count($matches)) {
$phonenumber = [];
$length = 0;
foreach (\str_split(\trim($matches[0])) as $key => $char) {
switch ($char) {
case '0':
$length++;
if ($key === 0) { $phonenumber[] = '+32'; }
else { $phonenumber[] = $char; }
break;
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
$length++;
$phonenumber[] = $char;
break;
case ' ':
break;
default:
throw new \LogicException("should not match not alnum character");
}
}
if ($length > 5) {
$filtered = \trim(\strtr($subject, [$matches[0] => '']));
return new SearchExtractionResult($filtered, [\implode('', $phonenumber)] );
}
}
return new SearchExtractionResult($subject, []);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Chill\MainBundle\Search\Utils;
class SearchExtractionResult
{
private string $filteredSubject;
private array $found;
public function __construct(string $filteredSubject, array $found)
{
$this->filteredSubject = $filteredSubject;
$this->found = $found;
}
public function getFound(): array
{
return $this->found;
}
public function hasResult(): bool
{
return [] !== $this->found;
}
public function getFilteredSubject(): string
{
return $this->filteredSubject;
}
}

View File

@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace Chill\MainBundle\Security\Resolver;
use Chill\MainBundle\Entity\Center;
final class CenterResolverManager implements CenterResolverManagerInterface
{
/**
@@ -20,7 +22,17 @@ final class CenterResolverManager implements CenterResolverManagerInterface
{
foreach($this->resolvers as $resolver) {
if ($resolver->supports($entity, $options)) {
return (array) $resolver->resolveCenter($entity, $options);
$resolved = $resolver->resolveCenter($entity, $options);
if (null === $resolved) {
return [];
} elseif ($resolved instanceof Center) {
return [$resolved];
} elseif (\is_array($resolved)) {
return $resolved;
}
throw new \UnexpectedValueException(sprintf("the return type of a %s should be an instance of %s, an array or null. Resolver is %s",
CenterResolverInterface::class, Center::class, get_class($resolver)));
}
}

View File

@@ -6,20 +6,21 @@ use Twig\TwigFilter;
final class ResolverTwigExtension extends \Twig\Extension\AbstractExtension
{
private CenterResolverDispatcher $centerResolverDispatcher;
private CenterResolverManagerInterface $centerResolverDispatcher;
private ScopeResolverDispatcher $scopeResolverDispatcher;
/**
* @param CenterResolverDispatcher $centerResolverDispatcher
*/
public function __construct(CenterResolverDispatcher $centerResolverDispatcher)
public function __construct(CenterResolverManagerInterface $centerResolverDispatcher, ScopeResolverDispatcher $scopeResolverDispatcher)
{
$this->centerResolverDispatcher = $centerResolverDispatcher;
$this->scopeResolverDispatcher = $scopeResolverDispatcher;
}
public function getFilters()
{
return [
new TwigFilter('chill_resolve_center', [$this, 'resolveCenter'])
new TwigFilter('chill_resolve_center', [$this, 'resolveCenter']),
new TwigFilter('chill_resolve_scope', [$this, 'resolveScope']),
new TwigFilter('chill_is_scope_concerned', [$this, 'isScopeConcerned']),
];
}
@@ -30,7 +31,26 @@ final class ResolverTwigExtension extends \Twig\Extension\AbstractExtension
*/
public function resolveCenter($entity, ?array $options = [])
{
return $this->centerResolverDispatcher->resolveCenter($entity, $options);
return $this->centerResolverDispatcher->resolveCenters($entity, $options);
}
/**
* @param $entity
* @param array|null $options
* @return bool
*/
public function isScopeConcerned($entity, ?array $options = [])
{
return $this->scopeResolverDispatcher->isConcerned($entity, $options);
}
/**
* @param $entity
* @param array|null $options
* @return array|\Chill\MainBundle\Entity\Scope|\Chill\MainBundle\Entity\Scope[]
*/
public function resolveScope($entity, ?array $options = [])
{
return $this->scopeResolverDispatcher->resolveScope();
}
}

View File

@@ -1,18 +1,18 @@
<?php
/*
*
*
* Copyright (C) 2014-2021, Champs Libres Cooperative SCRLFS, <http://www.champs-libres.coop>
*
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
@@ -27,7 +27,7 @@ use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
/**
*
*
*
*/
class CenterNormalizer implements NormalizerInterface, DenormalizerInterface
@@ -52,7 +52,7 @@ class CenterNormalizer implements NormalizerInterface, DenormalizerInterface
public function supportsNormalization($data, string $format = null): bool
{
return $data instanceof Center;
return $data instanceof Center && $format === 'json';
}
public function denormalize($data, string $type, string $format = null, array $context = [])

View File

@@ -19,23 +19,78 @@
namespace Chill\MainBundle\Serializer\Normalizer;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
class DateNormalizer implements NormalizerInterface, DenormalizerInterface
class DateNormalizer implements ContextAwareNormalizerInterface, DenormalizerInterface
{
private RequestStack $requestStack;
private ParameterBagInterface $parameterBag;
public function __construct(RequestStack $requestStack, ParameterBagInterface $parameterBag)
{
$this->requestStack = $requestStack;
$this->parameterBag = $parameterBag;
}
public function normalize($date, string $format = null, array $context = array())
{
/** @var \DateTimeInterface $date */
return [
'datetime' => $date->format(\DateTimeInterface::ISO8601)
];
switch($format) {
case 'json':
return [
'datetime' => $date->format(\DateTimeInterface::ISO8601)
];
case 'docgen':
if (null === $date) {
return [
'long' => '', 'short' => ''
];
}
$hasTime = $date->format('His') !== "000000";
$request = $this->requestStack->getCurrentRequest();
$locale = null !== $request ? $request->getLocale() : $this->parameterBag->get('kernel.default_locale');
$formatterLong = \IntlDateFormatter::create(
$locale,
\IntlDateFormatter::LONG,
$hasTime ? \IntlDateFormatter::SHORT: \IntlDateFormatter::NONE
);
$formatterShort = \IntlDateFormatter::create(
$locale,
\IntlDateFormatter::SHORT,
$hasTime ? \IntlDateFormatter::SHORT: \IntlDateFormatter::NONE
);
return [
'short' => $formatterShort->format($date),
'long' => $formatterLong->format($date)
];
}
}
public function supportsNormalization($data, string $format = null): bool
public function supportsNormalization($data, string $format = null, array $context = []): bool
{
return $data instanceof \DateTimeInterface;
if ($format === 'json') {
return $data instanceof \DateTimeInterface;
} elseif ($format === 'docgen') {
return $data instanceof \DateTimeInterface || (
$data === null
&& \array_key_exists('docgen:expects', $context)
&& (
$context['docgen:expects'] === \DateTimeInterface::class
|| $context['docgen:expects'] === \DateTime::class
|| $context['docgen:expects'] === \DateTimeImmutable::class
)
);
}
return false;
}
public function denormalize($data, string $type, string $format = null, array $context = [])

View File

@@ -52,6 +52,6 @@ class UserNormalizer implements NormalizerInterface, NormalizerAwareInterface
public function supportsNormalization($data, string $format = null): bool
{
return $data instanceof User;
return $format === 'json' && $data instanceof User;
}
}

View File

@@ -12,7 +12,7 @@ class SearchApiQueryTest extends TestCase
$q = new SearchApiQuery();
$q->setSelectJsonbMetadata('boum')
->setSelectKey('bim')
->setSelectPertinence('1')
->setSelectPertinence('?', ['gamma'])
->setFromClause('badaboum')
->andWhereClause('foo', [ 'alpha' ])
->andWhereClause('bar', [ 'beta' ])
@@ -21,12 +21,12 @@ class SearchApiQueryTest extends TestCase
$query = $q->buildQuery();
$this->assertStringContainsString('(foo) AND (bar)', $query);
$this->assertEquals(['alpha', 'beta'], $q->buildParameters());
$this->assertEquals(['gamma', 'alpha', 'beta'], $q->buildParameters());
$query = $q->buildQuery(true);
$this->assertStringContainsString('(foo) AND (bar)', $query);
$this->assertEquals(['alpha', 'beta'], $q->buildParameters());
$this->assertEquals(['gamma', 'alpha', 'beta'], $q->buildParameters());
}
public function testWithoutWhereClause()
@@ -42,4 +42,20 @@ class SearchApiQueryTest extends TestCase
$this->assertEquals([], $q->buildParameters());
}
public function testBuildParams()
{
$q = new SearchApiQuery();
$q
->addSelectClause('bada', [ 'one', 'two' ])
->addSelectClause('boum', ['three', 'four'])
->setWhereClauses('mywhere', [ 'six', 'seven'])
;
$params = $q->buildParameters();
$this->assertEquals(['six', 'seven'], $q->buildParameters(true));
$this->assertEquals(['one', 'two', 'three', 'four', 'six', 'seven'], $q->buildParameters());
}
}

View File

@@ -26,17 +26,17 @@ use PHPUnit\Framework\TestCase;
class SearchProviderTest extends TestCase
{
/**
*
* @var SearchProvider
* @var SearchProvider
*/
private $search;
public function setUp()
{
$this->search = new SearchProvider();
//add a default service
$this->addSearchService(
$this->createDefaultSearchService('I am default', 10), 'default'
@@ -46,7 +46,7 @@ class SearchProviderTest extends TestCase
$this->createNonDefaultDomainSearchService('I am domain bar', 20, FALSE), 'bar'
);
}
/**
* @expectedException \Chill\MainBundle\Search\UnknowSearchNameException
*/
@@ -54,11 +54,11 @@ class SearchProviderTest extends TestCase
{
$this->search->getByName("invalid name");
}
public function testSimplePattern()
{
$terms = $this->p("@person birthdate:2014-01-02 name:(my name) is not my name");
$terms = $this->p("@person birthdate:2014-01-02 name:\"my name\" is not my name");
$this->assertEquals(array(
'_domain' => 'person',
'birthdate' => '2014-01-02',
@@ -66,40 +66,40 @@ class SearchProviderTest extends TestCase
'name' => 'my name'
), $terms);
}
public function testWithoutDomain()
{
$terms = $this->p('foo:bar residual');
$this->assertEquals(array(
'_domain' => null,
'foo' => 'bar',
'_default' => 'residual'
), $terms);
}
public function testWithoutDefault()
{
$terms = $this->p('@person foo:bar');
$this->assertEquals(array(
'_domain' => 'person',
'foo' => 'bar',
'_default' => ''
), $terms);
}
public function testCapitalLetters()
{
$terms = $this->p('Foo:Bar LOL marCi @PERSON');
$this->assertEquals(array(
'_domain' => 'person',
'_default' => 'lol marci',
'foo' => 'bar'
), $terms);
}
/**
* @expectedException Chill\MainBundle\Search\ParsingException
*/
@@ -107,12 +107,11 @@ class SearchProviderTest extends TestCase
{
$term = $this->p("@person @report");
}
public function testDoubleParenthesis()
{
$terms = $this->p("@papamobile name:(my beautiful name) residual "
. "surname:(i love techno)");
$terms = $this->p('@papamobile name:"my beautiful name" residual surname:"i love techno"');
$this->assertEquals(array(
'_domain' => 'papamobile',
'name' => 'my beautiful name',
@@ -120,65 +119,65 @@ class SearchProviderTest extends TestCase
'surname' => 'i love techno'
), $terms);
}
public function testAccentued()
{
//$this->markTestSkipped('accentued characters must be implemented');
$terms = $this->p('manço bélier aztèque à saloù ê');
$this->assertEquals(array(
'_domain' => NULL,
'_default' => 'manco belier azteque a salou e'
), $terms);
}
public function testAccentuedCapitals()
{
//$this->markTestSkipped('accentued characters must be implemented');
$terms = $this->p('MANÉÀ oÛ lÎ À');
$this->assertEquals(array(
'_domain' => null,
'_default' => 'manea ou li a'
), $terms);
}
public function testTrimInParenthesis()
{
$terms = $this->p('foo:(bar )');
$terms = $this->p('foo:"bar "');
$this->assertEquals(array(
'_domain' => null,
'foo' => 'bar',
'_default' => ''
), $terms);
}
public function testTrimInDefault()
{
$terms = $this->p(' foo bar ');
$this->assertEquals(array(
'_domain' => null,
'_default' => 'foo bar'
), $terms);
}
public function testArgumentNameWithTrait()
{
$terms = $this->p('date-from:2016-05-04');
$this->assertEquals(array(
'_domain' => null,
'date-from' => '2016-05-04',
'_default' => ''
), $terms);
}
/**
* Test the behaviour when no domain is provided in the search pattern :
* Test the behaviour when no domain is provided in the search pattern :
* the default search should be enabled
*/
public function testSearchResultDefault()
@@ -186,12 +185,12 @@ class SearchProviderTest extends TestCase
$response = $this->search->getSearchResults('default search');
//$this->markTestSkipped();
$this->assertEquals(array(
"I am default"
), $response);
), $response);
}
/**
* @expectedException \Chill\MainBundle\Search\UnknowSearchDomainException
*/
@@ -200,49 +199,49 @@ class SearchProviderTest extends TestCase
$response = $this->search->getSearchResults('@unknow domain');
//$this->markTestSkipped();
}
public function testSearchResultDomainSearch()
{
//add a search service which will be supported
$this->addSearchService(
$this->createNonDefaultDomainSearchService("I am domain foo", 100, TRUE), 'foo'
);
$response = $this->search->getSearchResults('@foo default search');
$this->assertEquals(array(
"I am domain foo"
), $response);
}
public function testSearchWithinSpecificSearchName()
{
//add a search service which will be supported
$this->addSearchService(
$this->createNonDefaultDomainSearchService("I am domain foo", 100, TRUE), 'foo'
);
$response = $this->search->getResultByName('@foo search', 'foo');
$this->assertEquals('I am domain foo', $response);
}
/**
* @expectedException \Chill\MainBundle\Search\ParsingException
*/
public function testSearchWithinSpecificSearchNameInConflictWithSupport()
{
$response = $this->search->getResultByName('@foo default search', 'bar');
}
/**
* shortcut for executing parse method
*
*
* @param unknown $pattern
* @return string[]
*/
@@ -250,12 +249,12 @@ class SearchProviderTest extends TestCase
{
return $this->search->parse($pattern);
}
/**
* Add a search service to the chill.main.search_provider
*
*
* Useful for mocking the SearchInterface
*
*
* @param SearchInterface $search
* @param string $name
*/
@@ -264,52 +263,52 @@ class SearchProviderTest extends TestCase
$this->search
->addSearchService($search, $name);
}
private function createDefaultSearchService($result, $order)
{
$mock = $this
->getMockForAbstractClass('Chill\MainBundle\Search\AbstractSearch');
//set the mock as default
$mock->expects($this->any())
->method('isActiveByDefault')
->will($this->returnValue(TRUE));
$mock->expects($this->any())
->method('getOrder')
->will($this->returnValue($order));
//set the return value
$mock->expects($this->any())
->method('renderResult')
->will($this->returnValue($result));
return $mock;
}
private function createNonDefaultDomainSearchService($result, $order, $domain)
{
$mock = $this
->getMockForAbstractClass('Chill\MainBundle\Search\AbstractSearch');
//set the mock as default
$mock->expects($this->any())
->method('isActiveByDefault')
->will($this->returnValue(FALSE));
$mock->expects($this->any())
->method('getOrder')
->will($this->returnValue($order));
$mock->expects($this->any())
->method('supports')
->will($this->returnValue($domain));
//set the return value
$mock->expects($this->any())
->method('renderResult')
->will($this->returnValue($result));
return $mock;
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Search\Utils;
use Chill\MainBundle\Search\Utils\ExtractDateFromPattern;
use PHPUnit\Framework\TestCase;
class ExtractDateFromPatternTest extends TestCase
{
/**
* @dataProvider provideSubjects
*/
public function testExtractDate(string $subject, string $filtered, int $count, ...$datesSearched)
{
$extractor = new ExtractDateFromPattern();
$result = $extractor->extractDates($subject);
$this->assertCount($count, $result->getFound());
$this->assertEquals($filtered, $result->getFilteredSubject());
$this->assertContainsOnlyInstancesOf(\DateTimeImmutable::class, $result->getFound());
$dates = \array_map(
function (\DateTimeImmutable $d) {
return $d->format('Y-m-d');
}, $result->getFound()
);
foreach ($datesSearched as $date) {
$this->assertContains($date, $dates);
}
}
public function provideSubjects()
{
yield ["15/06/1981", "", 1, '1981-06-15'];
yield ["15/06/1981 30/12/1987", "", 2, '1981-06-15', '1987-12-30'];
yield ["diallo 15/06/1981", "diallo", 1, '1981-06-15'];
yield ["diallo 31/03/1981", "diallo", 1, '1981-03-31'];
yield ["diallo 15-06-1981", "diallo", 1, '1981-06-15'];
yield ["diallo 1981-12-08", "diallo", 1, '1981-12-08'];
yield ["diallo", "diallo", 0];
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Search\Utils;
use Chill\MainBundle\Search\Utils\ExtractPhonenumberFromPattern;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
class ExtractPhonenumberFromPatternTest extends KernelTestCase
{
/**
* @dataProvider provideData
*/
public function testExtract($subject, $expectedCount, $expected, $filteredSubject, $msg)
{
$extractor = new ExtractPhonenumberFromPattern();
$result = $extractor->extractPhonenumber($subject);
$this->assertCount($expectedCount, $result->getFound());
$this->assertEquals($filteredSubject, $result->getFilteredSubject());
$this->assertEquals($expected, $result->getFound());
}
public function provideData()
{
yield ['Diallo', 0, [], 'Diallo', "no phonenumber"];
yield ['Diallo 15/06/2021', 0, [], 'Diallo 15/06/2021', "no phonenumber and a date"];
yield ['Diallo 0486 123 456', 1, ['+32486123456'], 'Diallo', "a phonenumber and a name"];
yield ['Diallo 123 456', 1, ['123456'], 'Diallo', "a number and a name, without leadiing 0"];
yield ['123 456', 1, ['123456'], '', "only phonenumber"];
yield ['0123 456', 1, ['+32123456'], '', "only phonenumber with a leading 0"];
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace Serializer\Normalizer;
use Chill\MainBundle\Serializer\Normalizer\DateNormalizer;
use Prophecy\Prophet;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
class DateNormalizerTest extends KernelTestCase
{
private Prophet $prophet;
public function setUp()
{
$this->prophet = new Prophet();
}
public function testSupports()
{
$this->assertTrue($this->buildDateNormalizer()->supportsNormalization(new \DateTime(), 'json'));
$this->assertTrue($this->buildDateNormalizer()->supportsNormalization(new \DateTimeImmutable(), 'json'));
$this->assertTrue($this->buildDateNormalizer()->supportsNormalization(new \DateTime(), 'docgen'));
$this->assertTrue($this->buildDateNormalizer()->supportsNormalization(new \DateTimeImmutable(), 'docgen'));
$this->assertTrue($this->buildDateNormalizer()->supportsNormalization(null, 'docgen', ['docgen:expects' => \DateTimeImmutable::class]));
$this->assertTrue($this->buildDateNormalizer()->supportsNormalization(null, 'docgen', ['docgen:expects' => \DateTimeInterface::class]));
$this->assertTrue($this->buildDateNormalizer()->supportsNormalization(null, 'docgen', ['docgen:expects' => \DateTime::class]));
$this->assertFalse($this->buildDateNormalizer()->supportsNormalization(new \stdClass(), 'docgen'));
$this->assertFalse($this->buildDateNormalizer()->supportsNormalization(new \DateTime(), 'xml'));
}
/**
* @dataProvider generateDataNormalize
*/
public function testNormalize($expected, $date, $format, $locale, $msg)
{
$this->assertEquals($expected, $this->buildDateNormalizer($locale)->normalize($date, $format, []), $msg);
}
private function buildDateNormalizer(string $locale = null): DateNormalizer
{
$requestStack = $this->prophet->prophesize(RequestStack::class);
$parameterBag = new ParameterBag();
$parameterBag->set('kernel.default_locale', 'fr');
if ($locale === null) {
$requestStack->getCurrentRequest()->willReturn(null);
} else {
$request = $this->prophet->prophesize(Request::class);
$request->getLocale()->willReturn($locale);
$requestStack->getCurrentRequest()->willReturn($request->reveal());
}
return new DateNormalizer($requestStack->reveal(), $parameterBag);
}
public function generateDataNormalize()
{
$datetime = \DateTime::createFromFormat('Y-m-d H:i:sO', '2021-06-05 15:05:01+02:00');
$date = \DateTime::createFromFormat('Y-m-d H:i:sO', '2021-06-05 00:00:00+02:00');
yield [
['datetime' => '2021-06-05T15:05:01+0200'],
$datetime, 'json', null, 'simple normalization to json'
];
yield [
['long' => '5 juin 2021', 'short' => '05/06/2021'],
$date, 'docgen', 'fr', 'normalization to docgen for a date, with current request'
];
yield [
['long' => '5 juin 2021', 'short' => '05/06/2021'],
$date, 'docgen', null, 'normalization to docgen for a date, without current request'
];
yield [
['long' => '5 juin 2021 à 15:05', 'short' => '05/06/2021 15:05'],
$datetime, 'docgen', null, 'normalization to docgen for a datetime, without current request'
];
yield [
['long' => '', 'short' => ''],
null, 'docgen', null, 'normalization to docgen for a null datetime'
];
}
}

View File

@@ -128,4 +128,16 @@ services:
Chill\MainBundle\Form\DataTransform\AddressToIdDataTransformer: ~
Chill\MainBundle\Form\DataTransform\AddressToIdDataTransformer:
autoconfigure: true
autowire: true
Chill\MainBundle\Form\LocationFormType:
autowire: true
autoconfigure: true
Chill\MainBundle\Form\UserCurrentLocationType:
autowire: true
autoconfigure: true
Chill\MainBundle\Form\Type\LocationFormType: ~

View File

@@ -7,8 +7,8 @@ services:
resource: '../../Routing/MenuBuilder'
Chill\MainBundle\Routing\MenuBuilder\UserMenuBuilder:
arguments:
$tokenStorage: '@Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface'
autowire: true
autoconfigure: true
tags:
- { name: 'chill.menu_builder' }

View File

@@ -8,7 +8,16 @@ services:
Chill\MainBundle\Search\SearchProvider: '@chill_main.search_provider'
Chill\MainBundle\Search\SearchApi: ~
Chill\MainBundle\Search\SearchApi:
autowire: true
autoconfigure: true
arguments:
$providers: !tagged_iterator chill.search_api_provider
Chill\MainBundle\Search\Entity\:
resource: '../../Search/Entity'
Chill\MainBundle\Search\Utils\:
autowire: true
autoconfigure: true
resource: './../Search/Utils/'

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Chill\Migrations\Main;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Add current location to User entity
*/
final class Version20211116162847 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add current location to User entity';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE users ADD currentLocation_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE users ADD CONSTRAINT FK_1483A5E93C219753 FOREIGN KEY (currentLocation_id) REFERENCES chill_main_location (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('CREATE INDEX IDX_1483A5E93C219753 ON users (currentLocation_id)');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE users DROP CONSTRAINT FK_1483A5E93C219753');
$this->addSql('DROP INDEX IDX_1483A5E93C219753');
$this->addSql('ALTER TABLE users DROP currentLocation_id');
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Chill\Migrations\Main;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20211119173554 extends AbstractMigration
{
public function getDescription(): string
{
return 'remove comment on deprecated json_array type';
}
public function up(Schema $schema): void
{
$columns = [
'users.attributes'
];
foreach ($columns as $col) {
$this->addSql("COMMENT ON COLUMN $col IS NULL");
}
}
public function down(Schema $schema): void
{
$this->throwIrreversibleMigrationException();
}
}

View File

@@ -86,6 +86,10 @@ address more:
Create a new address: Créer une nouvelle adresse
Create an address: Créer une adresse
Update address: Modifier l'adresse
City or postal code: Ville ou code postal
# contact
Part of the phonenumber: Partie du numéro de téléphone
#serach
Your search is empty. Please provide search terms.: La recherche est vide. Merci de fournir des termes de recherche.
@@ -176,6 +180,13 @@ Flags: Drapeaux
# admin section for users jobs
User jobs: Métiers
# user page for current location
Current location: Localisation actuelle
Edit my current location: Éditer ma localisation actuelle
Change current location: Changer ma localisation actuelle
Set a location: Indiquer une localisation
Current location successfully updated: Localisation actuelle mise à jour
Pick a location: Choisir un lieu
#admin section for circles (old: scopes)
List circles: Cercles
@@ -190,7 +201,7 @@ Location: Localisation
Location type list: Liste des types de localisation
Create a new location type: Créer un nouveau type de localisation
Available for users: Disponible aux utilisateurs
Address required: Adresse requise?
Address required: Adresse requise?
Contact data: Données de contact?
optional: optionnel
required: requis
@@ -200,8 +211,7 @@ Location list: Liste des localisations
Location type: Type de localisation
Phonenumber1: Numéro de téléphone
Phonenumber2: Autre numéro de téléphone
Configure location: Configuration des localisations
Configure location type: Configuration des types de localisations
Configure location and location type: Configuration des localisations
# circles / scopes
Choose the circle: Choisir le cercle

View File

@@ -0,0 +1,35 @@
<?php
namespace Chill\PersonBundle\DataFixtures\Helper;
use Chill\PersonBundle\Entity\Person;
use Doctrine\ORM\EntityManagerInterface;
trait RandomPersonHelperTrait
{
private ?int $nbOfPersons = null;
protected function getRandomPerson(EntityManagerInterface $em): Person
{
$qb = $em->createQueryBuilder();
$qb
->from(Person::class, 'p')
;
if (null === $this->nbOfPersons) {
$this->nbOfPersons = $qb
->select('COUNT(p)')
->getQuery()
->getSingleScalarResult()
;
}
return $qb
->select('p')
->setMaxResults(1)
->setFirstResult(\random_int(0, $this->nbOfPersons))
->getQuery()
->getSingleResult()
;
}
}

View File

@@ -86,13 +86,6 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
$loader->load('services/security.yaml');
$loader->load('services/doctrineEventListener.yaml');
// load service advanced search only if configure
if ($config['search']['search_by_phone'] != 'never') {
$loader->load('services/search_by_phone.yaml');
$container->setParameter('chill_person.search.search_by_phone',
$config['search']['search_by_phone']);
}
if ($container->getParameter('chill_person.accompanying_period') !== 'hidden') {
$loader->load('services/exports_accompanying_period.yaml');
}

View File

@@ -27,19 +27,6 @@ class Configuration implements ConfigurationInterface
$rootNode
->canBeDisabled()
->children()
->arrayNode('search')
->canBeDisabled()
->children()
->enumNode('search_by_phone')
->values(['always', 'on-domain', 'never'])
->defaultValue('on-domain')
->info('enable search by phone. \'always\' show the result '
. 'on every result. \'on-domain\' will show the result '
. 'only if the domain is given in the search box. '
. '\'never\' disable this feature')
->end()
->end() //children for 'search', parent = array node 'search'
->end() // array 'search', parent = children of root
->arrayNode('validation')
->canBeDisabled()
->children()

View File

@@ -1150,11 +1150,11 @@ class AccompanyingPeriod implements TrackCreationInterface, TrackUpdateInterface
public function getGroupSequence()
{
if ($this->getStep() == self::STEP_DRAFT) {
if ($this->getStep() == self::STEP_DRAFT)
{
return [[self::STEP_DRAFT]];
}
if ($this->getStep() == self::STEP_CONFIRMED) {
} elseif ($this->getStep() == self::STEP_CONFIRMED)
{
return [[self::STEP_DRAFT, self::STEP_CONFIRMED]];
}

View File

@@ -434,6 +434,5 @@ class Household
->addViolation();
}
}
dump($cond);
}
}

View File

@@ -37,20 +37,20 @@ class MaritalStatus
* @ORM\Id()
* @ORM\Column(type="string", length=7)
*/
private $id;
private ?string $id;
/**
* @var string array
* @ORM\Column(type="json")
*/
private $name;
private array $name;
/**
* Get id
*
* @return string
*/
public function getId()
public function getId(): string
{
return $this->id;
}
@@ -61,7 +61,7 @@ class MaritalStatus
* @param string $id
* @return MaritalStatus
*/
public function setId($id)
public function setId(string $id): self
{
$this->id = $id;
return $this;
@@ -73,7 +73,7 @@ class MaritalStatus
* @param string array $name
* @return MaritalStatus
*/
public function setName($name)
public function setName(array $name): self
{
$this->name = $name;
@@ -85,7 +85,7 @@ class MaritalStatus
*
* @return string array
*/
public function getName()
public function getName(): array
{
return $this->name;
}

View File

@@ -39,10 +39,16 @@ use DateTimeInterface;
*
* @ORM\Entity
* @ORM\Table(name="chill_person_person",
* indexes={@ORM\Index(
* indexes={
* @ORM\Index(
* name="person_names",
* columns={"firstName", "lastName"}
* )})
* ),
* @ORM\Index(
* name="person_birthdate",
* columns={"birthdate"}
* )
* })
* @ORM\HasLifecycleCallbacks()
* @DiscriminatorMap(typeProperty="type", mapping={
* "person"=Person::class
@@ -213,7 +219,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* groups={"general", "creation"}
* )
*/
private ?\DateTime $maritalStatusDate;
private ?\DateTime $maritalStatusDate = null;
/**
* Comment on marital status
@@ -246,7 +252,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* The person's phonenumber
* @var string
*
* @ORM\Column(type="text", length=40, nullable=true)
* @ORM\Column(type="text")
* @Assert\Regex(
* pattern="/^([\+{1}])([0-9\s*]{4,20})$/",
* groups={"general", "creation"}
@@ -256,13 +262,13 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* groups={"general", "creation"}
* )
*/
private $phonenumber = '';
private string $phonenumber = '';
/**
* The person's mobile phone number
* @var string
*
* @ORM\Column(type="text", length=40, nullable=true)
* @ORM\Column(type="text")
* @Assert\Regex(
* pattern="/^([\+{1}])([0-9\s*]{4,20})$/",
* groups={"general", "creation"}
@@ -272,7 +278,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* groups={"general", "creation"}
* )
*/
private $mobilenumber = '';
private string $mobilenumber = '';
/**
* @var Collection
@@ -1088,9 +1094,9 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
/**
* Get nationality
*
* @return Chill\MainBundle\Entity\Country
* @return Country
*/
public function getNationality()
public function getNationality(): ?Country
{
return $this->nationality;
}
@@ -1170,7 +1176,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
*
* @return string
*/
public function getPhonenumber()
public function getPhonenumber(): string
{
return $this->phonenumber;
}
@@ -1193,7 +1199,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
*
* @return string
*/
public function getMobilenumber()
public function getMobilenumber(): string
{
return $this->mobilenumber;
}

View File

@@ -9,7 +9,10 @@ use Doctrine\ORM\Mapping as ORM;
* Person Phones
*
* @ORM\Entity
* @ORM\Table(name="chill_person_phone")
* @ORM\Table(name="chill_person_phone",
* indexes={
* @ORM\Index(name="phonenumber", columns={"phonenumber"})
* })
*/
class PersonPhone
{
@@ -107,7 +110,7 @@ class PersonPhone
{
$this->date = $date;
}
public function isEmpty(): bool
{
return empty($this->getDescription()) && empty($this->getPhonenumber());

View File

@@ -31,7 +31,7 @@ class GenderType extends AbstractType {
'choices' => $a,
'expanded' => true,
'multiple' => false,
'placeholder' => null
'placeholder' => null,
));
}

View File

@@ -2,11 +2,15 @@
namespace Chill\PersonBundle\Repository;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Repository\CountryRepository;
use Chill\MainBundle\Search\ParsingException;
use Chill\MainBundle\Search\SearchApi;
use Chill\MainBundle\Search\SearchApiQuery;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\NonUniqueResultException;
use Doctrine\ORM\NoResultException;
@@ -49,125 +53,114 @@ final class PersonACLAwareRepository implements PersonACLAwareRepositoryInterfac
string $default = null,
string $firstname = null,
string $lastname = null,
?\DateTime $birthdate = null,
?\DateTime $birthdateBefore = null,
?\DateTime $birthdateAfter = null,
?\DateTimeInterface $birthdate = null,
?\DateTimeInterface $birthdateBefore = null,
?\DateTimeInterface $birthdateAfter = null,
string $gender = null,
string $countryCode = null
string $countryCode = null,
string $phonenumber = null,
string $city = null
): array {
$qb = $this->createSearchQuery($default, $firstname, $lastname,
$query = $this->buildAuthorizedQuery($default, $firstname, $lastname,
$birthdate, $birthdateBefore, $birthdateAfter, $gender,
$countryCode);
$this->addACLClauses($qb, 'p');
$countryCode, $phonenumber, $city);
return $this->getQueryResult($qb, 'p', $simplify, $limit, $start);
}
/**
* Helper method to prepare and return the search query for PersonACL.
*
* This method replace the select clause with required parameters, depending on the
* "simplify" parameter. It also add query limits.
*
* The given alias must represent the person alias.
*
* @return array|Person[]
*/
public function getQueryResult(QueryBuilder $qb, string $alias, bool $simplify, int $limit, int $start): array
{
if ($simplify) {
$qb->select(
$alias.'.id',
$qb->expr()->concat(
$alias.'.firstName',
$qb->expr()->literal(' '),
$alias.'.lastName'
).'AS text'
);
} else {
$qb->select($alias);
}
$qb
->setMaxResults($limit)
->setFirstResult($start);
//order by firstname, lastname
$qb
->orderBy($alias.'.firstName')
->addOrderBy($alias.'.lastName');
if ($simplify) {
return $qb->getQuery()->getResult(Query::HYDRATE_ARRAY);
} else {
return $qb->getQuery()->getResult();
}
return $this->fetchQueryPerson($query);
}
public function countBySearchCriteria(
string $default = null,
string $firstname = null,
string $lastname = null,
?\DateTime $birthdate = null,
?\DateTime $birthdateBefore = null,
?\DateTime $birthdateAfter = null,
?\DateTimeInterface $birthdate = null,
?\DateTimeInterface $birthdateBefore = null,
?\DateTimeInterface $birthdateAfter = null,
string $gender = null,
string $countryCode = null
string $countryCode = null,
string $phonenumber = null,
string $city = null
): int {
$qb = $this->createSearchQuery($default, $firstname, $lastname,
$query = $this->buildAuthorizedQuery($default, $firstname, $lastname,
$birthdate, $birthdateBefore, $birthdateAfter, $gender,
$countryCode);
$this->addACLClauses($qb, 'p');
$countryCode, $phonenumber, $city)
;
return $this->getCountQueryResult($qb,'p');
return $this->fetchQueryCount($query);
}
public function fetchQueryCount(SearchApiQuery $query): int
{
$rsm = new Query\ResultSetMapping();
$rsm->addScalarResult('c', 'c');
$nql = $this->em->createNativeQuery($query->buildQuery(true), $rsm);
$nql->setParameters($query->buildParameters(true));
return $nql->getSingleScalarResult();
}
/**
* Helper method to prepare and return the count for search query
*
* This method replace the select clause with required parameters, depending on the
* "simplify" parameter.
*
* The given alias must represent the person alias in the query builder.
* @return array|Person[]
*/
public function getCountQueryResult(QueryBuilder $qb, $alias): int
public function fetchQueryPerson(SearchApiQuery $query, ?int $start = 0, ?int $limit = 50): array
{
$qb->select('COUNT('.$alias.'.id)');
$rsm = new Query\ResultSetMappingBuilder($this->em);
$rsm->addRootEntityFromClassMetadata(Person::class, 'person');
return $qb->getQuery()->getSingleScalarResult();
$query->addSelectClause($rsm->generateSelectClause());
$nql = $this->em->createNativeQuery(
$query->buildQuery()." ORDER BY pertinence DESC OFFSET ? LIMIT ?", $rsm
)->setParameters(\array_merge($query->buildParameters(), [$start, $limit]));
return $nql->getResult();
}
public function findBySimilaritySearch(string $pattern, int $firstResult,
int $maxResult, bool $simplify = false)
{
$qb = $this->createSimilarityQuery($pattern);
$this->addACLClauses($qb, 'sp');
public function buildAuthorizedQuery(
string $default = null,
string $firstname = null,
string $lastname = null,
?\DateTimeInterface $birthdate = null,
?\DateTimeInterface $birthdateBefore = null,
?\DateTimeInterface $birthdateAfter = null,
string $gender = null,
string $countryCode = null,
string $phonenumber = null,
string $city = null
): SearchApiQuery {
$query = $this->createSearchQuery($default, $firstname, $lastname,
$birthdate, $birthdateBefore, $birthdateAfter, $gender,
$countryCode, $phonenumber)
;
return $this->getQueryResult($qb, 'sp', $simplify, $maxResult, $firstResult);
return $this->addAuthorizations($query);
}
public function countBySimilaritySearch(string $pattern)
private function addAuthorizations(SearchApiQuery $query): SearchApiQuery
{
$qb = $this->createSimilarityQuery($pattern);
$this->addACLClauses($qb, 'sp');
$authorizedCenters = $this->authorizationHelper
->getReachableCenters($this->security->getUser(), PersonVoter::SEE);
return $this->getCountQueryResult($qb, 'sp');
if ([] === $authorizedCenters) {
return $query->andWhereClause("FALSE = TRUE", []);
}
return $query
->andWhereClause(
strtr(
"person.center_id IN ({{ center_ids }})",
[
'{{ center_ids }}' => \implode(', ',
\array_fill(0, count($authorizedCenters), '?')),
]
),
\array_map(function(Center $c) {return $c->getId();}, $authorizedCenters)
);
}
/**
* Create a search query without ACL
*
* The person alias is a "p"
*
* @param string|null $default
* @param string|null $firstname
* @param string|null $lastname
* @param \DateTime|null $birthdate
* @param \DateTime|null $birthdateBefore
* @param \DateTime|null $birthdateAfter
* @param string|null $gender
* @param string|null $countryCode
* @return QueryBuilder
* @throws NonUniqueResultException
* @throws ParsingException
*/
@@ -175,118 +168,107 @@ final class PersonACLAwareRepository implements PersonACLAwareRepositoryInterfac
string $default = null,
string $firstname = null,
string $lastname = null,
?\DateTime $birthdate = null,
?\DateTime $birthdateBefore = null,
?\DateTime $birthdateAfter = null,
?\DateTimeInterface $birthdate = null,
?\DateTimeInterface $birthdateBefore = null,
?\DateTimeInterface $birthdateAfter = null,
string $gender = null,
string $countryCode = null
): QueryBuilder {
string $countryCode = null,
string $phonenumber = null,
string $city = null
): SearchApiQuery {
$query = new SearchApiQuery();
$query
->setFromClause("chill_person_person AS person")
;
if (!$this->security->getUser() instanceof User) {
throw new \RuntimeException("Search must be performed by a valid user");
}
$qb = $this->em->createQueryBuilder();
$qb->from(Person::class, 'p');
$pertinence = [];
$pertinenceArgs = [];
$orWhereSearchClause = [];
$orWhereSearchClauseArgs = [];
if (NULL !== $firstname) {
$qb->andWhere($qb->expr()->like('UNACCENT(LOWER(p.firstName))', ':firstname'))
->setParameter('firstname', '%'.$firstname.'%');
}
if ("" !== $default) {
foreach (\explode(" ", $default) as $str) {
$pertinence[] =
"STRICT_WORD_SIMILARITY(LOWER(UNACCENT(?)), person.fullnamecanonical) + ".
"(person.fullnamecanonical LIKE '%' || LOWER(UNACCENT(?)) || '%')::int + ".
"(EXISTS (SELECT 1 FROM unnest(string_to_array(fullnamecanonical, ' ')) AS t WHERE starts_with(t, UNACCENT(LOWER(?)))))::int";
\array_push($pertinenceArgs, $str, $str, $str);
if (NULL !== $lastname) {
$qb->andWhere($qb->expr()->like('UNACCENT(LOWER(p.lastName))', ':lastname'))
->setParameter('lastname', '%'.$lastname.'%');
$orWhereSearchClause[] =
"(LOWER(UNACCENT(?)) <<% person.fullnamecanonical OR ".
"person.fullnamecanonical LIKE '%' || LOWER(UNACCENT(?)) || '%' )";
\array_push($orWhereSearchClauseArgs, $str, $str);
}
$query->andWhereClause(\implode(' OR ', $orWhereSearchClause),
$orWhereSearchClauseArgs);
} else {
$pertinence = ["1"];
$pertinenceArgs = [];
}
$query
->setSelectPertinence(\implode(' + ', $pertinence), $pertinenceArgs)
;
if (NULL !== $birthdate) {
$qb->andWhere($qb->expr()->eq('p.birthdate', ':birthdate'))
->setParameter('birthdate', $birthdate);
$query->andWhereClause(
"person.birthdate = ?::date",
[$birthdate->format('Y-m-d')]
);
}
if (NULL !== $birthdateAfter) {
$qb->andWhere($qb->expr()->gt('p.birthdate', ':birthdateafter'))
->setParameter('birthdateafter', $birthdateAfter);
if (NULL !== $firstname) {
$query->andWhereClause(
"UNACCENT(LOWER(person.firstname)) LIKE '%' || UNACCENT(LOWER(?)) || '%'",
[$firstname]
);
}
if (NULL !== $lastname) {
$query->andWhereClause(
"UNACCENT(LOWER(person.lastname)) LIKE '%' || UNACCENT(LOWER(?)) || '%'",
[$lastname]
);
}
if (NULL !== $birthdateBefore) {
$qb->andWhere($qb->expr()->lt('p.birthdate', ':birthdatebefore'))
->setParameter('birthdatebefore', $birthdateBefore);
$query->andWhereClause(
'p.birthdate < ?::date',
[$birthdateBefore->format('Y-m-d')]
);
}
if (NULL !== $gender) {
$qb->andWhere($qb->expr()->eq('p.gender', ':gender'))
->setParameter('gender', $gender);
if (NULL !== $birthdateAfter) {
$query->andWhereClause(
'p.birthdate > ?::date',
[$birthdateAfter->format('Y-m-d')]
);
}
if (NULL !== $countryCode) {
try {
$country = $this->countryRepository->findOneBy(['countryCode' => $countryCode]);
} catch (NoResultException $ex) {
throw new ParsingException('The country code "'.$countryCode.'" '
. ', used in nationality, is unknow', 0, $ex);
} catch (NonUniqueResultException $e) {
throw $e;
}
$qb->andWhere($qb->expr()->eq('p.nationality', ':nationality'))
->setParameter('nationality', $country);
if (NULL !== $phonenumber) {
$query->andWhereClause(
"person.phonenumber LIKE '%' || ? || '%' OR person.mobilenumber LIKE '%' || ? || '%' OR pp.phonenumber LIKE '%' || ? || '%'"
,
[$phonenumber, $phonenumber, $phonenumber]
);
$query->setFromClause($query->getFromClause()." LEFT JOIN chill_person_phone pp ON pp.person_id = person.id");
}
if (null !== $city) {
$query->setFromClause($query->getFromClause()." ".
"JOIN view_chill_person_current_address vcpca ON vcpca.person_id = person.id ".
"JOIN chill_main_address cma ON vcpca.address_id = cma.id ".
"JOIN chill_main_postal_code cmpc ON cma.postcode_id = cmpc.id");
if (NULL !== $default) {
$grams = explode(' ', $default);
foreach($grams as $key => $gram) {
$qb->andWhere($qb->expr()
->like('p.fullnameCanonical', 'UNACCENT(LOWER(:default_'.$key.'))'))
->setParameter('default_'.$key, '%'.$gram.'%');
foreach (\explode(" ", $city) as $cityStr) {
$query->andWhereClause(
"(UNACCENT(LOWER(cmpc.label)) LIKE '%' || UNACCENT(LOWER(?)) || '%' OR cmpc.code LIKE '%' || UNACCENT(LOWER(?)) || '%')",
[$cityStr, $city]
);
}
}
return $qb;
}
private function addACLClauses(QueryBuilder $qb, string $personAlias): void
{
// restrict center for security
$reachableCenters = $this->authorizationHelper
->getReachableCenters($this->security->getUser(), 'CHILL_PERSON_SEE');
$qb->andWhere(
$qb->expr()->orX(
$qb->expr()
->in($personAlias.'.center', ':centers'),
$qb->expr()
->isNull($personAlias.'.center')
)
);
$qb->setParameter('centers', $reachableCenters);
}
/**
* Create a query for searching by similarity.
*
* The person alias is "sp".
*
* @param $pattern
* @return QueryBuilder
*/
public function createSimilarityQuery($pattern): QueryBuilder
{
$qb = $this->em->createQueryBuilder();
$qb->from(Person::class, 'sp');
$grams = explode(' ', $pattern);
foreach($grams as $key => $gram) {
$qb->andWhere('STRICT_WORD_SIMILARITY_OPS(:default_'.$key.', sp.fullnameCanonical) = TRUE')
->setParameter('default_'.$key, '%'.$gram.'%');
// remove the perfect matches
$qb->andWhere($qb->expr()
->notLike('sp.fullnameCanonical', 'UNACCENT(LOWER(:not_default_'.$key.'))'))
->setParameter('not_default_'.$key, '%'.$gram.'%');
if (null !== $countryCode) {
$query->setFromClause($query->getFromClause()." JOIN country ON person.nationality_id = country.id");
$query->andWhereClause("country.countrycode = UPPER(?)", [$countryCode]);
}
if (null !== $gender) {
$query->andWhereClause("person.gender = ?", [$gender]);
}
return $qb;
return $query;
}
}

View File

@@ -3,16 +3,14 @@
namespace Chill\PersonBundle\Repository;
use Chill\MainBundle\Search\ParsingException;
use Chill\MainBundle\Search\SearchApiQuery;
use Chill\PersonBundle\Entity\Person;
use Doctrine\ORM\NonUniqueResultException;
interface PersonACLAwareRepositoryInterface
{
/**
* @return array|Person[]
* @throws NonUniqueResultException
* @throws ParsingException
*/
public function findBySearchCriteria(
int $start,
@@ -21,30 +19,38 @@ interface PersonACLAwareRepositoryInterface
string $default = null,
string $firstname = null,
string $lastname = null,
?\DateTime $birthdate = null,
?\DateTime $birthdateBefore = null,
?\DateTime $birthdateAfter = null,
?\DateTimeInterface $birthdate = null,
?\DateTimeInterface $birthdateBefore = null,
?\DateTimeInterface $birthdateAfter = null,
string $gender = null,
string $countryCode = null
string $countryCode = null,
string $phonenumber = null,
string $city = null
): array;
public function countBySearchCriteria(
string $default = null,
string $firstname = null,
string $lastname = null,
?\DateTime $birthdate = null,
?\DateTime $birthdateBefore = null,
?\DateTime $birthdateAfter = null,
?\DateTimeInterface $birthdate = null,
?\DateTimeInterface $birthdateBefore = null,
?\DateTimeInterface $birthdateAfter = null,
string $gender = null,
string $countryCode = null
string $countryCode = null,
string $phonenumber = null,
string $city = null
);
public function findBySimilaritySearch(
string $pattern,
int $firstResult,
int $maxResult,
bool $simplify = false
);
public function countBySimilaritySearch(string $pattern);
public function buildAuthorizedQuery(
string $default = null,
string $firstname = null,
string $lastname = null,
?\DateTimeInterface $birthdate = null,
?\DateTimeInterface $birthdateBefore = null,
?\DateTimeInterface $birthdateAfter = null,
string $gender = null,
string $countryCode = null,
string $phonenumber = null,
string $city = null
): SearchApiQuery;
}

View File

@@ -60,6 +60,16 @@ h2.badge-title {
h3 {
margin-bottom: 0.5rem;
}
//position: relative;
span {
display: none;
//position: absolute;
//top: 0;
//left: 0;
//transform: rotate(270deg);
//transform-origin: 0 0;
}
}
span.title_action {
flex-grow: 1;
@@ -117,3 +127,36 @@ div.activity-list {
}
}
}
/// AccompanyingCourse: HeaderSlider Carousel
div#header-accompanying_course-details {
button.carousel-control-prev,
button.carousel-control-next {
width: 8%;
opacity: inherit;
}
button.carousel-control-prev {
left: unset;
right: 0;
}
span.to-social-issues,
span.to-persons-associated {
display: inline-block;
border-radius: 15px;
width: 24px;
height: 24px;
box-shadow: 0 0 3px 1px grey;
opacity: 0.8;
&:hover {
opacity: 1;
}
}
span.to-social-issues {
background-color: #4bafe8;
border-left: 12px solid #32749a;
}
span.to-persons-associated {
background-color: #16d9b4;
border-right: 12px solid #ffffff;
}
}

View File

@@ -4,7 +4,7 @@
///
@mixin chill_badge($color) {
text-transform: capitalize !important;
//text-transform: capitalize !important;
font-weight: 500 !important;
border-left: 20px groove $color;
&:before {

View File

@@ -22,7 +22,7 @@
<i>{{ $t('course.open_at') }}{{ $d(accompanyingCourse.openingDate.datetime, 'text') }}</i>
</span>
<span v-if="accompanyingCourse.user" class="d-md-block ms-3 ms-md-0">
<abbr :title="$t('course.referrer')">{{ $t('course.referrer') }}:</abbr> <b>{{ accompanyingCourse.user.username }}</b>
<span class="item-key">{{ $t('course.referrer') }}:</span> <b>{{ accompanyingCourse.user.username }}</b>
</span>
</span>
</span>
@@ -34,13 +34,15 @@
</teleport>
<teleport to="#header-accompanying_course-details #banner-social-issues">
<div class="col-12">
<social-issue
v-for="issue in accompanyingCourse.socialIssues"
v-bind:key="issue.id"
v-bind:issue="issue">
</social-issue>
</div>
<social-issue
v-for="issue in accompanyingCourse.socialIssues"
v-bind:key="issue.id"
v-bind:issue="issue">
</social-issue>
</teleport>
<teleport to="#header-accompanying_course-details #banner-persons-associated">
<persons-associated :accompanyingCourse="accompanyingCourse"></persons-associated>
</teleport>
</template>
@@ -48,12 +50,14 @@
<script>
import ToggleFlags from './Banner/ToggleFlags';
import SocialIssue from './Banner/SocialIssue.vue';
import PersonsAssociated from './Banner/PersonsAssociated.vue';
export default {
name: 'Banner',
components: {
ToggleFlags,
SocialIssue
SocialIssue,
PersonsAssociated
},
computed: {
accompanyingCourse() {

View File

@@ -0,0 +1,75 @@
<template>
<span v-for="h in personsByHousehold()" :class="{ 'household': householdExists(h.id), 'no-household': !householdExists(h.id) }">
<a v-if="householdExists(h.id)" :href="householdLink(h.id)">
<i class="fa fa-home fa-fw text-light" :title="$t('persons_associated.show_household_number', { id: h.id })"></i>
</a>
<span v-for="person in h.persons" class="me-1">
<on-the-fly :type="person.type" :id="person.id" :buttonText="person.text" :displayBadge="'true' === 'true'" action="show"></on-the-fly>
</span>
</span>
</template>
<script>
import OnTheFly from 'ChillMainAssets/vuejs/OnTheFly/components/OnTheFly'
export default {
name: "PersonsAssociated",
components: {
OnTheFly
},
props: [ 'accompanyingCourse' ],
computed: {
participations() {
return this.accompanyingCourse.participations.filter(p => p.endDate === null)
},
persons() {
return this.participations.map(p => p.person)
},
resources() {
return this.accompanyingCourse.resources
},
requestor() {
return this.accompanyingCourse.requestor
}
},
methods: {
uniq(array) {
return [...new Set(array)]
},
personsByHousehold() {
let households = []
this.persons.forEach(p => { households.push(p.current_household_id) })
let personsByHousehold = []
this.uniq(households).forEach(h => {
personsByHousehold.push({
id: h !== null ? h : 0,
persons: this.persons.filter(p => p.current_household_id === h)
})
})
console.log(personsByHousehold)
return personsByHousehold
},
householdExists(id) {
return id !== 0
},
householdLink(id) {
return `/fr/person/household/${id}/summary`
}
}
}
</script>
<style lang="scss" scoped>
span.household {
display: inline-block;
border-top: 1px solid rgba(255, 255, 255, 0.3);
background-color: rgba(255, 255, 255, 0.1);
border-radius: 10px;
margin-right: 0.3em;
padding: 5px;
}
</style>

View File

@@ -10,7 +10,7 @@
<div class="alert alert-warning">
{{ $t('confirm.alert_validation') }}
<ul class="mt-2">
<li v-for="k in validationKeys">
<li v-for="k in validationKeys" :key=k>
{{ $t(notValidMessages[k].msg) }}
<a :href="notValidMessages[k].anchor">
<i class="fa fa-level-up fa-fw"></i>
@@ -83,7 +83,11 @@ export default {
},
location: {
msg: 'confirm.location_not_valid',
anchor: '#section-20' //
anchor: '#section-20'
},
origin: {
msg: 'confirm.origin_not_valid',
anchor: '#section-30'
},
socialIssue: {
msg: 'confirm.socialIssue_not_valid',
@@ -103,6 +107,7 @@ export default {
...mapGetters([
'isParticipationValid',
'isSocialIssueValid',
'isOriginValid',
'isLocationValid',
'validationKeys',
'isValidToBeConfirmed'

View File

@@ -19,15 +19,18 @@
:options="options"
@select="updateOrigin">
</VueMultiselect>
</div>
<div v-if="!isOriginValid" class="alert alert-warning to-confirm">
{{ $t('origin.not_valid') }}
</div>
</div>
</template>
<script>
import VueMultiselect from 'vue-multiselect';
import { getListOrigins } from '../api';
import { mapState } from 'vuex';
import { mapState, mapGetters } from 'vuex';
export default {
name: 'OriginDemand',
@@ -41,6 +44,9 @@ export default {
...mapState({
value: state => state.accompanyingCourse.origin,
}),
...mapGetters([
'isOriginValid'
])
},
mounted() {
this.getOptions();

View File

@@ -5,7 +5,7 @@
addId : false,
addEntity: false,
addLink: false,
addHouseholdLink: true,
addHouseholdLink: false,
addAltNames: true,
addAge : true,
hLevel : 3,
@@ -20,14 +20,15 @@
v-if="hasCurrentHouseholdAddress"
v-bind:person="participation.person">
</button-location>
<li v-if="participation.person.current_household_id">
<a class="btn btn-sm btn-chill-beige"
:href="getCurrentHouseholdUrl"
:title="$t('persons_associated.show_household_number', { id: participation.person.current_household_id })">
<i class="fa fa-fw fa-home"></i>
</a>
</li>
<li><on-the-fly :type="participation.person.type" :id="participation.person.id" action="show"></on-the-fly></li>
<li><on-the-fly :type="participation.person.type" :id="participation.person.id" action="edit" @saveFormOnTheFly="saveFormOnTheFly"></on-the-fly></li>
<!-- <li>
<button class="btn btn-delete"
:title="$t('action.delete')"
@click.prevent="$emit('remove', participation)">
</button>
</li> -->
<li>
<button v-if="!participation.endDate"
class="btn btn-sm btn-remove"
@@ -100,6 +101,9 @@ export default {
},
getAccompanyingCourseReturnPath() {
return `fr/parcours/${this.$store.state.accompanyingCourse.id}/edit#section-10`;
},
getCurrentHouseholdUrl() {
return `/fr/person/household/${this.participation.person.current_household_id}/summary?returnPath=${this.getAccompanyingCourseReturnPath}`
}
},
methods: {

View File

@@ -19,16 +19,16 @@
@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 class="list-unstyled">
<li v-for="u in referrersSuggested" @click="updateReferrer(u)">
<span class="badge bg-primary" style="cursor: pointer">
<i class="fa fa-plus fa-fw text-success"></i>
<user-render-box-badge :user="u"></user-render-box-badge>
</span>
</li>
</ul>
</template>
</div>
<div>

View File

@@ -35,6 +35,7 @@ const appMessages = {
title: "Origine de la demande",
label: "Origine de la demande",
placeholder: "Renseignez l'origine de la demande",
not_valid: "Indiquez une origine de la demande",
},
persons_associated: {
title: "Usagers concernés",
@@ -53,7 +54,7 @@ const appMessages = {
show_household_number: "Voir le ménage (n° {id})",
show_household: "Voir le ménage",
person_without_household_warning: "Certaines usagers n'appartiennent actuellement à aucun ménage. Renseignez leur appartenance dès que possible.",
update_household: "Modifier l'appartenance",
update_household: "Renseigner l'appartenance",
participation_not_valid: "Sélectionnez ou créez au minimum 1 usager",
},
requestor: {
@@ -125,6 +126,7 @@ const appMessages = {
participation_not_valid: "sélectionnez au minimum 1 usager",
socialIssue_not_valid: "sélectionnez au minimum une problématique sociale",
location_not_valid: "indiquez au minimum une localisation temporaire du parcours",
origin_not_valid: "indiquez une origine de la demande",
set_a_scope: "indiquez au moins un service",
sure: "Êtes-vous sûr ?",
sure_description: "Une fois le changement confirmé, il ne sera plus possible de le remettre à l'état de brouillon !",

View File

@@ -52,6 +52,9 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
isSocialIssueValid(state) {
return state.accompanyingCourse.socialIssues.length > 0;
},
isOriginValid(state) {
return state.accompanyingCourse.origin !== null;
},
isLocationValid(state) {
return state.accompanyingCourse.location !== null;
},
@@ -64,6 +67,7 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
if (!getters.isParticipationValid) { keys.push('participation'); }
if (!getters.isLocationValid) { keys.push('location'); }
if (!getters.isSocialIssueValid) { keys.push('socialIssue'); }
if (!getters.isOriginValid) { keys.push('origin'); }
if (!getters.isScopeValid) { keys.push('scopes'); }
//console.log('getter keys', keys);
return keys;

View File

@@ -119,9 +119,9 @@
<div id="persons" class="action-row">
<h3>{{ $t('persons_involved') }}</h3>
<ul>
<ul class="list-unstyled">
<li v-for="p in personsReachables" :key="p.id">
<input v-model="personsPicked" :value="p.id" type="checkbox">
<input v-model="personsPicked" :value="p.id" type="checkbox" class="me-2">
<person-render-box render="badge" :options="{}" :person="p"></person-render-box>
</li>
</ul>

View File

@@ -96,14 +96,9 @@
<p class="chill-no-data-statement">{{ $t('renderbox.no_data') }}</p>
</li>
<li v-if="person.center && options.addCenter">
<i class="fa fa-li fa-long-arrow-right"></i>
<template v-if="person.center.type !== undefined">
{{ person.center.name }}
</template>
<template v-else>
<template v-for="c in person.center">{{ c.name }}</template>
</template>
<li v-if="person.centers !== undefined && person.centers.length > 0 && options.addCenter">
<i class="fa fa-li fa-long-arrow-right"></i>
<template v-for="c in person.centers">{{ c.name }}</template>
</li>
<li v-else-if="options.addNoData">
<i class="fa fa-li fa-long-arrow-right"></i>

View File

@@ -6,7 +6,7 @@
<a class="btn btn-sm btn-update change-icon"
href="{{ path('chill_person_accompanying_course_edit', { 'accompanying_period_id': accompanyingCourse.id, '_fragment': 'section-10' }) }}">
<i class="fa fa-fw fa-crosshairs"></i>
Corriger
{{ 'fix it'|trans }}
</a>
</li>
</ul>

View File

@@ -8,7 +8,7 @@
<a class="btn btn-sm btn-update change-icon"
href="{{ path('chill_person_accompanying_course_edit', { 'accompanying_period_id': accompanyingCourse.id, '_fragment': 'section-20' }) }}">
<i class="fa fa-fw fa-crosshairs"></i>
Corriger
{{ 'fix it'|trans }}
</a>
</li>
</ul>

View File

@@ -23,11 +23,28 @@
</div>
<div id="header-accompanying_course-details" class="header-details">
<div class="container-xxl">
<div
class="row justify-content-md-right">
<div class="row">
{# vue teleport fragment here #}
<div class="col-md-10 ps-md-5 ps-xxl-0" id="banner-social-issues"></div>
<div class="col-md-12 px-md-5 px-xxl-0">
<div id="ACHeaderSlider" class="carousel carousel-dark slide" data-bs-ride="carousel">
<div class="carousel-inner">
<div class="carousel-item active">
{# vue teleport fragment here #}
<div id="banner-social-issues" class="col-11"></div>
</div>
<div class="carousel-item">
{# vue teleport fragment here #}
<div id="banner-persons-associated" class="col-11"></div>
</div>
</div>
<button class="carousel-control-prev justify-content-end visually-hidden" type="button" data-bs-target="#ACHeaderSlider" data-bs-slide="prev">
<span class="to-social-issues" title="{{ 'see social issues'|trans }}"></span>
</button>
<button class="carousel-control-next justify-content-end visually-hidden" type="button" data-bs-target="#ACHeaderSlider" data-bs-slide="next">
<span class="to-persons-associated" title="{{ 'see persons associated'|trans }}"></span>
</button>
</div>
</div>
</div>
</div>

View File

@@ -23,32 +23,6 @@
{% block content %}
<div class="accompanyingcourse-resume row">
<div class="associated-persons mb-5">
{% for h in participationsByHousehold %}
{% set householdClass = (h.household is not null) ? 'household-' ~ h.household.id : 'no-household alert alert-warning' %}
{% set householdTitle = (h.household is not null) ?
'household.Household number'|trans({'household_num': h.household.id }) : 'household.Never in any household'|trans %}
<span class="household {{ householdClass }}" title="{{ householdTitle }}">
{% if h.household is not null %}
<a href="{{ path('chill_person_household_summary', { 'household_id': h.household.id }) }}"
title="{{ 'household.Household number'|trans({'household_num': h.household.id }) }}"
><i class="fa fa-home fa-fw"></i></a>
{% endif %}
{% for p in h.members %}
{# include vue_onthefly component #}
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
targetEntity: { name: 'person', id: p.person.id },
action: 'show',
displayBadge: true,
buttonText: p.person|chill_entity_render_string
} %}
{% endfor %}
</span>
{% endfor %}
</div>
{% if 'DRAFT' == accompanyingCourse.step %}
<div class="col-md-6 warnings mb-5">
{% include '@ChillPerson/AccompanyingCourse/_still_draft.html.twig' %}
@@ -83,7 +57,7 @@
</div>
<div class="social-actions mb-5">
<h2 class="mb-3">{{ 'Last social actions'|trans }}</h2>
<h2 class="mb-3 d-none">{{ 'Last social actions'|trans }}</h2>
{% include 'ChillPersonBundle:AccompanyingCourseWork:list_recent_by_accompanying_period.html.twig' with {'buttonText': false } %}
</div>
@@ -101,8 +75,7 @@
{% set accompanying_course_id = accompanyingCourse.id %}
{% endif %}
<h2 class="mb-3">{{ 'Last activities' |trans }}</h2>
<h2 class="mb-3 d-none">{{ 'Last activities' |trans }}</h2>
{% include 'ChillActivityBundle:Activity:list_recent.html.twig' with { 'context': 'accompanyingCourse', 'no_action': true } %}
</div>
{% endblock %}

View File

@@ -1,29 +1,33 @@
{% if works|length == 0 %}
<p class="chill-no-data-statement">{{ 'accompanying_course_work.Any work'|trans }}
<a class="btn btn-sm btn-create"
href="" title="TODO"></a>{# TODO link #}
</p>
{% endif %}
<div class="accompanying_course_work-list">
{% for w in works | slice(0,5) %}
<a href="{{ chill_path_add_return_path('chill_person_accompanying_period_work_edit', { 'id': w.id }) }}"></a>
<h2 class="badge-title">
<span class="title_label">{{ 'accompanying_course_work.action'|trans }}</span>
<span class="title_action">{{ w.socialAction|chill_entity_render_string }}
<span class="title_label">
<span>{{ 'accompanying_course_work.action'|trans }}</span>
</span>
<span class="title_action">
{{ w.socialAction|chill_entity_render_string }}
<ul class="small_in_title">
<li>
<abbr title="{{ 'accompanying_course_work.start_date'|trans }}">{{ 'accompanying_course_work.start_date'|trans ~ ' : ' }}</abbr>
{{ w.startDate|format_date('short') }}
<span class="item-key">{{ 'accompanying_course_work.start_date'|trans ~ ' : ' }}</span>
<b>{{ w.startDate|format_date('short') }}</b>
</li>
{% if w.endDate %}
<li>
<abbr title="{{ 'Last updated by'|trans }}">{{ 'Last updated by'|trans ~ ' : ' }}</abbr>
{{ w.updatedBy|chill_entity_render_box }}, {{ w.updatedAt|format_datetime('short', 'short') }}
<span class="item-key">{{ 'accompanying_course_work.end_date'|trans ~ ' : ' }}</span>
<b>{{ w.endDate|format_date('short') }}</b>
</li>
{% endif %}
</ul>
<div class="metadata text-end" style="font-size: 60%">
{{ 'Last updated by'|trans }}
<span class="user">{{ w.updatedBy|chill_entity_render_box }}</span>:
<span class="date">{{ w.updatedAt|format_datetime('short', 'short') }}</span>
</div>
</span>
</h2>
{% endfor %}

View File

@@ -148,10 +148,12 @@
{% endif %}
{% endif %}
</li>
{% if options['addCenter'] and person|chill_resolve_center is not null %}
{% if options['addCenter'] and person|chill_resolve_center|length > 0 %}
<li>
<i class="fa fa-li fa-long-arrow-right"></i>
{{ person|chill_resolve_center.name }}
{% for c in person|chill_resolve_center %}
{{ c.name|upper }}{% if not loop.last %}, {% endif %}
{% endfor %}
</li>
{% endif %}
</ul>

View File

@@ -17,18 +17,14 @@
{%- endif -%}
</div>
<div class="text-md-end">
{% if person|chill_resolve_center is not null %}
{% if person|chill_resolve_center|length > 0 %}
<span class="open_sansbold">
{{ 'Center'|trans|upper}} :
</span>
{% if person|chill_resolve_center is iterable %}
{% for c in person|chill_resolve_center %}
{{ c.name|upper }}{% if not loop.last %}, {% endif %}
{% endfor %}
{% else %}
{{ person|chill_resolve_center.name|upper }}
{% endif %}
{% for c in person|chill_resolve_center %}
{{ c.name|upper }}{% if not loop.last %}, {% endif %}
{% endfor %}
{%- endif -%}
</div>

View File

@@ -1,7 +1,12 @@
{% macro button_person(person) %}
{% macro button_person_after(person) %}
{% set household = person.getCurrentHousehold %}
{% if household is not null %}
<li>
<a href="{{ path('chill_person_household_summary', { 'household_id': household.id }) }}" class="btn btn-sm btn-chill-beige"><i class="fa fa-home"></i></a>
</li>
{% endif %}
<li>
<a href="{{ path('chill_person_accompanying_course_new', { 'person_id': [ person.id ]}) }}"
class="btn btn-sm btn-create change-icon" title="{{ 'Create an accompanying period'|trans }}"><i class="fa fa-random"></i></a>
<a href="{{ path('chill_person_accompanying_course_new', { 'person_id': [ person.id ]}) }}" class="btn btn-sm btn-create change-icon" title="{{ 'Create an accompanying period'|trans }}"><i class="fa fa-random"></i></a>
</li>
{% endmacro %}
@@ -56,7 +61,7 @@
'addAltNames': true,
'addCenter': true,
'address_multiline': false,
'customButtons': { 'after': _self.button_person(person) }
'customButtons': { 'after': _self.button_person_after(person) }
}) }}
{#- 'acps' is for AcCompanyingPeriodS #}
@@ -76,12 +81,20 @@
<div class="wl-row separator">
<div class="wl-col title">
<div class="date">
{% if acp.requestorPerson == person %}
<span class="as-requestor badge bg-info" title="{{ 'Requestor'|trans|e('html_attr') }}">
{% if acp.step == 'DRAFT' %}
<div class="is-draft">
<span class="course-draft badge bg-secondary" title="{{ 'course.draft'|trans }}">{{ 'course.draft'|trans }}</span>
</div>
{% endif %}
{% if acp.requestorPerson == person %}
<div>
<span class="as-requestor badge bg-info" title="{{ 'Requestor'|trans|e('html_attr') }}">
{{ 'Requestor'|trans({'gender': person.gender}) }}
</span>
{% endif %}
</span>
</div>
{% endif %}
<div class="date">
{% if app != null %}
{{ 'Since %date%'|trans({'%date%': app.startDate|format_date('medium') }) }}
{% endif %}
@@ -94,6 +107,11 @@
</div>
{% endif %}
<div class="courseid">
{{ 'File number'|trans }} {{ acp.id }}
</div>
</div>
<div class="wl-col list">
@@ -101,17 +119,76 @@
{{ issue|chill_entity_render_box }}
{% endfor %}
<ul class="record_actions">
<ul class="record_actions record_actions_column">
<li>
<a href="{{ path('chill_person_accompanying_course_index', { 'accompanying_period_id': acp.id }) }}"
class="btn btn-sm btn-outline-primary" title="{{ 'See accompanying period'|trans }}">
<i class="fa fa-random fa-fw"></i>
</a>
</li>
<li>
</li>
</ul>
</div>
</div>
{% if acp.currentParticipations|length > 1 %}
<div class="wl-row">
<div class="wl-col title">
<div class="participants">
{{ 'Participants'|trans }}
</div>
</div>
<div class="wl-col list">
{% set participating = false %}
{% for part in acp.currentParticipations %}
{% if part.person.id != person.id %}
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
targetEntity: { name: 'person', id: part.person.id },
action: 'show',
displayBadge: true,
buttonText: part.person|chill_entity_render_string
} %}
{% else %}
{% set participating = true %}
{% endif %}
{% endfor %}
{% if participating %}
{{ 'person.and_himself'|trans({'gender': person.gender}) }}
{% endif %}
</div>
</div>
{% endif %}
{% if (acp.requestorPerson is not null and acp.requestorPerson.id != person.id) or acp.requestorThirdParty is not null %}
<div class="wl-row">
<div class="wl-col title">
<div>
{% if acp.requestorPerson is not null %}
{{ 'Requestor'|trans({'gender': acp.requestorPerson.gender}) }}
{% else %}
{{ 'Requestor'|trans({'gender': 'other'})}}
{% endif %}
</div>
</div>
<div class="wl-col list">
{% if acp.requestorThirdParty is not null %}
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
targetEntity: { name: 'thirdparty', id: acp.requestorThirdParty.id },
action: 'show',
displayBadge: true,
buttonText: acp.requestorThirdParty|chill_entity_render_string
} %}
{% else %}
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
targetEntity: { name: 'person', id: acp.requestorPerson.id },
action: 'show',
displayBadge: true,
buttonText: acp.requestorPerson|chill_entity_render_string
} %}
{% endif %}
</div>
</div>
{% endif %}
{% endfor %}
</div>
</div>

View File

@@ -5,11 +5,15 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Search;
use Chill\MainBundle\Search\AbstractSearch;
use Chill\MainBundle\Search\Utils\ExtractDateFromPattern;
use Chill\MainBundle\Search\Utils\ExtractPhonenumberFromPattern;
use Chill\PersonBundle\Form\Type\GenderType;
use Chill\PersonBundle\Repository\PersonACLAwareRepositoryInterface;
use Chill\PersonBundle\Entity\Person;
use Chill\MainBundle\Search\SearchInterface;
use Chill\MainBundle\Search\ParsingException;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Symfony\Component\Form\Extension\Core\Type\TelType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Chill\MainBundle\Form\Type\ChillDateType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
@@ -19,23 +23,29 @@ use Symfony\Component\Templating\EngineInterface;
class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterface
{
protected EngineInterface $templating;
protected PaginatorFactory $paginatorFactory;
protected PersonACLAwareRepositoryInterface $personACLAwareRepository;
private EngineInterface $templating;
private PaginatorFactory $paginatorFactory;
private PersonACLAwareRepositoryInterface $personACLAwareRepository;
private ExtractDateFromPattern $extractDateFromPattern;
private ExtractPhonenumberFromPattern $extractPhonenumberFromPattern;
public const NAME = "person_regular";
private const POSSIBLE_KEYS = [
'_default', 'firstname', 'lastname', 'birthdate', 'birthdate-before',
'birthdate-after', 'gender', 'nationality'
'birthdate-after', 'gender', 'nationality', 'phonenumber', 'city'
];
public function __construct(
EngineInterface $templating,
ExtractDateFromPattern $extractDateFromPattern,
ExtractPhonenumberFromPattern $extractPhonenumberFromPattern,
PaginatorFactory $paginatorFactory,
PersonACLAwareRepositoryInterface $personACLAwareRepository
) {
$this->templating = $templating;
$this->extractDateFromPattern = $extractDateFromPattern;
$this->extractPhonenumberFromPattern = $extractPhonenumberFromPattern;
$this->paginatorFactory = $paginatorFactory;
$this->personACLAwareRepository = $personACLAwareRepository;
}
@@ -69,6 +79,7 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
*/
public function renderResult(array $terms, $start = 0, $limit = 50, array $options = array(), $format = 'html')
{
$terms = $this->findAdditionnalInDefault($terms);
$total = $this->count($terms);
$paginator = $this->paginatorFactory->create($total);
@@ -99,6 +110,26 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
}
}
private function findAdditionnalInDefault(array $terms): array
{
// chaining some extractor
$datesResults = $this->extractDateFromPattern->extractDates($terms['_default']);
$phoneResults = $this->extractPhonenumberFromPattern->extractPhonenumber($datesResults->getFilteredSubject());
$terms['_default'] = $phoneResults->getFilteredSubject();
if ($datesResults->hasResult() && (!\array_key_exists('birthdate', $terms)
|| NULL !== $terms['birthdate'])) {
$terms['birthdate'] = $datesResults->getFound()[0]->format('Y-m-d');
}
if ($phoneResults->hasResult() && (!\array_key_exists('phonenumber', $terms)
|| NULL !== $terms['phonenumber'])) {
$terms['phonenumber'] = $phoneResults->getFound()[0];
}
return $terms;
}
/**
* @return Person[]
*/
@@ -113,6 +144,8 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
'birthdate-after' => $birthdateAfter,
'gender' => $gender,
'nationality' => $countryCode,
'phonenumber' => $phonenumber,
'city' => $city,
] = $terms + \array_fill_keys(self::POSSIBLE_KEYS, null);
foreach (['birthdateBefore', 'birthdateAfter', 'birthdate'] as $v) {
@@ -139,6 +172,8 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
$birthdateAfter,
$gender,
$countryCode,
$phonenumber,
$city
);
}
@@ -153,7 +188,8 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
'birthdate-after' => $birthdateAfter,
'gender' => $gender,
'nationality' => $countryCode,
'phonenumber' => $phonenumber,
'city' => $city,
] = $terms + \array_fill_keys(self::POSSIBLE_KEYS, null);
foreach (['birthdateBefore', 'birthdateAfter', 'birthdate'] as $v) {
@@ -177,6 +213,8 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
$birthdateAfter,
$gender,
$countryCode,
$phonenumber,
$city
);
}
@@ -207,13 +245,19 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
'label' => 'Birthdate before',
'required' => false
])
->add('gender', ChoiceType::class, [
'choices' => [
'Man' => Person::MALE_GENDER,
'Woman' => Person::FEMALE_GENDER
],
->add('phonenumber', TelType::class, [
'required' => false,
'label' => 'Part of the phonenumber'
])
->add('gender', GenderType::class, [
'label' => 'Gender',
'required' => false
'required' => false,
'expanded' => false,
'placeholder' => 'All genders'
])
->add('city', TextType::class, [
'required' => false,
'label' => 'City or postal code'
])
;
}
@@ -224,7 +268,7 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
$string .= empty($data['_default']) ? '' : $data['_default'].' ';
foreach(['firstname', 'lastname', 'gender'] as $key) {
foreach(['firstname', 'lastname', 'gender', 'phonenumber', 'city'] as $key) {
$string .= empty($data[$key]) ? '' : $key.':'.
// add quote if contains spaces
(strpos($data[$key], ' ') !== false ? '"'.$data[$key].'"': $data[$key])
@@ -246,7 +290,7 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
{
$data = [];
foreach(['firstname', 'lastname', 'gender', '_default'] as $key) {
foreach(['firstname', 'lastname', 'gender', '_default', 'phonenumber', 'city'] as $key) {
$data[$key] = $terms[$key] ?? null;
}
@@ -275,6 +319,4 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
{
return self::NAME;
}
}

View File

@@ -1,133 +0,0 @@
<?php
/*
*
* Copyright (C) 2014-2019, Champs Libres Cooperative SCRLFS, <http://www.champs-libres.coop>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Chill\PersonBundle\Search;
use Chill\MainBundle\Search\AbstractSearch;
use Chill\PersonBundle\Repository\PersonRepository;
use Chill\PersonBundle\Entity\Person;
use Chill\MainBundle\Search\SearchInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Symfony\Component\Security\Core\Role\Role;
use Symfony\Component\Templating\EngineInterface;
/**
*
*
*/
class PersonSearchByPhone extends AbstractSearch
{
/**
*
* @var PersonRepository
*/
private $personRepository;
/**
*
* @var TokenStorageInterface
*/
private $tokenStorage;
/**
*
* @var AuthorizationHelper
*/
private $helper;
/**
*
* @var PaginatorFactory
*/
protected $paginatorFactory;
/**
*
* @var bool
*/
protected $activeByDefault;
/**
*
* @var Templating
*/
protected $engine;
const NAME = 'phone';
public function __construct(
PersonRepository $personRepository,
TokenStorageInterface $tokenStorage,
AuthorizationHelper $helper,
PaginatorFactory $paginatorFactory,
EngineInterface $engine,
$activeByDefault)
{
$this->personRepository = $personRepository;
$this->tokenStorage = $tokenStorage;
$this->helper = $helper;
$this->paginatorFactory = $paginatorFactory;
$this->engine = $engine;
$this->activeByDefault = $activeByDefault === 'always';
}
public function getOrder(): int
{
return 110;
}
public function isActiveByDefault(): bool
{
return $this->activeByDefault;
}
public function renderResult(array $terms, $start = 0, $limit = 50, $options = array(), $format = 'html')
{
$phonenumber = $terms['_default'];
$centers = $this->helper->getReachableCenters($this->tokenStorage
->getToken()->getUser(), new Role(PersonVoter::SEE));
$total = $this->personRepository
->countByPhone($phonenumber, $centers);
$persons = $this->personRepository
->findByPhone($phonenumber, $centers, $start, $limit)
;
$paginator = $this->paginatorFactory
->create($total);
return $this->engine->render('ChillPersonBundle:Person:list_by_phonenumber.html.twig',
array(
'persons' => $persons,
'pattern' => $this->recomposePattern($terms, array(), $terms['_domain'] ?? self::NAME),
'phonenumber' => $phonenumber,
'total' => $total,
'start' => $start,
'search_name' => self::NAME,
'preview' => $options[SearchInterface::SEARCH_PREVIEW_OPTION],
'paginator' => $paginator
));
}
public function supports($domain, $format): bool
{
return $domain === 'phone' && $format === 'html';
}
}

View File

@@ -2,12 +2,13 @@
namespace Chill\PersonBundle\Search;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Search\Utils\ExtractDateFromPattern;
use Chill\MainBundle\Search\Utils\ExtractPhonenumberFromPattern;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Chill\PersonBundle\Repository\PersonACLAwareRepositoryInterface;
use Chill\PersonBundle\Repository\PersonRepository;
use Chill\MainBundle\Search\SearchApiQuery;
use Chill\MainBundle\Search\SearchApiInterface;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Symfony\Component\Security\Core\Security;
class SearchPersonApiProvider implements SearchApiInterface
@@ -15,59 +16,47 @@ class SearchPersonApiProvider implements SearchApiInterface
private PersonRepository $personRepository;
private Security $security;
private AuthorizationHelperInterface $authorizationHelper;
private ExtractDateFromPattern $extractDateFromPattern;
private PersonACLAwareRepositoryInterface $personACLAwareRepository;
private ExtractPhonenumberFromPattern $extractPhonenumberFromPattern;
public function __construct(PersonRepository $personRepository, Security $security, AuthorizationHelperInterface $authorizationHelper)
{
public function __construct(
PersonRepository $personRepository,
PersonACLAwareRepositoryInterface $personACLAwareRepository,
Security $security,
AuthorizationHelperInterface $authorizationHelper,
ExtractDateFromPattern $extractDateFromPattern,
ExtractPhonenumberFromPattern $extractPhonenumberFromPattern
) {
$this->personRepository = $personRepository;
$this->personACLAwareRepository = $personACLAwareRepository;
$this->security = $security;
$this->authorizationHelper = $authorizationHelper;
$this->extractDateFromPattern = $extractDateFromPattern;
$this->extractPhonenumberFromPattern = $extractPhonenumberFromPattern;
}
public function provideQuery(string $pattern, array $parameters): SearchApiQuery
{
return $this->addAuthorizations($this->buildBaseQuery($pattern, $parameters));
}
$datesResult = $this->extractDateFromPattern->extractDates($pattern);
$phoneResult = $this->extractPhonenumberFromPattern->extractPhonenumber($datesResult->getFilteredSubject());
$filtered = $phoneResult->getFilteredSubject();
public function buildBaseQuery(string $pattern, array $parameters): SearchApiQuery
{
$query = new SearchApiQuery();
$query
return $this->personACLAwareRepository->buildAuthorizedQuery(
$filtered,
null,
null,
count($datesResult->getFound()) > 0 ? $datesResult->getFound()[0] : null,
null,
null,
null,
null,
count($phoneResult->getFound()) > 0 ? $phoneResult->getFound()[0] : null
)
->setSelectKey("person")
->setSelectJsonbMetadata("jsonb_build_object('id', person.id)")
->setSelectPertinence("".
"STRICT_WORD_SIMILARITY(LOWER(UNACCENT(?)), person.fullnamecanonical) + ".
"(person.fullnamecanonical LIKE '%' || LOWER(UNACCENT(?)) || '%')::int + ".
"(EXISTS (SELECT 1 FROM unnest(string_to_array(fullnamecanonical, ' ')) AS t WHERE starts_with(t, UNACCENT(LOWER(?)))))::int"
, [ $pattern, $pattern, $pattern ])
->setFromClause("chill_person_person AS person")
->setWhereClauses("LOWER(UNACCENT(?)) <<% person.fullnamecanonical OR ".
"person.fullnamecanonical LIKE '%' || LOWER(UNACCENT(?)) || '%' ", [ $pattern, $pattern ])
;
return $query;
->setSelectJsonbMetadata("jsonb_build_object('id', person.id)");
}
private function addAuthorizations(SearchApiQuery $query): SearchApiQuery
{
$authorizedCenters = $this->authorizationHelper
->getReachableCenters($this->security->getUser(), PersonVoter::SEE);
if ([] === $authorizedCenters) {
return $query->andWhereClause("FALSE = TRUE", []);
}
return $query
->andWhereClause(
strtr(
"person.center_id IN ({{ center_ids }})",
[
'{{ center_ids }}' => \implode(', ',
\array_fill(0, count($authorizedCenters), '?')),
]
),
\array_map(function(Center $c) {return $c->getId();}, $authorizedCenters)
);
}
public function supportsTypes(string $pattern, array $types, array $parameters): bool
{

View File

@@ -1,114 +0,0 @@
<?php
namespace Chill\PersonBundle\Search;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Search\AbstractSearch;
use Chill\MainBundle\Search\SearchInterface;
use Chill\PersonBundle\Repository\PersonACLAwareRepositoryInterface;
use Symfony\Component\Templating\EngineInterface;
class SimilarityPersonSearch extends AbstractSearch
{
protected PaginatorFactory $paginatorFactory;
private PersonACLAwareRepositoryInterface $personACLAwareRepository;
private EngineInterface $templating;
const NAME = "person_similarity";
public function __construct(
PaginatorFactory $paginatorFactory,
PersonACLAwareRepositoryInterface $personACLAwareRepository,
EngineInterface $templating
) {
$this->paginatorFactory = $paginatorFactory;
$this->personACLAwareRepository = $personACLAwareRepository;
$this->templating = $templating;
}
/*
* (non-PHPdoc)
* @see \Chill\MainBundle\Search\SearchInterface::getOrder()
*/
public function getOrder()
{
return 200;
}
/*
* (non-PHPdoc)
* @see \Chill\MainBundle\Search\SearchInterface::isActiveByDefault()
*/
public function isActiveByDefault()
{
return true;
}
public function supports($domain, $format)
{
return 'person' === $domain;
}
/**
* @param array $terms
* @param int $start
* @param int $limit
* @param array $options
* @param string $format
*/
public function renderResult(array $terms, $start = 0, $limit = 50, array $options = array(), $format = 'html')
{
$total = $this->count($terms);
$paginator = $this->paginatorFactory->create($total);
if ($format === 'html') {
if ($total !== 0) {
return $this->templating->render('ChillPersonBundle:Person:list.html.twig',
array(
'persons' => $this->search($terms, $start, $limit, $options),
'pattern' => $this->recomposePattern($terms, array('nationality',
'firstname', 'lastname', 'birthdate', 'gender',
'birthdate-before','birthdate-after'), $terms['_domain']),
'total' => $total,
'start' => $start,
'search_name' => self::NAME,
'preview' => $options[SearchInterface::SEARCH_PREVIEW_OPTION],
'paginator' => $paginator,
'title' => "Similar persons"
));
}
else {
return null;
}
} elseif ($format === 'json') {
return [
'results' => $this->search($terms, $start, $limit, \array_merge($options, [ 'simplify' => true ])),
'pagination' => [
'more' => $paginator->hasNextPage()
]
];
}
}
/**
*
* @param string $pattern
* @param int $start
* @param int $limit
* @param array $options
* @return Person[]
*/
protected function search(array $terms, $start, $limit, array $options = array())
{
return $this->personACLAwareRepository
->findBySimilaritySearch($terms['_default'], $start, $limit, $options['simplify'] ?? false);
}
protected function count(array $terms)
{
return $this->personACLAwareRepository->countBySimilaritySearch($terms['_default']);
}
}

View File

@@ -0,0 +1,116 @@
<?php
namespace Chill\PersonBundle\Serializer\Normalizer;
use Chill\DocGeneratorBundle\Serializer\Helper\NormalizeNullValueHelper;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\PersonAltName;
use Chill\PersonBundle\Templating\Entity\PersonRender;
use Symfony\Component\Serializer\Exception\CircularReferenceException;
use Symfony\Component\Serializer\Exception\ExceptionInterface;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class PersonDocGenNormalizer implements
ContextAwareNormalizerInterface,
NormalizerAwareInterface
{
use NormalizerAwareTrait;
private PersonRender $personRender;
private TranslatorInterface $translator;
private TranslatableStringHelper $translatableStringHelper;
/**
* @param PersonRender $personRender
* @param TranslatorInterface $translator
* @param TranslatableStringHelper $translatableStringHelper
*/
public function __construct(
PersonRender $personRender,
TranslatorInterface $translator,
TranslatableStringHelper $translatableStringHelper
) {
$this->personRender = $personRender;
$this->translator = $translator;
$this->translatableStringHelper = $translatableStringHelper;
}
public function normalize($person, string $format = null, array $context = [])
{
/** @var Person $person */
$dateContext = $context;
$dateContext['docgen:expects'] = \DateTimeInterface::class;
if (null === $person) {
return $this->normalizeNullValue($format, $context);
}
return [
'firstname' => $person->getFirstName(),
'lastname' => $person->getLastName(),
'altNames' => \implode(
', ',
\array_map(
function (PersonAltName $altName) {
return $altName->getLabel();
},
$person->getAltNames()->toArray()
)
),
'text' => $this->personRender->renderString($person, []),
'birthdate' => $this->normalizer->normalize($person->getBirthdate(), $format, $dateContext),
'deathdate' => $this->normalizer->normalize($person->getDeathdate(), $format, $dateContext),
'gender' => $this->translator->trans($person->getGender()),
'maritalStatus' => null !== ($ms = $person->getMaritalStatus()) ? $this->translatableStringHelper->localize($ms->getName()) : '',
'maritalStatusDate' => $this->normalizer->normalize($person->getMaritalStatusDate(), $format, $dateContext),
'email' => $person->getEmail(),
'firstPhoneNumber' => $person->getPhonenumber() ?? $person->getMobilenumber(),
'fixPhoneNumber' => $person->getPhonenumber(),
'mobilePhoneNumber' => $person->getMobilenumber(),
'nationality' => null !== ($c = $person->getNationality()) ? $this->translatableStringHelper->localize($c->getName()) : '',
'placeOfBirth' => $person->getPlaceOfBirth(),
'memo' => $person->getMemo(),
'numberOfChildren' => (string) $person->getNumberOfChildren(),
];
}
private function normalizeNullValue(string $format, array $context)
{
$normalizer = new NormalizeNullValueHelper($this->normalizer);
$attributes = [
'firstname', 'lastname', 'altNames', 'text',
'birthdate' => \DateTimeInterface::class,
'deathdate' => \DateTimeInterface::class,
'gender', 'maritalStatus',
'maritalStatusDate' => \DateTimeInterface::class,
'email', 'firstPhoneNumber', 'fixPhoneNumber', 'mobilePhoneNumber', 'nationality',
'placeOfBirth', 'memo', 'numberOfChildren'
];
return $normalizer->normalize($attributes, $format, $context);
}
public function supportsNormalization($data, string $format = null, array $context = [])
{
if ($format !== 'docgen') {
return false;
}
return
$data instanceof Person
|| (
\array_key_exists('docgen:expects', $context)
&& $context['docgen:expects'] === Person::class
);
}
}

Some files were not shown because too many files have changed in this diff Show More