Merge branch 'master' into person_renderbox_thirdparty_onthefly

This commit is contained in:
Mathieu Jaumotte 2021-09-23 13:56:28 +02:00
commit d5f1de1fbc
129 changed files with 4150 additions and 1615 deletions

View File

@ -1,12 +1,70 @@
{
"name": "chill-project/chill-bundles",
"license": "AGPL-3.0-only",
"type": "library",
"description": "Most used bundles for chill-project",
"keywords": [
"chill",
"social worker"
],
"license": "AGPL-3.0-only",
"require": {
"champs-libres/async-uploader-bundle": "dev-sf4",
"champs-libres/wopi-bundle": "dev-master",
"composer/package-versions-deprecated": "^1.10",
"doctrine/doctrine-bundle": "^2.1",
"doctrine/doctrine-migrations-bundle": "^3.0",
"doctrine/orm": "^2.7",
"erusev/parsedown": "^1.7",
"graylog2/gelf-php": "^1.5",
"knplabs/knp-menu": "^3.1",
"knplabs/knp-menu-bundle": "^3.0",
"knplabs/knp-time-bundle": "^1.12",
"league/csv": "^9.7.1",
"nyholm/psr7": "^1.4",
"phpoffice/phpspreadsheet": "^1.16",
"sensio/framework-extra-bundle": "^5.5",
"symfony/asset": "4.*",
"symfony/browser-kit": "^5.2",
"symfony/css-selector": "^5.2",
"symfony/expression-language": "4.*",
"symfony/form": "4.*",
"symfony/intl": "4.*",
"symfony/monolog-bundle": "^3.5",
"symfony/security-bundle": "4.*",
"symfony/serializer": "^5.2",
"symfony/swiftmailer-bundle": "^3.5",
"symfony/templating": "4.*",
"symfony/translation": "4.*",
"symfony/twig-bundle": "^4.4",
"symfony/validator": "4.*",
"symfony/webpack-encore-bundle": "^1.11",
"symfony/workflow": "4.*",
"symfony/yaml": "4.*",
"twig/extra-bundle": "^2.12 || ^3.0",
"twig/intl-extra": "^3.0",
"twig/markdown-extra": "^3.3",
"twig/twig": "^2.12 || ^3.0"
},
"conflict": {
"symfony/symfony": "*"
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.3",
"fakerphp/faker": "^1.13",
"nelmio/alice": "^3.8",
"phpunit/phpunit": "^7.0",
"symfony/debug-bundle": "^5.1",
"symfony/dotenv": "^5.1",
"symfony/maker-bundle": "^1.20",
"symfony/phpunit-bridge": "^5.2",
"symfony/stopwatch": "^5.1",
"symfony/var-dumper": "4.*",
"symfony/web-profiler-bundle": "^5.0"
},
"config": {
"bin-dir": "bin",
"vendor-dir": "tests/app/vendor"
},
"autoload": {
"psr-4": {
"Chill\\ActivityBundle\\": "src/Bundle/ChillActivityBundle",
@ -33,68 +91,10 @@
},
"minimum-stability": "dev",
"prefer-stable": true,
"require": {
"champs-libres/async-uploader-bundle": "dev-sf4",
"champs-libres/wopi-bundle": "dev-master",
"nyholm/psr7": "^1.4",
"graylog2/gelf-php": "^1.5",
"symfony/form": "4.*",
"symfony/twig-bundle": "^4.4",
"twig/extra-bundle": "^2.12|^3.0",
"twig/twig": "^2.12|^3.0",
"composer/package-versions-deprecated": "^1.10",
"doctrine/doctrine-bundle": "^2.1",
"doctrine/doctrine-migrations-bundle": "^3.0",
"doctrine/orm": "^2.7",
"symfony/asset": "4.*",
"symfony/monolog-bundle": "^3.5",
"symfony/security-bundle": "4.*",
"symfony/translation": "4.*",
"symfony/validator": "4.*",
"sensio/framework-extra-bundle": "^5.5",
"symfony/yaml": "4.*",
"symfony/webpack-encore-bundle": "^1.11",
"knplabs/knp-menu": "^3.1",
"knplabs/knp-menu-bundle": "^3.0",
"symfony/templating": "4.*",
"twig/intl-extra": "^3.0",
"symfony/workflow": "4.*",
"symfony/expression-language": "4.*",
"knplabs/knp-time-bundle": "^1.12",
"symfony/intl": "4.*",
"symfony/swiftmailer-bundle": "^3.5",
"league/csv": "^9.7.1",
"phpoffice/phpspreadsheet": "^1.16",
"symfony/browser-kit": "^5.2",
"symfony/css-selector": "^5.2",
"twig/markdown-extra": "^3.3",
"erusev/parsedown": "^1.7",
"symfony/serializer": "^5.2",
"symfony/webpack-encore-bundle": "^1.11"
},
"conflict": {
"symfony/symfony": "*"
},
"require-dev": {
"fakerphp/faker": "^1.13",
"phpunit/phpunit": "^7.0",
"symfony/dotenv": "^5.1",
"symfony/maker-bundle": "^1.20",
"doctrine/doctrine-fixtures-bundle": "^3.3",
"symfony/stopwatch": "^5.1",
"symfony/web-profiler-bundle": "^5.0",
"symfony/var-dumper": "4.*",
"symfony/debug-bundle": "^5.1",
"symfony/phpunit-bridge": "^5.2",
"nelmio/alice": "^3.8"
},
"scripts": {
"auto-scripts": {
"cache:clear": "symfony-cmd",
"assets:install %PUBLIC_DIR%": "symfony-cmd"
}
},
"config": {
"bin-dir": "bin"
}
}

View File

@ -22,6 +22,9 @@
namespace Chill\ActivityBundle\Controller;
use Chill\ActivityBundle\Repository\ActivityACLAwareRepository;
use Chill\ActivityBundle\Repository\ActivityACLAwareRepositoryInterface;
use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
@ -36,6 +39,7 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Role\Role;
use Chill\ActivityBundle\Entity\Activity;
use Chill\ActivityBundle\Form\ActivityType;
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
use Symfony\Component\Serializer\SerializerInterface;
/**
@ -53,12 +57,16 @@ class ActivityController extends AbstractController
protected SerializerInterface $serializer;
protected ActivityACLAwareRepositoryInterface $activityACLAwareRepository;
public function __construct(
ActivityACLAwareRepositoryInterface $activityACLAwareRepository,
EventDispatcherInterface $eventDispatcher,
AuthorizationHelper $authorizationHelper,
LoggerInterface $logger,
SerializerInterface $serializer
) {
$this->activityACLAwareRepository = $activityACLAwareRepository;
$this->eventDispatcher = $eventDispatcher;
$this->authorizationHelper = $authorizationHelper;
$this->logger = $logger;
@ -77,13 +85,9 @@ class ActivityController extends AbstractController
[$person, $accompanyingPeriod] = $this->getEntity($request);
if ($person instanceof Person) {
$reachableScopes = $this->authorizationHelper
->getReachableCircles($this->getUser(), new Role('CHILL_ACTIVITY_SEE'),
$person->getCenter());
$activities = $em->getRepository(Activity::class)
->findByPersonImplied($person, $reachableScopes)
;
$this->denyAccessUnlessGranted(ActivityVoter::SEE, $person);
$activities = $this->activityACLAwareRepository
->findByPerson($person, ActivityVoter::SEE, 0, null);
$event = new PrivacyEvent($person, array(
'element_class' => Activity::class,
@ -93,10 +97,10 @@ class ActivityController extends AbstractController
$view = 'ChillActivityBundle:Activity:listPerson.html.twig';
} elseif ($accompanyingPeriod instanceof AccompanyingPeriod) {
$activities = $em->getRepository('ChillActivityBundle:Activity')->findBy(
['accompanyingPeriod' => $accompanyingPeriod],
['date' => 'DESC'],
);
$this->denyAccessUnlessGranted(ActivityVoter::SEE, $accompanyingPeriod);
$activities = $this->activityACLAwareRepository
->findByAccompanyingPeriod($accompanyingPeriod, ActivityVoter::SEE);
$view = 'ChillActivityBundle:Activity:listAccompanyingCourse.html.twig';
}
@ -136,6 +140,12 @@ class ActivityController extends AbstractController
];
}
if ($request->query->has('activityData')) {
$activityData = $request->query->get('activityData');
} else {
$activityData = [];
}
if ($view === null) {
throw $this->createNotFoundException('Template not found');
}
@ -144,6 +154,7 @@ class ActivityController extends AbstractController
'person' => $person,
'accompanyingCourse' => $accompanyingPeriod,
'data' => $data,
'activityData' => $activityData
]);
}
@ -163,10 +174,19 @@ class ActivityController extends AbstractController
$activityType = $em->getRepository(\Chill\ActivityBundle\Entity\ActivityType::class)
->find($activityType_id);
$activityData = null;
if ($request->query->has('activityData')) {
$activityData = $request->query->get('activityData');
}
if (!$activityType instanceof \Chill\ActivityBundle\Entity\ActivityType ||
!$activityType->isActive()) {
$params = $this->buildParamsToUrl($person, $accompanyingPeriod);
if (null !== $activityData) {
$params['activityData'] = $activityData;
}
return $this->redirectToRoute('chill_activity_activity_select_type', $params);
}
@ -184,6 +204,50 @@ class ActivityController extends AbstractController
$entity->setType($activityType);
$entity->setDate(new \DateTime('now'));
if ($request->query->has('activityData')) {
$activityData = $request->query->get('activityData');
if (array_key_exists('durationTime', $activityData)) {
$durationTimeInMinutes = $activityData['durationTime'];
$hours = floor($durationTimeInMinutes / 60);
$minutes = $durationTimeInMinutes % 60;
$duration = \DateTime::createFromFormat("H:i", $hours.':'.$minutes);
if ($duration) {
$entity->setDurationTime($duration);
}
}
if (array_key_exists('date', $activityData)) {
$date = \DateTime::createFromFormat('Y-m-d', $activityData['date']);
if ($date) {
$entity->setDate($date);
}
}
if (array_key_exists('personsId', $activityData)) {
foreach($activityData['personsId'] as $personId){
$concernedPerson = $em->getRepository(\Chill\PersonBundle\Entity\Person::class)->find($personId);
$entity->addPerson($concernedPerson);
}
}
if (array_key_exists('professionalsId', $activityData)) {
foreach($activityData['professionalsId'] as $professionalsId){
$professional = $em->getRepository(\Chill\ThirdPartyBundle\Entity\ThirdParty::class)->find($professionalsId);
$entity->addThirdParty($professional);
}
}
if (array_key_exists('comment', $activityData)) {
$comment = new CommentEmbeddable();
$comment->setComment($activityData['comment']);
$comment->setUserId($this->getUser()->getid());
$comment->setDate(new \DateTime('now'));
$entity->setComment($comment);
}
}
// TODO revoir le Voter de Activity pour tenir compte qu'une activité peut appartenir a une période
// $this->denyAccessUnlessGranted('CHILL_ACTIVITY_CREATE', $entity);
@ -201,6 +265,7 @@ class ActivityController extends AbstractController
$this->addFlash('success', $this->get('translator')->trans('Success : activity created!'));
$params = $this->buildParamsToUrl($person, $accompanyingPeriod);
$params['id'] = $entity->getId();
return $this->redirectToRoute('chill_activity_activity_show', $params);
@ -238,7 +303,7 @@ class ActivityController extends AbstractController
if (!$entity) {
throw $this->createNotFoundException('Unable to find Activity entity.');
}
if (null !== $accompanyingPeriod) {
$entity->personsAssociated = $entity->getPersonsAssociated();
$entity->personsNotAssociated = $entity->getPersonsNotAssociated();

View File

@ -23,6 +23,8 @@
namespace Chill\ActivityBundle\Repository;
use Chill\ActivityBundle\Entity\Activity;
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Chill\ActivityBundle\Repository\ActivityRepository;
use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
@ -33,9 +35,10 @@ use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Role\Role;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\Security;
final class ActivityACLAwareRepository
final class ActivityACLAwareRepository implements ActivityACLAwareRepositoryInterface
{
private AuthorizationHelper $authorizationHelper;
@ -45,16 +48,63 @@ final class ActivityACLAwareRepository
private EntityManagerInterface $em;
private Security $security;
private CenterResolverDispatcher $centerResolverDispatcher;
public function __construct(
AuthorizationHelper $authorizationHelper,
CenterResolverDispatcher $centerResolverDispatcher,
TokenStorageInterface $tokenStorage,
ActivityRepository $repository,
EntityManagerInterface $em
EntityManagerInterface $em,
Security $security
) {
$this->authorizationHelper = $authorizationHelper;
$this->centerResolverDispatcher = $centerResolverDispatcher;
$this->tokenStorage = $tokenStorage;
$this->repository = $repository;
$this->em = $em;
$this->security = $security;
}
/**
* @param Person $person
* @param string $role
* @param int|null $start
* @param int|null $limit
* @param array $orderBy
* @return array|Activity[]
*/
public function findByPerson(Person $person, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = []): array
{
$user = $this->security->getUser();
$center = $this->centerResolverDispatcher->resolveCenter($person);
if (0 === count($orderBy)) {
$orderBy = ['date' => 'DESC'];
}
$reachableScopes = $this->authorizationHelper
->getReachableCircles($user, $role, $center);
return $this->em->getRepository(Activity::class)
->findByPersonImplied($person, $reachableScopes, $orderBy, $limit, $start);
;
}
public function findByAccompanyingPeriod(AccompanyingPeriod $period, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = []): array
{
$user = $this->security->getUser();
$center = $this->centerResolverDispatcher->resolveCenter($period);
if (0 === count($orderBy)) {
$orderBy = ['date' => 'DESC'];
}
$scopes = $this->authorizationHelper
->getReachableCircles($user, $role, $center);
return $this->em->getRepository(Activity::class)
->findByAccompanyingPeriod($period, $scopes, true, $limit, $start, $orderBy);
}
public function queryTimelineIndexer(string $context, array $args = []): array
@ -81,7 +131,7 @@ final class ActivityACLAwareRepository
$metadataActivity = $this->em->getClassMetadata(Activity::class);
$metadataPerson = $this->em->getClassMetadata(Person::class);
$associationMapping = $metadataActivity->getAssociationMapping('person');
return $metadataActivity->getTableName().' JOIN '
.$metadataPerson->getTableName().' ON '
.$metadataPerson->getTableName().'.'.
@ -95,7 +145,7 @@ final class ActivityACLAwareRepository
{
$where = '';
$parameters = [];
$metadataActivity = $this->em->getClassMetadata(Activity::class);
$metadataPerson = $this->em->getClassMetadata(Person::class);
$activityToPerson = $metadataActivity->getAssociationMapping('person')['joinColumns'][0]['name'];
@ -105,20 +155,20 @@ final class ActivityACLAwareRepository
// acls:
$role = new Role(ActivityVoter::SEE);
$reachableCenters = $this->authorizationHelper->getReachableCenters($this->tokenStorage->getToken()->getUser(),
$reachableCenters = $this->authorizationHelper->getReachableCenters($this->tokenStorage->getToken()->getUser(),
$role);
if (count($reachableCenters) === 0) {
// insert a dummy condition
return 'FALSE = TRUE';
}
if ($context === 'person') {
// we start with activities having the person_id linked to person
if ($context === 'person') {
// we start with activities having the person_id linked to person
$where .= sprintf('%s = ? AND ', $activityToPerson);
$parameters[] = $person->getId();
}
// we add acl (reachable center and scopes)
$where .= '('; // first loop for the for centers
$centersI = 0; // like centers#i
@ -131,7 +181,7 @@ final class ActivityACLAwareRepository
$reachableScopes = $this->authorizationHelper->getReachableScopes($this->tokenStorage->getToken()->getUser(), $role, $center);
// we get the ids for those scopes
$reachablesScopesId = array_map(
function(Scope $scope) { return $scope->getId(); },
function(Scope $scope) { return $scope->getId(); },
$reachableScopes
);
@ -162,7 +212,7 @@ final class ActivityACLAwareRepository
}
// close loop for centers
$where .= ')';
return [$where, $parameters];
}

View File

@ -0,0 +1,19 @@
<?php
namespace Chill\ActivityBundle\Repository;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
interface ActivityACLAwareRepositoryInterface
{
/**
* @return array|Activity[]
*/
public function findByPerson(Person $person, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = []): array;
/**
* @return array|Activity[]
*/
public function findByAccompanyingPeriod(AccompanyingPeriod $period, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = []): array;
}

View File

@ -23,6 +23,8 @@
namespace Chill\ActivityBundle\Repository;
use Chill\ActivityBundle\Entity\Activity;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
@ -39,15 +41,22 @@ class ActivityRepository extends ServiceEntityRepository
parent::__construct($registry, Activity::class);
}
public function findByPersonImplied($person, array $scopes, $orderBy = [ 'date' => 'DESC'], $limit = 100, $offset = 0)
/**
* @param $person
* @param array $scopes
* @param string[] $orderBy
* @param int $limit
* @param int $offset
* @return array|Activity[]
*/
public function findByPersonImplied(Person $person, array $scopes, ?array $orderBy = [ 'date' => 'DESC'], ?int $limit = 100, ?int $offset = 0): array
{
$qb = $this->createQueryBuilder('a');
$qb->select('a');
$qb
// TODO add acl
//->where($qb->expr()->in('a.scope', ':scopes'))
//->setParameter('scopes', $scopes)
->where($qb->expr()->in('a.scope', ':scopes'))
->setParameter('scopes', $scopes)
->andWhere(
$qb->expr()->orX(
$qb->expr()->eq('a.person', ':person'),
@ -61,7 +70,56 @@ class ActivityRepository extends ServiceEntityRepository
$qb->addOrderBy('a.'.$k, $dir);
}
$qb->setMaxResults($limit)->setFirstResult($offset);
return $qb->getQuery()
->getResult();
}
}
/**
* @param AccompanyingPeriod $period
* @param array $scopes
* @param int|null $limit
* @param int|null $offset
* @param array|string[] $orderBy
* @return array|Activity[]
*/
public function findByAccompanyingPeriod(AccompanyingPeriod $period, array $scopes, ?bool $allowNullScope = false, ?int $limit = 100, ?int $offset = 0, array $orderBy = ['date' => 'desc']): array
{
$qb = $this->createQueryBuilder('a');
$qb->select('a');
if (!$allowNullScope) {
$qb
->where($qb->expr()->in('a.scope', ':scopes'))
->setParameter('scopes', $scopes)
;
} else {
$qb
->where(
$qb->expr()->orX(
$qb->expr()->in('a.scope', ':scopes'),
$qb->expr()->isNull('a.scope')
)
)
->setParameter('scopes', $scopes)
;
}
$qb
->andWhere(
$qb->expr()->eq('a.accompanyingPeriod', ':period')
)
->setParameter('period', $period)
;
foreach ($orderBy as $k => $dir) {
$qb->addOrderBy('a.'.$k, $dir);
}
$qb->setMaxResults($limit)->setFirstResult($offset);
return $qb->getQuery()
->getResult();
}
}

View File

@ -19,7 +19,7 @@
{% endif %}
<div class="duration">
{% if t.durationTimeVisible > 0 %}
{% if activity.durationTime and t.durationTimeVisible %}
<p>
<i class="fa fa-fw fa-hourglass-end"></i>
{{ activity.durationTime|date('H:i') }}

View File

@ -18,7 +18,12 @@
{% set accompanying_course_id = accompanyingCourse.id %}
{% endif %}
<a href="{{ path('chill_activity_activity_new', {'person_id': person_id, 'activityType_id': activityType.id, 'accompanying_period_id': accompanying_course_id }) }}">
<a href="{{ path('chill_activity_activity_new', {
'person_id': person_id,
'activityType_id': activityType.id,
'accompanying_period_id': accompanying_course_id,
'activityData': activityData
}) }}">
<div class="bloc btn btn-primary btn-lg btn-block">
{{ activityType.name|localize_translatable_string }}

View File

@ -64,12 +64,22 @@
{% if t.durationTimeVisible %}
<dt class="inline">{{ 'Duration Time'|trans }}</dt>
<dd>{{ entity.durationTime|date('H:i') }}</dd>
<dd>{% if entity.durationTime is not null %}
{{ entity.durationTime|date('H:i') }}
{% else %}
{{ 'None'|trans|capitalize }}
{% endif %}
</dd>
{% endif %}
{% if t.travelTimeVisible %}
<dt class="inline">{{ 'Travel Time'|trans }}</dt>
<dd>{{ entity.travelTime|date('H:i') }}</dd>
<dd>{% if entity.travelTime is not null %}
{{ entity.travelTime|date('H:i') }}
{% else %}
{{ 'None'|trans|capitalize }}
{% endif %}
</dd>
{% endif %}
{% if t.commentVisible %}

View File

@ -19,6 +19,11 @@
namespace Chill\ActivityBundle\Security\Authorization;
use Chill\MainBundle\Security\Authorization\VoterHelperFactoryInterface;
use Chill\MainBundle\Security\Authorization\VoterHelperInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Chill\MainBundle\Security\Authorization\AbstractChillVoter;
@ -28,11 +33,10 @@ use Chill\MainBundle\Entity\User;
use Chill\ActivityBundle\Entity\Activity;
use Chill\PersonBundle\Entity\Person;
use Symfony\Component\Security\Core\Role\Role;
use Symfony\Component\Security\Core\Security;
/**
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
* Voter for Activity class
*/
class ActivityVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface
{
@ -41,30 +45,37 @@ class ActivityVoter extends AbstractChillVoter implements ProvideRoleHierarchyIn
const SEE_DETAILS = 'CHILL_ACTIVITY_SEE_DETAILS';
const UPDATE = 'CHILL_ACTIVITY_UPDATE';
const DELETE = 'CHILL_ACTIVITY_DELETE';
const FULL = 'CHILL_ACTIVITY_FULL';
/**
*
* @var AuthorizationHelper
*/
protected $helper;
private const ALL = [
self::CREATE,
self::SEE,
self::UPDATE,
self::DELETE,
self::SEE_DETAILS,
self::FULL
];
public function __construct(AuthorizationHelper $helper)
{
$this->helper = $helper;
protected VoterHelperInterface $voterHelper;
protected Security $security;
public function __construct(
Security $security,
VoterHelperFactoryInterface $voterHelperFactory
) {
$this->security = $security;
$this->voterHelper = $voterHelperFactory->generate(self::class)
->addCheckFor(Person::class, [self::SEE, self::CREATE])
->addCheckFor(AccompanyingPeriod::class, [self::SEE, self::CREATE])
->addCheckFor(Activity::class, self::ALL)
->build();
}
protected function supports($attribute, $subject)
{
if ($subject instanceof Activity) {
return \in_array($attribute, $this->getAttributes());
} elseif ($subject instanceof Person) {
return $attribute === self::SEE
||
$attribute === self::CREATE;
} else {
return false;
}
return $this->voterHelper->supports($attribute, $subject);
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
@ -72,32 +83,34 @@ class ActivityVoter extends AbstractChillVoter implements ProvideRoleHierarchyIn
if (!$token->getUser() instanceof User) {
return false;
}
if ($subject instanceof Person) {
$centers = $this->helper->getReachableCenters($token->getUser(), new Role($attribute));
return \in_array($subject->getCenter(), $centers);
if ($subject instanceof Activity) {
if ($subject->getPerson() instanceof Person) {
// the context is person: we must have the right to see the person
if (!$this->security->isGranted(PersonVoter::SEE, $subject->getPerson())) {
return false;
}
} elseif ($subject->getAccompanyingPeriod() instanceof AccompanyingPeriod) {
if (!$this->security->isGranted(AccompanyingPeriodVoter::SEE, $subject->getAccompanyingPeriod())) {
return false;
}
} else {
throw new \RuntimeException("could not determine context of activity");
}
}
/* @var $subject Activity */
return $this->helper->userHasAccess($token->getUser(), $subject, $attribute);
}
private function getAttributes()
{
return [ self::CREATE, self::SEE, self::UPDATE, self::DELETE,
self::SEE_DETAILS ];
return $this->voterHelper->voteOnAttribute($attribute, $subject, $token);
}
public function getRoles()
{
return $this->getAttributes();
return self::ALL;
}
public function getRolesWithoutScope()
{
return array();
return [];
}

View File

@ -1,20 +1,4 @@
services:
chill.activity.security.authorization.activity_voter:
class: Chill\ActivityBundle\Security\Authorization\ActivityVoter
arguments:
- "@chill.main.security.authorization.helper"
tags:
- { name: security.voter }
- { name: chill.role }
chill.activity.security.authorization.activity_stats_voter:
class: Chill\ActivityBundle\Security\Authorization\ActivityStatsVoter
arguments:
- "@chill.main.security.authorization.helper"
tags:
- { name: security.voter }
- { name: chill.role }
chill.activity.timeline:
class: Chill\ActivityBundle\Timeline\TimelineActivityProvider
@ -38,3 +22,8 @@ services:
autowire: true
autoconfigure: true
resource: '../Notification'
Chill\ActivityBundle\Security\Authorization\:
resource: '../Security/Authorization/'
autowire: true
autoconfigure: true

View File

@ -1,8 +1,4 @@
services:
Chill\ActivityBundle\Controller\ActivityController:
arguments:
$eventDispatcher: '@Symfony\Component\EventDispatcher\EventDispatcherInterface'
$authorizationHelper: '@Chill\MainBundle\Security\Authorization\AuthorizationHelper'
$logger: '@chill.main.logger'
$serializer: '@Symfony\Component\Serializer\SerializerInterface'
autowire: true
tags: ['controller.service_arguments']

View File

@ -24,9 +24,7 @@ services:
- '@Doctrine\Persistence\ManagerRegistry'
Chill\ActivityBundle\Repository\ActivityACLAwareRepository:
arguments:
$tokenStorage: '@Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface'
$authorizationHelper: '@Chill\MainBundle\Security\Authorization\AuthorizationHelper'
$repository: '@Chill\ActivityBundle\Repository\ActivityRepository'
$em: '@Doctrine\ORM\EntityManagerInterface'
autowire: true
autoconfigure: true
Chill\ActivityBundle\Repository\ActivityACLAwareRepositoryInterface: '@Chill\ActivityBundle\Repository\ActivityACLAwareRepository'

View File

@ -36,7 +36,9 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Role\Role;
use Chill\CalendarBundle\Entity\Calendar;
use Chill\CalendarBundle\Form\CalendarType;
use Chill\CalendarBundle\Repository\CalendarRepository;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Routing\Annotation\Route;
@ -55,16 +57,24 @@ class CalendarController extends AbstractController
protected SerializerInterface $serializer;
protected PaginatorFactory $paginator;
private CalendarRepository $calendarRepository;
public function __construct(
EventDispatcherInterface $eventDispatcher,
AuthorizationHelper $authorizationHelper,
LoggerInterface $logger,
SerializerInterface $serializer
SerializerInterface $serializer,
PaginatorFactory $paginator,
CalendarRepository $calendarRepository
) {
$this->eventDispatcher = $eventDispatcher;
$this->authorizationHelper = $authorizationHelper;
$this->logger = $logger;
$this->serializer = $serializer;
$this->paginator = $paginator;
$this->calendarRepository = $calendarRepository;
}
/**
@ -73,31 +83,40 @@ class CalendarController extends AbstractController
*/
public function listAction(Request $request): Response
{
$em = $this->getDoctrine()->getManager();
$view = null;
[$user, $accompanyingPeriod] = $this->getEntity($request);
if ($user instanceof User) {
$calendarItems = $em->getRepository(Calendar::class)
->findByUser($user)
;
$calendarItems = $this->calendarRepository->findByUser($user);
$view = '@ChillCalendar/Calendar/listByUser.html.twig';
return $this->render($view, [
'calendarItems' => $calendarItems,
'user' => $user
]);
} elseif ($accompanyingPeriod instanceof AccompanyingPeriod) {
$calendarItems = $em->getRepository(Calendar::class)->findBy(
$total = $this->calendarRepository->countByAccompanyingPeriod($accompanyingPeriod);
$paginator = $this->paginator->create($total);
$calendarItems = $this->calendarRepository->findBy(
['accompanyingPeriod' => $accompanyingPeriod],
['startDate' => 'DESC']
['startDate' => 'DESC'],
$paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber()
);
$view = '@ChillCalendar/Calendar/listByAccompanyingCourse.html.twig';
}
return $this->render($view, [
'calendarItems' => $calendarItems,
'user' => $user,
'accompanyingCourse' => $accompanyingPeriod,
]);
return $this->render($view, [
'calendarItems' => $calendarItems,
'accompanyingCourse' => $accompanyingPeriod,
'paginator' => $paginator
]);
}
}
/**
@ -111,7 +130,7 @@ class CalendarController extends AbstractController
[$user, $accompanyingPeriod] = $this->getEntity($request);
if ($accompanyingPeriod instanceof AccompanyingPeriod) {
$view = '@ChillCalendar/Calendar/newAccompanyingCourse.html.twig';
$view = '@ChillCalendar/Calendar/newByAccompanyingCourse.html.twig';
}
// elseif ($user instanceof User) {
// $view = '@ChillCalendar/Calendar/newUser.html.twig';
@ -196,10 +215,33 @@ class CalendarController extends AbstractController
throw $this->createNotFoundException('Template not found');
}
$personsId = [];
foreach ($entity->getPersons() as $p) {
array_push($personsId, $p->getId());
}
$professionalsId = [];
foreach ($entity->getProfessionals() as $p) {
array_push($professionalsId, $p->getId());
}
$durationTime = $entity->getEndDate()->diff($entity->getStartDate());
$durationTimeInMinutes = $durationTime->days*1440 + $durationTime->h*60 + $durationTime->i;
$activityData = [
'calendarId' => $id,
'personsId' => $personsId,
'professionalsId' => $professionalsId,
'date' => $entity->getStartDate()->format('Y-m-d'),
'durationTime' => $durationTimeInMinutes,
'comment' => $entity->getComment()->getComment(),
];
return $this->render($view, [
'accompanyingCourse' => $accompanyingPeriod,
'entity' => $entity,
'user' => $user
'user' => $user,
'activityData' => $activityData
//'delete_form' => $deleteForm->createView(),
]);
}

View File

@ -29,6 +29,7 @@ class ChillCalendarExtension extends Extension implements PrependExtensionInterf
$loader->load('services/controller.yml');
$loader->load('services/fixtures.yml');
$loader->load('services/form.yml');
$loader->load('services/event.yml');
}
public function prepend(ContainerBuilder $container)

View File

@ -0,0 +1,40 @@
<?php
namespace Chill\CalendarBundle\Event;
use Chill\ActivityBundle\Entity\Activity;
use Doctrine\Persistence\Event\LifecycleEventArgs;
use Symfony\Component\HttpFoundation\RequestStack;
class ListenToActivityCreate
{
private RequestStack $requestStack;
public function __construct(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}
public function postPersist(Activity $activity, LifecycleEventArgs $event): void
{
// Get the calendarId from the request
$request = $this->requestStack->getCurrentRequest();
if ($request->query->has('activityData')) {
$activityData = $request->query->get('activityData');
if (array_key_exists('calendarId', $activityData)) {
$calendarId = $activityData['calendarId'];
}
}
// Attach the activity to the calendar
$em = $event->getObjectManager();
$calendar = $em->getRepository(\Chill\CalendarBundle\Entity\Calendar::class)->find($calendarId);
$calendar->setActivity($activity);
$em->persist($calendar);
$em->flush();
}
}

View File

@ -3,7 +3,9 @@
namespace Chill\CalendarBundle\Repository;
use Chill\CalendarBundle\Entity\Calendar;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
@ -14,9 +16,13 @@ use Doctrine\Persistence\ManagerRegistry;
*/
class CalendarRepository extends ServiceEntityRepository
{
// private EntityRepository $repository;
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Calendar::class);
// $this->repository = $entityManager->getRepository(AccompanyingPeriodWork::class);
}
// /**

View File

@ -0,0 +1,10 @@
services:
Chill\CalendarBundle\Event\ListenToActivityCreate:
autowire: true
autoconfigure: true
tags:
-
name: 'doctrine.orm.entity_listener'
event: 'postPersist'
entity: 'Chill\ActivityBundle\Entity\Activity'

View File

@ -1,135 +0,0 @@
{% set user_id = null %}
{% if user %}
{% set user_id = user.id %}
{% endif %}
{% set accompanying_course_id = null %}
{% if accompanyingCourse %}
{% set accompanying_course_id = accompanyingCourse.id %}
{% endif %}
{% if context == 'user' %}
<h2>{{ 'My calendar list' |trans }}</h2>
{% else %}
<h2>{{ 'Calendar list' |trans }}</h2>
{% endif %}
{% if context == 'user' %}
<div id="myCalendar"></div>
{% else %}
{% if calendarItems|length == 0 %}
<p class="chill-no-data-statement">
{{ "There is no calendar items."|trans }}
<a href="{{ path('chill_calendar_calendar_new', {'user_id': user_id, 'accompanying_period_id': accompanying_course_id}) }}" class="btn btn-create button-small"></a>
</p>
{% else %}
<div class="flex-table list-records context-{{ context }}">
{% for calendar in calendarItems %}
<div class="item-bloc">
<div class="item-row main">
<div class="item-col">
{% if calendar.startDate and calendar.endDate %}
{% if calendar.endDate.diff(calendar.startDate).days >= 1 %}
<h3>{{ "From the day"|trans }} {{ calendar.startDate|format_datetime('medium', 'short') }} </h3>
<h3>{{ "to the day"|trans }} {{ calendar.endDate|format_datetime('medium', 'short') }}</h3>
{% else %}
<h3>{{ calendar.startDate|format_date('full') }} </h3>
<h3>{{ calendar.startDate|format_datetime('none', 'short', locale='fr') }} - {{ calendar.endDate|format_datetime('none', 'short', locale='fr') }}</h3>
<div class="duration">
<p>
<i class="fa fa-fw fa-hourglass-end"></i>
{{ calendar.endDate.diff(calendar.startDate)|date("%H:%M")}}
</p>
</div>
{% endif %}
{% endif %}
</div>
<div class="item-col">
<ul class="list-content">
{% if calendar.user %}
<li>
<b>{{ 'by'|trans }}{{ calendar.user.usernameCanonical }}</b>
</li>
{% endif %}
{% if calendar.mainUser is not empty %}
<li>
<b>{{ 'main user concerned'|trans }}: {{ calendar.mainUser.usernameCanonical }}</b>
</li>
{% endif %}
</ul>
<ul class="record_actions">
<li>
<a href="{{ path('chill_calendar_calendar_show', { 'id': calendar.id, 'user_id': user_id, 'accompanying_period_id': accompanying_course_id }) }}" class="btn btn-show "></a>
</li>
{# TOOD
{% if is_granted('CHILL_ACTIVITY_UPDATE', calendar) %}
#}
<li>
<a href="{{ path('chill_calendar_calendar_edit', { 'id': calendar.id, 'user_id': user_id, 'accompanying_period_id': accompanying_course_id }) }}" class="btn btn-update "></a>
</li>
{# TOOD
{% endif %}
{% if is_granted('CHILL_ACTIVITY_DELETE', calendar) %}
#}
<li>
<a href="{{ path('chill_calendar_calendar_delete', { 'id': calendar.id, 'user_id' : user_id, 'accompanying_period_id': accompanying_course_id } ) }}" class="btn btn-delete "></a>
</li>
{#
{% endif %}
#}
</ul>
</div>
</div>
{%
if calendar.comment.comment is not empty
or calendar.users|length > 0
or calendar.thirdParties|length > 0
or calendar.users|length > 0
%}
<div class="item-row details">
<div class="item-col">
{% include 'ChillActivityBundle:Activity:concernedGroups.html.twig' with {'context': context, 'with_display': 'row', 'entity': calendar } %}
</div>
{% if calendar.comment.comment is not empty %}
<div class="item-col comment">
{{ calendar.comment|chill_entity_render_box( { 'limit_lines': 3, 'metadata': false } ) }}
</div>
{% endif %}
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% endif %}
{% if context != 'user' %}
{# TODO set this condition in configuration #}
<ul class="record_actions">
<li>
<a href="{{ path('chill_calendar_calendar_new', {'user_id': user_id, 'accompanying_period_id': accompanying_course_id}) }}" class="btn btn-create">
{{ 'Add a new calendar' | trans }}
</a>
</li>
</ul>
{% endif %}
{% endif %}

View File

@ -4,6 +4,123 @@
{% block title %}{{ 'Calendar list' |trans }}{% endblock title %}
{% set user_id = null %}
{% set accompanying_course_id = accompanyingCourse.id %}
{% block content %}
{% include 'ChillCalendarBundle:Calendar:list.html.twig' with {'context': 'accompanyingCourse'} %}
{% endblock %}
<h1>{{ 'Calendar list' |trans }}</h1>
{% if calendarItems|length == 0 %}
<p class="chill-no-data-statement">
{{ "There is no calendar items."|trans }}
<a href="{{ path('chill_calendar_calendar_new', {'user_id': user_id, 'accompanying_period_id': accompanying_course_id}) }}" class="btn btn-create button-small"></a>
</p>
{% else %}
<div class="flex-table list-records context-accompanyingCourse">
{% for calendar in calendarItems %}
<div class="item-bloc">
<div class="item-row main">
<div class="item-col">
{% if calendar.startDate and calendar.endDate %}
{% if calendar.endDate.diff(calendar.startDate).days >= 1 %}
<h3>{{ "From the day"|trans }} {{ calendar.startDate|format_datetime('medium', 'short') }} </h3>
<h3>{{ "to the day"|trans }} {{ calendar.endDate|format_datetime('medium', 'short') }}</h3>
{% else %}
<h3>{{ calendar.startDate|format_date('full') }} </h3>
<h3>{{ calendar.startDate|format_datetime('none', 'short', locale='fr') }} - {{ calendar.endDate|format_datetime('none', 'short', locale='fr') }}</h3>
<div class="duration">
<p>
<i class="fa fa-fw fa-hourglass-end"></i>
{{ calendar.endDate.diff(calendar.startDate)|date("%H:%M")}}
</p>
</div>
{% endif %}
{% endif %}
</div>
<div class="item-col">
<ul class="list-content">
{% if calendar.user %}
<li>
<b>{{ 'by'|trans }}{{ calendar.user.usernameCanonical }}</b>
</li>
{% endif %}
{% if calendar.mainUser is not empty %}
<li>
<b>{{ 'main user concerned'|trans }}: {{ calendar.mainUser.usernameCanonical }}</b>
</li>
{% endif %}
</ul>
<ul class="record_actions">
<li>
<a href="{{ path('chill_calendar_calendar_show', { 'id': calendar.id, 'user_id': user_id, 'accompanying_period_id': accompanying_course_id }) }}" class="btn btn-show "></a>
</li>
{# TOOD
{% if is_granted('CHILL_ACTIVITY_UPDATE', calendar) %}
#}
<li>
<a href="{{ path('chill_calendar_calendar_edit', { 'id': calendar.id, 'user_id': user_id, 'accompanying_period_id': accompanying_course_id }) }}" class="btn btn-update "></a>
</li>
{# TOOD
{% endif %}
{% if is_granted('CHILL_ACTIVITY_DELETE', calendar) %}
#}
<li>
<a href="{{ path('chill_calendar_calendar_delete', { 'id': calendar.id, 'user_id' : user_id, 'accompanying_period_id': accompanying_course_id } ) }}" class="btn btn-delete "></a>
</li>
{#
{% endif %}
#}
</ul>
</div>
</div>
{%
if calendar.comment.comment is not empty
or calendar.users|length > 0
or calendar.thirdParties|length > 0
or calendar.users|length > 0
%}
<div class="item-row details">
<div class="item-col">
{% include 'ChillActivityBundle:Activity:concernedGroups.html.twig' with {'context': accompanyingCourse, 'with_display': 'row', 'entity': calendar } %}
</div>
{% if calendar.comment.comment is not empty %}
<div class="item-col comment">
{{ calendar.comment|chill_entity_render_box( { 'limit_lines': 3, 'metadata': false } ) }}
</div>
{% endif %}
</div>
{% endif %}
</div>
{% endfor %}
{% if calendarItems|length < paginator.getTotalItems %}
{{ chill_pagination(paginator) }}
{% endif %}
</div>
{% endif %}
<ul class="record_actions">
<li>
<a href="{{ path('chill_calendar_calendar_new', {'user_id': user_id, 'accompanying_period_id': accompanying_course_id}) }}" class="btn btn-create">
{{ 'Add a new calendar' | trans }}
</a>
</li>
</ul>
{% endblock %}

View File

@ -5,7 +5,10 @@
{% block title %}{{ 'My calendar list' |trans }}{% endblock title %}
{% block content %}
{% include 'ChillCalendarBundle:Calendar:list.html.twig' with {'context': 'user'} %}
<h1>{{ 'My calendar list' |trans }}</h1>
<div id="myCalendar"></div>
{% endblock %}
{% block js %}

View File

@ -65,13 +65,21 @@
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a class="btn btn-cancel" href="{{ path('chill_calendar_calendar_list', { 'accompanying_period_id': accompanying_course_id, 'user_id': user_id } ) }}">
<a class="btn btn-cancel" href="{{ path('chill_calendar_calendar_list',
{ 'accompanying_period_id': accompanying_course_id, 'user_id': user_id }) }}">
{{ 'Back to the list'|trans }}
</a>
</li>
<li>
<a class="btn btn-create" href="{{ chill_path_add_return_path('chill_activity_activity_new',
{ 'accompanying_period_id': accompanying_course_id, 'activityData': activityData }) }}">
{{ 'Transform to activity'|trans }}
</a>
</li>
{% if accompanyingCourse %}
<li>
<a class="btn btn-update" href="{{ path('chill_calendar_calendar_edit', { 'id': entity.id, 'accompanying_period_id': accompanying_course_id }) }}">
<a class="btn btn-update" href="{{ path('chill_calendar_calendar_edit',
{ 'id': entity.id, 'accompanying_period_id': accompanying_course_id }) }}">
{{ 'Edit'|trans }}
</a>
</li>

View File

@ -23,4 +23,5 @@ Add a new calendar: Ajouter un nouveau rendez-vous
"Success : calendar item created!": "Rendez-vous créé"
The calendar item has been successfully removed.: Le rendez-vous a été supprimé
From the day: Du
to the day: au
to the day: au
Transform to activity: Transformer en échange

View File

@ -23,6 +23,7 @@ use Chill\MainBundle\Security\Authorization\AbstractChillVoter;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
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;
@ -42,30 +43,25 @@ class PersonDocumentVoter extends AbstractChillVoter implements ProvideRoleHiera
const UPDATE = 'CHILL_PERSON_DOCUMENT_UPDATE';
const DELETE = 'CHILL_PERSON_DOCUMENT_DELETE';
/**
* @var AuthorizationHelper
*/
protected $authorizationHelper;
protected AuthorizationHelper $authorizationHelper;
/**
* @var AccessDecisionManagerInterface
*/
protected $accessDecisionManager;
protected AccessDecisionManagerInterface $accessDecisionManager;
/**
* @var LoggerInterface
*/
protected $logger;
protected LoggerInterface $logger;
protected CenterResolverDispatcher $centerResolverDispatcher;
public function __construct(
AccessDecisionManagerInterface $accessDecisionManager,
AuthorizationHelper $authorizationHelper,
LoggerInterface $logger
LoggerInterface $logger//,
//CenterResolverDispatcher $centerResolverDispatcher
)
{
$this->accessDecisionManager = $accessDecisionManager;
$this->authorizationHelper = $authorizationHelper;
$this->logger = $logger;
//$this->centerResolverDispatcher = $centerResolverDispatcher;
}
public function getRoles()
@ -78,17 +74,18 @@ class PersonDocumentVoter extends AbstractChillVoter implements ProvideRoleHiera
self::DELETE
];
}
protected function supports($attribute, $subject)
{
if (\in_array($attribute, $this->getRoles()) && $subject instanceof PersonDocument) {
return true;
}
if ($subject instanceof Person && $attribute === self::CREATE) {
if ($subject instanceof Person
&& \in_array($attribute, [self::CREATE, self::SEE])) {
return true;
}
return false;
}
@ -107,6 +104,8 @@ class PersonDocumentVoter extends AbstractChillVoter implements ProvideRoleHiera
return false;
}
$center = $this->centerResolverDispatcher->resolveCenter($subject);
if ($subject instanceof PersonDocument) {
return $this->authorizationHelper->userHasAccess($token->getUser(), $subject, $attribute);

View File

@ -3,6 +3,11 @@
namespace Chill\MainBundle;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\MainBundle\Search\SearchInterface;
use Chill\MainBundle\Security\Authorization\ChillVoterInterface;
use Chill\MainBundle\Security\ProvideRoleInterface;
use Chill\MainBundle\Security\Resolver\CenterResolverInterface;
use Chill\MainBundle\Security\Resolver\ScopeResolverInterface;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Chill\MainBundle\DependencyInjection\CompilerPass\SearchableServicesCompilerPass;
@ -27,6 +32,12 @@ class ChillMainBundle extends Bundle
$container->registerForAutoconfiguration(LocalMenuBuilderInterface::class)
->addTag('chill.menu_builder');
$container->registerForAutoconfiguration(ProvideRoleInterface::class)
->addTag('chill.role');
$container->registerForAutoconfiguration(CenterResolverInterface::class)
->addTag('chill_main.center_resolver');
$container->registerForAutoconfiguration(ScopeResolverInterface::class)
->addTag('chill_main.scope_resolver');
$container->addCompilerPass(new SearchableServicesCompilerPass());
$container->addCompilerPass(new ConfigConsistencyCompilerPass());

View File

@ -19,6 +19,10 @@
namespace Chill\MainBundle\DependencyInjection;
use Chill\MainBundle\Doctrine\DQL\STContains;
use Chill\MainBundle\Doctrine\DQL\StrictWordSimilarityOPS;
use Chill\MainBundle\Entity\UserJob;
use Chill\MainBundle\Form\UserJobType;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
@ -183,6 +187,8 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface,
'JSONB_EXISTS_IN_ARRAY' => JsonbExistsInArray::class,
'SIMILARITY' => Similarity::class,
'OVERLAPSI' => OverlapsI::class,
'STRICT_WORD_SIMILARITY_OPS' => StrictWordSimilarityOPS::class,
'ST_CONTAINS' => STContains::class,
],
],
'hydrators' => [
@ -264,6 +270,27 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface,
protected function prependCruds(ContainerBuilder $container)
{
$container->prependExtensionConfig('chill_main', [
'cruds' => [
[
'class' => UserJob::class,
'name' => 'admin_user_job',
'base_path' => '/admin/main/user-job',
'base_role' => 'ROLE_ADMIN',
'form_class' => UserJobType::class,
'actions' => [
'index' => [
'role' => 'ROLE_ADMIN',
'template' => '@ChillMain/UserJob/index.html.twig',
],
'new' => [
'role' => 'ROLE_ADMIN'
],
'edit' => [
'role' => 'ROLE_ADMIN'
]
],
],
],
'apis' => [
[
'class' => \Chill\MainBundle\Entity\Address::class,
@ -371,6 +398,26 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface,
],
]
],
[
'class' => \Chill\MainBundle\Entity\Scope::class,
'name' => 'scope',
'base_path' => '/api/1.0/main/scope',
'base_role' => 'ROLE_USER',
'actions' => [
'_index' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true
],
],
'_entity' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true,
]
],
]
],
]
]);
}

View File

@ -0,0 +1,52 @@
<?php
/*
* Chill is a software for social workers
* Copyright (C) 2018 Champs-Libres Coopérative <info@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\MainBundle\Doctrine\DQL;
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Lexer;
/**
* Geometry function 'ST_CONTAINS', added by postgis
*/
class STContains extends FunctionNode
{
private $firstPart;
private $secondPart;
public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker)
{
return 'ST_CONTAINS('.$this->firstPart->dispatch($sqlWalker).
', ' . $this->secondPart->dispatch($sqlWalker) .")";
}
public function parse(\Doctrine\ORM\Query\Parser $parser)
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$this->firstPart = $parser->StringPrimary();
$parser->match(Lexer::T_COMMA);
$this->secondPart = $parser->StringPrimary();
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
}

View File

@ -21,20 +21,15 @@ namespace Chill\MainBundle\Doctrine\DQL;
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Lexer;
/**
*
*
*
*/
class Similarity extends FunctionNode
{
private $firstPart;
private $secondPart;
public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker)
{
return 'SIMILARITY('.$this->firstPart->dispatch($sqlWalker).
return 'SIMILARITY('.$this->firstPart->dispatch($sqlWalker).
', ' . $this->secondPart->dispatch($sqlWalker) .")";
}
@ -42,13 +37,13 @@ class Similarity extends FunctionNode
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$this->firstPart = $parser->StringPrimary();
$parser->match(Lexer::T_COMMA);
$this->secondPart = $parser->StringPrimary();
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace Chill\MainBundle\Doctrine\DQL;
use Doctrine\ORM\Query\Lexer;
use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\SqlWalker;
class StrictWordSimilarityOPS extends \Doctrine\ORM\Query\AST\Functions\FunctionNode
{
private $firstPart;
private $secondPart;
public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker)
{
return $this->firstPart->dispatch($sqlWalker).
' <<% ' . $this->secondPart->dispatch($sqlWalker);
}
public function parse(\Doctrine\ORM\Query\Parser $parser)
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$this->firstPart = $parser->StringPrimary();
$parser->match(Lexer::T_COMMA);
$this->secondPart = $parser->StringPrimary();
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
}

View File

@ -21,7 +21,7 @@ class PointType extends Type {
*
* @param array $fieldDeclaration
* @param AbstractPlatform $platform
* @return type
* @return string
*/
public function getSqlDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
{
@ -32,7 +32,7 @@ class PointType extends Type {
*
* @param type $value
* @param AbstractPlatform $platform
* @return Point
* @return ?Point
*/
public function convertToPHPValue($value, AbstractPlatform $platform)
{

View File

@ -383,6 +383,16 @@ class Address
;
}
public static function createFromAddressReference(AddressReference $original): Address
{
return (new Address())
->setPoint($original->getPoint())
->setPostcode($original->getPostcode())
->setStreet($original->getStreet())
->setStreetNumber($original->getStreetNumber())
;
}
public function getStreet(): ?string
{
return $this->street;

View File

@ -0,0 +1,8 @@
<?php
namespace Chill\MainBundle\Entity;
interface HasCentersInterface
{
public function getCenters(): ?iterable;
}

View File

@ -0,0 +1,11 @@
<?php
namespace Chill\MainBundle\Entity;
interface HasScopesInterface
{
/**
* @return array|Scope[]
*/
public function getScopes(): iterable;
}

View File

@ -3,17 +3,17 @@
/*
* Chill is a suite of a modules, Chill is a software for social workers
* 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/>.
*/
@ -46,17 +46,17 @@ class Scope
* @Groups({"read"})
*/
private $id;
/**
* translatable names
*
*
* @var array
*
* @ORM\Column(type="json_array")
* @Groups({"read"})
*/
private $name = [];
/**
* @var Collection
*
@ -66,8 +66,8 @@ class Scope
* @ORM\Cache(usage="NONSTRICT_READ_WRITE")
*/
private $roleScopes;
/**
* Scope constructor.
*/
@ -75,7 +75,7 @@ class Scope
{
$this->roleScopes = new ArrayCollection();
}
/**
* @return int
*/
@ -91,7 +91,7 @@ class Scope
{
return $this->name;
}
/**
* @param $name
* @return $this
@ -101,7 +101,7 @@ class Scope
$this->name = $name;
return $this;
}
/**
* @return Collection
*/
@ -109,7 +109,7 @@ class Scope
{
return $this->roleScopes;
}
/**
* @param RoleScope $roleScope
*/

View File

@ -5,6 +5,7 @@ namespace Chill\MainBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ArrayCollection;
use Chill\MainBundle\Entity\UserJob;
use Symfony\Component\Security\Core\User\AdvancedUserInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
@ -20,7 +21,7 @@ use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
* })
*/
class User implements AdvancedUserInterface {
/**
* @var integer
*
@ -36,24 +37,30 @@ class User implements AdvancedUserInterface {
* @ORM\Column(type="string", length=80)
*/
private $username;
/**
* @var string
*
* @ORM\Column(
* type="string",
* length=80,
* unique=true)
* unique=true,
* nullable=true)
*/
private $usernameCanonical;
/**
* @ORM\Column(type="string", length=200)
*/
private string $label = '';
/**
* @var string
*
* @ORM\Column(type="string", length=150, nullable=true)
*/
private $email;
/**
* @var string
*
@ -64,14 +71,14 @@ class User implements AdvancedUserInterface {
* unique=true)
*/
private $emailCanonical;
/**
* @var string
*
* @ORM\Column(type="string", length=255)
*/
private $password;
/**
* @var string
* @internal must be set to null if we use bcrypt
@ -79,7 +86,7 @@ class User implements AdvancedUserInterface {
* @ORM\Column(type="string", length=255, nullable=true)
*/
private $salt = null;
/**
* @var boolean
*
@ -87,14 +94,14 @@ class User implements AdvancedUserInterface {
* sf4 check: in yml was false by default !?
*/
private $locked = true;
/**
* @var boolean
*
* @ORM\Column(type="boolean")
*/
private $enabled = true;
/**
* @var Collection
*
@ -112,7 +119,25 @@ class User implements AdvancedUserInterface {
* @ORM\Column(type="json_array", nullable=true)
*/
private $attributes;
/**
* @var Center|null
* @ORM\ManyToOne(targetEntity=Center::class)
*/
private ?Center $mainCenter = null;
/**
* @var Scope|null
* @ORM\ManyToOne(targetEntity=Scope::class)
*/
private ?Scope $mainScope = null;
/**
* @var UserJob|null
* @ORM\ManyToOne(targetEntity=UserJob::class)
*/
private ?UserJob $userJob = null;
/**
* User constructor.
*/
@ -120,13 +145,13 @@ class User implements AdvancedUserInterface {
{
$this->groupCenters = new ArrayCollection();
}
/**
* @return string
*/
public function __toString()
{
return $this->getUsername();
return $this->getLabel();
}
/**
@ -148,10 +173,14 @@ class User implements AdvancedUserInterface {
public function setUsername($name)
{
$this->username = $name;
if (empty($this->getLabel())) {
$this->setLabel($name);
}
return $this;
}
/**
* @return string
*/
@ -159,11 +188,11 @@ class User implements AdvancedUserInterface {
{
return $this->username;
}
/**
*/
public function eraseCredentials() {}
/**
* @return array
*/
@ -171,7 +200,7 @@ class User implements AdvancedUserInterface {
{
return array('ROLE_USER');
}
/**
* @return null|string
*/
@ -179,7 +208,7 @@ class User implements AdvancedUserInterface {
{
return $this->salt;
}
/**
* @param $usernameCanonical
* @return $this
@ -187,10 +216,10 @@ class User implements AdvancedUserInterface {
public function setUsernameCanonical($usernameCanonical)
{
$this->usernameCanonical = $usernameCanonical;
return $this;
}
/**
* @return string
*/
@ -198,7 +227,7 @@ class User implements AdvancedUserInterface {
{
return $this->usernameCanonical;
}
/**
* @param $email
* @return $this
@ -206,10 +235,10 @@ class User implements AdvancedUserInterface {
public function setEmail($email)
{
$this->email = $email;
return $this;
}
/**
* @return string
*/
@ -217,7 +246,7 @@ class User implements AdvancedUserInterface {
{
return $this->email;
}
/**
* @param $emailCanonical
* @return $this
@ -225,10 +254,10 @@ class User implements AdvancedUserInterface {
public function setEmailCanonical($emailCanonical)
{
$this->emailCanonical = $emailCanonical;
return $this;
}
/**
* @return string
*/
@ -236,7 +265,7 @@ class User implements AdvancedUserInterface {
{
return $this->emailCanonical;
}
/**
* @param $password
* @return $this
@ -244,7 +273,7 @@ class User implements AdvancedUserInterface {
function setPassword($password)
{
$this->password = $password;
return $this;
}
@ -255,7 +284,7 @@ class User implements AdvancedUserInterface {
{
return $this->password;
}
/**
* @param $salt
* @return $this
@ -265,7 +294,7 @@ class User implements AdvancedUserInterface {
$this->salt = $salt;
return $this;
}
/**
* @return bool
*/
@ -273,7 +302,7 @@ class User implements AdvancedUserInterface {
{
return true;
}
/**
* @return bool
*/
@ -281,7 +310,7 @@ class User implements AdvancedUserInterface {
{
return $this->locked;
}
/**
* @return bool
*/
@ -289,7 +318,7 @@ class User implements AdvancedUserInterface {
{
return true;
}
/**
* @return bool
*/
@ -297,17 +326,17 @@ class User implements AdvancedUserInterface {
{
return $this->enabled;
}
/**
* @param bool $enabled
*/
public function setEnabled($enabled)
{
$this->enabled = $enabled;
return $this;
}
/**
* @return GroupCenter
*/
@ -315,7 +344,7 @@ class User implements AdvancedUserInterface {
{
return $this->groupCenters;
}
/**
* @param \Chill\MainBundle\Entity\GroupCenter $groupCenter
* @return \Chill\MainBundle\Entity\User
@ -325,7 +354,7 @@ class User implements AdvancedUserInterface {
$this->groupCenters->add($groupCenter);
return $this;
}
/**
* @param \Chill\MainBundle\Entity\GroupCenter $groupCenter
* @throws \RuntimeException if the groupCenter is not in the collection
@ -337,9 +366,9 @@ class User implements AdvancedUserInterface {
. "it seems not to be associated with the user. Aborting."));
}
}
/**
* This function check that groupCenter are present only once. The validator
* This function check that groupCenter are present only once. The validator
* use this function to avoid a user to be associated to the same groupCenter
* more than once.
*/
@ -350,7 +379,7 @@ class User implements AdvancedUserInterface {
if (in_array($groupCenter->getId(), $groupCentersIds)) {
$context->buildViolation("The user has already those permissions")
->addViolation();
} else {
$groupCentersIds[] = $groupCenter->getId();
}
@ -384,4 +413,76 @@ class User implements AdvancedUserInterface {
return $this->attributes;
}
/**
* @return string
*/
public function getLabel(): string
{
return $this->label;
}
/**
* @param string $label
* @return User
*/
public function setLabel(string $label): User
{
$this->label = $label;
return $this;
}
/**
* @return Center|null
*/
public function getMainCenter(): ?Center
{
return $this->mainCenter;
}
/**
* @param Center|null $mainCenter
* @return User
*/
public function setMainCenter(?Center $mainCenter): User
{
$this->mainCenter = $mainCenter;
return $this;
}
/**
* @return Scope|null
*/
public function getMainScope(): ?Scope
{
return $this->mainScope;
}
/**
* @param Scope|null $mainScope
* @return User
*/
public function setMainScope(?Scope $mainScope): User
{
$this->mainScope = $mainScope;
return $this;
}
/**
* @return UserJob|null
*/
public function getUserJob(): ?UserJob
{
return $this->userJob;
}
/**
* @param UserJob|null $userJob
* @return User
*/
public function setUserJob(?UserJob $userJob): User
{
$this->userJob = $userJob;
return $this;
}
}

View File

@ -0,0 +1,76 @@
<?php
namespace Chill\MainBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table("chill_main_user_job")
*/
class UserJob
{
/**
* @ORM\Id
* @ORM\Column(name="id", type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected ?int $id = null;
/**
* @var array|string[]A
* @ORM\Column(name="label", type="json")
*/
protected array $label = [];
/**
* @var bool
* @ORM\Column(name="active", type="boolean")
*/
protected bool $active = true;
/**
* @return int|null
*/
public function getId(): ?int
{
return $this->id;
}
/**
* @return array|string[]
*/
public function getLabel(): array
{
return $this->label;
}
/**
* @param array|string[] $label
* @return UserJob
*/
public function setLabel(array $label): UserJob
{
$this->label = $label;
return $this;
}
/**
* @return bool
*/
public function isActive(): bool
{
return $this->active;
}
/**
* @param bool $active
* @return UserJob
*/
public function setActive(bool $active): UserJob
{
$this->active = $active;
return $this;
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace Chill\MainBundle\Form\Event;
use Symfony\Component\Form\FormBuilderInterface;
class CustomizeFormEvent extends \Symfony\Component\EventDispatcher\Event
{
const NAME = 'chill_main.customize_form';
protected string $type;
protected FormBuilderInterface $builder;
public function __construct(string $type, FormBuilderInterface $builder)
{
$this->type = $type;
$this->builder = $builder;
}
/**
* @return string
*/
public function getType(): string
{
return $this->type;
}
/**
* @return FormBuilderInterface
*/
public function getBuilder(): FormBuilderInterface
{
return $this->builder;
}
}

View File

@ -95,9 +95,12 @@ class CenterType extends AbstractType
public function configureOptions(OptionsResolver $resolver)
{
if (count($this->reachableCenters) > 1) {
$resolver->setDefault('class', Center::class);
$resolver->setDefault('choices', $this->reachableCenters);
$resolver->setDefault('class', Center::class)
->setDefault('choices', $this->reachableCenters)
->setDefault('placeholder', 'Pick a center')
;
}
}
/**

View File

@ -146,14 +146,7 @@ class ScopePickerType extends AbstractType
->setParameter('center', $center->getId())
// role constraints
->andWhere($qb->expr()->in('rs.role', ':roles'))
->setParameter(
'roles', \array_map(
function (Role $role) {
return $role->getRole();
},
$roles
)
)
->setParameter('roles', $roles)
// user contraint
->andWhere(':user MEMBER OF gc.users')
->setParameter('user', $this->tokenStorage->getToken()->getUser());

View File

@ -0,0 +1,27 @@
<?php
namespace Chill\MainBundle\Form;
use Chill\MainBundle\Form\Type\TranslatableStringFormType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
class UserJobType extends \Symfony\Component\Form\AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('label', TranslatableStringFormType::class, [
'label' => 'Label',
'required' => true
])
->add('active', ChoiceType::class, [
'choices' => [
'Active' => true,
'Inactive' => false
]
])
;
}
}

View File

@ -2,7 +2,15 @@
namespace Chill\MainBundle\Form;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\UserJob;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Doctrine\ORM\EntityRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
@ -16,6 +24,16 @@ use Chill\MainBundle\Form\UserPasswordType;
class UserType extends AbstractType
{
private TranslatableStringHelper $translatableStringHelper;
/**
* @param TranslatableStringHelper $translatableStringHelper
*/
public function __construct(TranslatableStringHelper $translatableStringHelper)
{
$this->translatableStringHelper = $translatableStringHelper;
}
/**
* @param FormBuilderInterface $builder
* @param array $options
@ -24,7 +42,40 @@ class UserType extends AbstractType
{
$builder
->add('username')
->add('email')
->add('email', EmailType::class, [
'required' => true
])
->add('label', TextType::class)
->add('mainCenter', EntityType::class, [
'label' => 'main center',
'required' => false,
'placeholder' => 'choose a main center',
'class' => Center::class,
'query_builder' => function (EntityRepository $er) {
$qb = $er->createQueryBuilder('c');
$qb->addOrderBy('c.name');
return $qb;
}
])
->add('mainScope', EntityType::class, [
'label' => 'Choose a main scope',
'required' => false,
'placeholder' => 'choose a main scope',
'class' => Scope::class,
'choice_label' => function (Scope $c) {
return $this->translatableStringHelper->localize($c->getName());
},
])
->add('userJob', EntityType::class, [
'label' => 'Choose a job',
'required' => false,
'placeholder' => 'choose a job',
'class' => UserJob::class,
'choice_label' => function (UserJob $c) {
return $this->translatableStringHelper->localize($c->getLabel());
},
])
;
if ($options['is_creation']) {
$builder->add('plainPassword', RepeatedType::class, array(

View File

@ -36,6 +36,14 @@ final class AddressReferenceRepository implements ObjectRepository
return $this->repository->findAll();
}
public function countAll(): int
{
$qb = $this->repository->createQueryBuilder('ar');
$qb->select('count(ar.id)');
return $qb->getQuery()->getSingleScalarResult();
}
/**
* @return AddressReference[]
*/

View File

@ -0,0 +1,17 @@
const fetchScopes = () => {
return window.fetch('/api/1.0/main/scope.json').then(response => {
if (response.ok) {
return response.json();
}
}).then(data => {
console.log(data);
return new Promise((resolve, reject) => {
console.log(data);
resolve(data.results);
});
});
};
export {
fetchScopes
};

View File

@ -0,0 +1,8 @@
{% extends '@ChillMain/Admin/layout.html.twig' %}
{% block title %}{{ ('crud.' ~ crud_name ~ '.index.title')|trans({'%crud_name%': crud_name}) }}{% endblock %}
{% block content %}
{% embed '@ChillMain/CRUD/_index.html.twig' %}
{% endembed %}
{% endblock content %}

View File

@ -0,0 +1,26 @@
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
{% block content %}
{% embed '@ChillMain/CRUD/_index.html.twig' %}
{% block table_entities_thead_tr %}
<th>id</th>
<th>label</th>
<th>&nbsp;</th>
{% endblock %}
{% block table_entities_tbody %}
{% for entity in entities %}
<tr>
<td>{{ entity.id }}</td>
<td>{{ entity.label|localize_translatable_string }}</td>
<td>
<ul class="record_actions">
<li>
<a href="{{ chill_path_add_return_path('chill_crud_admin_user_job_edit', { 'id': entity.id}) }}" class="btn btn-sm btn-edit btn-mini"></a>
</li>
</ul>
</td>
</tr>
{% endfor %}
{% endblock %}
{% endembed %}
{% endblock content %}

View File

@ -23,45 +23,44 @@ namespace Chill\MainBundle\Search;
/**
* This interface must be implemented on services which provide search results.
*
*
* @todo : write doc and add a link to documentation
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*
*/
interface SearchInterface
{
const SEARCH_PREVIEW_OPTION = '_search_preview';
/**
* Request parameters contained inside the `add_q` parameters
*/
const REQUEST_QUERY_PARAMETERS = '_search_parameters';
/**
* Supplementary parameters to the query string
*/
const REQUEST_QUERY_KEY_ADD_PARAMETERS = 'add_q';
/**
/**
* return the result in a html string. The string will be inclued (as raw)
* into a global view.
*
*
* The global view may be :
* {% for result as resultsFromDifferentSearchInterface %}
* {{ result|raw }}
* {% endfor %}
*
*
* **available options** :
* - SEARCH_PREVIEW_OPTION (boolean) : if renderResult should return a "preview" of
* - SEARCH_PREVIEW_OPTION (boolean) : if renderResult should return a "preview" of
* the results. In this case, a subset of results should be returned, and,
* if the query return more results, a button "see all results" should be
* displayed at the end of the list.
*
* **Interaction between `start` and `limit` and pagination : you should
* take only the given parameters into account; the results from pagination
* should be ignored. (Most of the time, it should be the same).
*
* **Interaction between `start` and `limit` and pagination : you should
* take only the given parameters into account; the results from pagination
* should be ignored. (Most of the time, it should be the same).
*
* @param array $terms the string to search
* @param int $start the first result (for pagination)
@ -72,10 +71,10 @@ interface SearchInterface
*/
public function renderResult(array $terms, $start=0, $limit=50, array $options = array(), $format = 'html');
/**
/**
* we may desactive the search interface by default. in this case,
* the search will be launch and rendered only with "advanced search"
*
* the search will be launch and rendered only with "advanced search"
*
* this may be activated/desactived from bundle definition in config.yml
*
* @return bool
@ -84,18 +83,18 @@ interface SearchInterface
/**
* the order in which the results will appears in the global view
*
*
* (this may be eventually defined in config.yml)
*
* @return int
*
* @return int
*/
public function getOrder();
/**
* indicate if the implementation supports the given domain
*
*
* @return boolean
*/
public function supports($domain, $format);
}

View File

@ -23,8 +23,8 @@ use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
/**
* Voter for Chill software.
*
* Voter for Chill software.
*
* This abstract Voter provide generic methods to handle object specific to Chill
*
*
@ -36,20 +36,20 @@ abstract class AbstractChillVoter extends Voter implements ChillVoterInterface
{
@trigger_error('This voter should implements the new `supports` '
. 'methods introduced by Symfony 3.0, and do not rely on '
. 'getSupportedAttributes and getSupportedClasses methods.',
. 'getSupportedAttributes and getSupportedClasses methods.',
E_USER_DEPRECATED);
return \in_array($attribute, $this->getSupportedAttributes($attribute))
&& \in_array(\get_class($subject), $this->getSupportedClasses());
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
@trigger_error('This voter should implements the new `voteOnAttribute` '
. 'methods introduced by Symfony 3.0, and do not rely on '
. 'isGranted method', E_USER_DEPRECATED);
return $this->isGranted($attribute, $subject, $token->getUser());
}
}

View File

@ -23,6 +23,11 @@ use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\HasCenterInterface;
use Chill\MainBundle\Entity\HasScopeInterface;
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher;
use Chill\MainBundle\Security\Resolver\ScopeResolverDispatcher;
use Chill\MainBundle\Security\Resolver\ScopeResolverInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;
use Symfony\Component\Security\Core\Role\Role;
use Chill\MainBundle\Entity\Scope;
@ -32,87 +37,126 @@ use Chill\MainBundle\Entity\GroupCenter;
use Chill\MainBundle\Entity\RoleScope;
/**
* Helper for authorizations.
*
* Helper for authorizations.
*
* Provides methods for user and entities information.
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class AuthorizationHelper
{
protected RoleHierarchyInterface $roleHierarchy;
/**
*
* @var RoleHierarchyInterface
*/
protected $roleHierarchy;
/**
* The role in a hierarchy, given by the parameter
* The role in a hierarchy, given by the parameter
* `security.role_hierarchy.roles` from the container.
*
* @var string[]
*/
protected $hierarchy;
/**
*
* @var EntityManagerInterface
*/
protected $em;
protected array $hierarchy;
protected EntityManagerInterface $em;
protected CenterResolverDispatcher $centerResolverDispatcher;
protected ScopeResolverDispatcher $scopeResolverDispatcher;
protected LoggerInterface $logger;
public function __construct(
RoleHierarchyInterface $roleHierarchy,
$hierarchy,
EntityManagerInterface $em
ParameterBagInterface $parameterBag,
EntityManagerInterface $em,
CenterResolverDispatcher $centerResolverDispatcher,
LoggerInterface $logger,
ScopeResolverDispatcher $scopeResolverDispatcher
) {
$this->roleHierarchy = $roleHierarchy;
$this->hierarchy = $hierarchy;
$this->hierarchy = $parameterBag->get('security.role_hierarchy.roles');
$this->em = $em;
$this->centerResolverDispatcher = $centerResolverDispatcher;
$this->logger = $logger;
$this->scopeResolverDispatcher = $scopeResolverDispatcher;
}
/**
* Determines if a user is active on this center
*
*
* If
*
* @param User $user
* @param Center $center
* @param Center|Center[] $center May be an array of center
* @return bool
*/
public function userCanReachCenter(User $user, Center $center)
public function userCanReachCenter(User $user, $center)
{
foreach ($user->getGroupCenters() as $groupCenter) {
if ($center->getId() === $groupCenter->getCenter()->getId()) {
return true;
if ($center instanceof \Traversable) {
foreach ($center as $c) {
if ($c->userCanReachCenter($user, $c)) {
return true;
}
}
return false;
} elseif ($center instanceof Center) {
foreach ($user->getGroupCenters() as $groupCenter) {
if ($center->getId() === $groupCenter->getCenter()->getId()) {
return true;
}
}
return false;
}
return false;
throw new \UnexpectedValueException(sprintf("The entity given is not an ".
"instance of %s, %s given", Center::class, get_class($center)));
}
/**
*
*
* Determines if the user has access to the given entity.
*
*
* if the entity implements Chill\MainBundle\Entity\HasScopeInterface,
* the scope is taken into account.
*
*
* @param User $user
* @param HasCenterInterface $entity the entity may also implement HasScopeInterface
* @param mixed $entity the entity may also implement HasScopeInterface
* @param string|Role $attribute
* @return boolean true if the user has access
*/
public function userHasAccess(User $user, HasCenterInterface $entity, $attribute)
public function userHasAccess(User $user, $entity, $attribute)
{
$center = $this->centerResolverDispatcher->resolveCenter($entity);
if (is_iterable($center)) {
foreach ($center as $c) {
if ($this->userHasAccessForCenter($user, $c, $entity, $attribute)) {
return true;
}
}
return false;
} elseif ($center instanceof Center) {
return $this->userHasAccessForCenter($user, $center, $entity, $attribute);
} elseif (NULL === $center) {
return false;
} else {
throw new \UnexpectedValueException("could not resolver a center");
}
}
private function userHasAccessForCenter(User $user, Center $center, $entity, $attribute): bool
{
$center = $entity->getCenter();
if (!$this->userCanReachCenter($user, $center)) {
$this->logger->debug("user cannot reach center of entity", [
'center_name' => $center->getName(),
'user' => $user->getUsername()
]);
return false;
}
foreach ($user->getGroupCenters() as $groupCenter){
//filter on center
if ($groupCenter->getCenter()->getId() === $entity->getCenter()->getId()) {
if ($groupCenter->getCenter() === $center) {
$permissionGroup = $groupCenter->getPermissionsGroup();
//iterate on roleScopes
foreach($permissionGroup->getRoleScopes() as $roleScope) {
@ -120,31 +164,42 @@ class AuthorizationHelper
if ($this->isRoleReached($attribute, $roleScope->getRole())) {
//if yes, we have a right on something...
// perform check on scope if necessary
if ($entity instanceof HasScopeInterface) {
$scope = $entity->getScope();
if ($scope === NULL) {
return true;
}
if ($scope->getId() === $roleScope
->getScope()->getId()) {
return true;
}
if ($this->scopeResolverDispatcher->isConcerned($entity)) {
$scope = $this->scopeResolverDispatcher->resolveScope($entity);
if (NULL === $scope) {
return true;
} elseif (is_iterable($scope)) {
foreach ($scope as $s) {
if ($s === $roleScope->getScope()) {
return true;
}
}
} else {
if ($scope === $roleScope->getScope()) {
return true;
}
}
} else {
return true;
}
}
}
}
}
$this->logger->debug("user can reach center entity, but not role", [
'username' => $user->getUsername(),
'center' => $center->getName()
]);
return false;
}
/**
* Get reachable Centers for the given user, role,
* and optionnaly Scope
*
*
* @param User $user
* @param string|Role $role
* @param null|Scope $scope
@ -156,7 +211,7 @@ class AuthorizationHelper
$role = $role->getRole();
}
$centers = array();
foreach ($user->getGroupCenters() as $groupCenter){
$permissionGroup = $groupCenter->getPermissionsGroup();
//iterate on roleScopes
@ -170,13 +225,13 @@ class AuthorizationHelper
if ($scope->getId() == $roleScope->getScope()->getId()){
$centers[] = $groupCenter->getCenter();
break 1;
}
}
}
}
}
}
return $centers;
}
@ -203,18 +258,18 @@ class AuthorizationHelper
return $results;
}
/**
* Return all reachable scope for a given user, center and role
*
*
* @deprecated Use getReachableCircles
*
* @param User $user
* @param Role $role
* @param Center $center
* @param string role
* @param Center|Center[] $center
* @return Scope[]
*/
public function getReachableScopes(User $user, $role, Center $center)
public function getReachableScopes(User $user, $role, $center)
{
if ($role instanceof Role) {
$role = $role->getRole();
@ -222,22 +277,31 @@ class AuthorizationHelper
return $this->getReachableCircles($user, $role, $center);
}
/**
* Return all reachable circle for a given user, center and role
*
*
* @param User $user
* @param string|Role $role
* @param Center $center
* @param Center|Center[] $center
* @return Scope[]
*/
public function getReachableCircles(User $user, $role, Center $center)
public function getReachableCircles(User $user, $role, $center)
{
$scopes = [];
if (is_iterable($center)) {
foreach ($center as $c) {
$scopes = \array_merge($scopes, $this->getReachableCircles($user, $role, $c));
}
return $scopes;
}
if ($role instanceof Role) {
$role = $role->getRole();
}
$scopes = array();
foreach ($user->getGroupCenters() as $groupCenter){
if ($center->getId() === $groupCenter->getCenter()->getId()) {
//iterate on permissionGroup
@ -251,23 +315,19 @@ class AuthorizationHelper
}
}
}
return $scopes;
}
/**
*
* @param Role $role
* @param Center $center
* @param Scope $circle
* @return Users
*
* @return User[]
*/
public function findUsersReaching(Role $role, Center $center, Scope $circle = null)
public function findUsersReaching(string $role, Center $center, Scope $circle = null): array
{
$parents = $this->getParentRoles($role);
$parents[] = $role;
$parentRolesString = \array_map(function(Role $r) { return $r->getRole(); }, $parents);
$qb = $this->em->createQueryBuilder();
$qb
->select('u')
@ -276,23 +336,23 @@ class AuthorizationHelper
->join('gc.permissionsGroup', 'pg')
->join('pg.roleScopes', 'rs')
->where('gc.center = :center')
->andWhere($qb->expr()->in('rs.role', $parentRolesString))
->andWhere($qb->expr()->in('rs.role', $parents))
;
$qb->setParameter('center', $center);
if ($circle !== null) {
$qb->andWhere('rs.scope = :circle')
->setParameter('circle', $circle)
;
}
return $qb->getQuery()->getResult();
}
/**
* Test if a parent role may give access to a given child role
*
*
* @param Role $childRole The role we want to test if he is reachable
* @param Role $parentRole The role which should give access to $childRole
* @return boolean true if the child role is granted by parent role
@ -301,36 +361,31 @@ class AuthorizationHelper
{
$reachableRoles = $this->roleHierarchy
->getReachableRoleNames([$parentRole]);
return in_array($childRole, $reachableRoles);
}
/**
* Return all the role which give access to the given role. Only the role
* Return all the role which give access to the given role. Only the role
* which are registered into Chill are taken into account.
*
*
* @param Role $role
* @return Role[] the role which give access to the given $role
* @return string[] the role which give access to the given $role
*/
public function getParentRoles(Role $role)
public function getParentRoles($role): array
{
$parentRoles = [];
// transform the roles from role hierarchy from string to Role
$roles = \array_map(
function($string) {
return new Role($string);
},
\array_keys($this->hierarchy)
);
$roles = \array_keys($this->hierarchy);
foreach ($roles as $r) {
$childRoles = $this->roleHierarchy->getReachableRoleNames([$r->getRole()]);
$childRoles = $this->roleHierarchy->getReachableRoleNames([$r]);
if (\in_array($role, $childRoles)) {
$parentRoles[] = $r;
}
}
return $parentRoles;
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace Chill\MainBundle\Security\Authorization;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher;
final class DefaultVoterHelper implements VoterHelperInterface
{
protected AuthorizationHelper $authorizationHelper;
protected CenterResolverDispatcher $centerResolverDispatcher;
protected array $configuration = [];
/**
* @param AuthorizationHelper $authorizationHelper
* @param CenterResolverDispatcher $centerResolverDispatcher
* @param array $configuration
*/
public function __construct(
AuthorizationHelper $authorizationHelper,
CenterResolverDispatcher $centerResolverDispatcher,
array $configuration
) {
$this->authorizationHelper = $authorizationHelper;
$this->centerResolverDispatcher = $centerResolverDispatcher;
$this->configuration = $configuration;
}
public function supports($attribute, $subject): bool
{
foreach ($this->configuration as list($attributes, $subj)) {
if ($subj === null) {
if ($subject === null && \in_array($attribute, $attributes)) {
return true;
}
} elseif ($subject instanceof $subj) {
return \in_array($attribute, $attributes);
}
}
return false;
}
public function voteOnAttribute($attribute, $subject, $token): bool
{
if (!$token->getUser() instanceof User) {
return false;
}
if (NULL === $subject) {
return 0 < count($this->authorizationHelper->getReachableCenters($token->getUser(), $attribute, null));
}
return $this->authorizationHelper->userHasAccess(
$token->getUser(),
$subject,
$attribute
);
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Chill\MainBundle\Security\Authorization;
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher;
class DefaultVoterHelperFactory implements VoterHelperFactoryInterface
{
protected AuthorizationHelper $authorizationHelper;
protected CenterResolverDispatcher $centerResolverDispatcher;
public function __construct(
AuthorizationHelper $authorizationHelper,
CenterResolverDispatcher $centerResolverDispatcher
) {
$this->authorizationHelper = $authorizationHelper;
$this->centerResolverDispatcher = $centerResolverDispatcher;
}
public function generate($context): VoterGeneratorInterface
{
return new DefaultVoterHelperGenerator(
$this->authorizationHelper,
$this->centerResolverDispatcher
);
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace Chill\MainBundle\Security\Authorization;
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher;
final class DefaultVoterHelperGenerator implements VoterGeneratorInterface
{
protected AuthorizationHelper $authorizationHelper;
protected CenterResolverDispatcher $centerResolverDispatcher;
protected array $configuration = [];
public function __construct(
AuthorizationHelper $authorizationHelper,
CenterResolverDispatcher $centerResolverDispatcher
) {
$this->authorizationHelper = $authorizationHelper;
$this->centerResolverDispatcher = $centerResolverDispatcher;
}
public function addCheckFor(?string $subject, array $attributes): self
{
$this->configuration[] = [$attributes, $subject];
return $this;
}
public function build(): VoterHelperInterface
{
return new DefaultVoterHelper(
$this->authorizationHelper,
$this->centerResolverDispatcher,
$this->configuration
);
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace Chill\MainBundle\Security\Authorization;
interface VoterGeneratorInterface
{
/**
* @param string $class The FQDN of a class
* @param array $attributes an array of attributes
* @return $this
*/
public function addCheckFor(?string $class, array $attributes): self;
public function build(): VoterHelperInterface;
}

View File

@ -0,0 +1,8 @@
<?php
namespace Chill\MainBundle\Security\Authorization;
interface VoterHelperFactoryInterface
{
public function generate($context): VoterGeneratorInterface;
}

View File

@ -0,0 +1,12 @@
<?php
namespace Chill\MainBundle\Security\Authorization;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
interface VoterHelperInterface
{
public function supports($attribute, $subject): bool;
public function voteOnAttribute($attribute, $subject, TokenInterface $token);
}

View File

@ -0,0 +1,32 @@
<?php
namespace Chill\MainBundle\Security\Resolver;
class CenterResolverDispatcher
{
/**
* @var iterabble|CenterResolverInterface[]
*/
private iterable $resolvers = [];
public function __construct(iterable $resolvers)
{
$this->resolvers = $resolvers;
}
/**
* @param mixed $entity
* @param array|null $options
* @return null|Center|Center[]
*/
public function resolveCenter($entity, ?array $options = [])
{
foreach($this->resolvers as $priority => $resolver) {
if ($resolver->supports($entity, $options)) {
return $resolver->resolveCenter($entity, $options);
}
}
return null;
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace Chill\MainBundle\Security\Resolver;
use Chill\MainBundle\Entity\Center;
interface CenterResolverInterface
{
public function supports($entity, ?array $options = []): bool;
/**
* @param $entity
* @param array|null $options
* @return Center|array|Center[]
*/
public function resolveCenter($entity, ?array $options = []);
public static function getDefaultPriority(): int;
}

View File

@ -0,0 +1,37 @@
<?php
namespace Chill\MainBundle\Security\Resolver;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\HasCenterInterface;
use Chill\MainBundle\Entity\HasCentersInterface;
class DefaultCenterResolver implements CenterResolverInterface
{
public function supports($entity, ?array $options = []): bool
{
return $entity instanceof HasCenterInterface || $entity instanceof HasCentersInterface;
}
/**
* @inheritDoc
*
* @param HasCenterInterface $entity
* @param array $options
*/
public function resolveCenter($entity, ?array $options = [])
{
if ($entity instanceof HasCenterInterface) {
return $entity->getCenter();
} elseif ($entity instanceof HasCentersInterface) {
return $entity->getCenters();
} else {
throw new \UnexpectedValueException("should be an instanceof");
}
}
public static function getDefaultPriority(): int
{
return -256;
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace Chill\MainBundle\Security\Resolver;
use Chill\MainBundle\Entity\HasScopeInterface;
use Chill\MainBundle\Entity\HasScopesInterface;
class DefaultScopeResolver implements ScopeResolverInterface
{
public function supports($entity, ?array $options = []): bool
{
return $entity instanceof HasScopeInterface || $entity instanceof HasScopesInterface;
}
/**
* @inheritDoc
*
* @param HasScopeInterface|HasScopesInterface $entity
*/
public function resolveScope($entity, ?array $options = [])
{
if ($entity instanceof HasScopeInterface) {
return $entity->getScope();
} elseif ($entity instanceof HasScopesInterface) {
return $entity->getScopes();
} else {
throw new \UnexpectedValueException("should be an instanceof %s or %s",
HasScopesInterface::class, HasScopeInterface::class);
}
}
public function isConcerned($entity, ?array $options = []): bool
{
return $entity instanceof HasScopeInterface || $entity instanceof HasScopesInterface;
}
public static function getDefaultPriority(): int
{
return -256;
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace Chill\MainBundle\Security\Resolver;
use Twig\TwigFilter;
final class ResolverTwigExtension extends \Twig\Extension\AbstractExtension
{
private CenterResolverDispatcher $centerResolverDispatcher;
/**
* @param CenterResolverDispatcher $centerResolverDispatcher
*/
public function __construct(CenterResolverDispatcher $centerResolverDispatcher)
{
$this->centerResolverDispatcher = $centerResolverDispatcher;
}
public function getFilters()
{
return [
new TwigFilter('chill_resolve_center', [$this, 'resolveCenter'])
];
}
/**
* @param mixed $entity
* @param array|null $options
* @return Center|Center[]|null
*/
public function resolveCenter($entity, ?array $options = [])
{
return $this->centerResolverDispatcher->resolveCenter($entity, $options);
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace Chill\MainBundle\Security\Resolver;
use Chill\MainBundle\Entity\Scope;
final class ScopeResolverDispatcher
{
/**
* @var iterable|ScopeResolverInterface[]
*/
private iterable $resolvers;
public function __construct(iterable $resolvers)
{
$this->resolvers = $resolvers;
}
/**
* @param $entity
* @return Scope|Scope[]|iterable
*/
public function resolveScope($entity, ?array $options = [])
{
foreach ($this->resolvers as $resolver) {
if ($resolver->supports($entity, $options)) {
return $resolver->resolveScope($entity, $options);
}
}
return null;
}
public function isConcerned($entity, ?array $options = []): bool
{
foreach ($this->resolvers as $resolver) {
if ($resolver->supports($entity, $options)) {
return $resolver->isConcerned($entity, $options);
}
}
return false;
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace Chill\MainBundle\Security\Resolver;
use Chill\MainBundle\Entity\Scope;
/**
* Interface to implement to define a ScopeResolver.
*/
interface ScopeResolverInterface
{
/**
* Return true if this resolve is able to decide "something" on this entity.
*/
public function supports($entity, ?array $options = []): bool;
/**
* Will return the scope for the entity
*
* @return Scope|array|Scope[]
*/
public function resolveScope($entity, ?array $options = []);
/**
* Return true if the entity is concerned by scope, false otherwise.
*/
public function isConcerned($entity, ?array $options = []): bool;
/**
* get the default priority for this resolver. Resolver with an higher priority will be
* queried first.
*/
public static function getDefaultPriority(): int;
}

View File

@ -7,11 +7,11 @@ use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class UserControllerTest extends WebTestCase
{
private $client;
public function setUp()
{
self::bootKernel();
$this->client = static::createClient(array(), array(
'PHP_AUTH_USER' => 'admin',
'PHP_AUTH_PW' => 'password',
@ -23,62 +23,64 @@ class UserControllerTest extends WebTestCase
{
// get the list
$crawler = $this->client->request('GET', '/fr/admin/user/');
$this->assertEquals(200, $this->client->getResponse()->getStatusCode(),
$this->assertEquals(200, $this->client->getResponse()->getStatusCode(),
"Unexpected HTTP status code for GET /admin/user/");
$link = $crawler->selectLink('Ajouter un nouvel utilisateur')->link();
$this->assertInstanceOf('Symfony\Component\DomCrawler\Link', $link);
$this->assertRegExp('|/fr/admin/user/new$|', $link->getUri());
}
public function testNew()
{
$crawler = $this->client->request('GET', '/fr/admin/user/new');
$username = 'Test_user'. uniqid();
$password = 'Password1234!';
dump($crawler->text());
// Fill in the form and submit it
$form = $crawler->selectButton('Créer')->form(array(
'chill_mainbundle_user[username]' => $username,
'chill_mainbundle_user[plainPassword][first]' => $password,
'chill_mainbundle_user[plainPassword][second]' => $password
'chill_mainbundle_user[plainPassword][second]' => $password,
'chill_mainbundle_user[email]' => $username.'@gmail.com',
'chill_mainbundle_user[label]' => $username,
));
$this->client->submit($form);
$crawler = $this->client->followRedirect();
// Check data in the show view
$this->assertGreaterThan(0, $crawler->filter('td:contains("Test_user")')->count(),
$this->assertGreaterThan(0, $crawler->filter('td:contains("Test_user")')->count(),
'Missing element td:contains("Test user")');
$update = $crawler->selectLink('Modifier')->link();
$this->assertInstanceOf('Symfony\Component\DomCrawler\Link', $update);
$this->assertRegExp('|/fr/admin/user/[0-9]{1,}/edit$|', $update->getUri());
//test the auth of the new client
$this->isPasswordValid($username, $password);
return $update;
}
protected function isPasswordValid($username, $password)
{
/* @var $passwordEncoder \Symfony\Component\Security\Core\Encoder\UserPasswordEncoder */
$passwordEncoder = self::$kernel->getContainer()
->get('security.password_encoder');
$user = self::$kernel->getContainer()
->get('doctrine.orm.entity_manager')
->getRepository('ChillMainBundle:User')
->findOneBy(array('username' => $username));
$this->assertTrue($passwordEncoder->isPasswordValid($user, $password));
}
/**
*
*
* @param \Symfony\Component\DomCrawler\Link $update
* @depends testNew
*/
@ -90,24 +92,24 @@ class UserControllerTest extends WebTestCase
$form = $crawler->selectButton('Mettre à jour')->form(array(
'chill_mainbundle_user[username]' => $username,
));
$this->client->submit($form);
$crawler = $this->client->followRedirect();
// Check the element contains an attribute with value equals "Foo"
$this->assertGreaterThan(0, $crawler->filter('[value="'.$username.'"]')->count(),
$this->assertGreaterThan(0, $crawler->filter('[value="'.$username.'"]')->count(),
'Missing element [value="Foo bar"]');
$updatePassword = $crawler->selectLink('Modifier le mot de passe')->link();
$this->assertInstanceOf('Symfony\Component\DomCrawler\Link', $updatePassword);
$this->assertRegExp('|/fr/admin/user/[0-9]{1,}/edit_password$|',
$this->assertRegExp('|/fr/admin/user/[0-9]{1,}/edit_password$|',
$updatePassword->getUri());
return array('link' => $updatePassword, 'username' => $username);
}
/**
*
*
* @param \Symfony\Component\DomCrawler\Link $updatePassword
* @depends testUpdate
*/
@ -116,22 +118,22 @@ class UserControllerTest extends WebTestCase
$link = $params['link'];
$username = $params['username'];
$newPassword = '1234Password!';
$crawler = $this->client->click($link);
$form = $crawler->selectButton('Changer le mot de passe')->form(array(
'chill_mainbundle_user_password[new_password][first]' => $newPassword,
'chill_mainbundle_user_password[new_password][second]' => $newPassword,
));
$this->client->submit($form);
$this->assertTrue($this->client->getResponse()->isRedirect(),
$this->assertTrue($this->client->getResponse()->isRedirect(),
"the response is a redirection");
$this->client->followRedirect();
$this->isPasswordValid($username, $newPassword);
}
}

View File

@ -19,6 +19,10 @@
namespace Chill\MainBundle\Tests\Security\Authorization;
use Chill\MainBundle\Entity\HasCenterInterface;
use Chill\MainBundle\Entity\HasCentersInterface;
use Chill\MainBundle\Entity\HasScopeInterface;
use Chill\MainBundle\Entity\HasScopesInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Chill\MainBundle\Test\PrepareUserTrait;
use Chill\MainBundle\Test\PrepareCenterTrait;
@ -30,22 +34,22 @@ use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\Center;
/**
*
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class AuthorizationHelperTest extends KernelTestCase
{
use PrepareUserTrait, PrepareCenterTrait, PrepareScopeTrait, ProphecyTrait;
public function setUp()
public function setUp()
{
static::bootKernel();
}
/**
*
*
* @return \Chill\MainBundle\Security\Authorization\AuthorizationHelper
*/
private function getAuthorizationHelper()
@ -54,13 +58,13 @@ class AuthorizationHelperTest extends KernelTestCase
->get('chill.main.security.authorization.helper')
;
}
/**
* Test function userCanReach of helper.
*
*
* A user can reach center => the function should return true.
*/
public function testUserCanReachCenter_UserShouldReach()
public function testUserCanReachCenter_UserShouldReach()
{
$center = $this->prepareCenter(1, 'center');
$scope = $this->prepareScope(1, 'default');
@ -72,16 +76,16 @@ class AuthorizationHelperTest extends KernelTestCase
)
));
$helper = $this->getAuthorizationHelper();
$this->assertTrue($helper->userCanReachCenter($user, $center));
}
/**
* Test function userCanReach of helper
*
*
* A user can not reachcenter =>W the function should return false
*/
public function testUserCanReachCenter_UserShouldNotReach()
public function testUserCanReachCenter_UserShouldNotReach()
{
$centerA = $this->prepareCenter(1, 'center');
$centerB = $this->prepareCenter(2, 'centerB');
@ -94,11 +98,11 @@ class AuthorizationHelperTest extends KernelTestCase
)
));
$helper = $this->getAuthorizationHelper();
$this->assertFalse($helper->userCanReachCenter($user, $centerB));
}
public function testUserHasAccess_shouldHaveAccess_EntityWithoutScope()
{
$center = $this->prepareCenter(1, 'center');
@ -114,11 +118,11 @@ class AuthorizationHelperTest extends KernelTestCase
$entity = $this->getProphet()->prophesize();
$entity->willImplement('\Chill\MainBundle\Entity\HasCenterInterface');
$entity->getCenter()->willReturn($center);
$this->assertTrue($helper->userHasAccess($user, $entity->reveal(),
$this->assertTrue($helper->userHasAccess($user, $entity->reveal(),
'CHILL_ROLE'));
}
public function testUserHasAccess_ShouldHaveAccessWithInheritance_EntityWithoutScope()
{
$center = $this->prepareCenter(1, 'center');
@ -130,17 +134,17 @@ class AuthorizationHelperTest extends KernelTestCase
)
)
));
$helper = $this->getAuthorizationHelper();
$entity = $this->getProphet()->prophesize();
$entity->willImplement('\Chill\MainBundle\Entity\HasCenterInterface');
$entity->getCenter()->willReturn($center);
$this->assertTrue($helper->userHasAccess($user, $entity->reveal(),
$this->assertTrue($helper->userHasAccess($user, $entity->reveal(),
'CHILL_INHERITED_ROLE_1'));
}
public function testuserHasAccess_UserHasNoRole_EntityWithoutScope()
{
$center = $this->prepareCenter(1, 'center');
@ -156,10 +160,10 @@ class AuthorizationHelperTest extends KernelTestCase
$entity = $this->getProphet()->prophesize();
$entity->willImplement('\Chill\MainBundle\Entity\HasCenterInterface');
$entity->getCenter()->willReturn($center);
$this->assertFalse($helper->userHasAccess($user, $entity->reveal(), 'CHILL_ROLE'));
}
/**
* test that a user has no access on a entity, but is granted on the same role
* on another center
@ -186,10 +190,10 @@ class AuthorizationHelperTest extends KernelTestCase
$entity = $this->getProphet()->prophesize();
$entity->willImplement('\Chill\MainBundle\Entity\HasCenterInterface');
$entity->getCenter()->willReturn($centerA);
$this->assertFalse($helper->userHasAccess($user, $entity->reveal(), 'CHILL_ROLE'));
}
public function testtestUserHasAccess_UserShouldHaveAccess_EntityWithScope()
{
$center = $this->prepareCenter(1, 'center');
@ -207,10 +211,10 @@ class AuthorizationHelperTest extends KernelTestCase
$entity->willImplement('\Chill\MainBundle\Entity\HasScopeInterface');
$entity->getCenter()->willReturn($center);
$entity->getScope()->willReturn($scope);
$this->assertTrue($helper->userHasAccess($user, $entity->reveal(), 'CHILL_ROLE'));
}
public function testUserHasAccess_UserHasNoRole_EntityWithScope()
{
$center = $this->prepareCenter(1, 'center');
@ -228,10 +232,10 @@ class AuthorizationHelperTest extends KernelTestCase
$entity->willImplement('\Chill\MainBundle\Entity\HasScopeInterface');
$entity->getCenter()->willReturn($center);
$entity->getScope()->willReturn($scope);
$this->assertFalse($helper->userHasAccess($user, $entity->reveal(), 'ANOTHER_ROLE'));
}
public function testUserHasAccess_UserHasNoCenter_EntityWithScope()
{
$centerA = $this->prepareCenter(1, 'center'); //the user will have this center
@ -250,10 +254,10 @@ class AuthorizationHelperTest extends KernelTestCase
$entity->willImplement('\Chill\MainBundle\Entity\HasScopeInterface');
$entity->getCenter()->willReturn($centerB);
$entity->getScope()->willReturn($scope);
$this->assertFalse($helper->userHasAccess($user, $entity->reveal(), 'CHILL_ROLE'));
}
public function testUserHasAccess_UserHasNoScope_EntityWithScope()
{
$center = $this->prepareCenter(1, 'center');
@ -268,16 +272,106 @@ class AuthorizationHelperTest extends KernelTestCase
));
$helper = $this->getAuthorizationHelper();
$entity = $this->getProphet()->prophesize();
$entity->willImplement('\Chill\MainBundle\Entity\HasCenterInterface');
$entity->willImplement('\Chill\MainBundle\Entity\HasScopeInterface');
$entity->willImplement(HasCenterInterface::class);
$entity->willImplement(HasScopeInterface::class);
$entity->getCenter()->willReturn($center);
$entity->getScope()->willReturn($scopeA);
$this->assertFalse($helper->userHasAccess($user, $entity->reveal(), 'CHILL_ROLE'));
}
public function testUserHasAccess_MultiCenter_EntityWithoutScope()
{
$center = $this->prepareCenter(1, 'center');
$centerB = $this->prepareCenter(1, 'centerB');
$scopeB = $this->prepareScope(2, 'other'); //the user will be granted this scope
$user = $this->prepareUser(array(
array(
'center' => $center, 'permissionsGroup' => array(
['scope' => $scopeB, 'role' => 'CHILL_ROLE']
)
)
));
$helper = $this->getAuthorizationHelper();
$entity = $this->getProphet()->prophesize();
$entity->willImplement(HasCentersInterface::class);
$entity->getCenters()->willReturn([$center, $centerB]);
$this->assertTrue($helper->userHasAccess($user, $entity->reveal(), 'CHILL_ROLE'));
}
public function testUserHasNoAccess_MultiCenter_EntityWithoutScope()
{
$center = $this->prepareCenter(1, 'center');
$centerB = $this->prepareCenter(1, 'centerB');
$centerC = $this->prepareCenter(1, 'centerC');
$scopeB = $this->prepareScope(2, 'other'); //the user will be granted this scope
$user = $this->prepareUser(array(
array(
'center' => $center, 'permissionsGroup' => array(
['scope' => $scopeB, 'role' => 'CHILL_ROLE']
)
)
));
$helper = $this->getAuthorizationHelper();
$entity = $this->getProphet()->prophesize();
$entity->willImplement(HasCentersInterface::class);
$entity->getCenters()->willReturn([$centerB, $centerC]);
$this->assertFalse($helper->userHasAccess($user, $entity->reveal(), 'CHILL_ROLE'));
}
public function testUserHasNoAccess_EntityMultiScope()
{
$centerA = $this->prepareCenter(1, 'center');
$centerB = $this->prepareCenter(1, 'centerB');
$scopeA = $this->prepareScope(2, 'other'); //the user will be granted this scope
$scopeB = $this->prepareScope(2, 'other'); //the user will be granted this scope
$scopeC = $this->prepareScope(2, 'other'); //the user will be granted this scope
$user = $this->prepareUser(array(
array(
'center' => $centerA, 'permissionsGroup' => array(
['scope' => $scopeA, 'role' => 'CHILL_ROLE']
)
)
));
$helper = $this->getAuthorizationHelper();
$entity = $this->getProphet()->prophesize();
$entity->willImplement(HasCentersInterface::class);
$entity->willImplement(HasScopesInterface::class);
$entity->getCenters()->willReturn([$centerA, $centerB]);
$entity->getScopes()->willReturn([$scopeB, $scopeC]);
$this->assertFalse($helper->userHasAccess($user, $entity->reveal(), 'CHILL_ROLE'));
}
public function testUserHasAccess_EntityMultiScope()
{
$centerA = $this->prepareCenter(1, 'center');
$centerB = $this->prepareCenter(1, 'centerB');
$scopeA = $this->prepareScope(2, 'other'); //the user will be granted this scope
$scopeB = $this->prepareScope(2, 'other'); //the user will be granted this scope
$user = $this->prepareUser(array(
array(
'center' => $centerA, 'permissionsGroup' => array(
['scope' => $scopeA, 'role' => 'CHILL_ROLE']
)
)
));
$helper = $this->getAuthorizationHelper();
$entity = $this->getProphet()->prophesize();
$entity->willImplement(HasCentersInterface::class);
$entity->willImplement(HasScopesInterface::class);
$entity->getCenters()->willReturn([$centerA, $centerB]);
$entity->getScopes()->willReturn([$scopeA, $scopeB]);
$this->assertTrue($helper->userHasAccess($user, $entity->reveal(), 'CHILL_ROLE'));
}
/**
*
*
* @dataProvider dataProvider_getReachableCenters
* @param Center $shouldHaveCenter
* @param User $user
@ -288,7 +382,7 @@ class AuthorizationHelperTest extends KernelTestCase
{
$this->assertEquals($test, $result, $msg);
}
public function dataProvider_getReachableCenters()
{
$this->setUp();
@ -297,10 +391,10 @@ class AuthorizationHelperTest extends KernelTestCase
$scopeA = $this->prepareScope(1, 'scope default');
$scopeB = $this->prepareScope(2, 'scope B');
$scopeC = $this->prepareScope(3, 'scope C');
$userA = $this->prepareUser(array(
array(
'center' => $centerA,
'center' => $centerA,
'permissionsGroup' => array(
['scope' => $scopeB, 'role' => 'CHILL_ROLE_1'],
['scope' => $scopeA, 'role' => 'CHILL_ROLE_2']
@ -313,62 +407,62 @@ class AuthorizationHelperTest extends KernelTestCase
['scope' => $scopeC, 'role' => 'CHILL_ROLE_2']
)
)
));
$ah = $this->getAuthorizationHelper();
return array(
// without scopes
array(
true,
in_array($centerA, $ah->getReachableCenters($userA,
true,
in_array($centerA, $ah->getReachableCenters($userA,
new Role('CHILL_ROLE_1'), null)),
'center A should be available for userA, with role 1 '
),
array(
true,
in_array($centerA, $ah->getReachableCenters($userA,
true,
in_array($centerA, $ah->getReachableCenters($userA,
new Role('CHILL_ROLE_2'), null)),
'center A should be available for userA, with role 2 '
),
array(
true,
in_array($centerB, $ah->getReachableCenters($userA,
true,
in_array($centerB, $ah->getReachableCenters($userA,
new Role('CHILL_ROLE_2'), null)),
'center A should be available for userA, with role 2 '
),
array(
false,
false,
in_array($centerB, $ah->getReachableCenters($userA,
new Role('CHILL_ROLE_1'), null)),
'center B should NOT be available for userA, with role 1 '
),
// with scope
array(
true,
in_array($centerA, $ah->getReachableCenters($userA,
true,
in_array($centerA, $ah->getReachableCenters($userA,
new Role('CHILL_ROLE_1'), $scopeB)),
'center A should be available for userA, with role 1, scopeC '
),
array(
false,
in_array($centerA, $ah->getReachableCenters($userA,
false,
in_array($centerA, $ah->getReachableCenters($userA,
new Role('CHILL_ROLE_2'), $scopeC)),
'center A should NOT be available for userA, with role 2, scopeA '
),
array(
true,
in_array($centerB, $ah->getReachableCenters($userA,
true,
in_array($centerB, $ah->getReachableCenters($userA,
new Role('CHILL_ROLE_2'), $scopeA)),
'center B should be available for userA, with role 2, scopeA '
),
);
}
/**
*
*
* @dataProvider dataProvider_getReachableScopes
* @param boolean $expectedResult
* @param Scope $testedScope
@ -382,11 +476,11 @@ class AuthorizationHelperTest extends KernelTestCase
{
$reachableScopes = $this->getAuthorizationHelper()
->getReachableScopes($user, $role, $center);
$this->assertEquals($expectedResult, in_array($testedScope, $reachableScopes),
$message);
}
public function dataProvider_getReachableScopes()
{
$centerA = $this->prepareCenter(1, 'center A');
@ -394,10 +488,10 @@ class AuthorizationHelperTest extends KernelTestCase
$scopeA = $this->prepareScope(1, 'scope default');
$scopeB = $this->prepareScope(2, 'scope B');
$scopeC = $this->prepareScope(3, 'scope C');
$userA = $this->prepareUser(array(
array(
'center' => $centerA,
'center' => $centerA,
'permissionsGroup' => array(
['scope' => $scopeB, 'role' => 'CHILL_ROLE_1'],
['scope' => $scopeA, 'role' => 'CHILL_ROLE_2']
@ -411,9 +505,9 @@ class AuthorizationHelperTest extends KernelTestCase
['scope' => $scopeB, 'role' => 'CHILL_ROLE_2']
)
)
));
return array(
array(
true,
@ -442,37 +536,30 @@ class AuthorizationHelperTest extends KernelTestCase
)
);
}
public function testGetParentRoles()
{
$parentRoles = $this->getAuthorizationHelper()
->getParentRoles(new Role('CHILL_INHERITED_ROLE_1'));
$this->assertContains(
'CHILL_MASTER_ROLE',
\array_map(
function(Role $role) {
return $role->getRole();
},
$parentRoles
),
->getParentRoles('CHILL_INHERITED_ROLE_1');
$this->assertContains('CHILL_MASTER_ROLE', $parentRoles,
"Assert that `CHILL_MASTER_ROLE` is a parent of `CHILL_INHERITED_ROLE_1`");
}
public function testFindUsersReaching()
{
$centerA = static::$kernel->getContainer()
->get('doctrine.orm.entity_manager')
->getRepository(Center::class)
->findOneByName('Center A');
$users = $this->getAuthorizationHelper()
->findUsersReaching(new Role('CHILL_PERSON_SEE'),
->findUsersReaching(new Role('CHILL_PERSON_SEE'),
$centerA);
$usernames = \array_map(function(User $u) { return $u->getUsername(); }, $users);
$this->assertContains('center a_social', $usernames);
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Chill\MainBundle\Tests\Security\Resolver;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
class CenterResolverDispatcherTest extends KernelTestCase
{
private CenterResolverDispatcher $dispatcher;
protected function setUp()
{
self::bootKernel();
$this->dispatcher = self::$container->get(CenterResolverDispatcher::class);
}
public function testResolveCenter()
{
$center = new Center();
$resolved = $this->dispatcher->resolveCenter($center);
$this->assertSame($center, $resolved);
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace Chill\MainBundle\Tests\Security\Resolver;
use Chill\MainBundle\Entity\HasScopeInterface;
use Chill\MainBundle\Entity\HasScopesInterface;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Security\Resolver\DefaultScopeResolver;
use PHPUnit\Framework\TestCase;
class DefaultScopeResolverTest extends TestCase
{
private DefaultScopeResolver $scopeResolver;
public function setUp()
{
$this->scopeResolver = new DefaultScopeResolver();
}
public function testHasScopeInterface()
{
$scope = new Scope();
$entity = new class($scope) implements HasScopeInterface {
public function __construct(Scope $scope) {
$this->scope = $scope;
}
public function getScope()
{
return $this->scope;
}
};
$this->assertTrue($this->scopeResolver->supports($entity));
$this->assertTrue($this->scopeResolver->isConcerned($entity));
$this->assertSame($scope, $this->scopeResolver->resolveScope($entity));
}
public function testHasScopesInterface()
{
$entity = new class($scopeA = new Scope(), $scopeB = new Scope()) implements HasScopesInterface {
public function __construct(Scope $scopeA, Scope $scopeB) {
$this->scopes = [$scopeA, $scopeB];
}
public function getScopes(): iterable
{
return $this->scopes;
}
};
$this->assertTrue($this->scopeResolver->supports($entity));
$this->assertTrue($this->scopeResolver->isConcerned($entity));
$this->assertIsArray($this->scopeResolver->resolveScope($entity));
$this->assertSame($scopeA, $this->scopeResolver->resolveScope($entity)[0]);
$this->assertSame($scopeB, $this->scopeResolver->resolveScope($entity)[1]);
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace Chill\MainBundle\Tests\Security\Resolver;
use Chill\MainBundle\Entity\HasScopeInterface;
use Chill\MainBundle\Entity\HasScopesInterface;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Security\Resolver\DefaultScopeResolver;
use Chill\MainBundle\Security\Resolver\ScopeResolverDispatcher;
use PHPUnit\Framework\TestCase;
class DefaultScopeResolverDispatcherTest extends TestCase
{
private ScopeResolverDispatcher $scopeResolverDispatcher;
public function setUp()
{
$this->scopeResolverDispatcher = new ScopeResolverDispatcher([new DefaultScopeResolver()]);
}
public function testHasScopeInterface()
{
$scope = new Scope();
$entity = new class($scope) implements HasScopeInterface {
public function __construct(Scope $scope) {
$this->scope = $scope;
}
public function getScope()
{
return $this->scope;
}
};
$this->assertTrue($this->scopeResolverDispatcher->isConcerned($entity));
$this->assertSame($scope, $this->scopeResolverDispatcher->resolveScope($entity));
}
public function testHasScopesInterface()
{
$entity = new class($scopeA = new Scope(), $scopeB = new Scope()) implements HasScopesInterface {
public function __construct(Scope $scopeA, Scope $scopeB) {
$this->scopes = [$scopeA, $scopeB];
}
public function getScopes(): iterable
{
return $this->scopes;
}
};
$this->assertTrue($this->scopeResolverDispatcher->isConcerned($entity));
$this->assertIsArray($this->scopeResolverDispatcher->resolveScope($entity));
$this->assertSame($scopeA, $this->scopeResolverDispatcher->resolveScope($entity)[0]);
$this->assertSame($scopeB, $this->scopeResolverDispatcher->resolveScope($entity)[1]);
}
}

View File

@ -480,3 +480,34 @@ paths:
description: "not found"
401:
description: "Unauthorized"
/1.0/main/scope.json:
get:
tags:
- scope
summary: return a list of scopes
responses:
200:
description: "ok"
401:
description: "Unauthorized"
/1.0/main/scope/{id}.json:
get:
tags:
- scope
summary: return a list of scopes
parameters:
- name: id
in: path
required: true
description: The scope id
schema:
type: integer
format: integer
minimum: 1
responses:
200:
description: "ok"
401:
description: "Unauthorized"

View File

@ -113,6 +113,10 @@ services:
tags:
- { name: form.type }
Chill\MainBundle\Form\UserType:
autowire: true
autoconfigure: true
Chill\MainBundle\Form\PermissionsGroupType:
tags:
- { name: form.type }
@ -123,3 +127,4 @@ services:
- "@security.token_storage"
tags:
- { name: form.type }

View File

@ -3,16 +3,45 @@ services:
autowire: true
autoconfigure: true
# do not autowire the directory Security/Resolver
Chill\MainBundle\Security\Resolver\CenterResolverDispatcher:
arguments:
- !tagged_iterator chill_main.center_resolver
Chill\MainBundle\Security\Resolver\ScopeResolverDispatcher:
arguments:
- !tagged_iterator chill_main.scope_resolver
# do not autowire the directory Security/Resolver
Chill\MainBundle\Security\Resolver\DefaultCenterResolver:
autoconfigure: true
autowire: true
Chill\MainBundle\Security\Resolver\DefaultScopeResolver:
autoconfigure: true
autowire: true
# do not autowire the directory Security/Resolver
Chill\MainBundle\Security\Resolver\ResolverTwigExtension:
autoconfigure: true
autowire: true
# do not autowire the directory Security/Resolver
Chill\MainBundle\Security\Authorization\DefaultVoterHelperFactory:
autowire: true
# do not autowire the directory Security/Resolver
Chill\MainBundle\Security\Authorization\VoterHelperFactoryInterface: '@Chill\MainBundle\Security\Authorization\DefaultVoterHelperFactory'
chill.main.security.authorization.helper:
class: Chill\MainBundle\Security\Authorization\AuthorizationHelper
arguments:
$roleHierarchy: "@security.role_hierarchy"
$hierarchy: "%security.role_hierarchy.roles%"
$em: '@Doctrine\ORM\EntityManagerInterface'
autowire: true
autoconfigure: true
Chill\MainBundle\Security\Authorization\AuthorizationHelper: '@chill.main.security.authorization.helper'
chill.main.role_provider:
class: Chill\MainBundle\Security\RoleProvider
Chill\MainBundle\Security\RoleProvider: '@chill.main.role_provider'
chill.main.user_provider:
class: Chill\MainBundle\Security\UserProvider\UserProvider

View File

@ -18,6 +18,8 @@ Chill\MainBundle\Entity\User:
min: 3
email:
- Email: ~
label:
- NotBlank: ~
constraints:
- Callback:
callback: isGroupCenterPresentOnce

View File

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Chill\Migrations\Main;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Add metadata on users
*/
final class Version20210903144853 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add metadata on users';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE SEQUENCE chill_main_user_job_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE chill_main_user_job (id INT NOT NULL, label JSON NOT NULL, active BOOLEAN NOT NULL, PRIMARY KEY(id))');
$this->addSql('ALTER TABLE users ADD label VARCHAR(200) NULL DEFAULT NULL');
$this->addSql('UPDATE users SET label=username');
$this->addSql('ALTER TABLE users ALTER label DROP DEFAULT');
$this->addSql('ALTER TABLE users ALTER label SET NOT NULL');
$this->addSql('ALTER TABLE users ADD mainCenter_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE users ADD mainScope_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE users ADD userJob_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE users ADD CONSTRAINT FK_1483A5E92C2125C1 FOREIGN KEY (mainCenter_id) REFERENCES centers (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE users ADD CONSTRAINT FK_1483A5E9115E73F3 FOREIGN KEY (mainScope_id) REFERENCES scopes (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE users ADD CONSTRAINT FK_1483A5E964B65C5B FOREIGN KEY (userJob_id) REFERENCES chill_main_user_job (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('CREATE INDEX IDX_1483A5E92C2125C1 ON users (mainCenter_id)');
$this->addSql('CREATE INDEX IDX_1483A5E9115E73F3 ON users (mainScope_id)');
$this->addSql('CREATE INDEX IDX_1483A5E964B65C5B ON users (userJob_id)');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE users DROP CONSTRAINT FK_1483A5E964B65C5B');
$this->addSql('DROP SEQUENCE chill_main_user_job_id_seq CASCADE');
$this->addSql('DROP TABLE chill_main_user_job');
$this->addSql('ALTER TABLE users DROP CONSTRAINT FK_1483A5E92C2125C1');
$this->addSql('ALTER TABLE users DROP CONSTRAINT FK_1483A5E9115E73F3');
$this->addSql('ALTER TABLE users DROP label');
$this->addSql('ALTER TABLE users DROP mainCenter_id');
$this->addSql('ALTER TABLE users DROP mainScope_id');
$this->addSql('ALTER TABLE users DROP userJob_id');
$this->addSql('ALTER TABLE users ALTER usernameCanonical DROP NOT NULL');
}
}

View File

@ -186,6 +186,7 @@ Exports list: Liste des exports
Create an export: Créer un export
#export creation step 'center' : pick a center
Pick centers: Choisir les centres
Pick a center: Choisir un centre
The export will contains only data from the picked centers.: L'export ne contiendra que les données des centres choisis.
This will eventually restrict your possibilities in filtering the data.: Les possibilités de filtrages seront adaptées aux droits de consultation pour les centres choisis.
Go to export options: Vers la préparation de l'export

View File

@ -73,7 +73,7 @@ class AccompanyingCourseController extends Controller
}
}
$this->denyAccessUnlessGranted(AccompanyingPeriodVoter::SEE, $period);
$this->denyAccessUnlessGranted(AccompanyingPeriodVoter::CREATE, $period);
$em->persist($period);
$em->flush();
@ -92,6 +92,8 @@ class AccompanyingCourseController extends Controller
*/
public function indexAction(AccompanyingPeriod $accompanyingCourse): Response
{
$this->denyAccessUnlessGranted(AccompanyingPeriodVoter::SEE, $accompanyingCourse);
// compute some warnings
// get persons without household
$withoutHousehold = [];
@ -131,6 +133,8 @@ class AccompanyingCourseController extends Controller
*/
public function editAction(AccompanyingPeriod $accompanyingCourse): Response
{
$this->denyAccessUnlessGranted(AccompanyingPeriodVoter::SEE, $accompanyingCourse);
return $this->render('@ChillPerson/AccompanyingCourse/edit.html.twig', [
'accompanyingCourse' => $accompanyingCourse
]);
@ -146,6 +150,8 @@ class AccompanyingCourseController extends Controller
*/
public function historyAction(AccompanyingPeriod $accompanyingCourse): Response
{
$this->denyAccessUnlessGranted(AccompanyingPeriodVoter::SEE, $accompanyingCourse);
return $this->render('@ChillPerson/AccompanyingCourse/history.html.twig', [
'accompanyingCourse' => $accompanyingCourse
]);

View File

@ -23,7 +23,10 @@
namespace Chill\PersonBundle\Controller;
use Chill\PersonBundle\Privacy\PrivacyEvent;
use Chill\PersonBundle\Repository\AccompanyingPeriodACLAwareRepositoryInterface;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Doctrine\DBAL\Exception;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Form\AccompanyingPeriodType;
@ -53,21 +56,24 @@ class AccompanyingPeriodController extends AbstractController
*/
protected $validator;
/**
* AccompanyingPeriodController constructor.
*
* @param EventDispatcherInterface $eventDispatcher
* @param ValidatorInterface $validator
*/
public function __construct(EventDispatcherInterface $eventDispatcher, ValidatorInterface $validator)
{
protected AccompanyingPeriodACLAwareRepositoryInterface $accompanyingPeriodACLAwareRepository;
public function __construct(
AccompanyingPeriodACLAwareRepositoryInterface $accompanyingPeriodACLAwareRepository,
EventDispatcherInterface $eventDispatcher,
ValidatorInterface $validator
) {
$this->accompanyingPeriodACLAwareRepository = $accompanyingPeriodACLAwareRepository;
$this->eventDispatcher = $eventDispatcher;
$this->validator = $validator;
}
public function listAction(int $person_id): Response
/**
* @ParamConverter("person", options={"id"="person_id"})
*/
public function listAction(Person $person): Response
{
$person = $this->_getPerson($person_id);
$this->denyAccessUnlessGranted(AccompanyingPeriodVoter::SEE, $person);
$event = new PrivacyEvent($person, [
'element_class' => AccompanyingPeriod::class,
@ -75,9 +81,10 @@ class AccompanyingPeriodController extends AbstractController
]);
$this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event);
$accompanyingPeriods = $person->getAccompanyingPeriodsOrdered();
$accompanyingPeriods = $this->accompanyingPeriodACLAwareRepository
->findByPerson($person, AccompanyingPeriodVoter::SEE);
return $this->render('ChillPersonBundle:AccompanyingPeriod:list.html.twig', [
return $this->render('@ChillPerson/AccompanyingPeriod/list.html.twig', [
'accompanying_periods' => $accompanyingPeriods,
'person' => $person
]);

View File

@ -230,13 +230,16 @@ final class PersonController extends AbstractController
*/
public function newAction(Request $request)
{
$defaultCenter = $this->security
->getUser()
->getGroupCenters()[0]
->getCenter();
$person = new Person();
$person = (new Person(new \DateTime('now')))
->setCenter($defaultCenter);
if (1 === count($this->security->getUser()
->getGroupCenters())) {
$person->setCenter(
$this->security->getUser()
->getGroupCenters()[0]
->getCenter()
);
}
$form = $this->createForm(CreationPersonType::class, $person, [
'validation_groups' => ['create']

View File

@ -24,8 +24,12 @@ namespace Chill\PersonBundle\DataFixtures\ORM;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Country;
use Chill\MainBundle\Entity\PostalCode;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Repository\CenterRepository;
use Chill\MainBundle\Repository\CountryRepository;
use Chill\MainBundle\Repository\ScopeRepository;
use Chill\MainBundle\Repository\UserRepository;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\MaritalStatus;
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
@ -90,12 +94,26 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con
protected MaritalStatusRepository $maritalStatusRepository;
/**
* @var array|Scope[]
*/
protected array $cacheScopes = [];
protected ScopeRepository $scopeRepository;
/** @var array|User[] */
protected array $cacheUsers = [];
protected UserRepository $userRepository;
public function __construct(
Registry $workflowRegistry,
SocialIssueRepository $socialIssueRepository,
CenterRepository $centerRepository,
CountryRepository $countryRepository,
MaritalStatusRepository $maritalStatusRepository
MaritalStatusRepository $maritalStatusRepository,
ScopeRepository $scopeRepository,
UserRepository $userRepository
) {
$this->faker = Factory::create('fr_FR');
$this->faker->addProvider($this);
@ -105,7 +123,8 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con
$this->countryRepository = $countryRepository;
$this->maritalStatusRepository = $maritalStatusRepository;
$this->loader = new NativeLoader($this->faker);
$this->scopeRepository = $scopeRepository;
$this->userRepository = $userRepository;
}
public function getOrder()
@ -220,10 +239,16 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con
new \DateInterval('P' . \random_int(0, 180) . 'D')
)
);
$accompanyingPeriod->setCreatedBy($this->getRandomUser())
->setCreatedAt(new \DateTimeImmutable('now'));
$person->addAccompanyingPeriod($accompanyingPeriod);
$accompanyingPeriod->addSocialIssue($this->getRandomSocialIssue());
if (\random_int(0, 10) > 3) {
// always add social scope:
$accompanyingPeriod->addScope($this->getReference('scope_social'));
var_dump(count($accompanyingPeriod->getScopes()));
$accompanyingPeriod->setAddressLocation($this->createAddress());
$manager->persist($accompanyingPeriod->getAddressLocation());
$workflow = $this->workflowRegistry->get($accompanyingPeriod);
@ -231,9 +256,19 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con
}
$manager->persist($person);
$manager->persist($accompanyingPeriod);
echo "add person'".$person->__toString()."'\n";
}
private function getRandomUser(): User
{
if (0 === count($this->cacheUsers)) {
$this->cacheUsers = $this->userRepository->findAll();
}
return $this->cacheUsers[\array_rand($this->cacheUsers)];
}
private function createAddress(): Address
{
$objectSet = $this->loader->loadData([

View File

@ -40,13 +40,13 @@ class LoadPersonACL extends AbstractFixture implements OrderedFixtureInterface
return 9600;
}
public function load(ObjectManager $manager)
{
foreach (LoadPermissionsGroup::$refs as $permissionsGroupRef) {
$permissionsGroup = $this->getReference($permissionsGroupRef);
$scopeSocial = $this->getReference('scope_social');
//create permission group
switch ($permissionsGroup->getName()) {
case 'social':
@ -55,7 +55,7 @@ class LoadPersonACL extends AbstractFixture implements OrderedFixtureInterface
$permissionsGroup->addRoleScope(
(new RoleScope())
->setRole(AccompanyingPeriodVoter::SEE)
->setRole(AccompanyingPeriodVoter::FULL)
->setScope($scopeSocial)
);
@ -87,7 +87,7 @@ class LoadPersonACL extends AbstractFixture implements OrderedFixtureInterface
$manager->persist($roleScopeUpdate);
$manager->persist($roleScopeCreate);
$manager->persist($roleScopeDuplicate);
break;
case 'administrative':
printf("Adding CHILL_PERSON_SEE to %s permission group \n", $permissionsGroup->getName());
@ -98,9 +98,9 @@ class LoadPersonACL extends AbstractFixture implements OrderedFixtureInterface
$manager->persist($roleScopeSee);
break;
}
}
$manager->flush();
}

View File

@ -18,6 +18,7 @@
namespace Chill\PersonBundle\DependencyInjection;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
@ -60,6 +61,9 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
$container->setParameter('chill_person.allow_multiple_simultaneous_accompanying_periods',
$config['allow_multiple_simultaneous_accompanying_periods']);
// register all configuration in a unique parameter
$container->setParameter('chill_person', $config);
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
$loader->load('services.yaml');
$loader->load('services/widgets.yaml');
@ -255,14 +259,26 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
*/
protected function prependRoleHierarchy(ContainerBuilder $container)
{
$container->prependExtensionConfig('security', array(
'role_hierarchy' => array(
'CHILL_PERSON_UPDATE' => array('CHILL_PERSON_SEE'),
'CHILL_PERSON_CREATE' => array('CHILL_PERSON_SEE'),
PersonVoter::LISTS => [ ChillExportVoter::EXPORT ],
PersonVoter::STATS => [ ChillExportVoter::EXPORT ]
)
));
$container->prependExtensionConfig('security', [
'role_hierarchy' => [
PersonVoter::UPDATE => [PersonVoter::SEE],
PersonVoter::CREATE => [PersonVoter::SEE],
PersonVoter::LISTS => [ChillExportVoter::EXPORT],
PersonVoter::STATS => [ChillExportVoter::EXPORT],
// accompanying period
AccompanyingPeriodVoter::SEE_DETAILS => [AccompanyingPeriodVoter::SEE],
AccompanyingPeriodVoter::CREATE => [AccompanyingPeriodVoter::SEE_DETAILS],
AccompanyingPeriodVoter::DELETE => [AccompanyingPeriodVoter::SEE_DETAILS],
AccompanyingPeriodVoter::EDIT => [AccompanyingPeriodVoter::SEE_DETAILS],
// give all ACL for FULL
AccompanyingPeriodVoter::FULL => [
AccompanyingPeriodVoter::SEE_DETAILS,
AccompanyingPeriodVoter::CREATE,
AccompanyingPeriodVoter::EDIT,
AccompanyingPeriodVoter::DELETE
]
]
]);
}
/**

View File

@ -43,23 +43,26 @@ class Configuration implements ConfigurationInterface
->arrayNode('validation')
->canBeDisabled()
->children()
->booleanNode('center_required')
->info('Enable a center for each person entity. If disabled, you must provide your own center provider')
->defaultValue(true)
->end()
->scalarNode('birthdate_not_after')
->info($this->validationBirthdateNotAfterInfos)
->defaultValue('P1D')
->validate()
->ifTrue(function($period) {
try {
$interval = new \DateInterval($period);
} catch (\Exception $ex) {
return true;
}
return false;
})
->thenInvalid('Invalid period for birthdate validation : "%s" '
. 'The parameter should match duration as defined by ISO8601 : '
. 'https://en.wikipedia.org/wiki/ISO_8601#Durations')
->info($this->validationBirthdateNotAfterInfos)
->defaultValue('P1D')
->validate()
->ifTrue(function($period) {
try {
$interval = new \DateInterval($period);
} catch (\Exception $ex) {
return true;
}
return false;
})
->thenInvalid('Invalid period for birthdate validation : "%s" '
. 'The parameter should match duration as defined by ISO8601 : '
. 'https://en.wikipedia.org/wiki/ISO_8601#Durations')
->end() // birthdate_not_after, parent = children of validation
->end() // children for 'validation', parent = validation
->end() //validation, parent = children of root
->end() // children of root, parent = root

View File

@ -24,6 +24,8 @@ namespace Chill\PersonBundle\Entity;
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Entity\HasCentersInterface;
use Chill\MainBundle\Entity\HasScopesInterface;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\Address;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
@ -52,7 +54,8 @@ use Symfony\Component\Validator\Constraints as Assert;
* "accompanying_period"=AccompanyingPeriod::class
* })
*/
class AccompanyingPeriod implements TrackCreationInterface, TrackUpdateInterface
class AccompanyingPeriod implements TrackCreationInterface, TrackUpdateInterface,
HasScopesInterface, HasCentersInterface
{
/**
* Mark an accompanying period as "occasional"
@ -809,14 +812,21 @@ class AccompanyingPeriod implements TrackCreationInterface, TrackUpdateInterface
return $this;
}
/**
* @return iterable|Collection
*/
public function getScopes(): Collection
{
return $this->scopes;
}
public function addScope(Scope $scope): self
{
$this->scopes[] = $scope;
if (!$this->scopes->contains($scope)) {
$this->scopes[] = $scope;
}
return $this;
}
@ -1040,4 +1050,16 @@ class AccompanyingPeriod implements TrackCreationInterface, TrackUpdateInterface
return 'none';
}
}
public function getCenters(): ?iterable
{
foreach ($this->getPersons() as $person) {
if (!in_array($person->getCenter(), $centers ?? [])
&& NULL !== $person->getCenter()) {
$centers[] = $person->getCenter();
}
}
return $centers ?? null;
}
}

View File

@ -8,6 +8,29 @@ use Chill\PersonBundle\Entity\Person;
use Doctrine\ORM\Mapping as ORM;
/**
* This class links a person to the history of his addresses, through
* household membership.
*
* It is optimized on DB side, and compute the start date and end date
* of each address by the belonging of household.
*
* **note**: the start date and end date are the date of belonging to the address,
* not the belonging of the household.
*
* Example:
*
* * person A is member of household W from 2021-01-01 to 2021-12-01
* * person A is member of household V from 2021-12-01, still present after
* * household W lives in address Q from 2020-06-01 to 2021-06-01
* * household W lives in address R from 2021-06-01 to 2022-06-01
* * household V lives in address T from 2021-12-01 to still living there after
*
* The person A will have those 3 entities:
*
* 1. 1st entity: from 2021-01-01 to 2021-06-01, household W, address Q;
* 2. 2st entity: from 2021-06-01 to 2021-12-01, household W, address R;
* 3. 3st entity: from 2021-12-01 to NULL, household V, address T;
*
* @ORM\Entity(readOnly=true)
* @ORM\Table(name="view_chill_person_household_address")
*/
@ -45,11 +68,23 @@ class PersonHouseholdAddress
*/
private $address;
/**
* The start date of the intersection address/household
*
* (this is not the startdate of the household, not
* the startdate of the address)
*/
public function getValidFrom(): ?\DateTimeInterface
{
return $this->validFrom;
}
/**
* The end date of the intersection address/household
*
* (this is not the enddate of the household, not
* the enddate of the address)
*/
public function getValidTo(): ?\DateTimeImmutable
{
return $this->validTo;

View File

@ -35,6 +35,7 @@ use Chill\PersonBundle\Entity\Household\HouseholdMember;
use Chill\MainBundle\Entity\HasCenterInterface;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
use Chill\PersonBundle\Entity\Person\PersonCurrentAddress;
use DateTime;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\Collection;
@ -43,6 +44,11 @@ use Doctrine\Common\Collections\Criteria;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
use Chill\PersonBundle\Entity\Household\PersonHouseholdAddress;
use Symfony\Component\Validator\Constraints as Assert;
use Chill\PersonBundle\Validator\Constraints\Person\Birthdate;
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
use Chill\PersonBundle\Validator\Constraints\Person\PersonHasCenter;
use Chill\PersonBundle\Validator\Constraints\Household\HouseholdMembershipSequential;
/**
* Person Class
@ -57,6 +63,12 @@ use Chill\PersonBundle\Entity\Household\PersonHouseholdAddress;
* @DiscriminatorMap(typeProperty="type", mapping={
* "person"=Person::class
* })
* @PersonHasCenter(
* groups={"general", "creation"}
* )
* @HouseholdMembershipSequential(
* groups={"household_memberships"}
* )
*/
class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateInterface
{
@ -75,6 +87,13 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* @var string
*
* @ORM\Column(type="string", length=255)
* @Assert\NotBlank(
* groups={"general", "creation"}
* )
* @Assert\Length(
* max=255,
* groups={"general", "creation"}
* )
*/
private $firstName;
@ -83,6 +102,13 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* @var string
*
* @ORM\Column(type="string", length=255)
* @Assert\NotBlank(
* groups={"general", "creation"}
* )
* @Assert\Length(
* max=255,
* groups={"general", "creation"}
* )
*/
private $lastName;
@ -102,6 +128,12 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* @var \DateTime
*
* @ORM\Column(type="date", nullable=true)
* @Assert\Date(
* groups={"general", "creation"}
* )
* @Birthdate(
* groups={"general", "creation"}
* )
*/
private $birthdate;
@ -110,6 +142,9 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* @var \DateTimeImmutable
*
* @ORM\Column(type="date_immutable", nullable=true)
* @Assert\Date(
* groups={"general", "creation"}
* )
*/
private ?\DateTimeImmutable $deathdate;
@ -150,6 +185,9 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* @var string
*
* @ORM\Column(type="string", length=9, nullable=true)
* @Assert\NotNull(
* groups={"general", "creation"}
* )
*/
private $gender;
@ -179,8 +217,11 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* @var \DateTime
*
* @ORM\Column(type="date", nullable=true)
* @Assert\Date(
* groups={"general", "creation"}
* )
*/
private $maritalStatusDate;
private ?\DateTime $maritalStatusDate;
/**
* Comment on marital status
@ -202,6 +243,10 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* @var string
*
* @ORM\Column(type="text", nullable=true)
* @Assert\Email(
* checkMX=true,
* groups={"general", "creation"}
* )
*/
private $email = '';
@ -210,6 +255,14 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* @var string
*
* @ORM\Column(type="text", length=40, nullable=true)
* @Assert\Regex(
* pattern="/^([\+{1}])([0-9\s*]{4,20})$/",
* groups={"general", "creation"}
* )
* @PhonenumberConstraint(
* type="landline",
* groups={"general", "creation"}
* )
*/
private $phonenumber = '';
@ -218,6 +271,14 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* @var string
*
* @ORM\Column(type="text", length=40, nullable=true)
* @Assert\Regex(
* pattern="/^([\+{1}])([0-9\s*]{4,20})$/",
* groups={"general", "creation"}
* )
* @PhonenumberConstraint(
* type="mobile",
* groups={"general", "creation"}
* )
*/
private $mobilenumber = '';
@ -230,12 +291,13 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* cascade={"persist", "remove", "merge", "detach"},
* orphanRemoval=true
* )
* @Assert\Valid(
* traverse=true,
* groups={"general", "creation"}
* )
*/
private $otherPhoneNumbers;
//TO-ADD caseOpeningDate
//TO-ADD nativeLanguag
/**
* The person's spoken languages
* @var ArrayCollection
@ -254,7 +316,6 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* @var Center
*
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\Center")
* @ORM\JoinColumn(nullable=false)
*/
private $center;
@ -353,6 +414,18 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
private $addresses;
/**
* The current person address.
*
* This is computed through database and is optimized on database side.
*
* @var PersonCurrentAddress|null
* @ORM\OneToOne(targetEntity=PersonCurrentAddress::class, mappedBy="person")
*/
private ?PersonCurrentAddress $currentPersonAddress = null;
/**
* fullname canonical. Read-only field, which is calculated by
* the database.
* @var string
*
* @ORM\Column(type="text", nullable=true)
@ -373,6 +446,8 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
private array $currentHouseholdAt = [];
/**
* Read-only field, computed by the database
*
* @ORM\OneToMany(
* targetEntity=PersonHouseholdAddress::class,
* mappedBy="person"
@ -390,8 +465,6 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
/**
* Person constructor.
*
* @param \DateTime|null $opening
*/
public function __construct()
{
@ -404,6 +477,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
$this->householdAddresses = new ArrayCollection();
$this->genderComment = new CommentEmbeddable();
$this->maritalStatusComment = new CommentEmbeddable();
$this->periodLocatedOn = new ArrayCollection();
}
/**
@ -501,6 +575,8 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
return $participation->getAccompanyingPeriod();
}
}
return null;
}
/**
@ -1179,13 +1255,31 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
return $this->addresses;
}
/**
* @deprecated Use `getCurrentPersonAddress` instead
* @param DateTime|null $from
* @return false|mixed|null
* @throws \Exception
*/
public function getLastAddress(DateTime $from = null)
{
$from ??= new DateTime('now');
return $this->getCurrentPersonAddress($from);
}
/**
* get the address associated with the person at the given date
*
* @param DateTime|null $at
* @return Address|null
* @throws \Exception
*/
public function getCurrentPersonAddress(?\DateTime $at = null): ?Address
{
$at ??= new DateTime('now');
/** @var ArrayIterator $addressesIterator */
$addressesIterator = $this->getAddresses()
->filter(static fn (Address $address): bool => $address->getValidFrom() <= $from)
->filter(static fn (Address $address): bool => $address->getValidFrom() <= $at)
->getIterator();
$addressesIterator->uasort(
@ -1201,6 +1295,10 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* Validation callback that checks if the accompanying periods are valid
*
* This method add violation errors.
*
* @Assert\Callback(
* groups={"accompanying_period_consistent"}
* )
*/
public function isAccompanyingPeriodValid(ExecutionContextInterface $context)
{
@ -1246,6 +1344,10 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* two addresses with the same validFrom date)
*
* This method add violation errors.
*
* @Assert\Callback(
* groups={"addresses_consistent"}
* )
*/
public function isAddressesValid(ExecutionContextInterface $context)
{
@ -1425,7 +1527,16 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
public function getCurrentHouseholdAddress(?\DateTimeImmutable $at = null): ?Address
{
$at = $at === null ? new \DateTimeImmutable('today') : $at;
if (
NULL === $at
||
$at->format('Ymd') === (new \DateTime('today'))->format('Ymd')
) {
return $this->currentPersonAddress instanceof PersonCurrentAddress
? $this->currentPersonAddress->getAddress() : NULL;
}
// if not now, compute the date from history
$criteria = new Criteria();
$expr = Criteria::expr();

View File

@ -0,0 +1,82 @@
<?php
namespace Chill\PersonBundle\Entity\Person;
use Chill\MainBundle\Entity\Address;
use Chill\PersonBundle\Entity\Person;
use Doctrine\ORM\Mapping as ORM;
/**
* Entity which associate person with his current address, through
* household participation.
*
* The computation is optimized on database side.
*
* The validFrom and validTo properties are the intersection of
* household membership and address validity. See @link{PersonHouseholdAddress}
*
* @ORM\Entity(readOnly=true)
* @ORM\Table("view_chill_person_current_address")
*/
class PersonCurrentAddress
{
/**
* @ORM\Id
* @ORM\OneToOne(targetEntity=Person::class, inversedBy="currentPersonAddress")
* @ORM\JoinColumn(name="person_id", referencedColumnName="id")
*/
protected Person $person;
/**
* @ORM\OneToOne(targetEntity=Address::class)
*/
protected Address $address;
/**
* @ORM\Column(name="valid_from", type="date_immutable")
*/
protected \DateTimeImmutable $validFrom;
/**
* @ORM\Column(name="valid_to", type="date_immutable")
*/
protected ?\DateTimeImmutable $validTo;
/**
* @return Person
*/
public function getPerson(): Person
{
return $this->person;
}
/**
* @return Address
*/
public function getAddress(): Address
{
return $this->address;
}
/**
* This date is the intersection of household membership
* and address validity
*
* @return \DateTimeImmutable
*/
public function getValidFrom(): \DateTimeImmutable
{
return $this->validFrom;
}
/**
* This date is the intersection of household membership
* and address validity
*
* @return \DateTimeImmutable|null
*/
public function getValidTo(): ?\DateTimeImmutable
{
return $this->validTo;
}
}

View File

@ -21,7 +21,9 @@
namespace Chill\PersonBundle\Form;
use Chill\MainBundle\Form\Event\CustomizeFormEvent;
use Chill\PersonBundle\Entity\Person;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
@ -33,6 +35,7 @@ use Chill\PersonBundle\Form\Type\GenderType;
use Chill\MainBundle\Form\Type\DataTransformer\CenterTransformer;
use Chill\PersonBundle\Config\ConfigPersonAltNamesHelper;
use Chill\PersonBundle\Form\Type\PersonAltNameType;
use Chill\MainBundle\Form\Type\Export\PickCenterType;
final class CreationPersonType extends AbstractType
{
@ -40,10 +43,6 @@ final class CreationPersonType extends AbstractType
// TODO: See if this is still valid and update accordingly.
const NAME = 'chill_personbundle_person_creation';
const FORM_NOT_REVIEWED = 'not_reviewed';
const FORM_REVIEWED = 'reviewed' ;
const FORM_BEING_REVIEWED = 'being_reviewed';
/**
*
* @var CenterTransformer
@ -56,12 +55,16 @@ final class CreationPersonType extends AbstractType
*/
protected $configPersonAltNamesHelper;
private EventDispatcherInterface $dispatcher;
public function __construct(
CenterTransformer $centerTransformer,
ConfigPersonAltNamesHelper $configPersonAltNamesHelper
ConfigPersonAltNamesHelper $configPersonAltNamesHelper,
EventDispatcherInterface $dispatcher
) {
$this->centerTransformer = $centerTransformer;
$this->configPersonAltNamesHelper = $configPersonAltNamesHelper;
$this->dispatcher = $dispatcher;
}
/**
@ -79,7 +82,9 @@ final class CreationPersonType extends AbstractType
->add('gender', GenderType::class, array(
'required' => true, 'placeholder' => null
))
->add('center', CenterType::class)
->add('center', CenterType::class, [
'required' => false
])
;
if ($this->configPersonAltNamesHelper->hasAltNames()) {
@ -87,6 +92,11 @@ final class CreationPersonType extends AbstractType
'by_reference' => false
]);
}
$this->dispatcher->dispatch(
new CustomizeFormEvent(static::class, $builder),
CustomizeFormEvent::NAME
);
}
/**

View File

@ -20,6 +20,9 @@
namespace Chill\PersonBundle\Form\Type;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\PersonBundle\Entity\MaritalStatus;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Chill\MainBundle\Form\Type\DataTransformer\ObjectToIdTransformer;
@ -35,15 +38,13 @@ use Chill\MainBundle\Form\Type\Select2ChoiceType;
*/
class Select2MaritalStatusType extends AbstractType
{
/** @var RequestStack */
private $requestStack;
private EntityManagerInterface $em;
/** @var ObjectManager */
private $em;
private TranslatableStringHelper $translatableStringHelper;
public function __construct(RequestStack $requestStack,ObjectManager $em)
public function __construct(TranslatableStringHelper $translatableStringHelper, EntityManagerInterface $em)
{
$this->requestStack = $requestStack;
$this->translatableStringHelper = $translatableStringHelper;
$this->em = $em;
}
@ -63,18 +64,17 @@ class Select2MaritalStatusType extends AbstractType
public function configureOptions(OptionsResolver $resolver)
{
$locale = $this->requestStack->getCurrentRequest()->getLocale();
$maritalStatuses = $this->em->getRepository('Chill\PersonBundle\Entity\MaritalStatus')->findAll();
$choices = array();
foreach ($maritalStatuses as $ms) {
$choices[$ms->getId()] = $ms->getName()[$locale];
$choices[$ms->getId()] = $this->translatableStringHelper->localize($ms->getName());
}
asort($choices, SORT_STRING | SORT_FLAG_CASE);
$resolver->setDefaults(array(
'class' => 'Chill\PersonBundle\Entity\MaritalStatus',
'class' => MaritalStatus::class,
'choices' => array_combine(array_values($choices),array_keys($choices))
));
}

View File

@ -18,14 +18,17 @@
namespace Chill\PersonBundle\Menu;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Knp\Menu\MenuItem;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Add menu entrie to person menu.
*
*
* Menu entries added :
*
*
* - person details ;
* - accompanying period (if `visible`)
*
@ -37,21 +40,25 @@ class PersonMenuBuilder implements LocalMenuBuilderInterface
* @var string 'visible' or 'hidden'
*/
protected $showAccompanyingPeriod;
/**
*
* @var TranslatorInterface
*/
protected $translator;
private Security $security;
public function __construct(
$showAccompanyingPeriod,
ParameterBagInterface $parameterBag,
Security $security,
TranslatorInterface $translator
) {
$this->showAccompanyingPeriod = $showAccompanyingPeriod;
$this->showAccompanyingPeriod = $parameterBag->get('chill_person.accompanying_period');
$this->security = $security;
$this->translator = $translator;
}
public function buildMenu($menuId, MenuItem $menu, array $parameters)
{
$menu->addChild($this->translator->trans('Person details'), [
@ -83,8 +90,10 @@ class PersonMenuBuilder implements LocalMenuBuilderInterface
->setExtras([
'order' => 99999
]);
if ($this->showAccompanyingPeriod === 'visible') {
if ($this->showAccompanyingPeriod === 'visible'
&& $this->security->isGranted(AccompanyingPeriodVoter::SEE, $parameters['person'])
) {
$menu->addChild($this->translator->trans('Accompanying period list'), [
'route' => 'chill_person_accompanying_period_list',
'routeParameters' => [

View File

@ -0,0 +1,76 @@
<?php
namespace Chill\PersonBundle\Repository;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Symfony\Component\Security\Core\Security;
final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodACLAwareRepositoryInterface
{
private AccompanyingPeriodRepository $accompanyingPeriodRepository;
private Security $security;
private AuthorizationHelper $authorizationHelper;
private CenterResolverDispatcher $centerResolverDispatcher;
public function __construct(AccompanyingPeriodRepository $accompanyingPeriodRepository, Security $security, AuthorizationHelper $authorizationHelper, CenterResolverDispatcher $centerResolverDispatcher)
{
$this->accompanyingPeriodRepository = $accompanyingPeriodRepository;
$this->security = $security;
$this->authorizationHelper = $authorizationHelper;
$this->centerResolverDispatcher = $centerResolverDispatcher;
}
public function findByPerson(
Person $person,
string $role,
?array $orderBy = [],
int $limit = null,
int $offset = null
): array {
$qb = $this->accompanyingPeriodRepository->createQueryBuilder('ap');
$scopes = $this->authorizationHelper
->getReachableCircles($this->security->getUser(), $role,
$this->centerResolverDispatcher->resolveCenter($person));
if (0 === count($scopes)) {
return [];
}
$qb
->join('ap.participations', 'participation')
->where($qb->expr()->eq('participation.person', ':person'))
->andWhere(
$qb->expr()->orX(
'ap.confidential = FALSE',
$qb->expr()->eq('ap.user', ':user')
)
)
->andWhere(
$qb->expr()->orX(
$qb->expr()->neq('ap.step', ':draft'),
$qb->expr()->eq('ap.createdBy', ':creator')
)
)
->setParameter('draft', AccompanyingPeriod::STEP_DRAFT)
->setParameter('person', $person)
->setParameter('user', $this->security->getUser())
->setParameter('creator', $this->security->getUser())
;
// add join condition for scopes
$orx = $qb->expr()->orX(
$qb->expr()->eq('ap.step', ':draft')
);
foreach ($scopes as $key => $scope) {
$orx->add($qb->expr()->isMemberOf(':scope_'.$key, 'ap.scopes'));
$qb->setParameter('scope_'.$key, $scope);
}
$qb->andWhere($orx);
return $qb->getQuery()->getResult();
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace Chill\PersonBundle\Repository;
use Chill\PersonBundle\Entity\Person;
interface AccompanyingPeriodACLAwareRepositoryInterface
{
public function findByPerson(
Person $person,
string $role,
?array $orderBy = [],
int $limit = null,
int $offset = null
): array;
}

View File

@ -59,6 +59,11 @@ final class AccompanyingPeriodRepository implements ObjectRepository
return $this->findOneBy($criteria);
}
public function createQueryBuilder(string $alias, ?string $indexBy = null): QueryBuilder
{
return $this->repository->createQueryBuilder($alias, $indexBy);
}
public function getClassName()
{
return AccompanyingPeriod::class;

View File

@ -0,0 +1,292 @@
<?php
namespace Chill\PersonBundle\Repository;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Repository\CountryRepository;
use Chill\MainBundle\Search\ParsingException;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\PersonBundle\Entity\Person;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\NonUniqueResultException;
use Doctrine\ORM\NoResultException;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Security\Core\Security;
final class PersonACLAwareRepository implements PersonACLAwareRepositoryInterface
{
private Security $security;
private EntityManagerInterface $em;
private CountryRepository $countryRepository;
private AuthorizationHelper $authorizationHelper;
public function __construct(
Security $security,
EntityManagerInterface $em,
CountryRepository $countryRepository,
AuthorizationHelper $authorizationHelper
) {
$this->security = $security;
$this->em = $em;
$this->countryRepository = $countryRepository;
$this->authorizationHelper = $authorizationHelper;
}
/**
* @return array|Person[]
* @throws NonUniqueResultException
* @throws ParsingException
*/
public function findBySearchCriteria(
int $start,
int $limit,
bool $simplify = false,
string $default = null,
string $firstname = null,
string $lastname = null,
?\DateTime $birthdate = null,
?\DateTime $birthdateBefore = null,
?\DateTime $birthdateAfter = null,
string $gender = null,
string $countryCode = null
): array {
$qb = $this->createSearchQuery($default, $firstname, $lastname,
$birthdate, $birthdateBefore, $birthdateAfter, $gender,
$countryCode);
$this->addACLClauses($qb, 'p');
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();
}
}
public function countBySearchCriteria(
string $default = null,
string $firstname = null,
string $lastname = null,
?\DateTime $birthdate = null,
?\DateTime $birthdateBefore = null,
?\DateTime $birthdateAfter = null,
string $gender = null,
string $countryCode = null
): int {
$qb = $this->createSearchQuery($default, $firstname, $lastname,
$birthdate, $birthdateBefore, $birthdateAfter, $gender,
$countryCode);
$this->addACLClauses($qb, 'p');
return $this->getCountQueryResult($qb,'p');
}
/**
* 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.
*/
public function getCountQueryResult(QueryBuilder $qb, $alias): int
{
$qb->select('COUNT('.$alias.'.id)');
return $qb->getQuery()->getSingleScalarResult();
}
public function findBySimilaritySearch(string $pattern, int $firstResult,
int $maxResult, bool $simplify = false)
{
$qb = $this->createSimilarityQuery($pattern);
$this->addACLClauses($qb, 'sp');
return $this->getQueryResult($qb, 'sp', $simplify, $maxResult, $firstResult);
}
public function countBySimilaritySearch(string $pattern)
{
$qb = $this->createSimilarityQuery($pattern);
$this->addACLClauses($qb, 'sp');
return $this->getCountQueryResult($qb, 'sp');
}
/**
* 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
*/
public function createSearchQuery(
string $default = null,
string $firstname = null,
string $lastname = null,
?\DateTime $birthdate = null,
?\DateTime $birthdateBefore = null,
?\DateTime $birthdateAfter = null,
string $gender = null,
string $countryCode = null
): QueryBuilder {
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');
if (NULL !== $firstname) {
$qb->andWhere($qb->expr()->like('UNACCENT(LOWER(p.firstName))', ':firstname'))
->setParameter('firstname', '%'.$firstname.'%');
}
if (NULL !== $lastname) {
$qb->andWhere($qb->expr()->like('UNACCENT(LOWER(p.lastName))', ':lastname'))
->setParameter('lastname', '%'.$lastname.'%');
}
if (NULL !== $birthdate) {
$qb->andWhere($qb->expr()->eq('s.birthdate', ':birthdate'))
->setParameter('birthdate', $birthdate);
}
if (NULL !== $birthdateAfter) {
$qb->andWhere($qb->expr()->gt('p.birthdate', ':birthdateafter'))
->setParameter('birthdateafter', $birthdateAfter);
}
if (NULL !== $birthdateBefore) {
$qb->andWhere($qb->expr()->lt('p.birthdate', ':birthdatebefore'))
->setParameter('birthdatebefore', $birthdateBefore);
}
if (NULL !== $gender) {
$qb->andWhere($qb->expr()->eq('p.gender', ':gender'))
->setParameter('gender', $gender);
}
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 !== $default) {
$grams = explode(' ', $default);
foreach($grams as $key => $gram) {
$qb->andWhere($qb->expr()
->like('p.fullnameCanonical', 'UNACCENT(LOWER(:default_'.$key.'))'))
->setParameter('default_'.$key, '%'.$gram.'%');
}
}
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.'%');
}
return $qb;
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace Chill\PersonBundle\Repository;
use Chill\MainBundle\Search\ParsingException;
use Chill\PersonBundle\Entity\Person;
use Doctrine\ORM\NonUniqueResultException;
interface PersonACLAwareRepositoryInterface
{
/**
* @return array|Person[]
* @throws NonUniqueResultException
* @throws ParsingException
*/
public function findBySearchCriteria(
int $start,
int $limit,
bool $simplify = false,
string $default = null,
string $firstname = null,
string $lastname = null,
?\DateTime $birthdate = null,
?\DateTime $birthdateBefore = null,
?\DateTime $birthdateAfter = null,
string $gender = null,
string $countryCode = null
): array;
public function countBySearchCriteria(
string $default = null,
string $firstname = null,
string $lastname = null,
?\DateTime $birthdate = null,
?\DateTime $birthdateBefore = null,
?\DateTime $birthdateAfter = null,
string $gender = null,
string $countryCode = null
);
public function findBySimilaritySearch(
string $pattern,
int $firstResult,
int $maxResult,
bool $simplify = false
);
public function countBySimilaritySearch(string $pattern);
}

View File

@ -23,9 +23,11 @@ use Chill\PersonBundle\Entity\Person;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectRepository;
use UnexpectedValueException;
final class PersonRepository
final class PersonRepository implements ObjectRepository
{
private EntityRepository $repository;
@ -44,6 +46,26 @@ final class PersonRepository
return $this->repository->findBy(['id' => $ids]);
}
public function findAll()
{
return $this->repository->findAll();
}
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null)
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
public function findOneBy(array $criteria)
{
return $this->repository->findOneBy($criteria);
}
public function getClassName()
{
return Person::class;
}
/**
* @param $centers
* @param $firstResult

View File

@ -10,6 +10,7 @@
<origin-demand></origin-demand>
<requestor></requestor>
<social-issue></social-issue>
<scopes></scopes>
<referrer></referrer>
<resources></resources>
<comment v-if="accompanyingCourse.step === 'DRAFT'"></comment>
@ -32,6 +33,7 @@ import PersonsAssociated from './components/PersonsAssociated.vue';
import Requestor from './components/Requestor.vue';
import SocialIssue from './components/SocialIssue.vue';
import CourseLocation from './components/CourseLocation.vue';
import Scopes from './components/Scopes.vue';
import Referrer from './components/Referrer.vue';
import Resources from './components/Resources.vue';
import Comment from './components/Comment.vue';
@ -47,6 +49,7 @@ export default {
Requestor,
SocialIssue,
CourseLocation,
Scopes,
Referrer,
Resources,
Comment,

View File

@ -191,7 +191,49 @@ const getListOrigins = () => {
if (response.ok) { return response.json(); }
throw { msg: 'Error while retriving origin\'s list.', sta: response.status, txt: response.statusText, err: new Error(), body: response.body };
});
}
};
const addScope = (id, scope) => {
const url = `/api/1.0/person/accompanying-course/${id}/scope.json`;
console.log(url);
console.log(scope);
return fetch(url, {
method: 'POST',
body: JSON.stringify({
id: scope.id,
type: scope.type,
}),
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
})
.then(response => {
if (response.ok) { return response.json(); }
throw { msg: 'Error while adding scope', sta: response.status, txt: response.statusText, err: new Error(), body: response.body };
});
};
const removeScope = (id, scope) => {
const url = `/api/1.0/person/accompanying-course/${id}/scope.json`;
console.log(url);
console.log(scope);
return fetch(url, {
method: 'DELETE',
body: JSON.stringify({
id: scope.id,
type: scope.type,
}),
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
})
.then(response => {
if (response.ok) { return response.json(); }
throw { msg: 'Error while adding scope', sta: response.status, txt: response.statusText, err: new Error(), body: response.body };
});
};
export {
getAccompanyingCourse,
@ -204,5 +246,7 @@ export {
getUsers,
whoami,
getListOrigins,
postSocialIssue
postSocialIssue,
addScope,
removeScope,
};

View File

@ -88,6 +88,10 @@ export default {
socialIssue: {
msg: 'confirm.socialIssue_not_valid',
anchor: '#section-50'
},
scopes: {
msg: 'confirm.set_a_scope',
anchor: '#section-65'
}
}
}

View File

@ -0,0 +1,47 @@
<template>
<div class="vue-component">
<h2><a name="section-65"></a>{{ $t('scopes.title') }}</h2>
<ul>
<li v-for="s in scopes">
<input type="checkbox" v-model="checkedScopes" :value="s" />
{{ s.name.fr }}
</li>
</ul>
<div v-if="!isScopeValid" class="alert alert-warning separator">
{{ $t('scopes.add_at_least_one') }}
</div>
</div>
</template>
<script>
import { mapState, mapGetters } from 'vuex';
export default {
name: "Scopes",
computed: {
...mapState([
'scopes',
'scopesAtStart'
]),
...mapGetters([
'isScopeValid'
]),
checkedScopes: {
get: function() {
return this.$store.state.accompanyingCourse.scopes;
},
set: function(v) {
this.$store.dispatch('setScopes', v);
}
}
}
}
</script>
<style scoped>
</style>

View File

@ -86,6 +86,10 @@ const appMessages = {
person_locator: "Parcours localisé auprès de {0}",
no_address: "Il n'y a pas d'adresse associée au parcours"
},
scopes: {
title: "Services",
add_at_least_one: "Indiquez au moins un service",
},
referrer: {
title: "Référent du parcours",
label: "Vous pouvez choisir un TMS ou vous assigner directement comme référent",
@ -113,6 +117,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",
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 !",
ok: "Confirmer le parcours"

View File

@ -1,28 +1,41 @@
import 'es6-promise/auto';
import { createStore } from 'vuex';
import { fetchScopes } from 'ChillMainAssets/lib/api/scope.js';
import { getAccompanyingCourse,
patchAccompanyingCourse,
confirmAccompanyingCourse,
postParticipation,
postRequestor,
postResource,
postSocialIssue } from '../api';
postSocialIssue,
addScope,
removeScope,
} from '../api';
const debug = process.env.NODE_ENV !== 'production';
const id = window.accompanyingCourseId;
let initPromise = getAccompanyingCourse(id)
.then(accompanying_course => new Promise((resolve, reject) => {
let scopesPromise = fetchScopes();
let accompanyingCoursePromise = getAccompanyingCourse(id);
let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
.then(([scopes, accompanyingCourse]) => new Promise((resolve, reject) => {
const store = createStore({
strict: debug,
modules: {
},
state: {
accompanyingCourse: accompanying_course,
accompanyingCourse: accompanyingCourse,
addressContext: {},
errorMsg: []
errorMsg: [],
// all the available scopes
scopes: scopes,
// the scopes at start. If the user remove all scopes, we re-add those scopes, by security
scopesAtStart: accompanyingCourse.scopes.map(scope => scope),
// the scope states at server side
scopesAtBackend: accompanyingCourse.scopes.map(scope => scope),
},
getters: {
isParticipationValid(state) {
@ -34,11 +47,16 @@ let initPromise = getAccompanyingCourse(id)
isLocationValid(state) {
return state.accompanyingCourse.location !== null;
},
isScopeValid(state) {
console.log('is scope valid', state.accompanyingCourse.scopes.length > 0);
return state.accompanyingCourse.scopes.length > 0;
},
validationKeys(state, getters) {
let keys = [];
if (!getters.isParticipationValid) { keys.push('participation'); }
if (!getters.isLocationValid) { keys.push('location'); }
if (!getters.isSocialIssueValid) { keys.push('socialIssue'); }
if (!getters.isScopeValid) { keys.push('scopes'); }
//console.log('getter keys', keys);
return keys;
},
@ -142,6 +160,21 @@ let initPromise = getAccompanyingCourse(id)
//console.log('### mutation: set edit context true with addressId', payload.addressId);
state.addressContext.edit = true;
state.addressContext.addressId = payload.addressId;
},
setScopes(state, scopes) {
state.accompanyingCourse.scopes = scopes;
},
addScopeAtBackend(state, scope) {
let scopeIds = state.scopesAtBackend.map(s => s.id);
if (!scopeIds.includes(scope.id)) {
state.scopesAtBackend.push(scope);
}
},
removeScopeAtBackend(state, scope){
let scopeIds = state.scopesAtBackend.map(s => s.id);
if (scopeIds.includes(scope.id)) {
state.scopesAtBackend = state.scopesAtBackend.filter(s => s.id !== scope.id);
}
}
},
actions: {
@ -228,6 +261,107 @@ let initPromise = getAccompanyingCourse(id)
resolve();
})).catch((error) => { commit('catchError', error) });
},
/**
* Handle the checked/unchecked scopes
*
* When the user set the scopes in a invalid situation (when no scopes are cheched), this
* method will internally re-add the scopes as they were originally when the page was loaded, but
* this does not appears for the user (they remains unchecked). When the user re-add a scope, the
* scope is back in a valid state, and the store synchronize with the new state (all the original scopes
* are removed if necessary, and the new checked scopes is backed).
*
* So, for instance:
*
* at load:
*
* [x] scope A (at backend: [x])
* [x] scope B (at backend: [x])
* [ ] scope C (at backend: [ ])
*
* The user uncheck scope A:
*
* [ ] scope A (at backend: [ ] as soon as the operation finish)
* [x] scope B (at backend: [x])
* [ ] scope C (at backend: [ ])
*
* The user uncheck scope B. The state is invalid (no scope checked), so we go back to initial state when
* the page loaded):
*
* [ ] scope A (at backend: [x] as soon as the operation finish)
* [ ] scope B (at backend: [x] as soon as the operation finish)
* [ ] scope C (at backend: [ ])
*
* The user check scope C. The scopes are back to valid state. So we go back to synchronization with UI and
* backend):
*
* [ ] scope A (at backend: [ ] as soon as the operation finish)
* [ ] scope B (at backend: [ ] as soon as the operation finish)
* [x] scope C (at backend: [x] as soon as the operation finish)
*
* **Warning** There is a problem if the user check/uncheck faster than the backend is synchronized.
*
* @param commit
* @param state
* @param dispatch
* @param scopes
* @returns Promise
*/
setScopes({ commit, state, dispatch }, scopes) {
let currentServerScopesIds = state.scopesAtBackend.map(scope => scope.id);
let checkedScopesIds = scopes.map(scope => scope.id);
let removedScopesIds = currentServerScopesIds.filter(id => !checkedScopesIds.includes(id));
let addedScopesIds = checkedScopesIds.filter(id => !currentServerScopesIds.includes(id));
let lengthAfterOperation = currentServerScopesIds.length + addedScopesIds.length
- removedScopesIds.length;
if (lengthAfterOperation > 0 || (lengthAfterOperation === 0 && state.scopesAtStart.length === 0) ) {
return dispatch('updateScopes', {
addedScopesIds, removedScopesIds
}).then(() => {
// warning: when the operation of dispatch are too slow, the user may check / uncheck before
// the end of the synchronisation with the server (done by dispatch operation). Then, it leads to
// check/uncheck in the UI. I do not know of to avoid it.
commit('setScopes', scopes);
return Promise.resolve();
});
} else {
return dispatch('setScopes', state.scopesAtStart).then(() => {
commit('setScopes', scopes);
return Promise.resolve();
});
}
},
/**
* Internal function for the store to effectively update scopes.
*
* Return a promise which resolves when all update operation are
* successful and finished.
*
* @param state
* @param commit
* @param addedScopesIds
* @param removedScopesIds
* @return Promise
*/
updateScopes({ state, commit }, { addedScopesIds, removedScopesIds }) {
let promises = [];
state.scopes.forEach(scope => {
if (addedScopesIds.includes(scope.id)) {
promises.push(addScope(state.accompanyingCourse.id, scope).then(() => {
commit('addScopeAtBackend', scope);
return Promise.resolve();
}));
}
if (removedScopesIds.includes(scope.id)) {
promises.push(removeScope(state.accompanyingCourse.id, scope).then(() => {
commit('removeScopeAtBackend', scope);
return Promise.resolve();
}));
}
});
return Promise.all(promises);
},
postFirstComment({ commit }, payload) {
//console.log('## action: postFirstComment: payload', payload);
patchAccompanyingCourse(id, { type: "accompanying_period", initialComment: payload })

View File

@ -146,10 +146,10 @@
{% endif %}
{% endif %}
</li>
{% if options['addCenter'] %}
{% if options['addCenter'] and person|chill_resolve_center is not null %}
<li>
<i class="fa fa-li fa-long-arrow-right"></i>
{{ person.center }}
{{ person|chill_resolve_center.name }}
</li>
{% endif %}
</ul>

View File

@ -17,11 +17,11 @@
{%- endif -%}
</div>
<div class="text-md-end">
{%- if chill_person.fields.spoken_languages == 'visible' -%}
{% if person|chill_resolve_center is not null%}
<span class="open_sansbold">
{{ 'Center'|trans|upper}} :
</span>
{{ person.center.name|upper }}
{{ person|chill_resolve_center.name|upper }}
{%- endif -%}
</div>
</div>

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