Compare commits

...

31 Commits

Author SHA1 Message Date
f0445ea798 FIX [documents][parcours] only show document menu entry when parcours is no longer draft 2023-02-13 19:37:43 +01:00
a4f47ad58b FIX [parcours][create] only allow creation of parcours from within household context if user has the create parcours right 2023-02-13 19:10:15 +01:00
860d14e082 FIX [delete][parcours] delete right removed from voter. 2023-02-13 17:51:46 +01:00
9465f26f09 FIX [parcours][rights] user with only simple view rights cannot create new parcours. 2023-02-13 17:51:02 +01:00
51681edda7 FIX [voter][household] only allow editing of household if user has chill_person_household_edit right linked to being able to edit persons 2023-02-13 17:17:56 +01:00
eac3471cbb FIX [person][contactinfo] place the contactinfo fields underneath birthdate for easier access 2023-02-10 11:37:32 +01:00
f653f8fd7a FIX [translations][wording] make distinction between parties concernees for an activity and an appointment in the translations 2023-02-10 11:30:26 +01:00
6d2c6fb6e1 FIX [renaming][translation] changing a translation for concerned groups in activities 2023-02-10 10:51:16 +01:00
aea6272c4d Merge improvements for admin's budget bundle 2023-02-09 18:20:33 +01:00
25cbb528ec Fixed: [budget] in admin list, show pagination and kind in resource_kind and charge_kind pages 2023-02-09 18:18:30 +01:00
2d013e110a Fixed: [budget] force budget kind and resource kind to have a unique kind field in database 2023-02-09 18:08:44 +01:00
d6df16973a Merge branch '693-filter-acp-by-user-job' into 'master'
693 on doit utiliser le métier du référent, et pas le métier du parcours

See merge request Chill-Projet/chill-bundles!482
2023-02-08 15:59:43 +00:00
80835dd7c3 Fixed: [export][filter by user's job] take into account the job associated to the user instead of the one associated to the course 2023-02-08 15:59:42 +00:00
9e63480c70 Merge remote-tracking branch 'origin/master' into 693-filter-acp-by-user-job 2023-02-08 16:44:50 +01:00
988495df27 Merge branch '697-add-filter-location' into 'master'
697 Filtrer les échanges par localisation

See merge request Chill-Projet/chill-bundles!481
2023-02-08 15:42:44 +00:00
cc62c9cc4a Feature: [export][activity] Add filter on localisation for activities 2023-02-08 15:42:44 +00:00
40924d9d39 Merge branch '698-filter-correction' into 'master'
698 Filtrer les actions par agent traitant: utiliser le formulaire avec AddPersons, et pas un EntityType

See merge request Chill-Projet/chill-bundles!480
2023-02-08 15:39:29 +00:00
8b505410ca Fixed: [export] filter referrer using dynamic picker in filter "agent traitant" for social works 2023-02-08 15:39:29 +00:00
d535ec6cfb Merge branch '676-docgen-center' into 'master'
676 fix docgen person center

See merge request Chill-Projet/chill-bundles!479
2023-02-08 15:22:09 +00:00
9029426d03 Feature: [docgen] add center when normalizing person 2023-02-08 15:22:08 +00:00
9ae2e51819 Merge branch '669-no-limit-for-location-endpoint' into 'master'
669 no limit for location endpoint

See merge request Chill-Projet/chill-bundles!478
2023-02-08 14:42:16 +00:00
68f7a832b4 Fixed: [activity] fetch all the available location, beyond the first page 2023-02-08 14:42:16 +00:00
af5f27ff49 Merge branch 'exports_activite_annexe' into 'master'
Feature: exports for aside activities

See merge request Chill-Projet/chill-bundles!485
2023-02-08 14:28:21 +00:00
5e58d36e79 Feature: exports for aside activities 2023-02-08 14:28:21 +00:00
b0ab591cbd Feature: [Document action buttons] do now show "Editer en ligne" for document which are not editable 2023-02-07 16:50:07 +01:00
d8af7d455e Fixed: [doc generation] fix summary budget 2023-02-07 16:22:26 +01:00
5830c3e177 Feature: [doc generation] show all the deps of the tree for debug information 2023-02-07 15:49:57 +01:00
88eefa698b Fixed: add string key for summary of charges and resources into doc generation 2023-02-07 15:09:40 +01:00
2f091a639b remove unused private function 2023-01-26 11:52:45 +01:00
8e3a83de85 DX: [export] Rename JobAggregator to more logical UserJobAggregator 2023-01-25 12:03:39 +01:00
050a4feab5 Fix: [export][aggregator] group by user job, not by job 2023-01-25 11:55:13 +01:00
74 changed files with 1772 additions and 357 deletions

View File

@@ -0,0 +1,74 @@
<?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\Export\Filter\ACPFilters;
use Chill\ActivityBundle\Export\Declarations;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\PickUserLocationType;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
class LocationFilter implements FilterInterface
{
private TranslatableStringHelper $translatableStringHelper;
public function __construct(TranslatableStringHelper $translatableStringHelper)
{
$this->translatableStringHelper = $translatableStringHelper;
}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
$qb->andWhere(
$qb->expr()->in('activity.location', ':location')
);
$qb->setParameter('location', $data['accepted_location']);
}
public function applyOn(): string
{
return Declarations::ACTIVITY_ACP;
}
public function buildForm(FormBuilderInterface $builder)
{
$builder->add('accepted_location', PickUserLocationType::class, [
'multiple' => true,
'label' => 'pick location'
]);
}
public function describeAction($data, $format = 'string'): array
{
$locations = [];
foreach ($data['accepted_location'] as $location) {
$locations[] = $location->getName();
}
return ['Filtered activity by location: only %locations%', [
'%locations%' => implode(', ', $locations),
]];
}
public function getTitle(): string
{
return 'Filter activity by location';
}
}

View File

@@ -79,6 +79,11 @@ services:
tags:
- { name: chill.export_filter, alias: 'accompanyingcourse_activitytype_filter' }
chill.activity.export.location_filter:
class: Chill\ActivityBundle\Export\Filter\ACPFilters\LocationFilter
tags:
- { name: chill.export_filter, alias: 'activity_location_filter' }
chill.activity.export.locationtype_filter:
class: Chill\ActivityBundle\Export\Filter\ACPFilters\LocationTypeFilter
tags:

View File

@@ -77,7 +77,7 @@ Choose a type: Choisir un type
4 hours: 4 heures
4 hours 30: 4 heures 30
5 hours: 5 heures
Concerned groups: Parties concernées
Concerned groups: Parties concernées par l'échange
Persons in accompanying course: Usagers du parcours
Third persons: Tiers non-pro.
Others persons: Usagers
@@ -252,6 +252,8 @@ Activity reasons for those activities: Sujets de ces activités
Filter by activity type: Filtrer les activités par type
Filter activity by location: Filtrer les activités par localisation
'Filtered activity by location: only %locations%': "Filtré par localisation: uniquement %locations%"
Filter activity by locationtype: Filtrer les activités par type de localisation
'Filtered activity by locationtype: only %types%': "Filtré par type de localisation: uniquement %types%"
Accepted locationtype: Types de localisation

View File

@@ -53,19 +53,15 @@ class ByActivityTypeAggregator implements AggregatorInterface
public function getLabels($key, array $values, $data)
{
$this->asideActivityCategoryRepository->findBy(['id' => $values]);
return function ($value): string {
if ('_header' === $value) {
return 'export.aggregator.Aside activity type';
}
if (null === $value) {
if (null === $value || null === $t = $this->asideActivityCategoryRepository->find($value)) {
return '';
}
$t = $this->asideActivityCategoryRepository->find($value);
return $this->translatableStringHelper->localize($t->getTitle());
};
}

View File

@@ -0,0 +1,89 @@
<?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\AsideActivityBundle\Export\Aggregator;
use Chill\AsideActivityBundle\Export\Declarations;
use Chill\MainBundle\Export\AggregatorInterface;
use Chill\MainBundle\Repository\UserJobRepositoryInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
use function in_array;
class ByUserJobAggregator implements AggregatorInterface
{
private TranslatableStringHelperInterface $translatableStringHelper;
private UserJobRepositoryInterface $userJobRepository;
public function __construct(UserJobRepositoryInterface $userJobRepository, TranslatableStringHelperInterface $translatableStringHelper)
{
$this->userJobRepository = $userJobRepository;
$this->translatableStringHelper = $translatableStringHelper;
}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
if (!in_array('aside_user', $qb->getAllAliases(), true)) {
$qb->leftJoin('aside.agent', 'aside_user');
}
$qb
->addSelect('IDENTITY(aside_user.userJob) AS aside_activity_user_job_aggregator')
->addGroupBy('aside_activity_user_job_aggregator');
}
public function applyOn()
{
return Declarations::ASIDE_ACTIVITY_TYPE;
}
public function buildForm(FormBuilderInterface $builder)
{
// nothing to add in the form
}
public function getLabels($key, array $values, $data)
{
return function ($value): string {
if ('_header' === $value) {
return 'Users \'s job';
}
if (null === $value || '' === $value) {
return '';
}
$j = $this->userJobRepository->find($value);
return $this->translatableStringHelper->localize(
$j->getLabel()
);
};
}
public function getQueryKeys($data): array
{
return ['aside_activity_user_job_aggregator'];
}
public function getTitle()
{
return 'export.aggregator.Aggregate by user job';
}
}

View File

@@ -0,0 +1,89 @@
<?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\AsideActivityBundle\Export\Aggregator;
use Chill\AsideActivityBundle\Export\Declarations;
use Chill\MainBundle\Export\AggregatorInterface;
use Chill\MainBundle\Repository\ScopeRepositoryInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
use function in_array;
class ByUserScopeAggregator implements AggregatorInterface
{
private ScopeRepositoryInterface $scopeRepository;
private TranslatableStringHelperInterface $translatableStringHelper;
public function __construct(ScopeRepositoryInterface $scopeRepository, TranslatableStringHelperInterface $translatableStringHelper)
{
$this->scopeRepository = $scopeRepository;
$this->translatableStringHelper = $translatableStringHelper;
}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
if (!in_array('aside_user', $qb->getAllAliases(), true)) {
$qb->leftJoin('aside.agent', 'aside_user');
}
$qb
->addSelect('IDENTITY(aside_user.mainScope) AS aside_activity_user_scope_aggregator')
->addGroupBy('aside_activity_user_scope_aggregator');
}
public function applyOn()
{
return Declarations::ASIDE_ACTIVITY_TYPE;
}
public function buildForm(FormBuilderInterface $builder)
{
// nothing to add in the form
}
public function getLabels($key, array $values, $data)
{
return function ($value): string {
if ('_header' === $value) {
return 'Users \'s scope';
}
if (null === $value || '' === $value) {
return '';
}
$s = $this->scopeRepository->find($value);
return $this->translatableStringHelper->localize(
$s->getName()
);
};
}
public function getQueryKeys($data): array
{
return ['aside_activity_user_scope_aggregator'];
}
public function getTitle()
{
return 'export.aggregator.Aggregate by user scope';
}
}

View File

@@ -0,0 +1,102 @@
<?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\AsideActivityBundle\Export\Export;
use Chill\AsideActivityBundle\Export\Declarations;
use Chill\AsideActivityBundle\Repository\AsideActivityRepository;
use Chill\AsideActivityBundle\Security\AsideActivityVoter;
use Chill\MainBundle\Export\ExportInterface;
use Chill\MainBundle\Export\FormatterInterface;
use Chill\MainBundle\Export\GroupedExportInterface;
use Doctrine\ORM\Query;
use LogicException;
use Symfony\Component\Form\FormBuilderInterface;
class AvgAsideActivityDuration implements ExportInterface, GroupedExportInterface
{
private AsideActivityRepository $repository;
public function __construct(
AsideActivityRepository $repository
) {
$this->repository = $repository;
}
public function buildForm(FormBuilderInterface $builder)
{
}
public function getAllowedFormattersTypes(): array
{
return [FormatterInterface::TYPE_TABULAR];
}
public function getDescription(): string
{
return 'export.Average aside activities duration';
}
public function getGroup(): string
{
return 'export.Exports of aside activities';
}
public function getLabels($key, array $values, $data)
{
if ('export_avg_aside_activity_duration' !== $key) {
throw new LogicException("the key {$key} is not used by this export");
}
return static fn ($value) => '_header' === $value ? 'Average duration aside activities' : $value;
}
public function getQueryKeys($data): array
{
return ['export_avg_aside_activity_duration'];
}
public function getResult($query, $data)
{
return $query->getQuery()->getResult(Query::HYDRATE_SCALAR);
}
public function getTitle(): string
{
return 'export.Average aside activities duration';
}
public function getType(): string
{
return Declarations::ASIDE_ACTIVITY_TYPE;
}
public function initiateQuery(array $requiredModifiers, array $acl, array $data = [])
{
$qb = $this->repository->createQueryBuilder('aside');
$qb
->select('AVG(aside.duration) as export_avg_aside_activity_duration')
->andWhere($qb->expr()->isNotNull('aside.duration'));
return $qb;
}
public function requiredRole(): string
{
return AsideActivityVoter::STATS;
}
public function supportsModifiers(): array
{
return [Declarations::ASIDE_ACTIVITY_TYPE];
}
}

View File

@@ -11,12 +11,12 @@ declare(strict_types=1);
namespace Chill\AsideActivityBundle\Export\Export;
use Chill\AsideActivityBundle\Export\Declarations;
use Chill\AsideActivityBundle\Repository\AsideActivityRepository;
use Chill\AsideActivityBundle\Security\AsideActivityVoter;
use Chill\MainBundle\Export\ExportInterface;
use Chill\MainBundle\Export\FormatterInterface;
use Chill\MainBundle\Export\GroupedExportInterface;
use ChillAsideActivityBundle\Export\Declarations;
use Doctrine\ORM\Query;
use LogicException;
use Symfony\Component\Form\FormBuilderInterface;
@@ -100,6 +100,8 @@ class CountAsideActivity implements ExportInterface, GroupedExportInterface
public function supportsModifiers(): array
{
return [];
return [
Declarations::ASIDE_ACTIVITY_TYPE,
];
}
}

View File

@@ -0,0 +1,236 @@
<?php
namespace Chill\AsideActivityBundle\Export\Export;
use Chill\AsideActivityBundle\Entity\AsideActivity;
use Chill\AsideActivityBundle\Export\Declarations;
use Chill\AsideActivityBundle\Form\AsideActivityCategoryType;
use Chill\AsideActivityBundle\Repository\AsideActivityCategoryRepository;
use Chill\AsideActivityBundle\Security\AsideActivityVoter;
use Chill\AsideActivityBundle\Templating\Entity\CategoryRender;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Export\FormatterInterface;
use Chill\MainBundle\Export\GroupedExportInterface;
use Chill\MainBundle\Export\Helper\DateTimeHelper;
use Chill\MainBundle\Export\Helper\UserHelper;
use Chill\MainBundle\Export\ListInterface;
use Chill\MainBundle\Repository\CenterRepositoryInterface;
use Chill\MainBundle\Repository\ScopeRepositoryInterface;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Closure;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
final class ListAsideActivity implements ListInterface, GroupedExportInterface
{
private EntityManagerInterface $em;
private UserHelper $userHelper;
private DateTimeHelper $dateTimeHelper;
private ScopeRepositoryInterface $scopeRepository;
private CenterRepositoryInterface $centerRepository;
private AsideActivityCategoryRepository $asideActivityCategoryRepository;
private CategoryRender $categoryRender;
private TranslatableStringHelperInterface $translatableStringHelper;
public function __construct(
EntityManagerInterface $em,
DateTimeHelper $dateTimeHelper,
UserHelper $userHelper,
ScopeRepositoryInterface $scopeRepository,
CenterRepositoryInterface $centerRepository,
AsideActivityCategoryRepository $asideActivityCategoryRepository,
CategoryRender $categoryRender,
TranslatableStringHelperInterface $translatableStringHelper
) {
$this->em = $em;
$this->dateTimeHelper = $dateTimeHelper;
$this->userHelper = $userHelper;
$this->scopeRepository = $scopeRepository;
$this->centerRepository = $centerRepository;
$this->asideActivityCategoryRepository = $asideActivityCategoryRepository;
$this->categoryRender = $categoryRender;
$this->translatableStringHelper = $translatableStringHelper;
}
public function buildForm(FormBuilderInterface $builder)
{
}
public function getAllowedFormattersTypes()
{
return [FormatterInterface::TYPE_LIST];
}
public function getDescription()
{
return 'export.aside_activity.List of aside activities';
}
public function getTitle()
{
return 'export.aside_activity.List of aside activities';
}
public function getGroup(): string
{
return 'export.Exports of aside activities';
}
public function getLabels($key, array $values, $data)
{
switch ($key) {
case 'id':
case 'note':
return function ($value) use ($key) {
if ('_header' === $value) {
return 'export.aside_activity.' . $key;
}
return $value ?? '';
};
case 'duration':
return function ($value) use ($key) {
if ('_header' === $value) {
return 'export.aside_activity.' . $key;
}
if (null === $value) {
return '';
}
if ($value instanceof \DateTimeInterface) {
return $value->format('H:i:s');
}
return $value;
};
case 'createdAt':
case 'updatedAt':
case 'date':
return $this->dateTimeHelper->getLabel('export.aside_activity.'.$key);
case 'agent_id':
case 'creator_id':
return $this->userHelper->getLabel($key, $values, 'export.aside_activity.' . $key);
case 'aside_activity_type':
return function ($value) {
if ('_header' === $value) {
return 'export.aside_activity.aside_activity_type';
}
if (null === $value || '' === $value || null === $c = $this->asideActivityCategoryRepository->find($value)) {
return '';
}
return $this->categoryRender->renderString($c, []);
};
case 'main_scope':
return function ($value) {
if ('_header' === $value) {
return 'export.aside_activity.main_scope';
}
if (null === $value || '' === $value || null === $c = $this->scopeRepository->find($value)) {
return '';
}
return $this->translatableStringHelper->localize($c->getName());
};
case 'main_center':
return function ($value) {
if ('_header' === $value) {
return 'export.aside_activity.main_center';
}
/** @var Center $c */
if (null === $value || '' === $value || null === $c = $this->centerRepository->find($value)) {
return '';
}
return $c->getName();
};
default:
throw new \LogicException('this key is not supported : ' . $key);
}
}
public function getQueryKeys($data)
{
return [
'id',
'createdAt',
'updatedAt',
'agent_id',
'creator_id',
'main_scope',
'main_center',
'aside_activity_type',
'date',
'duration',
'note'
];
}
/**
* @param QueryBuilder $query
* @param array $data
*/
public function getResult($query, $data): array
{
return $query->getQuery()->getResult(AbstractQuery::HYDRATE_ARRAY);
}
public function getType(): string
{
return Declarations::ASIDE_ACTIVITY_TYPE;
}
public function initiateQuery(array $requiredModifiers, array $acl, array $data = [])
{
$qb = $this->em->createQueryBuilder()
->from(AsideActivity::class, 'aside')
->leftJoin('aside.agent', 'agent')
;
$qb
->addSelect('aside.id AS id')
->addSelect('aside.createdAt AS createdAt')
->addSelect('aside.updatedAt AS updatedAt')
->addSelect('IDENTITY(aside.agent) AS agent_id')
->addSelect('IDENTITY(aside.createdBy) AS creator_id')
->addSelect('IDENTITY(agent.mainScope) AS main_scope')
->addSelect('IDENTITY(agent.mainCenter) AS main_center')
->addSelect('IDENTITY(aside.type) AS aside_activity_type')
->addSelect('aside.date')
->addSelect('aside.duration')
->addSelect('aside.note')
;
return $qb;
}
public function requiredRole(): string
{
return AsideActivityVoter::STATS;
}
public function supportsModifiers()
{
return [Declarations::ASIDE_ACTIVITY_TYPE];
}
}

View File

@@ -0,0 +1,102 @@
<?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\AsideActivityBundle\Export\Export;
use Chill\AsideActivityBundle\Export\Declarations;
use Chill\AsideActivityBundle\Repository\AsideActivityRepository;
use Chill\AsideActivityBundle\Security\AsideActivityVoter;
use Chill\MainBundle\Export\ExportInterface;
use Chill\MainBundle\Export\FormatterInterface;
use Chill\MainBundle\Export\GroupedExportInterface;
use Doctrine\ORM\Query;
use LogicException;
use Symfony\Component\Form\FormBuilderInterface;
class SumAsideActivityDuration implements ExportInterface, GroupedExportInterface
{
private AsideActivityRepository $repository;
public function __construct(
AsideActivityRepository $repository
) {
$this->repository = $repository;
}
public function buildForm(FormBuilderInterface $builder)
{
}
public function getAllowedFormattersTypes(): array
{
return [FormatterInterface::TYPE_TABULAR];
}
public function getDescription(): string
{
return 'export.Sum aside activities duration';
}
public function getGroup(): string
{
return 'export.Exports of aside activities';
}
public function getLabels($key, array $values, $data)
{
if ('export_sum_aside_activity_duration' !== $key) {
throw new LogicException("the key {$key} is not used by this export");
}
return static fn ($value) => '_header' === $value ? 'Sum duration aside activities' : $value;
}
public function getQueryKeys($data): array
{
return ['export_sum_aside_activity_duration'];
}
public function getResult($query, $data)
{
return $query->getQuery()->getResult(Query::HYDRATE_SCALAR);
}
public function getTitle(): string
{
return 'export.Sum aside activities duration';
}
public function getType(): string
{
return Declarations::ASIDE_ACTIVITY_TYPE;
}
public function initiateQuery(array $requiredModifiers, array $acl, array $data = [])
{
$qb = $this->repository
->createQueryBuilder('aside');
$qb->select('SUM(aside.duration) as export_sum_aside_activity_duration')
->andWhere($qb->expr()->isNotNull('aside.duration'));
return $qb;
}
public function requiredRole(): string
{
return AsideActivityVoter::STATS;
}
public function supportsModifiers(): array
{
return [Declarations::ASIDE_ACTIVITY_TYPE];
}
}

View File

@@ -80,8 +80,8 @@ class ByActivityTypeFilter implements FilterInterface
public function describeAction($data, $format = 'string'): array
{
$types = array_map(
fn (AsideActivityCategory $t): string => $this->translatableStringHelper->localize($t->getName()),
$this->asideActivityTypeRepository->findBy(['id' => $data['types']->toArray()])
fn (AsideActivityCategory $t): string => $this->translatableStringHelper->localize($t->getTitle()),
$data['types']->toArray()
);
return ['export.filter.Filtered by aside activity type: only %type%', [

View File

@@ -46,25 +46,18 @@ class ByDateFilter implements FilterInterface
public function alterQuery(QueryBuilder $qb, $data)
{
$where = $qb->getDQLPart('where');
$clause = $qb->expr()->between(
'aside.date',
':date_from',
':date_to'
);
if ($where instanceof Andx) {
$where->add($clause);
} else {
$where = $qb->expr()->andX($clause);
}
$qb->andWhere($clause);
$qb->add('where', $where);
$qb->setParameter(
'date_from',
$this->rollingDateConverter->convert($data['date_from'])
);
$qb->setParameter(
)->setParameter(
'date_to',
$this->rollingDateConverter->convert($data['date_to'])
);

View File

@@ -0,0 +1,75 @@
<?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\AsideActivityBundle\Export\Filter;
use Chill\AsideActivityBundle\Export\Declarations;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\PickUserDynamicType;
use Chill\MainBundle\Templating\Entity\UserRender;
use Doctrine\ORM\Query\Expr\Andx;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
class ByUserFilter implements FilterInterface
{
private UserRender $userRender;
public function __construct(UserRender $userRender)
{
$this->userRender = $userRender;
}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
$clause = $qb->expr()->in('aside.agent', ':users');
$qb
->andWhere($clause)
->setParameter('users', $data['accepted_users']);
}
public function applyOn(): string
{
return Declarations::ASIDE_ACTIVITY_TYPE;
}
public function buildForm(FormBuilderInterface $builder)
{
$builder->add('accepted_users', PickUserDynamicType::class, [
'multiple' => true,
'label' => 'Creators',
]);
}
public function describeAction($data, $format = 'string'): array
{
$users = [];
foreach ($data['accepted_users'] as $u) {
$users[] = $this->userRender->renderString($u, []);
}
return ['export.filter.Filtered aside activity by user: only %users%', [
'%users%' => implode(', ', $users),
]];
}
public function getTitle(): string
{
return 'export.filter.Filter aside activity by user';
}
}

View File

@@ -0,0 +1,81 @@
<?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\AsideActivityBundle\Export\Filter;
use Chill\AsideActivityBundle\Entity\AsideActivity;
use Chill\AsideActivityBundle\Export\Declarations;
use Chill\MainBundle\Entity\UserJob;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\FormBuilderInterface;
class ByUserJobFilter implements FilterInterface
{
private TranslatableStringHelperInterface $translatableStringHelper;
public function __construct(TranslatableStringHelperInterface $translatableStringHelper)
{
$this->translatableStringHelper = $translatableStringHelper;
}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
$qb
->andWhere(
$qb->expr()->exists(
'SELECT 1 FROM ' . AsideActivity::class . ' aside_activity_user_job_filter_act
JOIN aside_activity_user_job_filter_act.agent aside_activity_user_job_filter_user WHERE aside_activity_user_job_filter_user.userJob IN (:aside_activity_user_job_filter_jobs) AND aside_activity_user_job_filter_act = aside'
)
)
->setParameter('aside_activity_user_job_filter_jobs', $data['jobs']);
}
public function applyOn()
{
return Declarations::ASIDE_ACTIVITY_TYPE;
}
public function buildForm(FormBuilderInterface $builder)
{
$builder->add('jobs', EntityType::class, [
'class' => UserJob::class,
'choice_label' => fn (UserJob $j) => $this->translatableStringHelper->localize($j->getLabel()),
'multiple' => true,
'expanded' => true,
]);
}
public function describeAction($data, $format = 'string')
{
return ['export.filter.Filtered aside activities by user jobs: only %jobs%', [
'%jobs%' => implode(
', ',
array_map(
fn (UserJob $job) => $this->translatableStringHelper->localize($job->getLabel()),
$data['jobs']->toArray()
)
),
]];
}
public function getTitle()
{
return 'export.filter.Filter by user jobs';
}
}

View File

@@ -0,0 +1,88 @@
<?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\AsideActivityBundle\Export\Filter;
use Chill\AsideActivityBundle\Entity\AsideActivity;
use Chill\AsideActivityBundle\Export\Declarations;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Repository\ScopeRepositoryInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\FormBuilderInterface;
class ByUserScopeFilter implements FilterInterface
{
private ScopeRepositoryInterface $scopeRepository;
private TranslatableStringHelperInterface $translatableStringHelper;
public function __construct(
ScopeRepositoryInterface $scopeRepository,
TranslatableStringHelperInterface $translatableStringHelper
) {
$this->scopeRepository = $scopeRepository;
$this->translatableStringHelper = $translatableStringHelper;
}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
$qb
->andWhere(
$qb->expr()->exists(
'SELECT 1 FROM ' . AsideActivity::class . ' aside_activity_user_scope_filter_act
JOIN aside_activity_user_scope_filter_act.agent aside_activity_user_scope_filter_user WHERE aside_activity_user_scope_filter_user.mainScope IN (:aside_activity_user_scope_filter_scopes) AND aside_activity_user_scope_filter_act = aside '
)
)
->setParameter('aside_activity_user_scope_filter_scopes', $data['scopes']);
}
public function applyOn()
{
return Declarations::ASIDE_ACTIVITY_TYPE;
}
public function buildForm(FormBuilderInterface $builder)
{
$builder->add('scopes', EntityType::class, [
'class' => Scope::class,
'choices' => $this->scopeRepository->findAllActive(),
'choice_label' => fn (Scope $s) => $this->translatableStringHelper->localize($s->getName()),
'multiple' => true,
'expanded' => true,
]);
}
public function describeAction($data, $format = 'string')
{
return ['export.filter.Filtered aside activities by user scope: only %scopes%', [
'%scopes%' => implode(
', ',
array_map(
fn (Scope $s) => $this->translatableStringHelper->localize($s->getName()),
$data['scopes']->toArray()
)
),
]];
}
public function getTitle()
{
return 'export.filter.Filter by user scope';
}
}

View File

@@ -20,33 +20,3 @@ services:
resource: "../Controller"
autowire: true
autoconfigure: true
## Exports
# indicators
Chill\AsideActivityBundle\Export\Export\CountAsideActivity:
autowire: true
autoconfigure: true
tags:
- { name: chill.export, alias: count_asideactivity }
# filters
Chill\AsideActivityBundle\Export\Filter\ByDateFilter:
autowire: true
autoconfigure: true
tags:
- { name: chill.export_filter, alias: asideactivity_bydate_filter }
Chill\AsideActivityBundle\Export\Filter\ByActivityTypeFilter:
autowire: true
autoconfigure: true
tags:
- { name: chill.export_filter, alias: asideactivity_activitytype_filter }
# aggregators
Chill\AsideActivityBundle\Export\Aggregator\ByActivityTypeAggregator:
autowire: true
autoconfigure: true
tags:
- { name: chill.export_aggregator, alias: asideactivity_activitytype_aggregator }

View File

@@ -3,11 +3,23 @@ services:
autowire: true
autoconfigure: true
Chill\AsideActivityBundle\Export\Export\ListAsideActivity:
tags:
- { name: chill.export, alias: 'list_aside_activity' }
## Indicators
Chill\AsideActivityBundle\Export\Export\CountAsideActivity:
tags:
- { name: chill.export, alias: 'count_aside_activity' }
Chill\AsideActivityBundle\Export\Export\SumAsideActivityDuration:
tags:
- { name: chill.export, alias: 'sum_aside_activity_duration' }
Chill\AsideActivityBundle\Export\Export\AvgAsideActivityDuration:
tags:
- { name: chill.export, alias: 'avg_aside_activity_duration' }
## Filters
chill.aside_activity.export.date_filter:
class: Chill\AsideActivityBundle\Export\Filter\ByDateFilter
@@ -19,9 +31,34 @@ services:
tags:
- { name: chill.export_filter, alias: 'aside_activity_type_filter' }
chill.aside_activity.export.user_job_filter:
class: Chill\AsideActivityBundle\Export\Filter\ByUserJobFilter
tags:
- { name: chill.export_filter, alias: 'aside_activity_user_job_filter' }
chill.aside_activity.export.user_scope_filter:
class: Chill\AsideActivityBundle\Export\Filter\ByUserScopeFilter
tags:
- { name: chill.export_filter, alias: 'aside_activity_user_scope_filter' }
chill.aside_activity.export.user_filter:
class: Chill\AsideActivityBundle\Export\Filter\ByUserFilter
tags:
- { name: chill.export_filter, alias: 'aside_activity_user_filter' }
## Aggregators
chill.aside_activity.export.type_aggregator:
class: Chill\AsideActivityBundle\Export\Aggregator\ByActivityTypeAggregator
tags:
- { name: chill.export_aggregator, alias: activity_type_aggregator }
chill.aside_activity.export.user_job_aggregator:
class: Chill\AsideActivityBundle\Export\Aggregator\ByUserJobAggregator
tags:
- { name: chill.export_aggregator, alias: aside_activity_user_job_aggregator }
chill.aside_activity.export.user_scope_aggregator:
class: Chill\AsideActivityBundle\Export\Aggregator\ByUserScopeAggregator
tags:
- { name: chill.export_aggregator, alias: aside_activity_user_scope_aggregator }

View File

@@ -169,19 +169,43 @@ Aside activity configuration: Configuration des activités annexes
# exports
export:
aside_activity:
List of aside activities: Liste des activités annexes
createdAt: Création
updatedAt: Dernière mise à jour
agent_id: Utilisateur
creator_id: Créateur
main_scope: Service principal de l'utilisateur
main_center: Centre principal de l'utilisteur
aside_activity_type: Catégorie d'activité annexe
date: Date
duration: Durée
note: Note
Exports of aside activities: Exports des activités annexes
Count aside activities: Nombre d'activités annexes
Count aside activities by various parameters.: Compte le nombre d'activités annexes selon divers critères
Average aside activities duration: Durée moyenne des activités annexes
Sum aside activities duration: Durée des activités annexes
filter:
Filter by aside activity date: Filtrer les activités annexes par date
Filter by aside activity type: Filtrer les activités annexes par type d'activité
'Filtered by aside activity type: only %type%': "Filtré par type d'activité annexe: uniquement %type%"
Filtered by aside activities between %dateFrom% and %dateTo%: Filtré par date d'activité annexe, entre %dateFrom% et %dateTo%
This date should be after the date given in "Implied in an aside activity after this date" field: Cette date devrait être postérieure à la date donnée dans le champ "activités annexes après cette date"
Aside activities after this date: Actvitités annexes après cette date
Aside activities before this date: Actvitités annexes avant cette date
'Filtered aside activity by user: only %users%': "Filtré par utilisateur: uniquement %users%"
Filter aside activity by user: Filtrer par utilisateur
'Filtered aside activities by user jobs: only %jobs%': "Filtré par métier des utilisateurs: uniquement %jobs%"
Filter by user jobs: Filtrer les activités annexes par métier des utilisateurs
'Filtered aside activities by user scope: only %scopes%': "Filtré par service des utilisateur: uniquement %scopes%"
Filter by user scope: Filtrer les activités annexes par service d'utilisateur
aggregator:
Group by aside activity type: Grouper les activités annexes par type d'activité
Aside activity type: Type d'activité annexe
Aggregate by user job: Grouper les activités annexes par métier des utilisateurs
Aggregate by user scope: Grouper les activités annexes par service des utilisateurs
# ROLES
CHILL_ASIDE_ACTIVITY_STATS: Statistiques pour les activités annexes

View File

@@ -12,12 +12,17 @@ declare(strict_types=1);
namespace Chill\BudgetBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert;
/**
* Type of charge.
*
* @ORM\Table(name="chill_budget.charge_type")
* @ORM\Table(name="chill_budget.charge_type",
* uniqueConstraints={@ORM\UniqueConstraint(name="charge_kind_unique_type_idx", fields={"kind"})}
* )
* @ORM\Entity
* @UniqueEntity(fields={"kind"})
*/
class ChargeKind
{
@@ -35,6 +40,8 @@ class ChargeKind
/**
* @ORM\Column(type="string", length=255, options={"default": ""}, nullable=false)
* @Assert\Regex(pattern="/^[a-z0-9\-_]{1,}$/", message="budget.admin.form.kind.only_alphanumeric")
* @Assert\Length(min=3)
*/
private string $kind = '';

View File

@@ -12,12 +12,17 @@ declare(strict_types=1);
namespace Chill\BudgetBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert;
/**
* Type of resource.
*
* @ORM\Table(name="chill_budget.resource_type")
* @ORM\Table(name="chill_budget.resource_type", uniqueConstraints={
* @ORM\UniqueConstraint(name="resource_kind_unique_type_idx", fields={"kind"})
* })
* @ORM\Entity
* @UniqueEntity(fields={"kind"})
*/
class ResourceKind
{
@@ -35,6 +40,8 @@ class ResourceKind
/**
* @ORM\Column(type="string", length=255, nullable=false, options={"default": ""})
* @Assert\Regex(pattern="/^[a-z0-9\-_]{1,}$/", message="budget.admin.form.kind.only_alphanumeric")
* @Assert\Length(min=3)
*/
private string $kind = '';

View File

@@ -16,6 +16,7 @@ use Chill\MainBundle\Form\Type\TranslatableStringFormType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
@@ -25,7 +26,11 @@ class ChargeKindType extends AbstractType
{
$builder
->add('name', TranslatableStringFormType::class, [
'label' => 'Nom',
'label' => 'Title',
])
->add('kind', TextType::class, [
'label' => 'budget.admin.form.Charge_kind_key',
'help' => 'budget.admin.form.This kind must contains only alphabeticals characters, and dashes. This string is in use during document generation. Changes may have side effect on document'
])
->add('ordering', NumberType::class)
->add('isActive', CheckboxType::class, [

View File

@@ -16,6 +16,7 @@ use Chill\MainBundle\Form\Type\TranslatableStringFormType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
@@ -25,7 +26,11 @@ class ResourceKindType extends AbstractType
{
$builder
->add('name', TranslatableStringFormType::class, [
'label' => 'Nom',
'label' => 'Title',
])
->add('kind', TextType::class, [
'label' => 'budget.admin.form.Resource_kind_key',
'help' => 'budget.admin.form.This kind must contains only alphabeticals characters, and dashes. This string is in use during document generation. Changes may have side effect on document'
])
->add('ordering', NumberType::class)
->add('isActive', CheckboxType::class, [

View File

@@ -14,9 +14,8 @@ namespace Chill\BudgetBundle\Repository;
use Chill\BudgetBundle\Entity\ChargeKind;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository;
class ChargeKindRepository implements ObjectRepository
final class ChargeKindRepository implements ChargeKindRepositoryInterface
{
private EntityRepository $repository;
@@ -50,7 +49,8 @@ class ChargeKindRepository implements ObjectRepository
->where($qb->expr()->eq('c.isActive', 'true'))
->orderBy('c.ordering', 'ASC')
->getQuery()
->getResult();
->getResult()
;
}
/**
@@ -77,6 +77,11 @@ class ChargeKindRepository implements ObjectRepository
return $this->repository->findOneBy($criteria);
}
public function findOneByKind(string $kind): ?ChargeKind
{
return $this->repository->findOneBy(['kind' => $kind]);
}
public function getClassName(): string
{
return ChargeKind::class;

View File

@@ -0,0 +1,49 @@
<?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\BudgetBundle\Repository;
use Chill\BudgetBundle\Entity\ChargeKind;
use Doctrine\Persistence\ObjectRepository;
interface ChargeKindRepositoryInterface extends ObjectRepository
{
public function find($id): ?ChargeKind;
/**
* @return ChargeType[]
*/
public function findAll(): array;
/**
* @return ChargeType[]
*/
public function findAllActive(): array;
public function findOneByKind(string $kind): ?ChargeKind;
/**
* @return ChargeType[]
*/
public function findAllByType(string $type): array;
/**
* @param mixed|null $limit
* @param mixed|null $offset
*
* @return ChargeType[]
*/
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array;
public function findOneBy(array $criteria): ?ChargeKind;
public function getClassName(): string;
}

View File

@@ -14,9 +14,8 @@ namespace Chill\BudgetBundle\Repository;
use Chill\BudgetBundle\Entity\ResourceKind;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository;
class ResourceKindRepository implements ObjectRepository
final class ResourceKindRepository implements ResourceKindRepositoryInterface
{
private EntityRepository $repository;
@@ -50,7 +49,8 @@ class ResourceKindRepository implements ObjectRepository
->where($qb->expr()->eq('r.isActive', 'true'))
->orderBy('r.ordering', 'ASC')
->getQuery()
->getResult();
->getResult()
;
}
/**
@@ -77,6 +77,11 @@ class ResourceKindRepository implements ObjectRepository
return $this->repository->findOneBy($criteria);
}
public function findOneByKind(string $kind): ?ResourceKind
{
return $this->repository->findOneBy(['kind' => $kind]);
}
public function getClassName(): string
{
return ResourceKind::class;

View File

@@ -0,0 +1,49 @@
<?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\BudgetBundle\Repository;
use Chill\BudgetBundle\Entity\ResourceKind;
use Doctrine\Persistence\ObjectRepository;
interface ResourceKindRepositoryInterface extends ObjectRepository
{
public function find($id): ?ResourceKind;
/**
* @return ResourceType[]
*/
public function findAll(): array;
/**
* @return ResourceType[]
*/
public function findAllActive(): array;
/**
* @return ResourceType[]
*/
public function findAllByType(string $type): array;
/**
* @param mixed|null $limit
* @param mixed|null $offset
*
* @return ResourceType[]
*/
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array;
public function findOneBy(array $criteria): ?ResourceKind;
public function findOneByKind(string $kind): ?ResourceKind;
public function getClassName(): string;
}

View File

@@ -19,7 +19,10 @@
{% for entity in entities %}
<tr>
<td>{{ entity.ordering }}</td>
<td>{{ entity|chill_entity_render_box }}</td>
<td>
{{ entity|chill_entity_render_box }}<br/>
<strong>{{ 'budget.admin.form.Charge_kind_key'|trans }}&nbsp;:</strong> <code>{{ entity.kind }}</code>
</td>
<td style="text-align:center;">
{%- if entity.isActive -%}
<i class="fa fa-check-square-o"></i>
@@ -39,6 +42,8 @@
</tbody>
</table>
{{ chill_pagination(paginator) }}
<ul class="record_actions sticky-form-buttons">
<li>
<a href="{{ path('chill_crud_charge_kind_new') }}" class="btn btn-create">

View File

@@ -19,7 +19,10 @@
{% for entity in entities %}
<tr>
<td>{{ entity.ordering }}</td>
<td>{{ entity|chill_entity_render_box }}</td>
<td>
{{ entity|chill_entity_render_box }}<br/>
<strong>{{ 'budget.admin.form.Resource_kind_key'|trans }}&nbsp;:</strong> <code>{{ entity.kind }}</code>
</td>
<td style="text-align:center;">
{%- if entity.isActive -%}
<i class="fa fa-check-square-o"></i>
@@ -39,6 +42,8 @@
</tbody>
</table>
{{ chill_pagination(paginator) }}
<ul class="record_actions sticky-form-buttons">
<li>
<a href="{{ path('chill_crud_resource_kind_new') }}" class="btn btn-create">

View File

@@ -14,7 +14,9 @@ namespace Chill\BudgetBundle\Service\Summary;
use Chill\BudgetBundle\Entity\ChargeKind;
use Chill\BudgetBundle\Entity\ResourceKind;
use Chill\BudgetBundle\Repository\ChargeKindRepository;
use Chill\BudgetBundle\Repository\ChargeKindRepositoryInterface;
use Chill\BudgetBundle\Repository\ResourceKindRepository;
use Chill\BudgetBundle\Repository\ResourceKindRepositoryInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Person;
@@ -27,7 +29,7 @@ use function count;
/**
* Helps to find a summary of the budget: the sum of resources and charges.
*/
class SummaryBudget implements SummaryBudgetInterface
final class SummaryBudget implements SummaryBudgetInterface
{
private const QUERY_CHARGE_BY_HOUSEHOLD = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, charge_id AS kind_id FROM chill_budget.charge WHERE (person_id IN (_ids_) OR household_id = ?) AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY charge_id';
@@ -37,23 +39,19 @@ class SummaryBudget implements SummaryBudgetInterface
private const QUERY_RESOURCE_BY_PERSON = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, resource_id AS kind_id FROM chill_budget.resource WHERE person_id = ? AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY resource_id';
private ChargeKindRepository $chargeKindRepository;
private array $chargeLabels;
private ChargeKindRepositoryInterface $chargeKindRepository;
private EntityManagerInterface $em;
private ResourceKindRepository $resourceKindRepository;
private array $resourcesLabels;
private ResourceKindRepositoryInterface $resourceKindRepository;
private TranslatableStringHelperInterface $translatableStringHelper;
public function __construct(
EntityManagerInterface $em,
TranslatableStringHelperInterface $translatableStringHelper,
ResourceKindRepository $resourceKindRepository,
ChargeKindRepository $chargeKindRepository
ResourceKindRepositoryInterface $resourceKindRepository,
ChargeKindRepositoryInterface $chargeKindRepository
) {
$this->em = $em;
$this->translatableStringHelper = $translatableStringHelper;
@@ -129,19 +127,19 @@ class SummaryBudget implements SummaryBudgetInterface
private function getEmptyChargeArray(): array
{
$keys = array_map(static fn (ChargeKind $kind) => $kind->getId(), $this->chargeKindRepository->findAll());
$keys = array_map(static fn (ChargeKind $kind) => $kind->getKind(), $this->chargeKindRepository->findAll());
return array_combine($keys, array_map(function ($id) {
return ['sum' => 0.0, 'label' => $this->translatableStringHelper->localize($this->chargeKindRepository->find($id)->getName()), 'comment' => ''];
return array_combine($keys, array_map(function ($kind) {
return ['sum' => 0.0, 'label' => $this->translatableStringHelper->localize($this->chargeKindRepository->findOneByKind($kind)->getName()), 'comment' => ''];
}, $keys));
}
private function getEmptyResourceArray(): array
{
$keys = array_map(static fn (ResourceKind $kind) => $kind->getId(), $this->resourceKindRepository->findAll());
$keys = array_map(static fn (ResourceKind $kind) => $kind->getKind(), $this->resourceKindRepository->findAll());
return array_combine($keys, array_map(function ($id) {
return ['sum' => 0.0, 'label' => $this->translatableStringHelper->localize($this->resourceKindRepository->find($id)->getName()), 'comment' => ''];
return array_combine($keys, array_map(function ($kind) {
return ['sum' => 0.0, 'label' => $this->translatableStringHelper->localize($this->resourceKindRepository->findOneByKind($kind)->getName()), 'comment' => ''];
}, $keys));
}
@@ -155,7 +153,7 @@ class SummaryBudget implements SummaryBudgetInterface
$chargeKind = $this->chargeKindRepository->find($row['kind_id']);
if (null === $chargeKind) {
throw new RuntimeException('charge kind not found');
throw new RuntimeException('charge kind not found: ' . $row['kind_id']);
}
$result[$chargeKind->getKind()] = [
'sum' => (float) $row['sum'],
@@ -171,7 +169,7 @@ class SummaryBudget implements SummaryBudgetInterface
$resourceKind = $this->resourceKindRepository->find($row['kind_id']);
if (null === $resourceKind) {
throw new RuntimeException('charge kind not found');
throw new RuntimeException('charge kind not found: ' . $row['kind_id']);
}
$result[$resourceKind->getKind()] = [

View File

@@ -0,0 +1,158 @@
<?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\BudgetBundle\Tests\Service\Summary;
use Chill\BudgetBundle\Entity\ChargeKind;
use Chill\BudgetBundle\Entity\ResourceKind;
use Chill\BudgetBundle\Repository\ChargeKindRepositoryInterface;
use Chill\BudgetBundle\Repository\ResourceKindRepositoryInterface;
use Chill\BudgetBundle\Service\Summary\SummaryBudget;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Household\HouseholdMember;
use Chill\PersonBundle\Entity\Person;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
/**
* @internal
*
* @coversNothing
*/
final class SummaryBudgetTest extends TestCase
{
use ProphecyTrait;
public function testGenerateSummaryForPerson(): void
{
$queryCharges = $this->prophesize(AbstractQuery::class);
$queryCharges->getResult()->willReturn([
[
'sum' => 250.0,
'comment' => '',
'kind_id' => 1, // kind: rental
],
]);
$queryCharges->setParameters(Argument::type('array'))
->will(function ($args, $query) {
return $query;
})
;
$queryResources = $this->prophesize(AbstractQuery::class);
$queryResources->getResult()->willReturn([
[
'sum' => 1500.0,
'comment' => '',
'kind_id' => 2, // kind: 'salary',
],
]);
$queryResources->setParameters(Argument::type('array'))
->will(function ($args, $query) {
return $query;
})
;
$em = $this->prophesize(EntityManagerInterface::class);
$em->createNativeQuery(Argument::type('string'), Argument::type(Query\ResultSetMapping::class))
->will(function ($args) use ($queryResources, $queryCharges) {
if (false !== strpos($args[0], 'chill_budget.resource')) {
return $queryResources->reveal();
}
if (false !== strpos($args[0], 'chill_budget.charge')) {
return $queryCharges->reveal();
}
throw new \RuntimeException('this query does not have a stub counterpart: '.$args[0]);
})
;
$chargeRepository = $this->prophesize(ChargeKindRepositoryInterface::class);
$chargeRepository->findAll()->willReturn([
$rental = (new ChargeKind())->setKind('rental')->setName(['fr' => 'Rental']),
$other = (new ChargeKind())->setKind('other')->setName(['fr' => 'Other']),
]);
$chargeRepository->find(1)->willReturn($rental);
$chargeRepository->findOneByKind('rental')->willReturn($rental);
$chargeRepository->findOneByKind('other')->willReturn($other);
$resourceRepository = $this->prophesize(ResourceKindRepositoryInterface::class);
$resourceRepository->findAll()->willReturn([
$salary = (new ResourceKind())->setKind('salary')->setName(['fr' => 'Salary']),
$misc = (new ResourceKind())->setKind('misc')->setName(['fr' => 'Misc']),
]);
$resourceRepository->find(2)->willReturn($salary);
$resourceRepository->findOneByKind('salary')->willReturn($salary);
$resourceRepository->findOneByKind('misc')->willReturn($misc);
$translatableStringHelper = $this->prophesize(TranslatableStringHelperInterface::class);
$translatableStringHelper->localize(Argument::type('array'))->will(function ($arg) {
return $arg[0]['fr'];
});
$person = new Person();
$personReflection = new \ReflectionClass($person);
$personIdReflection = $personReflection->getProperty('id');
$personIdReflection->setAccessible(true);
$personIdReflection->setValue($person, 1);
$household = new Household();
$householdReflection = new \ReflectionClass($household);
$householdId = $householdReflection->getProperty('id');
$householdId->setAccessible(true);
$householdId->setValue($household, 1);
$householdMember = (new HouseholdMember())->setPerson($person)
->setStartDate(new \DateTimeImmutable('1 month ago'))
;
$household->addMember($householdMember);
$summaryBudget = new SummaryBudget(
$em->reveal(),
$translatableStringHelper->reveal(),
$resourceRepository->reveal(),
$chargeRepository->reveal()
);
$summary = $summaryBudget->getSummaryForPerson($person);
$summaryForHousehold = $summaryBudget->getSummaryForHousehold($household);
// we check the structure for the summary. The structure is the same for household
// and persons
$expected = [
'charges' => [
'rental' => ['sum' => 250.0, 'comment' => '', 'label' => 'Rental'],
'other' => ['sum' => 0.0, 'comment' => '', 'label' => 'Other'],
],
'resources' => [
'salary' => ['sum' => 1500.0, 'comment' => '', 'label' => 'Salary'],
'misc' => ['sum' => 0.0, 'comment' => '', 'label' => 'Misc'],
],
];
foreach ([$summaryForHousehold, $summary] as $summary) {
$this->assertIsArray($summary);
$this->assertEqualsCanonicalizing(['charges', 'resources'], array_keys($summary));
$this->assertEqualsCanonicalizing(['rental', 'other'], array_keys($summary['charges']));
$this->assertEqualsCanonicalizing(['salary', 'misc'], array_keys($summary['resources']));
foreach ($expected as $resCha => $contains) {
foreach ($contains as $kind => $row) {
$this->assertEqualsCanonicalizing($row, $summary[$resCha][$kind]);
}
}
}
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Chill\Migrations\Budget;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20230209161546 extends AbstractMigration
{
public function getDescription(): string
{
return 'Budget: add unique constraint on kind for charge_kind and resource_kind';
}
public function up(Schema $schema): void
{
$this->addSql("UPDATE chill_budget.resource_type SET kind=md5(random()::text) WHERE kind = ''");
$this->addSql("UPDATE chill_budget.charge_type SET kind=md5(random()::text) WHERE kind = ''");
$this->addSql('CREATE UNIQUE INDEX resource_kind_unique_type_idx ON chill_budget.resource_type (kind);');
$this->addSql('CREATE UNIQUE INDEX charge_kind_unique_type_idx ON chill_budget.charge_type (kind);');
}
public function down(Schema $schema): void
{
$this->addSql('DROP INDEX resource_kind_unique_type_idx');
$this->addSql('DROP INDEX charge_kind_unique_type_idx');
}
}

View File

@@ -77,6 +77,13 @@ The balance: Différence entre ressources et charges
Valid since %startDate% until %endDate%: Valide depuis le %startDate% jusqu'au %endDate%
Valid since %startDate%: Valide depuis le %startDate%
budget:
admin:
form:
Charge_kind_key: Clé d'identification
Resource_kind_key: Clé d'identification
This kind must contains only alphabeticals characters, and dashes. This string is in use during document generation. Changes may have side effect on document: Cette clé sert à identifier le type de charge ou de revenu lors de la génération de document. Seuls les caractères alpha-numériques sont autorisés. Modifier cette clé peut avoir un effet lors de la génération de nouveaux documents.
# ROLES
Budget elements: Budget
CHILL_BUDGET_ELEMENT_CREATE: Créer une ressource/charge

View File

@@ -1,2 +1,8 @@
The amount cannot be empty: Le montant ne peut pas être vide ou égal à zéro
The budget element's end date must be after the start date: La date de fin doit être après la date de début
budget:
admin:
form:
kind:
only_alphanumeric

View File

@@ -7,7 +7,7 @@
<div id="mainUser"></div> {# <=== vue component: mainUser #}
<h2 class="chill-red">{{ 'Concerned groups'|trans }}</h2>
<h2 class="chill-red">{{ 'Concerned groups calendar'|trans }}</h2>
{%- if form.persons is defined -%}
{{ form_widget(form.persons) }}

View File

@@ -7,7 +7,7 @@
<div id="mainUser"></div> {# <=== vue component: mainUser #}
<h2 class="chill-red">{{ 'Concerned groups'|trans }}</h2>
<h2 class="chill-red">{{ 'Concerned groups calendar'|trans }}</h2>
{%- if form.mainUser is defined -%}
{{ form_row(form.mainUser) }}

View File

@@ -14,7 +14,7 @@
<dd>{{ entity.mainUser }}</dd>
</dl>
<h2 class="chill-red">{{ 'Concerned groups'|trans }}</h2>
<h2 class="chill-red">{{ 'Concerned groups calendar'|trans }}</h2>
{% include 'ChillActivityBundle:Activity:concernedGroups.html.twig' with {'context': 'calendar_' ~ context, 'render': 'bloc' } %}

View File

@@ -4,7 +4,7 @@ My calendar list: Mes rendez-vous
There is no calendar items.: Il n'y a pas de rendez-vous
Remove calendar item: Supprimer le rendez-vous
Are you sure you want to remove the calendar item?: Êtes-vous sûr de vouloir supprimer le rendez-vous?
Concerned groups: Parties concernées
Concerned groups calendar: Parties concernées
Calendar data: Données du rendez-vous
Update calendar: Modifier le rendez-vous
main user concerned: Utilisateur concerné

View File

@@ -272,8 +272,9 @@ final class DocGeneratorTemplateController extends AbstractController
}
if ($isTest && isset($form) && $form['show_data']->getData()) {
// very ugly hack...
dd($context->getData($template, $entity, $contextGenerationData));
return $this->render('@ChillDocGenerator/Generator/debug_value.html.twig', [
'datas' => json_encode($context->getData($template, $entity, $contextGenerationData), JSON_PRETTY_PRINT)
]);
}
try {

View File

@@ -0,0 +1,8 @@
<html>
<head>
<title>{{ 'Doc generator debug'|trans }}</title>
</head>
<body>
<pre>{{ datas }}</pre>
</body>
</html>

View File

@@ -14,6 +14,7 @@ namespace Chill\DocStoreBundle\Menu;
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Knp\Menu\MenuItem;
use LogicException;
use Symfony\Component\Security\Core\Security;
@@ -60,7 +61,7 @@ final class MenuBuilder implements LocalMenuBuilderInterface
{
$course = $parameters['accompanyingCourse'];
if ($this->security->isGranted(AccompanyingCourseDocumentVoter::SEE, $course)) {
if ($this->security->isGranted(AccompanyingCourseDocumentVoter::SEE, $course) && !(AccompanyingPeriod::STEP_DRAFT === $course->getStep())) {
$menu->addChild($this->translator->trans('Documents'), [
'route' => 'accompanying_course_document_index',
'routeParameters' => [

View File

@@ -7,7 +7,7 @@
<li v-if="props.canEdit && is_extension_editable(props.storedObject.type)">
<wopi-edit-button :stored-object="props.storedObject" :classes="{'dropdown-item': true}" :execute-before-leave="props.executeBeforeLeave"></wopi-edit-button>
</li>
<li v-if="props.storedObject.type != 'application/pdf' && props.canConvertPdf">
<li v-if="props.storedObject.type != 'application/pdf' && is_extension_viewable(props.storedObject.type) && props.canConvertPdf">
<convert-button :stored-object="props.storedObject" :filename="filename" :classes="{'dropdown-item': true}"></convert-button>
</li>
<li v-if="props.canDownload">
@@ -23,7 +23,7 @@
import ConvertButton from "./StoredObjectButton/ConvertButton.vue";
import DownloadButton from "./StoredObjectButton/DownloadButton.vue";
import WopiEditButton from "./StoredObjectButton/WopiEditButton.vue";
import {is_extension_editable} from "./StoredObjectButton/helpers";
import {is_extension_editable, is_extension_viewable} from "./StoredObjectButton/helpers";
import {StoredObject, WopiEditButtonExecutableBeforeLeaveFunction} from "../types";
interface DocumentActionButtonsGroupConfig {

View File

@@ -1,47 +1,59 @@
const SUPPORTED_MIMES = new Set([
'image/svg+xml',
const MIMES_EDIT = new Set([
'application/vnd.ms-powerpoint',
'application/vnd.ms-excel',
'application/vnd.sun.xml.writer',
'application/vnd.oasis.opendocument.text',
'application/vnd.oasis.opendocument.text-flat-xml',
'application/vnd.sun.xml.calc',
'application/vnd.oasis.opendocument.spreadsheet',
'application/vnd.oasis.opendocument.spreadsheet-flat-xml',
'application/vnd.sun.xml.impress',
'application/vnd.oasis.opendocument.presentation',
'application/vnd.oasis.opendocument.presentation-flat-xml',
'application/vnd.sun.xml.draw',
'application/vnd.oasis.opendocument.graphics',
'application/vnd.oasis.opendocument.graphics-flat-xml',
'application/vnd.oasis.opendocument.chart',
'application/vnd.sun.xml.writer.global',
'application/vnd.oasis.opendocument.text-master',
'application/vnd.sun.xml.writer.template',
'application/vnd.oasis.opendocument.text-template',
'application/vnd.oasis.opendocument.text-master-template',
'application/vnd.sun.xml.calc.template',
'application/vnd.oasis.opendocument.spreadsheet-template',
'application/vnd.sun.xml.impress.template',
'application/vnd.oasis.opendocument.presentation-template',
'application/vnd.sun.xml.draw.template',
'application/vnd.oasis.opendocument.graphics-template',
'application/msword',
'application/msword',
'application/vnd.ms-excel',
'application/vnd.ms-powerpoint',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-word.document.macroEnabled.12',
'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
'application/vnd.ms-word.template.macroEnabled.12',
'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
'application/vnd.ms-excel.template.macroEnabled.12',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
'application/vnd.ms-excel.sheet.macroEnabled.12',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
'application/x-dif-document',
'text/spreadsheet',
'text/csv',
'application/x-dbase',
'text/rtf',
'text/plain',
'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
]);
const MIMES_VIEW = new Set([
...MIMES_EDIT,
[
'image/svg+xml',
'application/vnd.sun.xml.writer',
'application/vnd.sun.xml.calc',
'application/vnd.sun.xml.impress',
'application/vnd.sun.xml.draw',
'application/vnd.sun.xml.writer.global',
'application/vnd.sun.xml.writer.template',
'application/vnd.sun.xml.calc.template',
'application/vnd.sun.xml.impress.template',
'application/vnd.sun.xml.draw.template',
'application/vnd.oasis.opendocument.text-master',
'application/vnd.oasis.opendocument.text-template',
'application/vnd.oasis.opendocument.text-master-template',
'application/vnd.oasis.opendocument.spreadsheet-template',
'application/vnd.oasis.opendocument.presentation-template',
'application/vnd.oasis.opendocument.graphics-template',
'application/vnd.ms-word.template.macroEnabled.12',
'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
'application/vnd.ms-excel.template.macroEnabled.12',
'application/vnd.openxmlformats-officedocument.presentationml.template',
'application/vnd.ms-powerpoint.template.macroEnabled.12',
'application/vnd.wordperfect',
@@ -49,10 +61,6 @@ const SUPPORTED_MIMES = new Set([
'application/x-hwp',
'application/vnd.ms-works',
'application/x-mswrite',
'application/x-dif-document',
'text/spreadsheet',
'text/csv',
'application/x-dbase',
'application/vnd.lotus-1-2-3',
'image/cgm',
'image/vnd.dxf',
@@ -69,13 +77,10 @@ const SUPPORTED_MIMES = new Set([
'application/x-iwork-numbers-sffnumbers',
'application/vnd.oasis.opendocument.text-web',
'application/x-pagemaker',
'text/rtf',
'text/plain',
'application/x-fictionbook+xml',
'application/clarisworks',
'image/x-wpg',
'application/x-iwork-pages-sffpages',
'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
'application/x-iwork-keynote-sffkey',
'application/x-abiword',
'image/x-freehand',
@@ -88,10 +93,15 @@ const SUPPORTED_MIMES = new Set([
'image/jpg',
'image/jpeg',
'application/pdf',
]);
]
])
function is_extension_editable(mimeType: string): boolean {
return SUPPORTED_MIMES.has(mimeType);
return MIMES_EDIT.has(mimeType);
}
function is_extension_viewable(mimeType: string): boolean {
return MIMES_VIEW.has(mimeType);
}
function build_convert_link(uuid: string) {
@@ -165,4 +175,5 @@ export {
download_and_decrypt_doc,
download_doc,
is_extension_editable,
is_extension_viewable,
};

View File

@@ -36,9 +36,9 @@
{{ chill_pagination(pagination) }}
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_CREATE', accompanyingCourse) and not accompanyingCourse.getStep() is same as('DRAFT') %}
<div data-docgen-template-picker="data-docgen-template-picker" data-entity-class="Chill\PersonBundle\Entity\AccompanyingPeriod" data-entity-id="{{ accompanyingCourse.id }}"></div>
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_CREATE', accompanyingCourse) %}
<ul class="record_actions sticky-form-buttons">
<li class="create">
<a href="{{ path('accompanying_course_document_new', {'course': accompanyingCourse.id}) }}" class="btn btn-create">

View File

@@ -56,9 +56,9 @@
{{ chill_pagination(pagination) }}
{% if is_granted('CHILL_PERSON_DOCUMENT_CREATE', person) %}
<div data-docgen-template-picker="data-docgen-template-picker" data-entity-class="Chill\PersonBundle\Entity\Person" data-entity-id="{{ person.id }}"></div>
{% if is_granted('CHILL_PERSON_DOCUMENT_CREATE', person) %}
<ul class="record_actions sticky-form-buttons">
<li class="create">
<a href="{{ path('person_document_new', {'person': person.id}) }}" class="btn btn-create">

View File

@@ -28,7 +28,7 @@ class LocationController extends CRUDController
protected function customizeQuery(string $action, Request $request, $query): void
{
$query->where('e.availableForUsers = true'); //TODO not working
$query->where('e.availableForUsers = "TRUE"');
}
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator)

View File

@@ -83,9 +83,9 @@ interface ExportInterface extends ExportElementInterface
*
* @param string $key The column key, as added in the query
* @param mixed[] $values The values from the result. if there are duplicates, those might be given twice. Example: array('FR', 'BE', 'CZ', 'FR', 'BE', 'FR')
* @param mixed $data The data from the export's form (as defined in `buildForm`
* @param mixed $data The data from the export's form (as defined in `buildForm`)
*
* @return Closure where the first argument is the value, and the function should return the label to show in the formatted file. Example : `function($countryCode) use ($countries) { return $countries[$countryCode]->getName(); }`
* @return pure-callable(null|string|int|float|'_header' $value):string|int|\DateTimeInterface where the first argument is the value, and the function should return the label to show in the formatted file. Example : `function($countryCode) use ($countries) { return $countries[$countryCode]->getName(); }`
*/
public function getLabels($key, array $values, $data);

View File

@@ -35,6 +35,10 @@ class DateTimeHelper
return '';
}
if ($value instanceof \DateTimeInterface) {
return $value;
}
// warning: won't work with DateTimeImmutable as we reset time a few lines later
$date = DateTime::createFromFormat('Y-m-d', $value);
$hasTime = false;

View File

@@ -229,6 +229,7 @@ Create a new circle: Créer un nouveau cercle
#admin section for location
Location: Localisation
pick location: Localisation
Location type list: Liste des types de localisation
Create a new location type: Créer un nouveau type de localisation
Available for users: Disponible aux utilisateurs

View File

@@ -248,6 +248,8 @@ class AccompanyingCourseController extends Controller
*/
public function newAction(Request $request): Response
{
$this->denyAccessUnlessGranted(AccompanyingPeriodVoter::CREATE);
$period = new AccompanyingPeriod();
$em = $this->getDoctrine()->getManager();

View File

@@ -133,7 +133,7 @@ class HouseholdCompositionController extends AbstractController
*/
public function index(Household $household, Request $request): Response
{
if (!$this->security->isGranted(HouseholdVoter::SEE, $household)) {
if (!$this->security->isGranted(HouseholdVoter::EDIT, $household)) {
throw new AccessDeniedException('not allowed to edit a household');
}

View File

@@ -24,6 +24,7 @@ use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\SerializerInterface;
@@ -123,7 +124,9 @@ class HouseholdController extends AbstractController
*/
public function addressEdit(Request $request, Household $household)
{
// TODO ACL
if (!$this->security->isGranted(HouseholdVoter::EDIT, $household)) {
throw new AccessDeniedException('You are not allowed to edit a household address');
}
$address_id = $request->query->get('address_id');
$address = $this->getDoctrine()->getManager()
@@ -149,7 +152,9 @@ class HouseholdController extends AbstractController
*/
public function addresses(Request $request, Household $household)
{
// TODO ACL
if (!$this->security->isGranted(HouseholdVoter::SEE, $household)) {
throw new AccessDeniedException('You have no access to this household\'s details');
}
//TODO put these lines into a validator constraint on household->getAddress
$addresses = $household->getAddresses();
@@ -179,7 +184,9 @@ class HouseholdController extends AbstractController
*/
public function addressMove(Request $request, Household $household)
{
// TODO ACL
if (!$this->security->isGranted(HouseholdVoter::EDIT, $household)) {
throw new AccessDeniedException('You are not allowed to edit this household');
}
return $this->render(
'@ChillPerson/Household/address_move.html.twig',
@@ -255,7 +262,10 @@ class HouseholdController extends AbstractController
*/
public function editHouseholdMetadata(Request $request, Household $household)
{
// TODO ACL
if (!$this->security->isGranted(HouseholdVoter::EDIT, $household)) {
throw new AccessDeniedException('not allowed to edit a household');
}
$form = $this->createMetadataForm($household);
$form->handleRequest($request);
@@ -311,7 +321,9 @@ class HouseholdController extends AbstractController
*/
public function summary(Request $request, Household $household)
{
// TODO ACL
if (!$this->security->isGranted(HouseholdVoter::SEE, $household)) {
throw new AccessDeniedException('not allowed to edit a household');
}
$positions = $this->positionRepository
->findByActiveOrdered();

View File

@@ -19,12 +19,15 @@ use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Form\HouseholdMemberType;
use Chill\PersonBundle\Household\MembersEditor;
use Chill\PersonBundle\Repository\AccompanyingPeriodRepository;
use Chill\PersonBundle\Security\Authorization\HouseholdVoter;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Exception;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -38,14 +41,18 @@ class HouseholdMemberController extends ApiController
private TranslatorInterface $translator;
private Security $security;
public function __construct(
UrlGeneratorInterface $generator,
TranslatorInterface $translator,
AccompanyingPeriodRepository $periodRepository
AccompanyingPeriodRepository $periodRepository,
Security $security
) {
$this->generator = $generator;
$this->translator = $translator;
$this->periodRepository = $periodRepository;
$this->security = $security;
}
/**
@@ -56,7 +63,9 @@ class HouseholdMemberController extends ApiController
*/
public function editMembership(Request $request, HouseholdMember $member): Response
{
// TODO ACL
if (!$this->security->isGranted(HouseholdVoter::EDIT, $member->getHousehold())) {
throw new AccessDeniedException('You are not allowed to edit this household');
}
$form = $this->createForm(HouseholdMemberType::class, $member, [
'validation_groups' => ['household_memberships'],

View File

@@ -973,14 +973,14 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
// accompanying period
AccompanyingPeriodVoter::SEE_DETAILS => [AccompanyingPeriodVoter::SEE],
AccompanyingPeriodVoter::CREATE => [AccompanyingPeriodVoter::SEE_DETAILS],
AccompanyingPeriodVoter::DELETE => [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,
// AccompanyingPeriodVoter::DELETE,
],
AccompanyingPeriodVoter::REASSIGN_BULK => [
AccompanyingPeriodVoter::CONFIDENTIAL_CRUD,

View File

@@ -19,7 +19,7 @@ use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
use function in_array;
final class JobAggregator implements AggregatorInterface
final class UserJobAggregator implements AggregatorInterface
{
private UserJobRepository $jobRepository;
@@ -40,11 +40,11 @@ final class JobAggregator implements AggregatorInterface
public function alterQuery(QueryBuilder $qb, $data)
{
if (!in_array('acpjob', $qb->getAllAliases(), true)) {
$qb->leftJoin('acp.job', 'acpjob');
if (!in_array('acpuser', $qb->getAllAliases(), true)) {
$qb->leftJoin('acp.user', 'acpuser');
}
$qb->addSelect('IDENTITY(acp.job) AS job_aggregator');
$qb->addSelect('IDENTITY(acpuser.userJob) AS job_aggregator');
$qb->addGroupBy('job_aggregator');
}

View File

@@ -72,6 +72,7 @@ class HasTemporaryLocationFilter implements FilterInterface
{
$builder
->add('having_temporarily', ChoiceType::class, [
'label' => 'export.filter.course.having_temporarily.label',
'choices' => [
'export.filter.course.having_temporarily.Having a temporarily location' => true,
'export.filter.course.having_temporarily.Having a person\'s location' => false,

View File

@@ -126,12 +126,4 @@ class UserJobFilter implements FilterInterface
{
return 'Filter by user job';
}
private function getUserJob(): UserJob
{
/** @var User $user */
$user = $this->security->getUser();
return $user->getUserJob();
}
}

View File

@@ -11,13 +11,12 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Export\Filter\SocialWorkFilters;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\PickUserDynamicType;
use Chill\MainBundle\Templating\Entity\UserRender;
use Chill\PersonBundle\Export\Declarations;
use Doctrine\ORM\Query\Expr\Andx;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\FormBuilderInterface;
use function in_array;
@@ -61,13 +60,8 @@ class ReferrerFilter implements FilterInterface
public function buildForm(FormBuilderInterface $builder)
{
$builder->add('accepted_agents', EntityType::class, [
'class' => User::class,
'choice_label' => function (User $u) {
return $this->userRender->renderString($u, []);
},
$builder->add('accepted_agents', PickUserDynamicType::class, [
'multiple' => true,
'expanded' => true,
]);
}

View File

@@ -34,7 +34,7 @@
<script>
import VueMultiselect from 'vue-multiselect';
import { makeFetch } from 'ChillMainAssets/lib/api/apiMethods';
import { fetchResults } from 'ChillMainAssets/lib/api/apiMethods';
import { mapState, mapGetters } from 'vuex';
export default {
@@ -58,23 +58,17 @@ export default {
},
methods: {
getOptions() {
const url = `/api/1.0/main/location.json`;
makeFetch('GET', url)
fetchResults(`/api/1.0/main/location.json`)
.then(response => {
let options = response.results;
let uniqueLocationTypeId = [...new Set(options.map(o => o.locationType.id))];
let uniqueLocationTypeId = [...new Set(response.map(o => o.locationType.id))];
let results = [];
for (let id of uniqueLocationTypeId) {
results.push({
locationCategories: options.filter(o => o.locationType.id === id)[0].locationType.title.fr,
locations: options.filter(o => o.locationType.id === id)
locationCategories: response.filter(o => o.locationType.id === id)[0].locationType.title.fr,
locations: response.filter(o => o.locationType.id === id)
})
}
this.options = results;
return response;
})
.catch((error) => {
this.$toast.open({message: error.txt})
})
},
updateAdminLocation(value) {

View File

@@ -17,12 +17,14 @@
class="btn btn-cancel">{{ 'Person details'|trans }}
</a>
</li>
{% if is_granted('CHILL_PERSON_ACCOMPANYING_PERIOD_CREATE') %}
<li>
<a class="btn btn-create"
href="{{ path ('chill_person_accompanying_course_new', {'person_id' : [ person.id ] } ) }}" role="button">
{{ 'Create an accompanying period'|trans }}
</a>
</li>
{% endif %}
{# Disabled dropdown
<li class="dropdown">

View File

@@ -30,12 +30,13 @@
{{ customButtons['before'] }}
{% endif %}
{% if is_granted('CHILL_PERSON_HOUSEHOLD_EDIT', member.household) %}
<li>
<a class="btn btn-sm btn-edit"
title="{{ 'household.Edit member household'|trans }}"
href="{{ chill_path_add_return_path('chill_person_household_member_edit', { 'id': member.id }) }}"></a>
</li>
{% endif %}
{% if customButtons['after'] is defined %}
{{ customButtons['after'] }}
{% endif %}

View File

@@ -40,13 +40,14 @@
{{ 'Household summary'|trans }}
</a>
</li>
{# TODO: add ACL to check if user is allowed to edit household? #}
{% if is_granted('CHILL_PERSON_ACCOMPANYING_PERIOD_CREATE') %}
<li>
<a class="btn btn-create"
href="{{ path ('chill_household_accompanying_course_new', {'household_id' : household.id } ) }}" role="button">
{{ 'Create an accompanying period'|trans }}
</a>
</li>
{% endif %}
</ul>
</div>

View File

@@ -10,7 +10,7 @@
{% if household.addresses|length == 0 %}
<span class="chill-no-data-statement">{{ 'No address given'|trans }}</span>
{% else %}
{% if is_granted('CHILL_PERSON_HOUSEHOLD_EDIT', household) %}
<ul class="record_actions my-3">
<li style="margin: auto;">
@@ -27,7 +27,7 @@
</li>
</ul>
{% endif %}
<div class="address-timeline grid">
<div class="top"><i class="fa fa-caret-up fa-3x"></i></div>
@@ -92,6 +92,7 @@
{{ 'Back to household'|trans }}
</a>
</li>
{% if is_granted('CHILL_PERSON_HOUSEHOLD_EDIT', household) %}
<li>
<a class="btn btn-create"
@@ -100,6 +101,7 @@
</a>
</li>
{% endif %}
</ul>
</div>

View File

@@ -27,6 +27,7 @@
{% endif %}
<ul class="list-inline text-right mt-2">
{% if is_granted('CHILL_PERSON_HOUSEHOLD_EDIT', household) %}
<li class="list-inline-item">
{# include vue_address component #}
{% include '@ChillMain/Address/_insert_vue_address.html.twig' with {
@@ -41,6 +42,7 @@
useValidFrom: true,
} %}
</li>
{% endif %}
<li class="list-inline-item">
<a class="btn btn-secondary btn-sm" title="{{ "Addresses history"|trans }}"
href="{{ path('chill_person_household_addresses', { 'household_id': household.id } ) }}">
@@ -63,6 +65,7 @@
<p>
{{ 'household_composition.Since'|trans({'startDate': currentComposition.startDate}) }}
</p>
{% if is_granted('CHILL_PERSON_HOUSEHOLD_EDIT', household) %}
<ul class="record_actions">
<li>
<a class="btn btn-sm btn-update change-icon"
@@ -71,12 +74,14 @@
</a>
</li>
</ul>
{% endif %}
</div>
{% else %}
<div class="alert alert-danger">
<p>
{{ 'household_composition.Currently no composition'|trans }}
</p>
{% if is_granted('CHILL_PERSON_HOUSEHOLD_EDIT', household) %}
<ul class="record_actions" style="margin-bottom: 0">
<li>
<a class="btn btn-sm btn-update change-icon"
@@ -85,6 +90,7 @@
</a>
</li>
</ul>
{% endif %}
</div>
{% endif %}
{% if household.waitingForBirth or not household.commentMembers.isEmpty() %}
@@ -104,6 +110,7 @@
</div>
{% endif %}
{% if is_granted('CHILL_PERSON_HOUSEHOLD_EDIT', household) %}
{% if not household.commentMembers.isEmpty() %}
<a href="{{ chill_path_add_return_path('chill_person_household_summary', { 'household_id': household.id, 'edit': 1 }) }}"
class="btn btn-edit btn-block">
@@ -115,7 +122,7 @@
{{ 'household.New comment and expecting birth'|trans }}
</a>
{% endif %}
{% endif %}
{% else %}
{{ form_start(form) }}
@@ -167,6 +174,7 @@
{% macro customButtons(member, household) %}
{% if is_granted('CHILL_PERSON_HOUSEHOLD_EDIT', household) %}
<li>
<a href="{{ chill_path_add_return_path('chill_person_household_members_editor', {'persons': [ member.person.id ], 'allow_leave_without_household': true } ) }}"
class="btn btn-sm btn-misc" title="{{ 'household.person.leave'|trans }}"><i class="fa fa-scissors"></i></a>
@@ -175,6 +183,7 @@
<a href="{{ chill_path_add_return_path('chill_person_household_members_editor', {'persons': [ member.person.id ], 'household': household.id} ) }}"
class="btn btn-sm btn-misc" title="{{ 'household.Change position'|trans }}"><i class="fa fa-arrows-h"></i></a>
</li>
{% endif %}
{% endmacro %}
{% if members|length > 0 %}
@@ -244,6 +253,7 @@
{% if is_granted('CHILL_PERSON_HOUSEHOLD_EDIT', household) %}
<ul class="record_actions">
<li>
<a href="{{ chill_path_add_return_path('chill_person_household_members_editor', {'household': household.id }) }}"
@@ -252,7 +262,7 @@
</a>
</li>
</ul>
{% endif %}
</div>
{% endblock %}

View File

@@ -63,36 +63,6 @@
{%- endif -%}
</fieldset>
{%- if form.nationality is defined or form.spokenLanguages is defined -%}
<fieldset>
<legend><h2>{{ 'Administrative information'|trans }}</h2></legend>
{%- if form.nationality is defined -%}
{{ form_row(form.nationality, { 'label' : 'Nationality'|trans} ) }}
{%- endif -%}
{%- if form.spokenLanguages is defined -%}
{{ form_row(form.spokenLanguages, {'label' : 'Spoken languages'}) }}
{%- endif -%}
</fieldset>
{%- endif -%}
{%- if form.numberOfChildren is defined or form.maritalStatus is defined -%}
<fieldset>
<legend><h2>{{ 'Marital status'|trans }}</h2></legend>
{{ form_row(form.numberOfChildren, {'label' : 'Number of children'}) }}
{%- if form.maritalStatus is defined -%}
<div id="maritalStatus">
{{ form_row(form.maritalStatus, { 'label' : 'Marital status'} ) }}
</div>
<div id="maritalStatusDate">
{{ form_row(form.maritalStatusDate, { 'label' : 'Date of last marital status change'} ) }}
</div>
{%- endif -%}
<div id="maritalStatusComment">
{{ form_row(form.maritalStatusComment, { 'label' : 'Comment on the marital status'} ) }}
</div>
</fieldset>
{%- endif -%}
{%- if form.email is defined or form.phonenumber is defined or form.mobilenumber is defined or form.contactInfo is defined-%}
<fieldset>
<legend><h2>{{ 'Contact information'|trans }}</h2></legend>
@@ -128,6 +98,36 @@
</fieldset>
{%- endif -%}
{%- if form.nationality is defined or form.spokenLanguages is defined -%}
<fieldset>
<legend><h2>{{ 'Administrative information'|trans }}</h2></legend>
{%- if form.nationality is defined -%}
{{ form_row(form.nationality, { 'label' : 'Nationality'|trans} ) }}
{%- endif -%}
{%- if form.spokenLanguages is defined -%}
{{ form_row(form.spokenLanguages, {'label' : 'Spoken languages'}) }}
{%- endif -%}
</fieldset>
{%- endif -%}
{%- if form.numberOfChildren is defined or form.maritalStatus is defined -%}
<fieldset>
<legend><h2>{{ 'Marital status'|trans }}</h2></legend>
{{ form_row(form.numberOfChildren, {'label' : 'Number of children'}) }}
{%- if form.maritalStatus is defined -%}
<div id="maritalStatus">
{{ form_row(form.maritalStatus, { 'label' : 'Marital status'} ) }}
</div>
<div id="maritalStatusDate">
{{ form_row(form.maritalStatusDate, { 'label' : 'Date of last marital status change'} ) }}
</div>
{%- endif -%}
<div id="maritalStatusComment">
{{ form_row(form.maritalStatusComment, { 'label' : 'Comment on the marital status'} ) }}
</div>
</fieldset>
{%- endif -%}
{{ form_rest(form) }}
<ul class="record_actions sticky-form-buttons">

View File

@@ -119,11 +119,13 @@
<a href="{{ chill_path_add_return_path('chill_person_household_summary',{ 'household_id': p.household.id }) }}"
class="btn btn-show" title="{{ 'Show'|trans }}"></a>
</li>
{% if is_granted('CHILL_PERSON_HOUSEHOLD_EDIT', p.household) %}
<li>
<a href="{{ chill_path_add_return_path('chill_person_household_member_edit', { id: p.id }) }}"
class="btn btn-edit" title="{{ 'Edit'|trans }}"></a>
</li>
{% if p.isCurrent() %}
{% endif %}
{% if p.isCurrent() and is_granted('CHILL_PERSON_HOUSEHOLD_EDIT', p.household) %}
<li>
<a class="btn btn-misc" href="{{ chill_path_add_return_path( 'chill_person_household_members_editor', { 'persons': [ person.id ], 'allow_leave_without_household': true }) }}">
<i class="fa fa-scissors"></i>
@@ -138,7 +140,7 @@
{% endfor %}
</div>
{% if not person.isSharingHousehold() %}
{% if not person.isSharingHousehold() and is_granted('CHILL_PERSON_HOUSEHOLD_EDIT', p.household) %}
<ul class="record_actions">
<li>
<a class="btn btn-misc" href="{{chill_path_add_return_path('chill_person_household_members_editor', { 'persons': [ person.id ], 'followAfter': true}) }}">
@@ -164,6 +166,7 @@
{{ _self.bloc_content(p) }}
{% if is_granted('CHILL_PERSON_HOUSEHOLD_EDIT', p.household) %}
<div class="item-row separator">
<ul class="record_actions">
<li>
@@ -172,7 +175,7 @@
</li>
</ul>
</div>
{% endif %}
</div>
{% endfor %}
</div>

View File

@@ -34,7 +34,6 @@ class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleH
self::SEE_DETAILS,
self::CREATE,
self::EDIT,
self::DELETE,
self::FULL,
self::TOGGLE_CONFIDENTIAL_ALL,
self::TOGGLE_INTENSITY,
@@ -48,13 +47,6 @@ class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleH
public const CREATE = 'CHILL_PERSON_ACCOMPANYING_PERIOD_CREATE';
/**
* role to DELETE the course.
*
* Will be true only for the creator, and if the course is still at DRAFT step.
*/
public const DELETE = 'CHILL_PERSON_ACCOMPANYING_PERIOD_DELETE';
/**
* role to EDIT the course.
*
@@ -132,7 +124,6 @@ class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleH
self::CONFIDENTIAL_CRUD,
self::CREATE,
self::EDIT,
self::DELETE,
self::FULL,
self::TOGGLE_CONFIDENTIAL_ALL,
self::REASSIGN_BULK,
@@ -163,7 +154,7 @@ class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleH
if ($subject instanceof AccompanyingPeriod) {
if (AccompanyingPeriod::STEP_CLOSED === $subject->getStep()) {
if (in_array($attribute, [self::EDIT, self::DELETE], true)) {
if (in_array($attribute, [self::EDIT], true)) {
return false;
}

View File

@@ -14,6 +14,7 @@ namespace Chill\PersonBundle\Serializer\Normalizer;
use Chill\BudgetBundle\Service\Summary\SummaryBudgetInterface;
use Chill\DocGeneratorBundle\Serializer\Helper\NormalizeNullValueHelper;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Civility;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\PersonBundle\Entity\Household\Household;
@@ -87,6 +88,7 @@ class PersonDocGenNormalizer implements
$dateContext['docgen:expects'] = DateTimeInterface::class;
$addressContext = array_merge($context, ['docgen:expects' => Address::class]);
$phonenumberContext = array_merge($context, ['docgen:expects' => PhoneNumber::class]);
$centerContext = array_merge($context, ['docgen:expects' => Center::class]);
$personResourceContext = array_merge($context, [
'docgen:expects' => Person\PersonResource::class,
// we simplify the list of attributes for the embedded persons
@@ -139,6 +141,7 @@ class PersonDocGenNormalizer implements
'numberOfChildren' => (string) $person->getNumberOfChildren(),
'address' => $this->normalizer->normalize($person->getCurrentPersonAddress(), $format, $addressContext),
'resources' => $this->normalizer->normalize($person->getResources(), $format, $personResourceContext),
'center' => $this->normalizer->normalize($person->getCenter(), $format, $centerContext),
];
if ($context['docgen:person:with-household'] ?? false) {
@@ -240,6 +243,7 @@ class PersonDocGenNormalizer implements
$attributes = [
'id', 'firstName', 'lastName', 'age', 'altNames', 'text',
'center' => Center::class,
'civility' => Civility::class,
'birthdate' => DateTimeInterface::class,
'deathdate' => DateTimeInterface::class,

View File

@@ -13,16 +13,16 @@ namespace Chill\PersonBundle\Tests\Export\Aggregator\AccompanyingCourseAggregato
use Chill\MainBundle\Test\Export\AbstractAggregatorTest;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\JobAggregator;
use Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\UserJobAggregator;
use Doctrine\ORM\EntityManagerInterface;
/**
* @internal
* @coversNothing
*/
final class JobAggregatorTest extends AbstractAggregatorTest
final class UserJobAggregatorTest extends AbstractAggregatorTest
{
private JobAggregator $aggregator;
private UserJobAggregator $aggregator;
protected function setUp(): void
{

View File

@@ -40,6 +40,7 @@ final class PersonDocGenNormalizerTest extends KernelTestCase
private const BLANK = [
'id' => '',
'center' => '',
'firstName' => '',
'lastName' => '',
'altNames' => '',
@@ -64,6 +65,7 @@ final class PersonDocGenNormalizerTest extends KernelTestCase
'numberOfChildren' => '',
'age' => '@ignored',
'resources' => [],
'center' => '@ignored',
];
private NormalizerInterface $normalizer;

View File

@@ -0,0 +1,62 @@
<?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 Validator\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Templating\Entity\PersonRenderInterface;
use Chill\PersonBundle\Validator\Constraints\AccompanyingPeriod\ParticipationOverlap;
use Chill\PersonBundle\Validator\Constraints\AccompanyingPeriod\ParticipationOverlapValidator;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Chill\ThirdPartyBundle\Templating\Entity\ThirdPartyRender;
use Doctrine\Common\Collections\ArrayCollection;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
class ParticipationOverlapValidatorTest extends ConstraintValidatorTestCase
{
use ProphecyTrait;
protected function createValidator()
{
$personRender = $this->prophesize(PersonRenderInterface::class);
$personRender->renderString(Argument::is(Person::class), [])->willReturn('person');
$thirdPartyRender = $this->prophesize(ThirdPartyRender::class);
$thirdPartyRender->renderString(Argument::is(ThirdParty::class), [])->willReturn('thirdparty');
return new ParticipationOverlapValidator($personRender->reveal(), $thirdPartyRender->reveal());
}
public function testOneParticipation()
{
$period = new AccompanyingPeriod();
$person = new Person();
$collection = new ArrayCollection([
new AccompanyingPeriodParticipation($period, $person)
]);
$this->validator->validate($collection, $this->getConstraint());
$this->assertNoViolation();
}
/**
* @return mixed
*/
public function getConstraint()
{
return new ParticipationOverlap();
}
}

View File

@@ -136,7 +136,7 @@ services:
- { name: chill.export_aggregator, alias: accompanyingcourse_scope_aggregator }
chill.person.export.aggregator_referrer_job:
class: Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\JobAggregator
class: Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\UserJobAggregator
tags:
- { name: chill.export_aggregator, alias: accompanyingcourse_referrer_job_aggregator }

View File

@@ -1068,6 +1068,7 @@ export:
by_referrer:
Computation date for referrer: Date à laquelle le référent était actif
having_temporarily:
label: Qualité de la localisation
Having a temporarily location: Ayant une localisation temporaire
Having a person's location: Ayant une localisation auprès d'un usager
Calculation date: Date de la localisation