Merge branch 'master' into improvements_acitivite_annexe

This commit is contained in:
Julie Lenaerts 2021-09-23 14:39:39 +02:00
commit d4116a4659
181 changed files with 5717 additions and 2359 deletions

View File

@ -62,7 +62,8 @@
"symfony/web-profiler-bundle": "^5.0"
},
"config": {
"bin-dir": "bin"
"bin-dir": "bin",
"vendor-dir": "tests/app/vendor"
},
"autoload": {
"psr-4": {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,45 @@
<?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 (null === $request) {
return;
}
if ($request->query->has('activityData')) {
$activityData = $request->query->get('activityData');
if (array_key_exists('calendarId', $activityData)) {
$calendarId = $activityData['calendarId'];
}
}
// Attach the activity to the calendar
$em = $event->getObjectManager();
$calendar = $em->getRepository(\Chill\CalendarBundle\Entity\Calendar::class)->find($calendarId);
$calendar->setActivity($activity);
$em->persist($calendar);
$em->flush();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,52 @@
<?php
/*
* Chill is a software for social workers
* Copyright (C) 2018 Champs-Libres Coopérative <info@champs-libres.coop>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Chill\MainBundle\Doctrine\DQL;
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Lexer;
/**
* Geometry function 'ST_CONTAINS', added by postgis
*/
class STContains extends FunctionNode
{
private $firstPart;
private $secondPart;
public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker)
{
return 'ST_CONTAINS('.$this->firstPart->dispatch($sqlWalker).
', ' . $this->secondPart->dispatch($sqlWalker) .")";
}
public function parse(\Doctrine\ORM\Query\Parser $parser)
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$this->firstPart = $parser->StringPrimary();
$parser->match(Lexer::T_COMMA);
$this->secondPart = $parser->StringPrimary();
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,17 +3,17 @@
/*
* Chill is a suite of a modules, Chill is a software for social workers
* Copyright (C) 2014, Champs Libres Cooperative SCRLFS, <http://www.champs-libres.coop>
*
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
@ -46,17 +46,17 @@ class Scope
* @Groups({"read"})
*/
private $id;
/**
* translatable names
*
*
* @var array
*
* @ORM\Column(type="json_array")
* @Groups({"read"})
*/
private $name = [];
/**
* @var Collection
*
@ -66,8 +66,8 @@ class Scope
* @ORM\Cache(usage="NONSTRICT_READ_WRITE")
*/
private $roleScopes;
/**
* Scope constructor.
*/
@ -75,7 +75,7 @@ class Scope
{
$this->roleScopes = new ArrayCollection();
}
/**
* @return int
*/
@ -91,7 +91,7 @@ class Scope
{
return $this->name;
}
/**
* @param $name
* @return $this
@ -101,7 +101,7 @@ class Scope
$this->name = $name;
return $this;
}
/**
* @return Collection
*/
@ -109,7 +109,7 @@ class Scope
{
return $this->roleScopes;
}
/**
* @param RoleScope $roleScope
*/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -98,6 +98,9 @@ section.chill-entity {
&.date-since {}
&.date-until {}
}
.address-more {
margin-bottom: 1em;
}
}
// used for comment-embeddable

View File

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

View File

@ -52,10 +52,10 @@ Fields.forEach(function(field) {
ClassicEditor
.create( field )
.then( editor => {
console.log( 'CkEditor was initialized', editor );
//console.log( 'CkEditor was initialized', editor );
})
.catch( error => {
console.error( error.stack );
})
;
});
});

View File

@ -1,24 +1,15 @@
<template>
<add-address
v-bind:key="context.entity.type"
v-bind:key="key"
v-bind:context="context"
v-bind:options="addAddress.options"
v-bind:result="addAddress.result"
@submitAddress="submitAddress"
v-bind:options="options"
v-bind:addressChangedCallback="submitAddress"
ref="addAddress">
</add-address>
</template>
<script>
/*
* Address component is a uniq component for many contexts.
* Allow to create/attach/edit an address to
* - a person (new or edit address),
* - a household (move or edit address)
*
* */
import AddAddress from './components/AddAddress.vue';
import { patchAddress } from "./api";
import { postAddressToHousehold, postAddressToPerson } from "ChillPersonAssets/vuejs/_api/AddAddress";
export default {
@ -26,60 +17,46 @@ export default {
components: {
AddAddress
},
data() {
return {
context: {
edit: window.mode === 'edit',
entity: {
type: window.entityType,
id: window.entityId
},
addressId: window.addressId | null,
backUrl: window.backUrl,
valid: {
from: new Date()
},
},
addAddress: {
options: {
/// Options override default.
/// null value take default component value defined in AddAddress data()
button: {
text: {
create: window.buttonText || null,
edit: window.buttonText || null
},
size: window.buttonSize || null,
displayText: window.buttonDisplayText //boolean, default: true
},
/// Modal title text if create or edit address (trans chain, see i18n)
title: {
create: window.modalTitle || null,
edit: window.modalTitle || null
},
/// Display each step in page or Modal
bindModal: {
step1: window.binModalStep1, //boolean, default: true
step2: window.binModalStep2 //boolean, default: true
}
}
}
props: ['addAddress'],
computed: {
context() {
return this.addAddress.context;
},
options() {
return this.addAddress.options;
},
key() {
return (this.context.edit) ? 'address_' + this.context.addressId
: this.context.target.name + '_' + this.context.target.id ;
}
},
mounted() {
//console.log('AddAddress: data context', this.context);
//console.log('AddAddress: data options', this.options);
},
methods: {
displayErrors() {
return this.$refs.addAddress.errorMsg;
},
submitAddress() {
console.log('@@@ click on Submit Address Button');
let payload = this.$refs.addAddress.submitNewAddress(); // Cast child method
this.addDateToAddressAndPostAddressTo(payload);
submitAddress(payload) {
console.log('@@@ click on Submit Address Button', payload);
// Existing address
if (this.context.edit) {
// address is already linked, just finish !
this.$refs.addAddress.afterLastPaneAction({});
// New created address
} else {
this.postAddressTo(payload);
}
},
addDateToAddressAndPostAddressTo(payload)
{
/*
* Patch date
* TO MOVE in DatePane
addDateToAddressAndPostAddressTo(payload) {
payload.body = {
validFrom: {
datetime: `${this.context.valid.from.toISOString().split('T')[0]}T00:00:00+0100`
@ -100,50 +77,57 @@ export default {
this.$refs.addAddress.flag.loading = false;
});
},
*/
postAddressTo(payload)
{
console.log('postAddressTo', payload.entity);
if (!this.context.edit) {
switch (payload.entity) {
case 'household':
postAddressToHousehold(payload.entityId, payload.addressId)
.then(household => new Promise((resolve, reject) => {
console.log('..toHousehold', household);
this.$refs.addAddress.flag.loading = false;
this.$refs.addAddress.flag.success = true;
window.location.assign(this.context.backUrl);
resolve();
}))
.catch((error) => {
this.$refs.addAddress.errorMsg.push(error);
this.$refs.addAddress.flag.loading = false;
})
;
break;
case 'person':
postAddressToPerson(payload.entityId, payload.addressId)
.then(person => new Promise((resolve, reject) => {
console.log('..toPerson', person);
this.$refs.addAddress.flag.loading = false;
this.$refs.addAddress.flag.success = true;
window.location.assign(this.context.backUrl);
resolve();
}))
.catch((error) => {
this.$refs.addAddress.errorMsg.push(error);
this.$refs.addAddress.flag.loading = false;
})
;
break;
default:
this.$refs.addAddress.errorMsg.push('That entity is not managed by address !');
}
} else {
// address is already linked, just finish !
window.location.assign(this.context.backUrl);
/*
* Post new created address to targetEntity
*/
postAddressTo(payload) {
console.log('postAddress', payload.addressId, 'To', payload.target, payload.targetId);
switch (payload.target) {
case 'household':
postAddressToHousehold(payload.targetId, payload.addressId)
.then(address => new Promise((resolve, reject) => {
console.log('..household address', address);
this.$refs.addAddress.flag.loading = false;
this.$refs.addAddress.flag.success = true;
// finish
this.$refs.addAddress.afterLastPaneAction({ addressId: address.address_id });
resolve();
}))
.catch((error) => {
this.$refs.addAddress.errorMsg.push(error);
this.$refs.addAddress.flag.loading = false;
})
;
break;
case 'person':
postAddressToPerson(payload.targetId, payload.addressId)
.then(address => new Promise((resolve, reject) => {
console.log('..person address', address);
this.$refs.addAddress.flag.loading = false;
this.$refs.addAddress.flag.success = true;
// finish
this.$refs.addAddress.afterLastPaneAction({ addressId: address.address_id });
resolve();
}))
.catch((error) => {
this.$refs.addAddress.errorMsg.push(error);
this.$refs.addAddress.flag.loading = false;
})
;
break;
case 'thirdparty':
console.log('TODO write postAddressToThirdparty');
break;
default:
this.$refs.addAddress.errorMsg.push('That entity is not managed by address !');
}
},
}
}
}
</script>

View File

@ -109,7 +109,7 @@ const patchAddress = (id, body) => {
* method POST, post Postal Code Object
* @returns {Promise}
*/
const postPostalCode = (postalCode) => {
const postPostalCode = (postalCode) => { //<--
const url = `/api/1.0/main/postal-code.json?`;
const body = postalCode;

View File

@ -0,0 +1,31 @@
<template>
<ul class="record_actions"
:class="{ 'sticky-form-buttons': isStickyForm }">
<li v-if="isStickyForm" class="cancel">
<slot name="before"></slot>
</li>
<slot name="action"></slot>
<li v-if="isStickyForm">
<slot name="after"></slot>
</li>
</ul>
</template>
<script>
export default {
name: "ActionButtons",
props: ['options', 'defaultz'],
computed: {
isStickyForm() {
return (typeof this.options.stickyActions !== 'undefined') ?
this.options.stickyActions : this.defaultz.stickyActions;
},
},
methods: {
}
}
</script>

View File

@ -1,18 +1,22 @@
<template>
<!-- start with a button -->
<button v-if="step1WithModal"
@click="openShowPane"
class="btn" :class="getClassButton"
type="button" name="button" :title="$t(getTextButton)">
<span v-if="displayTextButton">{{ $t(getTextButton) }}</span>
</button>
<!-- step0 -->
<show-pane v-if="flag.showPane"
v-bind:context="this.context"
v-bind:options="this.options"
v-bind:defaultz="this.defaultz"
v-bind:entity="this.entity"
v-bind:flag="this.flag"
v-bind:useDatePane="this.useDatePane"
@openEditPane="openEditPane"
ref="showAddress">
</show-pane>
<!-- step 1 -->
<teleport to="body" v-if="step1WithModal">
<modal v-if="flag.showPane"
<teleport to="body" v-if="inModal">
<modal v-if="flag.suggestPane"
modalDialogClass="modal-dialog-scrollable modal-xl"
@close="flag.showPane = false">
@close="resetPane">
<template v-slot:header>
<h2 class="modal-title">{{ $t(getTextTitle) }}
@ -24,24 +28,86 @@
</template>
<template v-slot:body>
<show-address-pane
<suggest-pane
v-bind:context="this.context"
v-bind:options="this.options"
v-bind:default="this.default"
v-bind:defaultz="this.defaultz"
v-bind:entity="this.entity"
v-bind:valid="this.context.valid"
v-bind:flag="this.flag"
ref="showAddress">
</show-address-pane>
ref="suggestAddress">
</suggest-pane>
</template>
<template v-slot:footer>
<button @click="openEditPane"
class="btn btn-update">
{{ $t('action.edit')}}
class="btn btn-create">
{{ $t('create_a_new_address')}}
</button>
<button class="btn btn-save"
@click.prevent="$emit('submitAddress')">
</template>
</modal>
</teleport>
<div class="mt-4" v-else>
<suggest-pane v-if="flag.suggestPane"
v-bind:context="this.context"
v-bind:options="this.options"
v-bind:defaultz="this.defaultz"
v-bind:entity="this.entity"
v-bind:flag="this.flag"
v-bind:insideModal="false"
ref="suggestAddress">
<template v-slot:before v-if="!bypassFirstStep">
<a class="btn btn-cancel" @click="resetPane">
{{ $t('action.cancel') }}
</a>
</template>
<template v-slot:action>
<li>
<button @click="openEditPane"
class="btn btn-create">
{{ $t('create_a_new_address')}}
</button>
</li>
</template>
</suggest-pane>
</div>
<!-- step 2 -->
<teleport to="body" v-if="inModal">
<modal v-if="flag.editPane"
modalDialogClass="modal-dialog-scrollable modal-xl"
@close="resetPane">
<template v-slot:header>
<h2 class="modal-title">{{ $t(getTextTitle) }}
<span v-if="flag.loading" class="loading">
<i class="fa fa-circle-o-notch fa-spin fa-fw"></i>
<span class="sr-only">{{ $t('loading') }}</span>
</span>
</h2>
</template>
<template v-slot:body>
<edit-pane
v-bind:context="this.context"
v-bind:options="this.options"
v-bind:defaultz="this.defaultz"
v-bind:entity="this.entity"
v-bind:flag="this.flag"
@getCities="getCities"
@getReferenceAddresses="getReferenceAddresses">
</edit-pane>
</template>
<template v-slot:footer>
<!--<button class="btn btn-cancel change-icon" @click="resetPane">{{ $t('action.cancel') }}</button>-->
<button v-if="!this.context.edit && this.useDatePane" class="btn btn-update change-icon" @click="closeEditPane">
{{ $t('nav.next')}}
<i class="fa fa-fw fa-arrow-right"></i>
</button>
<button v-else class="btn btn-save" @click="closeEditPane">
{{ $t('action.save')}}
</button>
</template>
@ -49,25 +115,43 @@
</modal>
</teleport>
<div class="mt-4" v-else>
<show-address-pane v-if="flag.showPane"
<edit-pane v-if="flag.editPane"
v-bind:context="this.context"
v-bind:options="this.options"
v-bind:default="this.default"
v-bind:defaultz="this.defaultz"
v-bind:entity="this.entity"
v-bind:valid="this.context.valid"
v-bind:flag="this.flag"
ref="showAddress"
v-bind:insideModal="false" @openEditPane="openEditPane"
@submitAddress="$emit('submitAddress')">
</show-address-pane>
v-bind:insideModal="false"
@getCities="getCities"
@getReferenceAddresses="getReferenceAddresses">
<template v-slot:before>
<a class="btn btn-cancel" @click="resetPane">
{{ $t('action.cancel') }}
</a>
</template>
<template v-slot:action>
<li v-if="!this.context.edit && this.useDatePane">
<button class="btn btn-update change-icon" @click="closeEditPane">
{{ $t('nav.next')}}
<i class="fa fa-fw fa-arrow-right"></i>
</button>
</li>
<li v-else>
<button class="btn btn-save" @click="closeEditPane">
{{ $t('action.save')}}
</button>
</li>
</template>
</edit-pane>
</div>
<!-- step 2 -->
<teleport to="body" v-if="step2WithModal">
<modal v-if="flag.editPane"
modalDialogClass="modal-dialog-scrollable modal-xl"
@close="flag.editPane = false">
<!-- step 3 -->
<teleport to="body" v-if="inModal">
<modal v-if="flag.datePane"
modalDialogClass="modal-dialog-scrollable modal-xl"
@close="resetPane">
<template v-slot:header>
<h2 class="modal-title">{{ $t(getTextTitle) }}
@ -79,41 +163,54 @@
</template>
<template v-slot:body>
<edit-address-pane
<date-pane
v-bind:context="this.context"
v-bind:options="this.options"
v-bind:default="this.default"
v-bind:defaultz="this.defaultz"
v-bind:entity="this.entity"
v-bind:flag="this.flag"
@getCities="getCities"
@getReferenceAddresses="getReferenceAddresses">
</edit-address-pane>
ref="dateAddress">
</date-pane>
</template>
<template v-slot:footer>
<button class="btn btn-cancel change-icon" @click="flag.showPane = true; flag.editPane = false;">
{{ $t('action.cancel') }}
<button class="btn btn-misc" @click="openEditPane">
<i class="fa fa-fw fa-arrow-left"></i>
{{ $t('nav.previous')}}
</button>
<button class="btn btn-update"
@click="closeEditPane">
{{ $t('action.valid')}}
<button class="btn btn-save" @click="closeDatePane">
{{ $t('action.save')}}
</button>
<!-- -->
</template>
</modal>
</teleport>
<div class="mt-4" v-else>
<edit-address-pane v-if="flag.editPane"
<date-pane v-if="flag.datePane"
v-bind:context="this.context"
v-bind:options="this.options"
v-bind:default="this.default"
v-bind:defaultz="this.defaultz"
v-bind:entity="this.entity"
v-bind:flag="this.flag"
v-bind:insideModal="false" @closeEditPane="closeEditPane"
@getCities="getCities"
@getReferenceAddresses="getReferenceAddresses">
</edit-address-pane>
v-bind:insideModal="false"
ref="dateAddress">
<template v-slot:before>
<button class="btn btn-misc" @click="openEditPane">
<i class="fa fa-fw fa-arrow-left"></i>
{{ $t('nav.previous')}}
</button>
</template>
<template v-slot:action>
<li>
<button class="btn btn-save" @click="closeDatePane">
{{ $t('action.save')}}
</button>
</li>
</template>
</date-pane>
</div>
</template>
@ -122,37 +219,45 @@
import Modal from 'ChillMainAssets/vuejs/_components/Modal';
import { getAddress, fetchCountries, fetchCities, fetchReferenceAddresses, patchAddress, postAddress, postPostalCode } from '../api';
import { postAddressToPerson, postAddressToHousehold } from "ChillPersonAssets/vuejs/_api/AddAddress.js";
import ShowAddressPane from './ShowAddressPane.vue';
import EditAddressPane from './EditAddressPane.vue';
import ShowPane from './ShowPane.vue';
import SuggestPane from './SuggestPane.vue';
import EditPane from './EditPane.vue';
import DatePane from './DatePane.vue';
export default {
name: "AddAddress",
props: ['context', 'options', 'result'],
emits: ['submitAddress'],
props: ['context', 'options', 'addressChangedCallback'],
components: {
Modal,
ShowAddressPane,
EditAddressPane,
ShowPane,
SuggestPane,
EditPane,
DatePane
},
data() {
return {
flag: {
showPane: false,
showPane: true, // begin with showPane
suggestPane: false,
editPane: false,
datePane: false,
loading: false,
success: false
},
default: {
defaultz: {
button: {
text: { create: 'add_an_address_title', edit: 'edit_address' },
type: { create: 'btn-create', edit: 'btn-update'},
displayText: true
},
title: { create: 'add_an_address_title', edit: 'edit_address' },
bindModal: {
step1: true,
step2: true,
openPanesInModal: true,
stickyActions: false,
useDate: {
validFrom: false,
validTo: false
},
hideAddress: false
},
entity: {
address: {}, // <== loaded and returned
@ -173,6 +278,10 @@ export default {
writeNew: {
address: false,
postcode: false
},
valid: {
from: new Date(),
to: null
}
},
addressMap: {
@ -186,19 +295,26 @@ export default {
}
},
computed: {
step1WithModal() {
return (this.options.bindModal !== null && typeof this.options.bindModal.step1 !== 'undefined') ?
this.options.bindModal.step1 : this.default.bindModal.step1;
inModal() {
return (typeof this.options.openPanesInModal !== 'undefined') ?
this.options.openPanesInModal : this.defaultz.openPanesInModal;
},
step2WithModal() {
let step2 = (this.options.bindModal !== null && typeof this.options.bindModal.step2 !== 'undefined') ?
this.options.bindModal.step2 : this.default.bindModal.step2;
if (step2 === false && this.step1WithModal === true) {
console.log("step2 must open in a Modal");
return true;
}
return step2;
useDatePane() {
let vFrom = (typeof this.options.useDate !== 'undefined'
&& typeof this.options.useDate.validFrom !== 'undefined') ?
this.options.useDate.validFrom : this.defaultz.useDate.validFrom ;
let vTo = (typeof this.options.useDate !== 'undefined'
&& typeof this.options.useDate.validTo !== 'undefined') ?
this.options.useDate.validTo : this.defaultz.useDate.validTo ;
return (vFrom || vTo) ? true : false;
},
hasSuggestions() {
// TODO
//return addressSuggestions.length > 0
return false;
},
displaySuggestions() {
return !this.context.edit && this.hasSuggestions;
},
getTextTitle() {
if ( typeof this.options.title !== 'undefined'
@ -207,36 +323,27 @@ export default {
)) {
return (this.context.edit) ? this.options.title.edit : this.options.title.create;
}
return (this.context.edit) ? this.default.title.edit : this.default.title.create;
return (this.context.edit) ? this.defaultz.title.edit : this.defaultz.title.create;
},
getTextButton() {
if ( typeof this.options.button.text !== 'undefined'
&& ( this.options.button.text.edit !== null
|| this.options.button.text.create !== null
)) {
return (this.context.edit) ? this.options.button.text.edit : this.options.button.text.create;
}
return (this.context.edit) ? this.default.button.text.edit : this.default.button.text.create;
bypassFirstStep() {
// exception: passing step0 if new address and pane are not in modal
return !this.context.edit && !this.inModal;
},
getClassButton() {
let type = (this.context.edit) ? this.default.button.type.edit : this.default.button.type.create;
let size = (typeof this.options.button !== 'undefined' && this.options.button.size !== null) ?
`${this.options.button.size} ` : '';
return `${size}${type}`;
},
displayTextButton() {
return (typeof this.options.button !== 'undefined' && typeof this.options.button.displayText !== 'undefined') ?
this.options.button.displayText : this.default.button.displayText;
},
context() {
return this.context;
forceRedirect() {
return (!(this.context.backUrl === null || typeof this.context.backUrl === 'undefined'));
}
},
mounted() {
if (!this.step1WithModal) {
//console.log('Mounted now !');
this.openShowPane();
console.log('useDatePane', this.useDatePane);
console.log('Mounted now !');
if (this.context.edit) {
console.log('getInitialAddress', this.context.addressId);
this.getInitialAddress(this.context.addressId);
}
this.openShowPane();
},
methods: {
@ -244,35 +351,89 @@ export default {
* Opening and closing Panes when interacting with buttons
*/
openShowPane() {
if (this.context.edit) {
this.getInitialAddress(this.context.addressId);
}
// when create new address, start first with editPane
if ( this.context.edit === false
&& this.flag.editPane === false
) {
if (this.flag.editPane === false && this.bypassFirstStep) {
console.log('bypassFirstStep');
this.closeShowPane();
this.openEditPane();
this.flag.editPane = true;
} else {
this.flag.showPane = true;
console.log('step1: open the Show Panel');
console.log('step0: open the Show Panel');
}
},
closeShowPane() {
// Show pane can be closed only when openPanesInModal is false
if (!this.inModal) {
this.flag.showPane = false;
console.log('step0: close the Show Panel');
}
},
openSuggestPane() {
this.flag.suggestPane = true;
console.log('step1: open the Suggestion Panel');
},
closeSuggestPane() {
this.flag.suggestPane = false;
console.log('step1: close the Suggestion Panel');
},
openEditPane() {
console.log('step2: open the Edit panel');
this.initForm();
this.getCountries();
if (this.flag.suggestPane === false && this.displaySuggestions) {
console.log('displaySuggestions');
this.openSuggestPane();
} else {
if (this.flag.datePane === false) {
this.initForm(); // reset form except if we come back from datePane
}
this.getCountries(); // will open editPane when resolve promise
}
},
closeEditPane() {
this.flag.editPane = false;
console.log('step2: close the Edit Panel');
if (!this.context.edit && this.useDatePane) {
this.openDatePane();
} else {
this.applyChanges();
if (!this.forceRedirect) {
this.openShowPane();
}
}
},
openDatePane() {
this.flag.datePane = true;
console.log('step3: open the Date Panel');
},
closeDatePane() {
this.applyChanges();
this.flag.datePane = false;
console.log('step3: close the Date Panel');
},
resetPane() {
console.log('resetPane');
this.flag.suggestPane = false;
this.flag.editPane = false;
this.flag.datePane = false;
this.openShowPane();
},
closeEditPane() {
console.log('step2: close the Edit Panel');
this.applyChanges();
this.flag.showPane = true;
this.flag.editPane = false;
/*
* What happens when submitting last Pane:
* - redirect or reset pane,
* - change context to editing
*/
afterLastPaneAction(params) {
this.initForm();
if (this.forceRedirect) {
console.log("redirect to backUrl");
window.location.assign(this.context.backUrl);
} else {
console.log("don't redirect");
this.resetPane();
if (!this.context.edit) {
this.context.edit = true;
this.context.addressId = params.addressId;
console.log("context is now edit, with address", params.addressId);
}
}
},
/*
@ -297,7 +458,16 @@ export default {
fetchCountries().then(
countries => new Promise((resolve, reject) => {
this.entity.loaded.countries = countries.results;
this.flag.showPane = false;
if (this.flag.showPane === true) {
this.closeShowPane();
}
if (this.flag.suggestPane === true) {
this.closeSuggestPane();
}
if (this.flag.datePane === true) {
this.flag.datePane = false;
}
console.log('step2: open the Edit panel');
this.flag.editPane = true;
this.flag.loading = false;
resolve()
@ -365,6 +535,7 @@ export default {
this.entity.selected.writeNew.address = this.context.edit;
this.entity.selected.writeNew.postcode = this.context.edit;
console.log('!! just set writeNew.postcode to', this.entity.selected.writeNew.postcode);
},
/*
@ -374,6 +545,8 @@ export default {
*/
applyChanges()
{
console.log('apply changes');
let newAddress = {
'isNoAddress': this.entity.selected.isNoAddress,
'street': this.entity.selected.isNoAddress ? '' : this.entity.selected.address.street,
@ -398,18 +571,22 @@ export default {
newPostcode = Object.assign(newPostcode, {
'country': {'id': this.entity.selected.country.id },
});
console.log('writeNew postcode is true! newPostcode: ', newPostcode);
newAddress = Object.assign(newAddress, {
'newPostcode': newPostcode
});
}
if (this.context.edit) {
if (!this.context.edit) {
this.addNewAddress(newAddress)
.then(payload => this.addressChangedCallback(payload));
} else {
this.updateAddress({
addressId: this.context.addressId,
newAddress: newAddress
});
} else {
this.addNewAddress(newAddress);
})
.then(payload => this.addressChangedCallback(payload));
}
},
@ -419,34 +596,42 @@ export default {
*/
addNewAddress(payload)
{
//console.log('addNewAddress', payload);
console.log('addNewAddress', payload);
this.flag.loading = true;
if ('newPostcode' in payload) {
let postcodeBody = payload.newPostcode;
if (this.context.entity.type === 'person') {
if (this.context.target.name === 'person') { // !!! maintain here ?
postcodeBody = Object.assign(postcodeBody, {'origin': 3});
}
postPostalCode(postcodeBody)
console.log('juste before post new postcode', postcodeBody);
return postPostalCode(postcodeBody)
.then(postalCode => {
console.log('new postcode created', postalCode.id);
payload.postcode = {'id': postalCode.id };
this.postNewAddress(payload);
return this.postNewAddress(payload);
});
} else {
this.postNewAddress(payload);
return this.postNewAddress(payload);
}
},
postNewAddress(payload) {
//console.log('postNewAddress', payload);
postAddress(payload)
postNewAddress(payload)
{
console.log('postNewAddress', payload);
return postAddress(payload)
.then(address => new Promise((resolve, reject) => {
this.entity.address = address;
this.flag.loading = false;
this.flag.success = true;
resolve();
resolve({
target: this.context.target.name,
targetId: this.context.target.id,
addressId: this.entity.address.address_id
}
);
}))
.catch((error) => {
this.errorMsg.push(error);
@ -463,31 +648,39 @@ export default {
this.flag.loading = true;
// TODO change the condition because it writes new postal code in edit mode now: !writeNewPostalCode
// BUG réécrit un postcode à chaque édition !
if ('newPostcode' in payload.newAddress) {
let postcodeBody = payload.newAddress.newPostcode;
postcodeBody = Object.assign(postcodeBody, {'origin': 3});
postPostalCode(postcodeBody)
console.log('juste before post new postcode', postcodeBody);
return postPostalCode(postcodeBody)
.then(postalCode => {
console.log('new postcode created', postalCode.id);
payload.newAddress.postcode = {'id': postalCode.id };
this.patchExistingAddress(payload);
return this.patchExistingAddress(payload);
});
} else {
this.patchExistingAddress(payload);
return this.patchExistingAddress(payload);
}
},
patchExistingAddress(payload) {
console.log('patchExistingAddress', payload);
patchAddress(payload.addressId, payload.newAddress)
return patchAddress(payload.addressId, payload.newAddress)
.then(address => new Promise((resolve, reject) => {
this.entity.address = address;
this.flag.loading = false;
this.flag.success = true;
resolve();
}))
this.entity.address = address;
this.flag.loading = false;
this.flag.success = true;
return resolve({
target: this.context.target.name,
targetId: this.context.target.id,
addressId: this.entity.address.address_id
});
})
)
.catch((error) => {
this.errorMsg.push(error);
this.flag.loading = false;
@ -495,24 +688,17 @@ export default {
},
/*
* Method called by parent when submitting address
* Method just add closing pane to the callback method
* (get out step1 show pane, submit button)
*/
submitNewAddress()
closePaneAndCallbackSubmit(payload)
{
let payload = {
entity: this.context.entity.type,
entityId: this.context.entity.id,
addressId: this.entity.address.address_id
};
console.log('submitNewAddress return', payload);
this.initForm();
this.flag.showPane = false;
return payload;
//this.initForm();
//this.resetPane(); // because parent callback will cast afterLastPaneAction()
console.log('will call parent callback method', payload);
// callback props method from parent
this.addressChangedCallback(payload);
}
*/
}
}
</script>
@ -525,4 +711,7 @@ div.entity-address {
right: 0; top: -55px;
}
}
h4.h3 {
font-weight: bold;
}
</style>

View File

@ -1,6 +1,6 @@
<template>
<div class="my-1">
<label class="col-form-label" for="citySelector">{{ $t('city') }}</label>
<label class="col-form-label">{{ $t('city') }}</label>
<VueMultiselect
id="citySelector"
v-model="value"
@ -25,7 +25,7 @@
<div class="form-floating">
<input class="form-control"
type="text"
name="code"
id="code"
:placeholder="$t('postalCode_code')"
v-model="code"/>
<label for="code">{{ $t('postalCode_code') }}</label>
@ -35,7 +35,7 @@
<div class="form-floating">
<input class="form-control"
type="text"
name="name"
id="name"
:placeholder="$t('postalCode_name')"
v-model="name"/>
<label for="name">{{ $t('postalCode_name') }}</label>
@ -87,6 +87,9 @@ export default {
}
},
},
mounted() {
console.log('writeNew.postcode', this.entity.selected.writeNew.postcode, 'in mounted');
},
methods: {
transName(value) {
return (value.code && value.name) ? `${value.code}-${value.name}` : '';
@ -96,6 +99,7 @@ export default {
this.entity.selected.postcode.name = value.name;
this.entity.selected.postcode.code = value.code;
this.entity.selected.writeNew.postcode = false;
console.log('writeNew.postcode false, in selectCity');
this.$emit('getReferenceAddresses', value);
this.focusOnAddress();
},
@ -110,6 +114,7 @@ export default {
this.entity.selected.postcode.name = city.name;
this.entity.selected.postcode.code = city.code;
this.entity.selected.writeNew.postcode = true;
console.log('writeNew.postcode true, in listenInputSearch');
}
},
splitCity(city) {
@ -142,6 +147,7 @@ export default {
addPostcode() {
console.log('addPostcode: pass here ?? never, it seems');
this.entity.selected.writeNew.postcode = true;
console.log('writeNew.postcode true, in addPostcode');
}
}
};

View File

@ -1,47 +0,0 @@
<template>
<div class="container">
<VueMultiselect
v-model="value"
@select="selectAddress"
name="field"
track-by="id"
label="value"
:custom-label="transName"
:multiple="false"
:placeholder="$t('select_address')"
:options="addresses">
</VueMultiselect>
</div>
</template>
<script>
import VueMultiselect from 'vue-multiselect';
export default {
name: 'SelectHouseholdAddress',
components: { VueMultiselect },
props: ['address'],
data() {
return {
value: null
}
},
computed: {
addresses() {
return this.address.loaded.addresses;
}
},
methods: {
transName(value) {
return `${value.text} ${value.postcode.name}`
},
selectAddress(value) {
this.address.selected.address = value;
}
}
};
</script>
<style src="vue-multiselect/dist/vue-multiselect.css"></style>

View File

@ -0,0 +1,139 @@
<template>
<div v-if="insideModal === false" class="loading">
<i v-if="flag.loading" class="fa fa-circle-o-notch fa-spin fa-2x fa-fw"></i>
<span class="sr-only">{{ $t('loading') }}</span>
</div>
<div v-if="errorMsg && errorMsg.length > 0" class="alert alert-danger">
{{ errorMsg }}
</div>
<address-render-box :address="selectedAddress"></address-render-box>
<div class="row">
<div v-if="showDateFrom" class='col-lg-6 address-valid date-since'>
<h3>{{ $t(getValidFromDateText) }}</h3>
<div class="input-group mb-3">
<span class="input-group-text" id="validFrom"><i class="fa fa-fw fa-calendar"></i></span>
<input type="date" class="form-control form-control-lg" name="validFrom"
v-bind:placeholder="$t(getValidFromDateText)"
v-model="validFrom"
aria-describedby="validFrom"
/>
</div>
</div>
<div v-if="showDateTo" class='col-lg-6 address-valid date-until'>
<h3>{{ $t(getValidToDateText) }}</h3>
<div class="input-group mb-3">
<span class="input-group-text" id="validTo"><i class="fa fa-fw fa-calendar"></i></span>
<input type="date" class="form-control form-control-lg" name="validTo"
v-bind:placeholder="$t(getValidToDateText)"
v-model="validTo"
aria-describedby="validTo"
/>
</div>
</div>
</div>
<action-buttons v-if="insideModal === false"
:options="this.options"
:defaultz="this.defaultz">
<template v-slot:before>
<slot name="before"></slot>
</template>
<template v-slot:action>
<slot name="action"></slot>
</template>
<template v-slot:after>
<slot name="after"></slot>
</template>
</action-buttons>
</template>
<script>
import { dateToISO, ISOToDate, ISOToDatetime } from 'ChillMainAssets/chill/js/date.js';
import AddressRenderBox from 'ChillMainAssets/vuejs/_components/Entity/AddressRenderBox.vue';
import ActionButtons from './ActionButtons.vue';
export default {
name: "DatePane",
components: {
AddressRenderBox,
ActionButtons
},
props: [
'context',
'options',
'defaultz',
'flag',
'entity',
'errorMsg',
'insideModal'
],
computed: {
address() {
return this.entity.address;
},
validFrom: {
set(value) {
this.entity.selected.valid.from = ISOToDate(value);
},
get() {
return dateToISO(this.entity.selected.valid.from);
}
},
validTo: {
set(value) {
this.entity.selected.valid.to = ISOToDate(value);
},
get() {
return dateToISO(this.entity.selected.valid.to);
}
},
getValidFromDateText() {
return (this.context.target.name === 'household') ? 'move_date' : 'validFrom';
},
getValidToDateText() {
return 'validTo';
},
showDateFrom() {
return !this.context.edit && this.options.useDate.validFrom;
},
showDateTo() {
return !this.context.edit && this.options.useDate.validTo;
},
selectedAddress() {
let address = {};
address['country'] = (this.entity.selected.country) ? this.entity.selected.country : null;
address['postcode'] = (this.entity.selected.postcode) ? this.entity.selected.postcode : null;
if (this.entity.selected.address) {
let number = (this.entity.selected.address.streetNumber) ? this.entity.selected.address.streetNumber : null;
let street = (this.entity.selected.address.street) ? this.entity.selected.address.street : null;
address['text'] = number + ', ' + street;
address['street'] = (this.entity.selected.address.street) ? this.entity.selected.address.street : null;
address['streetNumber'] = (this.entity.selected.address.streetNumber) ? this.entity.selected.address.streetNumber : null;
address['floor'] = (this.entity.selected.address.floor) ? this.entity.selected.address.floor : null;
address['corridor'] = (this.entity.selected.address.corridor) ? this.entity.selected.address.corridor : null;
address['steps'] = (this.entity.selected.address.steps) ? this.entity.selected.address.steps : null;
address['flat'] = (this.entity.selected.address.flat) ? this.entity.selected.address.flat : null;
address['buildingName'] = (this.entity.selected.address.buildingName) ? this.entity.selected.address.buildingName : null;
address['distribution'] = (this.entity.selected.address.distribution) ? this.entity.selected.address.distribution : null;
address['extra'] = (this.entity.selected.address.extra) ? this.entity.selected.address.extra : null;
}
if (this.entity.selected.valid) {
address['validFrom'] = (this.entity.selected.valid.from) ? dateToISO(this.entity.selected.valid.from) : null;
address['validTo'] = (this.entity.selected.valid.to) ? dateToISO(this.entity.selected.valid.to) : null;
}
return address;
}
}
}
</script>

View File

@ -2,7 +2,7 @@
<div class="address-form">
<!-- Not display in modal -->
<div v-if="insideModal == false" class="loading">
<div v-if="insideModal === false" class="loading">
<i v-if="flag.loading" class="fa fa-circle-o-notch fa-spin fa-2x fa-fw"></i>
<span class="sr-only">Loading...</span>
</div>
@ -54,25 +54,19 @@
v-bind:entity="entity">
</address-more>
<!-- Not display in modal -->
<ul v-if="insideModal == false"
class="record_actions sticky-form-buttons">
<li class="cancel">
<a class="btn btn-cancel" v-bind:href="context.backUrl">
{{ $t('back_to_the_list') }}
</a>
</li>
<li>
<a class="btn btn-cancel change-icon" @click="flag.showPane = true; flag.editPane = false;">
{{ $t('action.cancel') }}
</a>
</li>
<li>
<a class="btn btn-update" @click.prevent="$emit('closeEditPane')">
{{ $t('action.valid_and_see')}}
</a>
</li>
</ul>
<action-buttons v-if="insideModal === false"
:options="this.options"
:defaultz="this.defaultz">
<template v-slot:before>
<slot name="before"></slot>
</template>
<template v-slot:action>
<slot name="action"></slot>
</template>
<template v-slot:after>
<slot name="after"></slot>
</template>
</action-buttons>
</div>
</template>
@ -82,27 +76,29 @@ import CountrySelection from './AddAddress/CountrySelection';
import CitySelection from './AddAddress/CitySelection';
import AddressSelection from './AddAddress/AddressSelection';
import AddressMap from './AddAddress/AddressMap';
import AddressMore from './AddAddress/AddressMore'
import AddressMore from './AddAddress/AddressMore';
import ActionButtons from './ActionButtons.vue';
export default {
name: "EditAddressPane",
name: "EditPane",
components: {
CountrySelection,
CitySelection,
AddressSelection,
AddressMap,
AddressMore
AddressMore,
ActionButtons
},
props: [
'context',
'options',
'default',
'defaultz',
'flag',
'entity',
'errorMsg',
'insideModal'
],
emits: ['closeEditPane', 'getCities', 'getReferenceAddresses'],
emits: ['getCities', 'getReferenceAddresses'],
data() {
return {
value: false
@ -149,11 +145,7 @@ export default {
</script>
<style lang="scss">
div.address-form {
h4.h3 {
font-weight: bold;
}
div#address_map {
height: 400px;
width: 100%;

View File

@ -1,101 +0,0 @@
<template>
<div v-if="insideModal == false" class="loading">
<i v-if="flag.loading" class="fa fa-circle-o-notch fa-spin fa-2x fa-fw"></i>
<span class="sr-only">{{ $t('loading') }}</span>
</div>
<div v-if="errorMsg && errorMsg.length > 0" class="alert alert-danger">
{{ errorMsg }}
</div>
<div v-if="flag.success" class="alert alert-success">
{{ $t(getSuccessText) }}
</div>
<address-render-box :address="address"></address-render-box>
<div v-if="showDateFrom" class='address-valid date-since'>
<h3>{{ $t(getValidFromDateText) }}</h3>
<div class="input-group mb-3">
<span class="input-group-text" id="validFrom"><i class="fa fa-fw fa-calendar"></i></span>
<input type="date" class="form-control form-control-lg" name="validFrom"
v-bind:placeholder="$t(getValidFromDateText)"
v-model="validFrom"
aria-describedby="validFrom"
/>
</div>
</div>
<ul v-if="insideModal == false"
class="record_actions sticky-form-buttons">
<li class="cancel">
<a class="btn btn-cancel" v-bind:href="context.backUrl">
{{ $t('back_to_the_list') }}</a>
</li>
<li>
<a @click.prevent="$emit('openEditPane')"
class="btn btn-update">
{{ $t('action.edit')}}
</a>
</li>
<li>
<a class="btn btn-save"
@click.prevent="$emit('submitAddress')">
{{ $t('action.save')}}
</a>
</li>
</ul>
</template>
<script>
import { dateToISO, ISOToDate, ISOToDatetime } from 'ChillMainAssets/chill/js/date.js';
import AddressRenderBox from 'ChillMainAssets/vuejs/_components/Entity/AddressRenderBox.vue';
export default {
name: 'ShowAddressPane',
components: {
AddressRenderBox
},
props: [
'context',
'options',
'default',
'flag',
'entity',
'valid',
'errorMsg',
'insideModal'
],
emits: ['openEditPane', 'submitAddress'], //?
computed: {
address() {
return this.entity.address;
},
validFrom: {
set(value) {
this.valid.from = ISOToDate(value);
},
get() {
return dateToISO(this.valid.from);
}
},
getValidFromDateText() {
return (this.context.entity.type === 'household') ? 'move_date' : 'validFrom';
},
getSuccessText() {
switch (this.context.entity.type) {
/*
case 'household':
return (this.context.edit) ? 'household_address_edit_success' : 'household_address_move_success';
case 'person':
return (this.context.edit) ? 'person_address_edit_success' : 'person_address_creation_success';
*/
default:
return (this.context.edit) ? 'address_edit_success' : 'address_new_success';
}
},
showDateFrom() {
return !this.context.edit && !this.options.hideDateFrom;
}
}
};
</script>

View File

@ -0,0 +1,101 @@
<template>
<div v-if="!hideAddress">
<div class="loading">
<i v-if="flag.loading" class="fa fa-circle-o-notch fa-spin fa-2x fa-fw"></i>
<span class="sr-only">{{ $t('loading') }}</span>
</div>
<div v-if="errorMsg && errorMsg.length > 0" class="alert alert-danger">
{{ errorMsg }}
</div>
<div v-if="flag.success" class="alert alert-success">
{{ $t(getSuccessText) }}
<span v-if="forceRedirect">{{ $t('wait_redirection') }}</span>
</div>
<div v-if="noAddressWithStickyActions" class="mt-5">
<p class="chill-no-data-statement">
{{ $t('not_yet_address') }}
</p>
</div>
<address-render-box :address="address" :useDatePane="useDatePane"></address-render-box>
</div>
<action-buttons
:options="this.options"
:defaultz="this.defaultz">
<template v-slot:action>
<li>
<button @click.prevent="$emit('openEditPane')"
class="btn" :class="getClassButton"
type="button" name="button" :title="$t(getTextButton)">
<span v-if="displayTextButton">{{ $t(getTextButton) }}</span>
</button>
</li>
</template>
</action-buttons>
</template>
<script>
import AddressRenderBox from 'ChillMainAssets/vuejs/_components/Entity/AddressRenderBox.vue';
import ActionButtons from './ActionButtons.vue';
export default {
name: 'ShowPane',
components: {
AddressRenderBox,
ActionButtons
},
props: [
'context',
'options',
'defaultz',
'flag',
'entity',
'errorMsg',
'useDatePane'
],
emits: ['openEditPane'],
computed: {
address() {
return this.entity.address;
},
displayTextButton() {
return (typeof this.options.button !== 'undefined' && typeof this.options.button.displayText !== 'undefined') ?
this.options.button.displayText : this.defaultz.button.displayText;
},
getClassButton() {
let type = (this.context.edit) ? this.defaultz.button.type.edit : this.defaultz.button.type.create;
let size = (typeof this.options.button !== 'undefined' && this.options.button.size !== null) ?
`${this.options.button.size} ` : '';
return `${size}${type}`;
},
getTextButton() {
if ( typeof this.options.button.text !== 'undefined'
&& ( this.options.button.text.edit !== null
|| this.options.button.text.create !== null
)) {
return (this.context.edit) ? this.options.button.text.edit : this.options.button.text.create;
}
return (this.context.edit) ? this.defaultz.button.text.edit : this.defaultz.button.text.create;
},
getSuccessText() {
return (this.context.edit) ? 'address_edit_success' : 'address_new_success';
},
hideAddress() {
return (typeof this.options.hideAddress !== 'undefined') ?
this.options.hideAddress : this.defaultz.hideAddress;
},
forceRedirect() {
return (!(this.context.backUrl === null || typeof this.context.backUrl === 'undefined'));
},
noAddressWithStickyActions() {
return !this.context.edit && !this.address.id && this.options.stickyActions;
}
}
};
</script>

View File

@ -0,0 +1,76 @@
<template>
<div v-if="insideModal === false" class="loading">
<i v-if="flag.loading" class="fa fa-circle-o-notch fa-spin fa-2x fa-fw"></i>
<span class="sr-only">{{ $t('loading') }}</span>
</div>
<div v-if="errorMsg && errorMsg.length > 0" class="alert alert-danger">{{ errorMsg }}</div>
<h4 class="h3">{{ $t('address_suggestions') }}</h4>
<div class="flex-table AddressSuggestionList">
<div class="item-bloc">
<div class="float-button bottom">
<div class="box">
<div class="action">
<ul class="record_actions">
<li>
<button class="btn btn-sm btn-choose">
{{ $t('use_this_address') }}
</button>
</li>
</ul>
</div>
<ul class="list-content fa-ul">
<li>
<i class="fa fa-li fa-map-marker"></i>
<!--
<address-render-box></address-render-box>
-->
</li>
</ul>
</div>
</div>
</div>
</div>
<action-buttons v-if="insideModal === false"
:options="this.options"
:defaultz="this.defaultz">
<template v-slot:before>
<slot name="before"></slot>
</template>
<template v-slot:action>
<slot name="action"></slot>
</template>
<template v-slot:after>
<slot name="after"></slot>
</template>
</action-buttons>
</template>
<script>
import AddressRenderBox from 'ChillMainAssets/vuejs/_components/Entity/AddressRenderBox.vue';
import ActionButtons from './ActionButtons.vue';
export default {
name: "SuggestPane",
components: {
AddressRenderBox,
ActionButtons
},
props: [
'context',
'options',
'defaultz',
'flag',
'entity',
'errorMsg',
'insideModal'
],
computed: {},
methods: {}
}
</script>

View File

@ -22,7 +22,7 @@ const addressMessages = {
corridor: 'Couloir',
steps: 'Escalier',
flat: 'Appartement',
buildingName: 'Nom du bâtiment',
buildingName: 'Résidence',
extra: 'Complément d\'adresse',
distribution: 'Cedex',
create_postal_code: 'Localité inconnue. Cliquez ici pour créer une nouvelle localité',
@ -30,22 +30,18 @@ const addressMessages = {
postalCode_code: 'Code postal',
date: "Date de la nouvelle adresse",
validFrom: "L'adresse est valable à partir du",
validTo: "L'adresse est valable jusqu'au",
back_to_the_list: 'Retour à la liste',
loading: 'chargement en cours...',
address_new_success: 'La nouvelle adresse est enregistrée',
address_edit_success: 'L\'adresse a été mise à jour',
// person specific
add_an_address_to_person: 'Ajouter l\'adresse à la personne',
person_address_creation_success: 'La nouvelle adresse de la personne est enregistrée',
person_address_edit_success: 'L\'adresse de la personne a été mise à jour',
address_suggestions: "Suggestion d'adresses",
address_new_success: 'La nouvelle adresse est enregistrée.',
address_edit_success: 'L\'adresse a été mise à jour.',
wait_redirection: " La page est redirigée.",
not_yet_address: "Il n'y a pas encore d'adresse. Cliquez sur '+ Créer une adresse'",
use_this_address: "Utiliser cette adresse",
// household specific
move_date: 'Date du déménagement',
select_a_existing_address: 'Sélectionner une adresse existante',
add_an_address_to_household: 'Enregistrer',
household_address_move_success: 'La nouvelle adresse du ménage est enregistrée',
household_address_edit_success: 'L\'adresse du ménage a été mise à jour',
}
};

View File

@ -1,13 +1,67 @@
import { createApp } from 'vue';
import App from './App.vue';
import { _createI18n } from 'ChillMainAssets/vuejs/_js/i18n';
import { addressMessages } from './i18n';
import App from './App.vue';
const i18n = _createI18n( addressMessages );
const app = createApp({
template: `<app></app>`,
})
.use(i18n)
.component('app', App)
.mount('#address');
let containers = document.querySelectorAll('.address-container');
containers.forEach((container) => {
const app = createApp({
template: `<app v-bind:addAddress="this.addAddress" ></app>`,
data() {
return {
addAddress: {
context: {
target: {
name: container.dataset.targetName,
id: parseInt(container.dataset.targetId)
},
edit: container.dataset.mode === 'edit', //boolean
addressId: parseInt(container.dataset.addressId) || null,
backUrl: container.dataset.backUrl || null
},
options: {
/// Options override default.
/// null value take default component value defined in AddAddress data()
button: {
text: {
create: container.dataset.buttonText || null,
edit: container.dataset.buttonText || null
},
size: container.dataset.buttonSize || null,
displayText: container.dataset.buttonDisplayText !== 'false' //boolean, default: true
},
/// Modal title text if create or edit address (trans chain, see i18n)
title: {
create: container.dataset.modalTitle || null,
edit: container.dataset.modalTitle || null
},
/// Display panes in Modal for step123
openPanesInModal: container.dataset.openPanesInModal !== 'false', //boolean, default: true
/// Display actions buttons of panes in a sticky-form-button navbar
stickyActions: container.dataset.stickyActions === 'true', //boolean, default: false
/// Use Date fields
useDate: {
validFrom: container.dataset.useValidFrom === 'true', //boolean, default: false
validTo: container.dataset.useValidTo === 'true' //boolean, default: false
},
/// Don't display show renderbox Address: showPane display only a button
hideAddress: container.dataset.hideAddress === 'true' //boolean, default: false
}
}
}
}
})
.use(i18n)
.component('app', App)
.mount(container);
//console.log('container dataset', container.dataset);
});

View File

@ -16,7 +16,7 @@
</p>
</component>
<div v-if="isMultiline === true">
<div v-if="isMultiline === true" class="address-more">
<div v-if="address.floor">
<span class="floor">
<b>{{ $t('floor') }}</b>: {{ address.floor }}
@ -54,6 +54,19 @@
</div>
</div>
<div v-if="useDatePane === true" class="address-more">
<div v-if="address.validFrom">
<span class="validFrom">
<b>{{ $t('validFrom') }}</b>: {{ $d(address.validFrom.date) }}
</span>
</div>
<div v-if="address.validTo">
<span class="validTo">
<b>{{ $t('validTo') }}</b>: {{ $d(address.validTo.date) }}
</span>
</div>
</div>
</component>
</template>
@ -67,6 +80,10 @@ export default {
isMultiline: {
default: true,
type: Boolean
},
useDatePane: {
default: false,
type: Boolean
}
},
computed: {

View File

@ -76,7 +76,9 @@ const messages = {
birthday: {
man: "Né le",
woman: "Née le"
} ,
},
deathdate: "Date de décès",
years_old: "ans",
household_without_address: "Le ménage de l'usager est sans adresse",
no_data: "Aucune information renseignée",
type: {

View File

@ -0,0 +1,78 @@
{#
This Twig template include load vue_address component.
It push all variables from context in Address/App.vue.
OPTIONS
* targetEntity {
name: string
id: integer
}
* mode string ['edit*'|'new']
* addressId integer
* backUrl twig route: path('route', {parameters})
* modalTitle twig translated chain
* buttonText twig translated chain
* buttonSize bootstrap class like 'btn-sm'
* buttonDisplayText bool (default: true)
* openPanesInModal bool (default: true)
* stickyActions bool (default: false)
* useValidFrom bool (default: false)
* useValidTo bool (default: false)
* hideAddress bool (default: false)
#}
<div class="address-container"
data-target-name="{{ targetEntity.name|e('html_attr') }}"
data-target-id="{{ targetEntity.id|e('html_attr') }}"
{% if 'edit' in app.request.get('_route') %}
data-mode="edit"
data-address-id="{{ app.request.get('address_id')|e('html_attr') }}"
{% elseif mode is defined and mode == 'edit' %}
data-mode="edit"
data-address-id="{{ addressId|e('html_attr') }}"
{% endif %}
{% if backUrl is defined %}
data-back-url="{{ backUrl|e('html_attr') }}"
{% endif %}
{% if modalTitle is defined %}
data-modal-title="{{ modalTitle|trans|e('html_attr') }}"
{% endif %}
{% if buttonText is defined %}
data-button-text="{{ buttonText|trans|e('html_attr') }}"
{% endif %}
{% if buttonSize is defined %}
data-button-size="{{ buttonSize|e('html_attr') }}"
{% endif %}
{% if buttonDisplayText is defined and buttonDisplayText != 1 %}
data-button-display-text="false"
{% endif %}
{% if openPanesInModal is defined and openPanesInModal != 1 %}
data-open-panes-in-modal="false"
{% endif %}
{% if stickyActions is defined and stickyActions == 1 %}
data-sticky-actions="true"
{% endif %}
{% if useValidFrom is defined and useValidFrom == 1 %}
data-use-valid-from="true"
{% endif %}
{% if useValidTo is defined and useValidTo == 1 %}
data-use-valid-to="true"
{% endif %}
{% if hideAddress is defined and hideAddress == 1 %}
data-hide-address="true"
{% endif %}
></div>
{{ encore_entry_script_tags('vue_address') }}
{{ encore_entry_link_tags('vue_address') }}

View File

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

View File

@ -34,25 +34,25 @@
{% macro extended(address, options) %}
{% if address.floor is not empty %}
<span class="floor">{{ address.floor }}</span>
<span class="floor">{{ 'address more.floor'|trans }} {{ address.floor }}</span>
{% endif %}
{% if address.corridor is not empty %}
<span class="corridor">{{ address.corridor }}</span>
<span class="corridor">{{ 'address more.corridor'|trans }} {{ address.corridor }}</span>
{% endif %}
{% if address.steps is not empty %}
<span class="steps">{{ address.steps }}</span>
<span class="steps">{{ 'address more.steps'|trans }} {{ address.steps }}</span>
{% endif %}
{% if address.buildingName is not empty %}
<span class="buildingName">{{ address.buildingName }}</span>
<span class="buildingName">{{ 'address more.buildingName'|trans }} {{ address.buildingName }}</span>
{% endif %}
{% if address.flat is not empty %}
<span class="flat">{{ address.flat }}</span>
{% endif %}
{% if address.distribution is not empty %}
<span class="distribution">{{ address.distribution }}</span>
<span class="flat">{{ 'address more.flat'|trans }} {{ address.flat }}</span>
{% endif %}
{% if address.extra is not empty %}
<span class="extra">{{ address.extra }}</span>
<span class="extra">{{ 'address more.extra'|trans }} {{ address.extra }}</span>
{% endif %}
{% if address.distribution is not empty %}
<span class="distribution">{{ 'address more.distribution'|trans }} {{ address.distribution }}</span>
{% endif %}
{% endmacro %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,12 +13,15 @@ class AddressNormalizer implements NormalizerAwareInterface, NormalizerInterface
public function normalize($address, string $format = null, array $context = [])
{
/** @var Address $address */
$data['address_id'] = $address->getId();
$data['text'] = $address->isNoAddress() ? '' : $address->getStreetNumber().', '.$address->getStreet();
$data['street'] = $address->getStreet();
$data['streetNumber'] = $address->getStreetNumber();
$data['postcode']['id'] = $address->getPostCode()->getId();
$data['postcode']['name'] = $address->getPostCode()->getName();
$data['postcode']['code'] = $address->getPostCode()->getCode();
$data['country']['id'] = $address->getPostCode()->getCountry()->getId();
$data['country']['name'] = $address->getPostCode()->getCountry()->getName();
$data['country']['code'] = $address->getPostCode()->getCountry()->getCountryCode();
$data['floor'] = $address->getFloor();
@ -28,6 +31,8 @@ class AddressNormalizer implements NormalizerAwareInterface, NormalizerInterface
$data['buildingName'] = $address->getBuildingName();
$data['distribution'] = $address->getDistribution();
$data['extra'] = $address->getExtra();
$data['validFrom'] = $address->getValidFrom();
$data['validTo'] = $address->getValidTo();
return $data;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -74,6 +74,15 @@ address:
address_homeless: L'adresse est-elle celle d'un domicile fixe ?
real address: Adresse d'un domicile
consider homeless: N'est pas l'adresse d'un domicile (SDF)
address more:
floor: ét
corridor: coul
steps: esc
flat: appart
buildingName: résidence
extra: ''
distribution: cedex
Create a new address: Créer une nouvelle adresse
#serach
Your search is empty. Please provide search terms.: La recherche est vide. Merci de fournir des termes de recherche.
@ -177,6 +186,7 @@ Exports list: Liste des exports
Create an export: Créer un export
#export creation step 'center' : pick a center
Pick centers: Choisir les centres
Pick a center: Choisir un centre
The export will contains only data from the picked centers.: L'export ne contiendra que les données des centres choisis.
This will eventually restrict your possibilities in filtering the data.: Les possibilités de filtrages seront adaptées aux droits de consultation pour les centres choisis.
Go to export options: Vers la préparation de l'export

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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