mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
Merge branch 'master' into person_renderbox_thirdparty_onthefly
This commit is contained in:
commit
d5f1de1fbc
118
composer.json
118
composer.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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];
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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') }}
|
||||
|
@ -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 }}
|
||||
|
@ -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 %}
|
||||
|
@ -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 [];
|
||||
}
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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']
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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(),
|
||||
]);
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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();
|
||||
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
// /**
|
||||
|
@ -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'
|
||||
|
@ -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 %}
|
||||
|
@ -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 %}
|
@ -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 %}
|
||||
|
@ -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>
|
||||
|
@ -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
|
@ -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);
|
||||
|
||||
|
@ -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());
|
||||
|
@ -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,
|
||||
]
|
||||
],
|
||||
]
|
||||
],
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
52
src/Bundle/ChillMainBundle/Doctrine/DQL/STContains.php
Normal file
52
src/Bundle/ChillMainBundle/Doctrine/DQL/STContains.php
Normal 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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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)
|
||||
{
|
||||
|
@ -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;
|
||||
|
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\MainBundle\Entity;
|
||||
|
||||
interface HasCentersInterface
|
||||
{
|
||||
public function getCenters(): ?iterable;
|
||||
}
|
11
src/Bundle/ChillMainBundle/Entity/HasScopesInterface.php
Normal file
11
src/Bundle/ChillMainBundle/Entity/HasScopesInterface.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\MainBundle\Entity;
|
||||
|
||||
interface HasScopesInterface
|
||||
{
|
||||
/**
|
||||
* @return array|Scope[]
|
||||
*/
|
||||
public function getScopes(): iterable;
|
||||
}
|
@ -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
|
||||
*/
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
76
src/Bundle/ChillMainBundle/Entity/UserJob.php
Normal file
76
src/Bundle/ChillMainBundle/Entity/UserJob.php
Normal 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;
|
||||
}
|
||||
}
|
36
src/Bundle/ChillMainBundle/Form/Event/CustomizeFormEvent.php
Normal file
36
src/Bundle/ChillMainBundle/Form/Event/CustomizeFormEvent.php
Normal 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;
|
||||
}
|
||||
}
|
@ -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')
|
||||
;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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());
|
||||
|
27
src/Bundle/ChillMainBundle/Form/UserJobType.php
Normal file
27
src/Bundle/ChillMainBundle/Form/UserJobType.php
Normal 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
|
||||
]
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
}
|
@ -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(
|
||||
|
@ -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[]
|
||||
*/
|
||||
|
17
src/Bundle/ChillMainBundle/Resources/public/lib/api/scope.js
Normal file
17
src/Bundle/ChillMainBundle/Resources/public/lib/api/scope.js
Normal 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
|
||||
};
|
@ -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 %}
|
@ -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> </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 %}
|
@ -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);
|
||||
|
||||
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\MainBundle\Security\Authorization;
|
||||
|
||||
interface VoterHelperFactoryInterface
|
||||
{
|
||||
public function generate($context): VoterGeneratorInterface;
|
||||
}
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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]);
|
||||
}
|
||||
|
||||
}
|
@ -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]);
|
||||
}
|
||||
}
|
@ -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"
|
||||
|
@ -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 }
|
||||
|
||||
|
@ -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
|
||||
|
@ -18,6 +18,8 @@ Chill\MainBundle\Entity\User:
|
||||
min: 3
|
||||
email:
|
||||
- Email: ~
|
||||
label:
|
||||
- NotBlank: ~
|
||||
constraints:
|
||||
- Callback:
|
||||
callback: isGroupCenterPresentOnce
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
]);
|
||||
|
@ -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
|
||||
]);
|
||||
|
@ -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']
|
||||
|
@ -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([
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
]
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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))
|
||||
));
|
||||
}
|
||||
|
@ -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' => [
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -88,6 +88,10 @@ export default {
|
||||
socialIssue: {
|
||||
msg: 'confirm.socialIssue_not_valid',
|
||||
anchor: '#section-50'
|
||||
},
|
||||
scopes: {
|
||||
msg: 'confirm.set_a_scope',
|
||||
anchor: '#section-65'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
@ -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"
|
||||
|
@ -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 })
|
||||
|
@ -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>
|
||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user