Merge branch 'testing' of gitlab.com:Chill-Projet/chill-bundles into testing

This commit is contained in:
Julie Lenaerts 2022-11-09 11:39:28 +01:00
commit a2e1228c09
116 changed files with 5990 additions and 572 deletions

View File

@ -5,69 +5,70 @@ Add condition with distinct alias on each export join clauses (Indicators + Filt
These are alias conventions :
| Entity | Join | Attribute | Alias |
|:----------------------------------------|:----------------------------------------|:-------------------------------------------|:----------------------------------|
| AccompanyingPeriod::class | | | acp |
| | AccompanyingPeriodWork::class | acp.works | acpw |
| | AccompanyingPeriodParticipation::class | acp.participations | acppart |
| | Location::class | acp.administrativeLocation | acploc |
| | ClosingMotive::class | acp.closingMotive | acpmotive |
| | UserJob::class | acp.job | acpjob |
| | Origin::class | acp.origin | acporigin |
| | Scope::class | acp.scopes | acpscope |
| | SocialIssue::class | acp.socialIssues | acpsocialissue |
| | User::class | acp.user | acpuser |
| | AccompanyingPeriopStepHistory::class | acp.stepHistories | acpstephistories |
| AccompanyingPeriodWork::class | | | acpw |
| | AccompanyingPeriodWorkEvaluation::class | acpw.accompanyingPeriodWorkEvaluations | workeval |
| | User::class | acpw.referrers | acpwuser |
| | SocialAction::class | acpw.socialAction | acpwsocialaction |
| | Goal::class | acpw.goals | goal |
| | Result::class | acpw.results | result |
| AccompanyingPeriodParticipation::class | | | acppart |
| | Person::class | acppart.person | partperson |
| AccompanyingPeriodWorkEvaluation::class | | | workeval |
| | Evaluation::class | workeval.evaluation | eval |
| Goal::class | | | goal |
| | Result::class | goal.results | goalresult |
| Person::class | | | person |
| | Center::class | person.center | center |
| | HouseholdMember::class | partperson.householdParticipations | householdmember |
| | MaritalStatus::class | person.maritalStatus | personmarital |
| | VendeePerson::class | | vp |
| | VendeePersonMineur::class | | vpm |
| ResidentialAddress::class | | | resaddr |
| | ThirdParty::class | resaddr.hostThirdParty | tparty |
| ThirdParty::class | | | tparty |
| | ThirdPartyCategory::class | tparty.categories | tpartycat |
| HouseholdMember::class | | | householdmember |
| | Household::class | householdmember.household | household |
| | Person::class | householdmember.person | memberperson |
| | | memberperson.center | membercenter |
| Household::class | | | household |
| | HouseholdComposition::class | household.compositions | composition |
| Activity::class | | | activity |
| | Person::class | activity.person | actperson |
| | AccompanyingPeriod::class | activity.accompanyingPeriod | acp |
| | Person::class | activity\_person\_having\_activity.person | person\_person\_having\_activity |
| | ActivityReason::class | activity\_person\_having\_activity.reasons | reasons\_person\_having\_activity |
| | ActivityType::class | activity.activityType | acttype |
| | Location::class | activity.location | actloc |
| | SocialAction::class | activity.socialActions | actsocialaction |
| | SocialIssue::class | activity.socialIssues | actsocialssue |
| | ThirdParty::class | activity.thirdParties | acttparty |
| | User::class | activity.user | actuser |
| | User::class | activity.users | actusers |
| | ActivityReason::class | activity.reasons | actreasons |
| | Center::class | actperson.center | actcenter |
| | Person::class | activity.createdBy | actcreator |
| ActivityReason::class | | | actreasons |
| | ActivityReasonCategory::class | actreason.category | actreasoncat |
| Calendar::class | | | cal |
| | CancelReason::class | cal.cancelReason | calcancel |
| | Location::class | cal.location | calloc |
| | User::class | cal.user | caluser |
| VendeePerson::class | | | vp |
| | SituationProfessionelle::class | vp.situationProfessionelle | vpprof |
| | StatutLogement::class | vp.statutLogement | vplog |
| | TempsDeTravail::class | vp.tempsDeTravail | vptt |
| Entity | Join | Attribute | Alias |
|:----------------------------------------|:----------------------------------------|:-------------------------------------------|:---------------------------------------|
| AccompanyingPeriod::class | | | acp |
| | AccompanyingPeriodWork::class | acp.works | acpw |
| | AccompanyingPeriodParticipation::class | acp.participations | acppart |
| | Location::class | acp.administrativeLocation | acploc |
| | ClosingMotive::class | acp.closingMotive | acpmotive |
| | UserJob::class | acp.job | acpjob |
| | Origin::class | acp.origin | acporigin |
| | Scope::class | acp.scopes | acpscope |
| | SocialIssue::class | acp.socialIssues | acpsocialissue |
| | User::class | acp.user | acpuser |
| | AccompanyingPeriopStepHistory::class | acp.stepHistories | acpstephistories |
| AccompanyingPeriodWork::class | | | acpw |
| | AccompanyingPeriodWorkEvaluation::class | acpw.accompanyingPeriodWorkEvaluations | workeval |
| | User::class | acpw.referrers | acpwuser |
| | SocialAction::class | acpw.socialAction | acpwsocialaction |
| | Goal::class | acpw.goals | goal |
| | Result::class | acpw.results | result |
| AccompanyingPeriodParticipation::class | | | acppart |
| | Person::class | acppart.person | partperson |
| AccompanyingPeriodWorkEvaluation::class | | | workeval |
| | Evaluation::class | workeval.evaluation | eval |
| Goal::class | | | goal |
| | Result::class | goal.results | goalresult |
| Person::class | | | person |
| | Center::class | person.center | center |
| | HouseholdMember::class | partperson.householdParticipations | householdmember |
| | MaritalStatus::class | person.maritalStatus | personmarital |
| | VendeePerson::class | | vp |
| | VendeePersonMineur::class | | vpm |
| | CurrentPersonAddress::class | person.currentPersonAddress | currentPersonAddress (on a given date) |
| ResidentialAddress::class | | | resaddr |
| | ThirdParty::class | resaddr.hostThirdParty | tparty |
| ThirdParty::class | | | tparty |
| | ThirdPartyCategory::class | tparty.categories | tpartycat |
| HouseholdMember::class | | | householdmember |
| | Household::class | householdmember.household | household |
| | Person::class | householdmember.person | memberperson |
| | | memberperson.center | membercenter |
| Household::class | | | household |
| | HouseholdComposition::class | household.compositions | composition |
| Activity::class | | | activity |
| | Person::class | activity.person | actperson |
| | AccompanyingPeriod::class | activity.accompanyingPeriod | acp |
| | Person::class | activity\_person\_having\_activity.person | person\_person\_having\_activity |
| | ActivityReason::class | activity\_person\_having\_activity.reasons | reasons\_person\_having\_activity |
| | ActivityType::class | activity.activityType | acttype |
| | Location::class | activity.location | actloc |
| | SocialAction::class | activity.socialActions | actsocialaction |
| | SocialIssue::class | activity.socialIssues | actsocialssue |
| | ThirdParty::class | activity.thirdParties | acttparty |
| | User::class | activity.user | actuser |
| | User::class | activity.users | actusers |
| | ActivityReason::class | activity.reasons | actreasons |
| | Center::class | actperson.center | actcenter |
| | Person::class | activity.createdBy | actcreator |
| ActivityReason::class | | | actreasons |
| | ActivityReasonCategory::class | actreason.category | actreasoncat |
| Calendar::class | | | cal |
| | CancelReason::class | cal.cancelReason | calcancel |
| | Location::class | cal.location | calloc |
| | User::class | cal.user | caluser |
| VendeePerson::class | | | vp |
| | SituationProfessionelle::class | vp.situationProfessionelle | vpprof |
| | StatutLogement::class | vp.statutLogement | vplog |
| | TempsDeTravail::class | vp.tempsDeTravail | vptt |

View File

@ -5,11 +5,6 @@ parameters:
count: 1
path: src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php
-
message: "#^Access to an undefined property Chill\\\\PersonBundle\\\\Entity\\\\Household\\\\PersonHouseholdAddress\\:\\:\\$relation\\.$#"
count: 1
path: src/Bundle/ChillPersonBundle/Entity/Household/PersonHouseholdAddress.php
-
message: "#^Access to an undefined property Chill\\\\PersonBundle\\\\Entity\\\\AccompanyingPeriod\\:\\:\\$work\\.$#"
count: 1
@ -30,11 +25,6 @@ parameters:
count: 1
path: src/Bundle/ChillPersonBundle/Serializer/Normalizer/MembersEditorNormalizer.php
-
message: "#^Undefined variable\\: \\$choiceSlug$#"
count: 1
path: src/Bundle/ChillPersonBundle/Export/Export/ListPerson.php
-
message: "#^Undefined variable\\: \\$choiceSlug$#"
count: 1

View File

@ -10,16 +10,6 @@ parameters:
count: 1
path: src/Bundle/ChillActivityBundle/Entity/ActivityReasonCategory.php
-
message: "#^Method Chill\\\\ActivityBundle\\\\Export\\\\Export\\\\StatActivityDuration\\:\\:getDescription\\(\\) should return string but return statement is missing\\.$#"
count: 1
path: src/Bundle/ChillActivityBundle/Export/Export/StatActivityDuration.php
-
message: "#^Method Chill\\\\ActivityBundle\\\\Export\\\\Export\\\\StatActivityDuration\\:\\:getTitle\\(\\) should return string but return statement is missing\\.$#"
count: 1
path: src/Bundle/ChillActivityBundle/Export/Export/StatActivityDuration.php
-
message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#"
count: 1
@ -330,21 +320,6 @@ parameters:
count: 6
path: src/Bundle/ChillPersonBundle/Command/ImportPeopleFromCSVCommand.php
-
message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#"
count: 1
path: src/Bundle/ChillPersonBundle/Export/Aggregator/CountryOfBirthAggregator.php
-
message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#"
count: 1
path: src/Bundle/ChillPersonBundle/Export/Aggregator/NationalityAggregator.php
-
message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#"
count: 2
path: src/Bundle/ChillPersonBundle/Export/Export/ListPerson.php
-
message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#"
count: 1

View File

@ -0,0 +1,68 @@
<?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\Aggregator\ACPAggregators;
use Chill\ActivityBundle\Entity\Activity;
use Chill\MainBundle\Export\AggregatorInterface;
use Chill\PersonBundle\Export\Declarations;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
class ByActivityNumberAggregator implements AggregatorInterface
{
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data): void
{
$qb
->addSelect('(SELECT COUNT(activity.id) FROM ' . Activity::class . ' activity WHERE activity.accompanyingPeriod = acp) AS activity_by_number_aggregator')
->addGroupBy('activity_by_number_aggregator');
}
public function applyOn(): string
{
return Declarations::ACP_TYPE;
}
public function buildForm(FormBuilderInterface $builder): void
{
// No form needed
}
public function getLabels($key, array $values, $data)
{
return static function ($value) {
if ('_header' === $value) {
return '';
}
if (null === $value) {
return '';
}
return $value;
};
}
public function getQueryKeys($data): array
{
return ['activity_by_number_aggregator'];
}
public function getTitle(): string
{
return 'Group acp by activity number';
}
}

View File

@ -0,0 +1,83 @@
<?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\Aggregator;
use Chill\ActivityBundle\Export\Declarations;
use Chill\MainBundle\Export\AggregatorInterface;
use Doctrine\ORM\QueryBuilder;
use LogicException;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class SentReceivedAggregator implements AggregatorInterface
{
private TranslatorInterface $translator;
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data): void
{
$qb->addSelect('activity.sentReceived AS activity_sentreceived_aggregator')
->addGroupBy('activity_sentreceived_aggregator');
}
public function applyOn(): string
{
return Declarations::ACTIVITY;
}
public function buildForm(FormBuilderInterface $builder): void
{
// No form needed
}
public function getLabels($key, array $values, $data): callable
{
return function (?string $value): string {
if ('_header' === $value) {
return 'export.aggregator.activity.by_sent_received.Sent or received';
}
switch ($value) {
case null:
return '';
case 'sent':
return $this->translator->trans('export.aggregator.activity.by_sent_received.is sent');
case 'received':
return $this->translator->trans('export.aggregator.activity.by_sent_received.is received');
default:
throw new LogicException(sprintf('The value %s is not valid', $value));
}
};
}
public function getQueryKeys($data): array
{
return ['activity_sentreceived_aggregator'];
}
public function getTitle(): string
{
return 'export.aggregator.activity.by_sent_received.Group activity by sentreceived';
}
}

View File

@ -0,0 +1,57 @@
<?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\Entity\Activity;
use Chill\MainBundle\Export\FilterInterface;
use Chill\PersonBundle\Export\Declarations;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
class HasNoActivityFilter implements FilterInterface
{
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
$qb
->andWhere('
NOT EXISTS (
SELECT 1 FROM ' . Activity::class . ' activity
WHERE activity.accompanyingPeriod = acp
)
');
}
public function applyOn(): string
{
return Declarations::ACP_TYPE;
}
public function buildForm(FormBuilderInterface $builder)
{
//no form needed
}
public function describeAction($data, $format = 'string'): array
{
return ['Filtered acp which has no activities', []];
}
public function getTitle(): string
{
return 'Filter acp which has no activity';
}
}

View File

@ -39,9 +39,9 @@ class UsersJobFilter implements FilterInterface
$qb
->andWhere(
$qb->expr()->exists(
'SELECT 1 FROM ' . Activity::class . ' activity_users_job_filter_act
'SELECT 1 FROM ' . Activity::class . ' activity_users_job_filter_act
JOIN activity_users_job_filter_act.users users WHERE users.userJob IN (:activity_users_job_filter_jobs) AND activity_users_job_filter_act = activity '
)
)
)
->setParameter('activity_users_job_filter_jobs', $data['jobs']);
}

View File

@ -369,8 +369,12 @@ final class ActivityControllerTest extends WebTestCase
$center
);
$reachableScopesId = array_intersect(
array_map(static function ($s) { return $s->getId(); }, $reachableScopesDelete),
array_map(static function ($s) { return $s->getId(); }, $reachableScopesUpdate)
array_map(static function ($s) {
return $s->getId();
}, $reachableScopesDelete),
array_map(static function ($s) {
return $s->getId();
}, $reachableScopesUpdate)
);
if (count($reachableScopesId) === 0) {

View File

@ -188,7 +188,9 @@ final class ActivityTypeTest extends KernelTestCase
// map all the values in an array
$values = array_map(
static function ($choice) { return $choice->value; },
static function ($choice) {
return $choice->value;
},
$view['activity']['durationTime']->vars['choices']
);

View File

@ -120,6 +120,10 @@ services:
tags:
- { name: chill.export_filter, alias: 'activity_usersscope_filter' }
Chill\ActivityBundle\Export\Filter\ACPFilters\HasNoActivityFilter:
tags:
- { name: chill.export_filter, alias: 'accompanyingcourse_has_no_activity_filter' }
## Aggregators
Chill\ActivityBundle\Export\Aggregator\PersonAggregators\ActivityReasonAggregator:
tags:
@ -179,3 +183,11 @@ services:
Chill\ActivityBundle\Export\Aggregator\ActivityUsersJobAggregator:
tags:
- { name: chill.export_aggregator, alias: activity_users_job_aggregator }
Chill\ActivityBundle\Export\Aggregator\ACPAggregators\ByActivityNumberAggregator:
tags:
- { name: chill.export_aggregator, alias: accompanyingcourse_by_activity_number_aggregator }
Chill\ActivityBundle\Export\Aggregator\SentReceivedAggregator:
tags:
- { name: chill.export_aggregator, alias: activity_sentreceived_aggregator }

View File

@ -260,8 +260,6 @@ activity is not emergency: l'activité n'est pas urgente
Filter activity by sentreceived: Filtrer les activités par envoyé/reçu
'Filtered activity by sentreceived: only %sentreceived%': "Filtré par envoyé/reçu: uniquement %sentreceived%"
Accepted sentreceived: ''
is sent: envoyé
is received: reçu
Filter activity by linked socialaction: Filtrer les activités par action liée
'Filtered activity by linked socialaction: only %actions%': "Filtré par action liée: uniquement %actions%"
Filter activity by linked socialissue: Filtrer les activités par problématique liée
@ -276,6 +274,10 @@ Creators: Créateurs
Filter activity by userscope: Filtrer les activités par service du créateur
'Filtered activity by userscope: only %scopes%': "Filtré par service du créateur: uniquement %scopes%"
Accepted userscope: Services
Filter acp which has no activity: Filtrer les parcours qui nont pas dactivité
Filtered acp which has no activities: Filtrer les parcours sans activité associée
Group acp by activity number: Grouper les parcours par nombre dactivité
#aggregators
Activity type: Type d'activité
@ -332,4 +334,10 @@ export:
by_usersscope:
Filter by users scope: Filtrer les activités par services d'au moins un utilisateur participant
'Filtered activity by users scope: only %scopes%': 'Filtré par service d''au moins un utilisateur participant: seulement %scopes%'
aggregator:
activity:
by_sent_received:
Sent or received: Envoyé ou reçu
is sent: envoyé
is received: reçu
Group activity by sentreceived: Grouper les activités par envoyé / reçu

View File

@ -30,6 +30,8 @@ final class ChillAsideActivityExtension extends Extension implements PrependExte
$loader->load('services.yaml');
$loader->load('services/form.yaml');
$loader->load('services/menu.yaml');
$loader->load('services/security.yaml');
$loader->load('services/export.yaml');
}
public function prepend(ContainerBuilder $container)

View File

@ -0,0 +1,82 @@
<?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\AsideActivityBundle\Repository\AsideActivityCategoryRepository;
use Chill\MainBundle\Export\AggregatorInterface;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
class ByActivityTypeAggregator implements AggregatorInterface
{
private AsideActivityCategoryRepository $asideActivityCategoryRepository;
private TranslatableStringHelper $translatableStringHelper;
public function __construct(AsideActivityCategoryRepository $asideActivityCategoryRepository, TranslatableStringHelper $translatableStringHelper)
{
$this->asideActivityCategoryRepository = $asideActivityCategoryRepository;
$this->translatableStringHelper = $translatableStringHelper;
}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
$qb->addSelect('IDENTITY(aside.type) AS by_aside_activity_type_aggregator')
->addGroupBy('by_aside_activity_type_aggregator');
}
public function applyOn(): string
{
return Declarations::ASIDE_ACTIVITY_TYPE;
}
public function buildForm(FormBuilderInterface $builder)
{
// No form needed
}
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) {
return '';
}
$t = $this->asideActivityCategoryRepository->find($value);
return $this->translatableStringHelper->localize($t->getTitle());
};
}
public function getQueryKeys($data): array
{
return ['by_aside_activity_type_aggregator'];
}
public function getTitle(): string
{
return 'export.aggregator.Group by aside activity type';
}
}

View File

@ -0,0 +1,20 @@
<?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;
/**
* This class declare constants used for the export framework.
*/
abstract class Declarations
{
public const ASIDE_ACTIVITY_TYPE = 'aside_activity';
}

View File

@ -0,0 +1,105 @@
<?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\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;
class CountAsideActivity 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.Count aside activities by various parameters.';
}
public function getGroup(): string
{
return 'export.Exports of aside activities';
}
public function getLabels($key, array $values, $data)
{
if ('export_result' !== $key) {
throw new LogicException("the key {$key} is not used by this export");
}
$labels = array_combine($values, $values);
$labels['_header'] = $this->getTitle();
return static function ($value) use ($labels) {
return $labels[$value];
};
}
public function getQueryKeys($data): array
{
return ['export_result'];
}
public function getResult($query, $data)
{
return $query->getQuery()->getResult(Query::HYDRATE_SCALAR);
}
public function getTitle(): string
{
return 'export.Count aside activities';
}
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('COUNT(DISTINCT aside.id) AS export_result');
return $qb;
}
public function requiredRole(): string
{
return AsideActivityVoter::STATS;
}
public function supportsModifiers(): array
{
return [];
}
}

View File

@ -0,0 +1,96 @@
<?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\AsideActivityCategory;
use Chill\AsideActivityBundle\Export\Declarations;
use Chill\AsideActivityBundle\Repository\AsideActivityCategoryRepository;
use Chill\AsideActivityBundle\Templating\Entity\CategoryRender;
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 ByActivityTypeFilter implements FilterInterface
{
private AsideActivityCategoryRepository $asideActivityTypeRepository;
private CategoryRender $categoryRender;
private TranslatableStringHelperInterface $translatableStringHelper;
public function __construct(
CategoryRender $categoryRender,
TranslatableStringHelperInterface $translatableStringHelper,
AsideActivityCategoryRepository $asideActivityTypeRepository
) {
$this->categoryRender = $categoryRender;
$this->asideActivityTypeRepository = $asideActivityTypeRepository;
$this->translatableStringHelper = $translatableStringHelper;
}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
$clause = $qb->expr()->in('aside.type', ':types');
$qb->andWhere($clause);
$qb->setParameter('types', $data['types']);
}
public function applyOn(): string
{
return Declarations::ASIDE_ACTIVITY_TYPE;
}
public function buildForm(FormBuilderInterface $builder)
{
$builder
->add('types', EntityType::class, [
'class' => AsideActivityCategory::class,
'choices' => $this->asideActivityTypeRepository->findAllActive(),
'required' => false,
'multiple' => true,
'expanded' => false,
'attr' => [
'class' => 'select2',
],
'choice_label' => function (AsideActivityCategory $category) {
$options = [];
return $this->categoryRender->renderString($category, $options);
},
]);
}
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()])
);
return ['export.filter.Filtered by aside activity type: only %type%', [
'%type%' => implode(', ', $types),
]];
}
public function getTitle(): string
{
return 'export.filter.Filter by aside activity type';
}
}

View File

@ -0,0 +1,131 @@
<?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\ChillDateType;
use Chill\MainBundle\Form\Type\Export\FilterType;
use DateTime;
use Doctrine\ORM\Query\Expr\Andx;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Contracts\Translation\TranslatorInterface;
class ByDateFilter implements FilterInterface
{
protected TranslatorInterface $translator;
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
public function addRole(): ?string
{
return null;
}
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->add('where', $where);
$qb->setParameter('date_from', $data['date_from']);
$qb->setParameter('date_to', $data['date_to']);
}
public function applyOn(): string
{
return Declarations::ASIDE_ACTIVITY_TYPE;
}
public function buildForm(FormBuilderInterface $builder)
{
$builder
->add('date_from', ChillDateType::class, [
'label' => 'export.filter.Aside activities after this date',
'data' => new DateTime(),
])
->add('date_to', ChillDateType::class, [
'label' => 'export.filter.Aside activities before this date',
'data' => new DateTime(),
]);
$builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) {
/** @var \Symfony\Component\Form\FormInterface $filterForm */
$filterForm = $event->getForm()->getParent();
$enabled = $filterForm->get(FilterType::ENABLED_FIELD)->getData();
if (true === $enabled) {
// if the filter is enabled, add some validation
$form = $event->getForm();
$date_from = $form->get('date_from')->getData();
$date_to = $form->get('date_to')->getData();
// check that fields are not empty
if (null === $date_from) {
$form->get('date_from')->addError(new FormError(
$this->translator->trans('This field '
. 'should not be empty')
));
}
if (null === $date_to) {
$form->get('date_to')->addError(new FormError(
$this->translator->trans('This field '
. 'should not be empty')
));
}
// check that date_from is before date_to
if (
(null !== $date_from && null !== $date_to)
&& $date_from >= $date_to
) {
$form->get('date_to')->addError(new FormError(
$this->translator->trans('export.filter.This date should be after '
. 'the date given in "Implied in an aside activity after '
. 'this date" field')
));
}
}
});
}
public function describeAction($data, $format = 'string'): array
{
return ['export.filter.Filtered by aside activities between %dateFrom% and %dateTo%', [
'%dateFrom%' => $data['date_from']->format('d-m-Y'),
'%dateTo%' => $data['date_to']->format('d-m-Y'),
]];
}
public function getTitle(): string
{
return 'export.filter.Filter by aside activity date';
}
}

View File

@ -38,6 +38,11 @@ class AsideActivityCategoryRepository implements ObjectRepository
return $this->repository->findAll();
}
public function findAllActive(): array
{
return $this->repository->findBy(['isActive' => true]);
}
/**
* @param mixed|null $limit
* @param mixed|null $offset

View File

@ -12,46 +12,20 @@ declare(strict_types=1);
namespace Chill\AsideActivityBundle\Repository;
use Chill\AsideActivityBundle\Entity\AsideActivity;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
final class AsideActivityRepository implements ObjectRepository
/**
* @method AsideActivity|null find($id, $lockMode = null, $lockVersion = null)
* @method AsideActivity|null findOneBy(array $criteria, array $orderBy = null)
* @method AsideActivity[] findAll()
* @method AsideActivity[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
final class AsideActivityRepository extends ServiceEntityRepository
{
private EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager)
public function __construct(ManagerRegistry $registry)
{
$this->repository = $entityManager->getRepository(AsideActivity::class);
}
public function find($id): ?AsideActivity
{
return $this->repository->find($id);
}
/**
* @return AsideActivity[]
*/
public function findAll(): array
{
return $this->repository->findAll();
}
/**
* @param mixed|null $limit
* @param mixed|null $offset
*
* @return AsideActivity[]
*/
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
public function findOneBy(array $criteria): ?AsideActivity
{
return $this->repository->findOneBy($criteria);
parent::__construct($registry, AsideActivity::class);
}
public function getClassName(): string

View File

@ -0,0 +1,79 @@
<?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\Security;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Security\Authorization\AbstractChillVoter;
use Chill\MainBundle\Security\Authorization\VoterHelperFactoryInterface;
use Chill\MainBundle\Security\Authorization\VoterHelperInterface;
use Chill\MainBundle\Security\ProvideRoleHierarchyInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
class AsideActivityVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface
{
public const STATS = 'CHILL_ASIDE_ACTIVITY_STATS';
private VoterHelperInterface $voterHelper;
public function __construct(
VoterHelperFactoryInterface $voterHelperFactory
) {
$this->voterHelper = $voterHelperFactory
->generate(self::class)
->addCheckFor(Center::class, [self::STATS])
->build();
}
/**
* @return string[]
*/
public function getRoles(): array
{
return $this->getAttributes();
}
/**
* @return string[][]
*/
public function getRolesWithHierarchy(): array
{
return ['Aside activity' => $this->getRoles()];
}
/**
* @return string[]
*/
public function getRolesWithoutScope(): array
{
return $this->getAttributes();
}
protected function supports($attribute, $subject)
{
return $this->voterHelper->supports($attribute, $subject);
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
{
if (!$token->getUser() instanceof User) {
return false;
}
return $this->voterHelper->voteOnAttribute($attribute, $subject, $token);
}
private function getAttributes(): array
{
return [self::STATS];
}
}

View File

@ -20,3 +20,33 @@ 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

@ -0,0 +1,27 @@
services:
_defaults:
autowire: true
autoconfigure: true
## Indicators
Chill\AsideActivityBundle\Export\Export\CountAsideActivity:
tags:
- { name: chill.export, alias: 'count_aside_activity' }
## Filters
chill.aside_activity.export.date_filter:
class: Chill\AsideActivityBundle\Export\Filter\ByDateFilter
tags:
- { name: chill.export_filter, alias: 'aside_activity_date_filter' }
chill.aside_activity.export.type_filter:
class: Chill\AsideActivityBundle\Export\Filter\ByActivityTypeFilter
tags:
- { name: chill.export_filter, alias: 'aside_activity_type_filter' }
## Aggregators
chill.aside_activity.export.type_aggregator:
class: Chill\AsideActivityBundle\Export\Aggregator\ByActivityTypeAggregator
tags:
- { name: chill.export_aggregator, alias: activity_type_aggregator }

View File

@ -0,0 +1,7 @@
services:
Chill\AsideActivityBundle\Security\AsideActivityVoter:
autowire: true
autoconfigure: true
tags:
- { name: security.voter }
- { name: chill.role }

View File

@ -166,3 +166,21 @@ Aside activities: Activités annexes
Aside activity types: Types d'activités annexes
Aside activity type configuration: Configuration des categories d'activités annexes
Aside activity configuration: Configuration des activités annexes
# exports
export:
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
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%"
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
aggregator:
Group by aside activity type: Grouper les activités annexes par type d'activité
Aside activity type: Type d'activité annexe

View File

@ -31,7 +31,9 @@ class ConfigRepository
public function getChargesKeys(bool $onlyActive = false): array
{
return array_map(static function ($element) { return $element['key']; }, $this->getCharges($onlyActive));
return array_map(static function ($element) {
return $element['key'];
}, $this->getCharges($onlyActive));
}
/**
@ -50,7 +52,9 @@ class ConfigRepository
public function getResourcesKeys(bool $onlyActive = false): array
{
return array_map(static function ($element) { return $element['key']; }, $this->getResources($onlyActive));
return array_map(static function ($element) {
return $element['key'];
}, $this->getResources($onlyActive));
}
/**
@ -70,14 +74,18 @@ class ConfigRepository
private function getCharges(bool $onlyActive = false): array
{
return $onlyActive ?
array_filter($this->charges, static function ($el) { return $el['active']; })
array_filter($this->charges, static function ($el) {
return $el['active'];
})
: $this->charges;
}
private function getResources(bool $onlyActive = false): array
{
return $onlyActive ?
array_filter($this->resources, static function ($el) { return $el['active']; })
array_filter($this->resources, static function ($el) {
return $el['active'];
})
: $this->resources;
}

View File

@ -61,7 +61,9 @@ class SummaryBudget implements SummaryBudgetInterface
];
}
$personIds = $household->getCurrentPersons()->map(static function (Person $p) { return $p->getId(); });
$personIds = $household->getCurrentPersons()->map(static function (Person $p) {
return $p->getId();
});
$ids = implode(', ', array_fill(0, count($personIds), '?'));
$parameters = [...$personIds, $household->getId()];

View File

@ -239,7 +239,9 @@ final class ParticipationControllerTest extends WebTestCase
$this->personsIdsCache = array_merge(
$this->personsIdsCache,
$event->getParticipations()->map(
static function ($p) { return $p->getPerson()->getId(); }
static function ($p) {
return $p->getPerson()->getId();
}
)
->toArray()
);
@ -303,7 +305,9 @@ final class ParticipationControllerTest extends WebTestCase
$event = $this->getRandomEventWithMultipleParticipations();
$persons_id = implode(',', $event->getParticipations()->map(
static function ($p) { return $p->getPerson()->getId(); }
static function ($p) {
return $p->getPerson()->getId();
}
)->toArray());
$crawler = $this->client->request(
@ -329,7 +333,9 @@ final class ParticipationControllerTest extends WebTestCase
$nbParticipations = $event->getParticipations()->count();
// get the persons_id participating on this event
$persons_id = $event->getParticipations()->map(
static function ($p) { return $p->getPerson()->getId(); }
static function ($p) {
return $p->getPerson()->getId();
}
)->toArray();
// exclude the existing persons_ids from the new person
$this->personsIdsCache = array_merge($this->personsIdsCache, $persons_id);

View File

@ -11,23 +11,32 @@ declare(strict_types=1);
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\Entity\SavedExport;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Export\ExportManager;
use Chill\MainBundle\Form\SavedExportType;
use Chill\MainBundle\Form\Type\Export\ExportType;
use Chill\MainBundle\Form\Type\Export\FormatterType;
use Chill\MainBundle\Form\Type\Export\PickCenterType;
use Chill\MainBundle\Redis\ChillRedis;
use Chill\MainBundle\Security\Authorization\SavedExportVoter;
use Doctrine\ORM\EntityManagerInterface;
use LogicException;
use Psr\Log\LoggerInterface;
use RedisException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
use function count;
use function serialize;
use function unserialize;
@ -38,35 +47,37 @@ use function unserialize;
*/
class ExportController extends AbstractController
{
private EntityManagerInterface $entityManager;
/**
* @var ExportManager
*/
protected $exportManager;
private $exportManager;
/**
* @var FormFactoryInterface
*/
protected $formFactory;
private $formFactory;
/**
* @var LoggerInterface
*/
protected $logger;
private $logger;
/**
* @var ChillRedis
*/
protected $redis;
private $redis;
/**
* @var SessionInterface
*/
protected $session;
private $session;
/**
* @var TranslatorInterface
*/
protected $translator;
private $translator;
public function __construct(
ChillRedis $chillRedis,
@ -74,8 +85,10 @@ class ExportController extends AbstractController
FormFactoryInterface $formFactory,
LoggerInterface $logger,
SessionInterface $session,
TranslatorInterface $translator
TranslatorInterface $translator,
EntityManagerInterface $entityManager
) {
$this->entityManager = $entityManager;
$this->redis = $chillRedis;
$this->exportManager = $exportManager;
$this->formFactory = $formFactory;
@ -142,6 +155,29 @@ class ExportController extends AbstractController
);
}
/**
* @Route("/{_locale}/exports/generate-from-saved/{id}", name="chill_main_export_generate_from_saved")
*
* @throws RedisException
*/
public function generateFromSavedExport(SavedExport $savedExport): RedirectResponse
{
$this->denyAccessUnlessGranted(SavedExportVoter::GENERATE, $savedExport);
$key = md5(uniqid((string) mt_rand(), false));
$this->redis->setEx($key, 3600, serialize($savedExport->getOptions()));
return $this->redirectToRoute(
'chill_main_export_download',
[
'alias' => $savedExport->getExportAlias(),
'key' => $key, 'prevent_save' => true,
'returnPath' => $this->generateUrl('chill_main_export_saved_list_my'),
]
);
}
/**
* Render the list of available exports.
*/
@ -203,6 +239,46 @@ class ExportController extends AbstractController
}
}
/**
* @Route("/{_locale}/export/save-from-key/{alias}/{key}", name="chill_main_export_save_from_key")
*/
public function saveFromKey(string $alias, string $key, Request $request): Response
{
$this->denyAccessUnlessGranted('ROLE_USER');
$user = $this->getUser();
if (!$user instanceof User) {
throw new AccessDeniedHttpException();
}
$data = $this->rebuildRawData($key);
$savedExport = new SavedExport();
$savedExport
->setOptions($data)
->setExportAlias($alias)
->setUser($user);
$form = $this->createForm(SavedExportType::class, $savedExport);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->entityManager->persist($savedExport);
$this->entityManager->flush();
return $this->redirectToRoute('chill_main_export_index');
}
return $this->render(
'@ChillMain/SavedExport/new.html.twig',
[
'form' => $form->createView(),
'saved_export' => $savedExport,
]
);
}
/**
* create a form to show on different steps.
*
@ -418,28 +494,7 @@ class ExportController extends AbstractController
protected function rebuildData($key)
{
if (null === $key) {
throw $this->createNotFoundException('key does not exists');
}
if ($this->redis->exists($key) !== 1) {
$this->addFlash('error', $this->translator->trans('This report is not available any more'));
throw $this->createNotFoundException('key does not exists');
}
$serialized = $this->redis->get($key);
if (false === $serialized) {
throw new LogicException('the key could not be reached from redis');
}
$rawData = unserialize($serialized);
$this->logger->notice('[export] choices for an export unserialized', [
'key' => $key,
'rawData' => json_encode($rawData),
]);
$rawData = $this->rebuildRawData($key);
$alias = $rawData['alias'];
@ -585,4 +640,32 @@ class ExportController extends AbstractController
throw new LogicException("the step {$step} is not defined.");
}
}
private function rebuildRawData(string $key): array
{
if (null === $key) {
throw $this->createNotFoundException('key does not exists');
}
if ($this->redis->exists($key) !== 1) {
$this->addFlash('error', $this->translator->trans('This report is not available any more'));
throw $this->createNotFoundException('key does not exists');
}
$serialized = $this->redis->get($key);
if (false === $serialized) {
throw new LogicException('the key could not be reached from redis');
}
$rawData = unserialize($serialized);
$this->logger->notice('[export] choices for an export unserialized', [
'key' => $key,
'rawData' => json_encode($rawData),
]);
return $rawData;
}
}

View File

@ -0,0 +1,185 @@
<?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\MainBundle\Controller;
use Chill\MainBundle\Entity\SavedExport;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Export\ExportInterface;
use Chill\MainBundle\Export\ExportManager;
use Chill\MainBundle\Export\GroupedExportInterface;
use Chill\MainBundle\Form\SavedExportType;
use Chill\MainBundle\Repository\SavedExportRepositoryInterface;
use Chill\MainBundle\Security\Authorization\SavedExportVoter;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Templating\EngineInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use function count;
class SavedExportController
{
private EntityManagerInterface $entityManager;
private ExportManager $exportManager;
private FormFactoryInterface $formFactory;
private SavedExportRepositoryInterface $savedExportRepository;
private Security $security;
private SessionInterface $session;
private EngineInterface $templating;
private TranslatorInterface $translator;
private UrlGeneratorInterface $urlGenerator;
public function __construct(
EngineInterface $templating,
EntityManagerInterface $entityManager,
ExportManager $exportManager,
FormFactoryInterface $formBuilder,
SavedExportRepositoryInterface $savedExportRepository,
Security $security,
SessionInterface $session,
TranslatorInterface $translator,
UrlGeneratorInterface $urlGenerator
) {
$this->exportManager = $exportManager;
$this->entityManager = $entityManager;
$this->formFactory = $formBuilder;
$this->savedExportRepository = $savedExportRepository;
$this->security = $security;
$this->session = $session;
$this->templating = $templating;
$this->translator = $translator;
$this->urlGenerator = $urlGenerator;
}
/**
* @Route("/{_locale}/exports/saved/{id}/delete", name="chill_main_export_saved_delete")
*/
public function delete(SavedExport $savedExport, Request $request): Response
{
if (!$this->security->isGranted(SavedExportVoter::DELETE, $savedExport)) {
throw new AccessDeniedHttpException();
}
$form = $this->formFactory->create();
$form->add('submit', SubmitType::class, ['label' => 'Delete']);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->entityManager->remove($savedExport);
$this->entityManager->flush();
$this->session->getFlashBag()->add('success', $this->translator->trans('saved_export.Export is deleted'));
return new RedirectResponse(
$this->urlGenerator->generate('chill_main_export_saved_list_my')
);
}
return new Response(
$this->templating->render(
'@ChillMain/SavedExport/delete.html.twig',
[
'saved_export' => $savedExport,
'delete_form' => $form->createView(),
]
)
);
}
/**
* @Route("/{_locale}/exports/saved/{id}/edit", name="chill_main_export_saved_edit")
*/
public function edit(SavedExport $savedExport, Request $request): Response
{
if (!$this->security->isGranted(SavedExportVoter::EDIT, $savedExport)) {
throw new AccessDeniedHttpException();
}
$form = $this->formFactory->create(SavedExportType::class, $savedExport);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->entityManager->flush();
$this->session->getFlashBag()->add('success', $this->translator->trans('saved_export.Saved export is saved!'));
return new RedirectResponse(
$this->urlGenerator->generate('chill_main_export_saved_list_my')
);
}
return new Response(
$this->templating->render(
'@ChillMain/SavedExport/edit.html.twig',
[
'form' => $form->createView(),
]
)
);
}
/**
* @Route("/{_locale}/exports/saved/my", name="chill_main_export_saved_list_my")
*/
public function list(): Response
{
$user = $this->security->getUser();
if (!$this->security->isGranted('ROLE_USER') || !$user instanceof User) {
throw new AccessDeniedHttpException();
}
$exports = $this->savedExportRepository->findByUser($user, ['title' => 'ASC']);
// group by center
/** @var array<string, array{saved: SavedExport, export: ExportInterface}> $exportsGrouped */
$exportsGrouped = [];
foreach ($exports as $savedExport) {
$export = $this->exportManager->getExport($savedExport->getExportAlias());
$exportsGrouped[
$export instanceof GroupedExportInterface
? $this->translator->trans($export->getGroup()) : '_'
][] = ['saved' => $savedExport, 'export' => $export];
}
ksort($exportsGrouped);
return new Response(
$this->templating->render(
'@ChillMain/SavedExport/index.html.twig',
[
'grouped_exports' => $exportsGrouped,
'total' => count($exports),
]
)
);
}
}

View File

@ -34,6 +34,8 @@ use Chill\MainBundle\Doctrine\DQL\Replace;
use Chill\MainBundle\Doctrine\DQL\Similarity;
use Chill\MainBundle\Doctrine\DQL\STContains;
use Chill\MainBundle\Doctrine\DQL\StrictWordSimilarityOPS;
use Chill\MainBundle\Doctrine\DQL\STX;
use Chill\MainBundle\Doctrine\DQL\STY;
use Chill\MainBundle\Doctrine\DQL\ToChar;
use Chill\MainBundle\Doctrine\DQL\Unaccent;
use Chill\MainBundle\Doctrine\ORM\Hydration\FlatHierarchyEntityHydrator;
@ -245,6 +247,8 @@ class ChillMainExtension extends Extension implements
'STRICT_WORD_SIMILARITY_OPS' => StrictWordSimilarityOPS::class,
'ST_CONTAINS' => STContains::class,
'JSONB_ARRAY_LENGTH' => JsonbArrayLength::class,
'ST_X' => STX::class,
'ST_Y' => STY::class,
],
'datetime_functions' => [
'EXTRACT' => Extract::class,

View File

@ -0,0 +1,37 @@
<?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\MainBundle\Doctrine\DQL;
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Lexer;
use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\SqlWalker;
class STX extends FunctionNode
{
private $field;
public function getSql(SqlWalker $sqlWalker)
{
return sprintf('ST_X(%s)', $this->field->dispatch($sqlWalker));
}
public function parse(Parser $parser)
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$this->field = $parser->ArithmeticExpression();
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
}

View File

@ -0,0 +1,37 @@
<?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\MainBundle\Doctrine\DQL;
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Lexer;
use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\SqlWalker;
class STY extends FunctionNode
{
private $field;
public function getSql(SqlWalker $sqlWalker)
{
return sprintf('ST_Y(%s)', $this->field->dispatch($sqlWalker));
}
public function parse(Parser $parser)
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$this->field = $parser->ArithmeticExpression();
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
}

View File

@ -0,0 +1,52 @@
<?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\MainBundle\Entity\GeographicalUnit;
/**
* Simple GeographialUnit Data Transfer Object.
*
* This allow to get access to id, unitName, unitRefId, and layer's id
*/
class SimpleGeographicalUnitDTO
{
/**
* @readonly
* @psalm-readonly
*/
public int $id;
/**
* @readonly
* @psalm-readonly
*/
public int $layerId;
/**
* @readonly
* @psalm-readonly
*/
public string $unitName;
/**
* @readonly
* @psalm-readonly
*/
public string $unitRefId;
public function __construct(int $id, string $unitName, string $unitRefId, int $layerId)
{
$this->id = $id;
$this->unitName = $unitName;
$this->unitRefId = $unitRefId;
$this->layerId = $layerId;
}
}

View File

@ -0,0 +1,133 @@
<?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\MainBundle\Entity;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
use Doctrine\ORM\Mapping as ORM;
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;
/**
* @ORM\Entity
* @ORM\Table(name="chill_main_saved_export")
*/
class SavedExport implements TrackCreationInterface, TrackUpdateInterface
{
use TrackCreationTrait;
use TrackUpdateTrait;
/**
* @ORM\Column(type="text", nullable=false, options={"default": ""})
*/
private string $description = '';
/**
* @ORM\Column(type="text", nullable=false, options={"default": ""})
*/
private string $exportAlias;
/**
* @ORM\Id
* @ORM\Column(name="id", type="uuid", unique="true")
* @ORM\GeneratedValue(strategy="NONE")
*/
private UuidInterface $id;
/**
* @ORM\Column(type="json", nullable=false, options={"default": "[]"})
*/
private array $options = [];
/**
* @ORM\Column(type="text", nullable=false, options={"default": ""})
*/
private string $title = '';
/**
* @ORM\ManyToOne(targetEntity=User::class)
*/
private User $user;
public function __construct()
{
$this->id = Uuid::uuid4();
}
public function getDescription(): string
{
return $this->description;
}
public function getExportAlias(): string
{
return $this->exportAlias;
}
public function getId(): UuidInterface
{
return $this->id;
}
public function getOptions(): array
{
return $this->options;
}
public function getTitle(): string
{
return $this->title;
}
public function getUser(): User
{
return $this->user;
}
public function setDescription(string $description): SavedExport
{
$this->description = $description;
return $this;
}
public function setExportAlias(string $exportAlias): SavedExport
{
$this->exportAlias = $exportAlias;
return $this;
}
public function setOptions(array $options): SavedExport
{
$this->options = $options;
return $this;
}
public function setTitle(string $title): SavedExport
{
$this->title = $title;
return $this;
}
public function setUser(User $user): SavedExport
{
$this->user = $user;
return $this;
}
}

View File

@ -20,11 +20,12 @@ use PhpOffice\PhpSpreadsheet\Shared\Date;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use RuntimeException;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use function array_key_exists;
use function array_keys;
use function array_map;
@ -80,7 +81,7 @@ class SpreadsheetListFormatter implements FormatterInterface
*
* @uses appendAggregatorForm
*
* @param type $exportAlias
* @param string $exportAlias
*/
public function buildForm(
FormBuilderInterface $builder,
@ -144,8 +145,6 @@ class SpreadsheetListFormatter implements FormatterInterface
$i = 1;
foreach ($result as $row) {
$line = [];
if (true === $this->formatterData['numerotation']) {
$worksheet->setCellValue('A' . ($i + 1), (string) $i);
}
@ -155,13 +154,22 @@ class SpreadsheetListFormatter implements FormatterInterface
foreach ($row as $key => $value) {
$row = $a . ($i + 1);
if ($value instanceof DateTimeInterface) {
$worksheet->setCellValue($row, Date::PHPToExcel($value));
$worksheet->getStyle($row)
->getNumberFormat()
->setFormatCode(NumberFormat::FORMAT_DATE_DDMMYYYY);
$formattedValue = $this->getLabel($key, $value);
if ($formattedValue instanceof DateTimeInterface) {
$worksheet->setCellValue($row, Date::PHPToExcel($formattedValue));
if ($formattedValue->format('His') === '000000') {
$worksheet->getStyle($row)
->getNumberFormat()
->setFormatCode(NumberFormat::FORMAT_DATE_DDMMYYYY);
} else {
$worksheet->getStyle($row)
->getNumberFormat()
->setFormatCode(NumberFormat::FORMAT_DATE_DATETIME);
}
} else {
$worksheet->setCellValue($row, $this->getLabel($key, $value));
$worksheet->setCellValue($row, $formattedValue);
}
++$a;
}
@ -259,6 +267,10 @@ class SpreadsheetListFormatter implements FormatterInterface
foreach ($keys as $key) {
// get an array with all values for this key if possible
$values = array_map(static function ($v) use ($key) {
if (!array_key_exists($key, $v)) {
throw new RuntimeException(sprintf('This key does not exists: %s. Available keys are %s', $key, implode(', ', array_keys($v))));
}
return $v[$key];
}, $this->result);
// store the label in the labelsCache property

View File

@ -0,0 +1,60 @@
<?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\MainBundle\Export\Helper;
use DateTime;
use Exception;
use Symfony\Contracts\Translation\TranslatorInterface;
class DateTimeHelper
{
private TranslatorInterface $translator;
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
public function getLabel($header): callable
{
return function ($value) use ($header) {
if ('_header' === $value) {
return $this->translator->trans($header);
}
if (null === $value) {
return '';
}
// warning: won't work with DateTimeImmutable as we reset time a few lines later
$date = DateTime::createFromFormat('Y-m-d', $value);
$hasTime = false;
if (false === $date) {
$date = DateTime::createFromFormat('Y-m-d H:i:s', $value);
$hasTime = true;
}
// check that the creation could occurs.
if (false === $date) {
throw new Exception(sprintf('The value %s could '
. 'not be converted to %s', $value, DateTime::class));
}
if (!$hasTime) {
$date->setTime(0, 0, 0);
}
return $date;
};
}
}

View File

@ -0,0 +1,277 @@
<?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\MainBundle\Export\Helper;
use Chill\MainBundle\Repository\AddressRepository;
use Chill\MainBundle\Templating\Entity\AddressRender;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Doctrine\ORM\QueryBuilder;
use LogicException;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use function in_array;
use function strlen;
/**
* Helps to load addresses and format them in list.
*/
class ExportAddressHelper
{
public const F_ALL =
self::F_ATTRIBUTES | self::F_BUILDING | self::F_COUNTRY |
self::F_GEOM | self::F_POSTAL_CODE | self::F_STREET;
public const F_AS_STRING = 0b00010000;
public const F_ATTRIBUTES = 0b01000000;
public const F_BUILDING = 0b00001000;
public const F_COUNTRY = 0b00000001;
public const F_GEOM = 0b00100000;
public const F_POSTAL_CODE = 0b00000010;
public const F_STREET = 0b00000100;
private const ALL = [
'country' => self::F_COUNTRY,
'postal_code' => self::F_POSTAL_CODE,
'street' => self::F_STREET,
'building' => self::F_BUILDING,
'string' => self::F_AS_STRING,
'geom' => self::F_GEOM,
'attributes' => self::F_ATTRIBUTES,
];
private const COLUMN_MAPPING = [
'country' => ['country'],
'postal_code' => ['postcode_code', 'postcode_name'],
'street' => ['street', 'streetNumber'],
'building' => ['buildingName', 'corridor', 'distribution', 'extra', 'flat', 'floor', 'steps'],
'string' => ['_as_string'],
'attributes' => ['isNoAddress', 'confidential', 'id'],
'geom' => ['_lat', '_lon'],
];
private AddressRender $addressRender;
private AddressRepository $addressRepository;
private PropertyAccessor $propertyAccess;
private TranslatableStringHelperInterface $translatableStringHelper;
public function __construct(
AddressRepository $addressRepository,
TranslatableStringHelperInterface $translatableStringHelper,
AddressRender $addressRender
) {
$this->addressRepository = $addressRepository;
$this->propertyAccess = PropertyAccess::createPropertyAccessor();
$this->translatableStringHelper = $translatableStringHelper;
$this->addressRender = $addressRender;
}
public function addSelectClauses(int $params, QueryBuilder $queryBuilder, $entityName = 'address', $prefix = 'add')
{
foreach (self::ALL as $key => $bitmask) {
if (($params & $bitmask) === $bitmask) {
foreach (self::COLUMN_MAPPING[$key] as $field) {
switch ($field) {
case 'id':
case '_as_string':
$queryBuilder->addSelect(sprintf('%s.id AS %s%s', $entityName, $prefix, $field));
break;
case 'street':
case 'streetNumber':
case 'floor':
case 'corridor':
case 'steps':
case 'buildingName':
case 'flat':
case 'distribution':
case 'extra':
$queryBuilder->addSelect(sprintf('%s.%s AS %s%s', $entityName, $field, $prefix, $field));
break;
case 'country':
case 'postcode_name':
case 'postcode_code':
$postCodeAlias = sprintf('%spostcode_t', $prefix);
if (!in_array($postCodeAlias, $queryBuilder->getAllAliases(), true)) {
$queryBuilder->leftJoin($entityName . '.postcode', $postCodeAlias);
}
if ('postcode_name' === $field) {
$queryBuilder->addSelect(sprintf('%s.%s AS %s%s', $postCodeAlias, 'name', $prefix, $field));
break;
}
if ('postcode_code' === $field) {
$queryBuilder->addSelect(sprintf('%s.%s AS %s%s', $postCodeAlias, 'code', $prefix, $field));
break;
}
$countryAlias = sprintf('%scountry_t', $prefix);
if (!in_array($countryAlias, $queryBuilder->getAllAliases(), true)) {
$queryBuilder->leftJoin(sprintf('%s.country', $postCodeAlias), $countryAlias);
}
$queryBuilder->addSelect(sprintf('%s.%s AS %s%s', $countryAlias, 'name', $prefix, $field));
break;
case 'isNoAddress':
case 'confidential':
$queryBuilder->addSelect(sprintf('CASE WHEN %s.%s = \'TRUE\' THEN 1 ELSE 0 END AS %s%s', $entityName, $field, $prefix, $field));
break;
case '_lat':
$queryBuilder->addSelect(sprintf('ST_Y(%s.point) AS %s%s', $entityName, $prefix, $field));
break;
case '_lon':
$queryBuilder->addSelect(sprintf('ST_X(%s.point) AS %s%s', $entityName, $prefix, $field));
break;
default:
throw new LogicException('This key is not supported: ' . $key);
}
}
}
}
}
/**
* @param self::F_* $params
*
* @return array|string[]
*/
public function getKeys(int $params, string $prefix = ''): array
{
$prefixes = [];
foreach (self::ALL as $key => $bitmask) {
if (($params & $bitmask) === $bitmask) {
$prefixes = array_merge(
$prefixes,
array_map(
static function ($item) use ($prefix) {
return $prefix . $item;
},
self::COLUMN_MAPPING[$key]
)
);
}
}
return $prefixes;
}
public function getLabel($key, array $values, $data, string $prefix = '', string $translationPrefix = 'export.address_helper.'): callable
{
$sanitizedKey = substr($key, strlen($prefix));
switch ($sanitizedKey) {
case 'id':
case 'street':
case 'streetNumber':
case 'buildingName':
case 'corridor':
case 'distribution':
case 'extra':
case 'flat':
case 'floor':
case '_lat':
case '_lon':
case 'steps':
case 'postcode_code':
case 'postcode_name':
return static function ($value) use ($sanitizedKey, $translationPrefix) {
if ('_header' === $value) {
return $translationPrefix . $sanitizedKey;
}
if (null === $value) {
return '';
}
return $value;
};
case 'country':
return function ($value) use ($key) {
if ('_header' === $value) {
return 'export.list.acp' . $key;
}
if (null === $value) {
return '';
}
return $this->translatableStringHelper->localize(json_decode($value, true));
};
case 'isNoAddress':
case 'confidential':
return static function ($value) use ($sanitizedKey, $translationPrefix) {
if ('_header' === $value) {
return $translationPrefix . $sanitizedKey;
}
switch ($value) {
case null:
return '';
case true:
return 1;
case false:
return 0;
default:
throw new LogicException('this value is not supported for ' . $sanitizedKey . ': ' . $value);
}
};
case '_as_string':
return function ($value) use ($sanitizedKey, $translationPrefix) {
if ('_header' === $value) {
return $translationPrefix . $sanitizedKey;
}
if (null === $value) {
return '';
}
$address = $this->addressRepository->find($value);
return $this->addressRender->renderString($address, []);
};
default:
throw new LogicException('this key is not supported: ' . $sanitizedKey);
}
}
}

View File

@ -0,0 +1,43 @@
<?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\MainBundle\Export\Helper;
use Chill\MainBundle\Repository\UserRepositoryInterface;
use Chill\MainBundle\Templating\Entity\UserRender;
class UserHelper
{
private UserRender $userRender;
private UserRepositoryInterface $userRepository;
public function __construct(UserRender $userRender, UserRepositoryInterface $userRepository)
{
$this->userRender = $userRender;
$this->userRepository = $userRepository;
}
public function getLabel($key, array $values, string $header): callable
{
return function ($value) use ($header) {
if ('_header' === $value) {
return $header;
}
if (null === $value || null === $user = $this->userRepository->find($value)) {
return '';
}
return $this->userRender->renderString($user, []);
};
}
}

View File

@ -0,0 +1,45 @@
<?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\MainBundle\Form\DataMapper;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Symfony\Component\Form\DataMapperInterface;
use Symfony\Component\Form\Exception;
class RollingDateDataMapper implements DataMapperInterface
{
public function mapDataToForms($viewData, $forms)
{
if (null === $viewData) {
return;
}
if (!$viewData instanceof RollingDate) {
throw new Exception\UnexpectedTypeException($viewData, RollingDate::class);
}
$forms = iterator_to_array($forms);
$forms['roll']->setData($viewData->getRoll());
$forms['fixedDate']->setData($viewData->getFixedDate());
}
public function mapFormsToData($forms, &$viewData): void
{
$forms = iterator_to_array($forms);
$viewData = new RollingDate(
$forms['roll']->getData(),
$forms['fixedDate']->getData()
);
}
}

View File

@ -0,0 +1,40 @@
<?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\MainBundle\Form;
use Chill\MainBundle\Entity\SavedExport;
use Chill\MainBundle\Form\Type\ChillTextareaType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class SavedExportType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title', TextType::class, [
'required' => true,
])
->add('description', ChillTextareaType::class, [
'required' => false,
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'class' => SavedExport::class,
]);
}
}

View File

@ -51,7 +51,9 @@ class EntityToJsonTransformer implements DataTransformerInterface
}
return array_map(
function ($item) { return $this->denormalizeOne($item); },
function ($item) {
return $this->denormalizeOne($item);
},
$denormalized
);
}

View File

@ -0,0 +1,66 @@
<?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\MainBundle\Form\Type;
use Chill\MainBundle\Form\DataMapper\RollingDateDataMapper;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Callback;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
class PickRollingDateType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('roll', ChoiceType::class, [
'choices' => array_combine(
array_map(static fn (string $item) => 'rolling_date.' . $item, RollingDate::ALL_T),
RollingDate::ALL_T
),
'multiple' => false,
'expanded' => false,
'label' => 'rolling_date.roll_movement',
])
->add('fixedDate', ChillDateType::class, [
'input' => 'datetime_immutable',
'label' => 'rolling_date.fixed_date_date',
]);
$builder->setDataMapper(new RollingDateDataMapper());
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'class' => RollingDate::class,
'empty_data' => new RollingDate(RollingDate::T_TODAY),
'constraints' => [
new Callback([$this, 'validate']),
],
]);
}
public function validate($data, ExecutionContextInterface $context, $payload): void
{
/** @var RollingDate $data */
if (RollingDate::T_FIXED_DATE === $data->getRoll() && null === $data->getFixedDate()) {
$context
->buildViolation('rolling_date.When fixed date is selected, you must provide a date')
->atPath('roll')
->addViolation();
}
}
}

View File

@ -66,7 +66,9 @@ class ScopePickerType extends AbstractType
$options['role'] instanceof Role ? $options['role']->getRole() : $options['role'],
$options['center']
),
static function (Scope $s) { return $s->isActive(); }
static function (Scope $s) {
return $s->isActive();
}
);
if (0 === count($items)) {

View File

@ -12,19 +12,40 @@ declare(strict_types=1);
namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\Civility;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
/**
* @method Civility|null find($id, $lockMode = null, $lockVersion = null)
* @method Civility|null findOneBy(array $criteria, array $orderBy = null)
* @method Civility[] findAll()
* @method Civility[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class CivilityRepository extends ServiceEntityRepository
class CivilityRepository implements CivilityRepositoryInterface
{
public function __construct(ManagerRegistry $registry)
private EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager)
{
parent::__construct($registry, Civility::class);
$this->repository = $entityManager->getRepository($this->getClassName());
}
public function find($id): ?Civility
{
return $this->repository->find($id);
}
public function findAll(): array
{
return $this->repository->findAll();
}
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
public function findOneBy(array $criteria): ?Civility
{
return $this->repository->findOneBy($criteria);
}
public function getClassName(): string
{
return Civility::class;
}
}

View File

@ -0,0 +1,34 @@
<?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\MainBundle\Repository;
use Chill\MainBundle\Entity\Civility;
use Doctrine\Persistence\ObjectRepository;
interface CivilityRepositoryInterface extends ObjectRepository
{
public function find($id): ?Civility;
/**
* @return array|Civility[]
*/
public function findAll(): array;
/**
* @return array|Civility[]
*/
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array;
public function findOneBy(array $criteria): ?Civility;
public function getClassName(): string;
}

View File

@ -41,7 +41,7 @@ class GeographicalUnitRepository implements GeographicalUnitRepositoryInterface
{
return $this->repository
->createQueryBuilder('gu')
->select('PARTIAL gu.{id,unitName,unitRefId,layer}')
->select(sprintf('NEW %s(gu.id, gu.unitName, gu.unitRefId, IDENTITY(gu.layer))', GeographicalUnit\SimpleGeographicalUnitDTO::class))
->addOrderBy('IDENTITY(gu.layer)')
->addOrderBy(('gu.unitName'))
->getQuery()

View File

@ -14,15 +14,14 @@ namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\Language;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository;
final class LanguageRepository implements ObjectRepository
final class LanguageRepository implements LanguageRepositoryInterface
{
private EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager)
{
$this->repository = $entityManager->getRepository(Language::class);
$this->repository = $entityManager->getRepository($this->getClassName());
}
public function find($id, $lockMode = null, $lockVersion = null): ?Language
@ -54,7 +53,7 @@ final class LanguageRepository implements ObjectRepository
return $this->repository->findOneBy($criteria, $orderBy);
}
public function getClassName()
public function getClassName(): string
{
return Language::class;
}

View File

@ -0,0 +1,37 @@
<?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\MainBundle\Repository;
use Chill\MainBundle\Entity\Language;
use Doctrine\Persistence\ObjectRepository;
interface LanguageRepositoryInterface extends ObjectRepository
{
public function find($id, $lockMode = null, $lockVersion = null): ?Language;
/**
* @return Language[]
*/
public function findAll(): array;
/**
* @param mixed|null $limit
* @param mixed|null $offset
*
* @return Language[]
*/
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array;
public function findOneBy(array $criteria, ?array $orderBy = null): ?Language;
public function getClassName(): string;
}

View File

@ -0,0 +1,82 @@
<?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\MainBundle\Repository;
use Chill\MainBundle\Entity\SavedExport;
use Chill\MainBundle\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository;
/**
* @implements ObjectRepository<SavedExport>
*/
class SavedExportRepository implements SavedExportRepositoryInterface
{
private EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager)
{
$this->repository = $entityManager->getRepository($this->getClassName());
}
public function find($id): ?SavedExport
{
return $this->repository->find($id);
}
/**
* @return array|SavedExport[]
*/
public function findAll(): array
{
return $this->repository->findAll();
}
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
public function findByUser(User $user, ?array $orderBy = [], ?int $limit = null, ?int $offset = null): array
{
$qb = $this->repository->createQueryBuilder('se');
$qb
->where($qb->expr()->eq('se.user', ':user'))
->setParameter('user', $user);
if (null !== $limit) {
$qb->setMaxResults($limit);
}
if (null !== $offset) {
$qb->setFirstResult($offset);
}
foreach ($orderBy as $field => $order) {
$qb->addOrderBy('se.' . $field, $order);
}
return $qb->getQuery()->getResult();
}
public function findOneBy(array $criteria): ?SavedExport
{
return $this->repository->findOneBy($criteria);
}
public function getClassName(): string
{
return SavedExport::class;
}
}

View File

@ -0,0 +1,40 @@
<?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\MainBundle\Repository;
use Chill\MainBundle\Entity\SavedExport;
use Chill\MainBundle\Entity\User;
use Doctrine\Persistence\ObjectRepository;
/**
* @implements ObjectRepository<SavedExport>
*/
interface SavedExportRepositoryInterface extends ObjectRepository
{
public function find($id): ?SavedExport;
/**
* @return array|SavedExport[]
*/
public function findAll(): array;
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array;
/**
* @return array|SavedExport[]
*/
public function findByUser(User $user, ?array $orderBy = [], ?int $limit = null, ?int $offset = null): array;
public function findOneBy(array $criteria): ?SavedExport;
public function getClassName(): string;
}

View File

@ -0,0 +1,12 @@
<ul class="nav nav-pills justify-content-center">
<li class="nav-item">
<a href="{{ chill_path_forward_return_path('chill_main_export_index') }}" class="nav-link {% if current == 'common' %}active{% endif %}">
{{ 'Exports list'|trans }}
</a>
</li>
<li class="nav-item">
<a href="{{ chill_path_forward_return_path('chill_main_export_saved_list_my') }}" class="nav-link {% if current == 'my' %}active{% endif %}">
{{ 'saved_export.My saved exports'|trans }}
</a>
</li>
</ul>

View File

@ -49,5 +49,14 @@ window.addEventListener("DOMContentLoaded", function(e) {
data-download-text="{{ "Download your report"|trans|escape('html_attr') }}"
><span id="waiting_text">{{ "Waiting for your report"|trans ~ '...' }}</span></div>
</div>
<ul class="record_actions sticky-form-buttons">
<li class="cancel"><a href="{{ chill_return_path_or('chill_main_export_index') }}" class="btn btn-cancel">{{ 'Back to the list'|trans }}</a></li>
{% if not app.request.query.has('prevent_save') %}
<li>
<a href="{{ chill_path_add_return_path('chill_main_export_save_from_key', { alias: alias, key: app.request.query.get('key')}) }}" class="btn btn-save">{{ 'Save'|trans }}</a>
</li>
{% endif %}
</ul>
</div>
{% endblock content %}

View File

@ -22,9 +22,11 @@
{% block content %}
{{ include('@ChillMain/Export/_navbar.html.twig', {'current' : 'common'}) }}
<div class="col-md-10">
<h1>{{ 'Exports list'|trans }}</h1>
<div class="container mt-4">
{% for group, exports in grouped_exports %}{% if group != '_' %}
@ -32,13 +34,17 @@
<div class="row grouped">
{% for export_alias, export in exports %}
<div class="col-6 col-md-4 mb-3">
<h2>{{ export.title|trans }}</h2>
<p>{{ export.description|trans }}</p>
<p>
<a class="btn btn-action" href="{{ path('chill_main_export_new', { 'alias': export_alias } ) }}">
{{ 'Create an export'|trans }}
</a>
</p>
<div class="card">
<div class="card-body">
<h2 class="card-title">{{ export.title|trans }}</h2>
<p class="card-text">{{ export.description|trans }}</p>
<p>
<a class="btn btn-action" href="{{ path('chill_main_export_new', { 'alias': export_alias } ) }}">
{{ 'Create an export'|trans }}
</a>
</p>
</div>
</div>
</div>
{% endfor %}
</div>
@ -52,13 +58,17 @@
{% for export_alias,export in grouped_exports['_'] %}
<div class="col-6 col-md-4 mb-3">
<h2>{{ export.title|trans }}</h2>
<p>{{ export.description|trans }}</p>
<p>
<a class="btn btn-action" href="{{ path('chill_main_export_new', { 'alias': export_alias } ) }}">
{{ 'Create an export'|trans }}
</a>
</p>
<div class="card">
<div class="card-body">
<h2 class="card-title">{{ export.title|trans }}</h2>
<p class="card-text">{{ export.description|trans }}</p>
<p>
<a class="btn btn-action" href="{{ path('chill_main_export_new', { 'alias': export_alias } ) }}">
{{ 'Create an export'|trans }}
</a>
</p>
</div>
</div>
</div>
{% endfor %}

View File

@ -0,0 +1,25 @@
{% extends '@ChillMain/layout.html.twig' %}
{% block title 'saved_export.Delete saved ?'|trans %}
{% block display_content %}
<div class="col-10">
<h3>{{ saved_export.title }}</h3>
<p>{{ saved_export.description|chill_markdown_to_html }}</p>
</div>
{% endblock %}
{% block content %}
<div class="container chill-md-10">
{{ include('@ChillMain/Util/confirmation_template.html.twig',
{
'title' : 'saved_export.Delete saved ?'|trans,
'confirm_question' : 'saved_export.Are you sure you want to delete this saved ?'|trans,
'display_content' : block('display_content'),
'cancel_route' : 'chill_main_export_saved_list_my',
'cancel_parameters' : {},
'form' : delete_form
} ) }}
</div>
{% endblock %}

View File

@ -0,0 +1,21 @@
{% extends "@ChillMain/layout.html.twig" %}
{% block title %}{{ 'saved_export.Edit'|trans }}{% endblock %}
{% block content %}
<h1>{{ block('title') }}</h1>
{{ form_start(form) }}
{{ form_row(form.title) }}
{{ form_row(form.description) }}
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ chill_return_path_or('chill_main_homepage') }}" class="btn btn-cancel">{{ 'Cancel'|trans }}</a>
</li>
<li>
<button type="submit" class="btn btn-save">{{ 'Save'|trans }}</button>
</li>
</ul>
{{ form_end(form) }}
{% endblock %}

View File

@ -0,0 +1,82 @@
{% extends "@ChillMain/layout.html.twig" %}
{% block title %}{{ 'saved_export.My saved exports'|trans }}{% endblock %}
{% block content %}
<div class="col-md-10">
{{ include('@ChillMain/Export/_navbar.html.twig', {'current' : 'my'}) }}
<div class="container mt-4">
{% if total == 0 %}
<p class="chill-no-data-statement" >{{ 'saved_export.Any saved export'|trans }}</p>
{% endif %}
{% for group, saveds in grouped_exports %}
{% if group != '_' %}
<h2 class="display-6">{{ group }}</h2>
<div class="row grouped">
{% for s in saveds %}
<div class="col-6 col-md-4 mb-3">
<div class="card">
<div class="card-body">
<h2 class="card-title">{{ s.saved.title }}</h2>
<p class="card-subtitle"><strong>{{ s.export.title|trans }}</strong></p>
<div class="card-text">
{{ s.saved.description|chill_markdown_to_html }}
</div>
<div class="createdBy">{{ 'saved_export.Created on %date%'|trans({'%date%': s.saved.createdAt|format_datetime('long', 'short')}) }}</div>
<ul class="record_actions">
<li><a href="{{ chill_path_add_return_path('chill_main_export_saved_delete', {'id': s.saved.id }) }}" class="btn btn-delete"></a></li>
<li><a href="{{ chill_path_add_return_path('chill_main_export_saved_edit', {'id': s.saved.id }) }}" class="btn btn-edit"></a></li>
<li><a href="{{ path('chill_main_export_generate_from_saved', { id: s.saved.id }) }}" class="btn btn-action"><i class="fa fa-cog"></i></a></li>
</ul>
</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
{% endfor %}
{% if grouped_exports|keys|length > 1 and grouped_exports['_']|length > 0 %}
<h2 class="display-6">{{ 'Ungrouped exports'|trans }}</h2>
{% endif %}
<div class="row ungrouped">
{% for saveds in grouped_exports['_']|default([]) %}
{% for s in saveds %}
<div class="col-6 col-md-4 mb-3">
<div class="col-6 col-md-4 mb-3">
<div class="card">
<div class="card-body">
<h2 class="card-title">{{ s.saved.title }}</h2>
<p class="card-subtitle"><strong>{{ s.export.title|trans }}</strong></p>
<div class="card-text">
{{ s.saved.description|chill_markdown_to_html }}
</div>
<div class="createdBy">{{ 'saved_export.Created on %date%'|trans({'%date%': s.saved.createdAt|format_datetime('long', 'short')}) }}</div>
<ul class="record_actions">
<li><a href="{{ chill_path_add_return_path('chill_main_export_saved_delete', {'id': s.saved.id }) }}" class="btn btn-delete"></a></li>
<li><a href="{{ chill_path_add_return_path('chill_main_export_saved_edit', {'id': s.saved.id }) }}" class="btn btn-edit"></a></li>
<li><a href="{{ path('chill_main_export_generate_from_saved', { id: s.saved.id }) }}" class="btn btn-action"><i class="fa fa-cog"></i></a></li>
</ul>
</div>
</div>
</div>
</div>
{% endfor %}
{% endfor %}
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,21 @@
{% extends "@ChillMain/layout.html.twig" %}
{% block title %}{{ 'saved_export.New'|trans }}{% endblock %}
{% block content %}
<h1>{{ block('title') }}</h1>
{{ form_start(form) }}
{{ form_row(form.title) }}
{{ form_row(form.description) }}
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ chill_return_path_or('chill_main_homepage') }}" class="btn btn-cancel">{{ 'Cancel'|trans }}</a>
</li>
<li>
<button type="submit" class="btn btn-save">{{ 'Save'|trans }}</button>
</li>
</ul>
{{ form_end(form) }}
{% endblock %}

View File

@ -0,0 +1,52 @@
<?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\MainBundle\Security\Authorization;
use Chill\MainBundle\Entity\SavedExport;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use UnexpectedValueException;
use function in_array;
class SavedExportVoter extends Voter
{
public const DELETE = 'CHLL_MAIN_EXPORT_SAVED_DELETE';
public const EDIT = 'CHLL_MAIN_EXPORT_SAVED_EDIT';
public const GENERATE = 'CHLL_MAIN_EXPORT_SAVED_GENERATE';
private const ALL = [
self::DELETE,
self::EDIT,
self::GENERATE,
];
protected function supports($attribute, $subject): bool
{
return $subject instanceof SavedExport && in_array($attribute, self::ALL, true);
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
{
/** @var SavedExport $subject */
switch ($attribute) {
case self::DELETE:
case self::EDIT:
case self::GENERATE:
return $subject->getUser() === $token->getUser();
default:
throw new UnexpectedValueException('attribute not supported: ' . $attribute);
}
}
}

View File

@ -0,0 +1,101 @@
<?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\MainBundle\Service\RollingDate;
use DateTimeImmutable;
class RollingDate
{
public const ALL_T = [
self::T_YEAR_PREVIOUS_START,
self::T_QUARTER_PREVIOUS_START,
self::T_MONTH_PREVIOUS_START,
self::T_WEEK_PREVIOUS_START,
self::T_YEAR_CURRENT_START,
self::T_QUARTER_CURRENT_START,
self::T_MONTH_CURRENT_START,
self::T_WEEK_CURRENT_START,
self::T_TODAY,
self::T_WEEK_NEXT_START,
self::T_MONTH_NEXT_START,
self::T_QUARTER_NEXT_START,
self::T_YEAR_NEXT_START,
self::T_FIXED_DATE,
];
/**
* A given fixed date.
*/
public const T_FIXED_DATE = 'fixed_date';
public const T_MONTH_CURRENT_START = 'month_current_start';
public const T_MONTH_NEXT_START = 'month_next_start';
public const T_MONTH_PREVIOUS_START = 'month_previous_start';
public const T_QUARTER_CURRENT_START = 'quarter_current_start';
public const T_QUARTER_NEXT_START = 'quarter_next_start';
public const T_QUARTER_PREVIOUS_START = 'quarter_previous_start';
public const T_TODAY = 'today';
public const T_WEEK_CURRENT_START = 'week_current_start';
public const T_WEEK_NEXT_START = 'week_next_start';
public const T_WEEK_PREVIOUS_START = 'week_previous_start';
public const T_YEAR_CURRENT_START = 'year_current_start';
public const T_YEAR_NEXT_START = 'year_next_start';
public const T_YEAR_PREVIOUS_START = 'year_previous_start';
private ?DateTimeImmutable $fixedDate;
/**
* The pivot date is the date from the rolling is computed. By default, it is "now".
*/
private DateTimeImmutable $pivotDate;
private string $roll;
/**
* @param string|self::T_* $roll
* @param DateTimeImmutable|null $pivotDate Will be "now" if null is given
* @param DateTimeImmutable|null $fixedDate Only to insert if $roll equals @see{self::T_FIXED_DATE}
*/
public function __construct(string $roll, ?DateTimeImmutable $fixedDate = null, ?DateTimeImmutable $pivotDate = null)
{
$this->roll = $roll;
$this->pivotDate = $pivotDate ?? new DateTimeImmutable('now');
$this->fixedDate = $fixedDate;
}
public function getFixedDate(): ?DateTimeImmutable
{
return $this->fixedDate;
}
public function getPivotDate(): DateTimeImmutable
{
return $this->pivotDate;
}
public function getRoll(): string
{
return $this->roll;
}
}

View File

@ -0,0 +1,142 @@
<?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\MainBundle\Service\RollingDate;
use DateInterval;
use DateTimeImmutable;
use LogicException;
use UnexpectedValueException;
class RollingDateConverter implements RollingDateConverterInterface
{
public function convert(RollingDate $rollingDate): DateTimeImmutable
{
switch ($rollingDate->getRoll()) {
case RollingDate::T_MONTH_CURRENT_START:
return $this->toBeginOfMonth($rollingDate->getPivotDate());
case RollingDate::T_MONTH_NEXT_START:
return $this->toBeginOfMonth($rollingDate->getPivotDate()->add(new DateInterval('P1M')));
case RollingDate::T_MONTH_PREVIOUS_START:
return $this->toBeginOfMonth($rollingDate->getPivotDate()->sub(new DateInterval('P1M')));
case RollingDate::T_QUARTER_CURRENT_START:
return $this->toBeginOfQuarter($rollingDate->getPivotDate());
case RollingDate::T_QUARTER_NEXT_START:
return $this->toBeginOfQuarter($rollingDate->getPivotDate()->add(new DateInterval('P3M')));
case RollingDate::T_QUARTER_PREVIOUS_START:
return $this->toBeginOfQuarter($rollingDate->getPivotDate()->sub(new DateInterval('P3M')));
case RollingDate::T_WEEK_CURRENT_START:
return $this->toBeginOfWeek($rollingDate->getPivotDate());
case RollingDate::T_WEEK_NEXT_START:
return $this->toBeginOfWeek($rollingDate->getPivotDate()->add(new DateInterval('P1W')));
case RollingDate::T_WEEK_PREVIOUS_START:
return $this->toBeginOfWeek($rollingDate->getPivotDate()->sub(new DateInterval('P1W')));
case RollingDate::T_YEAR_CURRENT_START:
return $this->toBeginOfYear($rollingDate->getPivotDate());
case RollingDate::T_YEAR_PREVIOUS_START:
return $this->toBeginOfYear($rollingDate->getPivotDate()->sub(new DateInterval('P1Y')));
case RollingDate::T_YEAR_NEXT_START:
return $this->toBeginOfYear($rollingDate->getPivotDate()->add(new DateInterval('P1Y')));
case RollingDate::T_TODAY:
return $rollingDate->getPivotDate();
case RollingDate::T_FIXED_DATE:
if (null === $rollingDate->getFixedDate()) {
throw new LogicException('You must provide a fixed date when selecting a fixed date');
}
return $rollingDate->getFixedDate();
default:
throw new UnexpectedValueException(sprintf('%s rolling operation not supported', $rollingDate->getRoll()));
}
}
private function toBeginOfMonth(DateTimeImmutable $date): DateTimeImmutable
{
return DateTimeImmutable::createFromFormat(
'Y-m-d His',
sprintf('%s-%s-01 000000', $date->format('Y'), $date->format('m'))
);
}
private function toBeginOfQuarter(DateTimeImmutable $date): DateTimeImmutable
{
switch ((int) $date->format('n')) {
case 1:
case 2:
case 3:
$month = '01';
break;
case 4:
case 5:
case 6:
$month = '04';
break;
case 7:
case 8:
case 9:
$month = '07';
break;
case 10:
case 11:
case 12:
$month = '10';
break;
default:
throw new LogicException('this month is not valid: ' . $date->format('n'));
}
return DateTimeImmutable::createFromFormat(
'Y-m-d His',
sprintf('%s-%s-01 000000', $date->format('Y'), $month)
);
}
private function toBeginOfWeek(DateTimeImmutable $date): DateTimeImmutable
{
if (1 === $dayOfWeek = (int) $date->format('N')) {
return $date->setTime(0, 0, 0);
}
return $date
->sub(new DateInterval('P' . ($dayOfWeek - 1) . 'D'))
->setTime(0, 0, 0);
}
private function toBeginOfYear(DateTimeImmutable $date): DateTimeImmutable
{
return DateTimeImmutable::createFromFormat(
'Y-m-d His',
sprintf('%s-01-01 000000', $date->format('Y'))
);
}
}

View File

@ -0,0 +1,19 @@
<?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\MainBundle\Service\RollingDate;
use DateTimeImmutable;
interface RollingDateConverterInterface
{
public function convert(RollingDate $rollingDate): DateTimeImmutable;
}

View File

@ -134,7 +134,9 @@ final class NotificationTest extends KernelTestCase
$this->assertEquals($senderId, $notification->getSender()->getId());
$this->assertCount(count($addressesIds), $notification->getUnreadBy());
$unreadIds = $notification->getUnreadBy()->map(static function (User $u) { return $u->getId(); });
$unreadIds = $notification->getUnreadBy()->map(static function (User $u) {
return $u->getId();
});
foreach ($addressesIds as $addresseeId) {
$this->assertContains($addresseeId, $unreadIds);

View File

@ -680,10 +680,12 @@ final class ExportManagerTest extends KernelTestCase
return new ExportManager(
$logger ?? self::$container->get('logger'),
$em ?? self::$container->get('doctrine.orm.entity_manager'),
$authorizationChecker ?? self::$container->get('security.authorization_checker'),
$authorizationHelper ?? self::$container->get('chill.main.security.authorization.helper'),
$tokenStorage
$tokenStorage,
[],
[],
[]
);
}
}

View File

@ -0,0 +1,53 @@
<?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 Form\Type;
use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Symfony\Component\Form\PreloadedExtension;
use Symfony\Component\Form\Test\TypeTestCase;
/**
* @internal
* @coversNothing
*/
final class PickRollingDateTypeTest extends TypeTestCase
{
public function testSubmitValidData()
{
$formData = [
'roll' => 'year_previous_start',
'fixedDate' => null,
];
$form = $this->factory->create(PickRollingDateType::class);
$form->submit($formData);
$this->assertTrue($form->isSynchronized());
/** @var RollingDate $rollingDate */
$rollingDate = $form->getData();
$this->assertInstanceOf(RollingDate::class, $rollingDate);
$this->assertEquals(RollingDate::T_YEAR_PREVIOUS_START, $rollingDate->getRoll());
}
protected function getExtensions(): array
{
$type = new PickRollingDateType();
return [
new PreloadedExtension([$type], []),
];
}
}

View File

@ -96,7 +96,9 @@ final class ScopePickerTypeTest extends TypeTestCase
$translatableStringHelper = $this->prophesize(TranslatableStringHelperInterface::class);
$translatableStringHelper->localize(Argument::type('array'))->will(
static function ($args) { return $args[0]['fr']; }
static function ($args) {
return $args[0]['fr'];
}
);
$type = new ScopePickerType(

View File

@ -211,7 +211,9 @@ final class AuthorizationHelperTest extends KernelTestCase
$centerA
);
$usernames = array_map(static function (User $u) { return $u->getUsername(); }, $users);
$usernames = array_map(static function (User $u) {
return $u->getUsername();
}, $users);
$this->assertContains('center a_social', $usernames);
}

View File

@ -0,0 +1,101 @@
<?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 Services\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverter;
use DateTime;
use DateTimeImmutable;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversNothing
*/
final class RollingDateConverterTest extends TestCase
{
private RollingDateConverter $converter;
protected function setUp(): void
{
$this->converter = new RollingDateConverter();
}
public function generateDataConversionDate(): iterable
{
$format = 'Y-m-d His';
yield [RollingDate::T_MONTH_CURRENT_START, '2022-11-01 000000', $format];
yield [RollingDate::T_MONTH_NEXT_START, '2022-12-01 000000', $format];
yield [RollingDate::T_MONTH_PREVIOUS_START, '2022-10-01 000000', $format];
yield [RollingDate::T_QUARTER_CURRENT_START, '2022-10-01 000000', $format];
yield [RollingDate::T_QUARTER_NEXT_START, '2023-01-01 000000', $format];
yield [RollingDate::T_QUARTER_PREVIOUS_START, '2022-07-01 000000', $format];
yield [RollingDate::T_TODAY, '2022-11-07 000000', $format];
yield [RollingDate::T_WEEK_CURRENT_START, '2022-11-07 000000', $format];
yield [RollingDate::T_WEEK_NEXT_START, '2022-11-14 000000', $format];
yield [RollingDate::T_WEEK_PREVIOUS_START, '2022-10-31 000000', $format];
yield [RollingDate::T_YEAR_CURRENT_START, '2022-01-01 000000', $format];
yield [RollingDate::T_YEAR_NEXT_START, '2023-01-01 000000', $format];
yield [RollingDate::T_YEAR_PREVIOUS_START, '2021-01-01 000000', $format];
}
public function testConversionFixedDate()
{
$rollingDate = new RollingDate(RollingDate::T_FIXED_DATE, new DateTimeImmutable('2022-01-01'));
$this->assertEquals(
'2022-01-01',
$this->converter->convert($rollingDate)->format('Y-m-d')
);
}
public function testConvertOnDateNow()
{
$rollingDate = new RollingDate(RollingDate::T_YEAR_PREVIOUS_START);
$actual = $this->converter->convert($rollingDate);
$this->assertEquals(
(int) (new DateTimeImmutable('now'))->format('Y') - 1,
(int) $actual->format('Y')
);
$this->assertEquals(1, (int) $actual->format('m'));
$this->assertEquals(1, (int) $actual->format('d'));
}
/**
* @dataProvider generateDataConversionDate
*/
public function testConvertOnPivotDate(string $roll, string $expectedDateTime, string $format)
{
$pivot = DateTimeImmutable::createFromFormat('Y-m-d His', '2022-11-07 000000');
$rollingDate = new RollingDate($roll, null, $pivot);
$this->assertEquals(
DateTime::createFromFormat($format, $expectedDateTime),
$this->converter->convert($rollingDate)
);
}
}

View File

@ -107,7 +107,9 @@ class NotificationOnTransition implements EventSubscriberInterface
'dest' => $subscriber,
'place' => $place,
'workflow' => $workflow,
'is_dest' => in_array($subscriber->getId(), array_map(static function (User $u) { return $u->getId(); }, $entityWorkflow->futureDestUsers), true),
'is_dest' => in_array($subscriber->getId(), array_map(static function (User $u) {
return $u->getId();
}, $entityWorkflow->futureDestUsers), true),
];
$notification = new Notification();

View File

@ -106,3 +106,8 @@ services:
resource: '../Service/Import/'
autowire: true
autoconfigure: true
Chill\MainBundle\Service\RollingDate\:
resource: '../Service/RollingDate/'
autowire: true
autoconfigure: true

View File

@ -3,6 +3,9 @@ services:
autowire: true
autoconfigure: true
Chill\MainBundle\Export\Helper\:
resource: '../../Export/Helper'
chill.main.export_element_validator:
class: Chill\MainBundle\Validator\Constraints\Export\ExportElementConstraintValidator
tags:

View File

@ -50,6 +50,8 @@ services:
tags:
- { name: security.voter }
Chill\MainBundle\Security\Authorization\SavedExportVoter: ~
Chill\MainBundle\Security\PasswordRecover\TokenManager:
arguments:
$secret: '%kernel.secret%'

View File

@ -0,0 +1,43 @@
<?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\Migrations\Main;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20221107212201 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->addSql('DROP TABLE chill_main_saved_export');
}
public function getDescription(): string
{
return 'Create table for saved exports';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE TABLE chill_main_saved_export (id UUID NOT NULL, user_id INT DEFAULT NULL, description TEXT DEFAULT \'\' NOT NULL, exportAlias TEXT DEFAULT \'\' NOT NULL, options JSONB DEFAULT \'[]\' NOT NULL, title TEXT DEFAULT \'\' NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, createdBy_id INT DEFAULT NULL, updatedBy_id INT DEFAULT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_C2029B22A76ED395 ON chill_main_saved_export (user_id)');
$this->addSql('CREATE INDEX IDX_C2029B223174800F ON chill_main_saved_export (createdBy_id)');
$this->addSql('CREATE INDEX IDX_C2029B2265FF1AEC ON chill_main_saved_export (updatedBy_id)');
$this->addSql('COMMENT ON COLUMN chill_main_saved_export.id IS \'(DC2Type:uuid)\'');
$this->addSql('COMMENT ON COLUMN chill_main_saved_export.options IS \'(DC2Type:json)\'');
$this->addSql('COMMENT ON COLUMN chill_main_saved_export.createdAt IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('COMMENT ON COLUMN chill_main_saved_export.updatedAt IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('ALTER TABLE chill_main_saved_export ADD CONSTRAINT FK_C2029B22A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE chill_main_saved_export ADD CONSTRAINT FK_C2029B223174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE chill_main_saved_export ADD CONSTRAINT FK_C2029B2265FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
}
}

View File

@ -39,6 +39,8 @@ Last updated by: Dernière mise à jour par
on: "le "
Last updated on: Dernière mise à jour le
by_user: "par "
lifecycleUpdate: Evenements de création et mise à jour
address_fields: Données liées à l'adresse
Edit: Modifier
Update: Mettre à jour
@ -57,6 +59,7 @@ Until: Jusqu'au
#elements used in software
centers: centres
Centers: Centres
center: centre
comment: commentaire
Comment: Commentaire
Pinned comment: Commentaire épinglé
@ -79,17 +82,17 @@ Postal code: Code postal
Valid from: Valide à partir du
Choose a postal code: Choisir un code postal
address:
address_homeless: L'adresse est-elle celle d'un domicile fixe ?
real address: Adresse d'un domicile
consider homeless: Cette adresse est incomplète
address_homeless: L'adresse est-elle celle d'un domicile fixe ?
real address: Adresse d'un domicile
consider homeless: Cette adresse est incomplète
address more:
floor: ét
corridor: coul
steps: esc
flat: appart
buildingName: résidence
extra: ""
distribution: cedex
floor: ét
corridor: coul
steps: esc
flat: appart
buildingName: résidence
extra: ""
distribution: cedex
Create a new address: Créer une nouvelle adresse
Create an address: Créer une adresse
Update address: Modifier l'adresse
@ -125,7 +128,7 @@ Location and location type: Localisations et types de localisation
Back to the admin: Menu d'administration
"Administration interface": Interface d'administration
Welcome to the admin section !: >
Bienvenue dans l'interface d'administration !
Bienvenue dans l'interface d'administration !
#permissions
Permissions Menu: Gestion des droits
@ -236,6 +239,7 @@ Default for: Type de localisation par défaut pour
none: aucun
person: usager
thirdparty: tiers
civility: civilité
#admin section for civility
abbreviation: abbréviation
@ -334,69 +338,69 @@ Impersonate: Incarner l'utilisateur
Impersonate mode: Mode fantôme
crud:
# general items
new:
button_action_form: Créer
link_edit: Modifier
save_and_close: Créer & fermer
save_and_show: Créer & voir
save_and_new: Créer & nouveau
success: Les données ont été créées
edit:
button_action_form: Enregistrer
back_to_view: Voir
save_and_close: Enregistrer & fermer
save_and_show: Enregistrer & voir
success: Les données ont été modifiées
delete:
success: Les données ont été supprimées
link_to_form: Supprimer
default:
success: Les données ont été enregistrées
view:
link_duplicate: Dupliquer
admin_user:
index:
title: Utilisateurs
add_new: Créer
title_edit: Modifier un utilisateur
title_new: Créer un utilisateur
admin_user_job:
index:
title: Métiers
add_new: Créer
title_new: Nouveau métier
title_edit: Modifier un métier
main_location_type:
index:
title: Liste des types de localisations
add_new: Ajouter un type de localisation
title_new: Nouveau type de localisation
title_edit: Modifier un type de localisation
main_location:
index:
title: Liste des localisations
add_new: Ajouter une localisation
title_new: Nouvelle localisation
title_edit: Modifier une localisation
main_language:
index:
title: Liste des langues
add_new: Ajouter une langue
title_new: Nouvelle langue
title_edit: Modifier une langue
main_country:
index:
title: Liste des pays
add_new: Ajouter un pays
title_new: Nouveau pays
title_edit: Modifier un pays
main_civility:
index:
title: Liste des civilités
add_new: Ajouter une civilité
title_new: Nouvelle civilité
title_edit: Modifier une civilité
# general items
new:
button_action_form: Créer
link_edit: Modifier
save_and_close: Créer & fermer
save_and_show: Créer & voir
save_and_new: Créer & nouveau
success: Les données ont été créées
edit:
button_action_form: Enregistrer
back_to_view: Voir
save_and_close: Enregistrer & fermer
save_and_show: Enregistrer & voir
success: Les données ont été modifiées
delete:
success: Les données ont été supprimées
link_to_form: Supprimer
default:
success: Les données ont été enregistrées
view:
link_duplicate: Dupliquer
admin_user:
index:
title: Utilisateurs
add_new: Créer
title_edit: Modifier un utilisateur
title_new: Créer un utilisateur
admin_user_job:
index:
title: Métiers
add_new: Créer
title_new: Nouveau métier
title_edit: Modifier un métier
main_location_type:
index:
title: Liste des types de localisations
add_new: Ajouter un type de localisation
title_new: Nouveau type de localisation
title_edit: Modifier un type de localisation
main_location:
index:
title: Liste des localisations
add_new: Ajouter une localisation
title_new: Nouvelle localisation
title_edit: Modifier une localisation
main_language:
index:
title: Liste des langues
add_new: Ajouter une langue
title_new: Nouvelle langue
title_edit: Modifier une langue
main_country:
index:
title: Liste des pays
add_new: Ajouter un pays
title_new: Nouveau pays
title_edit: Modifier un pays
main_civility:
index:
title: Liste des civilités
add_new: Ajouter une civilité
title_new: Nouvelle civilité
title_edit: Modifier une civilité
No entities: Aucun élément
@ -515,3 +519,51 @@ notification:
Remove an email: Supprimer l'adresse email
Email with access link: Adresse email ayant reçu un lien d'accès
export:
address_helper:
id: Identifiant de l'adresse
street: Voie
streetNumber: Numéro de voie
buildingName: Résidence
corridor: Couloir
distribution: Distribution
extra: Extra
flat: Appartement
floor: Étage
postcode_code: Code postal
postcode_name: Libellé du code postal
country: Pays
_as_string: Adresse formattée
confidential: Adresse confidentielle ?
isNoAddress: Adresse incomplète ?
_lat: Latitude
_lon: Longitude
rolling_date:
year_previous_start: Début de l'année précédente
quarter_previous_start: Début du trimestre précédent
month_previous_start: Début du mois précédent
week_previous_start: Début de la semaine précédente
year_current_start: Début de l'année courante
quarter_current_start: Début du trimestre courant
month_current_start: Début du mois courant
week_current_start: Début de la semaine courante
today: Aujourd'hui (aucune modification de la date courante)
year_next_start: Début de l'année suivante
quarter_next_start: Début du trimestre suivante
month_next_start: Début du mois suivant
week_next_start: Début de la semaine suivante
fixed_date: Date fixe
roll_movement: Modification par rapport à aujourd'hui
fixed_date_date: Date fixe
saved_export:
Any saved export: Aucun rapport enregistré
New: Nouveau rapport enregistré
Edit: Modifier un rapport enregistré
Delete saved ?: Supprimer un rapport enregistré ?
Are you sure you want to delete this saved ?: Êtes-vous sûr·e de vouloir supprimer ce rapport ?
My saved exports: Mes rapports enregistrés
Export is deleted: Le rapport est supprimé
Saved export is saved!: Le rapport est enregistré
Created on %date%: Créé le %date%

View File

@ -84,7 +84,7 @@ class PersonHouseholdAddress
public function getHousehold(): ?Household
{
return $this->relation;
return $this->household;
}
public function getPerson(): ?Person

View File

@ -1768,7 +1768,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
}
/**
* @param type $spokenLanguages
* @param Collection $spokenLanguages
*/
public function setSpokenLanguages($spokenLanguages): self
{

View File

@ -0,0 +1,67 @@
<?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\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators;
use Chill\MainBundle\Export\AggregatorInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Chill\PersonBundle\Export\Declarations;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
class ByActionNumberAggregator implements AggregatorInterface
{
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data): void
{
$qb->addSelect('(SELECT COUNT(acp_by_action_action.id) FROM ' . AccompanyingPeriodWork::class . ' acp_by_action_action WHERE acp_by_action_action.accompanyingPeriod = acp) AS acp_by_action_number_aggregator')
->addGroupBy('acp_by_action_number_aggregator');
}
public function applyOn(): string
{
return Declarations::ACP_TYPE;
}
public function buildForm(FormBuilderInterface $builder): void
{
// No form needed
}
public function getLabels($key, array $values, $data)
{
return static function ($value) {
if ('_header' === $value) {
return 'export.aggregator.course.by_number_of_action.Number of actions';
}
if (null === $value) {
return '';
}
return $value;
};
}
public function getQueryKeys($data): array
{
return ['acp_by_action_number_aggregator'];
}
public function getTitle(): string
{
return 'Group by number of actions';
}
}

View File

@ -18,6 +18,7 @@ use Chill\PersonBundle\Entity\Household\HouseholdComposition;
use Chill\PersonBundle\Entity\Household\HouseholdMember;
use Chill\PersonBundle\Export\Declarations;
use Chill\PersonBundle\Repository\Household\HouseholdCompositionTypeRepositoryInterface;
use DateTimeImmutable;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
@ -96,7 +97,7 @@ class ByHouseholdCompositionAggregator implements AggregatorInterface
$builder->add('date_calc', ChillDateType::class, [
'label' => 'export.aggregator.course.by_household_composition.Calc date',
'input_format' => 'datetime_immutable',
'data' => new \DateTimeImmutable('now'),
'data' => new DateTimeImmutable('now'),
]);
}

View File

@ -0,0 +1,87 @@
<?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\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators;
use Chill\MainBundle\Export\AggregatorInterface;
use Chill\MainBundle\Repository\UserJobRepository;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\PersonBundle\Export\Declarations;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
use function in_array;
class CreatorJobAggregator implements AggregatorInterface
{
private UserJobRepository $jobRepository;
private TranslatableStringHelper $translatableStringHelper;
public function __construct(
UserJobRepository $jobRepository,
TranslatableStringHelper $translatableStringHelper
) {
$this->jobRepository = $jobRepository;
$this->translatableStringHelper = $translatableStringHelper;
}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
if (!in_array('acp_creator', $qb->getAllAliases(), true)) {
$qb->leftJoin('acp.createdBy', 'acp_creator');
}
$qb->addSelect('IDENTITY(acp_creator.userJob) AS acp_creator_job_aggregator')
->addGroupBy('acp_creator_job_aggregator');
}
public function applyOn(): string
{
return Declarations::ACP_TYPE;
}
public function buildForm(FormBuilderInterface $builder)
{
// No form needed
}
public function getLabels($key, array $values, $data)
{
return function ($value): string {
if ('_header' === $value) {
return 'export.aggregator.course.by_creator_job.Creator\'s job';
}
if (null === $value || null === $j = $this->jobRepository->find($value)) {
return '';
}
return $this->translatableStringHelper->localize(
$j->getLabel()
);
};
}
public function getQueryKeys($data): array
{
return ['acp_creator_job_aggregator'];
}
public function getTitle(): string
{
return 'Group by creator job';
}
}

View File

@ -12,10 +12,11 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators;
use Chill\MainBundle\Export\AggregatorInterface;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Export\Declarations;
use DateTime;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@ -27,11 +28,15 @@ final class StepAggregator implements AggregatorInterface
private const P = 'acp_step_agg_date';
private RollingDateConverterInterface $rollingDateConverter;
private TranslatorInterface $translator;
public function __construct(
RollingDateConverterInterface $rollingDateConverter,
TranslatorInterface $translator
) {
$this->rollingDateConverter = $rollingDateConverter;
$this->translator = $translator;
}
@ -60,7 +65,7 @@ final class StepAggregator implements AggregatorInterface
)
)
)
->setParameter(self::P, $data['on_date'])
->setParameter(self::P, $this->rollingDateConverter->convert($data['on_date']))
->addGroupBy('step_aggregator');
}
@ -71,8 +76,8 @@ final class StepAggregator implements AggregatorInterface
public function buildForm(FormBuilderInterface $builder)
{
$builder->add('on_date', ChillDateType::class, [
'data' => new DateTime(),
$builder->add('on_date', PickRollingDateType::class, [
'data' => new RollingDate(RollingDate::T_TODAY),
]);
}

View File

@ -0,0 +1,106 @@
<?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\PersonBundle\Export\Aggregator\EvaluationAggregators;
use Chill\MainBundle\Export\AggregatorInterface;
use Chill\PersonBundle\Export\Declarations;
use Doctrine\ORM\QueryBuilder;
use LogicException;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class ByEndDateAggregator implements AggregatorInterface
{
private const CHOICES = [
'by week' => 'week',
'by month' => 'month',
'by year' => 'year',
];
private const DEFAULT_CHOICE = 'year';
private TranslatorInterface $translator;
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
switch ($data['frequency']) {
case 'week':
$fmt = 'YYYY-IW';
break;
case 'month':
$fmt = 'YYYY-MM';
break;
case 'year':
$fmt = 'YYYY';
break;
default:
throw new LogicException(sprintf("The frequency data '%s' is invalid.", $data['frequency']));
}
$qb->addSelect(sprintf("TO_CHAR(workeval.endDate, '%s') AS eval_by_end_date_aggregator", $fmt));
$qb->addGroupBy(' eval_by_end_date_aggregator');
$qb->addOrderBy(' eval_by_end_date_aggregator', 'ASC');
}
public function applyOn(): string
{
return Declarations::EVAL_TYPE;
}
public function buildForm(FormBuilderInterface $builder)
{
$builder->add('frequency', ChoiceType::class, [
'choices' => self::CHOICES,
'multiple' => false,
'expanded' => true,
'empty_data' => self::DEFAULT_CHOICE,
'data' => self::DEFAULT_CHOICE,
]);
}
public function getLabels($key, array $values, $data)
{
return static function ($value): string {
if ('_header' === $value) {
return 'export.aggregator.eval.by_end_date.End date period';
}
if (null === $value) {
return '';
}
return $value;
};
}
public function getQueryKeys($data): array
{
return ['eval_by_end_date_aggregator'];
}
public function getTitle(): string
{
return 'export.aggregator.eval.by_end_date.Group by end date evaluations';
}
}

View File

@ -0,0 +1,106 @@
<?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\PersonBundle\Export\Aggregator\EvaluationAggregators;
use Chill\MainBundle\Export\AggregatorInterface;
use Chill\PersonBundle\Export\Declarations;
use Doctrine\ORM\QueryBuilder;
use LogicException;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class ByMaxDateAggregator implements AggregatorInterface
{
private const CHOICES = [
'by week' => 'week',
'by month' => 'month',
'by year' => 'year',
];
private const DEFAULT_CHOICE = 'year';
private TranslatorInterface $translator;
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
switch ($data['frequency']) {
case 'week':
$fmt = 'YYYY-IW';
break;
case 'month':
$fmt = 'YYYY-MM';
break;
case 'year':
$fmt = 'YYYY';
break;
default:
throw new LogicException(sprintf("The frequency data '%s' is invalid.", $data['frequency']));
}
$qb->addSelect(sprintf("TO_CHAR(workeval.maxDate, '%s') AS eval_by_max_date_aggregator", $fmt));
$qb->addGroupBy(' eval_by_max_date_aggregator');
$qb->addOrderBy(' eval_by_max_date_aggregator', 'ASC');
}
public function applyOn(): string
{
return Declarations::EVAL_TYPE;
}
public function buildForm(FormBuilderInterface $builder)
{
$builder->add('frequency', ChoiceType::class, [
'choices' => self::CHOICES,
'multiple' => false,
'expanded' => true,
'empty_data' => self::DEFAULT_CHOICE,
'data' => self::DEFAULT_CHOICE,
]);
}
public function getLabels($key, array $values, $data)
{
return static function ($value): string {
if ('_header' === $value) {
return 'export.aggregator.eval.by_max_date.Max date';
}
if (null === $value) {
return '';
}
return $value;
};
}
public function getQueryKeys($data): array
{
return ['eval_by_max_date_aggregator'];
}
public function getTitle(): string
{
return 'export.aggregator.eval.by_max_date.Group by max date evaluations';
}
}

View File

@ -0,0 +1,106 @@
<?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\PersonBundle\Export\Aggregator\EvaluationAggregators;
use Chill\MainBundle\Export\AggregatorInterface;
use Chill\PersonBundle\Export\Declarations;
use Doctrine\ORM\QueryBuilder;
use LogicException;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class ByStartDateAggregator implements AggregatorInterface
{
private const CHOICES = [
'by week' => 'week',
'by month' => 'month',
'by year' => 'year',
];
private const DEFAULT_CHOICE = 'year';
private TranslatorInterface $translator;
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
switch ($data['frequency']) {
case 'week':
$fmt = 'YYYY-IW';
break;
case 'month':
$fmt = 'YYYY-MM';
break;
case 'year':
$fmt = 'YYYY';
break;
default:
throw new LogicException(sprintf("The frequency data '%s' is invalid.", $data['frequency']));
}
$qb->addSelect(sprintf("TO_CHAR(workeval.startDate, '%s') AS eval_by_start_date_aggregator", $fmt));
$qb->addGroupBy(' eval_by_start_date_aggregator');
$qb->addOrderBy(' eval_by_start_date_aggregator', 'ASC');
}
public function applyOn(): string
{
return Declarations::EVAL_TYPE;
}
public function buildForm(FormBuilderInterface $builder)
{
$builder->add('frequency', ChoiceType::class, [
'choices' => self::CHOICES,
'multiple' => false,
'expanded' => true,
'empty_data' => self::DEFAULT_CHOICE,
'data' => self::DEFAULT_CHOICE,
]);
}
public function getLabels($key, array $values, $data)
{
return static function ($value): string {
if ('_header' === $value) {
return 'export.aggregator.eval.by_start_date_period.Start date period';
}
if (null === $value) {
return '';
}
return $value;
};
}
public function getQueryKeys($data): array
{
return ['eval_by_start_date_aggregator'];
}
public function getTitle(): string
{
return 'export.aggregator.eval.by_start_date_period.Group by start date evaluations';
}
}

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\PersonBundle\Export\Aggregator\EvaluationAggregators;
use Chill\MainBundle\Export\AggregatorInterface;
use Chill\PersonBundle\Export\Declarations;
use Doctrine\ORM\QueryBuilder;
use LogicException;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class HavingEndDateAggregator implements AggregatorInterface
{
private TranslatorInterface $translator;
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
$qb
->addSelect('CASE WHEN workeval.endDate IS NULL THEN true ELSE false END AS eval_enddate_aggregator')
->addGroupBy('eval_enddate_aggregator');
}
public function applyOn(): string
{
return Declarations::EVAL_TYPE;
}
public function buildForm(FormBuilderInterface $builder)
{
// No form needed
}
public function getLabels($key, array $values, $data)
{
return function ($value): string {
if ('_header' === $value) {
return 'export.aggregator.eval.by_end_date.Has end date ?';
}
switch ($value) {
case true:
return $this->translator->trans('export.aggregator.eval.by_end_date.enddate is specified');
case false:
return $this->translator->trans('export.aggregator.eval.by_end_date.enddate is not specified');
default:
throw new LogicException(sprintf('The value %s is not valid', $value));
}
};
}
public function getQueryKeys($data): array
{
return ['eval_enddate_aggregator'];
}
public function getTitle(): string
{
return 'export.aggregator.eval.by_end_date.Group evaluations having end date';
}
}

View File

@ -0,0 +1,84 @@
<?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\PersonBundle\Export\Aggregator\SocialWorkAggregators;
use Chill\MainBundle\Export\AggregatorInterface;
use Chill\PersonBundle\Export\Declarations;
use Doctrine\ORM\QueryBuilder;
use LogicException;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class CurrentActionAggregator implements AggregatorInterface
{
private TranslatorInterface $translator;
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data): void
{
$qb
->addSelect('
(CASE WHEN acpw.endDate IS NULL THEN true ELSE false END)
AS acpw_current_action_aggregator
')
->addGroupBy('acpw_current_action_aggregator');
}
public function applyOn(): string
{
return Declarations::SOCIAL_WORK_ACTION_TYPE;
}
public function buildForm(FormBuilderInterface $builder): void
{
// No form needed
}
public function getLabels($key, array $values, $data)
{
return function ($value): string {
if ('_header' === $value) {
return 'export.aggregator.course_work.by_current_action.Current action ?';
}
switch ($value) {
case true:
return $this->translator->trans('export.aggregator.course_work.by_current_action.Current action');
case false:
return $this->translator->trans('export.aggregator.course_work.by_current_action.Not current action');
default:
throw new LogicException(sprintf('The value %s is not valid', $value));
}
};
}
public function getQueryKeys($data): array
{
return ['acpw_current_action_aggregator'];
}
public function getTitle(): string
{
return 'export.aggregator.course_work.by_current_action.Group by current actions';
}
}

View File

@ -38,7 +38,7 @@ class CountAccompanyingCourse implements ExportInterface, GroupedExportInterface
public function buildForm(FormBuilderInterface $builder): void
{
// TODO: Implement buildForm() method.
// Nothing to add here
}
public function getAllowedFormattersTypes(): array

View File

@ -0,0 +1,416 @@
<?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\PersonBundle\Export\Export;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Export\FormatterInterface;
use Chill\MainBundle\Export\GroupedExportInterface;
use Chill\MainBundle\Export\Helper\DateTimeHelper;
use Chill\MainBundle\Export\Helper\ExportAddressHelper;
use Chill\MainBundle\Export\Helper\UserHelper;
use Chill\MainBundle\Export\ListInterface;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
use Chill\PersonBundle\Entity\Household\PersonHouseholdAddress;
use Chill\PersonBundle\Entity\Person\PersonCenterHistory;
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
use Chill\PersonBundle\Export\Declarations;
use Chill\PersonBundle\Repository\PersonRepository;
use Chill\PersonBundle\Repository\SocialWork\SocialIssueRepository;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Chill\PersonBundle\Templating\Entity\PersonRenderInterface;
use Chill\PersonBundle\Templating\Entity\SocialIssueRender;
use Chill\ThirdPartyBundle\Repository\ThirdPartyRepository;
use Chill\ThirdPartyBundle\Templating\Entity\ThirdPartyRender;
use DateTimeImmutable;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
use function strlen;
class ListAccompanyingPeriod implements ListInterface, GroupedExportInterface
{
private const FIELDS = [
'id',
'step',
'stepSince',
'openingDate',
'closingDate',
'referrer',
'referrerSince',
'administrativeLocation',
'locationIsPerson',
'locationIsTemp',
'locationPersonName',
'locationPersonId',
'origin',
'closingMotive',
'confidential',
'emergency',
'intensity',
'job',
'isRequestorPerson',
'isRequestorThirdParty',
'requestorPerson',
'requestorPersonId',
'requestorThirdParty',
'requestorThirdPartyId',
'scopes',
'socialIssues',
'createdAt',
'createdBy',
'updatedAt',
'updatedBy',
];
private ExportAddressHelper $addressHelper;
private DateTimeHelper $dateTimeHelper;
private EntityManagerInterface $entityManager;
private PersonRenderInterface $personRender;
private PersonRepository $personRepository;
private SocialIssueRender $socialIssueRender;
private SocialIssueRepository $socialIssueRepository;
private ThirdPartyRender $thirdPartyRender;
private ThirdPartyRepository $thirdPartyRepository;
private TranslatableStringHelperInterface $translatableStringHelper;
private UserHelper $userHelper;
public function __construct(
ExportAddressHelper $addressHelper,
DateTimeHelper $dateTimeHelper,
EntityManagerInterface $entityManager,
PersonRenderInterface $personRender,
PersonRepository $personRepository,
ThirdPartyRepository $thirdPartyRepository,
ThirdPartyRender $thirdPartyRender,
SocialIssueRepository $socialIssueRepository,
SocialIssueRender $socialIssueRender,
TranslatableStringHelperInterface $translatableStringHelper,
UserHelper $userHelper
) {
$this->addressHelper = $addressHelper;
$this->dateTimeHelper = $dateTimeHelper;
$this->entityManager = $entityManager;
$this->personRender = $personRender;
$this->personRepository = $personRepository;
$this->socialIssueRender = $socialIssueRender;
$this->socialIssueRepository = $socialIssueRepository;
$this->thirdPartyRender = $thirdPartyRender;
$this->thirdPartyRepository = $thirdPartyRepository;
$this->translatableStringHelper = $translatableStringHelper;
$this->userHelper = $userHelper;
}
public function buildForm(FormBuilderInterface $builder)
{
$builder
->add('calc_date', ChillDateType::class, [
'input' => 'datetime_immutable',
'label' => 'export.list.acp.Date of calculation for associated elements',
'help' => 'export.list.acp.The associated referree, localisation, and other elements will be valid at this date',
'required' => true,
]);
}
public function getAllowedFormattersTypes()
{
return [FormatterInterface::TYPE_LIST];
}
public function getDescription()
{
return 'export.list.acp.Generate a list of accompanying periods, filtered on different parameters.';
}
public function getGroup(): string
{
return 'Exports of accompanying courses';
}
public function getLabels($key, array $values, $data)
{
if (substr($key, 0, strlen('address_fields')) === 'address_fields') {
return $this->addressHelper->getLabel($key, $values, $data, 'address_fields');
}
switch ($key) {
case 'stepSince':
case 'openingDate':
case 'closingDate':
case 'referrerSince':
case 'createdAt':
case 'updatedAt':
return $this->dateTimeHelper->getLabel('export.list.acp.' . $key);
case 'origin':
case 'closingMotive':
case 'job':
return function ($value) use ($key) {
if ('_header' === $value) {
return 'export.list.acp.' . $key;
}
if (null === $value) {
return '';
}
return $this->translatableStringHelper->localize(json_decode($value, true));
};
case 'locationPersonName':
case 'requestorPerson':
return function ($value) use ($key) {
if ('_header' === $value) {
return 'export.list.acp.' . $key;
}
if (null === $value || null === $person = $this->personRepository->find($value)) {
return '';
}
return $this->personRender->renderString($person, []);
};
case 'requestorThirdParty':
return function ($value) use ($key) {
if ('_header' === $value) {
return 'export.list.acp.' . $key;
}
if (null === $value || null === $thirdparty = $this->thirdPartyRepository->find($value)) {
return '';
}
return $this->thirdPartyRender->renderString($thirdparty, []);
};
case 'scopes':
return function ($value) use ($key) {
if ('_header' === $value) {
return 'export.list.acp.' . $key;
}
if (null === $value) {
return '';
}
return implode(
'|',
array_map(
fn ($s) => $this->translatableStringHelper->localize($s),
json_decode($value, true)
)
);
};
case 'socialIssues':
return function ($value) use ($key) {
if ('_header' === $value) {
return 'export.list.acp.' . $key;
}
if (null === $value) {
return '';
}
return implode(
'|',
array_map(
fn ($s) => $this->socialIssueRender->renderString($this->socialIssueRepository->find($s), []),
json_decode($value, true)
)
);
};
default:
return static function ($value) use ($key) {
if ('_header' === $value) {
return 'export.list.acp.' . $key;
}
if (null === $value) {
return '';
}
return $value;
};
}
}
public function getQueryKeys($data)
{
return array_merge(
self::FIELDS,
$this->addressHelper->getKeys(ExportAddressHelper::F_ALL, 'address_fields')
);
}
public function getResult($query, $data)
{
return $query->getQuery()->getResult(AbstractQuery::HYDRATE_SCALAR);
}
public function getTitle()
{
return 'export.list.acp.List of accompanying periods';
}
public function getType()
{
return Declarations::PERSON_TYPE;
}
public function initiateQuery(array $requiredModifiers, array $acl, array $data = [])
{
$centers = array_map(static function ($el) {
return $el['center'];
}, $acl);
$qb = $this->entityManager->createQueryBuilder();
$qb
->from(AccompanyingPeriod::class, 'acp')
->andWhere('acp.step != :list_acp_step')
->andWhere(
$qb->expr()->exists(
'SELECT 1 FROM ' . AccompanyingPeriodParticipation::class . ' acl_count_part
JOIN ' . PersonCenterHistory::class . ' acl_count_person_history WITH IDENTITY(acl_count_person_history.person) = IDENTITY(acl_count_part.person)
WHERE acl_count_part.accompanyingPeriod = acp.id AND acl_count_person_history.center IN (:authorized_centers)
'
)
)
->setParameter('list_acp_step', AccompanyingPeriod::STEP_DRAFT)
->setParameter('authorized_centers', $centers);
$this->addSelectClauses($qb, $data['calc_date']);
return $qb;
}
public function requiredRole(): string
{
return PersonVoter::LISTS;
}
public function supportsModifiers()
{
return [
Declarations::ACP_TYPE,
];
}
private function addSelectClauses(QueryBuilder $qb, DateTimeImmutable $calcDate): void
{
// add the regular fields
foreach (['id', 'openingDate', 'closingDate', 'confidential', 'emergency', 'intensity', 'createdAt', 'updatedAt'] as $field) {
$qb->addSelect(sprintf('acp.%s AS %s', $field, $field));
}
// add the field which are simple association
foreach (['origin' => 'label', 'closingMotive' => 'name', 'job' => 'label', 'createdBy' => 'label', 'updatedBy' => 'label', 'administrativeLocation' => 'name'] as $entity => $field) {
$qb
->leftJoin(sprintf('acp.%s', $entity), "{$entity}_t")
->addSelect(sprintf('%s_t.%s AS %s', $entity, $field, $entity));
}
// step at date
$qb
->addSelect('stepHistory.step AS step')
->addSelect('stepHistory.startDate AS stepSince')
->leftJoin('acp.stepHistories', 'stepHistory')
->andWhere(
$qb->expr()->andX(
$qb->expr()->lte('stepHistory.startDate', ':calcDate'),
$qb->expr()->orX($qb->expr()->isNull('stepHistory.endDate'), $qb->expr()->gt('stepHistory.endDate', ':calcDate'))
)
);
// referree at date
$qb
->addSelect('referrer_t.label AS referrer')
->addSelect('userHistory.startDate AS referrerSince')
->leftJoin('acp.userHistories', 'userHistory')
->leftJoin('userHistory.user', 'referrer_t')
->andWhere(
$qb->expr()->orX(
$qb->expr()->isNull('userHistory'),
$qb->expr()->andX(
$qb->expr()->lte('userHistory.startDate', ':calcDate'),
$qb->expr()->orX($qb->expr()->isNull('userHistory.endDate'), $qb->expr()->gt('userHistory.endDate', ':calcDate'))
)
)
);
// location of the acp
$qb
->addSelect('CASE WHEN locationHistory.personLocation IS NOT NULL THEN 1 ELSE 0 END AS locationIsPerson')
->addSelect('CASE WHEN locationHistory.personLocation IS NOT NULL THEN 0 ELSE 1 END AS locationIsTemp')
->addSelect('IDENTITY(locationHistory.personLocation) AS locationPersonName')
->addSelect('IDENTITY(locationHistory.personLocation) AS locationPersonId')
->leftJoin('acp.locationHistories', 'locationHistory')
->andWhere(
$qb->expr()->orX(
$qb->expr()->isNull('locationHistory'),
$qb->expr()->andX(
$qb->expr()->lte('locationHistory.startDate', ':calcDate'),
$qb->expr()->orX($qb->expr()->isNull('locationHistory.endDate'), $qb->expr()->gt('locationHistory.endDate', ':calcDate'))
)
)
)
->leftJoin(PersonHouseholdAddress::class, 'personAddress', Join::WITH, 'locationHistory.personLocation = personAddress.person')
->andWhere(
$qb->expr()->orX(
$qb->expr()->isNull('personAddress'),
$qb->expr()->andX(
$qb->expr()->lte('personAddress.validFrom', ':calcDate'),
$qb->expr()->orX($qb->expr()->isNull('personAddress.validTo'), $qb->expr()->gt('personAddress.validTo', ':calcDate'))
)
)
)
->leftJoin(Address::class, 'acp_address', Join::WITH, 'COALESCE(IDENTITY(locationHistory.addressLocation), IDENTITY(personAddress.address)) = acp_address.id');
$this->addressHelper->addSelectClauses(ExportAddressHelper::F_ALL, $qb, 'acp_address', 'address_fields');
// requestor
$qb
->addSelect('CASE WHEN acp.requestorPerson IS NULL THEN 1 ELSE 0 END AS isRequestorPerson')
->addSelect('CASE WHEN acp.requestorPerson IS NULL THEN 0 ELSE 1 END AS isRequestorThirdParty')
->addSelect('IDENTITY(acp.requestorPerson) AS requestorPersonId')
->addSelect('IDENTITY(acp.requestorThirdParty) AS requestorThirdPartyId')
->addSelect('IDENTITY(acp.requestorPerson) AS requestorPerson')
->addSelect('IDENTITY(acp.requestorThirdParty) AS requestorThirdParty');
$qb
// scopes
->addSelect('(SELECT AGGREGATE(scope.name) FROM ' . Scope::class . ' scope WHERE scope MEMBER OF acp.scopes) AS scopes')
// social issues
->addSelect('(SELECT AGGREGATE(socialIssue.id) FROM ' . SocialIssue::class . ' socialIssue WHERE socialIssue MEMBER OF acp.socialIssues) AS socialIssues');
// add parameter
$qb->setParameter('calcDate', $calcDate);
}
}

View File

@ -17,21 +17,22 @@ use Chill\CustomFieldsBundle\Service\CustomFieldProvider;
use Chill\MainBundle\Export\ExportElementValidatedInterface;
use Chill\MainBundle\Export\FormatterInterface;
use Chill\MainBundle\Export\GroupedExportInterface;
use Chill\MainBundle\Export\Helper\ExportAddressHelper;
use Chill\MainBundle\Export\ListInterface;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Export\Declarations;
use Chill\PersonBundle\Export\Helper\ListPersonHelper;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use DateTime;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query;
use Exception;
use PhpOffice\PhpSpreadsheet\Shared\Date;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Constraints\Callback;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use function addcslashes;
use function array_key_exists;
@ -39,7 +40,7 @@ use function array_keys;
use function array_merge;
use function count;
use function in_array;
use function strtolower;
use function strlen;
use function uniqid;
/**
@ -47,40 +48,35 @@ use function uniqid;
*/
class ListPerson implements ExportElementValidatedInterface, ListInterface, GroupedExportInterface
{
protected CustomFieldProvider $customFieldProvider;
private ExportAddressHelper $addressHelper;
protected EntityManagerInterface $entityManager;
private CustomFieldProvider $customFieldProvider;
protected array $fields = [
'id', 'firstName', 'lastName', 'birthdate',
'placeOfBirth', 'gender', 'memo', 'email', 'phonenumber',
'mobilenumber', 'contactInfo', 'countryOfBirth', 'nationality',
'address_street_address_1', 'address_street_address_2',
'address_valid_from', 'address_postcode_label', 'address_postcode_code',
'address_country_name', 'address_country_code', 'address_isnoaddress',
];
private EntityManagerInterface $entityManager;
protected TranslatableStringHelper $translatableStringHelper;
protected TranslatorInterface $translator;
private ListPersonHelper $listPersonHelper;
private $slugs = [];
private TranslatableStringHelper $translatableStringHelper;
public function __construct(
ExportAddressHelper $addressHelper,
CustomFieldProvider $customFieldProvider,
ListPersonHelper $listPersonHelper,
EntityManagerInterface $em,
TranslatorInterface $translator,
TranslatableStringHelper $translatableStringHelper,
CustomFieldProvider $customFieldProvider
TranslatableStringHelper $translatableStringHelper
) {
$this->entityManager = $em;
$this->translator = $translator;
$this->translatableStringHelper = $translatableStringHelper;
$this->addressHelper = $addressHelper;
$this->customFieldProvider = $customFieldProvider;
$this->listPersonHelper = $listPersonHelper;
$this->entityManager = $em;
$this->translatableStringHelper = $translatableStringHelper;
}
public function buildForm(FormBuilderInterface $builder)
{
$choices = array_combine($this->fields, $this->fields);
$choices = array_combine(ListPersonHelper::FIELDS, ListPersonHelper::FIELDS);
foreach ($this->getCustomFields() as $cf) {
$choices[$this->translatableStringHelper->localize($cf->getName())]
@ -96,7 +92,7 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou
'label' => 'Fields to include in export',
'choice_attr' => static function (string $val): array {
// add a 'data-display-target' for address fields
if (substr($val, 0, 8) === 'address_') {
if (substr($val, 0, 7) === 'address' || 'center' === $val || 'household' === $val) {
return ['data-display-target' => 'address_date'];
}
@ -111,17 +107,15 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou
}
},
])],
'data' => array_values($choices),
]);
// add a date field for addresses
$builder->add('address_date', DateType::class, [
'label' => 'Address valid at this date',
'data' => new DateTime(),
'attr' => ['class' => 'datepicker'],
'widget' => 'single_text',
'format' => 'dd-MM-yyyy',
'required' => false,
'block_name' => 'list_export_form_address_date',
$builder->add('address_date', ChillDateType::class, [
'label' => 'Data valid at this date',
'help' => 'Data regarding center, addresses, and so on will be computed at this date',
'data' => new DateTimeImmutable(),
'input' => 'datetime_immutable',
]);
}
@ -142,113 +136,35 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou
public function getLabels($key, array $values, $data)
{
switch ($key) {
case 'birthdate':
// for birthdate, we have to transform the string into a date
// to format the date correctly.
return static function ($value) {
if ('_header' === $value) {
return 'birthdate';
}
if (empty($value)) {
return '';
}
$date = DateTime::createFromFormat('Y-m-d', $value);
// check that the creation could occurs.
if (false === $date) {
throw new Exception(sprintf('The value %s could '
. 'not be converted to %s', $value, DateTime::class));
}
return $date->format('d-m-Y');
};
case 'gender':
// for gender, we have to translate men/women statement
return function ($value) {
if ('_header' === $value) {
return 'gender';
}
return $this->translator->trans($value);
};
case 'countryOfBirth':
case 'nationality':
$countryRepository = $this->entityManager
->getRepository(\Chill\MainBundle\Entity\Country::class);
// load all countries in a single query
$countryRepository->findBy(['countryCode' => $values]);
return function ($value) use ($key, $countryRepository) {
if ('_header' === $value) {
return strtolower($key);
}
if (null === $value) {
return $this->translator->trans('no data');
}
$country = $countryRepository->find($value);
return $this->translatableStringHelper->localize(
$country->getName()
);
};
case 'address_country_name':
return function ($value) use ($key) {
if ('_header' === $value) {
return strtolower($key);
}
if (null === $value) {
return '';
}
return $this->translatableStringHelper->localize(json_decode($value, true));
};
case 'address_isnoaddress':
return static function (?string $value): string {
if ('_header' === $value) {
return 'address.address_homeless';
}
if (null !== $value) {
return 'X';
}
return '';
};
default:
// for fields which are associated with person
if (in_array($key, $this->fields, true)) {
return static function ($value) use ($key) {
if ('_header' === $value) {
return strtolower($key);
}
return $value;
};
}
return $this->getLabelForCustomField($key, $values, $data);
if (in_array($key, $this->listPersonHelper->getAllPossibleFields(), true)) {
return $this->listPersonHelper->getLabels($key, $values, $data);
}
return $this->getLabelForCustomField($key, $values, $data);
}
public function getQueryKeys($data)
{
$fields = [];
foreach ($data['fields'] as $key) {
if (in_array($key, $this->fields, true)) {
$fields[] = $key;
foreach (ListPersonHelper::FIELDS as $key) {
if (!in_array($key, $data['fields'], true)) {
continue;
}
if (substr($key, 0, strlen('address_fields')) === 'address_fields') {
$fields = array_merge($fields, $this->addressHelper->getKeys(ExportAddressHelper::F_ALL, 'address_fields'));
continue;
}
if ('lifecycleUpdate' === $key) {
$fields = array_merge($fields, ['createdAt', 'createdBy', 'updatedAt', 'updatedBy']);
continue;
}
$fields[] = $key;
}
// add the key from slugs and return
@ -270,6 +186,9 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou
return Declarations::PERSON_TYPE;
}
/**
* param array{fields: string[], address_date: DateTimeImmutable} $data.
*/
public function initiateQuery(array $requiredModifiers, array $acl, array $data = [])
{
$centers = array_map(static function ($el) {
@ -284,40 +203,24 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou
$qb = $this->entityManager->createQueryBuilder();
foreach ($this->fields as $f) {
if (in_array($f, $data['fields'], true)) {
switch ($f) {
case 'countryOfBirth':
case 'nationality':
$qb->addSelect(sprintf('IDENTITY(person.%s) as %s', $f, $f));
$qb
->from(Person::class, 'person')
->andWhere(
$qb->expr()->exists(
'SELECT 1 FROM ' . Person\PersonCenterHistory::class . ' pch WHERE pch.person = person.id AND pch.center IN (:authorized_centers)'
)
)
->setParameter('authorized_centers', $centers);
break;
$fields = $data['fields'];
case 'address_street_address_1':
case 'address_street_address_2':
case 'address_valid_from':
case 'address_postcode_label':
case 'address_postcode_code':
case 'address_country_name':
case 'address_country_code':
case 'address_isnoaddress':
$qb->addSelect(sprintf(
'GET_PERSON_ADDRESS_%s(person.id, :address_date) AS %s',
// get the part after address_
strtoupper(substr($f, 8)),
$f
));
$qb->setParameter('address_date', $data['address_date']);
break;
default:
$qb->addSelect(sprintf('person.%s as %s', $f, $f));
}
}
}
$this->listPersonHelper->addSelect($qb, $fields, $data['address_date']);
foreach ($this->getCustomFields() as $cf) {
if (!in_array($cf->getSlug(), $fields, true)) {
continue;
}
$cfType = $this->customFieldProvider->getCustomFieldByType($cf->getType());
if ($cfType instanceof CustomFieldChoice && $cfType->isMultiple($cf)) {
@ -345,12 +248,6 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou
}
}
$qb
->from('ChillPersonBundle:Person', 'person')
->join('person.center', 'center')
->andWhere('center IN (:authorized_centers)')
->setParameter('authorized_centers', $centers);
return $qb;
}
@ -368,14 +265,14 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou
{
// get the field starting with address_
$addressFields = array_filter(
$this->fields,
ListPersonHelper::FIELDS,
static fn (string $el): bool => substr($el, 0, 8) === 'address_'
);
// check if there is one field starting with address in data
if (count(array_intersect($data['fields'], $addressFields)) > 0) {
// if a field address is checked, the date must not be empty
if (empty($data['address_date'])) {
if (!$data['address_date'] instanceof DateTimeImmutable) {
$context
->buildViolation('You must set this date if an address is checked')
->atPath('address_date')
@ -456,7 +353,7 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou
. ' | ' . $label;
}
if ('_other' === $slugChoice && $cfType->isChecked($cf, $choiceSlug, $decoded)) {
if ('_other' === $slugChoice && $cfType->isChecked($cf, $slugChoice, $decoded)) {
return $cfType->extractOtherValue($cf, $decoded);
}

View File

@ -0,0 +1,220 @@
<?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\PersonBundle\Export\Export;
use Chill\MainBundle\Export\ExportElementValidatedInterface;
use Chill\MainBundle\Export\FormatterInterface;
use Chill\MainBundle\Export\GroupedExportInterface;
use Chill\MainBundle\Export\Helper\ExportAddressHelper;
use Chill\MainBundle\Export\ListInterface;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\Person\PersonCenterHistory;
use Chill\PersonBundle\Export\Declarations;
use Chill\PersonBundle\Export\Helper\ListPersonHelper;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use DateTimeImmutable;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Constraints\Callback;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use function array_key_exists;
use function count;
use function in_array;
use function strlen;
class ListPersonWithAccompanyingPeriod implements ExportElementValidatedInterface, ListInterface, GroupedExportInterface
{
private ExportAddressHelper $addressHelper;
private EntityManagerInterface $entityManager;
private ListPersonHelper $listPersonHelper;
public function __construct(
ExportAddressHelper $addressHelper,
ListPersonHelper $listPersonHelper,
EntityManagerInterface $em
) {
$this->addressHelper = $addressHelper;
$this->listPersonHelper = $listPersonHelper;
$this->entityManager = $em;
}
public function buildForm(FormBuilderInterface $builder)
{
$choices = array_combine(ListPersonHelper::FIELDS, ListPersonHelper::FIELDS);
// Add a checkbox to select fields
$builder->add('fields', ChoiceType::class, [
'multiple' => true,
'expanded' => true,
'choices' => $choices,
'label' => 'Fields to include in export',
'choice_attr' => static function (string $val): array {
// add a 'data-display-target' for address fields
if (substr($val, 0, 7) === 'address' || 'center' === $val || 'household' === $val) {
return ['data-display-target' => 'address_date'];
}
return [];
},
'constraints' => [new Callback([
'callback' => static function ($selected, ExecutionContextInterface $context) {
if (count($selected) === 0) {
$context->buildViolation('You must select at least one element')
->atPath('fields')
->addViolation();
}
},
])],
'data' => array_values($choices),
]);
// add a date field for addresses
$builder->add('address_date', ChillDateType::class, [
'label' => 'Data valid at this date',
'help' => 'Data regarding center, addresses, and so on will be computed at this date',
'data' => new DateTimeImmutable(),
'input' => 'datetime_immutable',
]);
}
public function getAllowedFormattersTypes()
{
return [FormatterInterface::TYPE_LIST];
}
public function getDescription()
{
return 'export.list.person_with_acp.Create a list of people having an accompaying periods, according to various filters.';
}
public function getGroup(): string
{
return 'Exports of persons';
}
public function getLabels($key, array $values, $data)
{
return $this->listPersonHelper->getLabels($key, $values, $data);
}
public function getQueryKeys($data)
{
$fields = [];
foreach (ListPersonHelper::FIELDS as $key) {
if (!in_array($key, $data['fields'], true)) {
continue;
}
if (substr($key, 0, strlen('address_fields')) === 'address_fields') {
$fields = array_merge($fields, $this->addressHelper->getKeys(ExportAddressHelper::F_ALL, 'address_fields'));
continue;
}
if ('lifecycleUpdate' === $key) {
$fields = array_merge($fields, ['createdAt', 'createdBy', 'updatedAt', 'updatedBy']);
continue;
}
$fields[] = $key;
}
return $fields;
}
public function getResult($query, $data)
{
return $query->getQuery()->getResult(AbstractQuery::HYDRATE_SCALAR);
}
public function getTitle()
{
return 'export.list.person_with_acp.List peoples having an accompanying period';
}
public function getType()
{
return Declarations::PERSON_TYPE;
}
/**
* param array{fields: string[], address_date: DateTimeImmutable} $data.
*/
public function initiateQuery(array $requiredModifiers, array $acl, array $data = [])
{
$centers = array_map(static function ($el) {
return $el['center'];
}, $acl);
// throw an error if any fields are present
if (!array_key_exists('fields', $data)) {
throw new \Doctrine\DBAL\Exception\InvalidArgumentException('any fields '
. 'have been checked');
}
$qb = $this->entityManager->createQueryBuilder();
$qb->from(Person::class, 'person')
->join('person.accompanyingPeriodParticipations', 'acppart')
->join('acppart.accompanyingPeriod', 'acp')
->andWhere($qb->expr()->neq('acp.step', "'" . AccompanyingPeriod::STEP_DRAFT . "'"))
->andWhere(
$qb->expr()->exists(
'SELECT 1 FROM ' . PersonCenterHistory::class . ' pch WHERE pch.person = person.id AND pch.center IN (:authorized_centers)'
)
)->setParameter('authorized_centers', $centers);
$fields = $data['fields'];
$this->listPersonHelper->addSelect($qb, $fields, $data['address_date']);
return $qb;
}
public function requiredRole(): string
{
return PersonVoter::LISTS;
}
public function supportsModifiers()
{
return [Declarations::PERSON_TYPE, Declarations::PERSON_IMPLIED_IN, Declarations::ACP_TYPE];
}
public function validateForm($data, ExecutionContextInterface $context)
{
// get the field starting with address_
$addressFields = array_filter(
ListPersonHelper::FIELDS,
static fn (string $el): bool => substr($el, 0, 8) === 'address_'
);
// check if there is one field starting with address in data
if (count(array_intersect($data['fields'], $addressFields)) > 0) {
// if a field address is checked, the date must not be empty
if (!$data['address_date'] instanceof DateTimeImmutable) {
$context
->buildViolation('You must set this date if an address is checked')
->atPath('address_date')
->addViolation();
}
}
}
}

View File

@ -0,0 +1,66 @@
<?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\PersonBundle\Export\Filter\AccompanyingCourseFilters;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\PickUserDynamicType;
use Chill\PersonBundle\Export\Declarations;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
use function in_array;
class CreatorFilter implements FilterInterface
{
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
if (!in_array('acp_creator', $qb->getAllAliases(), true)) {
$qb->join('acp.createdBy', 'acp_creator');
}
$qb
->andWhere($qb->expr()->in('acp_creator', ':creators'))
->setParameter('creators', $data['accepted_creators']);
}
public function applyOn(): string
{
return Declarations::ACP_TYPE;
}
public function buildForm(FormBuilderInterface $builder)
{
$builder
->add('accepted_creators', PickUserDynamicType::class, [
'multiple' => true,
'label' => false,
]);
}
public function describeAction($data, $format = 'string'): array
{
return [
'Filtered by creator: only %creators%', [
'%creators%' => implode(', ', array_map(static fn (User $u) => $u->getLabel(), $data['accepted_creators'])),
], ];
}
public function getTitle(): string
{
return 'Filter by creator';
}
}

View File

@ -0,0 +1,94 @@
<?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\PersonBundle\Export\Filter\AccompanyingCourseFilters;
use Chill\MainBundle\Entity\UserJob;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Repository\UserJobRepositoryInterface;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\PersonBundle\Export\Declarations;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\FormBuilderInterface;
use function in_array;
class CreatorJobFilter implements FilterInterface
{
private TranslatableStringHelper $translatableStringHelper;
private UserJobRepositoryInterface $userJobRepository;
public function __construct(
TranslatableStringHelper $translatableStringHelper,
UserJobRepositoryInterface $userJobRepository
) {
$this->translatableStringHelper = $translatableStringHelper;
$this->userJobRepository = $userJobRepository;
}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
if (!in_array('acp_creator', $qb->getAllAliases(), true)) {
$qb->join('acp.createdBy', 'acp_creator');
}
$qb
->andWhere($qb->expr()->in('acp_creator.userJob', ':creator_job'))
->setParameter('creator_job', $data['creator_job']);
}
public function applyOn(): string
{
return Declarations::ACP_TYPE;
}
public function buildForm(FormBuilderInterface $builder)
{
$builder->add('creator_job', EntityType::class, [
'class' => UserJob::class,
'choices' => $this->userJobRepository->findAllActive(),
'choice_label' => function (UserJob $j) {
return $this->translatableStringHelper->localize(
$j->getLabel()
);
},
'multiple' => true,
'expanded' => true,
'label' => 'Job',
]);
}
public function describeAction($data, $format = 'string'): array
{
$creatorJobs = [];
foreach ($data['creator_job'] as $j) {
$creatorJobs[] = $this->translatableStringHelper->localize(
$j->getLabel()
);
}
return ['export.filter.course.creator_job.Filtered by creator job: only %jobs%', [
'%jobs%' => implode(', ', $creatorJobs),
]];
}
public function getTitle(): string
{
return 'Filter by creator job';
}
}

View File

@ -13,8 +13,10 @@ namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Entity\GeographicalUnit;
use Chill\MainBundle\Entity\GeographicalUnit\SimpleGeographicalUnitDTO;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Repository\GeographicalUnitLayerRepositoryInterface;
use Chill\MainBundle\Repository\GeographicalUnitRepositoryInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
@ -23,7 +25,7 @@ use Chill\PersonBundle\Export\Declarations;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
/**
@ -33,6 +35,8 @@ class GeographicalUnitStatFilter implements FilterInterface
{
private EntityManagerInterface $em;
private GeographicalUnitLayerRepositoryInterface $geographicalUnitLayerRepository;
private GeographicalUnitRepositoryInterface $geographicalUnitRepository;
private TranslatableStringHelperInterface $translatableStringHelper;
@ -40,10 +44,12 @@ class GeographicalUnitStatFilter implements FilterInterface
public function __construct(
EntityManagerInterface $em,
GeographicalUnitRepositoryInterface $geographicalUnitRepository,
GeographicalUnitLayerRepositoryInterface $geographicalUnitLayerRepository,
TranslatableStringHelperInterface $translatableStringHelper
) {
$this->em = $em;
$this->geographicalUnitRepository = $geographicalUnitRepository;
$this->geographicalUnitLayerRepository = $geographicalUnitLayerRepository;
$this->translatableStringHelper = $translatableStringHelper;
}
@ -62,7 +68,7 @@ class GeographicalUnitStatFilter implements FilterInterface
WITH IDENTITY(acp_geog_filter_location_history.personLocation) = IDENTITY(acp_geog_filter_address_person_location.person)
LEFT JOIN ' . Address::class . ' acp_geog_filter_address
WITH COALESCE(IDENTITY(acp_geog_filter_address_person_location.address), IDENTITY(acp_geog_filter_location_history.addressLocation)) = acp_geog_filter_address.id
LEFT JOIN ' . GeographicalUnit::class . ' acp_geog_filter_units WITH ST_CONTAINS(acp_geog_units.geom, acp_geog_filter_address.point) = TRUE
LEFT JOIN ' . GeographicalUnit::class . ' acp_geog_filter_units WITH ST_CONTAINS(acp_geog_filter_units.geom, acp_geog_filter_address.point) = TRUE
WHERE
(acp_geog_filter_location_history.startDate <= :acp_geog_filter_date AND (
acp_geog_filter_location_history.endDate IS NULL OR acp_geog_filter_location_history.endDate < :acp_geog_filter_date
@ -78,7 +84,7 @@ class GeographicalUnitStatFilter implements FilterInterface
$qb
->andWhere($qb->expr()->exists($subQueryDql))
->setParameter('acp_geog_filter_date', $data['date_calc'])
->setParameter('acp_geog_filter_units', $data['units']);
->setParameter('acp_geog_filter_units', array_map(static fn (SimpleGeographicalUnitDTO $unitDTO) => $unitDTO->id, $data['units']));
}
public function applyOn(): string
@ -95,13 +101,13 @@ class GeographicalUnitStatFilter implements FilterInterface
'data' => new DateTimeImmutable('today'),
'input' => 'datetime_immutable',
])
->add('units', EntityType::class, [
->add('units', ChoiceType::class, [
'label' => 'Geographical unit',
'placeholder' => 'Select a geographical unit',
'class' => GeographicalUnit::class,
'choices' => $this->geographicalUnitRepository->findAll(),
'choice_label' => function (GeographicalUnit $item) {
return $this->translatableStringHelper->localize($item->getLayer()->getName()) . ' > ' . $item->getUnitName();
'choice_value' => static fn (SimpleGeographicalUnitDTO $item) => $item->id,
'choice_label' => function (SimpleGeographicalUnitDTO $item) {
return $this->translatableStringHelper->localize($this->geographicalUnitLayerRepository->find($item->layerId)->getName()) . ' > ' . $item->unitName;
},
'attr' => [
'class' => 'select2',
@ -117,8 +123,8 @@ class GeographicalUnitStatFilter implements FilterInterface
'%units' => implode(
', ',
array_map(
function (GeographicalUnit $item) {
return $this->translatableStringHelper->localize($item->getLayer()->getName()) . ' > ' . $item->getUnitName();
function (SimpleGeographicalUnitDTO $item) {
return $this->translatableStringHelper->localize($this->geographicalUnitLayerRepository->find($item->layerId)->getName()) . ' > ' . $item->unitName;
},
$data['units']
)

View File

@ -0,0 +1,51 @@
<?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\PersonBundle\Export\Filter\AccompanyingCourseFilters;
use Chill\MainBundle\Export\FilterInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Chill\PersonBundle\Export\Declarations;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
class HasNoActionFilter implements FilterInterface
{
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data): void
{
$qb->andWhere('NOT EXISTS (SELECT 1 FROM ' . AccompanyingPeriodWork::class . ' work WHERE work.accompanyingPeriod = acp)');
}
public function applyOn(): string
{
return Declarations::ACP_TYPE;
}
public function buildForm(FormBuilderInterface $builder): void
{
// no form
}
public function describeAction($data, $format = 'string'): array
{
return ['Filtered acp which has no actions'];
}
public function getTitle(): string
{
return 'Filter by which has no action';
}
}

View File

@ -0,0 +1,73 @@
<?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\PersonBundle\Export\Filter\AccompanyingCourseFilters;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\PersonBundle\Entity\AccompanyingPeriod\UserHistory;
use Chill\PersonBundle\Export\Declarations;
use DateTimeImmutable;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
class HasNoReferrerFilter implements FilterInterface
{
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
$qb
->andWhere('
NOT EXISTS (
SELECT 1 FROM ' . UserHistory::class . ' uh
WHERE uh.startDate <= :has_no_referrer_filter_date
AND (
uh.endDate IS NULL
or uh.endDate > :has_no_referrer_filter_date
)
AND uh.accompanyingPeriod = acp
)
')
->setParameter('has_no_referrer_filter_date', $data['calc_date'], Types::DATE_IMMUTABLE);
}
public function applyOn(): string
{
return Declarations::ACP_TYPE;
}
public function buildForm(FormBuilderInterface $builder)
{
$builder
->add('calc_date', ChillDateType::class, [
'label' => 'Has no referrer on this date',
'data' => new DateTimeImmutable(),
'input' => 'datetime_immutable',
]);
}
public function describeAction($data, $format = 'string'): array
{
return ['Filtered acp which has no referrer on date: %date%', [
'%date%' => $data['calc_date']->format('d-m-Y'),
]];
}
public function getTitle(): string
{
return 'Filter by which has no referrer';
}
}

View File

@ -0,0 +1,105 @@
<?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\PersonBundle\Export\Filter\AccompanyingCourseFilters;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\PersonBundle\Export\Declarations;
use DateTimeImmutable;
use Doctrine\ORM\QueryBuilder;
use LogicException;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
class HasTemporaryLocationFilter implements FilterInterface
{
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
$qb
->join('acp.locationHistories', 'acp_having_temporarily_location')
->andWhere('acp_having_temporarily_location.startDate <= :acp_having_temporarily_location_date
AND (acp_having_temporarily_location.endDate IS NULL OR acp_having_temporarily_location.endDate > :acp_having_temporarily_location_date)')
->setParameter('acp_having_temporarily_location_date', $data['calc_date']);
switch ($data['having_temporarily']) {
case true:
$qb->andWhere('acp_having_temporarily_location.addressLocation IS NOT NULL');
break;
case false:
$qb->andWhere('acp_having_temporarily_location.personLocation IS NOT NULL');
break;
default:
throw new LogicException('value not supported');
}
}
public function applyOn(): string
{
return Declarations::ACP_TYPE;
}
public function buildForm(FormBuilderInterface $builder)
{
$builder
->add('having_temporarily', ChoiceType::class, [
'choices' => [
'export.filter.course.having_temporarily.Having a temporarily location' => true,
'export.filter.course.having_temporarily.Having a person\'s location' => false,
],
'choice_label' => static function ($choice) {
switch ($choice) {
case true:
return 'export.filter.course.having_temporarily.Having a temporarily location';
case false:
return 'export.filter.course.having_temporarily.Having a person\'s location';
default:
throw new LogicException('this choice is not supported');
}
},
])
->add('calc_date', ChillDateType::class, [
'label' => 'export.filter.course.having_temporarily.Calculation date',
'input' => 'datetime_immutable',
'data' => new DateTimeImmutable(),
]);
}
public function describeAction($data, $format = 'string'): array
{
switch ($data['having_temporarily']) {
case true:
return ['export.filter.course.having_temporarily.Having a temporarily location', []];
case false:
return ['export.filter.course.having_temporarily.Having a person\'s location', []];
default:
throw new LogicException('value not supported');
}
}
public function getTitle(): string
{
return 'Filter by temporary location';
}
}

View File

@ -12,15 +12,23 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Chill\PersonBundle\Export\Declarations;
use DateTime;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
class OpenBetweenDatesFilter implements FilterInterface
{
private RollingDateConverterInterface $rollingDateConverter;
public function __construct(RollingDateConverterInterface $rollingDateConverter)
{
$this->rollingDateConverter = $rollingDateConverter;
}
public function addRole(): ?string
{
return null;
@ -34,8 +42,8 @@ class OpenBetweenDatesFilter implements FilterInterface
);
$qb->andWhere($clause);
$qb->setParameter('datefrom', $data['date_from'], Types::DATE_MUTABLE);
$qb->setParameter('dateto', $data['date_to'], Types::DATE_MUTABLE);
$qb->setParameter('datefrom', $this->rollingDateConverter->convert($data['date_from']), Types::DATE_IMMUTABLE);
$qb->setParameter('dateto', $this->rollingDateConverter->convert($data['date_to']), Types::DATE_IMMUTABLE);
}
public function applyOn(): string
@ -46,19 +54,19 @@ class OpenBetweenDatesFilter implements FilterInterface
public function buildForm(FormBuilderInterface $builder)
{
$builder
->add('date_from', ChillDateType::class, [
'data' => new DateTime(),
->add('date_from', PickRollingDateType::class, [
'data' => new RollingDate(RollingDate::T_MONTH_PREVIOUS_START),
])
->add('date_to', ChillDateType::class, [
'data' => new DateTime(),
->add('date_to', PickRollingDateType::class, [
'data' => new RollingDate(RollingDate::T_TODAY),
]);
}
public function describeAction($data, $format = 'string'): array
{
return ['Filtered by opening dates: between %datefrom% and %dateto%', [
'%datefrom%' => $data['date_from']->format('d-m-Y'),
'%dateto%' => $data['date_to']->format('d-m-Y'),
'%datefrom%' => $this->rollingDateConverter->convert($data['date_from'])->format('d-m-Y'),
'%dateto%' => $this->rollingDateConverter->convert($data['date_to'])->format('d-m-Y'),
]];
}

View File

@ -12,7 +12,6 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
use Chill\PersonBundle\Export\Declarations;
use Chill\PersonBundle\Form\Type\PickSocialIssueType;
@ -31,15 +30,11 @@ class SocialIssueFilter implements FilterInterface
private SocialIssueRender $socialIssueRender;
private TranslatableStringHelper $translatableStringHelper;
public function __construct(
TranslatorInterface $translator,
TranslatableStringHelper $translatableStringHelper,
SocialIssueRender $socialIssueRender
) {
$this->translator = $translator;
$this->translatableStringHelper = $translatableStringHelper;
$this->socialIssueRender = $socialIssueRender;
}
@ -59,7 +54,7 @@ class SocialIssueFilter implements FilterInterface
$qb->andWhere($clause)
->setParameter(
'socialissues',
SocialIssue::getDescendantsWithThisForIssues($data['accepted_socialissues'])
SocialIssue::getDescendantsWithThisForIssues($data['accepted_socialissues']->toArray())
);
}

View File

@ -12,31 +12,40 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Export\Declarations;
use Doctrine\ORM\Query\Expr\Andx;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use function in_array;
class StepFilter implements FilterInterface
{
private const A = 'acp_filter_bystep_stephistories';
private const DEFAULT_CHOICE = AccompanyingPeriod::STEP_CONFIRMED;
private const P = 'acp_step_filter_date';
private const STEPS = [
'Draft' => AccompanyingPeriod::STEP_DRAFT,
'Confirmed' => AccompanyingPeriod::STEP_CONFIRMED,
'Closed' => AccompanyingPeriod::STEP_CLOSED,
];
private RollingDateConverterInterface $rollingDateConverter;
/**
* @var TranslatorInterface
*/
protected $translator;
private $translator;
public function __construct(TranslatorInterface $translator)
public function __construct(RollingDateConverterInterface $rollingDateConverter, TranslatorInterface $translator)
{
$this->rollingDateConverter = $rollingDateConverter;
$this->translator = $translator;
}
@ -47,17 +56,25 @@ class StepFilter implements FilterInterface
public function alterQuery(QueryBuilder $qb, $data)
{
$where = $qb->getDQLPart('where');
$clause = $qb->expr()->eq('acp.step', ':step');
if ($where instanceof Andx) {
$where->add($clause);
} else {
$where = $qb->expr()->andX($clause);
if (!in_array(self::A, $qb->getAllAliases(), true)) {
$qb->leftJoin('acp.stepHistories', self::A);
}
$qb->add('where', $where);
$qb->setParameter('step', $data['accepted_steps']);
$qb
->andWhere(
$qb->expr()->andX(
$qb->expr()->lte(self::A . '.startDate', ':' . self::P),
$qb->expr()->orX(
$qb->expr()->isNull(self::A . '.endDate'),
$qb->expr()->lt(self::A . '.endDate', ':' . self::P)
)
)
)
->andWhere(
$qb->expr()->in(self::A . '.step', ':acp_filter_by_step_steps')
)
->setParameter(self::P, $this->rollingDateConverter->convert($data['calc_date']))
->setParameter('acp_filter_by_step_steps', $data['accepted_steps']);
}
public function applyOn()
@ -67,13 +84,17 @@ class StepFilter implements FilterInterface
public function buildForm(FormBuilderInterface $builder)
{
$builder->add('accepted_steps', ChoiceType::class, [
'choices' => self::STEPS,
'multiple' => false,
'expanded' => true,
'empty_data' => self::DEFAULT_CHOICE,
'data' => self::DEFAULT_CHOICE,
]);
$builder
->add('accepted_steps', ChoiceType::class, [
'choices' => self::STEPS,
'multiple' => false,
'expanded' => true,
'empty_data' => self::DEFAULT_CHOICE,
'data' => self::DEFAULT_CHOICE,
])
->add('calc_date', PickRollingDateType::class, [
'label' => 'export.acp.filter.by_step.date_calc',
]);
}
public function describeAction($data, $format = 'string')

View File

@ -0,0 +1,69 @@
<?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\PersonBundle\Export\Filter\EvaluationFilters;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\PersonBundle\Export\Declarations;
use DateTimeImmutable;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
class ByEndDateFilter implements FilterInterface
{
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data): void
{
$qb
->andWhere('workeval.endDate BETWEEN :work_eval_by_end_date_start_date and :work_eval_by_end_date_end_date')
->setParameter('work_eval_by_end_date_start_date', $data['start_date'], Types::DATE_IMMUTABLE)
->setParameter('work_eval_by_end_date_end_date', $data['end_date'], Types::DATE_IMMUTABLE);
}
public function applyOn(): string
{
return Declarations::EVAL_TYPE;
}
public function buildForm(FormBuilderInterface $builder): void
{
$builder
->add('start_date', ChillDateType::class, [
'label' => 'start period date',
'data' => new DateTimeImmutable('1 year ago'),
'input' => 'datetime_immutable',
])
->add('end_date', ChillDateType::class, [
'label' => 'end period date',
'data' => new DateTimeImmutable(),
'input' => 'datetime_immutable',
]);
}
public function describeAction($data, $format = 'string'): array
{
return ['Filtered by end date: between %start_date% and %end_date%', [
'%start_date%' => $data['start_date']->format('d-m-Y'),
'%end_date%' => $data['end_date']->format('d-m-Y'),
]];
}
public function getTitle(): string
{
return 'Filter by end date evaluations';
}
}

View File

@ -0,0 +1,69 @@
<?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\PersonBundle\Export\Filter\EvaluationFilters;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\PersonBundle\Export\Declarations;
use DateTimeImmutable;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
class ByStartDateFilter implements FilterInterface
{
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data): void
{
$qb
->andWhere('workeval.startDate BETWEEN :work_eval_by_start_date_start_date and :work_eval_by_start_date_end_date')
->setParameter('work_eval_by_start_date_start_date', $data['start_date'], Types::DATE_IMMUTABLE)
->setParameter('work_eval_by_start_date_end_date', $data['end_date'], Types::DATE_IMMUTABLE);
}
public function applyOn(): string
{
return Declarations::EVAL_TYPE;
}
public function buildForm(FormBuilderInterface $builder): void
{
$builder
->add('start_date', ChillDateType::class, [
'label' => 'start period date',
'data' => new DateTimeImmutable('1 year ago'),
'input' => 'datetime_immutable',
])
->add('end_date', ChillDateType::class, [
'label' => 'end period date',
'data' => new DateTimeImmutable(),
'input' => 'datetime_immutable',
]);
}
public function describeAction($data, $format = 'string'): array
{
return ['Filtered by start date: between %start_date% and %end_date%', [
'%start_date%' => $data['start_date']->format('d-m-Y'),
'%end_date%' => $data['end_date']->format('d-m-Y'),
]];
}
public function getTitle(): string
{
return 'Filter by start date evaluations';
}
}

View File

@ -0,0 +1,50 @@
<?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\PersonBundle\Export\Filter\EvaluationFilters;
use Chill\MainBundle\Export\FilterInterface;
use Chill\PersonBundle\Export\Declarations;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
class CurrentEvaluationsFilter implements FilterInterface
{
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data): void
{
$qb->andWhere('workeval.endDate IS NULL');
}
public function applyOn(): string
{
return Declarations::EVAL_TYPE;
}
public function buildForm(FormBuilderInterface $builder): void
{
//no form needed
}
public function describeAction($data, $format = 'string'): array
{
return ['Filtered by current evaluations'];
}
public function getTitle(): string
{
return 'Filter by current evaluations';
}
}

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