mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
adaptations for acl with tasks
This commit is contained in:
parent
bae06fcc9c
commit
965ea528e3
@ -90,6 +90,10 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface,
|
||||
$configuration = $this->getConfiguration($configs, $container);
|
||||
$config = $this->processConfiguration($configuration, $configs);
|
||||
|
||||
// replace all config with a main key:
|
||||
$container->setParameter('chill_main', $config);
|
||||
|
||||
// legacy config
|
||||
$container->setParameter('chill_main.installation_name',
|
||||
$config['installation_name']);
|
||||
|
||||
|
@ -97,6 +97,14 @@ class Configuration implements ConfigurationInterface
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->arrayNode('acl')
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
->booleanNode('form_show_scopes')
|
||||
->defaultTrue()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->arrayNode('redis')
|
||||
->children()
|
||||
->scalarNode('host')
|
||||
|
@ -23,7 +23,9 @@ use Chill\MainBundle\Entity\Scope;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Form\DataMapper\ScopePickerDataMapper;
|
||||
use Chill\MainBundle\Repository\ScopeRepository;
|
||||
use Chill\MainBundle\Repository\UserACLAwareRepositoryInterface;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
@ -36,6 +38,7 @@ use Symfony\Component\OptionsResolver\Options;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Core\Role\Role;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
/**
|
||||
* Allow to pick amongst available scope for the current
|
||||
@ -46,14 +49,10 @@ use Symfony\Component\Security\Core\Role\Role;
|
||||
* - `center`: the center of the entity
|
||||
* - `role` : the role of the user
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
class ScopePickerType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* @var AuthorizationHelper
|
||||
*/
|
||||
protected $authorizationHelper;
|
||||
protected AuthorizationHelperInterface $authorizationHelper;
|
||||
|
||||
/**
|
||||
* @var TokenStorageInterface
|
||||
@ -70,22 +69,26 @@ class ScopePickerType extends AbstractType
|
||||
*/
|
||||
protected $translatableStringHelper;
|
||||
|
||||
protected Security $security;
|
||||
|
||||
public function __construct(
|
||||
AuthorizationHelper $authorizationHelper,
|
||||
AuthorizationHelperInterface $authorizationHelper,
|
||||
TokenStorageInterface $tokenStorage,
|
||||
ScopeRepository $scopeRepository,
|
||||
Security $security,
|
||||
TranslatableStringHelper $translatableStringHelper
|
||||
) {
|
||||
$this->authorizationHelper = $authorizationHelper;
|
||||
$this->tokenStorage = $tokenStorage;
|
||||
$this->scopeRepository = $scopeRepository;
|
||||
$this->security = $security;
|
||||
$this->translatableStringHelper = $translatableStringHelper;
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$query = $this->buildAccessibleScopeQuery($options['center'], $options['role']);
|
||||
$items = $query->getQuery()->execute();
|
||||
$items = $this->authorizationHelper->getReachableScopes($this->security->getUser(),
|
||||
$options['role'], $options['center']);
|
||||
|
||||
if (1 !== count($items)) {
|
||||
$builder->add('scope', EntityType::class, [
|
||||
@ -94,9 +97,7 @@ class ScopePickerType extends AbstractType
|
||||
'choice_label' => function (Scope $c) {
|
||||
return $this->translatableStringHelper->localize($c->getName());
|
||||
},
|
||||
'query_builder' => function () use ($options) {
|
||||
return $this->buildAccessibleScopeQuery($options['center'], $options['role']);
|
||||
},
|
||||
'choices' => $items,
|
||||
]);
|
||||
$builder->setDataMapper(new ScopePickerDataMapper());
|
||||
} else {
|
||||
@ -121,19 +122,22 @@ class ScopePickerType extends AbstractType
|
||||
$resolver
|
||||
// create `center` option
|
||||
->setRequired('center')
|
||||
->setAllowedTypes('center', [Center::class])
|
||||
->setAllowedTypes('center', [Center::class, 'array', 'null'])
|
||||
// create ``role` option
|
||||
->setRequired('role')
|
||||
->setAllowedTypes('role', ['string', Role::class]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Center|array|Center[] $center
|
||||
* @param string $role
|
||||
* @return \Doctrine\ORM\QueryBuilder
|
||||
*/
|
||||
protected function buildAccessibleScopeQuery(Center $center, Role $role)
|
||||
protected function buildAccessibleScopeQuery($center, $role)
|
||||
{
|
||||
$roles = $this->authorizationHelper->getParentRoles($role);
|
||||
$roles[] = $role;
|
||||
$centers = $center instanceof Center ? [$center]: $center;
|
||||
|
||||
$qb = $this->scopeRepository->createQueryBuilder('s');
|
||||
$qb
|
||||
@ -142,8 +146,8 @@ class ScopePickerType extends AbstractType
|
||||
->join('rs.permissionsGroups', 'pg')
|
||||
->join('pg.groupCenters', 'gc')
|
||||
// add center constraint
|
||||
->where($qb->expr()->eq('IDENTITY(gc.center)', ':center'))
|
||||
->setParameter('center', $center->getId())
|
||||
->where($qb->expr()->in('IDENTITY(gc.center)', ':centers'))
|
||||
->setParameter('centers', \array_map(fn(Center $c) => $c->getId(), $centers))
|
||||
// role constraints
|
||||
->andWhere($qb->expr()->in('rs.role', ':roles'))
|
||||
->setParameter('roles', $roles)
|
||||
|
@ -17,6 +17,8 @@
|
||||
*/
|
||||
namespace Chill\MainBundle\Form\Type;
|
||||
|
||||
use Chill\MainBundle\Entity\Scope;
|
||||
use Chill\MainBundle\Repository\UserACLAwareRepositoryInterface;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
@ -56,14 +58,18 @@ class UserPickerType extends AbstractType
|
||||
|
||||
protected UserRepository $userRepository;
|
||||
|
||||
protected UserACLAwareRepositoryInterface $userACLAwareRepository;
|
||||
|
||||
public function __construct(
|
||||
AuthorizationHelper $authorizationHelper,
|
||||
TokenStorageInterface $tokenStorage,
|
||||
UserRepository $userRepository
|
||||
UserRepository $userRepository,
|
||||
UserACLAwareRepositoryInterface $userACLAwareRepository
|
||||
) {
|
||||
$this->authorizationHelper = $authorizationHelper;
|
||||
$this->tokenStorage = $tokenStorage;
|
||||
$this->userRepository = $userRepository;
|
||||
$this->userACLAwareRepository = $userACLAwareRepository;
|
||||
}
|
||||
|
||||
|
||||
@ -72,7 +78,7 @@ class UserPickerType extends AbstractType
|
||||
$resolver
|
||||
// create `center` option
|
||||
->setRequired('center')
|
||||
->setAllowedTypes('center', [\Chill\MainBundle\Entity\Center::class ])
|
||||
->setAllowedTypes('center', [\Chill\MainBundle\Entity\Center::class, 'null', 'array' ])
|
||||
// create ``role` option
|
||||
->setRequired('role')
|
||||
->setAllowedTypes('role', ['string', \Symfony\Component\Security\Core\Role\Role::class ])
|
||||
@ -86,10 +92,12 @@ class UserPickerType extends AbstractType
|
||||
->setDefault('choice_label', function(User $u) {
|
||||
return $u->getUsername();
|
||||
})
|
||||
->setDefault('scope', null)
|
||||
->setAllowedTypes('scope', [Scope::class, 'array', 'null'])
|
||||
->setNormalizer('choices', function(Options $options) {
|
||||
|
||||
$users = $this->authorizationHelper
|
||||
->findUsersReaching($options['role'], $options['center']);
|
||||
$users = $this->userACLAwareRepository
|
||||
->findUsersByReachedACL($options['role'], $options['center'], $options['scope'], true);
|
||||
|
||||
if (NULL !== $options['having_permissions_group_flag']) {
|
||||
return $this->userRepository
|
||||
|
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\MainBundle\Repository;
|
||||
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Entity\Scope;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
||||
|
||||
class UserACLAwareRepository implements UserACLAwareRepositoryInterface
|
||||
{
|
||||
|
||||
private AuthorizationHelper $authorizationHelper;
|
||||
|
||||
public function findUsersByReachedACL(string $role, $center, $scope = null, bool $onlyEnabled = true): array
|
||||
{
|
||||
$parents = $this->authorizationHelper->getParentRoles($role);
|
||||
$parents[] = $role;
|
||||
|
||||
$qb = $this->em->createQueryBuilder();
|
||||
|
||||
$qb
|
||||
->select('u')
|
||||
->from(User::class, 'u')
|
||||
->join('u.groupCenters', 'gc')
|
||||
->join('gc.permissionsGroup', 'pg')
|
||||
->join('pg.roleScopes', 'rs')
|
||||
->where($qb->expr()->in('rs.role', $parents))
|
||||
;
|
||||
|
||||
if ($onlyEnabled) {
|
||||
$qb->andWhere($qb->expr()->eq('u.enabled', "'TRUE'"));
|
||||
}
|
||||
|
||||
if (NULL !== $center) {
|
||||
$centers = $center instanceof Center ? [$center] : $center;
|
||||
$qb
|
||||
->andWhere($qb->expr()->in('gc.center', ':centers'))
|
||||
->setParameter('centers', $centers)
|
||||
;
|
||||
}
|
||||
|
||||
if (NULL !== $scope) {
|
||||
$scopes = $scope instanceof Scope ? [$scope] : $scope;
|
||||
$qb
|
||||
->andWhere($qb->expr()->in('rs.scope', ':scopes'))
|
||||
->setParameter('scopes', $scopes)
|
||||
;
|
||||
}
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\MainBundle\Repository;
|
||||
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Entity\Scope;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
|
||||
interface UserACLAwareRepositoryInterface
|
||||
{
|
||||
/**
|
||||
* Find the users reaching the given center and scope, for the given role
|
||||
*
|
||||
* @param string $role
|
||||
* @param Center|Center[]|array $center
|
||||
* @param Scope|Scope[]|array|null $scope
|
||||
* @param bool $onlyActive true if get only active users
|
||||
*
|
||||
* @return User[]
|
||||
*/
|
||||
public function findUsersByReachedACL(string $role, $center, $scope = null, bool $onlyActive = true): array;
|
||||
}
|
@ -8,7 +8,7 @@
|
||||
|
||||
{{ form_start(form) }}
|
||||
|
||||
<ul class="record_actions">
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li class="cancel">
|
||||
<a href="{{ path(cancel_route, cancel_parameters|default( { } ) ) }}" class="btn btn-cancel">
|
||||
{{ 'Cancel'|trans }}
|
||||
|
@ -23,6 +23,8 @@ use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Entity\HasCenterInterface;
|
||||
use Chill\MainBundle\Entity\HasScopeInterface;
|
||||
use Chill\MainBundle\Repository\UserACLAwareRepository;
|
||||
use Chill\MainBundle\Repository\UserACLAwareRepositoryInterface;
|
||||
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher;
|
||||
use Chill\MainBundle\Security\Resolver\ScopeResolverDispatcher;
|
||||
use Chill\MainBundle\Security\Resolver\ScopeResolverInterface;
|
||||
@ -62,6 +64,8 @@ class AuthorizationHelper implements AuthorizationHelperInterface
|
||||
|
||||
protected LoggerInterface $logger;
|
||||
|
||||
private UserACLAwareRepositoryInterface $userACLAwareRepository;
|
||||
|
||||
public function __construct(
|
||||
RoleHierarchyInterface $roleHierarchy,
|
||||
ParameterBagInterface $parameterBag,
|
||||
@ -320,33 +324,15 @@ class AuthorizationHelper implements AuthorizationHelperInterface
|
||||
|
||||
/**
|
||||
*
|
||||
* @deprecated use UserACLAwareRepositoryInterface::findUsersByReachedACL instead
|
||||
* @param Center|Center[]|array $center
|
||||
* @param Scope|Scope[]|array|null $scope
|
||||
* @param bool $onlyActive true if get only active users
|
||||
* @return User[]
|
||||
*/
|
||||
public function findUsersReaching(string $role, Center $center, Scope $circle = null): array
|
||||
public function findUsersReaching(string $role, $center, $scope = null, bool $onlyEnabled = true): array
|
||||
{
|
||||
$parents = $this->getParentRoles($role);
|
||||
$parents[] = $role;
|
||||
|
||||
$qb = $this->em->createQueryBuilder();
|
||||
$qb
|
||||
->select('u')
|
||||
->from(User::class, 'u')
|
||||
->join('u.groupCenters', 'gc')
|
||||
->join('gc.permissionsGroup', 'pg')
|
||||
->join('pg.roleScopes', 'rs')
|
||||
->where('gc.center = :center')
|
||||
->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();
|
||||
return $this->userACLAwareRepository->findUsersByReachedACL($role, $center, $scope, $onlyEnabled);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -11,6 +11,8 @@ services:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
||||
Chill\MainBundle\Repository\UserACLAwareRepositoryInterface: '@Chill\MainBundle\Repository\UserACLAwareRepository'
|
||||
|
||||
Chill\MainBundle\Serializer\Normalizer\:
|
||||
resource: '../Serializer/Normalizer'
|
||||
autoconfigure: true
|
||||
|
@ -3,9 +3,12 @@
|
||||
namespace Chill\TaskBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\Entity\Scope;
|
||||
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher;
|
||||
use Chill\MainBundle\Security\Resolver\CenterResolverInterface;
|
||||
use Chill\PersonBundle\Privacy\PrivacyEvent;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@ -24,7 +27,6 @@ use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
||||
use Chill\PersonBundle\Repository\PersonRepository;
|
||||
use Chill\TaskBundle\Event\TaskEvent;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Translation\TranslatorInterface;
|
||||
use Chill\TaskBundle\Event\UI\UIEvent;
|
||||
use Chill\MainBundle\Repository\CenterRepository;
|
||||
use Chill\MainBundle\Timeline\TimelineBuilder;
|
||||
@ -33,6 +35,7 @@ use Chill\PersonBundle\Repository\AccompanyingPeriodRepository;
|
||||
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface as TranslationTranslatorInterface;
|
||||
|
||||
/**
|
||||
@ -40,53 +43,34 @@ use Symfony\Contracts\Translation\TranslatorInterface as TranslationTranslatorIn
|
||||
*
|
||||
* @package Chill\TaskBundle\Controller
|
||||
*/
|
||||
class SingleTaskController extends AbstractController
|
||||
final class SingleTaskController extends AbstractController
|
||||
{
|
||||
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
protected $eventDispatcher;
|
||||
private EventDispatcherInterface $eventDispatcher;
|
||||
private TimelineBuilder $timelineBuilder;
|
||||
private LoggerInterface $logger;
|
||||
private CenterResolverDispatcher $centerResolverDispatcher;
|
||||
private TranslatorInterface $translator;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var TimelineBuilder
|
||||
*/
|
||||
protected $timelineBuilder;
|
||||
|
||||
/**
|
||||
* @var LoggerInterface
|
||||
*/
|
||||
protected $logger;
|
||||
|
||||
/**
|
||||
* @var RequestStack
|
||||
*/
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* SingleTaskController constructor.
|
||||
*
|
||||
* @param EventDispatcherInterface $eventDispatcher
|
||||
*/
|
||||
public function __construct(
|
||||
CenterResolverDispatcher $centerResolverDispatcher,
|
||||
TranslatorInterface $translator,
|
||||
EventDispatcherInterface $eventDispatcher,
|
||||
TimelineBuilder $timelineBuilder,
|
||||
LoggerInterface $logger,
|
||||
RequestStack $requestStack
|
||||
LoggerInterface $logger
|
||||
) {
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
$this->timelineBuilder = $timelineBuilder;
|
||||
$this->logger = $logger;
|
||||
$this->request = $requestStack->getCurrentRequest();
|
||||
$this->translator = $translator;
|
||||
$this->centerResolverDispatcher = $centerResolverDispatcher;
|
||||
}
|
||||
|
||||
|
||||
private function getEntityContext()
|
||||
private function getEntityContext(Request $request)
|
||||
{
|
||||
if($this->request->query->has('person_id')){
|
||||
if ($request->query->has('person_id')) {
|
||||
return 'person';
|
||||
} else if ($this->request->query->has('course_id')) {
|
||||
} else if ($request->query->has('course_id')) {
|
||||
return 'course';
|
||||
} else {
|
||||
return null;
|
||||
@ -100,28 +84,27 @@ class SingleTaskController extends AbstractController
|
||||
* name="chill_task_single_task_new"
|
||||
* )
|
||||
*/
|
||||
public function newAction(
|
||||
TranslationTranslatorInterface $translator
|
||||
) {
|
||||
public function newAction(Request $request) {
|
||||
|
||||
$task = (new SingleTask())
|
||||
->setAssignee($this->getUser())
|
||||
->setType('task_default')
|
||||
;
|
||||
|
||||
$entityType = $this->getEntityContext();
|
||||
$entityType = $this->getEntityContext($request);
|
||||
|
||||
if ($entityType !== null) {
|
||||
|
||||
$entityId = $this->request->query->getInt("{$entityType}_id", 0); // sf4 check:
|
||||
// prevent error: `Argument 2 passed to ::getInt() must be of the type int, null given`
|
||||
|
||||
if ($entityId === null) {
|
||||
return new Response("You must provide a {$entityType}_id", Response::HTTP_BAD_REQUEST);
|
||||
if (NULL === $entityType) {
|
||||
throw new BadRequestHttpException("You must provide a entity_type");
|
||||
}
|
||||
|
||||
if($entityType === 'person')
|
||||
{
|
||||
$entityId = $request->query->getInt("{$entityType}_id", 0);
|
||||
|
||||
if ($entityId === null) {
|
||||
return new BadRequestHttpException("You must provide a {$entityType}_id");
|
||||
}
|
||||
|
||||
switch ($entityType) {
|
||||
case 'person':
|
||||
$person = $this->getDoctrine()->getManager()
|
||||
->getRepository(Person::class)
|
||||
->find($entityId);
|
||||
@ -131,11 +114,8 @@ class SingleTaskController extends AbstractController
|
||||
}
|
||||
|
||||
$task->setPerson($person);
|
||||
}
|
||||
|
||||
if($entityType === 'course')
|
||||
{
|
||||
|
||||
break;
|
||||
case 'course':
|
||||
$course = $this->getDoctrine()->getManager()
|
||||
->getRepository(AccompanyingPeriod::class)
|
||||
->find($entityId);
|
||||
@ -145,20 +125,17 @@ class SingleTaskController extends AbstractController
|
||||
}
|
||||
|
||||
$task->setCourse($course);
|
||||
|
||||
break;
|
||||
default:
|
||||
return new BadRequestHttpException("context with {$entityType} is not supported");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
//TODO : resolve access rights
|
||||
|
||||
// $this->denyAccessUnlessGranted(TaskVoter::CREATE, $task, 'You are not '
|
||||
// . 'allowed to create this task');
|
||||
$this->denyAccessUnlessGranted(TaskVoter::CREATE, $task, 'You are not '
|
||||
. 'allowed to create this task');
|
||||
|
||||
$form = $this->setCreateForm($task, new Role(TaskVoter::CREATE));
|
||||
|
||||
$form->handleRequest($this->request);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted()) {
|
||||
if ($form->isValid()) {
|
||||
@ -169,44 +146,38 @@ class SingleTaskController extends AbstractController
|
||||
|
||||
$em->flush();
|
||||
|
||||
$this->addFlash('success', $translator->trans("The task is created"));
|
||||
$this->addFlash('success', $this->translator->trans("The task is created"));
|
||||
|
||||
if($entityType === 'person')
|
||||
{
|
||||
if ($entityType === 'person') {
|
||||
return $this->redirectToRoute('chill_task_singletask_list', [
|
||||
'person_id' => $task->getPerson()->getId()
|
||||
]);
|
||||
}
|
||||
|
||||
if($entityType === 'course')
|
||||
{
|
||||
} elseif ($entityType === 'course') {
|
||||
return $this->redirectToRoute('chill_task_singletask_courselist', [
|
||||
'course_id' => $task->getCourse()->getId()
|
||||
]);
|
||||
}
|
||||
|
||||
} else {
|
||||
$this->addFlash('error', $translator->trans("This form contains errors"));
|
||||
$this->addFlash('error', $this->translator->trans("This form contains errors"));
|
||||
}
|
||||
}
|
||||
|
||||
switch($this->getEntityContext()){
|
||||
switch ($entityType) {
|
||||
case 'person':
|
||||
return $this->render('@ChillTask/SingleTask/Person/new.html.twig', array(
|
||||
'form' => $form->createView(),
|
||||
'task' => $task,
|
||||
'person' => $person,
|
||||
));
|
||||
break;
|
||||
case 'course':
|
||||
return $this->render('@ChillTask/SingleTask/AccompanyingCourse/new.html.twig', array(
|
||||
'form' => $form->createView(),
|
||||
'task' => $task,
|
||||
'accompanyingCourse' => $course,
|
||||
));
|
||||
break;
|
||||
default:
|
||||
throw new \LogicException("entity context not supported");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -215,7 +186,7 @@ class SingleTaskController extends AbstractController
|
||||
* name="chill_task_single_task_show"
|
||||
* )
|
||||
*/
|
||||
public function showAction($id)
|
||||
public function showAction($id, Request $request)
|
||||
{
|
||||
|
||||
$em = $this->getDoctrine()->getManager();
|
||||
@ -299,7 +270,7 @@ class SingleTaskController extends AbstractController
|
||||
*/
|
||||
public function editAction(
|
||||
$id,
|
||||
TranslationTranslatorInterface $translator
|
||||
Request $request
|
||||
) {
|
||||
|
||||
$em = $this->getDoctrine()->getManager();
|
||||
@ -348,7 +319,7 @@ class SingleTaskController extends AbstractController
|
||||
|
||||
$form = $event->getForm();
|
||||
|
||||
$form->handleRequest($this->request);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted()) {
|
||||
if ($form->isValid()) {
|
||||
@ -357,7 +328,7 @@ class SingleTaskController extends AbstractController
|
||||
|
||||
$em->flush();
|
||||
|
||||
$this->addFlash('success', $translator
|
||||
$this->addFlash('success', $this->translator
|
||||
->trans("The task has been updated"));
|
||||
|
||||
if($task->getContext() instanceof Person){
|
||||
@ -370,16 +341,16 @@ class SingleTaskController extends AbstractController
|
||||
|
||||
return $this->redirectToRoute(
|
||||
'chill_task_singletask_list',
|
||||
$this->request->query->get('list_params', [])
|
||||
$request->query->get('list_params', [])
|
||||
);
|
||||
} else {
|
||||
return $this->redirectToRoute(
|
||||
'chill_task_singletask_courselist',
|
||||
$this->request->query->get('list_params', [])
|
||||
$request->query->get('list_params', [])
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$this->addFlash('error', $translator->trans("This form contains errors"));
|
||||
$this->addFlash('error', $this->translator->trans("This form contains errors"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -420,8 +391,7 @@ class SingleTaskController extends AbstractController
|
||||
*/
|
||||
public function deleteAction(
|
||||
Request $request,
|
||||
$id,
|
||||
TranslationTranslatorInterface $translator
|
||||
$id
|
||||
) {
|
||||
|
||||
$em = $this->getDoctrine()->getManager();
|
||||
@ -485,7 +455,7 @@ class SingleTaskController extends AbstractController
|
||||
$em->remove($task);
|
||||
$em->flush();
|
||||
|
||||
$this->addFlash('success', $translator
|
||||
$this->addFlash('success', $this->translator
|
||||
->trans("The task has been successfully removed."));
|
||||
|
||||
if($task->getContext() instanceof Person){
|
||||
@ -528,7 +498,7 @@ class SingleTaskController extends AbstractController
|
||||
protected function setCreateForm(SingleTask $task, Role $role)
|
||||
{
|
||||
$form = $this->createForm(SingleTaskType::class, $task, [
|
||||
'center' => $task->getCenter(),
|
||||
'center' => $this->centerResolverDispatcher->resolveCenter($task),
|
||||
'role' => $role,
|
||||
]);
|
||||
|
||||
@ -545,12 +515,12 @@ class SingleTaskController extends AbstractController
|
||||
* name="chill_task_single_my_tasks"
|
||||
* )
|
||||
*/
|
||||
public function myTasksAction(TranslationTranslatorInterface $translator)
|
||||
public function myTasksAction()
|
||||
{
|
||||
return $this->redirectToRoute('chill_task_singletask_list', [
|
||||
'user_id' => $this->getUser()->getId(),
|
||||
'hide_form' => true,
|
||||
'title' => $translator->trans('My tasks')
|
||||
'title' => $this->translator->trans('My tasks')
|
||||
]);
|
||||
}
|
||||
|
||||
@ -575,7 +545,8 @@ class SingleTaskController extends AbstractController
|
||||
PersonRepository $personRepository,
|
||||
AccompanyingPeriodRepository $courseRepository,
|
||||
CenterRepository $centerRepository,
|
||||
FormFactoryInterface $formFactory
|
||||
FormFactoryInterface $formFactory,
|
||||
Request $request
|
||||
) {
|
||||
/* @var $viewParams array The parameters for the view */
|
||||
/* @var $params array The parameters for the query */
|
||||
@ -589,9 +560,9 @@ class SingleTaskController extends AbstractController
|
||||
$viewParams['accompanyingCourse'] = null;
|
||||
$params['accompanyingCourse'] = null;
|
||||
|
||||
if (!empty($this->request->query->get('person_id', NULL))) {
|
||||
if (!empty($request->query->get('person_id', NULL))) {
|
||||
|
||||
$personId = $this->request->query->getInt('person_id', 0);
|
||||
$personId = $request->query->getInt('person_id', 0);
|
||||
$person = $personRepository->find($personId);
|
||||
|
||||
if ($person === null) {
|
||||
@ -603,9 +574,9 @@ class SingleTaskController extends AbstractController
|
||||
$params['person'] = $person;
|
||||
}
|
||||
|
||||
if (!empty($this->request->query->get('course_id', NULL))) {
|
||||
if (!empty($request->query->get('course_id', NULL))) {
|
||||
|
||||
$courseId = $this->request->query->getInt('course_id', 0);
|
||||
$courseId = $request->query->getInt('course_id', 0);
|
||||
$course = $courseRepository->find($courseId);
|
||||
|
||||
if ($course === null) {
|
||||
@ -617,27 +588,27 @@ class SingleTaskController extends AbstractController
|
||||
$params['accompanyingCourse'] = $course;
|
||||
}
|
||||
|
||||
if (!empty($this->request->query->get('center_id', NULL))) {
|
||||
$center = $centerRepository->find($this->request->query->getInt('center_id'));
|
||||
if (!empty($request->query->get('center_id', NULL))) {
|
||||
$center = $centerRepository->find($request->query->getInt('center_id'));
|
||||
if ($center === null) {
|
||||
throw $this->createNotFoundException('center not found');
|
||||
}
|
||||
$params['center'] = $center;
|
||||
}
|
||||
|
||||
if(!empty($this->request->query->get('types', []))) {
|
||||
$types = $this->request->query->get('types', []);
|
||||
if(!empty($request->query->get('types', []))) {
|
||||
$types = $request->query->get('types', []);
|
||||
if (count($types) > 0) {
|
||||
$params['types'] = $types;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($this->request->query->get('user_id', null))) {
|
||||
if ($this->request->query->get('user_id') === '_unassigned') {
|
||||
if (!empty($request->query->get('user_id', null))) {
|
||||
if ($request->query->get('user_id') === '_unassigned') {
|
||||
$params['unassigned'] = true;
|
||||
} else {
|
||||
|
||||
$userId = $this->request->query->getInt('user_id', 0);
|
||||
$userId = $request->query->getInt('user_id', 0);
|
||||
$user = $this->getDoctrine()->getManager()
|
||||
->getRepository(User::class)
|
||||
->find($userId);
|
||||
@ -651,9 +622,9 @@ class SingleTaskController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($this->request->query->get('scope_id'))) {
|
||||
if (!empty($request->query->get('scope_id'))) {
|
||||
|
||||
$scopeId = $this->request->query->getInt('scope_id', 0);
|
||||
$scopeId = $request->query->getInt('scope_id', 0);
|
||||
|
||||
$scope = $this->getDoctrine()->getManager()
|
||||
->getRepository(Scope::class)
|
||||
@ -668,7 +639,7 @@ class SingleTaskController extends AbstractController
|
||||
}
|
||||
|
||||
$possibleStatuses = \array_merge(SingleTaskRepository::DATE_STATUSES, [ 'closed' ]);
|
||||
$statuses = $this->request->query->get('status', $possibleStatuses);
|
||||
$statuses = $request->query->get('status', $possibleStatuses);
|
||||
|
||||
$diff = \array_diff($statuses, $possibleStatuses);
|
||||
if (count($diff) > 0) {
|
||||
@ -683,7 +654,7 @@ class SingleTaskController extends AbstractController
|
||||
$tasks_count = 0;
|
||||
|
||||
foreach($statuses as $status) {
|
||||
if($this->request->query->has('status')
|
||||
if($request->query->has('status')
|
||||
&& FALSE === \in_array($status, $statuses)) {
|
||||
continue;
|
||||
}
|
||||
@ -729,7 +700,7 @@ class SingleTaskController extends AbstractController
|
||||
'add_type' => true
|
||||
]);
|
||||
|
||||
$form->handleRequest($this->request);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if (isset($person)) {
|
||||
$event = new PrivacyEvent($person, array(
|
||||
@ -747,7 +718,7 @@ class SingleTaskController extends AbstractController
|
||||
protected function getPersonParam(EntityManagerInterface $em)
|
||||
{
|
||||
$person = $em->getRepository(Person::class)
|
||||
->find($this->request->query->getInt('person_id'))
|
||||
->find($request->query->getInt('person_id'))
|
||||
;
|
||||
|
||||
if (NULL === $person) {
|
||||
@ -763,7 +734,7 @@ class SingleTaskController extends AbstractController
|
||||
protected function getUserParam(EntityManagerInterface $em)
|
||||
{
|
||||
$user = $em->getRepository(User::class)
|
||||
->find($this->request->query->getInt('user_id'))
|
||||
->find($request->query->getInt('user_id'))
|
||||
;
|
||||
|
||||
if (NULL === $user) {
|
||||
@ -802,13 +773,13 @@ class SingleTaskController extends AbstractController
|
||||
AccompanyingPeriodRepository $courseRepository,
|
||||
SingleTaskRepository $taskRepository,
|
||||
FormFactoryInterface $formFactory,
|
||||
TranslationTranslatorInterface $translator
|
||||
Request $request
|
||||
): Response
|
||||
{
|
||||
|
||||
if (!empty($this->request->query->get('course_id', NULL))) {
|
||||
if (!empty($request->query->get('course_id', NULL))) {
|
||||
|
||||
$courseId = $this->request->query->getInt('course_id', 0);
|
||||
$courseId = $request->query->getInt('course_id', 0);
|
||||
$course = $courseRepository->find($courseId);
|
||||
|
||||
if ($course === null) {
|
||||
@ -842,7 +813,7 @@ class SingleTaskController extends AbstractController
|
||||
'accompanyingCourse' => $course,
|
||||
'layout' => '@ChillPerson/AccompanyingCourse/layout.html.twig',
|
||||
'form' => $form->createView(),
|
||||
'title' => $translator->trans('Tasks for this accompanying period')
|
||||
'title' => $this->translator->trans('Tasks for this accompanying period')
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -16,10 +16,6 @@ use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
* AbstractTask
|
||||
*
|
||||
* @ORM\MappedSuperclass()
|
||||
* @UserCircleConsistency(
|
||||
* "CHILL_TASK_TASK_SHOW",
|
||||
* getUserFunction="getAssignee"
|
||||
* )
|
||||
*/
|
||||
abstract class AbstractTask implements HasScopeInterface, HasCenterInterface
|
||||
{
|
||||
|
@ -17,6 +17,9 @@
|
||||
*/
|
||||
namespace Chill\TaskBundle\Form;
|
||||
|
||||
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher;
|
||||
use Chill\MainBundle\Security\Resolver\ScopeResolverDispatcher;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Chill\MainBundle\Form\Type\ChillDateType;
|
||||
@ -29,15 +32,26 @@ use Symfony\Component\Security\Core\Role\Role;
|
||||
use Chill\MainBundle\Form\Type\DateIntervalType;
|
||||
use Chill\MainBundle\Form\Type\ChillTextareaType;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
class SingleTaskType extends AbstractType
|
||||
{
|
||||
private ParameterBagInterface $parameterBag;
|
||||
private CenterResolverDispatcher $centerResolverDispatcher;
|
||||
private ScopeResolverDispatcher $scopeResolverDispatcher;
|
||||
|
||||
public function __construct(ParameterBagInterface $parameterBag, CenterResolverDispatcher $centerResolverDispatcher, ScopeResolverDispatcher $scopeResolverDispatcher)
|
||||
{
|
||||
$this->parameterBag = $parameterBag;
|
||||
$this->centerResolverDispatcher = $centerResolverDispatcher;
|
||||
$this->scopeResolverDispatcher = $scopeResolverDispatcher;
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
if (NULL !== $task = $options['data']) {
|
||||
$center = $this->centerResolverDispatcher->resolveCenter($task);
|
||||
$isScopeConcerned = $this->scopeResolverDispatcher->isConcerned($task);
|
||||
}
|
||||
|
||||
$builder
|
||||
->add('title', TextType::class)
|
||||
->add('description', ChillTextareaType::class, [
|
||||
@ -45,15 +59,10 @@ class SingleTaskType extends AbstractType
|
||||
])
|
||||
->add('assignee', UserPickerType::class, [
|
||||
'required' => false,
|
||||
'center' => $options['center'],
|
||||
'center' => $center,
|
||||
'role' => $options['role'],
|
||||
'placeholder' => 'Not assigned'
|
||||
])
|
||||
->add('circle', ScopePickerType::class, [
|
||||
'center' => $options['center'],
|
||||
'role' => $options['role'],
|
||||
'required' => false
|
||||
])
|
||||
->add('startDate', ChillDateType::class, [
|
||||
'required' => false
|
||||
])
|
||||
@ -63,15 +72,25 @@ class SingleTaskType extends AbstractType
|
||||
->add('warningInterval', DateIntervalType::class, [
|
||||
'required' => false
|
||||
]);
|
||||
|
||||
if ($this->parameterBag->get('chill_main')['acl']['form_show_scopes']
|
||||
&& $isScopeConcerned) {
|
||||
$builder
|
||||
->add('circle', ScopePickerType::class, [
|
||||
'center' => $center,
|
||||
'role' => $options['role'],
|
||||
'required' => false
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver
|
||||
->setRequired('center')
|
||||
->setAllowedTypes('center', [ Center::class ])
|
||||
->setAllowedTypes('center', [ Center::class, 'array', 'null' ])
|
||||
->setRequired('role')
|
||||
->setAllowedTypes('role', [ Role::class ])
|
||||
->setAllowedTypes('role', [ Role::class, 'string' ])
|
||||
;
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@
|
||||
{{ include('@ChillMain/Util/confirmation_template.html.twig',
|
||||
{
|
||||
'title' : 'Remove task'|trans,
|
||||
'confirm_question' : 'Are you sure you want to remove the task about accompanying period "%id%" ?'|trans({ '%id%' : course.id } ),
|
||||
'confirm_question' : 'Are you sure you want to remove the task "%title%" ?'|trans({ '%title%' : task.title } ),
|
||||
'cancel_route' : 'chill_task_singletask_courselist',
|
||||
'cancel_parameters' : app.request.query.get('list_params', { } ),
|
||||
'form' : delete_form,
|
||||
|
@ -95,11 +95,11 @@
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
<ul class="record_actions">
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li>
|
||||
{% if accompanyingCourse is not null %}
|
||||
<a href="{{ path('chill_task_single_task_new', {'course_id': accompanyingCourse.id}) }}" class="btn btn-create">
|
||||
{{ 'Add a new task' | trans }}
|
||||
{{ 'Create' | trans }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
|
@ -237,17 +237,11 @@
|
||||
{% endif %}
|
||||
|
||||
{% if isSingleStatus == false %}
|
||||
<ul class="record_actions">
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li>
|
||||
{% if person is not null and is_granted('CHILL_TASK_TASK_CREATE', person) %}
|
||||
<a href="{{ path('chill_task_single_task_new', {'person_id': person.id}) }}" class="btn btn-create">
|
||||
{{ 'Add a new task' | trans }}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if accompanyingCourse is not null %}
|
||||
<a href="{{ path('chill_task_single_task_new', {'course_id': accompanyingCourse.id}) }}" class="btn btn-create">
|
||||
{{ 'Add a new task' | trans }}
|
||||
{{ 'Create' | trans }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
|
@ -7,14 +7,16 @@
|
||||
{{ form_row(form.title) }}
|
||||
{{ form_row(form.description) }}
|
||||
{{ form_row(form.assignee) }}
|
||||
{% if form.circle is defined %}
|
||||
{{ form_row(form.circle) }}
|
||||
{% endif %}
|
||||
{{ form_row(form.startDate) }}
|
||||
{{ form_row(form.endDate) }}
|
||||
{{ form_row(form.warningInterval) }}
|
||||
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li class="cancel">
|
||||
<a class="btn btn-cancel" href={% if task.person is not null %} "{{ path('chill_task_singletask_list', { 'person_id': task.person.id, 'list_params': app.request.query.get('list_params', {} )} ) }}" {% else %} "{{ chill_return_path_or('chill_task_singletask_courselist', {'course_id': task.course.id}) }}" {% endif %}>
|
||||
<a class="btn btn-cancel" href={% if task.person is not null %}"{{ chill_return_path_or('chill_task_singletask_list', { 'person_id': task.person.id } ) }}"{% else %}"{{ chill_return_path_or('chill_task_singletask_courselist', {'course_id': task.course.id}) }}" {% endif %}>
|
||||
{{ 'Cancel'|trans }}</a>
|
||||
</li>
|
||||
<li>
|
||||
|
@ -9,7 +9,9 @@
|
||||
{{ form_row(form.title) }}
|
||||
{{ form_row(form.description) }}
|
||||
{{ form_row(form.assignee) }}
|
||||
{% if form.circle is defined %}
|
||||
{{ form_row(form.circle) }}
|
||||
{% endif %}
|
||||
{{ form_row(form.startDate) }}
|
||||
{{ form_row(form.endDate) }}
|
||||
{{ form_row(form.warningInterval) }}
|
||||
|
@ -65,7 +65,9 @@ final class TaskVoter extends AbstractChillVoter implements ProvideRoleHierarchy
|
||||
|
||||
protected VoterHelperInterface $voter;
|
||||
|
||||
|
||||
public function __construct(
|
||||
VoterHelperFactoryInterface $voterHelperFactory,
|
||||
AccessDecisionManagerInterface $accessDecisionManager,
|
||||
AuthorizationHelper $authorizationHelper,
|
||||
EventDispatcherInterface $eventDispatcher,
|
||||
@ -82,7 +84,8 @@ final class TaskVoter extends AbstractChillVoter implements ProvideRoleHierarchy
|
||||
$this->voter = $voterFactory
|
||||
->generate(AbstractTask::class)
|
||||
->addCheckFor(AbstractTask::class, self::ROLES)
|
||||
->addCheckFor(Person::class, [self::SHOW])
|
||||
->addCheckFor(Person::class, [self::SHOW, self::CREATE])
|
||||
->addCheckFor(AccompanyingPeriod::class, [self::SHOW, self::CREATE])
|
||||
->addCheckFor(null, [self::SHOW])
|
||||
->build()
|
||||
;
|
||||
@ -91,14 +94,6 @@ final class TaskVoter extends AbstractChillVoter implements ProvideRoleHierarchy
|
||||
public function supports($attribute, $subject)
|
||||
{
|
||||
return $this->voter->supports($attribute, $subject);
|
||||
/*
|
||||
return ($subject instanceof AbstractTask && in_array($attribute, self::ROLES))
|
||||
||
|
||||
($subject instanceof Person && \in_array($attribute, [ self::CREATE, self::SHOW ]))
|
||||
||
|
||||
(NULL === $subject && $attribute === self::SHOW )
|
||||
;
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
@ -134,12 +129,19 @@ final class TaskVoter extends AbstractChillVoter implements ProvideRoleHierarchy
|
||||
// do pre-flight check, relying on other decision manager
|
||||
// those pre-flight check concern associated entities
|
||||
if ($subject instanceof AbstractTask) {
|
||||
// a user can always see his own tasks
|
||||
if ($subject->getAssignee() === $token->getUser()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (NULL !== $person = $subject->getPerson()) {
|
||||
if (!$this->accessDecisionManager->decide($token, [PersonVoter::SEE], $person)) {
|
||||
return false;
|
||||
}
|
||||
} elseif (false) {
|
||||
// here will come the test if the task is associated to an accompanying course
|
||||
} elseif (NULL !== $period = $subject->getCourse()) {
|
||||
if (!$this->accessDecisionManager->decide($token, [AccompanyingPeriodVoter::SEE], $period)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,6 @@
|
||||
services:
|
||||
Chill\TaskBundle\Controller\:
|
||||
resource: "../../Controller"
|
||||
tags: ["controller.service_arguments"]
|
||||
|
||||
Chill\TaskBundle\Controller\SingleTaskController:
|
||||
arguments:
|
||||
$eventDispatcher: '@Symfony\Component\EventDispatcher\EventDispatcherInterface'
|
||||
$timelineBuilder: "@chill_main.timeline_builder"
|
||||
$logger: "@chill.main.logger"
|
||||
$requestStack: '@Symfony\Component\HttpFoundation\RequestStack'
|
||||
autowire: true
|
||||
autoconfigure: ture
|
||||
tags: ["controller.service_arguments"]
|
||||
|
@ -1,4 +1,9 @@
|
||||
services:
|
||||
Chill\TaskBundle\Form\:
|
||||
resource: '../../Form/'
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
||||
Chill\TaskBundle\Form\SingleTaskListType:
|
||||
arguments:
|
||||
$em: '@Doctrine\ORM\EntityManagerInterface'
|
||||
|
@ -41,7 +41,7 @@ User: Utilisateur
|
||||
"Delete": "Supprimer"
|
||||
"Change task status": "Changer le statut"
|
||||
'Are you sure you want to remove the task about "%name%" ?': 'Êtes-vous sûr·e de vouloir supprimer la tâche de "%name%"?'
|
||||
'Are you sure you want to remove the task about accompanying period "%id%" ?': 'Êtes-vous sûr·e de vouloir supprimer la tâche du parcours "%id%"?'
|
||||
'Are you sure you want to remove the task "%title%" ?': 'Êtes-vous sûr·e de vouloir supprimer la tâche "%title%" ?'
|
||||
"See more": "Voir plus"
|
||||
"Associated tasks": "Tâches associées"
|
||||
"My tasks": "Mes tâches"
|
||||
|
Loading…
x
Reference in New Issue
Block a user