Merge branch 'master' into VSR-issues

This commit is contained in:
2023-01-24 15:36:22 +01:00
201 changed files with 2768 additions and 708 deletions

View File

@@ -43,6 +43,7 @@ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Role\Role;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use function array_key_exists;
final class ActivityController extends AbstractController
@@ -73,6 +74,8 @@ final class ActivityController extends AbstractController
private ThirdPartyRepository $thirdPartyRepository;
private TranslatorInterface $translator;
private UserRepositoryInterface $userRepository;
public function __construct(
@@ -89,7 +92,8 @@ final class ActivityController extends AbstractController
LoggerInterface $logger,
SerializerInterface $serializer,
UserRepositoryInterface $userRepository,
CenterResolverManagerInterface $centerResolver
CenterResolverManagerInterface $centerResolver,
TranslatorInterface $translator
) {
$this->activityACLAwareRepository = $activityACLAwareRepository;
$this->activityTypeRepository = $activityTypeRepository;
@@ -105,6 +109,7 @@ final class ActivityController extends AbstractController
$this->serializer = $serializer;
$this->userRepository = $userRepository;
$this->centerResolver = $centerResolver;
$this->translator = $translator;
}
/**
@@ -160,7 +165,7 @@ final class ActivityController extends AbstractController
$this->entityManager->remove($activity);
$this->entityManager->flush();
$this->addFlash('success', $this->get('translator')
$this->addFlash('success', $this->translator
->trans('The activity has been successfully removed.'));
$params = $this->buildParamsToUrl($person, $accompanyingPeriod);
@@ -245,7 +250,7 @@ final class ActivityController extends AbstractController
);
}
$this->addFlash('success', $this->get('translator')->trans('Success : activity updated!'));
$this->addFlash('success', $this->translator->trans('Success : activity updated!'));
return $this->redirectToRoute('chill_activity_activity_show', $params);
}
@@ -363,6 +368,7 @@ final class ActivityController extends AbstractController
if ($person instanceof Person) {
$entity->setPerson($person);
$entity->getPersons()->add($person);
}
if ($accompanyingPeriod instanceof AccompanyingPeriod) {
@@ -472,7 +478,7 @@ final class ActivityController extends AbstractController
);
}
$this->addFlash('success', $this->get('translator')->trans('Success : activity created!'));
$this->addFlash('success', $this->translator->trans('Success : activity created!'));
$params = $this->buildParamsToUrl($person, $accompanyingPeriod);

View File

@@ -13,15 +13,24 @@ namespace Chill\ActivityBundle\Controller;
use Chill\ActivityBundle\Entity\ActivityReason;
use Chill\ActivityBundle\Form\ActivityReasonType;
use Chill\ActivityBundle\Repository\ActivityReasonRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* ActivityReason controller.
*/
class ActivityReasonController extends AbstractController
{
private ActivityReasonRepository $activityReasonRepository;
public function __construct(ActivityReasonRepository $activityReasonRepository)
{
$this->activityReasonRepository = $activityReasonRepository;
}
/**
* Creates a new ActivityReason entity.
*/
@@ -56,8 +65,8 @@ class ActivityReasonController extends AbstractController
$entity = $em->getRepository(\Chill\ActivityBundle\Entity\ActivityReason::class)->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find ActivityReason entity.');
if (null === $entity) {
throw new NotFoundHttpException('Unable to find ActivityReason entity.');
}
$editForm = $this->createEditForm($entity);
@@ -75,7 +84,7 @@ class ActivityReasonController extends AbstractController
{
$em = $this->getDoctrine()->getManager();
$entities = $em->getRepository(\Chill\ActivityBundle\Entity\ActivityReason::class)->findAll();
$entities = $this->activityReasonRepository->findAll();
return $this->render('ChillActivityBundle:ActivityReason:index.html.twig', [
'entities' => $entities,

View File

@@ -63,10 +63,8 @@ class ActivityReason
/**
* Get category.
*
* @return ActivityReasonCategory
*/
public function getCategory()
public function getCategory(): ?ActivityReasonCategory
{
return $this->category;
}
@@ -107,6 +105,11 @@ class ActivityReason
return $this->name;
}
public function isActiveAndParentActive(): bool
{
return $this->active && null !== $this->getCategory() && $this->getCategory()->getActive();
}
/**
* Set active.
*

View File

@@ -60,7 +60,7 @@ class ByCreatorAggregator implements AggregatorInterface
return 'Created by';
}
if (null === $value) {
if (null === $value || '' === $value) {
return '';
}

View File

@@ -65,7 +65,7 @@ class BySocialActionAggregator implements AggregatorInterface
return 'Social action';
}
if (null === $value) {
if (null === $value || '' === $value) {
return '';
}

View File

@@ -65,7 +65,7 @@ class BySocialIssueAggregator implements AggregatorInterface
return 'Social issues';
}
if (null === $value) {
if (null === $value || '' === $value) {
return '';
}

View File

@@ -65,7 +65,7 @@ class ByThirdpartyAggregator implements AggregatorInterface
return 'Accepted thirdparty';
}
if (null === $value) {
if (null === $value || '' === $value) {
return '';
}

View File

@@ -65,7 +65,7 @@ class CreatorScopeAggregator implements AggregatorInterface
return 'Scope';
}
if (null === $value) {
if (null === $value || '' === $value) {
return '';
}

View File

@@ -65,11 +65,13 @@ class LocationTypeAggregator implements AggregatorInterface
return 'Accepted locationtype';
}
if (null === $value) {
if (null === $value || '' === $value) {
return '';
}
$lt = $this->locationTypeRepository->find($value);
if (null === $lt = $this->locationTypeRepository->find($value)) {
return '';
}
return $this->translatableStringHelper->localize(
$lt->getTitle()

View File

@@ -71,7 +71,7 @@ class ActivityTypeAggregator implements AggregatorInterface
return 'Activity type';
}
if (null === $value) {
if (null === $value || '' === $value) {
return '';
}

View File

@@ -66,7 +66,7 @@ class ActivityUserAggregator implements AggregatorInterface
return 'Activity user';
}
if (null === $value) {
if (null === $value || '' === $value) {
return '';
}

View File

@@ -64,7 +64,7 @@ class ActivityUsersAggregator implements AggregatorInterface
return 'Activity users';
}
if (null === $value) {
if (null === $value || '' === $value) {
return '';
}

View File

@@ -63,7 +63,7 @@ class ActivityUsersJobAggregator implements \Chill\MainBundle\Export\AggregatorI
return 'Users \'s job';
}
if (null === $value) {
if (null === $value || '' === $value) {
return '';
}

View File

@@ -63,7 +63,7 @@ class ActivityUsersScopeAggregator implements \Chill\MainBundle\Export\Aggregato
return 'Users \'s scope';
}
if (null === $value) {
if (null === $value || '' === $value) {
return '';
}

View File

@@ -134,6 +134,10 @@ class ActivityReasonAggregator implements AggregatorInterface, ExportElementVali
return 'reasons' === $data['level'] ? 'Group by reasons' : 'Group by categories of reason';
}
if (null === $value || '' === $value) {
return '';
}
switch ($data['level']) {
case 'reasons':
$r = $this->activityReasonRepository->find($value);

View File

@@ -57,6 +57,7 @@ class SentReceivedAggregator implements AggregatorInterface
switch ($value) {
case null:
case '':
return '';
case 'sent':

View File

@@ -17,11 +17,9 @@ use Chill\ActivityBundle\Repository\ActivityTypeRepositoryInterface;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\PersonBundle\Export\Declarations;
use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\FormBuilderInterface;
use function in_array;
class ActivityTypeFilter implements FilterInterface
{
@@ -44,14 +42,13 @@ class ActivityTypeFilter implements FilterInterface
public function alterQuery(QueryBuilder $qb, $data)
{
if (!in_array('activity', $qb->getAllAliases(), true)) {
$qb->join(Activity::class, 'activity', Expr\Join::WITH, 'activity.accompanyingPeriod = acp');
}
$clause = $qb->expr()->in('activity.activityType', ':selected_activity_types');
$qb->andWhere($clause);
$qb->setParameter('selected_activity_types', $data['types']);
$qb->andWhere(
$qb->expr()->exists(
'SELECT 1 FROM ' . Activity::class . ' act_type_filter_activity
WHERE act_type_filter_activity.activityType IN (:act_type_filter_activity_types) AND act_type_filter_activity.accompanyingPeriod = acp'
)
);
$qb->setParameter('act_type_filter_activity_types', $data['accepted_activitytypes']);
}
public function applyOn()

View File

@@ -11,7 +11,8 @@ declare(strict_types=1);
namespace Chill\ActivityBundle\Form;
use Chill\ActivityBundle\Form\Type\TranslatableActivityReasonCategory;
use Chill\ActivityBundle\Entity\ActivityReason;
use Chill\ActivityBundle\Form\Type\TranslatableActivityReasonCategoryType;
use Chill\MainBundle\Form\Type\TranslatableStringFormType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
@@ -25,13 +26,13 @@ class ActivityReasonType extends AbstractType
$builder
->add('name', TranslatableStringFormType::class)
->add('active', CheckboxType::class, ['required' => false])
->add('category', TranslatableActivityReasonCategory::class);
->add('category', TranslatableActivityReasonCategoryType::class);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => 'Chill\ActivityBundle\Entity\ActivityReason',
'data_class' => ActivityReason::class,
]);
}

View File

@@ -13,7 +13,7 @@ namespace Chill\ActivityBundle\Form;
use Chill\ActivityBundle\Entity\Activity;
use Chill\ActivityBundle\Entity\ActivityPresence;
use Chill\ActivityBundle\Entity\ActivityReason;
use Chill\ActivityBundle\Form\Type\PickActivityReasonType;
use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
use Chill\DocStoreBundle\Form\StoredObjectType;
use Chill\MainBundle\Entity\Center;
@@ -229,19 +229,10 @@ class ActivityType extends AbstractType
}
if ($activityType->isVisible('reasons')) {
$builder->add('reasons', EntityType::class, [
$builder->add('reasons', PickActivityReasonType::class, [
'label' => $activityType->getLabel('reasons'),
'required' => $activityType->isRequired('reasons'),
'class' => ActivityReason::class,
'multiple' => true,
'choice_label' => function (ActivityReason $activityReason) {
return $this->translatableStringHelper->localize($activityReason->getName());
},
'attr' => ['class' => 'select2 '],
'query_builder' => static function (EntityRepository $er) {
return $er->createQueryBuilder('a')
->where('a.active = true');
},
]);
}

View File

@@ -12,9 +12,9 @@ declare(strict_types=1);
namespace Chill\ActivityBundle\Form\Type;
use Chill\ActivityBundle\Entity\ActivityReason;
use Chill\ActivityBundle\Repository\ActivityReasonRepository;
use Chill\ActivityBundle\Templating\Entity\ActivityReasonRender;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Doctrine\ORM\EntityRepository;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
@@ -22,31 +22,29 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* FormType to choose amongst activity reasons.
*/
class TranslatableActivityReason extends AbstractType
class PickActivityReasonType extends AbstractType
{
/**
* @var ActivityReasonRender
*/
protected $reasonRender;
private ActivityReasonRepository $activityReasonRepository;
/**
* @var TranslatableStringHelper
*/
protected $translatableStringHelper;
private ActivityReasonRender $reasonRender;
private TranslatableStringHelperInterface $translatableStringHelper;
public function __construct(
TranslatableStringHelper $translatableStringHelper,
ActivityReasonRender $reasonRender
ActivityReasonRepository $activityReasonRepository,
ActivityReasonRender $reasonRender,
TranslatableStringHelperInterface $translatableStringHelper
) {
$this->translatableStringHelper = $translatableStringHelper;
$this->activityReasonRepository = $activityReasonRepository;
$this->reasonRender = $reasonRender;
$this->translatableStringHelper = $translatableStringHelper;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'class' => 'ChillActivityBundle:ActivityReason',
'class' => ActivityReason::class,
'choice_label' => function (ActivityReason $choice) {
return $this->reasonRender->renderString($choice, []);
},
@@ -57,10 +55,7 @@ class TranslatableActivityReason extends AbstractType
return null;
},
'query_builder' => static function (EntityRepository $er) {
return $er->createQueryBuilder('r')
->where('r.active = true');
},
'choices' => $this->activityReasonRepository->findAll(),
'attr' => ['class' => ' select2 '],
]
);

View File

@@ -1,59 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\ActivityBundle\Form\Type;
use Doctrine\ORM\EntityRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Description of TranslatableActivityReasonCategory.
*/
class TranslatableActivityReasonCategory extends AbstractType
{
/**
* @var RequestStack
*/
private $requestStack;
public function __construct(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}
public function configureOptions(OptionsResolver $resolver)
{
$locale = $this->requestStack->getCurrentRequest()->getLocale();
$resolver->setDefaults(
[
'class' => 'ChillActivityBundle:ActivityReasonCategory',
'choice_label' => 'name[' . $locale . ']',
'query_builder' => static function (EntityRepository $er) {
return $er->createQueryBuilder('c')
->where('c.active = true');
},
]
);
}
public function getBlockPrefix()
{
return 'translatable_activity_reason_category';
}
public function getParent()
{
return EntityType::class;
}
}

View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\ActivityBundle\Form\Type;
use Chill\ActivityBundle\Entity\ActivityReasonCategory;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Description of TranslatableActivityReasonCategory.
*/
class TranslatableActivityReasonCategoryType extends AbstractType
{
private TranslatableStringHelperInterface $translatableStringHelper;
private TranslatorInterface $translator;
public function __construct(TranslatableStringHelperInterface $translatableStringHelper, TranslatorInterface $translator)
{
$this->translatableStringHelper = $translatableStringHelper;
$this->translator = $translator;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'class' => ActivityReasonCategory::class,
'choice_label' => function (ActivityReasonCategory $category) {
return $this->translatableStringHelper->localize($category->getName())
. (!$category->getActive() ? ' (' . $this->translator->trans('inactive') . ')' : '');
},
]
);
}
public function getBlockPrefix()
{
return 'translatable_activity_reason_category';
}
public function getParent()
{
return EntityType::class;
}
}

View File

@@ -14,17 +14,38 @@ namespace Chill\ActivityBundle\Repository;
use Chill\ActivityBundle\Entity\ActivityReason;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* @method ActivityReason|null find($id, $lockMode = null, $lockVersion = null)
* @method ActivityReason|null findOneBy(array $criteria, array $orderBy = null)
* @method ActivityReason[] findAll()
* @method ActivityReason[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class ActivityReasonRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
private RequestStack $requestStack;
public function __construct(
ManagerRegistry $registry,
RequestStack $requestStack
) {
parent::__construct($registry, ActivityReason::class);
$this->requestStack = $requestStack;
}
/**
* @return ActivityReason[]
*/
public function findAll(): array
{
$qb = $this->createQueryBuilder('ar');
$qb->select(['ar'])
->leftJoin('ar.category', 'category')
->addOrderBy('JSON_EXTRACT(category.name, :lang)')
->addOrderBy('JSON_EXTRACT(ar.name, :lang)')
->setParameter('lang', $this->requestStack->getCurrentRequest()->getLocale() ?? 'fr');
return $qb->getQuery()->getResult();
}
}

View File

@@ -117,7 +117,8 @@ export default {
target: { //name, id
},
edit: false,
addressId: null
addressId: null,
defaults: window.addaddress
}
}
}

View File

@@ -30,7 +30,7 @@ const store = createStore({
},
getters: {
suggestedEntities(state) {
if (typeof state.activity.accompanyingPeriod === "undefined") {
if (typeof state.activity.accompanyingPeriod === "undefined" || state.activity.accompanyingPeriod === null) {
return [];
}
const allEntities = [

View File

@@ -39,6 +39,9 @@ const makeConcernedThirdPartiesLocation = (locationType, store) => {
return locations;
};
const makeAccompanyingPeriodLocation = (locationType, store) => {
if (store.state.activity.accompanyingPeriod === null) {
return {};
}
const accPeriodLocation = store.state.activity.accompanyingPeriod.location;
return {
type: 'location',

View File

@@ -7,22 +7,34 @@
<thead>
<tr>
<th>{{ 'Name'|trans }}</th>
<th>{{ 'Active'|trans }}</th>
<th>{{ 'Actions'|trans }}</th>
</tr>
</thead>
<tbody>
{% for entity in entities %}
<tr class="{% if entity.active %}active{% else %}inactive{% endif %}">
<td><a href="{{ path('chill_activity_activityreason_show', { 'id': entity.id }) }}">{{ entity.name|localize_translatable_string }}</a></td>
<td>
<ul class="record_actions">
<li>
<a href="{{ path('chill_activity_activityreason_show', { 'id': entity.id }) }}" class="btn btn-show" title="{{ 'show'|trans }}"></a>
</li>
<li>
<a href="{{ path('chill_activity_activityreason_edit', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'edit'|trans }}"></a>
</li>
</ul>
{% if entity.category is not null -%}
{{ entity.category.name|localize_translatable_string }} >
{% endif -%}
{{ entity.name|localize_translatable_string }}
</td>
<td>
<i class="fa {% if entity.active %}fa-check-square-o{% else %}fa-square-o{% endif %}"></i>
{% if entity.active and not entity.isActiveAndParentActive %}
<span class="badge text-bg-danger text-white">{{ 'Associated activity reason category is inactive'|trans }}</span>
{% endif %}
</td>
<td>
<ul class="record_actions">
<li>
<a href="{{ path('chill_activity_activityreason_show', { 'id': entity.id }) }}" class="btn btn-show" title="{{ 'show'|trans }}"></a>
</li>
<li>
<a href="{{ path('chill_activity_activityreason_edit', { 'id': entity.id }) }}" class="btn btn-edit" title="{{ 'edit'|trans }}"></a>
</li>
</ul>
</td>
</tr>
{% endfor %}

View File

@@ -7,6 +7,7 @@
<thead>
<tr>
<th>{{ 'Name'|trans }}</th>
<th>{{ 'Active'|trans }}</th>
<th>{{ 'Actions'|trans }}</th>
</tr>
</thead>
@@ -14,7 +15,11 @@
{% for entity in entities %}
<tr>
<td>
<a href="{{ path('chill_activity_activityreasoncategory_show', { 'id': entity.id }) }}">{{ entity.name|localize_translatable_string }}</a></td>
{{ entity.name|localize_translatable_string }}
</td>
<td>
<i class="fa {% if entity.active %}fa-check-square-o{% else %}fa-square-o{% endif %}"></i>
</td>
<td>
<ul class="record_actions">
<li>

View File

@@ -11,7 +11,7 @@ declare(strict_types=1);
namespace Chill\ActivityBundle\Tests\Form\Type;
use Chill\ActivityBundle\Form\Type\TranslatableActivityReason;
use Chill\ActivityBundle\Form\Type\PickActivityReasonType;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Symfony\Component\Form\PreloadedExtension;
use Symfony\Component\Form\Test\TypeTestCase;
@@ -36,7 +36,7 @@ final class TranslatableActivityReasonTest extends TypeTestCase
public function testSimple()
{
$translatableActivityReasonType = new TranslatableActivityReason(
$translatableActivityReasonType = new PickActivityReasonType(
$this->getTranslatableStringHelper()
);

View File

@@ -1,4 +1,11 @@
services:
Chill\ActivityBundle\Controller\ActivityController:
_defaults:
autowire: true
autoconfigure: true
Chill\ActivityBundle\Controller\:
resource: '../../Controller/'
tags: ['controller.service_arguments']
Chill\ActivityBundle\Controller\ActivityController:
tags: ['controller.service_arguments']

View File

@@ -1,19 +1,12 @@
---
services:
chill.activity.form.type.translatableactivityreasoncategory:
class: Chill\ActivityBundle\Form\Type\TranslatableActivityReasonCategory
arguments:
- "@request_stack"
tags:
- { name: form.type, alias: translatable_activity_reason_category }
Chill\ActivityBundle\Form\Type\TranslatableActivityReasonCategoryType:
autowire: true
autoconfigure: true
chill.activity.form.type.translatableactivityreason:
class: Chill\ActivityBundle\Form\Type\TranslatableActivityReason
arguments:
$translatableStringHelper: "@chill.main.helper.translatable_string"
$reasonRender: '@Chill\ActivityBundle\Templating\Entity\ActivityReasonRender'
tags:
- { name: form.type, alias: translatable_activity_reason }
Chill\ActivityBundle\Form\Type\PickActivityReasonType:
autowire: true
autoconfigure: true
chill.activity.form.type.translatableactivitytype:
class: Chill\ActivityBundle\Form\Type\TranslatableActivityType

View File

@@ -1,5 +1,8 @@
---
services:
Chill\ActivityBundle\Repository\ActivityReasonRepository:
autowire: true
chill_activity.repository.activity_type: '@Chill\ActivityBundle\Repository\ActivityTypeRepository'
chill_activity.repository.reason: '@Chill\ActivityBundle\Repository\ActivityReasonRepository'
chill_activity.repository.reason_category: '@Chill\ActivityBundle\Repository\ActivityReasonCategoryRepository'

View File

@@ -117,6 +117,7 @@ Activity Reasons: Sujets d'une activité
Activity Reasons Category: Catégories de sujet d'activités
Activity Types Categories: Catégories des types d'activité
Activity Presences: Presences aux activités
Associated activity reason category is inactive: La catégorie de sujet attachée est inactive
# Crud
@@ -276,7 +277,7 @@ Creators: Créateurs
Filter activity by userscope: Filtrer les activités par service du créateur
'Filtered activity by userscope: only %scopes%': "Filtré par service du créateur: uniquement %scopes%"
Accepted userscope: Services
Filter acp which has no activity: Filtrer les parcours qui nont pas dactivité
Filtered acp which has no activities: Filtrer les parcours sans activité associée
Group acp by activity number: Grouper les parcours par nombre dactivité