mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-10-04 12:29:43 +00:00
Merge branch 'master' into migrate_to_sf72
# Conflicts: # docs/source/_static/code/exports/BirthdateFilter.php # rector.php # src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/ByActivityTypeAggregator.php # src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/BySocialActionAggregator.php # src/Bundle/ChillActivityBundle/Export/Aggregator/ACPAggregators/BySocialIssueAggregator.php # src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityLocationAggregator.php # src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityPresenceAggregator.php # src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityReasonAggregator.php # src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityTypeAggregator.php # src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUserAggregator.php # src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUsersAggregator.php # src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUsersJobAggregator.php # src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityUsersScopeAggregator.php # src/Bundle/ChillActivityBundle/Export/Aggregator/ByCreatorAggregator.php # src/Bundle/ChillActivityBundle/Export/Aggregator/ByThirdpartyAggregator.php # src/Bundle/ChillActivityBundle/Export/Aggregator/CreatorJobAggregator.php # src/Bundle/ChillActivityBundle/Export/Aggregator/CreatorScopeAggregator.php # src/Bundle/ChillActivityBundle/Export/Aggregator/DateAggregator.php # src/Bundle/ChillActivityBundle/Export/Aggregator/LocationTypeAggregator.php # src/Bundle/ChillActivityBundle/Export/Aggregator/PersonAggregators/HouseholdAggregator.php # src/Bundle/ChillActivityBundle/Export/Aggregator/PersonAggregators/PersonAggregator.php # src/Bundle/ChillActivityBundle/Export/Aggregator/PersonsAggregator.php # src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/SumActivityDuration.php # src/Bundle/ChillActivityBundle/Export/Export/LinkedToACP/SumActivityVisitDuration.php # src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/ActivityTypeFilter.php # src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/BySocialActionFilter.php # src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/BySocialIssueFilter.php # src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/HasNoActivityFilter.php # src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/PeriodHavingActivityBetweenDatesFilter.php # src/Bundle/ChillActivityBundle/Export/Filter/ActivityDateFilter.php # src/Bundle/ChillActivityBundle/Export/Filter/ActivityPresenceFilter.php # src/Bundle/ChillActivityBundle/Export/Filter/ActivityTypeFilter.php # src/Bundle/ChillActivityBundle/Export/Filter/ActivityUsersFilter.php # src/Bundle/ChillActivityBundle/Export/Filter/ByCreatorFilter.php # src/Bundle/ChillActivityBundle/Export/Filter/CreatorJobFilter.php # src/Bundle/ChillActivityBundle/Export/Filter/CreatorScopeFilter.php # src/Bundle/ChillActivityBundle/Export/Filter/EmergencyFilter.php # src/Bundle/ChillActivityBundle/Export/Filter/LocationFilter.php # src/Bundle/ChillActivityBundle/Export/Filter/LocationTypeFilter.php # src/Bundle/ChillActivityBundle/Export/Filter/PersonFilters/ActivityReasonFilter.php # src/Bundle/ChillActivityBundle/Export/Filter/PersonsFilter.php # src/Bundle/ChillActivityBundle/Export/Filter/SentReceivedFilter.php # src/Bundle/ChillActivityBundle/Export/Filter/UserFilter.php # src/Bundle/ChillActivityBundle/Export/Filter/UsersJobFilter.php # src/Bundle/ChillActivityBundle/Export/Filter/UsersScopeFilter.php # src/Bundle/ChillActivityBundle/Validator/Constraints/ActivityValidity.php # src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByActivityTypeAggregator.php # src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByUserJobAggregator.php # src/Bundle/ChillAsideActivityBundle/src/Export/Aggregator/ByUserScopeAggregator.php # src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByActivityTypeFilter.php # src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByDateFilter.php # src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserFilter.php # src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserJobFilter.php # src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserScopeFilter.php # src/Bundle/ChillCalendarBundle/Export/Aggregator/AgentAggregator.php # src/Bundle/ChillCalendarBundle/Export/Aggregator/CancelReasonAggregator.php # src/Bundle/ChillCalendarBundle/Export/Aggregator/JobAggregator.php # src/Bundle/ChillCalendarBundle/Export/Aggregator/LocationAggregator.php # src/Bundle/ChillCalendarBundle/Export/Aggregator/LocationTypeAggregator.php # src/Bundle/ChillCalendarBundle/Export/Aggregator/MonthYearAggregator.php # src/Bundle/ChillCalendarBundle/Export/Aggregator/ScopeAggregator.php # src/Bundle/ChillCalendarBundle/Export/Aggregator/UrgencyAggregator.php # src/Bundle/ChillCalendarBundle/Export/Filter/AgentFilter.php # src/Bundle/ChillCalendarBundle/Export/Filter/BetweenDatesFilter.php # src/Bundle/ChillCalendarBundle/Export/Filter/CalendarRangeFilter.php # src/Bundle/ChillCalendarBundle/Export/Filter/JobFilter.php # src/Bundle/ChillCalendarBundle/Export/Filter/ScopeFilter.php # src/Bundle/ChillEventBundle/Export/Aggregator/EventDateAggregator.php # src/Bundle/ChillEventBundle/Export/Aggregator/EventTypeAggregator.php # src/Bundle/ChillEventBundle/Export/Aggregator/RoleAggregator.php # src/Bundle/ChillEventBundle/Export/Filter/EventDateFilter.php # src/Bundle/ChillEventBundle/Export/Filter/EventTypeFilter.php # src/Bundle/ChillEventBundle/Export/Filter/RoleFilter.php # src/Bundle/ChillMainBundle/Controller/ExportController.php # src/Bundle/ChillMainBundle/Controller/SavedExportController.php # src/Bundle/ChillMainBundle/DependencyInjection/CompilerPass/ExportsCompilerPass.php # src/Bundle/ChillMainBundle/Entity/Notification.php # src/Bundle/ChillMainBundle/Export/ExportManager.php # src/Bundle/ChillMainBundle/Export/Formatter/CSVFormatter.php # src/Bundle/ChillMainBundle/Export/Formatter/CSVListFormatter.php # src/Bundle/ChillMainBundle/Export/Formatter/CSVPivotedListFormatter.php # src/Bundle/ChillMainBundle/Export/Formatter/SpreadSheetFormatter.php # src/Bundle/ChillMainBundle/Export/Formatter/SpreadsheetListFormatter.php # src/Bundle/ChillMainBundle/Form/SavedExportType.php # src/Bundle/ChillMainBundle/Form/Type/DataTransformer/EntityToJsonTransformer.php # src/Bundle/ChillMainBundle/Tests/Export/ExportManagerTest.php # src/Bundle/ChillMainBundle/Tests/Export/SortExportElementTest.php # src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/AdministrativeLocationAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ClosingDateAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ClosingMotiveAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ConfidentialAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/CreatorJobAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/DurationAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/EmergencyAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/EvaluationAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/GeographicalUnitStatAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/IntensityAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/JobWorkingOnCourseAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/OpeningDateAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/OriginAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/PersonParticipatingAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ReferrerAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ReferrerScopeAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/RequestorAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ScopeAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ScopeWorkingOnCourseAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/SocialActionAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/SocialIssueAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/StepAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/UserJobAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/UserWorkingOnCourseAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingPeriodStepHistoryAggregators/ByClosingMotiveAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingPeriodStepHistoryAggregators/ByDateAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingPeriodStepHistoryAggregators/ByStepAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/EvaluationAggregators/ByEndDateAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/EvaluationAggregators/ByMaxDateAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/EvaluationAggregators/ByStartDateAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/EvaluationAggregators/EvaluationTypeAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/EvaluationAggregators/HavingEndDateAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/HouseholdAggregators/ChildrenNumberAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/HouseholdAggregators/CompositionAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/AdministrativeStatusAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/AgeAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/ByHouseholdCompositionAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/CenterAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/EmploymentStatusAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/GenderAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/HouseholdPositionAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/MaritalStatusAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/NationalityAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/PostalCodeAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/ActionTypeAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/CreatorAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/CreatorJobAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/CreatorScopeAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/GoalAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/GoalResultAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/HandlingThirdPartyAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/JobAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/ReferrerAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/ResultAggregator.php # src/Bundle/ChillPersonBundle/Export/Aggregator/SocialWorkAggregators/ScopeAggregator.php # src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ActiveOnDateFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ActiveOneDayBetweenDatesFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/AdministrativeLocationFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ClosingMotiveFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ConfidentialFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/CreatorFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/CreatorJobFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/EmergencyFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/EvaluationFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/GeographicalUnitStatFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/HandlingThirdPartyFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/HasNoReferrerFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/HasTemporaryLocationFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/IntensityFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/NotAssociatedWithAReferenceAddressFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/OpenBetweenDatesFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/OriginFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ReferrerFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ReferrerFilterBetweenDates.php # src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/RequestorFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/SocialActionFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/SocialIssueFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/StepFilterBetweenDates.php # src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/StepFilterOnDate.php # src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserJobFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingPeriodStepHistoryFilters/ByDateFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingPeriodStepHistoryFilters/ByStepFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/EvaluationFilters/EvaluationTypeFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/EvaluationFilters/MaxDateFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/AddressRefStatusFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/AgeFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/BirthdateFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/ByHouseholdCompositionFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/DeadOrAliveFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/DeathdateFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/GenderFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/GeographicalUnitFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/MaritalStatusFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/NationalityFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/ResidentialAddressAtThirdpartyFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/ResidentialAddressAtUserFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/WithParticipationBetweenDatesFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/WithoutHouseholdComposition.php # src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/WithoutParticipationBetweenDatesFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/CreatorFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/CreatorJobFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/CreatorScopeFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/JobFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/ReferrerFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/ScopeFilter.php # src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/SocialWorkTypeFilter.php # src/Bundle/ChillPersonBundle/Export/Helper/FilterListAccompanyingPeriodHelper.php # src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodVoter.php # src/Bundle/ChillPersonBundle/Tests/Export/Export/ListAccompanyingPeriodTest.php # src/Bundle/ChillPersonBundle/Validator/Constraints/AccompanyingPeriod/AccompanyingPeriodValidity.php # src/Bundle/ChillPersonBundle/Validator/Constraints/AccompanyingPeriod/ConfidentialCourseMustHaveReferrer.php # src/Bundle/ChillPersonBundle/Validator/Constraints/AccompanyingPeriod/LocationValidity.php # src/Bundle/ChillPersonBundle/Validator/Constraints/Household/MaxHolder.php # src/Bundle/ChillReportBundle/Export/Export/ReportList.php # src/Bundle/ChillReportBundle/Export/Filter/ReportDateFilter.php
This commit is contained in:
@@ -14,13 +14,13 @@ namespace Chill\MainBundle;
|
||||
use Chill\MainBundle\Cron\CronJobInterface;
|
||||
use Chill\MainBundle\CRUD\CompilerPass\CRUDControllerCompilerPass;
|
||||
use Chill\MainBundle\DependencyInjection\CompilerPass\ACLFlagsCompilerPass;
|
||||
use Chill\MainBundle\DependencyInjection\CompilerPass\ExportsCompilerPass;
|
||||
use Chill\MainBundle\DependencyInjection\CompilerPass\MenuCompilerPass;
|
||||
use Chill\MainBundle\DependencyInjection\CompilerPass\NotificationCounterCompilerPass;
|
||||
use Chill\MainBundle\DependencyInjection\CompilerPass\SearchableServicesCompilerPass;
|
||||
use Chill\MainBundle\DependencyInjection\CompilerPass\TimelineCompilerClass;
|
||||
use Chill\MainBundle\DependencyInjection\CompilerPass\WidgetsCompilerPass;
|
||||
use Chill\MainBundle\DependencyInjection\ConfigConsistencyCompilerPass;
|
||||
use Chill\MainBundle\Notification\FlagProviders\NotificationFlagProviderInterface;
|
||||
use Chill\MainBundle\Notification\NotificationHandlerInterface;
|
||||
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
|
||||
use Chill\MainBundle\Search\SearchApiInterface;
|
||||
@@ -62,11 +62,12 @@ class ChillMainBundle extends Bundle
|
||||
->addTag('chill_main.entity_info_provider');
|
||||
$container->registerForAutoconfiguration(ProvideRoleInterface::class)
|
||||
->addTag('chill_main.provide_role');
|
||||
$container->registerForAutoconfiguration(NotificationFlagProviderInterface::class)
|
||||
->addTag('chill_main.notification_flag_provider');
|
||||
|
||||
$container->addCompilerPass(new SearchableServicesCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
|
||||
$container->addCompilerPass(new ConfigConsistencyCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
|
||||
$container->addCompilerPass(new TimelineCompilerClass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
|
||||
$container->addCompilerPass(new ExportsCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
|
||||
$container->addCompilerPass(new WidgetsCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
|
||||
$container->addCompilerPass(new NotificationCounterCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
|
||||
$container->addCompilerPass(new MenuCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
|
||||
|
@@ -11,36 +11,39 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\Entity\ExportGeneration;
|
||||
use Chill\MainBundle\Entity\SavedExport;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Export\DirectExportInterface;
|
||||
use Chill\MainBundle\Export\ExportConfigNormalizer;
|
||||
use Chill\MainBundle\Export\ExportConfigProcessor;
|
||||
use Chill\MainBundle\Export\ExportFormHelper;
|
||||
use Chill\MainBundle\Export\ExportInterface;
|
||||
use Chill\MainBundle\Export\ExportManager;
|
||||
use Chill\MainBundle\Form\SavedExportType;
|
||||
use Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessage;
|
||||
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\Repository\SavedExportRepositoryInterface;
|
||||
use Chill\MainBundle\Repository\SavedExportOrExportGenerationRepository;
|
||||
use Chill\MainBundle\Security\Authorization\ChillExportVoter;
|
||||
use Chill\MainBundle\Security\Authorization\SavedExportVoter;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
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\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
/**
|
||||
* Class ExportController
|
||||
@@ -51,117 +54,23 @@ class ExportController extends AbstractController
|
||||
private readonly bool $filterStatsByCenters;
|
||||
|
||||
public function __construct(
|
||||
private readonly ChillRedis $redis,
|
||||
private readonly ExportManager $exportManager,
|
||||
private readonly FormFactoryInterface $formFactory,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly RequestStack $requestStack,
|
||||
private readonly TranslatorInterface $translator,
|
||||
private readonly SessionInterface $session,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly ExportFormHelper $exportFormHelper,
|
||||
private readonly SavedExportRepositoryInterface $savedExportRepository,
|
||||
private readonly Security $security,
|
||||
ParameterBagInterface $parameterBag,
|
||||
private readonly MessageBusInterface $messageBus,
|
||||
private readonly ClockInterface $clock,
|
||||
private readonly ExportConfigNormalizer $exportConfigNormalizer,
|
||||
private readonly SavedExportOrExportGenerationRepository $savedExportOrExportGenerationRepository,
|
||||
private readonly ExportConfigProcessor $exportConfigProcessor,
|
||||
) {
|
||||
$this->filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center'];
|
||||
}
|
||||
|
||||
#[\Symfony\Component\Routing\Attribute\Route(path: '/{_locale}/exports/download/{alias}', name: 'chill_main_export_download', methods: ['GET'])]
|
||||
public function downloadResultAction(Request $request, mixed $alias): Response
|
||||
{
|
||||
/** @var ExportManager $exportManager */
|
||||
$exportManager = $this->exportManager;
|
||||
$export = $exportManager->getExport($alias);
|
||||
$key = $request->query->get('key', null);
|
||||
$savedExport = $this->getSavedExportFromRequest($request);
|
||||
|
||||
[$dataCenters, $dataExport, $dataFormatter] = $this->rebuildData($key, $savedExport);
|
||||
|
||||
$formatterAlias = $exportManager->getFormatterAlias($dataExport['export']);
|
||||
|
||||
if (null !== $formatterAlias) {
|
||||
$formater = $exportManager->getFormatter($formatterAlias);
|
||||
} else {
|
||||
$formater = null;
|
||||
}
|
||||
|
||||
$viewVariables = [
|
||||
'alias' => $alias,
|
||||
'export' => $export,
|
||||
'export_group' => $this->getExportGroup($export),
|
||||
'saved_export' => $savedExport,
|
||||
];
|
||||
|
||||
if ($formater instanceof \Chill\MainBundle\Export\Formatter\CSVListFormatter) {
|
||||
// due to a bug in php, we add the mime type in the download view
|
||||
$viewVariables['mime_type'] = 'text/csv';
|
||||
}
|
||||
|
||||
return $this->render('@ChillMain/Export/download.html.twig', $viewVariables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a report.
|
||||
*
|
||||
* This action must work with GET queries.
|
||||
*
|
||||
* @param string $alias
|
||||
*/
|
||||
#[\Symfony\Component\Routing\Attribute\Route(path: '/{_locale}/exports/generate/{alias}', name: 'chill_main_export_generate', methods: ['GET'])]
|
||||
public function generateAction(Request $request, $alias): Response
|
||||
{
|
||||
/** @var ExportManager $exportManager */
|
||||
$exportManager = $this->exportManager;
|
||||
$key = $request->query->get('key', null);
|
||||
$savedExport = $this->getSavedExportFromRequest($request);
|
||||
|
||||
[$dataCenters, $dataExport, $dataFormatter] = $this->rebuildData($key, $savedExport);
|
||||
|
||||
return $exportManager->generate(
|
||||
$alias,
|
||||
$dataCenters['centers'],
|
||||
$dataExport['export'],
|
||||
null !== $dataFormatter ? $dataFormatter['formatter'] : []
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \RedisException
|
||||
*/
|
||||
#[\Symfony\Component\Routing\Attribute\Route(path: '/{_locale}/exports/generate-from-saved/{id}', name: 'chill_main_export_generate_from_saved')]
|
||||
public function generateFromSavedExport(#[MapEntity(id: 'id')] SavedExport $savedExport): RedirectResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted(SavedExportVoter::GENERATE, $savedExport);
|
||||
|
||||
$key = md5(uniqid((string) random_int(0, mt_getrandmax()), 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.
|
||||
*/
|
||||
#[\Symfony\Component\Routing\Attribute\Route(path: '/{_locale}/exports/', name: 'chill_main_export_index')]
|
||||
public function indexAction(): Response
|
||||
{
|
||||
$exportManager = $this->exportManager;
|
||||
|
||||
$exports = $exportManager->getExportsGrouped(true);
|
||||
|
||||
return $this->render('@ChillMain/Export/layout.html.twig', [
|
||||
'grouped_exports' => $exports,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* handle the step to build a query for an export.
|
||||
*
|
||||
@@ -174,7 +83,7 @@ class ExportController extends AbstractController
|
||||
* 3. 'generate': gather data from session from the previous steps, and
|
||||
* make a redirection to the "generate" action with data in query (HTTP GET)
|
||||
*/
|
||||
#[\Symfony\Component\Routing\Attribute\Route(path: '/{_locale}/exports/new/{alias}', name: 'chill_main_export_new')]
|
||||
#[Route(path: '/{_locale}/exports/new/{alias}', name: 'chill_main_export_new')]
|
||||
public function newAction(Request $request, string $alias): Response
|
||||
{
|
||||
// first check for ACL
|
||||
@@ -198,64 +107,6 @@ class ExportController extends AbstractController
|
||||
};
|
||||
}
|
||||
|
||||
#[\Symfony\Component\Routing\Attribute\Route(path: '/{_locale}/export/saved/update-from-key/{id}/{key}', name: 'chill_main_export_saved_edit_options_from_key')]
|
||||
public function editSavedExportOptionsFromKey(#[MapEntity(id: 'id')] SavedExport $savedExport, string $key): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_USER');
|
||||
$user = $this->getUser();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
throw new AccessDeniedHttpException();
|
||||
}
|
||||
|
||||
$data = $this->rebuildRawData($key);
|
||||
|
||||
$savedExport
|
||||
->setOptions($data);
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $this->redirectToRoute('chill_main_export_saved_edit', ['id' => $savedExport->getId()]);
|
||||
}
|
||||
|
||||
#[\Symfony\Component\Routing\Attribute\Route(path: '/{_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,
|
||||
'saved_export' => $savedExport,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* create a form to show on different steps.
|
||||
*
|
||||
@@ -263,19 +114,26 @@ class ExportController extends AbstractController
|
||||
*/
|
||||
protected function createCreateFormExport(string $alias, string $step, array $data, ?SavedExport $savedExport): FormInterface
|
||||
{
|
||||
/** @var ExportManager $exportManager */
|
||||
$exportManager = $this->exportManager;
|
||||
$isGenerate = str_starts_with($step, 'generate_');
|
||||
$canEditFull = $this->security->isGranted(ChillExportVoter::COMPOSE_EXPORT);
|
||||
|
||||
if (!$canEditFull && null === $savedExport) {
|
||||
throw new AccessDeniedHttpException('The user is not allowed to edit all filter, it should edit only SavedExport');
|
||||
}
|
||||
|
||||
$options = match ($step) {
|
||||
'export', 'generate_export' => [
|
||||
'export_alias' => $alias,
|
||||
'picked_centers' => $exportManager->getPickedCenters($data['centers'] ?? []),
|
||||
'picked_centers' => $this->filterStatsByCenters ? $this->exportFormHelper->getPickedCenters($data) : [],
|
||||
'can_edit_full' => $canEditFull,
|
||||
'allowed_filters' => $canEditFull ? null : $this->exportConfigProcessor->retrieveUsedFilters($savedExport->getOptions()['filters']),
|
||||
'allowed_aggregators' => $canEditFull ? null : $this->exportConfigProcessor->retrieveUsedAggregators($savedExport->getOptions()['aggregators']),
|
||||
],
|
||||
'formatter', 'generate_formatter' => [
|
||||
'export_alias' => $alias,
|
||||
'formatter_alias' => $exportManager->getFormatterAlias($data['export']),
|
||||
'aggregator_aliases' => $exportManager->getUsedAggregatorsAliases($data['export']),
|
||||
'aggregator_aliases' => $exportManager->getUsedAggregatorsAliases($data['export']['aggregators']),
|
||||
],
|
||||
default => [
|
||||
'export_alias' => $alias,
|
||||
@@ -284,14 +142,14 @@ class ExportController extends AbstractController
|
||||
|
||||
$defaultFormData = match ($savedExport) {
|
||||
null => $this->exportFormHelper->getDefaultData($step, $exportManager->getExport($alias), $options),
|
||||
default => $this->exportFormHelper->savedExportDataToFormData($savedExport, $step, $options),
|
||||
default => $this->exportFormHelper->savedExportDataToFormData($savedExport, $step),
|
||||
};
|
||||
|
||||
$builder = $this->formFactory
|
||||
->createNamedBuilder(
|
||||
'',
|
||||
FormType::class,
|
||||
$defaultFormData,
|
||||
'centers' === $step ? ['centers' => $defaultFormData] : $defaultFormData,
|
||||
[
|
||||
'method' => $isGenerate ? Request::METHOD_GET : Request::METHOD_POST,
|
||||
'csrf_protection' => !$isGenerate,
|
||||
@@ -328,7 +186,7 @@ class ExportController extends AbstractController
|
||||
$exportManager = $this->exportManager;
|
||||
|
||||
// check we have data from the previous step (export step)
|
||||
$data = $this->requestStack->getSession()->get('centers_step', []);
|
||||
$data = $this->session->get('centers_step', []);
|
||||
|
||||
if (null === $data && true === $this->filterStatsByCenters) {
|
||||
return $this->redirectToRoute('chill_main_export_new', [
|
||||
@@ -350,11 +208,11 @@ class ExportController extends AbstractController
|
||||
|
||||
// store data for reusing in next steps
|
||||
$data = $form->getData();
|
||||
$this->requestStack->getSession()->set(
|
||||
$this->session->set(
|
||||
'export_step_raw',
|
||||
$request->request->all()
|
||||
);
|
||||
$this->requestStack->getSession()->set('export_step', $data);
|
||||
$this->session->set('export_step', $data);
|
||||
|
||||
// redirect to next step
|
||||
return $this->redirectToRoute('chill_main_export_new', [
|
||||
@@ -368,7 +226,7 @@ class ExportController extends AbstractController
|
||||
}
|
||||
|
||||
return $this->render('@ChillMain/Export/new.html.twig', [
|
||||
'form' => $form,
|
||||
'form' => $form->createView(),
|
||||
'export_alias' => $alias,
|
||||
'export' => $export,
|
||||
'export_group' => $this->getExportGroup($export),
|
||||
@@ -384,7 +242,7 @@ class ExportController extends AbstractController
|
||||
private function formatterFormStep(Request $request, DirectExportInterface|ExportInterface $export, string $alias, ?SavedExport $savedExport = null): Response
|
||||
{
|
||||
// check we have data from the previous step (export step)
|
||||
$data = $this->requestStack->getSession()->get('export_step', null);
|
||||
$data = $this->session->get('export_step', null);
|
||||
|
||||
if (null === $data) {
|
||||
return $this->redirectToRoute('chill_main_export_new', [
|
||||
@@ -400,8 +258,8 @@ class ExportController extends AbstractController
|
||||
|
||||
if ($form->isValid()) {
|
||||
$dataFormatter = $form->getData();
|
||||
$this->requestStack->getSession()->set('formatter_step', $dataFormatter);
|
||||
$this->requestStack->getSession()->set(
|
||||
$this->session->set('formatter_step', $dataFormatter);
|
||||
$this->session->set(
|
||||
'formatter_step_raw',
|
||||
$request->request->all()
|
||||
);
|
||||
@@ -418,7 +276,7 @@ class ExportController extends AbstractController
|
||||
return $this->render(
|
||||
'@ChillMain/Export/new_formatter_step.html.twig',
|
||||
[
|
||||
'form' => $form,
|
||||
'form' => $form->createView(),
|
||||
'export' => $export,
|
||||
'export_group' => $this->getExportGroup($export),
|
||||
]
|
||||
@@ -430,14 +288,17 @@ class ExportController extends AbstractController
|
||||
* and redirect to the `generate` action.
|
||||
*
|
||||
* The data from previous steps is removed from session.
|
||||
*
|
||||
* @param string $alias
|
||||
*/
|
||||
private function forwardToGenerate(Request $request, DirectExportInterface|ExportInterface $export, $alias, ?SavedExport $savedExport): RedirectResponse
|
||||
private function forwardToGenerate(Request $request, DirectExportInterface|ExportInterface $export, $alias, ?SavedExport $savedExport): Response
|
||||
{
|
||||
$dataCenters = $this->requestStack->getSession()->get('centers_step_raw', null);
|
||||
$dataFormatter = $this->requestStack->getSession()->get('formatter_step_raw', null);
|
||||
$dataExport = $this->requestStack->getSession()->get('export_step_raw', null);
|
||||
$user = $this->getUser();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
throw new AccessDeniedHttpException('only regular users can generate export');
|
||||
}
|
||||
$dataCenters = $this->session->get('centers_step_raw', null);
|
||||
$dataFormatter = $this->session->get('formatter_step_raw', null);
|
||||
$dataExport = $this->session->get('export_step_raw', null);
|
||||
|
||||
if (null === $dataFormatter && $export instanceof ExportInterface) {
|
||||
return $this->redirectToRoute('chill_main_export_new', [
|
||||
@@ -447,60 +308,82 @@ class ExportController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
$parameters = [
|
||||
'formatter' => $dataFormatter ?? [],
|
||||
'export' => $dataExport ?? [],
|
||||
'centers' => $dataCenters ?? [],
|
||||
'alias' => $alias,
|
||||
];
|
||||
unset($parameters['_token']);
|
||||
$key = md5(uniqid((string) random_int(0, mt_getrandmax()), false));
|
||||
$dataToNormalize = $this->buildExportDataForNormalization(
|
||||
$alias,
|
||||
$dataCenters,
|
||||
$dataExport,
|
||||
$dataFormatter,
|
||||
$savedExport,
|
||||
);
|
||||
|
||||
$this->redis->setEx($key, 3600, \serialize($parameters));
|
||||
$deleteAt = $this->clock->now()->add(new \DateInterval('P6M'));
|
||||
$options = $this->exportConfigNormalizer->normalizeConfig($alias, $dataToNormalize);
|
||||
$exportGeneration = match (null === $savedExport) {
|
||||
true => new ExportGeneration($alias, $options, $deleteAt),
|
||||
false => ExportGeneration::fromSavedExport($savedExport, $deleteAt, $options),
|
||||
};
|
||||
|
||||
$this->entityManager->persist($exportGeneration);
|
||||
$this->entityManager->flush();
|
||||
$this->messageBus->dispatch(new ExportRequestGenerationMessage($exportGeneration, $user));
|
||||
|
||||
// remove data from session
|
||||
$this->requestStack->getSession()->remove('export_step_raw');
|
||||
$this->requestStack->getSession()->remove('export_step');
|
||||
$this->requestStack->getSession()->remove('formatter_step_raw');
|
||||
$this->requestStack->getSession()->remove('formatter_step');
|
||||
$this->session->remove('centers_step_raw');
|
||||
$this->session->remove('export_step_raw');
|
||||
$this->session->remove('export_step');
|
||||
$this->session->remove('formatter_step_raw');
|
||||
$this->session->remove('formatter_step');
|
||||
|
||||
return $this->redirectToRoute('chill_main_export_download', [
|
||||
'key' => $key,
|
||||
'alias' => $alias,
|
||||
'from_saved' => $savedExport?->getId(),
|
||||
]);
|
||||
return $this->redirectToRoute('chill_main_export-generation_wait', ['id' => $exportGeneration->getId()]);
|
||||
}
|
||||
|
||||
private function rebuildData($key, ?SavedExport $savedExport)
|
||||
/**
|
||||
* Build the export form data into a way suitable for normalization.
|
||||
*
|
||||
* @param string $alias the export alias
|
||||
* @param array $dataCenters Raw data from center step
|
||||
* @param array $dataExport Raw data from export step
|
||||
* @param array $dataFormatter Raw data from formatter step
|
||||
*/
|
||||
private function buildExportDataForNormalization(string $alias, ?array $dataCenters, array $dataExport, array $dataFormatter, ?SavedExport $savedExport): array
|
||||
{
|
||||
$rawData = $this->rebuildRawData($key);
|
||||
|
||||
$alias = $rawData['alias'];
|
||||
|
||||
if ($this->filterStatsByCenters) {
|
||||
$formCenters = $this->createCreateFormExport($alias, 'generate_centers', [], $savedExport);
|
||||
$formCenters->submit($rawData['centers']);
|
||||
$dataCenters = $formCenters->getData();
|
||||
$formCenters = $this->createCreateFormExport($alias, 'generate_centers', [], null);
|
||||
$formCenters->submit($dataCenters);
|
||||
$dataAsCollection = $formCenters->getData()['centers'];
|
||||
$centers = $dataAsCollection['centers'];
|
||||
$regroupments = $dataAsCollection['regroupments'] ?? [];
|
||||
$dataCenters = [
|
||||
'centers' => $centers instanceof Collection ? $centers->toArray() : $centers,
|
||||
'regroupments' => $regroupments instanceof Collection ? $regroupments->toArray() : $regroupments,
|
||||
];
|
||||
} else {
|
||||
$dataCenters = ['centers' => []];
|
||||
$dataCenters = ['centers' => [], 'regroupments' => []];
|
||||
}
|
||||
|
||||
$formExport = $this->createCreateFormExport($alias, 'generate_export', $dataCenters, $savedExport);
|
||||
$formExport->submit($rawData['export']);
|
||||
$formExport->submit($dataExport);
|
||||
$dataExport = $formExport->getData();
|
||||
|
||||
if (\count($rawData['formatter']) > 0) {
|
||||
if (\count($dataFormatter) > 0) {
|
||||
$formFormatter = $this->createCreateFormExport(
|
||||
$alias,
|
||||
'generate_formatter',
|
||||
$dataExport,
|
||||
$savedExport
|
||||
);
|
||||
$formFormatter->submit($rawData['formatter']);
|
||||
$formFormatter->submit($dataFormatter);
|
||||
$dataFormatter = $formFormatter->getData();
|
||||
}
|
||||
|
||||
return [$dataCenters, $dataExport, $dataFormatter ?? null];
|
||||
return [
|
||||
'centers' => ['centers' => $dataCenters['centers'], 'regroupments' => $dataCenters['regroupments']],
|
||||
'export' => $dataExport['export']['export'] ?? [],
|
||||
'filters' => $dataExport['export']['filters'] ?? [],
|
||||
'aggregators' => $dataExport['export']['aggregators'] ?? [],
|
||||
'pick_formatter' => $dataExport['export']['pick_formatter']['alias'],
|
||||
'formatter' => $dataFormatter['formatter'] ?? [],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -508,7 +391,7 @@ class ExportController extends AbstractController
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
private function selectCentersStep(Request $request, DirectExportInterface|ExportInterface $export, $alias, ?SavedExport $savedExport = null): RedirectResponse|Response
|
||||
private function selectCentersStep(Request $request, DirectExportInterface|ExportInterface $export, $alias, ExportGeneration|SavedExport|null $savedExport = null)
|
||||
{
|
||||
if (!$this->filterStatsByCenters) {
|
||||
return $this->redirectToRoute('chill_main_export_new', [
|
||||
@@ -521,7 +404,12 @@ class ExportController extends AbstractController
|
||||
/** @var ExportManager $exportManager */
|
||||
$exportManager = $this->exportManager;
|
||||
|
||||
$form = $this->createCreateFormExport($alias, 'centers', [], $savedExport);
|
||||
$form = $this->createCreateFormExport(
|
||||
$alias,
|
||||
'centers',
|
||||
$this->exportFormHelper->getDefaultData('centers', $export, []),
|
||||
$savedExport
|
||||
);
|
||||
|
||||
if (Request::METHOD_POST === $request->getMethod()) {
|
||||
$form->handleRequest($request);
|
||||
@@ -537,17 +425,17 @@ class ExportController extends AbstractController
|
||||
false === $exportManager->isGrantedForElement(
|
||||
$export,
|
||||
null,
|
||||
$exportManager->getPickedCenters($data['centers'])
|
||||
$this->exportFormHelper->getPickedCenters($data['centers']),
|
||||
)
|
||||
) {
|
||||
throw $this->createAccessDeniedException('you do not have access to this export for those centers');
|
||||
}
|
||||
|
||||
$this->requestStack->getSession()->set(
|
||||
$this->session->set(
|
||||
'centers_step_raw',
|
||||
$request->request->all()
|
||||
);
|
||||
$this->requestStack->getSession()->set('centers_step', $data);
|
||||
$this->session->set('centers_step', $data['centers']);
|
||||
|
||||
return $this->redirectToRoute('chill_main_export_new', [
|
||||
'step' => $this->getNextStep('centers', $export),
|
||||
@@ -560,7 +448,7 @@ class ExportController extends AbstractController
|
||||
return $this->render(
|
||||
'@ChillMain/Export/new_centers_step.html.twig',
|
||||
[
|
||||
'form' => $form,
|
||||
'form' => $form->createView(),
|
||||
'export' => $export,
|
||||
'export_group' => $this->getExportGroup($export),
|
||||
]
|
||||
@@ -631,43 +519,15 @@ class ExportController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
private function rebuildRawData(?string $key): array
|
||||
{
|
||||
if (null === $key) {
|
||||
throw $this->createNotFoundException('key does not exists');
|
||||
}
|
||||
|
||||
if (1 !== $this->redis->exists($key)) {
|
||||
$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, JSON_THROW_ON_ERROR),
|
||||
]);
|
||||
|
||||
return $rawData;
|
||||
}
|
||||
|
||||
private function getSavedExportFromRequest(Request $request): ?SavedExport
|
||||
private function getSavedExportFromRequest(Request $request): SavedExport|ExportGeneration|null
|
||||
{
|
||||
$savedExport = match ($savedExportId = $request->query->get('from_saved', '')) {
|
||||
'' => null,
|
||||
default => $this->savedExportRepository->find($savedExportId),
|
||||
default => $this->savedExportOrExportGenerationRepository->findById($savedExportId),
|
||||
};
|
||||
|
||||
if (null !== $savedExport && !$this->security->isGranted(SavedExportVoter::EDIT, $savedExport)) {
|
||||
throw new AccessDeniedHttpException('saved export edition not allowed');
|
||||
if (null !== $savedExport && !$this->security->isGranted(SavedExportVoter::GENERATE, $savedExport)) {
|
||||
throw new AccessDeniedHttpException('saved export generation not allowed');
|
||||
}
|
||||
|
||||
return $savedExport;
|
||||
|
@@ -0,0 +1,64 @@
|
||||
<?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\ExportGeneration;
|
||||
use Chill\MainBundle\Export\ExportManager;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
use Twig\Environment;
|
||||
|
||||
final readonly class ExportGenerationController
|
||||
{
|
||||
public function __construct(
|
||||
private Security $security,
|
||||
private Environment $twig,
|
||||
private SerializerInterface $serializer,
|
||||
private ExportManager $exportManager,
|
||||
) {}
|
||||
|
||||
#[Route('/{_locale}/main/export-generation/{id}/wait', methods: ['GET'], name: 'chill_main_export-generation_wait')]
|
||||
public function wait(ExportGeneration $exportGeneration): Response
|
||||
{
|
||||
if (!$this->security->isGranted('ROLE_USER')) {
|
||||
throw new AccessDeniedHttpException('Only users can download an export');
|
||||
}
|
||||
|
||||
$export = $this->exportManager->getExport($exportGeneration->getExportAlias());
|
||||
|
||||
return new Response(
|
||||
$this->twig->render('@ChillMain/ExportGeneration/wait.html.twig', ['exportGeneration' => $exportGeneration, 'export' => $export]),
|
||||
);
|
||||
}
|
||||
|
||||
#[Route('/api/1.0/main/export-generation/{id}/object', methods: ['GET'])]
|
||||
public function objectStatus(ExportGeneration $exportGeneration): JsonResponse
|
||||
{
|
||||
if (!$this->security->isGranted('ROLE_USER')) {
|
||||
throw new AccessDeniedHttpException('Only users can download an export');
|
||||
}
|
||||
|
||||
return new JsonResponse(
|
||||
$this->serializer->serialize(
|
||||
$exportGeneration,
|
||||
'json',
|
||||
[AbstractNormalizer::GROUPS => ['read']],
|
||||
),
|
||||
json: true,
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\Entity\ExportGeneration;
|
||||
use Chill\MainBundle\Entity\SavedExport;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessage;
|
||||
use Chill\MainBundle\Security\Authorization\SavedExportVoter;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
class ExportGenerationCreateFromSavedExportController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Security $security,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly MessageBusInterface $messageBus,
|
||||
private readonly ClockInterface $clock,
|
||||
private readonly SerializerInterface $serializer,
|
||||
) {}
|
||||
|
||||
#[Route('/api/1.0/main/export/export-generation/create-from-saved-export/{id}', methods: ['POST'])]
|
||||
public function __invoke(SavedExport $export): JsonResponse
|
||||
{
|
||||
if (!$this->security->isGranted(SavedExportVoter::GENERATE, $export)) {
|
||||
throw new AccessDeniedHttpException('Not allowed to generate an export from this saved export');
|
||||
}
|
||||
$user = $this->security->getUser();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
throw new AccessDeniedHttpException('Only users can create exports');
|
||||
}
|
||||
|
||||
$exportGeneration = ExportGeneration::fromSavedExport($export, $this->clock->now()->add(new \DateInterval('P6M')));
|
||||
|
||||
$this->entityManager->persist($exportGeneration);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->messageBus->dispatch(new ExportRequestGenerationMessage($exportGeneration, $user));
|
||||
|
||||
return new JsonResponse(
|
||||
$this->serializer->serialize($exportGeneration, 'json', ['groups' => ['read']]),
|
||||
json: true,
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Export\ExportManager;
|
||||
use Chill\MainBundle\Repository\ExportGenerationRepository;
|
||||
use Chill\MainBundle\Security\Authorization\ChillExportVoter;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Twig\Environment;
|
||||
|
||||
final readonly class ExportIndexController
|
||||
{
|
||||
public function __construct(
|
||||
private ExportManager $exportManager,
|
||||
private Environment $twig,
|
||||
private ExportGenerationRepository $exportGenerationRepository,
|
||||
private Security $security,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Render the list of available exports.
|
||||
*/
|
||||
#[Route(path: '/{_locale}/exports/', name: 'chill_main_export_index')]
|
||||
public function indexAction(ExportController $exportController): Response
|
||||
{
|
||||
$user = $this->security->getUser();
|
||||
if (!$user instanceof User) {
|
||||
throw new AccessDeniedHttpException('Only regular user can see this page');
|
||||
}
|
||||
|
||||
if (!$this->security->isGranted(ChillExportVoter::COMPOSE_EXPORT)) {
|
||||
throw new AccessDeniedHttpException(sprintf('Require the %s role', ChillExportVoter::COMPOSE_EXPORT));
|
||||
}
|
||||
|
||||
$exports = $this->exportManager->getExportsGrouped(true);
|
||||
|
||||
$lastExecutions = [];
|
||||
foreach ($this->exportManager->getExports() as $alias => $export) {
|
||||
$lastExecutions[$alias] = $this->exportGenerationRepository->findExportGenerationByAliasAndUser($alias, $user, 5);
|
||||
}
|
||||
|
||||
return new Response(
|
||||
$this->twig->render('@ChillMain/Export/layout.html.twig', [
|
||||
'grouped_exports' => $exports,
|
||||
'last_executions' => $lastExecutions,
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
@@ -16,6 +16,7 @@ use Chill\MainBundle\Entity\NotificationComment;
|
||||
use Chill\MainBundle\Form\NotificationCommentType;
|
||||
use Chill\MainBundle\Form\NotificationType;
|
||||
use Chill\MainBundle\Notification\Exception\NotificationHandlerNotFound;
|
||||
use Chill\MainBundle\Notification\FlagProviders\NotificationByUserFlagProvider;
|
||||
use Chill\MainBundle\Notification\NotificationHandlerManager;
|
||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||
use Chill\MainBundle\Repository\NotificationRepository;
|
||||
@@ -58,7 +59,8 @@ class NotificationController extends AbstractController
|
||||
$notification
|
||||
->setRelatedEntityClass($request->query->get('entityClass'))
|
||||
->setRelatedEntityId($request->query->getInt('entityId'))
|
||||
->setSender($this->security->getUser());
|
||||
->setSender($this->security->getUser())
|
||||
->setType(NotificationByUserFlagProvider::FLAG);
|
||||
|
||||
$tos = $request->query->all('tos');
|
||||
|
||||
|
@@ -11,13 +11,13 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\Entity\ExportGeneration;
|
||||
use Chill\MainBundle\Entity\SavedExport;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Export\ExportInterface;
|
||||
use Chill\MainBundle\Export\ExportDescriptionHelper;
|
||||
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\ExportGenerationVoter;
|
||||
use Chill\MainBundle\Security\Authorization\SavedExportVoter;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
|
||||
@@ -27,15 +27,28 @@ use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Session\Session;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\Translation\TranslatableMessage;
|
||||
use Symfony\Contracts\Translation\TranslatableInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class SavedExportController
|
||||
final readonly class SavedExportController
|
||||
{
|
||||
public function __construct(private readonly \Twig\Environment $templating, private readonly EntityManagerInterface $entityManager, private readonly ExportManager $exportManager, private readonly FormFactoryInterface $formFactory, private readonly SavedExportRepositoryInterface $savedExportRepository, private readonly Security $security, private readonly RequestStack $requestStack, private readonly TranslatorInterface $translator, private readonly UrlGeneratorInterface $urlGenerator) {}
|
||||
public function __construct(
|
||||
private \Twig\Environment $templating,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private ExportManager $exportManager,
|
||||
private FormFactoryInterface $formFactory,
|
||||
private Security $security,
|
||||
private TranslatorInterface $translator,
|
||||
private UrlGeneratorInterface $urlGenerator,
|
||||
private ExportDescriptionHelper $exportDescriptionHelper,
|
||||
private RequestStack $requestStack,
|
||||
) {}
|
||||
|
||||
#[\Symfony\Component\Routing\Attribute\Route(path: '/{_locale}/exports/saved/{id}/delete', name: 'chill_main_export_saved_delete')]
|
||||
public function delete(#[MapEntity(id: 'id')] SavedExport $savedExport, Request $request): Response
|
||||
@@ -52,6 +65,10 @@ class SavedExportController
|
||||
$this->entityManager->remove($savedExport);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$session = $this->requestStack->getSession();
|
||||
if ($session instanceof Session) {
|
||||
$session->getFlashBag()->add('success', new TranslatableMessage('saved_export.Export is deleted'));
|
||||
}
|
||||
$this->requestStack->getSession()->getFlashBag()->add('success', $this->translator->trans('saved_export.Export is deleted'));
|
||||
|
||||
return new RedirectResponse(
|
||||
@@ -70,6 +87,105 @@ class SavedExportController
|
||||
);
|
||||
}
|
||||
|
||||
#[Route(path: '/exports/saved/create-from-export-generation/{id}/new', name: 'chill_main_export_saved_create_from_export_generation')]
|
||||
public function createFromExportGeneration(ExportGeneration $exportGeneration, Request $request): Response
|
||||
{
|
||||
if (!$this->security->isGranted(ExportGenerationVoter::VIEW, $exportGeneration)) {
|
||||
throw new AccessDeniedHttpException();
|
||||
}
|
||||
|
||||
$user = $this->security->getUser();
|
||||
if (!$user instanceof User) {
|
||||
throw new AccessDeniedHttpException('only regular user can create a saved export');
|
||||
}
|
||||
|
||||
$export = $this->exportManager->getExport($exportGeneration->getExportAlias());
|
||||
$title = $export->getTitle() instanceof TranslatableInterface ? $export->getTitle()->trans($this->translator) :
|
||||
$this->translator->trans($export->getTitle());
|
||||
|
||||
$savedExport = new SavedExport();
|
||||
$savedExport
|
||||
->setExportAlias($exportGeneration->getExportAlias())
|
||||
->setUser($user)
|
||||
->setOptions($exportGeneration->getOptions())
|
||||
->setTitle(
|
||||
$request->query->has('title') ? $request->query->get('title') : $title
|
||||
);
|
||||
|
||||
if ($exportGeneration->isLinkedToSavedExport()) {
|
||||
$savedExport->setDescription($exportGeneration->getSavedExport()->getDescription());
|
||||
} else {
|
||||
$savedExport->setDescription(
|
||||
implode(
|
||||
"\n",
|
||||
array_map(
|
||||
fn (string $item) => '- '.$item."\n",
|
||||
$this->exportDescriptionHelper->describe($savedExport->getExportAlias(), $savedExport->getOptions(), includeExportTitle: false)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $this->handleEdit($savedExport, $request, true);
|
||||
}
|
||||
|
||||
#[Route(path: '/exports/saved/duplicate-from-saved-export/{id}/new', name: 'chill_main_export_saved_duplicate')]
|
||||
public function duplicate(SavedExport $previousSavedExport, Request $request): Response
|
||||
{
|
||||
$user = $this->security->getUser();
|
||||
if (!$user instanceof User) {
|
||||
throw new AccessDeniedHttpException('only regular user can create a saved export');
|
||||
}
|
||||
|
||||
if (!$this->security->isGranted(SavedExportVoter::EDIT, $previousSavedExport)) {
|
||||
throw new AccessDeniedHttpException('Not allowed to edit this saved export');
|
||||
}
|
||||
|
||||
$savedExport = new SavedExport();
|
||||
$savedExport
|
||||
->setExportAlias($previousSavedExport->getExportAlias())
|
||||
->setUser($user)
|
||||
->setOptions($previousSavedExport->getOptions())
|
||||
->setDescription($previousSavedExport->getDescription())
|
||||
->setTitle(
|
||||
$request->query->has('title') ?
|
||||
$request->query->get('title') :
|
||||
$previousSavedExport->getTitle().' ('.$this->translator->trans('saved_export.Duplicated').' '.(new \DateTimeImmutable('now'))->format('d-m-Y H:i:s').')'
|
||||
);
|
||||
|
||||
return $this->handleEdit($savedExport, $request);
|
||||
|
||||
}
|
||||
|
||||
private function handleEdit(SavedExport $savedExport, Request $request, bool $showWarningAutoGeneratedDescription = false): Response
|
||||
{
|
||||
$form = $this->formFactory->create(SavedExportType::class, $savedExport);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$this->entityManager->persist($savedExport);
|
||||
$this->entityManager->flush();
|
||||
|
||||
if (($session = $request->getSession()) instanceof Session) {
|
||||
$session->getFlashBag()->add('success', new TranslatableMessage('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/new.html.twig',
|
||||
[
|
||||
'form' => $form->createView(),
|
||||
'showWarningAutoGeneratedDescription' => $showWarningAutoGeneratedDescription,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[\Symfony\Component\Routing\Attribute\Route(path: '/{_locale}/exports/saved/{id}/edit', name: 'chill_main_export_saved_edit')]
|
||||
public function edit(#[MapEntity(id: 'id')] SavedExport $savedExport, Request $request): Response
|
||||
{
|
||||
@@ -87,7 +203,7 @@ class SavedExportController
|
||||
$this->requestStack->getSession()->getFlashBag()->add('success', $this->translator->trans('saved_export.Saved export is saved!'));
|
||||
|
||||
return new RedirectResponse(
|
||||
$this->urlGenerator->generate('chill_main_export_saved_list_my')
|
||||
$this->urlGenerator->generate('chill_main_export_saved_list_my'),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -96,45 +212,37 @@ class SavedExportController
|
||||
'@ChillMain/SavedExport/edit.html.twig',
|
||||
[
|
||||
'form' => $form->createView(),
|
||||
]
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[\Symfony\Component\Routing\Attribute\Route(path: '/{_locale}/exports/saved/my', name: 'chill_main_export_saved_list_my')]
|
||||
public function list(): Response
|
||||
#[\Symfony\Component\Routing\Attribute\Route(path: '/{_locale}/exports/saved/{savedExport}/edit-options/{exportGeneration}', name: 'chill_main_export_saved_options_edit')]
|
||||
public function updateOptionsFromGeneration(SavedExport $savedExport, ExportGeneration $exportGeneration, Request $request): Response
|
||||
{
|
||||
$user = $this->security->getUser();
|
||||
|
||||
if (!$this->security->isGranted('ROLE_USER') || !$user instanceof User) {
|
||||
throw new AccessDeniedHttpException();
|
||||
if (!$this->security->isGranted(SavedExportVoter::DUPLICATE, $savedExport)) {
|
||||
throw new AccessDeniedHttpException('You are not allowed to access this saved export');
|
||||
}
|
||||
|
||||
$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];
|
||||
if (!$this->security->isGranted(ExportGenerationVoter::VIEW, $exportGeneration)) {
|
||||
throw new AccessDeniedHttpException('You are not allowed to access this export generation');
|
||||
}
|
||||
|
||||
ksort($exportsGrouped);
|
||||
if ($savedExport->getExportAlias() !== $exportGeneration->getExportAlias()) {
|
||||
throw new UnprocessableEntityHttpException('export alias does not match');
|
||||
}
|
||||
|
||||
return new Response(
|
||||
$this->templating->render(
|
||||
'@ChillMain/SavedExport/index.html.twig',
|
||||
[
|
||||
'grouped_exports' => $exportsGrouped,
|
||||
'total' => \count($exports),
|
||||
]
|
||||
)
|
||||
$savedExport->setOptions($exportGeneration->getOptions());
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
$session = $request->getSession();
|
||||
if ($session instanceof Session) {
|
||||
$session->getFlashBag()->add('success', new TranslatableMessage('saved_export.Options updated successfully'));
|
||||
}
|
||||
|
||||
return new RedirectResponse(
|
||||
$this->urlGenerator->generate('chill_main_export_saved_edit', ['id' => $savedExport->getId()]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,104 @@
|
||||
<?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\Repository\ExportGenerationRepository;
|
||||
use Chill\MainBundle\Repository\SavedExportRepositoryInterface;
|
||||
use Chill\MainBundle\Security\Authorization\ChillExportVoter;
|
||||
use Chill\MainBundle\Security\Authorization\SavedExportVoter;
|
||||
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
|
||||
use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
final readonly class SavedExportIndexController
|
||||
{
|
||||
public function __construct(
|
||||
private \Twig\Environment $templating,
|
||||
private ExportManager $exportManager,
|
||||
private SavedExportRepositoryInterface $savedExportRepository,
|
||||
private Security $security,
|
||||
private TranslatorInterface $translator,
|
||||
private ExportGenerationRepository $exportGenerationRepository,
|
||||
private FilterOrderHelperFactoryInterface $filterOrderHelperFactory,
|
||||
) {}
|
||||
|
||||
#[Route(path: '/{_locale}/exports/saved/my', name: 'chill_main_export_saved_list_my')]
|
||||
public function list(): Response
|
||||
{
|
||||
$user = $this->security->getUser();
|
||||
|
||||
if (!$this->security->isGranted(ChillExportVoter::GENERATE_SAVED_EXPORT) || !$user instanceof User) {
|
||||
throw new AccessDeniedHttpException(sprintf('Missing role: %s', ChillExportVoter::GENERATE_SAVED_EXPORT));
|
||||
}
|
||||
|
||||
$filter = $this->buildFilter();
|
||||
|
||||
$filterParams = [];
|
||||
if ('' !== $filter->getQueryString() && null !== $filter->getQueryString()) {
|
||||
$filterParams[SavedExportRepositoryInterface::FILTER_DESCRIPTION | SavedExportRepositoryInterface::FILTER_TITLE] = $filter->getQueryString();
|
||||
}
|
||||
|
||||
$exports = array_filter(
|
||||
$this->savedExportRepository->findSharedWithUser($user, ['exportAlias' => 'ASC', 'title' => 'ASC'], filters: $filterParams),
|
||||
fn (SavedExport $savedExport): bool => $this->security->isGranted(SavedExportVoter::GENERATE, $savedExport),
|
||||
);
|
||||
|
||||
// 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);
|
||||
|
||||
// get last executions
|
||||
$lastExecutions = [];
|
||||
foreach ($exports as $savedExport) {
|
||||
$lastExecutions[$savedExport->getId()->toString()] = $this->exportGenerationRepository
|
||||
->findExportGenerationBySavedExportAndUser($savedExport, $user, 5);
|
||||
}
|
||||
|
||||
return new Response(
|
||||
$this->templating->render(
|
||||
'@ChillMain/SavedExport/index.html.twig',
|
||||
[
|
||||
'grouped_exports' => $exportsGrouped,
|
||||
'total' => \count($exports),
|
||||
'last_executions' => $lastExecutions,
|
||||
'filter' => $filter,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private function buildFilter(): FilterOrderHelper
|
||||
{
|
||||
$filter = $this->filterOrderHelperFactory->create('saved-export-index-filter');
|
||||
$filter->addSearchBox();
|
||||
|
||||
return $filter->build();
|
||||
}
|
||||
}
|
@@ -11,14 +11,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\Form\UserPhonenumberType;
|
||||
use Chill\MainBundle\Form\UserProfileType;
|
||||
use Chill\MainBundle\Security\ChillSecurity;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
@@ -41,16 +38,19 @@ final class UserProfileController extends AbstractController
|
||||
}
|
||||
|
||||
$user = $this->security->getUser();
|
||||
$editForm = $this->createPhonenumberEditForm($user);
|
||||
$editForm = $this->createForm(UserProfileType::class, $user);
|
||||
|
||||
$editForm->get('notificationFlags')->setData($user->getNotificationFlags());
|
||||
|
||||
$editForm->handleRequest($request);
|
||||
|
||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||
$phonenumber = $editForm->get('phonenumber')->getData();
|
||||
$notificationFlagsData = $editForm->get('notificationFlags')->getData();
|
||||
$user->setNotificationFlags($notificationFlagsData);
|
||||
|
||||
$user->setPhonenumber($phonenumber);
|
||||
|
||||
$this->managerRegistry->getManager()->flush();
|
||||
$this->addFlash('success', $this->translator->trans('user.profile.Phonenumber successfully updated!'));
|
||||
$em = $this->managerRegistry->getManager();
|
||||
$em->flush();
|
||||
$this->addFlash('success', $this->translator->trans('user.profile.Profile successfully updated!'));
|
||||
|
||||
return $this->redirectToRoute('chill_main_user_profile');
|
||||
}
|
||||
@@ -60,13 +60,4 @@ final class UserProfileController extends AbstractController
|
||||
'form' => $editForm,
|
||||
]);
|
||||
}
|
||||
|
||||
private function createPhonenumberEditForm(UserInterface $user): FormInterface
|
||||
{
|
||||
return $this->createForm(
|
||||
UserPhonenumberType::class,
|
||||
$user,
|
||||
)
|
||||
->add('submit', SubmitType::class, ['label' => $this->translator->trans('Save')]);
|
||||
}
|
||||
}
|
||||
|
@@ -11,7 +11,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\DataFixtures\ORM;
|
||||
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Entity\GroupCenter;
|
||||
use Chill\MainBundle\Entity\PermissionsGroup;
|
||||
use Chill\MainBundle\Entity\RoleScope;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
|
||||
@@ -57,6 +60,15 @@ class LoadUsers extends Fixture implements OrderedFixtureInterface
|
||||
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
$roleScope = new RoleScope();
|
||||
$roleScope->setRole('CHILL_MAIN_COMPOSE_EXPORT');
|
||||
$permissionGroup = new PermissionsGroup();
|
||||
$permissionGroup->setName('export');
|
||||
$permissionGroup->addRoleScope($roleScope);
|
||||
|
||||
$manager->persist($roleScope);
|
||||
$manager->persist($permissionGroup);
|
||||
|
||||
foreach (self::$refs as $username => $params) {
|
||||
$user = new User();
|
||||
|
||||
@@ -76,7 +88,14 @@ class LoadUsers extends Fixture implements OrderedFixtureInterface
|
||||
->setEmail(sprintf('%s@chill.social', \str_replace(' ', '', (string) $username)));
|
||||
|
||||
foreach ($params['groupCenterRefs'] as $groupCenterRef) {
|
||||
$user->addGroupCenter($this->getReference($groupCenterRef, GroupCenter::class));
|
||||
$user->addGroupCenter($gc = $this->getReference($groupCenterRef, GroupCenter::class));
|
||||
|
||||
$exportGroupCenter = new GroupCenter();
|
||||
$exportGroupCenter->setPermissionsGroup($permissionGroup);
|
||||
$exportGroupCenter->setCenter($gc->getCenter());
|
||||
$manager->persist($exportGroupCenter);
|
||||
|
||||
$user->addGroupCenter($exportGroupCenter);
|
||||
}
|
||||
|
||||
echo 'Creating user '.$username."... \n";
|
||||
|
@@ -78,6 +78,7 @@ use Chill\MainBundle\Form\RegroupmentType;
|
||||
use Chill\MainBundle\Form\UserGroupType;
|
||||
use Chill\MainBundle\Form\UserJobType;
|
||||
use Chill\MainBundle\Form\UserType;
|
||||
use Chill\MainBundle\Security\Authorization\ChillExportVoter;
|
||||
use Misd\PhoneNumberBundle\Doctrine\DBAL\Types\PhoneNumberType;
|
||||
use Ramsey\Uuid\Doctrine\UuidType;
|
||||
use Symfony\Component\Config\FileLocator;
|
||||
@@ -332,6 +333,9 @@ class ChillMainExtension extends Extension implements
|
||||
'strategy' => 'unanimous',
|
||||
'allow_if_all_abstain' => false,
|
||||
],
|
||||
'role_hierarchy' => [
|
||||
ChillExportVoter::COMPOSE_EXPORT => ChillExportVoter::GENERATE_SAVED_EXPORT,
|
||||
],
|
||||
]);
|
||||
|
||||
// add crud api
|
||||
|
@@ -1,102 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\DependencyInjection\CompilerPass;
|
||||
|
||||
use Chill\MainBundle\Export\ExportManager;
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Definition;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
|
||||
/**
|
||||
* Compiles the services tagged with :.
|
||||
*
|
||||
* - chill.export
|
||||
* - chill.export_formatter
|
||||
* - chill.export_aggregator
|
||||
* - chill.export_filter
|
||||
* - chill.export_elements_provider
|
||||
*/
|
||||
class ExportsCompilerPass implements CompilerPassInterface
|
||||
{
|
||||
public function process(ContainerBuilder $container): void
|
||||
{
|
||||
if (!$container->has(ExportManager::class)) {
|
||||
throw new \LogicException('service '.ExportManager::class.' is not defined. It is required by ExportsCompilerPass');
|
||||
}
|
||||
|
||||
$chillManagerDefinition = $container->findDefinition(
|
||||
ExportManager::class
|
||||
);
|
||||
|
||||
$this->compileFormatters($chillManagerDefinition, $container);
|
||||
$this->compileExportElementsProvider($chillManagerDefinition, $container);
|
||||
}
|
||||
|
||||
private function compileExportElementsProvider(
|
||||
Definition $chillManagerDefinition,
|
||||
ContainerBuilder $container,
|
||||
): void {
|
||||
$taggedServices = $container->findTaggedServiceIds(
|
||||
'chill.export_elements_provider'
|
||||
);
|
||||
|
||||
$knownAliases = [];
|
||||
|
||||
foreach ($taggedServices as $id => $tagAttributes) {
|
||||
foreach ($tagAttributes as $attributes) {
|
||||
if (!isset($attributes['prefix'])) {
|
||||
throw new \LogicException("the 'prefix' attribute is missing in your service '{$id}' definition");
|
||||
}
|
||||
|
||||
if (array_search($attributes['prefix'], $knownAliases, true)) {
|
||||
throw new \LogicException('There is already a chill.export_elements_provider service with prefix '.$attributes['prefix'].'. Choose another prefix.');
|
||||
}
|
||||
$knownAliases[] = $attributes['prefix'];
|
||||
|
||||
$chillManagerDefinition->addMethodCall(
|
||||
'addExportElementsProvider',
|
||||
[new Reference($id), $attributes['prefix']]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function compileFormatters(
|
||||
Definition $chillManagerDefinition,
|
||||
ContainerBuilder $container,
|
||||
): void {
|
||||
$taggedServices = $container->findTaggedServiceIds(
|
||||
'chill.export_formatter'
|
||||
);
|
||||
|
||||
$knownAliases = [];
|
||||
|
||||
foreach ($taggedServices as $id => $tagAttributes) {
|
||||
foreach ($tagAttributes as $attributes) {
|
||||
if (!isset($attributes['alias'])) {
|
||||
throw new \LogicException("the 'alias' attribute is missing in your service '{$id}' definition");
|
||||
}
|
||||
|
||||
if (array_search($attributes['alias'], $knownAliases, true)) {
|
||||
throw new \LogicException('There is already a chill.export_formatter service with alias '.$attributes['alias'].'. Choose another alias.');
|
||||
}
|
||||
$knownAliases[] = $attributes['alias'];
|
||||
|
||||
$chillManagerDefinition->addMethodCall(
|
||||
'addFormatter',
|
||||
[new Reference($id), $attributes['alias']]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -23,7 +23,7 @@ class Unaccent extends FunctionNode
|
||||
{
|
||||
private ?\Doctrine\ORM\Query\AST\Node $string = null;
|
||||
|
||||
public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker)
|
||||
public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker): string
|
||||
{
|
||||
return 'UNACCENT('.$this->string->dispatch($sqlWalker).')';
|
||||
}
|
||||
|
@@ -63,4 +63,28 @@ class PrivateCommentEmbeddable
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges comments from the provided object into the current object.
|
||||
*
|
||||
* Identifies common user IDs between the current object's comments and the
|
||||
* newComment's comments. If a user ID exists in both, their comments are
|
||||
* concatenated with the provided separator. If a user ID exists only in the
|
||||
* newComment, their comment is added to the current object directly.
|
||||
*
|
||||
* @param self $commentsToAppend the object containing the new comments to be merged
|
||||
* @param string $separator the string used to separate concatenated comments
|
||||
*/
|
||||
public function concatenateComments(self $commentsToAppend, string $separator = "\n\n-----------\n\n"): void
|
||||
{
|
||||
$commonUserIds = array_intersect(array_keys($this->comments), array_keys($commentsToAppend->getComments()));
|
||||
|
||||
foreach ($commentsToAppend->getComments() as $userId => $comment) {
|
||||
if (in_array($userId, $commonUserIds, true)) {
|
||||
$this->comments[$userId] = $this->comments[$userId].$separator.$commentsToAppend->getComments()[$userId];
|
||||
} else {
|
||||
$this->comments[$userId] = $commentsToAppend->getComments()[$userId];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
150
src/Bundle/ChillMainBundle/Entity/ExportGeneration.php
Normal file
150
src/Bundle/ChillMainBundle/Entity/ExportGeneration.php
Normal file
@@ -0,0 +1,150 @@
|
||||
<?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\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||
|
||||
/**
|
||||
* Contains the single execution of an export.
|
||||
*
|
||||
* Attached to a stored object, which will contains the result of the execution. The status of the stored object
|
||||
* is the status of the export generation.
|
||||
*
|
||||
* Generated exports should be deleted after a certain time, given by the column `deletedAt`. The stored object can be
|
||||
* deleted after the export generation removal.
|
||||
*/
|
||||
#[ORM\Entity()]
|
||||
#[ORM\Table('chill_main_export_generation')]
|
||||
#[Serializer\DiscriminatorMap('type', ['export_generation' => ExportGeneration::class])]
|
||||
class ExportGeneration implements TrackCreationInterface
|
||||
{
|
||||
use TrackCreationTrait;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: 'uuid', unique: true)]
|
||||
#[Serializer\Groups(['read'])]
|
||||
private UuidInterface $id;
|
||||
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: StoredObject::class, cascade: ['persist', 'refresh'])]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
#[Serializer\Groups(['read'])]
|
||||
private StoredObject $storedObject;
|
||||
|
||||
public function __construct(
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])]
|
||||
#[Serializer\Groups(['read'])]
|
||||
private string $exportAlias,
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]'])]
|
||||
private array $options = [],
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: true)]
|
||||
private ?\DateTimeImmutable $deleteAt = null,
|
||||
|
||||
/**
|
||||
* The related saved export.
|
||||
*
|
||||
* Note that, in some case, the options of this ExportGeneration are not equals to the options of the saved export.
|
||||
* This happens when the options of the saved export are updated.
|
||||
*/
|
||||
#[ORM\ManyToOne(targetEntity: SavedExport::class)]
|
||||
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
|
||||
private ?SavedExport $savedExport = null,
|
||||
) {
|
||||
$this->id = Uuid::uuid4();
|
||||
$this->storedObject = new StoredObject(StoredObject::STATUS_PENDING);
|
||||
}
|
||||
|
||||
public function setDeleteAt(?\DateTimeImmutable $deleteAt): ExportGeneration
|
||||
{
|
||||
$this->deleteAt = $deleteAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDeleteAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->deleteAt;
|
||||
}
|
||||
|
||||
public function getId(): UuidInterface
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getStoredObject(): StoredObject
|
||||
{
|
||||
return $this->storedObject;
|
||||
}
|
||||
|
||||
public function getExportAlias(): string
|
||||
{
|
||||
return $this->exportAlias;
|
||||
}
|
||||
|
||||
public function getOptions(): array
|
||||
{
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
public function getSavedExport(): ?SavedExport
|
||||
{
|
||||
return $this->savedExport;
|
||||
}
|
||||
|
||||
#[Serializer\Groups(['read'])]
|
||||
#[Serializer\SerializedName('status')]
|
||||
public function getStatus(): string
|
||||
{
|
||||
return $this->getStoredObject()->getStatus();
|
||||
}
|
||||
|
||||
public function setSavedExport(SavedExport $savedExport): self
|
||||
{
|
||||
$this->savedExport = $savedExport;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isLinkedToSavedExport(): bool
|
||||
{
|
||||
return null !== $this->savedExport;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the options of the saved export and the current export generation.
|
||||
*
|
||||
* Return false if the current export generation's options are not equal to the one in the saved export. This may
|
||||
* happens when we update the configuration of a saved export.
|
||||
*/
|
||||
public function isConfigurationDifferentFromSavedExport(): bool
|
||||
{
|
||||
if (!$this->isLinkedToSavedExport()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->savedExport->getOptions() !== $this->getOptions();
|
||||
}
|
||||
|
||||
public static function fromSavedExport(SavedExport $savedExport, ?\DateTimeImmutable $deletedAt = null, ?array $overrideOptions = null): self
|
||||
{
|
||||
$generation = new self($savedExport->getExportAlias(), $overrideOptions ?? $savedExport->getOptions(), $deletedAt, $savedExport);
|
||||
$generation->getStoredObject()->setTitle($savedExport->getTitle());
|
||||
|
||||
return $generation;
|
||||
}
|
||||
}
|
@@ -50,4 +50,9 @@ class SimpleGeographicalUnitDTO
|
||||
#[Serializer\Groups(['read'])]
|
||||
public int $layerId,
|
||||
) {}
|
||||
|
||||
public function getId(): int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
}
|
||||
|
@@ -15,6 +15,7 @@ use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
@@ -22,10 +23,11 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
#[ORM\Entity]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ORM\Table(name: 'chill_main_notification')]
|
||||
#[ORM\Index(name: 'chill_main_notification_related_entity_idx', columns: ['relatedentityclass', 'relatedentityid'])]
|
||||
#[ORM\Index(columns: ['relatedentityclass', 'relatedentityid'], name: 'chill_main_notification_related_entity_idx')]
|
||||
class Notification implements TrackUpdateInterface
|
||||
{
|
||||
use TrackUpdateTrait;
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false)]
|
||||
private string $accessKey;
|
||||
|
||||
@@ -38,12 +40,19 @@ class Notification implements TrackUpdateInterface
|
||||
#[ORM\JoinTable(name: 'chill_main_notification_addresses_user')]
|
||||
private Collection $addressees;
|
||||
|
||||
/**
|
||||
* @var Collection<int, UserGroup>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: UserGroup::class)]
|
||||
#[ORM\JoinTable(name: 'chill_main_notification_addressee_user_group')]
|
||||
private Collection $addresseeUserGroups;
|
||||
|
||||
/**
|
||||
* a list of destinee which will receive notifications.
|
||||
*
|
||||
* @var array|string[]
|
||||
*/
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, options: ['default' => '[]', 'jsonb' => true])]
|
||||
#[ORM\Column(type: Types::JSON, options: ['default' => '[]', 'jsonb' => true])]
|
||||
private array $addressesEmails = [];
|
||||
|
||||
/**
|
||||
@@ -62,21 +71,21 @@ class Notification implements TrackUpdateInterface
|
||||
#[ORM\OrderBy(['createdAt' => \Doctrine\Common\Collections\Criteria::ASC])]
|
||||
private Collection $comments;
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE)]
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
|
||||
private \DateTimeImmutable $date;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
|
||||
#[ORM\Column(type: Types::INTEGER)]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT)]
|
||||
#[ORM\Column(type: Types::TEXT)]
|
||||
private string $message = '';
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 255)]
|
||||
#[ORM\Column(type: Types::STRING, length: 255)]
|
||||
private string $relatedEntityClass = '';
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
|
||||
#[ORM\Column(type: Types::INTEGER)]
|
||||
private int $relatedEntityId;
|
||||
|
||||
private array $removedAddresses = [];
|
||||
@@ -86,7 +95,7 @@ class Notification implements TrackUpdateInterface
|
||||
private ?User $sender = null;
|
||||
|
||||
#[Assert\NotBlank(message: 'notification.Title must be defined')]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, options: ['default' => ''])]
|
||||
#[ORM\Column(type: Types::TEXT, options: ['default' => ''])]
|
||||
private string $title = '';
|
||||
|
||||
/**
|
||||
@@ -96,26 +105,47 @@ class Notification implements TrackUpdateInterface
|
||||
#[ORM\JoinTable(name: 'chill_main_notification_addresses_unread')]
|
||||
private Collection $unreadBy;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
|
||||
private ?\DateTimeImmutable $updatedAt = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||
private ?User $updatedBy = null;
|
||||
|
||||
#[ORM\Column(name: 'type', type: Types::STRING, nullable: true)]
|
||||
private string $type = '';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->addressees = new ArrayCollection();
|
||||
$this->addresseeUserGroups = new ArrayCollection();
|
||||
$this->unreadBy = new ArrayCollection();
|
||||
$this->comments = new ArrayCollection();
|
||||
$this->setDate(new \DateTimeImmutable());
|
||||
$this->accessKey = bin2hex(openssl_random_pseudo_bytes(24));
|
||||
}
|
||||
|
||||
public function addAddressee(User $addressee): self
|
||||
public function addAddressee(User|UserGroup $addressee): self
|
||||
{
|
||||
if (!$this->addressees->contains($addressee)) {
|
||||
$this->addressees[] = $addressee;
|
||||
$this->addedAddresses[] = $addressee;
|
||||
if ($addressee instanceof User) {
|
||||
if (!$this->addressees->contains($addressee)) {
|
||||
$this->addressees->add($addressee);
|
||||
$this->addedAddresses[] = $addressee;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
if (!$this->addresseeUserGroups->contains($addressee)) {
|
||||
$this->addresseeUserGroups->add($addressee);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addAddressesEmail(string $email): void
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
public function addAddressesEmail(string $email)
|
||||
{
|
||||
if (!\in_array($email, $this->addressesEmails, true)) {
|
||||
$this->addressesEmails[] = $email;
|
||||
@@ -148,13 +178,23 @@ class Notification implements TrackUpdateInterface
|
||||
#[Assert\Callback]
|
||||
public function assertCountAddresses(ExecutionContextInterface $context, $payload): void
|
||||
{
|
||||
if (0 === (\count($this->getAddressesEmails()) + \count($this->getAddressees()))) {
|
||||
if (0 === (\count($this->getAddresseeUserGroups()) + \count($this->getAddressees()))) {
|
||||
$context->buildViolation('notification.At least one addressee')
|
||||
->atPath('addressees')
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
|
||||
public function getAddresseeUserGroups(): Collection
|
||||
{
|
||||
return $this->addresseeUserGroups;
|
||||
}
|
||||
|
||||
public function setAddresseeUserGroups(Collection $addresseeUserGroups): void
|
||||
{
|
||||
$this->addresseeUserGroups = $addresseeUserGroups;
|
||||
}
|
||||
|
||||
public function getAccessKey(): string
|
||||
{
|
||||
return $this->accessKey;
|
||||
@@ -178,6 +218,23 @@ class Notification implements TrackUpdateInterface
|
||||
return $this->addressees;
|
||||
}
|
||||
|
||||
public function getAllAddressees(): array
|
||||
{
|
||||
$allUsers = [];
|
||||
|
||||
foreach ($this->getAddressees() as $user) {
|
||||
$allUsers[$user->getId()] = $user;
|
||||
}
|
||||
|
||||
foreach ($this->getAddresseeUserGroups() as $userGroup) {
|
||||
foreach ($userGroup->getUsers() as $user) {
|
||||
$allUsers[$user->getId()] = $user;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($allUsers);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|string[]
|
||||
*/
|
||||
@@ -289,12 +346,18 @@ class Notification implements TrackUpdateInterface
|
||||
$this->addressesOnLoad = null;
|
||||
}
|
||||
|
||||
public function removeAddressee(User $addressee): self
|
||||
public function removeAddressee(User|UserGroup $addressee): self
|
||||
{
|
||||
if ($this->addressees->removeElement($addressee)) {
|
||||
$this->removedAddresses[] = $addressee;
|
||||
if ($addressee instanceof User) {
|
||||
if ($this->addressees->contains($addressee)) {
|
||||
$this->addressees->removeElement($addressee);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
$this->addresseeUserGroups->removeElement($addressee);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -361,4 +424,30 @@ class Notification implements TrackUpdateInterface
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setUpdatedAt(\DateTimeInterface $datetime): self
|
||||
{
|
||||
$this->updatedAt = \DateTimeImmutable::createFromInterface($datetime);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setUpdatedBy(User $user): self
|
||||
{
|
||||
$this->updatedBy = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setType(string $type): self
|
||||
{
|
||||
$this->type = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
}
|
||||
|
@@ -102,4 +102,22 @@ class Regroupment
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the given center is contained into this regroupment.
|
||||
*/
|
||||
public function containsCenter(Center $center): bool
|
||||
{
|
||||
return $this->centers->contains($center);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if at least one of the given centers is contained into this regroupment.
|
||||
*
|
||||
* @param list<Center> $centers
|
||||
*/
|
||||
public function containsAtLeastOneCenter(array $centers): bool
|
||||
{
|
||||
return array_reduce($centers, fn (bool $carry, Center $center) => $carry || $this->containsCenter($center), false);
|
||||
}
|
||||
}
|
||||
|
@@ -15,6 +15,9 @@ 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\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\Common\Collections\ReadableCollection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
@@ -50,9 +53,25 @@ class SavedExport implements TrackCreationInterface, TrackUpdateInterface
|
||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||
private User $user;
|
||||
|
||||
/**
|
||||
* @var Collection<int, User>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: User::class)]
|
||||
#[ORM\JoinTable(name: 'chill_main_saved_export_users')]
|
||||
private Collection $sharedWithUsers;
|
||||
|
||||
/**
|
||||
* @var Collection<int, UserGroup>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: UserGroup::class)]
|
||||
#[ORM\JoinTable(name: 'chill_main_saved_export_usergroups')]
|
||||
private Collection $sharedWithGroups;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->id = Uuid::uuid4();
|
||||
$this->sharedWithUsers = new ArrayCollection();
|
||||
$this->sharedWithGroups = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
@@ -119,4 +138,71 @@ class SavedExport implements TrackCreationInterface, TrackUpdateInterface
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addShare(User|UserGroup $shareUser): SavedExport
|
||||
{
|
||||
if ($shareUser instanceof User) {
|
||||
if (!$this->sharedWithUsers->contains($shareUser)) {
|
||||
$this->sharedWithUsers->add($shareUser);
|
||||
}
|
||||
} else {
|
||||
if (!$this->sharedWithGroups->contains($shareUser)) {
|
||||
$this->sharedWithGroups->add($shareUser);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeShare(User|UserGroup $shareUser): SavedExport
|
||||
{
|
||||
if ($shareUser instanceof User) {
|
||||
$this->sharedWithUsers->removeElement($shareUser);
|
||||
} else {
|
||||
$this->sharedWithGroups->removeElement($shareUser);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ReadableCollection<int, User|UserGroup>
|
||||
*/
|
||||
public function getShare(): ReadableCollection
|
||||
{
|
||||
return new ArrayCollection([
|
||||
...$this->sharedWithUsers->toArray(),
|
||||
...$this->sharedWithGroups->toArray(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if shared with at least one user or one group.
|
||||
*/
|
||||
public function isShared(): bool
|
||||
{
|
||||
return $this->sharedWithUsers->count() > 0 || $this->sharedWithGroups->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the user is shared with either directly or through a group.
|
||||
*
|
||||
* @param User $user the user to check
|
||||
*
|
||||
* @return bool returns true if the user is shared with directly or via group, otherwise false
|
||||
*/
|
||||
public function isSharedWithUser(User $user): bool
|
||||
{
|
||||
if ($this->sharedWithUsers->contains($user)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($this->sharedWithGroups as $group) {
|
||||
if ($group->contains($user)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@@ -34,6 +34,9 @@ use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
|
||||
#[ORM\Table(name: 'users')]
|
||||
class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInterface
|
||||
{
|
||||
public const NOTIF_FLAG_IMMEDIATE_EMAIL = 'immediate-email';
|
||||
public const NOTIF_FLAG_DAILY_DIGEST = 'daily-digest';
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(name: 'id', type: \Doctrine\DBAL\Types\Types::INTEGER)]
|
||||
#[ORM\GeneratedValue(strategy: 'AUTO')]
|
||||
@@ -116,6 +119,12 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
||||
#[PhonenumberConstraint]
|
||||
private ?PhoneNumber $phonenumber = null;
|
||||
|
||||
/**
|
||||
* @var array<string, list<string>>
|
||||
*/
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]', 'jsonb' => true])]
|
||||
private array $notificationFlags = [];
|
||||
|
||||
/**
|
||||
* User constructor.
|
||||
*/
|
||||
@@ -613,4 +622,57 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current object is an instance of User.
|
||||
*
|
||||
* @return bool returns true if the current object is an instance of User, false otherwise
|
||||
*/
|
||||
public function isUser(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getNotificationFlags(): array
|
||||
{
|
||||
return $this->notificationFlags;
|
||||
}
|
||||
|
||||
public function setNotificationFlags(array $notificationFlags)
|
||||
{
|
||||
$this->notificationFlags = $notificationFlags;
|
||||
}
|
||||
|
||||
public function getNotificationFlagData(string $flag): array
|
||||
{
|
||||
return $this->notificationFlags[$flag] ?? [];
|
||||
}
|
||||
|
||||
public function setNotificationFlagData(string $flag, array $data): void
|
||||
{
|
||||
$this->notificationFlags[$flag] = $data;
|
||||
}
|
||||
|
||||
public function isNotificationSendImmediately(string $type): bool
|
||||
{
|
||||
if ([] === $this->getNotificationFlagData($type) || in_array(User::NOTIF_FLAG_IMMEDIATE_EMAIL, $this->getNotificationFlagData($type), true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function isNotificationDailyDigest(string $type): bool
|
||||
{
|
||||
if (in_array(User::NOTIF_FLAG_DAILY_DIGEST, $this->getNotificationFlagData($type), true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getLocale(): string
|
||||
{
|
||||
return 'fr';
|
||||
}
|
||||
}
|
||||
|
@@ -21,6 +21,19 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* Represents a user group entity in the system.
|
||||
*
|
||||
* This class is used for managing user groups, including their relationships
|
||||
* with users, administrative users, and additional metadata such as colors and labels.
|
||||
*
|
||||
* Groups may be configured to have mutual exclusion properties based on an
|
||||
* exclusion key. This ensures that groups sharing the same key cannot coexist
|
||||
* in certain relationship contexts.
|
||||
*
|
||||
* Groups may be related to a UserJob. In that case, a cronjob task ensure that the members of the groups are
|
||||
* automatically synced with this group. Such groups are also automatically created by the cronjob.
|
||||
*/
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'chill_main_user_group')]
|
||||
// this discriminator key is required for automated denormalization
|
||||
@@ -71,6 +84,13 @@ class UserGroup
|
||||
#[Assert\Email]
|
||||
private string $email = '';
|
||||
|
||||
/**
|
||||
* UserJob to which the group is related.
|
||||
*/
|
||||
#[ORM\ManyToOne(targetEntity: UserJob::class)]
|
||||
#[ORM\JoinColumn(nullable: true)]
|
||||
private ?UserJob $userJob = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->adminUsers = new ArrayCollection();
|
||||
@@ -209,6 +229,21 @@ class UserGroup
|
||||
return '' !== $this->email;
|
||||
}
|
||||
|
||||
public function hasUserJob(): bool
|
||||
{
|
||||
return null !== $this->userJob;
|
||||
}
|
||||
|
||||
public function getUserJob(): ?UserJob
|
||||
{
|
||||
return $this->userJob;
|
||||
}
|
||||
|
||||
public function setUserJob(?UserJob $userJob): void
|
||||
{
|
||||
$this->userJob = $userJob;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current object is an instance of the UserGroup class.
|
||||
*
|
||||
|
@@ -12,25 +12,42 @@ declare(strict_types=1);
|
||||
namespace Chill\MainBundle\Export;
|
||||
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Contracts\Translation\TranslatableInterface;
|
||||
|
||||
/**
|
||||
* Interface for Aggregators.
|
||||
*
|
||||
* Aggregators gather result of a query. Most of the time, it will add
|
||||
* a GROUP BY clause.
|
||||
*
|
||||
* @template D of array
|
||||
*/
|
||||
interface AggregatorInterface extends ModifierInterface
|
||||
{
|
||||
/**
|
||||
* Add a form to collect data from the user.
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder);
|
||||
public function buildForm(FormBuilderInterface $builder): void;
|
||||
|
||||
/**
|
||||
* Get the default data, that can be use as "data" for the form.
|
||||
*
|
||||
* @return D
|
||||
*/
|
||||
public function getFormDefaultData(): array;
|
||||
|
||||
/**
|
||||
* @param D $formData
|
||||
*/
|
||||
public function normalizeFormData(array $formData): array;
|
||||
|
||||
/**
|
||||
* @return D
|
||||
*/
|
||||
public function denormalizeFormData(array $formData, int $fromVersion): array;
|
||||
|
||||
public function getNormalizationVersion(): int;
|
||||
|
||||
/**
|
||||
* get a callable which will be able to transform the results into
|
||||
* viewable and understable string.
|
||||
@@ -74,9 +91,9 @@ interface AggregatorInterface extends ModifierInterface
|
||||
* @param string $key The column key, as added in the query
|
||||
* @param mixed[] $values The values from the result. if there are duplicates, those might be given twice. Example: array('FR', 'BE', 'CZ', 'FR', 'BE', 'FR')
|
||||
*
|
||||
* @return \Closure where the first argument is the value, and the function should return the label to show in the formatted file. Example : `function($countryCode) use ($countries) { return $countries[$countryCode]->getName(); }`
|
||||
* @return callable(mixed $value): (string|int|\DateTimeInterface|TranslatableInterface) where the first argument is the value, and the function should return the label to show in the formatted file. Example : `function($countryCode) use ($countries) { return $countries[$countryCode]->getName(); }`
|
||||
*/
|
||||
public function getLabels($key, array $values, mixed $data);
|
||||
public function getLabels(string $key, array $values, mixed $data): callable;
|
||||
|
||||
/**
|
||||
* give the list of keys the current export added to the queryBuilder in
|
||||
@@ -85,7 +102,9 @@ interface AggregatorInterface extends ModifierInterface
|
||||
* Example: if your query builder will contains `SELECT count(id) AS count_id ...`,
|
||||
* this function will return `array('count_id')`.
|
||||
*
|
||||
* @param mixed[] $data the data from the export's form (added by self::buildForm)
|
||||
* @param D $data the data from the export's form (added by self::buildForm)
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getQueryKeys($data);
|
||||
public function getQueryKeys(array $data): array;
|
||||
}
|
||||
|
@@ -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\Export\Cronjob;
|
||||
|
||||
use Chill\MainBundle\Cron\CronJobInterface;
|
||||
use Chill\MainBundle\Entity\CronJobExecution;
|
||||
use Chill\MainBundle\Export\Messenger\RemoveExportGenerationMessage;
|
||||
use Chill\MainBundle\Repository\ExportGenerationRepository;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
|
||||
final readonly class RemoveExpiredExportGenerationCronJob implements CronJobInterface
|
||||
{
|
||||
public const KEY = 'remove-expired-export-generation';
|
||||
|
||||
public function __construct(private ClockInterface $clock, private ExportGenerationRepository $exportGenerationRepository, private MessageBusInterface $messageBus) {}
|
||||
|
||||
public function canRun(?CronJobExecution $cronJobExecution): bool
|
||||
{
|
||||
if (null === $cronJobExecution) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $cronJobExecution->getLastStart()->getTimestamp() < $this->clock->now()->sub(new \DateInterval('PT24H'))->getTimestamp();
|
||||
}
|
||||
|
||||
public function getKey(): string
|
||||
{
|
||||
return self::KEY;
|
||||
}
|
||||
|
||||
public function run(array $lastExecutionData): ?array
|
||||
{
|
||||
$now = $this->clock->now();
|
||||
|
||||
foreach ($this->exportGenerationRepository->findExpiredExportGeneration($now) as $exportGeneration) {
|
||||
$this->messageBus->dispatch(new Envelope(new RemoveExportGenerationMessage($exportGeneration)));
|
||||
}
|
||||
|
||||
return ['last-deletion' => $now->getTimestamp()];
|
||||
}
|
||||
}
|
@@ -28,8 +28,16 @@ interface DirectExportInterface extends ExportElementInterface
|
||||
|
||||
/**
|
||||
* Generate the export.
|
||||
*
|
||||
* @return FormattedExportGeneration
|
||||
*/
|
||||
public function generate(array $acl, array $data = []): Response;
|
||||
public function generate(array $acl, array $data, ExportGenerationContext $context): Response|FormattedExportGeneration;
|
||||
|
||||
public function normalizeFormData(array $formData): array;
|
||||
|
||||
public function denormalizeFormData(array $formData, int $fromVersion): array;
|
||||
|
||||
public function getNormalizationVersion(): int;
|
||||
|
||||
/**
|
||||
* get a description, which will be used in UI (and translated).
|
||||
|
@@ -0,0 +1,14 @@
|
||||
<?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\Exception;
|
||||
|
||||
class ExportGenerationException extends ExportRuntimeException {}
|
@@ -0,0 +1,14 @@
|
||||
<?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\Exception;
|
||||
|
||||
class ExportLogicException extends \LogicException {}
|
@@ -0,0 +1,14 @@
|
||||
<?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\Exception;
|
||||
|
||||
class ExportRuntimeException extends \RuntimeException {}
|
@@ -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\MainBundle\Export\Exception;
|
||||
|
||||
class UnauthorizedGenerationException extends ExportGenerationException
|
||||
{
|
||||
public function __construct(string $message, ?\Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, previous: $previous);
|
||||
}
|
||||
}
|
128
src/Bundle/ChillMainBundle/Export/ExportConfigNormalizer.php
Normal file
128
src/Bundle/ChillMainBundle/Export/ExportConfigNormalizer.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?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;
|
||||
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Entity\Regroupment;
|
||||
use Chill\MainBundle\Form\Type\Export\AggregatorType;
|
||||
use Chill\MainBundle\Form\Type\Export\ExportType;
|
||||
use Chill\MainBundle\Form\Type\Export\FilterType;
|
||||
use Chill\MainBundle\Repository\CenterRepositoryInterface;
|
||||
use Chill\MainBundle\Repository\RegroupmentRepositoryInterface;
|
||||
|
||||
/**
|
||||
* @phpstan-type NormalizedData array{centers: array{centers: list<int>, regroupments: list<int>}, export: array{form: array<string, mixed>, version: int}, filters: array<string, array{enabled: boolean, form: array<string, mixed>, version: int}>, aggregators: array<string, array{enabled: boolean, form: array<string, mixed>, version: int}>, pick_formatter: string, formatter: array{form: array<string, mixed>, version: int}}
|
||||
*/
|
||||
class ExportConfigNormalizer
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ExportManager $exportManager,
|
||||
private readonly CenterRepositoryInterface $centerRepository,
|
||||
private readonly RegroupmentRepositoryInterface $regroupmentRepository,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return NormalizedData
|
||||
*/
|
||||
public function normalizeConfig(string $exportAlias, array $formData): array
|
||||
{
|
||||
$exportData = $formData[ExportType::EXPORT_KEY];
|
||||
$export = $this->exportManager->getExport($exportAlias);
|
||||
|
||||
$serialized = [
|
||||
'export' => [
|
||||
'form' => $export->normalizeFormData($exportData),
|
||||
'version' => $export->getNormalizationVersion(),
|
||||
],
|
||||
];
|
||||
|
||||
$serialized['centers'] = [
|
||||
'centers' => array_values(array_map(static fn (Center $center) => $center->getId(), $formData['centers']['centers'] ?? [])),
|
||||
'regroupments' => array_values(array_map(static fn (Regroupment $group) => $group->getId(), $formData['centers']['regroupments'] ?? [])),
|
||||
];
|
||||
|
||||
$filtersSerialized = [];
|
||||
foreach ($formData[ExportType::FILTER_KEY] as $alias => $filterData) {
|
||||
$filter = $this->exportManager->getFilter($alias);
|
||||
$filtersSerialized[$alias][FilterType::ENABLED_FIELD] = (bool) $filterData[FilterType::ENABLED_FIELD];
|
||||
if ($filterData[FilterType::ENABLED_FIELD]) {
|
||||
$filtersSerialized[$alias]['form'] = $filter->normalizeFormData($filterData['form']);
|
||||
$filtersSerialized[$alias]['version'] = $filter->getNormalizationVersion();
|
||||
}
|
||||
}
|
||||
$serialized['filters'] = $filtersSerialized;
|
||||
|
||||
$aggregatorsSerialized = [];
|
||||
foreach ($formData[ExportType::AGGREGATOR_KEY] as $alias => $aggregatorData) {
|
||||
$aggregator = $this->exportManager->getAggregator($alias);
|
||||
$aggregatorsSerialized[$alias][FilterType::ENABLED_FIELD] = (bool) $aggregatorData[AggregatorType::ENABLED_FIELD];
|
||||
if ($aggregatorData[AggregatorType::ENABLED_FIELD]) {
|
||||
$aggregatorsSerialized[$alias]['form'] = $aggregator->normalizeFormData($aggregatorData['form']);
|
||||
$aggregatorsSerialized[$alias]['version'] = $aggregator->getNormalizationVersion();
|
||||
}
|
||||
}
|
||||
$serialized['aggregators'] = $aggregatorsSerialized;
|
||||
|
||||
$serialized['pick_formatter'] = $formData['pick_formatter'];
|
||||
$formatter = $this->exportManager->getFormatter($formData['pick_formatter']);
|
||||
$serialized['formatter']['form'] = $formatter->normalizeFormData($formData['formatter']);
|
||||
$serialized['formatter']['version'] = $formatter->getNormalizationVersion();
|
||||
|
||||
return $serialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param NormalizedData $serializedData
|
||||
* @param bool $replaceDisabledByDefaultData if true, when a filter is not enabled, the formDefaultData is set
|
||||
*/
|
||||
public function denormalizeConfig(string $exportAlias, array $serializedData, bool $replaceDisabledByDefaultData = false): array
|
||||
{
|
||||
$export = $this->exportManager->getExport($exportAlias);
|
||||
$formater = $this->exportManager->getFormatter($serializedData['pick_formatter']);
|
||||
|
||||
$filtersConfig = [];
|
||||
foreach ($serializedData['filters'] as $alias => $filterData) {
|
||||
$aggregator = $this->exportManager->getFilter($alias);
|
||||
$filtersConfig[$alias]['enabled'] = $filterData['enabled'];
|
||||
|
||||
if ($filterData['enabled']) {
|
||||
$filtersConfig[$alias]['form'] = $aggregator->denormalizeFormData($filterData['form'], $filterData['version']);
|
||||
} elseif ($replaceDisabledByDefaultData) {
|
||||
$filtersConfig[$alias]['form'] = $aggregator->getFormDefaultData();
|
||||
}
|
||||
}
|
||||
|
||||
$aggregatorsConfig = [];
|
||||
foreach ($serializedData['aggregators'] as $alias => $aggregatorData) {
|
||||
$aggregator = $this->exportManager->getAggregator($alias);
|
||||
$aggregatorsConfig[$alias]['enabled'] = $aggregatorData['enabled'];
|
||||
|
||||
if ($aggregatorData['enabled']) {
|
||||
$aggregatorsConfig[$alias]['form'] = $aggregator->denormalizeFormData($aggregatorData['form'], $aggregatorData['version']);
|
||||
} elseif ($replaceDisabledByDefaultData) {
|
||||
$aggregatorsConfig[$alias]['form'] = $aggregator->getFormDefaultData();
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'export' => $export->denormalizeFormData($serializedData['export']['form'], $serializedData['export']['version']),
|
||||
'filters' => $filtersConfig,
|
||||
'aggregators' => $aggregatorsConfig,
|
||||
'pick_formatter' => $serializedData['pick_formatter'],
|
||||
'formatter' => $formater->denormalizeFormData($serializedData['formatter']['form'], $serializedData['formatter']['version']),
|
||||
'centers' => [
|
||||
'centers' => array_values(array_filter(array_map(fn (int $id) => $this->centerRepository->find($id), $serializedData['centers']['centers']), fn ($item) => null !== $item)),
|
||||
'regroupments' => array_values(array_filter(array_map(fn (int $id) => $this->regroupmentRepository->find($id), $serializedData['centers']['regroupments']), fn ($item) => null !== $item)),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
49
src/Bundle/ChillMainBundle/Export/ExportConfigProcessor.php
Normal file
49
src/Bundle/ChillMainBundle/Export/ExportConfigProcessor.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Export;
|
||||
|
||||
class ExportConfigProcessor
|
||||
{
|
||||
public function __construct(private readonly ExportManager $exportManager) {}
|
||||
|
||||
/**
|
||||
* @return iterable<string, AggregatorInterface>
|
||||
*/
|
||||
public function retrieveUsedAggregators(mixed $data): iterable
|
||||
{
|
||||
if (null === $data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ($data as $alias => $aggregatorData) {
|
||||
if ($this->exportManager->hasAggregator($alias) && true === $aggregatorData['enabled']) {
|
||||
yield $alias => $this->exportManager->getAggregator($alias);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, FilterInterface>
|
||||
*/
|
||||
public function retrieveUsedFilters(mixed $data): iterable
|
||||
{
|
||||
if (null === $data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ($data as $alias => $filterData) {
|
||||
if ($this->exportManager->hasFilter($alias) && true === $filterData['enabled']) {
|
||||
yield $alias => $this->exportManager->getFilter($alias);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
189
src/Bundle/ChillMainBundle/Export/ExportDataNormalizerTrait.php
Normal file
189
src/Bundle/ChillMainBundle/Export/ExportDataNormalizerTrait.php
Normal file
@@ -0,0 +1,189 @@
|
||||
<?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;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Repository\UserRepositoryInterface;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
|
||||
/**
|
||||
* Provides utilities for normalizing and denormalizing data entities and dates.
|
||||
*/
|
||||
trait ExportDataNormalizerTrait
|
||||
{
|
||||
/**
|
||||
* Normalizes a Doctrine entity or a collection of entities to extract their identifiers.
|
||||
*
|
||||
* @param object|list<object>|null $entity the entity or collection of entities to normalize
|
||||
*
|
||||
* @return array|int|string Returns the identifier(s) of the entity or entities. If an array of entities is provided,
|
||||
* an array of their identifiers is returned. If a single entity is provided, its identifier
|
||||
* is returned. If null, returns an empty value.
|
||||
*/
|
||||
private function normalizeDoctrineEntity(object|array|null $entity): array|int|string
|
||||
{
|
||||
if (is_array($entity)) {
|
||||
return array_values(array_filter(array_map(static fn (object $entity) => $entity->getId(), $entity), fn ($value) => null !== $value));
|
||||
}
|
||||
if ($entity instanceof Collection) {
|
||||
return $this->normalizeDoctrineEntity($entity->toArray());
|
||||
}
|
||||
|
||||
return $entity?->getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Denormalizes a Doctrine entity by fetching it from the provided repository based on the given ID(s).
|
||||
*
|
||||
* @param list<int>|int|string $id the identifier(s) of the entity to find
|
||||
* @param ObjectRepository $repository the Doctrine repository to query
|
||||
*
|
||||
* @return object|array<object> the found entity or an array of entities if multiple IDs are provided
|
||||
*
|
||||
* @throws \UnexpectedValueException when the entity with the given ID does not exist
|
||||
*/
|
||||
private function denormalizeDoctrineEntity(array|int|string $id, ObjectRepository $repository): object|array
|
||||
{
|
||||
if (is_array($id)) {
|
||||
if ([] === $id) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $repository->findBy(['id' => $id]);
|
||||
}
|
||||
|
||||
if (null === $object = $repository->find($id)) {
|
||||
throw new \UnexpectedValueException(sprintf('Object with id "%s" does not exist.', $id));
|
||||
}
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizer the "user or me" values.
|
||||
*
|
||||
* @param 'me'|User|iterable<'me'|User> $user
|
||||
*
|
||||
* @return int|'me'|list<'me'|int>
|
||||
*/
|
||||
private function normalizeUserOrMe(string|User|iterable $user): int|string|array
|
||||
{
|
||||
if (is_iterable($user)) {
|
||||
$users = [];
|
||||
foreach ($user as $u) {
|
||||
$users[] = $this->normalizeUserOrMe($u);
|
||||
}
|
||||
|
||||
return $users;
|
||||
}
|
||||
|
||||
if ('me' === $user) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
return $user->getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param 'me'|int|iterable<'me'|int> $userId
|
||||
*
|
||||
* @return 'me'|User|array|null
|
||||
*/
|
||||
private function denormalizeUserOrMe(string|int|iterable $userId, UserRepositoryInterface $userRepository): string|User|array|null
|
||||
{
|
||||
if (is_iterable($userId)) {
|
||||
$users = [];
|
||||
foreach ($userId as $id) {
|
||||
$users[] = $this->denormalizeUserOrMe($id, $userRepository);
|
||||
}
|
||||
|
||||
return $users;
|
||||
}
|
||||
|
||||
if ('me' === $userId) {
|
||||
return 'me';
|
||||
}
|
||||
|
||||
return $userRepository->find($userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param 'me'|User|iterable<'me'|User> $user
|
||||
*
|
||||
* @return User|list<User>
|
||||
*/
|
||||
private function userOrMe(string|User|iterable $user, ExportGenerationContext $context): User|array
|
||||
{
|
||||
if (is_iterable($user)) {
|
||||
$users = [];
|
||||
foreach ($user as $u) {
|
||||
$users[] = $this->userOrMe($u, $context);
|
||||
}
|
||||
|
||||
return array_values(
|
||||
array_filter($users, static fn (?User $user) => null !== $user)
|
||||
);
|
||||
}
|
||||
|
||||
if ('me' === $user) {
|
||||
return $context->byUser;
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a provided date into a specific string format.
|
||||
*
|
||||
* @param \DateTimeImmutable|\DateTime $date the date instance to normalize
|
||||
*
|
||||
* @return string a formatted string containing the type and formatted date
|
||||
*/
|
||||
private function normalizeDate(\DateTimeImmutable|\DateTime $date): string
|
||||
{
|
||||
return sprintf(
|
||||
'%s,%s',
|
||||
$date instanceof \DateTimeImmutable ? 'imm1' : 'mut1',
|
||||
$date->format('d-m-Y-H:i:s.u e'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Denormalizes a string back into a DateTime instance.
|
||||
*
|
||||
* The string is expected to contain a kind selector (e.g., 'imm1' or 'mut1')
|
||||
* to determine the type of DateTime object (immutable or mutable) followed by a date format.
|
||||
*
|
||||
* @param string $date the string to be denormalized, containing the kind selector and formatted date
|
||||
*
|
||||
* @return \DateTimeImmutable|\DateTime a DateTime instance created from the given string
|
||||
*
|
||||
* @throws \UnexpectedValueException if the kind selector or date format is invalid
|
||||
*/
|
||||
private function denormalizeDate(string $date): \DateTimeImmutable|\DateTime
|
||||
{
|
||||
$format = 'd-m-Y-H:i:s.u e';
|
||||
|
||||
$denormalized = match (substr($date, 0, 4)) {
|
||||
'imm1' => \DateTimeImmutable::createFromFormat($format, substr($date, 5)),
|
||||
'mut1' => \DateTime::createFromFormat($format, substr($date, 5)),
|
||||
default => throw new \UnexpectedValueException(sprintf('Unexpected format for the kind selector: %s', substr($date, 0, 4))),
|
||||
};
|
||||
|
||||
if (false === $denormalized) {
|
||||
throw new \UnexpectedValueException(sprintf('Unexpected date format: %s', substr($date, 5)));
|
||||
}
|
||||
|
||||
return $denormalized;
|
||||
}
|
||||
}
|
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Export;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Contracts\Translation\TranslatableInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* Give an explanation of an export.
|
||||
*/
|
||||
final readonly class ExportDescriptionHelper
|
||||
{
|
||||
public function __construct(
|
||||
private ExportManager $exportManager,
|
||||
private ExportConfigNormalizer $exportConfigNormalizer,
|
||||
private ExportConfigProcessor $exportConfigProcessor,
|
||||
private TranslatorInterface $translator,
|
||||
private Security $security,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function describe(string $exportAlias, array $exportOptions, bool $includeExportTitle = true): array
|
||||
{
|
||||
$output = [];
|
||||
$denormalized = $this->exportConfigNormalizer->denormalizeConfig($exportAlias, $exportOptions);
|
||||
$user = $this->security->getUser();
|
||||
|
||||
if ($includeExportTitle) {
|
||||
$output[] = $this->trans($this->exportManager->getExport($exportAlias)->getTitle());
|
||||
}
|
||||
|
||||
if (!$user instanceof User) {
|
||||
return $output;
|
||||
}
|
||||
$context = new ExportGenerationContext($user);
|
||||
|
||||
foreach ($this->exportConfigProcessor->retrieveUsedFilters($denormalized['filters']) as $name => $filter) {
|
||||
$output[] = $this->trans($filter->describeAction($denormalized['filters'][$name]['form'], $context));
|
||||
}
|
||||
|
||||
foreach ($this->exportConfigProcessor->retrieveUsedAggregators($denormalized['aggregators']) as $name => $aggregator) {
|
||||
$output[] = $this->trans($aggregator->getTitle());
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
private function trans(string|TranslatableInterface|array $translatable): string
|
||||
{
|
||||
if (is_string($translatable)) {
|
||||
return $this->translator->trans($translatable);
|
||||
}
|
||||
|
||||
if ($translatable instanceof TranslatableInterface) {
|
||||
return $translatable->trans($this->translator);
|
||||
}
|
||||
|
||||
// array case
|
||||
return $this->translator->trans($translatable[0], $translatable[1] ?? []);
|
||||
}
|
||||
}
|
@@ -11,6 +11,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Export;
|
||||
|
||||
use Symfony\Contracts\Translation\TranslatableInterface;
|
||||
|
||||
/**
|
||||
* The common methods between different object used to build export (i.e. : ExportInterface,
|
||||
* FilterInterface, AggregatorInterface).
|
||||
@@ -19,8 +21,6 @@ interface ExportElementInterface
|
||||
{
|
||||
/**
|
||||
* get a title, which will be used in UI (and translated).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle();
|
||||
public function getTitle(): string|TranslatableInterface;
|
||||
}
|
||||
|
@@ -31,5 +31,5 @@ interface ExportElementValidatedInterface
|
||||
* validate the form's data and, if required, build a contraint
|
||||
* violation on the data.
|
||||
*/
|
||||
public function validateForm(mixed $data, ExecutionContextInterface $context);
|
||||
public function validateForm(mixed $data, ExecutionContextInterface $context): void;
|
||||
}
|
||||
|
@@ -21,7 +21,7 @@ namespace Chill\MainBundle\Export;
|
||||
interface ExportElementsProviderInterface
|
||||
{
|
||||
/**
|
||||
* @return ExportElementInterface[]
|
||||
* @return iterable<ExportElementInterface>
|
||||
*/
|
||||
public function getExportElements();
|
||||
public function getExportElements(): iterable;
|
||||
}
|
||||
|
@@ -11,27 +11,28 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Export;
|
||||
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Entity\ExportGeneration;
|
||||
use Chill\MainBundle\Entity\SavedExport;
|
||||
use Chill\MainBundle\Form\Type\Export\ExportType;
|
||||
use Chill\MainBundle\Form\Type\Export\FilterType;
|
||||
use Chill\MainBundle\Form\Type\Export\FormatterType;
|
||||
use Chill\MainBundle\Form\Type\Export\PickCenterType;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Chill\MainBundle\Service\Regroupement\CenterRegroupementResolver;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
|
||||
final readonly class ExportFormHelper
|
||||
{
|
||||
public function __construct(
|
||||
private AuthorizationHelperForCurrentUserInterface $authorizationHelper,
|
||||
private ExportManager $exportManager,
|
||||
private FormFactoryInterface $formFactory,
|
||||
private ExportConfigNormalizer $configNormalizer,
|
||||
private CenterRegroupementResolver $centerRegroupementResolver,
|
||||
) {}
|
||||
|
||||
public function getDefaultData(string $step, DirectExportInterface|ExportInterface $export, array $options = []): array
|
||||
{
|
||||
return match ($step) {
|
||||
'centers', 'generate_centers' => ['centers' => $this->authorizationHelper->getReachableCenters($export->requiredRole())],
|
||||
'centers', 'generate_centers' => ['centers' => $this->authorizationHelper->getReachableCenters($export->requiredRole()), 'regroupments' => []],
|
||||
'export', 'generate_export' => ['export' => $this->getDefaultDataStepExport($export, $options)],
|
||||
'formatter', 'generate_formatter' => ['formatter' => $this->getDefaultDataStepFormatter($options)],
|
||||
default => throw new \LogicException('step not allowed : '.$step),
|
||||
@@ -91,80 +92,68 @@ final readonly class ExportFormHelper
|
||||
}
|
||||
|
||||
public function savedExportDataToFormData(
|
||||
SavedExport $savedExport,
|
||||
ExportGeneration|SavedExport $savedExport,
|
||||
string $step,
|
||||
array $formOptions = [],
|
||||
): array {
|
||||
return match ($step) {
|
||||
'centers', 'generate_centers' => $this->savedExportDataToFormDataStepCenter($savedExport),
|
||||
'export', 'generate_export' => $this->savedExportDataToFormDataStepExport($savedExport, $formOptions),
|
||||
'formatter', 'generate_formatter' => $this->savedExportDataToFormDataStepFormatter($savedExport, $formOptions),
|
||||
'export', 'generate_export' => $this->savedExportDataToFormDataStepExport($savedExport),
|
||||
'formatter', 'generate_formatter' => $this->savedExportDataToFormDataStepFormatter($savedExport),
|
||||
default => throw new \LogicException('this step is not allowed: '.$step),
|
||||
};
|
||||
}
|
||||
|
||||
private function savedExportDataToFormDataStepCenter(
|
||||
SavedExport $savedExport,
|
||||
ExportGeneration|SavedExport $savedExport,
|
||||
): array {
|
||||
$builder = $this->formFactory
|
||||
->createBuilder(
|
||||
FormType::class,
|
||||
[],
|
||||
[
|
||||
'csrf_protection' => false,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add('centers', PickCenterType::class, [
|
||||
'export_alias' => $savedExport->getExportAlias(),
|
||||
]);
|
||||
$form = $builder->getForm();
|
||||
$form->submit($savedExport->getOptions()['centers']);
|
||||
|
||||
return $form->getData();
|
||||
return [
|
||||
'centers' => $this->configNormalizer->denormalizeConfig($savedExport->getExportAlias(), $savedExport->getOptions(), true)['centers'],
|
||||
];
|
||||
}
|
||||
|
||||
private function savedExportDataToFormDataStepExport(
|
||||
SavedExport $savedExport,
|
||||
array $formOptions,
|
||||
ExportGeneration|SavedExport $savedExport,
|
||||
): array {
|
||||
$builder = $this->formFactory
|
||||
->createBuilder(
|
||||
FormType::class,
|
||||
[],
|
||||
[
|
||||
'csrf_protection' => false,
|
||||
]
|
||||
);
|
||||
$data = $this->configNormalizer->denormalizeConfig($savedExport->getExportAlias(), $savedExport->getOptions(), true);
|
||||
|
||||
$builder->add('export', ExportType::class, [
|
||||
'export_alias' => $savedExport->getExportAlias(), ...$formOptions,
|
||||
]);
|
||||
$form = $builder->getForm();
|
||||
$form->submit($savedExport->getOptions()['export']);
|
||||
|
||||
return $form->getData();
|
||||
return [
|
||||
'export' => [
|
||||
'export' => $data['export'],
|
||||
'filters' => $data['filters'],
|
||||
'pick_formatter' => ['alias' => $data['pick_formatter']],
|
||||
'aggregators' => $data['aggregators'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function savedExportDataToFormDataStepFormatter(
|
||||
SavedExport $savedExport,
|
||||
array $formOptions,
|
||||
ExportGeneration|SavedExport $savedExport,
|
||||
): array {
|
||||
$builder = $this->formFactory
|
||||
->createBuilder(
|
||||
FormType::class,
|
||||
[],
|
||||
[
|
||||
'csrf_protection' => false,
|
||||
]
|
||||
);
|
||||
$data = $this->configNormalizer->denormalizeConfig($savedExport->getExportAlias(), $savedExport->getOptions(), true);
|
||||
|
||||
$builder->add('formatter', FormatterType::class, [
|
||||
'export_alias' => $savedExport->getExportAlias(), ...$formOptions,
|
||||
]);
|
||||
$form = $builder->getForm();
|
||||
$form->submit($savedExport->getOptions()['formatter']);
|
||||
return [
|
||||
'formatter' => $data['formatter'],
|
||||
];
|
||||
}
|
||||
|
||||
return $form->getData();
|
||||
/**
|
||||
* Get the Center picked by the user for this export. The data are
|
||||
* extracted from the PickCenterType data.
|
||||
*
|
||||
* @param array $data the data as given by the @see{Chill\MainBundle\Form\Type\Export\PickCenterType}
|
||||
*
|
||||
* @return list<Center>
|
||||
*/
|
||||
public function getPickedCenters(array $data): array
|
||||
{
|
||||
if (!array_key_exists('centers', $data)) {
|
||||
throw new \RuntimeException('array has not the expected shape');
|
||||
}
|
||||
|
||||
$centers = $data['centers'] instanceof Collection ? $data['centers']->toArray() : $data['centers'];
|
||||
$regroupments = ($data['regroupments'] ?? []) instanceof Collection ? $data['regroupments']->toArray() : ($data['regroupments'] ?? []);
|
||||
|
||||
return $this->centerRegroupementResolver->resolveCenters($regroupments, $centers);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,21 @@
|
||||
<?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;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
|
||||
class ExportGenerationContext
|
||||
{
|
||||
public function __construct(
|
||||
public readonly User $byUser,
|
||||
) {}
|
||||
}
|
273
src/Bundle/ChillMainBundle/Export/ExportGenerator.php
Normal file
273
src/Bundle/ChillMainBundle/Export/ExportGenerator.php
Normal file
@@ -0,0 +1,273 @@
|
||||
<?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;
|
||||
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Export\Exception\UnauthorizedGenerationException;
|
||||
use Chill\MainBundle\Form\Type\Export\ExportType;
|
||||
use Chill\MainBundle\Repository\CenterRepositoryInterface;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
|
||||
use Chill\MainBundle\Service\Regroupement\CenterRegroupementResolver;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Generate a single export.
|
||||
*/
|
||||
final readonly class ExportGenerator
|
||||
{
|
||||
private bool $filterStatsByCenters;
|
||||
|
||||
public function __construct(
|
||||
private ExportManager $exportManager,
|
||||
private ExportConfigNormalizer $configNormalizer,
|
||||
private LoggerInterface $logger,
|
||||
private AuthorizationHelperInterface $authorizationHelper,
|
||||
private CenterRegroupementResolver $centerRegroupementResolver,
|
||||
private ExportConfigProcessor $exportConfigProcessor,
|
||||
ParameterBagInterface $parameterBag,
|
||||
private CenterRepositoryInterface $centerRepository,
|
||||
) {
|
||||
$this->filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center'];
|
||||
}
|
||||
|
||||
public function generate(string $exportAlias, array $configuration, ?User $byUser = null): FormattedExportGeneration
|
||||
{
|
||||
$data = $this->configNormalizer->denormalizeConfig($exportAlias, $configuration);
|
||||
$export = $this->exportManager->getExport($exportAlias);
|
||||
|
||||
$centers = $this->filterCenters($byUser, $data['centers']['centers'], $data['centers']['regroupments'], $export);
|
||||
|
||||
$context = new ExportGenerationContext($byUser);
|
||||
|
||||
if ($export instanceof DirectExportInterface) {
|
||||
$generatedExport = $export->generate(
|
||||
$this->buildCenterReachableScopes($centers),
|
||||
$data['export'],
|
||||
$context,
|
||||
);
|
||||
|
||||
if ($generatedExport instanceof Response) {
|
||||
trigger_deprecation('chill-project/chill-bundles', '3.10', 'DirectExportInterface should not return a %s instance, but a %s instance', Response::class, FormattedExportGeneration::class);
|
||||
|
||||
return new FormattedExportGeneration($generatedExport->getContent(), $generatedExport->headers->get('Content-Type'));
|
||||
}
|
||||
|
||||
return $generatedExport;
|
||||
}
|
||||
|
||||
$query = $export->initiateQuery(
|
||||
$this->retrieveUsedModifiers($data),
|
||||
$this->buildCenterReachableScopes($centers),
|
||||
$data['export'],
|
||||
$context,
|
||||
);
|
||||
|
||||
if ($query instanceof \Doctrine\ORM\NativeQuery) {
|
||||
// throw an error if the export require other modifier, which is
|
||||
// not allowed when the export return a `NativeQuery`
|
||||
if (\count($export->supportsModifiers()) > 0) {
|
||||
throw new \LogicException("The export with alias `{$exportAlias}` return ".'a `\Doctrine\ORM\NativeQuery` and supports modifiers, which is not allowed. Either the method `supportsModifiers` should return an empty array, or return a `Doctrine\ORM\QueryBuilder`');
|
||||
}
|
||||
} elseif ($query instanceof QueryBuilder) {
|
||||
// handle filters
|
||||
$this->handleFilters($query, $data[ExportType::FILTER_KEY], $context);
|
||||
|
||||
// handle aggregators
|
||||
$this->handleAggregators($query, $data[ExportType::AGGREGATOR_KEY], $context);
|
||||
|
||||
$this->logger->notice('[export] will execute this qb in export', [
|
||||
'dql' => $query->getDQL(),
|
||||
]);
|
||||
$this->logger->debug('[export] will execute this sql qb in export', [
|
||||
'sql' => $query->getQuery()->getSQL(),
|
||||
]);
|
||||
} else {
|
||||
throw new \UnexpectedValueException('The method `intiateQuery` should return a `\Doctrine\ORM\NativeQuery` or a `Doctrine\ORM\QueryBuilder` object.');
|
||||
}
|
||||
|
||||
$result = $export->getResult($query, $data['export'], $context);
|
||||
|
||||
$formatter = $this->exportManager->getFormatter($data['pick_formatter']);
|
||||
$filtersData = [];
|
||||
$aggregatorsData = [];
|
||||
|
||||
if ($query instanceof QueryBuilder) {
|
||||
foreach ($this->exportConfigProcessor->retrieveUsedAggregators($data[ExportType::AGGREGATOR_KEY]) as $alias => $aggregator) {
|
||||
$aggregatorsData[$alias] = $data[ExportType::AGGREGATOR_KEY][$alias]['form'];
|
||||
}
|
||||
foreach ($this->exportConfigProcessor->retrieveUsedFilters($data[ExportType::FILTER_KEY]) as $alias => $filter) {
|
||||
$filtersData[$alias] = $data[ExportType::FILTER_KEY][$alias]['form'];
|
||||
}
|
||||
}
|
||||
|
||||
/* @phpstan-ignore-next-line the method "generate" is not yet implemented on all formatters */
|
||||
if (method_exists($formatter, 'generate')) {
|
||||
return $formatter->generate(
|
||||
$result,
|
||||
$data['formatter'],
|
||||
$exportAlias,
|
||||
$data['export'],
|
||||
$filtersData,
|
||||
$aggregatorsData,
|
||||
$context,
|
||||
);
|
||||
}
|
||||
|
||||
trigger_deprecation('chill-project/chill-bundles', '3.10', '%s should implements the "generate" method', FormatterInterface::class);
|
||||
|
||||
/* @phpstan-ignore-next-line this is a deprecated method that we must still call */
|
||||
$generatedExport = $formatter->getResponse(
|
||||
$result,
|
||||
$data['formatter'],
|
||||
$exportAlias,
|
||||
$data['export'],
|
||||
$filtersData,
|
||||
$aggregatorsData,
|
||||
$context,
|
||||
);
|
||||
|
||||
return new FormattedExportGeneration($generatedExport->getContent(), $generatedExport->headers->get('content-type'));
|
||||
}
|
||||
|
||||
private function filterCenters(User $byUser, array $centers, array $regroupements, ExportInterface|DirectExportInterface $export): array
|
||||
{
|
||||
if (!$this->filterStatsByCenters) {
|
||||
return $this->centerRepository->findActive();
|
||||
}
|
||||
|
||||
$authorizedCenters = new ArrayCollection($this->authorizationHelper->getReachableCenters($byUser, $export->requiredRole()));
|
||||
if ($authorizedCenters->isEmpty()) {
|
||||
throw new UnauthorizedGenerationException('No authorized centers');
|
||||
}
|
||||
|
||||
$wantedCenters = $this->centerRegroupementResolver->resolveCenters($regroupements, $centers);
|
||||
|
||||
$resolvedCenters = [];
|
||||
foreach ($wantedCenters as $wantedCenter) {
|
||||
if ($authorizedCenters->contains($wantedCenter)) {
|
||||
$resolvedCenters[] = $wantedCenter;
|
||||
}
|
||||
}
|
||||
|
||||
if ([] == $resolvedCenters) {
|
||||
throw new UnauthorizedGenerationException('No common centers between wanted centers and authorized centers');
|
||||
}
|
||||
|
||||
return $resolvedCenters;
|
||||
}
|
||||
|
||||
/**
|
||||
* parse the data to retrieve the used filters and aggregators.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
private function retrieveUsedModifiers(mixed $data): array
|
||||
{
|
||||
if (null === $data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$usedTypes = array_merge(
|
||||
$this->retrieveUsedFiltersType($data[ExportType::FILTER_KEY]),
|
||||
$this->retrieveUsedAggregatorsType($data[ExportType::AGGREGATOR_KEY]),
|
||||
);
|
||||
|
||||
return array_values(array_unique($usedTypes));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the filter used in this export.
|
||||
*
|
||||
* @return list<string> an array with types
|
||||
*/
|
||||
private function retrieveUsedFiltersType(mixed $data): array
|
||||
{
|
||||
if (null === $data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$usedTypes = [];
|
||||
|
||||
foreach ($this->exportConfigProcessor->retrieveUsedFilters($data) as $filter) {
|
||||
if (!\in_array($filter->applyOn(), $usedTypes, true)) {
|
||||
$usedTypes[] = $filter->applyOn();
|
||||
}
|
||||
}
|
||||
|
||||
return $usedTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function retrieveUsedAggregatorsType(mixed $data): array
|
||||
{
|
||||
if (null === $data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$usedTypes = [];
|
||||
|
||||
foreach ($this->exportConfigProcessor->retrieveUsedAggregators($data) as $alias => $aggregator) {
|
||||
if (!\in_array($aggregator->applyOn(), $usedTypes, true)) {
|
||||
$usedTypes[] = $aggregator->applyOn();
|
||||
}
|
||||
}
|
||||
|
||||
return $usedTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alter the query with selected aggregators.
|
||||
*/
|
||||
private function handleAggregators(
|
||||
QueryBuilder $qb,
|
||||
array $data,
|
||||
ExportGenerationContext $context,
|
||||
): void {
|
||||
foreach ($this->exportConfigProcessor->retrieveUsedAggregators($data) as $alias => $aggregator) {
|
||||
$formData = $data[$alias];
|
||||
$aggregator->alterQuery($qb, $formData['form'], $context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* alter the query with selected filters.
|
||||
*/
|
||||
private function handleFilters(
|
||||
QueryBuilder $qb,
|
||||
mixed $data,
|
||||
ExportGenerationContext $context,
|
||||
): void {
|
||||
foreach ($this->exportConfigProcessor->retrieveUsedFilters($data) as $alias => $filter) {
|
||||
$formData = $data[$alias];
|
||||
|
||||
$filter->alterQuery($qb, $formData['form'], $context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* build the array required for defining centers and circles in the initiate
|
||||
* queries of ExportElementsInterfaces.
|
||||
*
|
||||
* @param list<Center> $centers
|
||||
*/
|
||||
private function buildCenterReachableScopes(array $centers)
|
||||
{
|
||||
return array_map(static fn (Center $center) => ['center' => $center, 'circles' => []], $centers);
|
||||
}
|
||||
}
|
@@ -14,6 +14,7 @@ namespace Chill\MainBundle\Export;
|
||||
use Doctrine\ORM\NativeQuery;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Contracts\Translation\TranslatableInterface;
|
||||
|
||||
/**
|
||||
* Interface for Export.
|
||||
@@ -27,6 +28,7 @@ use Symfony\Component\Form\FormBuilderInterface;
|
||||
* @example Chill\PersonBundle\Export\CountPerson an example of implementation
|
||||
*
|
||||
* @template Q of QueryBuilder|NativeQuery
|
||||
* @template D of array
|
||||
*/
|
||||
interface ExportInterface extends ExportElementInterface
|
||||
{
|
||||
@@ -94,12 +96,12 @@ interface ExportInterface extends ExportElementInterface
|
||||
* which do not need to be translated, or value already translated in
|
||||
* database. But the header must be, in every case, translated.
|
||||
*
|
||||
* @param string $key The column key, as added in the query
|
||||
* @param mixed[] $values The values from the result. if there are duplicates, those might be given twice. Example: array('FR', 'BE', 'CZ', 'FR', 'BE', 'FR')
|
||||
* @param string $key The column key, as added in the query
|
||||
* @param list<mixed> $values The values from the result. if there are duplicates, those might be given twice. Example: array('FR', 'BE', 'CZ', 'FR', 'BE', 'FR')
|
||||
*
|
||||
* @return (callable(string|int|float|'_header'|null $value): string|int|\DateTimeInterface) where the first argument is the value, and the function should return the label to show in the formatted file. Example : `function($countryCode) use ($countries) { return $countries[$countryCode]->getName(); }`
|
||||
* @return (callable(string|int|float|'_header'|null $value): string|int|\DateTimeInterface|TranslatableInterface) where the first argument is the value, and the function should return the label to show in the formatted file. Example : `function($countryCode) use ($countries) { return $countries[$countryCode]->getName(); }`
|
||||
*/
|
||||
public function getLabels($key, array $values, mixed $data);
|
||||
public function getLabels(string $key, array $values, mixed $data);
|
||||
|
||||
/**
|
||||
* give the list of keys the current export added to the queryBuilder in
|
||||
@@ -108,29 +110,27 @@ interface ExportInterface extends ExportElementInterface
|
||||
* Example: if your query builder will contains `SELECT count(id) AS count_id ...`,
|
||||
* this function will return `array('count_id')`.
|
||||
*
|
||||
* @param mixed[] $data the data from the export's form (added by self::buildForm)
|
||||
* @param D $data the data from the export's form (added by self::buildForm)
|
||||
*/
|
||||
public function getQueryKeys($data);
|
||||
public function getQueryKeys(array $data): array;
|
||||
|
||||
/**
|
||||
* Return the results of the query builder.
|
||||
*
|
||||
* @param Q $query
|
||||
* @param mixed[] $data the data from the export's fomr (added by self::buildForm)
|
||||
* @param Q $query
|
||||
* @param D $data the data from the export's fomr (added by self::buildForm)
|
||||
*
|
||||
* @return mixed[] an array of results
|
||||
*/
|
||||
public function getResult($query, $data);
|
||||
public function getResult(QueryBuilder|NativeQuery $query, array $data, ExportGenerationContext $context): array;
|
||||
|
||||
/**
|
||||
* Return the Export's type. This will inform _on what_ export will apply.
|
||||
* Most of the type, it will be a string which references an entity.
|
||||
*
|
||||
* Example of types : Chill\PersonBundle\Export\Declarations::PERSON_TYPE
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType();
|
||||
public function getType(): string;
|
||||
|
||||
/**
|
||||
* The initial query, which will be modified by ModifiersInterface
|
||||
@@ -147,7 +147,21 @@ interface ExportInterface extends ExportElementInterface
|
||||
*
|
||||
* @return Q the query to execute
|
||||
*/
|
||||
public function initiateQuery(array $requiredModifiers, array $acl, array $data = []);
|
||||
public function initiateQuery(array $requiredModifiers, array $acl, array $data, ExportGenerationContext $context): QueryBuilder|NativeQuery;
|
||||
|
||||
/**
|
||||
* @param D $formData
|
||||
*/
|
||||
public function normalizeFormData(array $formData): array;
|
||||
|
||||
/**
|
||||
* @param array $formData the normalized data
|
||||
*
|
||||
* @return D
|
||||
*/
|
||||
public function denormalizeFormData(array $formData, int $fromVersion): array;
|
||||
|
||||
public function getNormalizationVersion(): int;
|
||||
|
||||
/**
|
||||
* Return the required Role to execute the Export.
|
||||
|
@@ -11,13 +11,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Export;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Form\Type\Export\ExportType;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
|
||||
@@ -63,14 +59,13 @@ class ExportManager
|
||||
iterable $exports,
|
||||
iterable $aggregators,
|
||||
iterable $filters,
|
||||
// iterable $formatters,
|
||||
iterable $formatters,
|
||||
// iterable $exportElementProvider
|
||||
) {
|
||||
$this->exports = iterator_to_array($exports);
|
||||
$this->aggregators = iterator_to_array($aggregators);
|
||||
$this->filters = iterator_to_array($filters);
|
||||
// NOTE: PHP crashes on the next line (exit error code 11). This is desactivated until further investigation
|
||||
// $this->formatters = iterator_to_array($formatters);
|
||||
$this->formatters = iterator_to_array($formatters);
|
||||
|
||||
// foreach ($exportElementProvider as $prefix => $provider) {
|
||||
// $this->addExportElementsProvider($provider, $prefix);
|
||||
@@ -102,7 +97,7 @@ class ExportManager
|
||||
\in_array($filter->applyOn(), $export->supportsModifiers(), true)
|
||||
&& $this->isGrantedForElement($filter, $export, $centers)
|
||||
) {
|
||||
$filters[$alias] = $filter;
|
||||
$filters[$alias] = $this->getFilter($alias);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,125 +131,32 @@ class ExportManager
|
||||
return $aggregators;
|
||||
}
|
||||
|
||||
public function addExportElementsProvider(ExportElementsProviderInterface $provider, string $prefix): void
|
||||
{
|
||||
foreach ($provider->getExportElements() as $suffix => $element) {
|
||||
$alias = $prefix.'_'.$suffix;
|
||||
|
||||
if ($element instanceof ExportInterface) {
|
||||
$this->exports[$alias] = $element;
|
||||
} elseif ($element instanceof FilterInterface) {
|
||||
$this->filters[$alias] = $element;
|
||||
} elseif ($element instanceof AggregatorInterface) {
|
||||
$this->aggregators[$alias] = $element;
|
||||
} elseif ($element instanceof FormatterInterface) {
|
||||
$this->addFormatter($element, $alias);
|
||||
} else {
|
||||
throw new \LogicException('This element '.$element::class.' is not an instance of export element');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* add a formatter.
|
||||
*
|
||||
* @internal used by DI
|
||||
*/
|
||||
public function addFormatter(FormatterInterface $formatter, string $alias): void
|
||||
public function addFormatter(FormatterInterface $formatter, string $alias)
|
||||
{
|
||||
$this->formatters[$alias] = $formatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a response which contains the requested data.
|
||||
*/
|
||||
public function generate(string $exportAlias, array $pickedCentersData, array $data, array $formatterData): Response
|
||||
{
|
||||
$export = $this->getExport($exportAlias);
|
||||
$centers = $this->getPickedCenters($pickedCentersData);
|
||||
|
||||
if ($export instanceof DirectExportInterface) {
|
||||
return $export->generate(
|
||||
$this->buildCenterReachableScopes($centers, $export),
|
||||
$data[ExportType::EXPORT_KEY]
|
||||
);
|
||||
}
|
||||
|
||||
$query = $export->initiateQuery(
|
||||
$this->retrieveUsedModifiers($data),
|
||||
$this->buildCenterReachableScopes($centers, $export),
|
||||
$data[ExportType::EXPORT_KEY]
|
||||
);
|
||||
|
||||
if ($query instanceof \Doctrine\ORM\NativeQuery) {
|
||||
// throw an error if the export require other modifier, which is
|
||||
// not allowed when the export return a `NativeQuery`
|
||||
if (\count($export->supportsModifiers()) > 0) {
|
||||
throw new \LogicException("The export with alias `{$exportAlias}` return ".'a `\Doctrine\ORM\NativeQuery` and supports modifiers, which is not allowed. Either the method `supportsModifiers` should return an empty array, or return a `Doctrine\ORM\QueryBuilder`');
|
||||
}
|
||||
} elseif ($query instanceof QueryBuilder) {
|
||||
// handle filters
|
||||
$this->handleFilters($export, $query, $data[ExportType::FILTER_KEY], $centers);
|
||||
|
||||
// handle aggregators
|
||||
$this->handleAggregators($export, $query, $data[ExportType::AGGREGATOR_KEY], $centers);
|
||||
|
||||
$this->logger->notice('[export] will execute this qb in export', [
|
||||
'dql' => $query->getDQL(),
|
||||
]);
|
||||
} else {
|
||||
throw new \UnexpectedValueException('The method `intiateQuery` should return a `\Doctrine\ORM\NativeQuery` or a `Doctrine\ORM\QueryBuilder` object.');
|
||||
}
|
||||
|
||||
$result = $export->getResult($query, $data[ExportType::EXPORT_KEY]);
|
||||
|
||||
if (!is_iterable($result)) {
|
||||
throw new \UnexpectedValueException(sprintf('The result of the export should be an iterable, %s given', \gettype($result)));
|
||||
}
|
||||
|
||||
/** @var FormatterInterface $formatter */
|
||||
$formatter = $this->getFormatter($this->getFormatterAlias($data));
|
||||
$filtersData = [];
|
||||
$aggregatorsData = [];
|
||||
|
||||
if ($query instanceof QueryBuilder) {
|
||||
$aggregators = $this->retrieveUsedAggregators($data[ExportType::AGGREGATOR_KEY]);
|
||||
|
||||
foreach ($aggregators as $alias => $aggregator) {
|
||||
$aggregatorsData[$alias] = $data[ExportType::AGGREGATOR_KEY][$alias]['form'];
|
||||
}
|
||||
}
|
||||
|
||||
$filters = $this->retrieveUsedFilters($data[ExportType::FILTER_KEY]);
|
||||
|
||||
foreach ($filters as $alias => $filter) {
|
||||
$filtersData[$alias] = $data[ExportType::FILTER_KEY][$alias]['form'];
|
||||
}
|
||||
|
||||
return $formatter->getResponse(
|
||||
$result,
|
||||
$formatterData,
|
||||
$exportAlias,
|
||||
$data[ExportType::EXPORT_KEY],
|
||||
$filtersData,
|
||||
$aggregatorsData
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $alias
|
||||
*
|
||||
* @return AggregatorInterface
|
||||
*
|
||||
* @throws \RuntimeException if the aggregator is not known
|
||||
*/
|
||||
public function getAggregator($alias)
|
||||
public function getAggregator($alias): AggregatorInterface
|
||||
{
|
||||
if (!\array_key_exists($alias, $this->aggregators)) {
|
||||
if (null === $aggregator = $this->aggregators[$alias] ?? null) {
|
||||
throw new \RuntimeException("The aggregator with alias {$alias} is not known.");
|
||||
}
|
||||
|
||||
return $this->aggregators[$alias];
|
||||
if ($aggregator instanceof ExportManagerAwareInterface) {
|
||||
$aggregator->setExportManager($this);
|
||||
}
|
||||
|
||||
return $aggregator;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -313,10 +215,10 @@ class ExportManager
|
||||
foreach ($this->exports as $alias => $export) {
|
||||
if ($whereUserIsGranted) {
|
||||
if ($this->isGrantedForElement($export, null, null)) {
|
||||
yield $alias => $export;
|
||||
yield $alias => $this->getExport($alias);
|
||||
}
|
||||
} else {
|
||||
yield $alias => $export;
|
||||
yield $alias => $this->getExport($alias);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -332,9 +234,9 @@ class ExportManager
|
||||
|
||||
foreach ($this->getExports($whereUserIsGranted) as $alias => $export) {
|
||||
if ($export instanceof GroupedExportInterface) {
|
||||
$groups[$export->getGroup()][$alias] = $export;
|
||||
$groups[$export->getGroup()][$alias] = $this->getExport($alias);
|
||||
} else {
|
||||
$groups['_'][$alias] = $export;
|
||||
$groups['_'][$alias] = $this->getExport($alias);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,11 +248,25 @@ class ExportManager
|
||||
*/
|
||||
public function getFilter(string $alias): FilterInterface
|
||||
{
|
||||
if (!\array_key_exists($alias, $this->filters)) {
|
||||
if (null === $filter = $this->filters[$alias] ?? null) {
|
||||
throw new \RuntimeException("The filter with alias {$alias} is not known.");
|
||||
}
|
||||
|
||||
return $this->filters[$alias];
|
||||
if ($filter instanceof ExportManagerAwareInterface) {
|
||||
$filter->setExportManager($this);
|
||||
}
|
||||
|
||||
return $filter;
|
||||
}
|
||||
|
||||
public function hasFilter(string $alias): bool
|
||||
{
|
||||
return array_key_exists($alias, $this->filters);
|
||||
}
|
||||
|
||||
public function hasAggregator(string $alias): bool
|
||||
{
|
||||
return array_key_exists($alias, $this->aggregators);
|
||||
}
|
||||
|
||||
public function getAllFilters(): array
|
||||
@@ -358,7 +274,7 @@ class ExportManager
|
||||
$filters = [];
|
||||
|
||||
foreach ($this->filters as $alias => $filter) {
|
||||
$filters[$alias] = $filter;
|
||||
$filters[$alias] = $this->getFilter($alias);
|
||||
}
|
||||
|
||||
return $filters;
|
||||
@@ -380,11 +296,15 @@ class ExportManager
|
||||
|
||||
public function getFormatter(string $alias): FormatterInterface
|
||||
{
|
||||
if (!\array_key_exists($alias, $this->formatters)) {
|
||||
if (null === $formatter = $this->formatters[$alias] ?? null) {
|
||||
throw new \RuntimeException("The formatter with alias {$alias} is not known.");
|
||||
}
|
||||
|
||||
return $this->formatters[$alias];
|
||||
if ($formatter instanceof ExportManagerAwareInterface) {
|
||||
$formatter->setExportManager($this);
|
||||
}
|
||||
|
||||
return $formatter;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -412,26 +332,13 @@ class ExportManager
|
||||
{
|
||||
foreach ($this->formatters as $alias => $formatter) {
|
||||
if (\in_array($formatter->getType(), $types, true)) {
|
||||
yield $alias => $formatter;
|
||||
yield $alias => $this->getFormatter($alias);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Center picked by the user for this export. The data are
|
||||
* extracted from the PickCenterType data.
|
||||
*
|
||||
* @param array $data the data from a PickCenterType
|
||||
*
|
||||
* @return \Chill\MainBundle\Entity\Center[] the picked center
|
||||
*/
|
||||
public function getPickedCenters(array $data): array
|
||||
{
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* get the aggregators typse used in the form export data.
|
||||
* get the aggregators types used in the form export data.
|
||||
*
|
||||
* @param array $data the data from the export form
|
||||
*
|
||||
@@ -439,9 +346,15 @@ class ExportManager
|
||||
*/
|
||||
public function getUsedAggregatorsAliases(array $data): array
|
||||
{
|
||||
$aggregators = $this->retrieveUsedAggregators($data[ExportType::AGGREGATOR_KEY]);
|
||||
$keys = [];
|
||||
|
||||
return array_keys(iterator_to_array($aggregators));
|
||||
foreach ($data as $alias => $aggregatorData) {
|
||||
if (true === $aggregatorData['enabled']) {
|
||||
$keys[] = $alias;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique($keys));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -490,190 +403,4 @@ class ExportManager
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* build the array required for defining centers and circles in the initiate
|
||||
* queries of ExportElementsInterfaces.
|
||||
*
|
||||
* @param \Chill\MainBundle\Entity\Center[] $centers
|
||||
*/
|
||||
private function buildCenterReachableScopes(array $centers, ExportElementInterface $element)
|
||||
{
|
||||
$r = [];
|
||||
|
||||
$user = $this->tokenStorage->getToken()->getUser();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ($centers as $center) {
|
||||
$r[] = [
|
||||
'center' => $center,
|
||||
'circles' => $this->authorizationHelper->getReachableScopes(
|
||||
$user,
|
||||
$element->requiredRole(),
|
||||
$center
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
return $r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alter the query with selected aggregators.
|
||||
*
|
||||
* Check for acl. If an user is not authorized to see an aggregator, throw an
|
||||
* UnauthorizedException.
|
||||
*
|
||||
* @throw UnauthorizedHttpException if the user is not authorized
|
||||
*/
|
||||
private function handleAggregators(
|
||||
ExportInterface $export,
|
||||
QueryBuilder $qb,
|
||||
array $data,
|
||||
array $center,
|
||||
): void {
|
||||
$aggregators = $this->retrieveUsedAggregators($data);
|
||||
|
||||
foreach ($aggregators as $alias => $aggregator) {
|
||||
if (false === $this->isGrantedForElement($aggregator, $export, $center)) {
|
||||
throw new UnauthorizedHttpException('You are not authorized to use the aggregator'.$aggregator->getTitle());
|
||||
}
|
||||
|
||||
$formData = $data[$alias];
|
||||
$aggregator->alterQuery($qb, $formData['form']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* alter the query with selected filters.
|
||||
*
|
||||
* This function check the acl.
|
||||
*
|
||||
* @param \Chill\MainBundle\Entity\Center[] $centers the picked centers
|
||||
*
|
||||
* @throw UnauthorizedHttpException if the user is not authorized
|
||||
*/
|
||||
private function handleFilters(
|
||||
ExportInterface $export,
|
||||
QueryBuilder $qb,
|
||||
mixed $data,
|
||||
array $centers,
|
||||
): void {
|
||||
$filters = $this->retrieveUsedFilters($data);
|
||||
|
||||
foreach ($filters as $alias => $filter) {
|
||||
if (false === $this->isGrantedForElement($filter, $export, $centers)) {
|
||||
throw new UnauthorizedHttpException('You are not authorized to use the filter '.$filter->getTitle());
|
||||
}
|
||||
|
||||
$formData = $data[$alias];
|
||||
|
||||
$this->logger->debug('alter query by filter '.$alias, [
|
||||
'class' => self::class, 'function' => __FUNCTION__,
|
||||
]);
|
||||
$filter->alterQuery($qb, $formData['form']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, AggregatorInterface>
|
||||
*/
|
||||
private function retrieveUsedAggregators(mixed $data): iterable
|
||||
{
|
||||
if (null === $data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ($data as $alias => $aggregatorData) {
|
||||
if (true === $aggregatorData['enabled']) {
|
||||
yield $alias => $this->getAggregator($alias);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function retrieveUsedAggregatorsType(mixed $data)
|
||||
{
|
||||
if (null === $data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$usedTypes = [];
|
||||
|
||||
foreach ($this->retrieveUsedAggregators($data) as $alias => $aggregator) {
|
||||
if (!\in_array($aggregator->applyOn(), $usedTypes, true)) {
|
||||
$usedTypes[] = $aggregator->applyOn();
|
||||
}
|
||||
}
|
||||
|
||||
return $usedTypes;
|
||||
}
|
||||
|
||||
private function retrieveUsedFilters(mixed $data): iterable
|
||||
{
|
||||
if (null === $data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ($data as $alias => $filterData) {
|
||||
if (true === $filterData['enabled']) {
|
||||
yield $alias => $this->getFilter($alias);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the filter used in this export.
|
||||
*
|
||||
* @return array an array with types
|
||||
*/
|
||||
private function retrieveUsedFiltersType(mixed $data): iterable
|
||||
{
|
||||
if (null === $data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$usedTypes = [];
|
||||
|
||||
foreach ($data as $alias => $filterData) {
|
||||
if (true === $filterData['enabled']) {
|
||||
$filter = $this->getFilter($alias);
|
||||
|
||||
if (!\in_array($filter->applyOn(), $usedTypes, true)) {
|
||||
$usedTypes[] = $filter->applyOn();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $usedTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* parse the data to retrieve the used filters and aggregators.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
private function retrieveUsedModifiers(mixed $data): array
|
||||
{
|
||||
if (null === $data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$usedTypes = array_merge(
|
||||
$this->retrieveUsedFiltersType($data[ExportType::FILTER_KEY]),
|
||||
$this->retrieveUsedAggregatorsType($data[ExportType::AGGREGATOR_KEY])
|
||||
);
|
||||
|
||||
$this->logger->debug(
|
||||
'Required types are '.implode(', ', $usedTypes),
|
||||
['class' => self::class, 'function' => __FUNCTION__]
|
||||
);
|
||||
|
||||
return array_unique($usedTypes);
|
||||
}
|
||||
}
|
||||
|
@@ -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\MainBundle\Export;
|
||||
|
||||
/**
|
||||
* Interface which is aware of the export manager.
|
||||
*/
|
||||
interface ExportManagerAwareInterface
|
||||
{
|
||||
public function setExportManager(ExportManager $exportManager): void;
|
||||
}
|
@@ -12,6 +12,7 @@ declare(strict_types=1);
|
||||
namespace Chill\MainBundle\Export;
|
||||
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Contracts\Translation\TranslatableInterface;
|
||||
|
||||
/**
|
||||
* Interface for filters.
|
||||
@@ -20,6 +21,8 @@ use Symfony\Component\Form\FormBuilderInterface;
|
||||
* it will add a `WHERE` clause on this query.
|
||||
*
|
||||
* Filters should not add column in `SELECT` clause.
|
||||
*
|
||||
* @template D of array
|
||||
*/
|
||||
interface FilterInterface extends ModifierInterface
|
||||
{
|
||||
@@ -28,16 +31,30 @@ interface FilterInterface extends ModifierInterface
|
||||
/**
|
||||
* Add a form to collect data from the user.
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder);
|
||||
public function buildForm(FormBuilderInterface $builder): void;
|
||||
|
||||
/**
|
||||
* Get the default data, that can be use as "data" for the form.
|
||||
*
|
||||
* In case of adding new parameters to a filter, you can implement a @see{DataTransformerFilterInterface} to
|
||||
* transforme the filters's data saved in an export to the desired state.
|
||||
*
|
||||
* @return D
|
||||
*/
|
||||
public function getFormDefaultData(): array;
|
||||
|
||||
/**
|
||||
* @param D $formData
|
||||
*/
|
||||
public function normalizeFormData(array $formData): array;
|
||||
|
||||
/**
|
||||
* @return D
|
||||
*/
|
||||
public function denormalizeFormData(array $formData, int $fromVersion): array;
|
||||
|
||||
public function getNormalizationVersion(): int;
|
||||
|
||||
/**
|
||||
* Describe the filtering action.
|
||||
*
|
||||
@@ -52,7 +69,7 @@ interface FilterInterface extends ModifierInterface
|
||||
* supported, later some 'html' will be added. The filter should always
|
||||
* implement the 'string' format and fallback to it if other format are used.
|
||||
*
|
||||
* If no i18n is necessery, or if the filter translate the string by himself,
|
||||
* If no i18n is necessary, or if the filter translate the string by himself,
|
||||
* this function should return a string. If the filter does not do any translation,
|
||||
* the return parameter should be an array, where
|
||||
*
|
||||
@@ -63,10 +80,9 @@ interface FilterInterface extends ModifierInterface
|
||||
*
|
||||
* Example: `array('my string with %parameter%', ['%parameter%' => 'good news'], 'mydomain', 'mylocale')`
|
||||
*
|
||||
* @param array $data
|
||||
* @param string $format the format
|
||||
* @param D $data
|
||||
*
|
||||
* @return array|string a string with the data or, if translatable, an array where first element is string, second elements is an array of arguments
|
||||
* @return array|string|TranslatableInterface a string with the data or, if translatable, an array where first element is string, second elements is an array of arguments
|
||||
*/
|
||||
public function describeAction($data, $format = 'string');
|
||||
public function describeAction(array $data, ExportGenerationContext $context): array|string|TranslatableInterface;
|
||||
}
|
||||
|
@@ -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\MainBundle\Export;
|
||||
|
||||
final readonly class FormattedExportGeneration
|
||||
{
|
||||
public function __construct(
|
||||
public string $content,
|
||||
public string $contentType,
|
||||
) {}
|
||||
}
|
@@ -1,224 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Export\Formatter;
|
||||
|
||||
use Chill\MainBundle\Export\ExportManager;
|
||||
use Chill\MainBundle\Export\FormatterInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use function count;
|
||||
|
||||
// command to get the report with curl : curl --user "center a_social:password" "http://localhost:8000/fr/exports/generate/count_person?export[filters][person_gender_filter][enabled]=&export[filters][person_nationality_filter][enabled]=&export[filters][person_nationality_filter][form][nationalities]=&export[aggregators][person_nationality_aggregator][order]=1&export[aggregators][person_nationality_aggregator][form][group_by_level]=country&export[submit]=&export[_token]=RHpjHl389GrK-bd6iY5NsEqrD5UKOTHH40QKE9J1edU" --globoff
|
||||
|
||||
/**
|
||||
* Create a CSV List for the export.
|
||||
*/
|
||||
class CSVListFormatter implements FormatterInterface
|
||||
{
|
||||
protected $exportAlias;
|
||||
|
||||
protected $exportData;
|
||||
|
||||
/**
|
||||
* @var ExportManager
|
||||
*/
|
||||
protected $exportManager;
|
||||
|
||||
protected $formatterData;
|
||||
|
||||
/**
|
||||
* This variable cache the labels internally.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $labelsCache;
|
||||
|
||||
protected $result;
|
||||
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
protected $translator;
|
||||
|
||||
public function __construct(TranslatorInterface $translatorInterface, ExportManager $exportManager)
|
||||
{
|
||||
$this->translator = $translatorInterface;
|
||||
$this->exportManager = $exportManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* build a form, which will be used to collect data required for the execution
|
||||
* of this formatter.
|
||||
*
|
||||
* @uses appendAggregatorForm
|
||||
*
|
||||
* @param type $exportAlias
|
||||
*/
|
||||
public function buildForm(
|
||||
FormBuilderInterface $builder,
|
||||
$exportAlias,
|
||||
array $aggregatorAliases,
|
||||
): void {
|
||||
$builder->add('numerotation', ChoiceType::class, [
|
||||
'choices' => [
|
||||
'yes' => true,
|
||||
'no' => false,
|
||||
],
|
||||
'expanded' => true,
|
||||
'multiple' => false,
|
||||
'label' => 'Add a number on first column',
|
||||
]);
|
||||
}
|
||||
|
||||
public function getFormDefaultData(array $aggregatorAliases): array
|
||||
{
|
||||
return ['numerotation' => true];
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
return 'CSV vertical list';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a response from the data collected on differents ExportElementInterface.
|
||||
*
|
||||
* @param mixed[] $result The result, as given by the ExportInterface
|
||||
* @param mixed[] $formatterData collected from the current form
|
||||
* @param string $exportAlias the id of the current export
|
||||
* @param array $filtersData an array containing the filters data. The key are the filters id, and the value are the data
|
||||
* @param array $aggregatorsData an array containing the aggregators data. The key are the filters id, and the value are the data
|
||||
*
|
||||
* @return Response The response to be shown
|
||||
*/
|
||||
public function getResponse(
|
||||
$result,
|
||||
$formatterData,
|
||||
$exportAlias,
|
||||
array $exportData,
|
||||
array $filtersData,
|
||||
array $aggregatorsData,
|
||||
): \Symfony\Component\HttpFoundation\Response {
|
||||
$this->result = $result;
|
||||
$this->exportAlias = $exportAlias;
|
||||
$this->exportData = $exportData;
|
||||
$this->formatterData = $formatterData;
|
||||
|
||||
$output = fopen('php://output', 'wb');
|
||||
|
||||
$this->prepareHeaders($output);
|
||||
|
||||
$i = 1;
|
||||
|
||||
foreach ($result as $row) {
|
||||
$line = [];
|
||||
|
||||
if (true === $this->formatterData['numerotation']) {
|
||||
$line[] = $i;
|
||||
}
|
||||
|
||||
foreach ($row as $key => $value) {
|
||||
$line[] = $this->getLabel($key, $value);
|
||||
}
|
||||
|
||||
fputcsv($output, $line);
|
||||
|
||||
++$i;
|
||||
}
|
||||
|
||||
$csvContent = stream_get_contents($output);
|
||||
fclose($output);
|
||||
|
||||
$response = new Response();
|
||||
$response->setStatusCode(200);
|
||||
$response->headers->set('Content-Type', 'text/csv; charset=utf-8');
|
||||
// $response->headers->set('Content-Disposition','attachment; filename="export.csv"');
|
||||
|
||||
$response->setContent($csvContent);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function getType()
|
||||
{
|
||||
return FormatterInterface::TYPE_LIST;
|
||||
}
|
||||
|
||||
/**
|
||||
* Give the label corresponding to the given key and value.
|
||||
*
|
||||
* @param string $key
|
||||
* @param string $value
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @throws \LogicException if the label is not found
|
||||
*/
|
||||
protected function getLabel($key, $value)
|
||||
{
|
||||
if (null === $this->labelsCache) {
|
||||
$this->prepareCacheLabels();
|
||||
}
|
||||
|
||||
if (!\array_key_exists($key, $this->labelsCache)) {
|
||||
throw new \OutOfBoundsException(sprintf('The key "%s" is not present in the list of keys handled by this query. Check your `getKeys` and `getLabels` methods. Available keys are %s.', $key, \implode(', ', \array_keys($this->labelsCache))));
|
||||
}
|
||||
|
||||
return $this->labelsCache[$key]($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the label cache which will be used by getLabel. This function
|
||||
* should be called only once in the generation lifecycle.
|
||||
*/
|
||||
protected function prepareCacheLabels()
|
||||
{
|
||||
$export = $this->exportManager->getExport($this->exportAlias);
|
||||
$keys = $export->getQueryKeys($this->exportData);
|
||||
|
||||
foreach ($keys as $key) {
|
||||
// get an array with all values for this key if possible
|
||||
$values = \array_map(static fn ($v) => $v[$key], $this->result);
|
||||
// store the label in the labelsCache property
|
||||
$this->labelsCache[$key] = $export->getLabels($key, $values, $this->exportData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* add the headers to the csv file.
|
||||
*
|
||||
* @param resource $output
|
||||
*/
|
||||
protected function prepareHeaders($output)
|
||||
{
|
||||
$keys = $this->exportManager->getExport($this->exportAlias)->getQueryKeys($this->exportData);
|
||||
// we want to keep the order of the first row. So we will iterate on the first row of the results
|
||||
$first_row = \count($this->result) > 0 ? $this->result[0] : [];
|
||||
$header_line = [];
|
||||
|
||||
if (true === $this->formatterData['numerotation']) {
|
||||
$header_line[] = $this->translator->trans('Number');
|
||||
}
|
||||
|
||||
foreach ($first_row as $key => $value) {
|
||||
$header_line[] = $this->translator->trans(
|
||||
$this->getLabel($key, '_header')
|
||||
);
|
||||
}
|
||||
|
||||
if (\count($header_line) > 0) {
|
||||
fputcsv($output, $header_line);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,217 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Export\Formatter;
|
||||
|
||||
use Chill\MainBundle\Export\ExportManager;
|
||||
use Chill\MainBundle\Export\FormatterInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* Create a CSV List for the export where the header are printed on the
|
||||
* first column, and the result goes from left to right.
|
||||
*/
|
||||
class CSVPivotedListFormatter implements FormatterInterface
|
||||
{
|
||||
protected $exportAlias;
|
||||
|
||||
protected $exportData;
|
||||
|
||||
/**
|
||||
* @var ExportManager
|
||||
*/
|
||||
protected $exportManager;
|
||||
|
||||
protected $formatterData;
|
||||
|
||||
/**
|
||||
* This variable cache the labels internally.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $labelsCache;
|
||||
|
||||
protected $result;
|
||||
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
protected $translator;
|
||||
|
||||
public function __construct(TranslatorInterface $translatorInterface, ExportManager $exportManager)
|
||||
{
|
||||
$this->translator = $translatorInterface;
|
||||
$this->exportManager = $exportManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* build a form, which will be used to collect data required for the execution
|
||||
* of this formatter.
|
||||
*
|
||||
* @uses appendAggregatorForm
|
||||
*
|
||||
* @param type $exportAlias
|
||||
*/
|
||||
public function buildForm(
|
||||
FormBuilderInterface $builder,
|
||||
$exportAlias,
|
||||
array $aggregatorAliases,
|
||||
): void {
|
||||
$builder->add('numerotation', ChoiceType::class, [
|
||||
'choices' => [
|
||||
'yes' => true,
|
||||
'no' => false,
|
||||
],
|
||||
'expanded' => true,
|
||||
'multiple' => false,
|
||||
'label' => 'Add a number on first column',
|
||||
'data' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getFormDefaultData(array $aggregatorAliases): array
|
||||
{
|
||||
return ['numerotation' => true];
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
return 'CSV horizontal list';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a response from the data collected on differents ExportElementInterface.
|
||||
*
|
||||
* @param mixed[] $result The result, as given by the ExportInterface
|
||||
* @param mixed[] $formatterData collected from the current form
|
||||
* @param string $exportAlias the id of the current export
|
||||
* @param array $filtersData an array containing the filters data. The key are the filters id, and the value are the data
|
||||
* @param array $aggregatorsData an array containing the aggregators data. The key are the filters id, and the value are the data
|
||||
*
|
||||
* @return Response The response to be shown
|
||||
*/
|
||||
public function getResponse(
|
||||
$result,
|
||||
$formatterData,
|
||||
$exportAlias,
|
||||
array $exportData,
|
||||
array $filtersData,
|
||||
array $aggregatorsData,
|
||||
): \Symfony\Component\HttpFoundation\Response {
|
||||
$this->result = $result;
|
||||
$this->exportAlias = $exportAlias;
|
||||
$this->exportData = $exportData;
|
||||
$this->formatterData = $formatterData;
|
||||
|
||||
$output = fopen('php://output', 'wb');
|
||||
|
||||
$i = 1;
|
||||
$lines = [];
|
||||
$this->prepareHeaders($lines);
|
||||
|
||||
foreach ($result as $row) {
|
||||
$j = 0;
|
||||
|
||||
if (true === $this->formatterData['numerotation']) {
|
||||
$lines[$j][] = $i;
|
||||
++$j;
|
||||
}
|
||||
|
||||
foreach ($row as $key => $value) {
|
||||
$lines[$j][] = $this->getLabel($key, $value);
|
||||
++$j;
|
||||
}
|
||||
++$i;
|
||||
}
|
||||
|
||||
// adding the lines to the csv output
|
||||
foreach ($lines as $line) {
|
||||
fputcsv($output, $line);
|
||||
}
|
||||
|
||||
$csvContent = stream_get_contents($output);
|
||||
fclose($output);
|
||||
|
||||
$response = new Response();
|
||||
$response->setStatusCode(200);
|
||||
$response->headers->set('Content-Type', 'text/csv; charset=utf-8');
|
||||
$response->headers->set('Content-Disposition', 'attachment; filename="export.csv"');
|
||||
|
||||
$response->setContent($csvContent);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function getType()
|
||||
{
|
||||
return FormatterInterface::TYPE_LIST;
|
||||
}
|
||||
|
||||
/**
|
||||
* Give the label corresponding to the given key and value.
|
||||
*
|
||||
* @param string $key
|
||||
* @param string $value
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @throws \LogicException if the label is not found
|
||||
*/
|
||||
protected function getLabel($key, $value)
|
||||
{
|
||||
if (null === $this->labelsCache) {
|
||||
$this->prepareCacheLabels();
|
||||
}
|
||||
|
||||
return $this->labelsCache[$key]($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the label cache which will be used by getLabel. This function
|
||||
* should be called only once in the generation lifecycle.
|
||||
*/
|
||||
protected function prepareCacheLabels()
|
||||
{
|
||||
$export = $this->exportManager->getExport($this->exportAlias);
|
||||
$keys = $export->getQueryKeys($this->exportData);
|
||||
|
||||
foreach ($keys as $key) {
|
||||
// get an array with all values for this key if possible
|
||||
$values = \array_map(static fn ($v) => $v[$key], $this->result);
|
||||
// store the label in the labelsCache property
|
||||
$this->labelsCache[$key] = $export->getLabels($key, $values, $this->exportData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* add the headers to lines array.
|
||||
*
|
||||
* @param array $lines the lines where the header will be added
|
||||
*/
|
||||
protected function prepareHeaders(array &$lines)
|
||||
{
|
||||
$keys = $this->exportManager->getExport($this->exportAlias)->getQueryKeys($this->exportData);
|
||||
// we want to keep the order of the first row. So we will iterate on the first row of the results
|
||||
$first_row = \count($this->result) > 0 ? $this->result[0] : [];
|
||||
$header_line = [];
|
||||
|
||||
if (true === $this->formatterData['numerotation']) {
|
||||
$lines[] = [$this->translator->trans('Number')];
|
||||
}
|
||||
|
||||
foreach ($first_row as $key => $value) {
|
||||
$lines[] = [$this->getLabel($key, '_header')];
|
||||
}
|
||||
}
|
||||
}
|
@@ -11,120 +11,26 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Export\Formatter;
|
||||
|
||||
use Chill\MainBundle\Export\ExportManager;
|
||||
use Chill\MainBundle\Export\ExportGenerationContext;
|
||||
use Chill\MainBundle\Export\ExportInterface;
|
||||
use Chill\MainBundle\Export\ExportManagerAwareInterface;
|
||||
use Chill\MainBundle\Export\FormattedExportGeneration;
|
||||
use Chill\MainBundle\Export\FormatterInterface;
|
||||
use Chill\MainBundle\Export\Helper\ExportManagerAwareTrait;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Contracts\Translation\TranslatableInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class SpreadSheetFormatter implements FormatterInterface
|
||||
final class SpreadSheetFormatter implements FormatterInterface, ExportManagerAwareInterface
|
||||
{
|
||||
/**
|
||||
* an array where keys are the aggregators aliases and
|
||||
* values are the data.
|
||||
*
|
||||
* replaced when `getResponse` is called.
|
||||
*
|
||||
* @var type
|
||||
*/
|
||||
protected $aggregatorsData;
|
||||
use ExportManagerAwareTrait;
|
||||
|
||||
/**
|
||||
* The export.
|
||||
*
|
||||
* replaced when `getResponse` is called.
|
||||
*
|
||||
* @var \Chill\MainBundle\Export\ExportInterface
|
||||
*/
|
||||
protected $export;
|
||||
|
||||
/**
|
||||
* replaced when `getResponse` is called.
|
||||
*
|
||||
* @var type
|
||||
*/
|
||||
// protected $aggregators;
|
||||
|
||||
/**
|
||||
* array containing value of export form.
|
||||
*
|
||||
* replaced when `getResponse` is called.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $exportData;
|
||||
|
||||
/**
|
||||
* @var ExportManager
|
||||
*/
|
||||
protected $exportManager;
|
||||
|
||||
/**
|
||||
* replaced when `getResponse` is called.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $filtersData;
|
||||
|
||||
/**
|
||||
* replaced when `getResponse` is called.
|
||||
*
|
||||
* @var type
|
||||
*/
|
||||
protected $formatterData;
|
||||
|
||||
/**
|
||||
* The result, as returned by the export.
|
||||
*
|
||||
* replaced when `getResponse` is called.
|
||||
*
|
||||
* @var type
|
||||
*/
|
||||
protected $result;
|
||||
|
||||
/**
|
||||
* replaced when `getResponse` is called.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
// protected $labels;
|
||||
|
||||
/**
|
||||
* temporary file to store spreadsheet.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $tempfile;
|
||||
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
protected $translator;
|
||||
|
||||
/**
|
||||
* cache for displayable result.
|
||||
*
|
||||
* This cache is reset when `getResponse` is called.
|
||||
*
|
||||
* The array's keys are the keys in the raw result, and
|
||||
* values are the callable which will transform the raw result to
|
||||
* displayable result.
|
||||
*/
|
||||
private ?array $cacheDisplayableResult = null;
|
||||
|
||||
/**
|
||||
* Whethe `cacheDisplayableResult` is initialized or not.
|
||||
*/
|
||||
private bool $cacheDisplayableResultIsInitialized = false;
|
||||
|
||||
public function __construct(TranslatorInterface $translatorInterface, ExportManager $exportManager)
|
||||
{
|
||||
$this->translator = $translatorInterface;
|
||||
$this->exportManager = $exportManager;
|
||||
}
|
||||
public function __construct(private readonly TranslatorInterface $translator) {}
|
||||
|
||||
public function buildForm(
|
||||
FormBuilderInterface $builder,
|
||||
@@ -142,7 +48,7 @@ class SpreadSheetFormatter implements FormatterInterface
|
||||
]);
|
||||
|
||||
// ordering aggregators
|
||||
$aggregators = $this->exportManager->getAggregators($aggregatorAliases);
|
||||
$aggregators = $this->getExportManager()->getAggregators($aggregatorAliases);
|
||||
$nb = \count($aggregatorAliases);
|
||||
|
||||
foreach ($aggregators as $alias => $aggregator) {
|
||||
@@ -155,11 +61,26 @@ class SpreadSheetFormatter implements FormatterInterface
|
||||
}
|
||||
}
|
||||
|
||||
public function getNormalizationVersion(): int
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
public function normalizeFormData(array $formData): array
|
||||
{
|
||||
return $formData;
|
||||
}
|
||||
|
||||
public function denormalizeFormData(array $formData, int $fromVersion): array
|
||||
{
|
||||
return $formData;
|
||||
}
|
||||
|
||||
public function getFormDefaultData(array $aggregatorAliases): array
|
||||
{
|
||||
$data = ['format' => 'xlsx'];
|
||||
|
||||
$aggregators = iterator_to_array($this->exportManager->getAggregators($aggregatorAliases));
|
||||
$aggregators = iterator_to_array($this->getExportManager()->getAggregators($aggregatorAliases));
|
||||
foreach (array_keys($aggregators) as $index => $alias) {
|
||||
$data[$alias] = ['order' => $index + 1];
|
||||
}
|
||||
@@ -172,6 +93,51 @@ class SpreadSheetFormatter implements FormatterInterface
|
||||
return 'SpreadSheet (xlsx, ods)';
|
||||
}
|
||||
|
||||
public function generate(
|
||||
$result,
|
||||
$formatterData,
|
||||
string $exportAlias,
|
||||
array $exportData,
|
||||
array $filtersData,
|
||||
array $aggregatorsData,
|
||||
ExportGenerationContext $context,
|
||||
) {
|
||||
// Initialize local variables instead of class properties
|
||||
/** @var ExportInterface $export */
|
||||
$export = $this->getExportManager()->getExport($exportAlias);
|
||||
|
||||
// Initialize cache variables
|
||||
$cacheDisplayableResult = $this->initializeDisplayable($result, $export, $exportData, $aggregatorsData);
|
||||
|
||||
$tempfile = \tempnam(\sys_get_temp_dir(), '');
|
||||
|
||||
if (false === $tempfile) {
|
||||
throw new \RuntimeException('Unable to create temporary file');
|
||||
}
|
||||
|
||||
$this->generateContent(
|
||||
$context,
|
||||
$tempfile,
|
||||
$result,
|
||||
$formatterData,
|
||||
$export,
|
||||
$exportData,
|
||||
$filtersData,
|
||||
$aggregatorsData,
|
||||
$cacheDisplayableResult,
|
||||
);
|
||||
|
||||
$result = new FormattedExportGeneration(
|
||||
file_get_contents($tempfile),
|
||||
$this->getContentType($formatterData['format']),
|
||||
);
|
||||
|
||||
// remove the temp file from disk
|
||||
\unlink($tempfile);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function getResponse(
|
||||
$result,
|
||||
$formatterData,
|
||||
@@ -179,44 +145,22 @@ class SpreadSheetFormatter implements FormatterInterface
|
||||
array $exportData,
|
||||
array $filtersData,
|
||||
array $aggregatorsData,
|
||||
ExportGenerationContext $context,
|
||||
): Response {
|
||||
// store all data when the process is initiated
|
||||
$this->result = $result;
|
||||
$this->formatterData = $formatterData;
|
||||
$this->export = $this->exportManager->getExport($exportAlias);
|
||||
$this->exportData = $exportData;
|
||||
$this->filtersData = $filtersData;
|
||||
$this->aggregatorsData = $aggregatorsData;
|
||||
$formattedResult = $this->generate($result, $formatterData, $exportAlias, $exportData, $filtersData, $aggregatorsData, $context);
|
||||
|
||||
// reset cache
|
||||
$this->cacheDisplayableResult = [];
|
||||
$this->cacheDisplayableResultIsInitialized = false;
|
||||
|
||||
$response = new Response();
|
||||
$response->headers->set(
|
||||
'Content-Type',
|
||||
$this->getContentType($this->formatterData['format'])
|
||||
);
|
||||
|
||||
$this->tempfile = \tempnam(\sys_get_temp_dir(), '');
|
||||
$this->generateContent();
|
||||
|
||||
$f = \fopen($this->tempfile, 'rb');
|
||||
$response->setContent(\stream_get_contents($f));
|
||||
fclose($f);
|
||||
|
||||
// remove the temp file from disk
|
||||
\unlink($this->tempfile);
|
||||
$response = new BinaryFileResponse($formattedResult->content);
|
||||
$response->headers->set('Content-Type', $formattedResult->contentType);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function getType()
|
||||
public function getType(): string
|
||||
{
|
||||
return 'tabular';
|
||||
}
|
||||
|
||||
protected function addContentTable(
|
||||
private function addContentTable(
|
||||
Worksheet $worksheet,
|
||||
$sortedResults,
|
||||
$line,
|
||||
@@ -238,20 +182,21 @@ class SpreadSheetFormatter implements FormatterInterface
|
||||
*
|
||||
* @return int the line number after the last description
|
||||
*/
|
||||
protected function addFiltersDescription(Worksheet &$worksheet)
|
||||
private function addFiltersDescription(Worksheet &$worksheet, ExportGenerationContext $context, array $filtersData)
|
||||
{
|
||||
$line = 3;
|
||||
|
||||
foreach ($this->filtersData as $alias => $data) {
|
||||
$filter = $this->exportManager->getFilter($alias);
|
||||
$description = $filter->describeAction($data, 'string');
|
||||
|
||||
foreach ($filtersData as $alias => $data) {
|
||||
$filter = $this->getExportManager()->getFilter($alias);
|
||||
$description = $filter->describeAction($data, $context);
|
||||
if (\is_array($description)) {
|
||||
$description = $this->translator
|
||||
->trans(
|
||||
$description[0],
|
||||
$description[1] ?? []
|
||||
$description[1] ?? [],
|
||||
);
|
||||
} elseif ($description instanceof TranslatableInterface) {
|
||||
$description = $description->trans($this->translator, $this->translator->getLocale());
|
||||
}
|
||||
|
||||
$worksheet->setCellValue('A'.$line, $description);
|
||||
@@ -266,23 +211,23 @@ class SpreadSheetFormatter implements FormatterInterface
|
||||
*
|
||||
* return the line number where the next content (i.e. result) should
|
||||
* be appended.
|
||||
*
|
||||
* @param int $line
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function addHeaders(
|
||||
private function addHeaders(
|
||||
Worksheet &$worksheet,
|
||||
array $globalKeys,
|
||||
$line,
|
||||
) {
|
||||
int $line,
|
||||
array $cacheDisplayableResult = [],
|
||||
): int {
|
||||
// get the displayable form of headers
|
||||
$displayables = [];
|
||||
|
||||
foreach ($globalKeys as $key) {
|
||||
$displayables[] = $this->translator->trans(
|
||||
$this->getDisplayableResult($key, '_header')
|
||||
);
|
||||
$displayable = $this->getDisplayableResult($key, '_header', $cacheDisplayableResult);
|
||||
|
||||
if ($displayable instanceof TranslatableInterface) {
|
||||
$displayables[] = $displayable->trans($this->translator, $this->translator->getLocale());
|
||||
} else {
|
||||
$displayables[] = $this->translator->trans($this->getDisplayableResult($key, '_header', $cacheDisplayableResult));
|
||||
}
|
||||
}
|
||||
|
||||
// add headers on worksheet
|
||||
@@ -299,9 +244,9 @@ class SpreadSheetFormatter implements FormatterInterface
|
||||
* Add the title to the worksheet and merge the cell containing
|
||||
* the title.
|
||||
*/
|
||||
protected function addTitleToWorkSheet(Worksheet &$worksheet)
|
||||
private function addTitleToWorkSheet(Worksheet &$worksheet, $export)
|
||||
{
|
||||
$worksheet->setCellValue('A1', $this->getTitle());
|
||||
$worksheet->setCellValue('A1', $this->getTitle($export));
|
||||
$worksheet->mergeCells('A1:G1');
|
||||
}
|
||||
|
||||
@@ -310,14 +255,14 @@ class SpreadSheetFormatter implements FormatterInterface
|
||||
*
|
||||
* @return array where 1st member is spreadsheet, 2nd is worksheet
|
||||
*/
|
||||
protected function createSpreadsheet()
|
||||
private function createSpreadsheet($export)
|
||||
{
|
||||
$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
|
||||
$worksheet = $spreadsheet->getActiveSheet();
|
||||
|
||||
// setting the worksheet title and code name
|
||||
$worksheet
|
||||
->setTitle($this->getTitle())
|
||||
->setTitle($this->getTitle($export))
|
||||
->setCodeName('result');
|
||||
|
||||
return [$spreadsheet, $worksheet];
|
||||
@@ -326,29 +271,38 @@ class SpreadSheetFormatter implements FormatterInterface
|
||||
/**
|
||||
* Generate the content and write it to php://temp.
|
||||
*/
|
||||
protected function generateContent()
|
||||
{
|
||||
[$spreadsheet, $worksheet] = $this->createSpreadsheet();
|
||||
private function generateContent(
|
||||
ExportGenerationContext $context,
|
||||
string $tempfile,
|
||||
$result,
|
||||
$formatterData,
|
||||
$export,
|
||||
array $exportData,
|
||||
array $filtersData,
|
||||
array $aggregatorsData,
|
||||
array $cacheDisplayableResult,
|
||||
) {
|
||||
[$spreadsheet, $worksheet] = $this->createSpreadsheet($export);
|
||||
|
||||
$this->addTitleToWorkSheet($worksheet);
|
||||
$line = $this->addFiltersDescription($worksheet);
|
||||
$this->addTitleToWorkSheet($worksheet, $export);
|
||||
$line = $this->addFiltersDescription($worksheet, $context, $filtersData);
|
||||
|
||||
// at this point, we are going to sort retsults for an easier manipulation
|
||||
// at this point, we are going to sort results for an easier manipulation
|
||||
[$sortedResult, $exportKeys, $aggregatorKeys, $globalKeys] =
|
||||
$this->sortResult();
|
||||
$this->sortResult($result, $export, $exportData, $aggregatorsData, $formatterData, $cacheDisplayableResult);
|
||||
|
||||
$line = $this->addHeaders($worksheet, $globalKeys, $line);
|
||||
$line = $this->addHeaders($worksheet, $globalKeys, $line, $cacheDisplayableResult);
|
||||
|
||||
$line = $this->addContentTable($worksheet, $sortedResult, $line);
|
||||
$this->addContentTable($worksheet, $sortedResult, $line);
|
||||
|
||||
$writer = match ($this->formatterData['format']) {
|
||||
$writer = match ($formatterData['format']) {
|
||||
'ods' => \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Ods'),
|
||||
'xlsx' => \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Xlsx'),
|
||||
'csv' => \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Csv'),
|
||||
default => throw new \LogicException(),
|
||||
};
|
||||
|
||||
$writer->save($this->tempfile);
|
||||
$writer->save($tempfile);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -357,7 +311,7 @@ class SpreadSheetFormatter implements FormatterInterface
|
||||
*
|
||||
* @return string[] an array containing the keys of aggregators
|
||||
*/
|
||||
protected function getAggregatorKeysSorted()
|
||||
private function getAggregatorKeysSorted(array $aggregatorsData, array $formatterData)
|
||||
{
|
||||
// empty array for aggregators keys
|
||||
$keys = [];
|
||||
@@ -365,7 +319,7 @@ class SpreadSheetFormatter implements FormatterInterface
|
||||
// during sorting
|
||||
$aggregatorKeyAssociation = [];
|
||||
|
||||
foreach ($this->aggregatorsData as $alias => $data) {
|
||||
foreach ($aggregatorsData as $alias => $data) {
|
||||
$aggregator = $this->exportManager->getAggregator($alias);
|
||||
$aggregatorsKeys = $aggregator->getQueryKeys($data);
|
||||
// append the keys from aggregator to the $keys existing array
|
||||
@@ -377,9 +331,9 @@ class SpreadSheetFormatter implements FormatterInterface
|
||||
}
|
||||
|
||||
// sort the result using the form
|
||||
usort($keys, function ($a, $b) use ($aggregatorKeyAssociation) {
|
||||
$A = $this->formatterData[$aggregatorKeyAssociation[$a]]['order'];
|
||||
$B = $this->formatterData[$aggregatorKeyAssociation[$b]]['order'];
|
||||
usort($keys, function ($a, $b) use ($aggregatorKeyAssociation, $formatterData) {
|
||||
$A = $formatterData[$aggregatorKeyAssociation[$a]]['order'];
|
||||
$B = $formatterData[$aggregatorKeyAssociation[$b]]['order'];
|
||||
|
||||
if ($A === $B) {
|
||||
return 0;
|
||||
@@ -395,7 +349,7 @@ class SpreadSheetFormatter implements FormatterInterface
|
||||
return $keys;
|
||||
}
|
||||
|
||||
protected function getContentType($format)
|
||||
private function getContentType($format)
|
||||
{
|
||||
switch ($format) {
|
||||
case 'csv':
|
||||
@@ -412,25 +366,26 @@ class SpreadSheetFormatter implements FormatterInterface
|
||||
|
||||
/**
|
||||
* Get the displayable result.
|
||||
*
|
||||
* @param string $key
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getDisplayableResult($key, mixed $value): mixed
|
||||
{
|
||||
if (false === $this->cacheDisplayableResultIsInitialized) {
|
||||
$this->initializeCache($key);
|
||||
}
|
||||
|
||||
private function getDisplayableResult(
|
||||
string $key,
|
||||
mixed $value,
|
||||
array $cacheDisplayableResult,
|
||||
): string|TranslatableInterface|\DateTimeInterface|int|float|bool {
|
||||
$value ??= '';
|
||||
|
||||
return \call_user_func($this->cacheDisplayableResult[$key], $value);
|
||||
return \call_user_func($cacheDisplayableResult[$key], $value);
|
||||
}
|
||||
|
||||
protected function getTitle()
|
||||
private function getTitle($export): string
|
||||
{
|
||||
$title = $this->translator->trans($this->export->getTitle());
|
||||
$original = $export->getTitle();
|
||||
|
||||
if ($original instanceof TranslatableInterface) {
|
||||
$title = $original->trans($this->translator, $this->translator->getLocale());
|
||||
} else {
|
||||
$title = $this->translator->trans($original);
|
||||
}
|
||||
|
||||
if (30 < strlen($title)) {
|
||||
return substr($title, 0, 30).'…';
|
||||
@@ -439,8 +394,13 @@ class SpreadSheetFormatter implements FormatterInterface
|
||||
return $title;
|
||||
}
|
||||
|
||||
protected function initializeCache($key)
|
||||
{
|
||||
private function initializeDisplayable(
|
||||
$result,
|
||||
ExportInterface $export,
|
||||
array $exportData,
|
||||
array $aggregatorsData,
|
||||
): array {
|
||||
$cacheDisplayableResult = [];
|
||||
/*
|
||||
* this function follows the following steps :
|
||||
*
|
||||
@@ -453,13 +413,12 @@ class SpreadSheetFormatter implements FormatterInterface
|
||||
// 1. create an associative array with key and export / aggregator
|
||||
$keysExportElementAssociation = [];
|
||||
// keys for export
|
||||
foreach ($this->export->getQueryKeys($this->exportData) as $key) {
|
||||
$keysExportElementAssociation[$key] = [$this->export,
|
||||
$this->exportData, ];
|
||||
foreach ($export->getQueryKeys($exportData) as $key) {
|
||||
$keysExportElementAssociation[$key] = [$export, $exportData];
|
||||
}
|
||||
// keys for aggregator
|
||||
foreach ($this->aggregatorsData as $alias => $data) {
|
||||
$aggregator = $this->exportManager->getAggregator($alias);
|
||||
foreach ($aggregatorsData as $alias => $data) {
|
||||
$aggregator = $this->getExportManager()->getAggregator($alias);
|
||||
|
||||
foreach ($aggregator->getQueryKeys($data) as $key) {
|
||||
$keysExportElementAssociation[$key] = [$aggregator, $data];
|
||||
@@ -471,7 +430,7 @@ class SpreadSheetFormatter implements FormatterInterface
|
||||
|
||||
$allValues = [];
|
||||
// store all the values in an array
|
||||
foreach ($this->result as $row) {
|
||||
foreach ($result as $row) {
|
||||
foreach ($keys as $key) {
|
||||
$allValues[$key][] = $row[$key];
|
||||
}
|
||||
@@ -482,15 +441,14 @@ class SpreadSheetFormatter implements FormatterInterface
|
||||
foreach ($keysExportElementAssociation as $key => [$element, $data]) {
|
||||
// handle the case when there is not results lines (query is empty)
|
||||
if ([] === $allValues) {
|
||||
$this->cacheDisplayableResult[$key] = $element->getLabels($key, ['_header'], $data);
|
||||
$cacheDisplayableResult[$key] = $element->getLabels($key, ['_header'], $data);
|
||||
} else {
|
||||
$this->cacheDisplayableResult[$key] =
|
||||
$cacheDisplayableResult[$key] =
|
||||
$element->getLabels($key, \array_unique($allValues[$key]), $data);
|
||||
}
|
||||
}
|
||||
|
||||
// the cache is initialized !
|
||||
$this->cacheDisplayableResultIsInitialized = true;
|
||||
return $cacheDisplayableResult;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -528,23 +486,28 @@ class SpreadSheetFormatter implements FormatterInterface
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
protected function sortResult()
|
||||
{
|
||||
private function sortResult(
|
||||
$result,
|
||||
ExportInterface $export,
|
||||
array $exportData,
|
||||
array $aggregatorsData,
|
||||
array $formatterData,
|
||||
array $cacheDisplayableResult,
|
||||
) {
|
||||
// get the keys for each row
|
||||
$exportKeys = $this->export->getQueryKeys($this->exportData);
|
||||
$aggregatorKeys = $this->getAggregatorKeysSorted();
|
||||
|
||||
$exportKeys = $export->getQueryKeys($exportData);
|
||||
$aggregatorKeys = $this->getAggregatorKeysSorted($aggregatorsData, $formatterData);
|
||||
$globalKeys = \array_merge($aggregatorKeys, $exportKeys);
|
||||
|
||||
$sortedResult = \array_map(function ($row) use ($globalKeys) {
|
||||
$sortedResult = \array_map(function ($row) use ($globalKeys, $cacheDisplayableResult) {
|
||||
$newRow = [];
|
||||
|
||||
foreach ($globalKeys as $key) {
|
||||
$newRow[] = $this->getDisplayableResult($key, $row[$key]);
|
||||
$newRow[] = $this->getDisplayableResult($key, $row[$key], $cacheDisplayableResult);
|
||||
}
|
||||
|
||||
return $newRow;
|
||||
}, $this->result);
|
||||
}, $result);
|
||||
|
||||
\array_multisort($sortedResult);
|
||||
|
||||
|
@@ -11,67 +11,40 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Export\Formatter;
|
||||
|
||||
use Chill\MainBundle\Export\ExportManager;
|
||||
use Chill\MainBundle\Export\ExportGenerationContext;
|
||||
use Chill\MainBundle\Export\ExportManagerAwareInterface;
|
||||
use Chill\MainBundle\Export\FormattedExportGeneration;
|
||||
use Chill\MainBundle\Export\FormatterInterface;
|
||||
use Chill\MainBundle\Export\Helper\ExportManagerAwareTrait;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\Date;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Contracts\Translation\TranslatableInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use function count;
|
||||
|
||||
// command to get the report with curl : curl --user "center a_social:password" "http://localhost:8000/fr/exports/generate/count_person?export[filters][person_gender_filter][enabled]=&export[filters][person_nationality_filter][enabled]=&export[filters][person_nationality_filter][form][nationalities]=&export[aggregators][person_nationality_aggregator][order]=1&export[aggregators][person_nationality_aggregator][form][group_by_level]=country&export[submit]=&export[_token]=RHpjHl389GrK-bd6iY5NsEqrD5UKOTHH40QKE9J1edU" --globoff
|
||||
|
||||
/**
|
||||
* Create a CSV List for the export.
|
||||
*/
|
||||
class SpreadsheetListFormatter implements FormatterInterface
|
||||
class SpreadsheetListFormatter implements FormatterInterface, ExportManagerAwareInterface
|
||||
{
|
||||
protected $exportAlias;
|
||||
use ExportManagerAwareTrait;
|
||||
|
||||
protected $exportData;
|
||||
|
||||
/**
|
||||
* @var ExportManager
|
||||
*/
|
||||
protected $exportManager;
|
||||
|
||||
protected $formatterData;
|
||||
|
||||
/**
|
||||
* This variable cache the labels internally.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $labelsCache;
|
||||
|
||||
protected $result;
|
||||
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
protected $translator;
|
||||
|
||||
public function __construct(TranslatorInterface $translatorInterface, ExportManager $exportManager)
|
||||
{
|
||||
$this->translator = $translatorInterface;
|
||||
$this->exportManager = $exportManager;
|
||||
}
|
||||
public function __construct(private readonly TranslatorInterface $translator) {}
|
||||
|
||||
/**
|
||||
* build a form, which will be used to collect data required for the execution
|
||||
* of this formatter.
|
||||
*
|
||||
* @uses appendAggregatorForm
|
||||
*
|
||||
* @param string $exportAlias
|
||||
*/
|
||||
public function buildForm(
|
||||
FormBuilderInterface $builder,
|
||||
$exportAlias,
|
||||
string $exportAlias,
|
||||
array $aggregatorAliases,
|
||||
): void {
|
||||
$builder
|
||||
@@ -93,58 +66,52 @@ class SpreadsheetListFormatter implements FormatterInterface
|
||||
]);
|
||||
}
|
||||
|
||||
public function getNormalizationVersion(): int
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
public function normalizeFormData(array $formData): array
|
||||
{
|
||||
return ['format' => $formData['format'], 'numerotation' => $formData['numerotation']];
|
||||
}
|
||||
|
||||
public function denormalizeFormData(array $formData, int $fromVersion): array
|
||||
{
|
||||
return ['format' => $formData['format'], 'numerotation' => $formData['numerotation']];
|
||||
}
|
||||
|
||||
public function getFormDefaultData(array $aggregatorAliases): array
|
||||
{
|
||||
return ['numerotation' => true, 'format' => 'xlsx'];
|
||||
}
|
||||
|
||||
public function getName()
|
||||
public function getName(): string|TranslatableInterface
|
||||
{
|
||||
return 'Spreadsheet list formatter (.xlsx, .ods)';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a response from the data collected on differents ExportElementInterface.
|
||||
*
|
||||
* @param mixed[] $result The result, as given by the ExportInterface
|
||||
* @param mixed[] $formatterData collected from the current form
|
||||
* @param string $exportAlias the id of the current export
|
||||
* @param array $filtersData an array containing the filters data. The key are the filters id, and the value are the data
|
||||
* @param array $aggregatorsData an array containing the aggregators data. The key are the filters id, and the value are the data
|
||||
*
|
||||
* @return Response The response to be shown
|
||||
*/
|
||||
public function getResponse(
|
||||
$result,
|
||||
$formatterData,
|
||||
$exportAlias,
|
||||
array $exportData,
|
||||
array $filtersData,
|
||||
array $aggregatorsData,
|
||||
): \Symfony\Component\HttpFoundation\Response {
|
||||
$this->result = $result;
|
||||
$this->exportAlias = $exportAlias;
|
||||
$this->exportData = $exportData;
|
||||
$this->formatterData = $formatterData;
|
||||
|
||||
public function generate($result, $formatterData, string $exportAlias, array $exportData, array $filtersData, array $aggregatorsData, ExportGenerationContext $context): FormattedExportGeneration
|
||||
{
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$worksheet = $spreadsheet->getActiveSheet();
|
||||
$cacheLabels = $this->prepareCacheLabels($result, $exportAlias, $exportData);
|
||||
|
||||
$this->prepareHeaders($worksheet);
|
||||
$this->prepareHeaders($cacheLabels, $worksheet, $result, $formatterData, $exportAlias, $exportData);
|
||||
|
||||
$i = 1;
|
||||
|
||||
foreach ($result as $row) {
|
||||
if (true === $this->formatterData['numerotation']) {
|
||||
if (true === $formatterData['numerotation']) {
|
||||
$worksheet->setCellValue('A'.($i + 1), (string) $i);
|
||||
}
|
||||
|
||||
$a = $this->formatterData['numerotation'] ? 'B' : 'A';
|
||||
$a = $formatterData['numerotation'] ? 'B' : 'A';
|
||||
|
||||
foreach ($row as $key => $value) {
|
||||
$row = $a.($i + 1);
|
||||
|
||||
$formattedValue = $this->getLabel($key, $value);
|
||||
$formattedValue = $this->getLabel($cacheLabels, $key, $value, $result, $exportAlias, $exportData);
|
||||
|
||||
if ($formattedValue instanceof \DateTimeInterface) {
|
||||
$worksheet->setCellValue($row, Date::PHPToExcel($formattedValue));
|
||||
@@ -158,6 +125,8 @@ class SpreadsheetListFormatter implements FormatterInterface
|
||||
->getNumberFormat()
|
||||
->setFormatCode(NumberFormat::FORMAT_DATE_DATETIME);
|
||||
}
|
||||
} elseif ($formattedValue instanceof TranslatableInterface) {
|
||||
$worksheet->setCellValue($row, $formattedValue->trans($this->translator));
|
||||
} else {
|
||||
$worksheet->setCellValue($row, $formattedValue);
|
||||
}
|
||||
@@ -167,7 +136,7 @@ class SpreadsheetListFormatter implements FormatterInterface
|
||||
++$i;
|
||||
}
|
||||
|
||||
switch ($this->formatterData['format']) {
|
||||
switch ($formatterData['format']) {
|
||||
case 'ods':
|
||||
$writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Ods');
|
||||
$contentType = 'application/vnd.oasis.opendocument.spreadsheet';
|
||||
@@ -190,26 +159,52 @@ class SpreadsheetListFormatter implements FormatterInterface
|
||||
default:
|
||||
// this should not happen
|
||||
// throw an exception to ensure that the error is catched
|
||||
throw new \OutOfBoundsException('The format '.$this->formatterData['format'].' is not supported');
|
||||
throw new \OutOfBoundsException('The format '.$formatterData['format'].' is not supported');
|
||||
}
|
||||
|
||||
$response = new Response();
|
||||
$response->headers->set('content-type', $contentType);
|
||||
|
||||
$tempfile = \tempnam(\sys_get_temp_dir(), '');
|
||||
$writer->save($tempfile);
|
||||
|
||||
$f = \fopen($tempfile, 'rb');
|
||||
$response->setContent(\stream_get_contents($f));
|
||||
fclose($f);
|
||||
$generated = new FormattedExportGeneration(
|
||||
file_get_contents($tempfile),
|
||||
$contentType,
|
||||
);
|
||||
|
||||
// remove the temp file from disk
|
||||
\unlink($tempfile);
|
||||
|
||||
return $generated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a response from the data collected on differents ExportElementInterface.
|
||||
*
|
||||
* @param mixed[] $result The result, as given by the ExportInterface
|
||||
* @param mixed[] $formatterData collected from the current form
|
||||
* @param string $exportAlias the id of the current export
|
||||
* @param array $aggregatorsData an array containing the aggregators data. The key are the filters id, and the value are the data
|
||||
* @param array $filtersData an array containing the filters data. The key are the filters id, and the value are the data
|
||||
*
|
||||
* @return Response The response to be shown
|
||||
*/
|
||||
public function getResponse(
|
||||
$result,
|
||||
$formatterData,
|
||||
$exportAlias,
|
||||
array $exportData,
|
||||
array $filtersData,
|
||||
array $aggregatorsData,
|
||||
ExportGenerationContext $context,
|
||||
) {
|
||||
$generated = $this->generate($result, $formatterData, $exportAlias, $exportData, $filtersData, $aggregatorsData, $context);
|
||||
|
||||
$response = new BinaryFileResponse($generated->content);
|
||||
$response->headers->set('Content-Type', $generated->contentType);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function getType()
|
||||
public function getType(): string
|
||||
{
|
||||
return FormatterInterface::TYPE_LIST;
|
||||
}
|
||||
@@ -217,34 +212,29 @@ class SpreadsheetListFormatter implements FormatterInterface
|
||||
/**
|
||||
* Give the label corresponding to the given key and value.
|
||||
*
|
||||
* @param string $key
|
||||
* @param string $value
|
||||
*
|
||||
* @return string
|
||||
* @return string|\DateTimeInterface|int|float|TranslatableInterface|null
|
||||
*
|
||||
* @throws \LogicException if the label is not found
|
||||
*/
|
||||
protected function getLabel($key, $value)
|
||||
private function getLabel(array $labelsCache, $key, $value, array $result, string $exportAlias, array $exportData)
|
||||
{
|
||||
if (null === $this->labelsCache) {
|
||||
$this->prepareCacheLabels();
|
||||
if (!\array_key_exists($key, $labelsCache)) {
|
||||
throw new \OutOfBoundsException(sprintf('The key "%s" is not present in the list of keys handled by this query. Check your `getKeys` and `getLabels` methods. Available keys are %s.', $key, \implode(', ', \array_keys($labelsCache))));
|
||||
}
|
||||
|
||||
if (!\array_key_exists($key, $this->labelsCache)) {
|
||||
throw new \OutOfBoundsException(sprintf('The key "%s" is not present in the list of keys handled by this query. Check your `getKeys` and `getLabels` methods. Available keys are %s.', $key, \implode(', ', \array_keys($this->labelsCache))));
|
||||
}
|
||||
|
||||
return $this->labelsCache[$key]($value);
|
||||
return $labelsCache[$key]($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the label cache which will be used by getLabel. This function
|
||||
* should be called only once in the generation lifecycle.
|
||||
* Prepare the label cache which will be used by getLabel.
|
||||
*
|
||||
* @return array The labels cache
|
||||
*/
|
||||
protected function prepareCacheLabels()
|
||||
private function prepareCacheLabels(array $result, string $exportAlias, array $exportData): array
|
||||
{
|
||||
$export = $this->exportManager->getExport($this->exportAlias);
|
||||
$keys = $export->getQueryKeys($this->exportData);
|
||||
$labelsCache = [];
|
||||
$export = $this->getExportManager()->getExport($exportAlias);
|
||||
$keys = $export->getQueryKeys($exportData);
|
||||
|
||||
foreach ($keys as $key) {
|
||||
// get an array with all values for this key if possible
|
||||
@@ -254,29 +244,31 @@ class SpreadsheetListFormatter implements FormatterInterface
|
||||
}
|
||||
|
||||
return $v[$key];
|
||||
}, $this->result);
|
||||
// store the label in the labelsCache property
|
||||
$this->labelsCache[$key] = $export->getLabels($key, $values, $this->exportData);
|
||||
}, $result);
|
||||
// store the label in the labelsCache
|
||||
$labelsCache[$key] = $export->getLabels($key, $values, $exportData);
|
||||
}
|
||||
|
||||
return $labelsCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* add the headers to the csv file.
|
||||
*/
|
||||
protected function prepareHeaders(Worksheet $worksheet)
|
||||
protected function prepareHeaders(array $labelsCache, Worksheet $worksheet, array $result, array $formatterData, string $exportAlias, array $exportData)
|
||||
{
|
||||
$keys = $this->exportManager->getExport($this->exportAlias)->getQueryKeys($this->exportData);
|
||||
$keys = $this->getExportManager()->getExport($exportAlias)->getQueryKeys($exportData);
|
||||
// we want to keep the order of the first row. So we will iterate on the first row of the results
|
||||
$first_row = \count($this->result) > 0 ? $this->result[0] : [];
|
||||
$first_row = \count($result) > 0 ? $result[0] : [];
|
||||
$header_line = [];
|
||||
|
||||
if (true === $this->formatterData['numerotation']) {
|
||||
if (true === $formatterData['numerotation']) {
|
||||
$header_line[] = $this->translator->trans('Number');
|
||||
}
|
||||
|
||||
foreach ($first_row as $key => $value) {
|
||||
$header_line[] = $this->translator->trans(
|
||||
$this->getLabel($key, '_header')
|
||||
$this->getLabel($labelsCache, $key, '_header', $result, $exportAlias, $exportData)
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -12,7 +12,11 @@ declare(strict_types=1);
|
||||
namespace Chill\MainBundle\Export;
|
||||
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Contracts\Translation\TranslatableInterface;
|
||||
|
||||
/**
|
||||
* @method generate($result, $formatterData, string $exportAlias, array $exportData, array $filtersData, array $aggregatorsData, ExportGenerationContext $context): FormattedExportGeneration
|
||||
*/
|
||||
interface FormatterInterface
|
||||
{
|
||||
public const TYPE_LIST = 'list';
|
||||
@@ -30,16 +34,16 @@ interface FormatterInterface
|
||||
*/
|
||||
public function buildForm(
|
||||
FormBuilderInterface $builder,
|
||||
$exportAlias,
|
||||
string $exportAlias,
|
||||
array $aggregatorAliases,
|
||||
);
|
||||
): void;
|
||||
|
||||
/**
|
||||
* get the default data for the form build by buildForm.
|
||||
*/
|
||||
public function getFormDefaultData(array $aggregatorAliases): array;
|
||||
|
||||
public function getName();
|
||||
public function getName(): string|TranslatableInterface;
|
||||
|
||||
/**
|
||||
* Generate a response from the data collected on differents ExportElementInterface.
|
||||
@@ -47,19 +51,28 @@ interface FormatterInterface
|
||||
* @param mixed[] $result The result, as given by the ExportInterface
|
||||
* @param mixed[] $formatterData collected from the current form
|
||||
* @param string $exportAlias the id of the current export
|
||||
* @param array $filtersData an array containing the filters data. The key are the filters id, and the value are the data
|
||||
* @param array $aggregatorsData an array containing the aggregators data. The key are the filters id, and the value are the data
|
||||
* @param array $filtersData an array containing the filters data. The key are the filters id, and the value are the data
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\Response The response to be shown
|
||||
*
|
||||
* @deprecated use generate instead
|
||||
*/
|
||||
public function getResponse(
|
||||
$result,
|
||||
$formatterData,
|
||||
$exportAlias,
|
||||
array $result,
|
||||
array $formatterData,
|
||||
string $exportAlias,
|
||||
array $exportData,
|
||||
array $filtersData,
|
||||
array $aggregatorsData,
|
||||
ExportGenerationContext $context,
|
||||
);
|
||||
|
||||
public function getType();
|
||||
public function getType(): string;
|
||||
|
||||
public function normalizeFormData(array $formData): array;
|
||||
|
||||
public function denormalizeFormData(array $formData, int $fromVersion): array;
|
||||
|
||||
public function getNormalizationVersion(): int;
|
||||
}
|
||||
|
@@ -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\Export\Helper;
|
||||
|
||||
use Chill\MainBundle\Export\Exception\ExportRuntimeException;
|
||||
use Chill\MainBundle\Export\ExportManager;
|
||||
|
||||
trait ExportManagerAwareTrait
|
||||
{
|
||||
private ?ExportManager $exportManager;
|
||||
|
||||
public function setExportManager(ExportManager $exportManager): void
|
||||
{
|
||||
$this->exportManager = $exportManager;
|
||||
}
|
||||
|
||||
public function getExportManager(): ExportManager
|
||||
{
|
||||
if (null === $this->exportManager) {
|
||||
throw new ExportRuntimeException('ExportManager not set');
|
||||
}
|
||||
|
||||
return $this->exportManager;
|
||||
}
|
||||
}
|
@@ -11,6 +11,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Export;
|
||||
|
||||
use Doctrine\ORM\NativeQuery;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
|
||||
/**
|
||||
* Define methods to export list.
|
||||
*
|
||||
@@ -19,5 +22,10 @@ namespace Chill\MainBundle\Export;
|
||||
* (and list does not support aggregation on their data).
|
||||
*
|
||||
* When used, the `ExportManager` will not handle aggregator for this class.
|
||||
*
|
||||
* @template Q of QueryBuilder|NativeQuery
|
||||
* @template D of array
|
||||
*
|
||||
* @template-extends ExportInterface<Q, D>
|
||||
*/
|
||||
interface ListInterface extends ExportInterface {}
|
||||
|
@@ -0,0 +1,31 @@
|
||||
<?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\Messenger;
|
||||
|
||||
use Chill\MainBundle\Entity\ExportGeneration;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
|
||||
final readonly class ExportRequestGenerationMessage
|
||||
{
|
||||
public UuidInterface $id;
|
||||
|
||||
public int $userId;
|
||||
|
||||
public function __construct(
|
||||
ExportGeneration $exportGeneration,
|
||||
User $user,
|
||||
) {
|
||||
$this->id = $exportGeneration->getId();
|
||||
$this->userId = $user->getId();
|
||||
}
|
||||
}
|
@@ -0,0 +1,77 @@
|
||||
<?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\Messenger;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
use Chill\MainBundle\Export\ExportGenerator;
|
||||
use Chill\MainBundle\Repository\ExportGenerationRepository;
|
||||
use Chill\MainBundle\Repository\UserRepositoryInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
|
||||
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
||||
|
||||
#[AsMessageHandler]
|
||||
final readonly class ExportRequestGenerationMessageHandler implements MessageHandlerInterface
|
||||
{
|
||||
private const LOG_PREFIX = '[export_generation] ';
|
||||
|
||||
public function __construct(
|
||||
private ExportGenerationRepository $repository,
|
||||
private UserRepositoryInterface $userRepository,
|
||||
private ExportGenerator $exportGenerator,
|
||||
private StoredObjectManagerInterface $storedObjectManager,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
public function __invoke(ExportRequestGenerationMessage $exportRequestGenerationMessage)
|
||||
{
|
||||
$start = microtime(true);
|
||||
|
||||
$this->logger->info(
|
||||
self::LOG_PREFIX.'Handle generation message',
|
||||
[
|
||||
'exportId' => (string) $exportRequestGenerationMessage->id,
|
||||
]
|
||||
);
|
||||
|
||||
if (null === $exportGeneration = $this->repository->find($exportRequestGenerationMessage->id)) {
|
||||
throw new \UnexpectedValueException('ExportRequestGenerationMessage not found');
|
||||
}
|
||||
|
||||
if (null === $user = $this->userRepository->find($exportRequestGenerationMessage->userId)) {
|
||||
throw new \UnexpectedValueException('User not found');
|
||||
}
|
||||
|
||||
if (StoredObject::STATUS_PENDING !== $exportGeneration->getStatus()) {
|
||||
throw new UnrecoverableMessageHandlingException('object already generated');
|
||||
}
|
||||
|
||||
$generated = $this->exportGenerator->generate($exportGeneration->getExportAlias(), $exportGeneration->getOptions(), $user);
|
||||
$this->storedObjectManager->write($exportGeneration->getStoredObject(), $generated->content, $generated->contentType);
|
||||
$exportGeneration->getStoredObject()->setStatus(StoredObject::STATUS_READY);
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
$end = microtime(true);
|
||||
|
||||
$this->logger->notice(self::LOG_PREFIX.'Export generation successfully finished', [
|
||||
'exportId' => (string) $exportRequestGenerationMessage->id,
|
||||
'exportAlias' => $exportGeneration->getExportAlias(),
|
||||
'full_generation_duration' => $end - $exportGeneration->getCreatedAt()->getTimestamp(),
|
||||
'message_handler_duration' => $end - $start,
|
||||
]);
|
||||
}
|
||||
}
|
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Export\Messenger;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\MainBundle\Entity\ExportGeneration;
|
||||
use Chill\MainBundle\Repository\ExportGenerationRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
|
||||
|
||||
class OnExportGenerationFails implements EventSubscriberInterface
|
||||
{
|
||||
private const LOG_PREFIX = '[export_generation failed] ';
|
||||
|
||||
public function __construct(
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly ExportGenerationRepository $repository,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
) {}
|
||||
|
||||
public static function getSubscribedEvents()
|
||||
{
|
||||
return [
|
||||
WorkerMessageFailedEvent::class => 'onMessageFailed',
|
||||
];
|
||||
}
|
||||
|
||||
public function onMessageFailed(WorkerMessageFailedEvent $event): void
|
||||
{
|
||||
if ($event->willRetry()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$message = $event->getEnvelope()->getMessage();
|
||||
|
||||
if (!$message instanceof ExportRequestGenerationMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (null === $exportGeneration = $this->repository->find($message->id)) {
|
||||
throw new \UnexpectedValueException('ExportRequestGenerationMessage not found');
|
||||
}
|
||||
|
||||
$this->logger->error(self::LOG_PREFIX.'ExportRequestGenerationMessage failed to execute generation', [
|
||||
'exportId' => (string) $message->id,
|
||||
'userId' => $message->userId,
|
||||
'alias' => $exportGeneration->getExportAlias(),
|
||||
'throwable_message' => $event->getThrowable()->getMessage(),
|
||||
'throwable_trace' => $event->getThrowable()->getTraceAsString(),
|
||||
'throwable' => $event->getThrowable()::class,
|
||||
'full_generation_duration_failure' => microtime(true) - $exportGeneration->getCreatedAt()->getTimestamp(),
|
||||
]);
|
||||
|
||||
$this->markObjectAsFailed($event, $exportGeneration);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
private function markObjectAsFailed(WorkerMessageFailedEvent $event, ExportGeneration $exportGeneration): void
|
||||
{
|
||||
$exportGeneration->getStoredObject()->addGenerationErrors($event->getThrowable()->getMessage());
|
||||
$exportGeneration->getStoredObject()->setStatus(StoredObject::STATUS_FAILURE);
|
||||
}
|
||||
}
|
@@ -0,0 +1,24 @@
|
||||
<?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\Messenger;
|
||||
|
||||
use Chill\MainBundle\Entity\ExportGeneration;
|
||||
|
||||
final readonly class RemoveExportGenerationMessage
|
||||
{
|
||||
public string $exportGenerationId;
|
||||
|
||||
public function __construct(ExportGeneration $exportGeneration)
|
||||
{
|
||||
$this->exportGenerationId = $exportGeneration->getId()->toString();
|
||||
}
|
||||
}
|
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Export\Messenger;
|
||||
|
||||
use Chill\MainBundle\Repository\ExportGenerationRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
|
||||
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
||||
|
||||
#[AsMessageHandler]
|
||||
final readonly class RemoveExportGenerationMessageHandler implements MessageHandlerInterface
|
||||
{
|
||||
private const LOG_PREFIX = '[RemoveExportGenerationMessageHandler] ';
|
||||
|
||||
public function __construct(
|
||||
private ExportGenerationRepository $exportGenerationRepository,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private LoggerInterface $logger,
|
||||
private ClockInterface $clock,
|
||||
) {}
|
||||
|
||||
public function __invoke(RemoveExportGenerationMessage $message): void
|
||||
{
|
||||
$exportGeneration = $this->exportGenerationRepository->find($message->exportGenerationId);
|
||||
|
||||
if (null === $exportGeneration) {
|
||||
$this->logger->error(self::LOG_PREFIX.'ExportGeneration not found');
|
||||
throw new UnrecoverableMessageHandlingException(self::LOG_PREFIX.'ExportGeneration not found');
|
||||
}
|
||||
|
||||
$storedObject = $exportGeneration->getStoredObject();
|
||||
$storedObject->setDeleteAt($this->clock->now());
|
||||
|
||||
$this->entityManager->remove($exportGeneration);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
}
|
@@ -0,0 +1,124 @@
|
||||
<?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\Migrator;
|
||||
|
||||
use Chill\MainBundle\Service\RollingDate\RollingDate;
|
||||
|
||||
class SavedExportOptionsMigrator
|
||||
{
|
||||
public static function migrate(array $fromOptions): array
|
||||
{
|
||||
$to = [];
|
||||
$to['aggregators'] = array_map(
|
||||
self::mapEnabledStatus(...),
|
||||
$fromOptions['export']['export']['aggregators'] ?? [],
|
||||
);
|
||||
|
||||
$to['filters'] = array_map(
|
||||
self::mapEnabledStatus(...),
|
||||
$fromOptions['export']['export']['filters'] ?? [],
|
||||
);
|
||||
|
||||
$to['export'] = [
|
||||
'form' => array_map(
|
||||
self::mapFormData(...),
|
||||
array_filter(
|
||||
$fromOptions['export']['export']['export'] ?? [],
|
||||
static fn (string $key) => !in_array($key, ['filters', 'aggregators', 'pick_formatter'], true),
|
||||
ARRAY_FILTER_USE_KEY,
|
||||
),
|
||||
),
|
||||
'version' => 1,
|
||||
];
|
||||
|
||||
$to['pick_formatter'] = $fromOptions['export']['export']['pick_formatter']['alias'] ?? null;
|
||||
$to['centers'] = [
|
||||
'centers' => array_values(array_map(static fn ($id) => (int) $id, $fromOptions['centers']['centers']['c'] ?? $fromOptions['centers']['centers']['center'] ?? [])),
|
||||
'regroupments' => array_values(array_map(static fn ($id) => (int) $id, $fromOptions['centers']['centers']['regroupment'] ?? [])),
|
||||
];
|
||||
$to['formatter'] = [
|
||||
'form' => $fromOptions['formatter']['formatter'] ?? [],
|
||||
'version' => 1,
|
||||
];
|
||||
|
||||
return $to;
|
||||
}
|
||||
|
||||
private static function mapEnabledStatus(array $modifiersData): array
|
||||
{
|
||||
if ('1' === ($modifiersData['enabled'] ?? '0')) {
|
||||
return [
|
||||
'form' => array_map(self::mapFormData(...), $modifiersData['form'] ?? []),
|
||||
'version' => 1,
|
||||
'enabled' => true,
|
||||
];
|
||||
}
|
||||
|
||||
return ['enabled' => false];
|
||||
}
|
||||
|
||||
private static function mapFormData(array|string $formData): array|string|null
|
||||
{
|
||||
if (is_array($formData) && array_key_exists('roll', $formData)) {
|
||||
return self::refactorRollingDate($formData);
|
||||
}
|
||||
|
||||
if (is_string($formData)) {
|
||||
// we try different date formats
|
||||
if (false !== \DateTimeImmutable::createFromFormat('d-m-Y', $formData)) {
|
||||
return $formData;
|
||||
}
|
||||
if (false !== \DateTimeImmutable::createFromFormat('Y-m-d', $formData)) {
|
||||
return $formData;
|
||||
}
|
||||
|
||||
// we try json content
|
||||
try {
|
||||
$data = json_decode($formData, true, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
if (is_array($data)) {
|
||||
if (array_key_exists('type', $data) && array_key_exists('id', $data) && in_array($data['type'], ['person', 'thirdParty', 'user'], true)) {
|
||||
return $data['id'];
|
||||
}
|
||||
$response = [];
|
||||
foreach ($data as $item) {
|
||||
if (array_key_exists('type', $item) && array_key_exists('id', $item) && in_array($item['type'], ['person', 'thirdParty', 'user'], true)) {
|
||||
$response[] = $item['id'];
|
||||
}
|
||||
}
|
||||
if ([] !== $response) {
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
} catch (\JsonException) {
|
||||
return $formData;
|
||||
}
|
||||
}
|
||||
|
||||
return $formData;
|
||||
}
|
||||
|
||||
private static function refactorRollingDate(array $formData): ?array
|
||||
{
|
||||
if ('' === $formData['roll']) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$fixedDate = null !== ($formData['fixedDate'] ?? null) && '' !== $formData['fixedDate'] ?
|
||||
\DateTimeImmutable::createFromFormat('Y-m-d H:i:s', sprintf('%s 00:00:00', $formData['fixedDate']), new \DateTimeZone(date_default_timezone_get())) : null;
|
||||
|
||||
return (new RollingDate(
|
||||
$formData['roll'],
|
||||
$fixedDate,
|
||||
))->normalize();
|
||||
}
|
||||
}
|
@@ -37,12 +37,12 @@ interface ModifierInterface extends ExportElementInterface
|
||||
* @param QueryBuilder $qb the QueryBuilder initiated by the Export (and eventually modified by other Modifiers)
|
||||
* @param mixed[] $data the data from the Form (builded by buildForm)
|
||||
*/
|
||||
public function alterQuery(QueryBuilder $qb, $data);
|
||||
public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void;
|
||||
|
||||
/**
|
||||
* On which type of Export this ModifiersInterface may apply.
|
||||
*
|
||||
* @return string the type on which the Modifiers apply
|
||||
*/
|
||||
public function applyOn();
|
||||
public function applyOn(): string;
|
||||
}
|
||||
|
@@ -11,6 +11,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Export;
|
||||
|
||||
use Symfony\Contracts\Translation\TranslatableInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
final readonly class SortExportElement
|
||||
@@ -19,12 +20,21 @@ final readonly class SortExportElement
|
||||
private TranslatorInterface $translator,
|
||||
) {}
|
||||
|
||||
private function trans(string|TranslatableInterface $message): string
|
||||
{
|
||||
if ($message instanceof TranslatableInterface) {
|
||||
return $message->trans($this->translator, $this->translator->getLocale());
|
||||
}
|
||||
|
||||
return $this->translator->trans($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int|string, FilterInterface> $elements
|
||||
*/
|
||||
public function sortFilters(array &$elements): void
|
||||
{
|
||||
uasort($elements, fn (FilterInterface $a, FilterInterface $b) => $this->translator->trans($a->getTitle()) <=> $this->translator->trans($b->getTitle()));
|
||||
uasort($elements, fn (FilterInterface $a, FilterInterface $b) => $this->trans($a->getTitle()) <=> $this->trans($b->getTitle()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,6 +42,6 @@ final readonly class SortExportElement
|
||||
*/
|
||||
public function sortAggregators(array &$elements): void
|
||||
{
|
||||
uasort($elements, fn (AggregatorInterface $a, AggregatorInterface $b) => $this->translator->trans($a->getTitle()) <=> $this->translator->trans($b->getTitle()));
|
||||
uasort($elements, fn (AggregatorInterface $a, AggregatorInterface $b) => $this->trans($a->getTitle()) <=> $this->trans($b->getTitle()));
|
||||
}
|
||||
}
|
||||
|
@@ -1,56 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Form\DataMapper;
|
||||
|
||||
use Chill\MainBundle\Entity\Regroupment;
|
||||
use Symfony\Component\Form\DataMapperInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
|
||||
final readonly class ExportPickCenterDataMapper implements DataMapperInterface
|
||||
{
|
||||
public function mapDataToForms($viewData, \Traversable $forms): void
|
||||
{
|
||||
if (null === $viewData) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var array<string, FormInterface> $form */
|
||||
$form = iterator_to_array($forms);
|
||||
|
||||
$form['center']->setData($viewData);
|
||||
|
||||
// NOTE: we do not map back the regroupments
|
||||
}
|
||||
|
||||
public function mapFormsToData(\Traversable $forms, &$viewData): void
|
||||
{
|
||||
/** @var array<string, FormInterface> $forms */
|
||||
$forms = iterator_to_array($forms);
|
||||
|
||||
$centers = [];
|
||||
|
||||
foreach ($forms['center']->getData() as $center) {
|
||||
$centers[spl_object_hash($center)] = $center;
|
||||
}
|
||||
|
||||
if (\array_key_exists('regroupment', $forms)) {
|
||||
/** @var Regroupment $regroupment */
|
||||
foreach ($forms['regroupment']->getData() as $regroupment) {
|
||||
foreach ($regroupment->getCenters() as $center) {
|
||||
$centers[spl_object_hash($center)] = $center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$viewData = array_values($centers);
|
||||
}
|
||||
}
|
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Form\DataMapper;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Symfony\Component\Form\DataMapperInterface;
|
||||
|
||||
final readonly class NotificationFlagDataMapper implements DataMapperInterface
|
||||
{
|
||||
public function __construct(private array $notificationFlagProviders) {}
|
||||
|
||||
public function mapDataToForms($viewData, $forms): void
|
||||
{
|
||||
if (null === $viewData) {
|
||||
$viewData = [];
|
||||
}
|
||||
|
||||
$formsArray = iterator_to_array($forms);
|
||||
|
||||
foreach ($this->notificationFlagProviders as $flagProvider) {
|
||||
$flag = $flagProvider->getFlag();
|
||||
|
||||
if (isset($formsArray[$flag])) {
|
||||
$flagForm = $formsArray[$flag];
|
||||
|
||||
$immediateEmailChecked = in_array(User::NOTIF_FLAG_IMMEDIATE_EMAIL, $viewData[$flag] ?? [], true)
|
||||
|| !array_key_exists($flag, $viewData);
|
||||
$dailyEmailChecked = in_array(User::NOTIF_FLAG_DAILY_DIGEST, $viewData[$flag] ?? [], true);
|
||||
|
||||
if ($flagForm->has('immediate_email')) {
|
||||
$flagForm->get('immediate_email')->setData($immediateEmailChecked);
|
||||
}
|
||||
if ($flagForm->has('daily_email')) {
|
||||
$flagForm->get('daily_email')->setData($dailyEmailChecked);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function mapFormsToData($forms, &$viewData): void
|
||||
{
|
||||
$formsArray = iterator_to_array($forms);
|
||||
$viewData = [];
|
||||
|
||||
foreach ($this->notificationFlagProviders as $flagProvider) {
|
||||
$flag = $flagProvider->getFlag();
|
||||
|
||||
if (isset($formsArray[$flag])) {
|
||||
$flagForm = $formsArray[$flag];
|
||||
$viewData[$flag] = [];
|
||||
|
||||
if (true === $flagForm['immediate_email']->getData()) {
|
||||
$viewData[$flag][] = User::NOTIF_FLAG_IMMEDIATE_EMAIL;
|
||||
}
|
||||
|
||||
if (true === $flagForm['daily_email']->getData()) {
|
||||
$viewData[$flag][] = User::NOTIF_FLAG_DAILY_DIGEST;
|
||||
}
|
||||
|
||||
if ([] === $viewData[$flag]) {
|
||||
$viewData[$flag][] = User::NOTIF_FLAG_IMMEDIATE_EMAIL;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -12,17 +12,12 @@ declare(strict_types=1);
|
||||
namespace Chill\MainBundle\Form;
|
||||
|
||||
use Chill\MainBundle\Entity\Notification;
|
||||
use Chill\MainBundle\Form\Type\ChillCollectionType;
|
||||
use Chill\MainBundle\Form\Type\ChillTextareaType;
|
||||
use Chill\MainBundle\Form\Type\PickUserDynamicType;
|
||||
use Chill\MainBundle\Form\Type\PickUserGroupOrUserDynamicType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Validator\Constraints\Email;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
use Symfony\Component\Validator\Constraints\NotNull;
|
||||
|
||||
class NotificationType extends AbstractType
|
||||
{
|
||||
@@ -33,29 +28,14 @@ class NotificationType extends AbstractType
|
||||
'label' => 'Title',
|
||||
'required' => true,
|
||||
])
|
||||
->add('addressees', PickUserDynamicType::class, [
|
||||
->add('addressees', PickUserGroupOrUserDynamicType::class, [
|
||||
'multiple' => true,
|
||||
'required' => false,
|
||||
'label' => 'notification.Pick user or user group',
|
||||
'empty_data' => '[]',
|
||||
'required' => true,
|
||||
])
|
||||
->add('message', ChillTextareaType::class, [
|
||||
'required' => false,
|
||||
])
|
||||
->add('addressesEmails', ChillCollectionType::class, [
|
||||
'label' => 'notification.dest by email',
|
||||
'help' => 'notification.dest by email help',
|
||||
'by_reference' => false,
|
||||
'allow_add' => true,
|
||||
'allow_delete' => true,
|
||||
'entry_type' => EmailType::class,
|
||||
'button_add_label' => 'notification.Add an email',
|
||||
'button_remove_label' => 'notification.Remove an email',
|
||||
'empty_collection_explain' => 'notification.Any email',
|
||||
'entry_options' => [
|
||||
'constraints' => [
|
||||
new NotNull(), new NotBlank(), new Email(),
|
||||
],
|
||||
'label' => 'Email',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
|
@@ -13,6 +13,9 @@ namespace Chill\MainBundle\Form;
|
||||
|
||||
use Chill\MainBundle\Entity\SavedExport;
|
||||
use Chill\MainBundle\Form\Type\ChillTextareaType;
|
||||
use Chill\MainBundle\Form\Type\PickUserGroupOrUserDynamicType;
|
||||
use Chill\MainBundle\Security\Authorization\SavedExportVoter;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
@@ -20,8 +23,12 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class SavedExportType extends AbstractType
|
||||
{
|
||||
public function __construct(private readonly Security $security) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$savedExport = $options['data'];
|
||||
|
||||
$builder
|
||||
->add('title', TextType::class, [
|
||||
'required' => true,
|
||||
@@ -29,6 +36,14 @@ class SavedExportType extends AbstractType
|
||||
->add('description', ChillTextareaType::class, [
|
||||
'required' => false,
|
||||
]);
|
||||
|
||||
if ($this->security->isGranted(SavedExportVoter::SHARE, $savedExport)) {
|
||||
$builder->add('share', PickUserGroupOrUserDynamicType::class, [
|
||||
'multiple' => true,
|
||||
'required' => false,
|
||||
'label' => 'saved_export.Share',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
|
@@ -26,7 +26,7 @@ class EntityToJsonTransformer implements DataTransformerInterface
|
||||
{
|
||||
public function __construct(private readonly DenormalizerInterface $denormalizer, private readonly SerializerInterface $serializer, private readonly bool $multiple, private readonly string $type) {}
|
||||
|
||||
public function reverseTransform($value): mixed
|
||||
public function reverseTransform($value)
|
||||
{
|
||||
if ('' === $value) {
|
||||
return $this->multiple ? [] : null;
|
||||
@@ -66,8 +66,11 @@ class EntityToJsonTransformer implements DataTransformerInterface
|
||||
]);
|
||||
}
|
||||
|
||||
private function denormalizeOne(array $item): mixed
|
||||
private function denormalizeOne(array|string $item)
|
||||
{
|
||||
if ('me' === $item) {
|
||||
return $item;
|
||||
}
|
||||
if (!\array_key_exists('type', $item)) {
|
||||
throw new TransformationFailedException('the key "type" is missing on element');
|
||||
}
|
||||
@@ -98,5 +101,6 @@ class EntityToJsonTransformer implements DataTransformerInterface
|
||||
'json',
|
||||
$context,
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -21,15 +21,18 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class AggregatorType extends AbstractType
|
||||
{
|
||||
public const ENABLED_FIELD = 'enabled';
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$exportManager = $options['export_manager'];
|
||||
$aggregator = $exportManager->getAggregator($options['aggregator_alias']);
|
||||
|
||||
$builder
|
||||
->add('enabled', CheckboxType::class, [
|
||||
->add(self::ENABLED_FIELD, CheckboxType::class, [
|
||||
'value' => true,
|
||||
'required' => false,
|
||||
'disabled' => $options['disable_enable_field'],
|
||||
]);
|
||||
|
||||
$aggregatorFormBuilder = $builder->create('form', FormType::class, [
|
||||
@@ -53,6 +56,7 @@ class AggregatorType extends AbstractType
|
||||
{
|
||||
$resolver->setRequired('aggregator_alias')
|
||||
->setRequired('export_manager')
|
||||
->setDefault('disable_enable_field', false)
|
||||
->setDefault('compound', true)
|
||||
->setDefault('error_bubbling', false);
|
||||
}
|
||||
|
@@ -35,7 +35,7 @@ class ExportType extends AbstractType
|
||||
public function __construct(
|
||||
private readonly ExportManager $exportManager,
|
||||
private readonly SortExportElement $sortExportElement,
|
||||
protected ParameterBagInterface $parameterBag,
|
||||
ParameterBagInterface $parameterBag,
|
||||
) {
|
||||
$this->personFieldsConfig = $parameterBag->get('chill_person.person_fields');
|
||||
}
|
||||
@@ -43,6 +43,8 @@ class ExportType extends AbstractType
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$export = $this->exportManager->getExport($options['export_alias']);
|
||||
/** @var bool $canEditFull */
|
||||
$canEditFull = $options['can_edit_full'];
|
||||
|
||||
$exportOptions = [
|
||||
'compound' => true,
|
||||
@@ -59,8 +61,18 @@ class ExportType extends AbstractType
|
||||
|
||||
if ($export instanceof \Chill\MainBundle\Export\ExportInterface) {
|
||||
// add filters
|
||||
$filters = $this->exportManager->getFiltersApplyingOn($export, $options['picked_centers']);
|
||||
$filterAliases = $options['allowed_filters'];
|
||||
$filters = [];
|
||||
if (is_iterable($filterAliases)) {
|
||||
foreach ($filterAliases as $alias => $filter) {
|
||||
$filters[$alias] = $filter;
|
||||
}
|
||||
} else {
|
||||
$filters = $this->exportManager->getFiltersApplyingOn($export, $options['picked_centers']);
|
||||
}
|
||||
|
||||
$this->sortExportElement->sortFilters($filters);
|
||||
|
||||
$filterBuilder = $builder->create(self::FILTER_KEY, FormType::class, ['compound' => true]);
|
||||
|
||||
foreach ($filters as $alias => $filter) {
|
||||
@@ -70,15 +82,26 @@ class ExportType extends AbstractType
|
||||
'constraints' => [
|
||||
new ExportElementConstraint(['element' => $filter]),
|
||||
],
|
||||
'disable_enable_field' => !$canEditFull,
|
||||
]);
|
||||
}
|
||||
|
||||
$builder->add($filterBuilder);
|
||||
|
||||
// add aggregators
|
||||
$aggregators = $this->exportManager
|
||||
->getAggregatorsApplyingOn($export, $options['picked_centers']);
|
||||
$aggregatorsAliases = $options['allowed_aggregators'];
|
||||
$aggregators = [];
|
||||
if (is_iterable($aggregatorsAliases)) {
|
||||
foreach ($aggregatorsAliases as $alias => $aggregator) {
|
||||
$aggregators[$alias] = $aggregator;
|
||||
}
|
||||
} else {
|
||||
$aggregators = $this->exportManager
|
||||
->getAggregatorsApplyingOn($export, $options['picked_centers']);
|
||||
}
|
||||
|
||||
$this->sortExportElement->sortAggregators($aggregators);
|
||||
|
||||
$aggregatorBuilder = $builder->create(
|
||||
self::AGGREGATOR_KEY,
|
||||
FormType::class,
|
||||
@@ -96,11 +119,11 @@ class ExportType extends AbstractType
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$aggregatorBuilder->add($alias, AggregatorType::class, [
|
||||
'aggregator_alias' => $alias,
|
||||
'export_manager' => $this->exportManager,
|
||||
'label' => $aggregator->getTitle(),
|
||||
'disable_enable_field' => !$canEditFull,
|
||||
'constraints' => [
|
||||
new ExportElementConstraint(['element' => $aggregator]),
|
||||
],
|
||||
@@ -125,8 +148,13 @@ class ExportType extends AbstractType
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setRequired(['export_alias', 'picked_centers'])
|
||||
$resolver->setRequired(['export_alias', 'picked_centers', 'can_edit_full'])
|
||||
->setAllowedTypes('export_alias', ['string'])
|
||||
->setAllowedValues('can_edit_full', [true, false])
|
||||
->setDefault('allowed_filters', null)
|
||||
->setAllowedTypes('allowed_filters', ['iterable', 'null'])
|
||||
->setDefault('allowed_aggregators', null)
|
||||
->setAllowedTypes('allowed_aggregators', ['iterable', 'null'])
|
||||
->setDefault('compound', true)
|
||||
->setDefault('constraints', [
|
||||
// new \Chill\MainBundle\Validator\Constraints\Export\ExportElementConstraint()
|
||||
|
@@ -34,6 +34,7 @@ class FilterType extends AbstractType
|
||||
->add(self::ENABLED_FIELD, CheckboxType::class, [
|
||||
'value' => true,
|
||||
'required' => false,
|
||||
'disabled' => $options['disable_enable_field'],
|
||||
]);
|
||||
|
||||
$filterFormBuilder = $builder->create('form', FormType::class, [
|
||||
@@ -58,6 +59,7 @@ class FilterType extends AbstractType
|
||||
$resolver
|
||||
->setRequired('filter')
|
||||
->setAllowedTypes('filter', [FilterInterface::class])
|
||||
->setDefault('disable_enable_field', false)
|
||||
->setDefault('compound', true)
|
||||
->setDefault('error_bubbling', false);
|
||||
}
|
||||
|
@@ -14,9 +14,9 @@ namespace Chill\MainBundle\Form\Type\Export;
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Entity\Regroupment;
|
||||
use Chill\MainBundle\Export\ExportManager;
|
||||
use Chill\MainBundle\Form\DataMapper\ExportPickCenterDataMapper;
|
||||
use Chill\MainBundle\Repository\RegroupmentRepository;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface;
|
||||
use Chill\MainBundle\Service\Regroupement\RegroupementFiltering;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
@@ -27,27 +27,26 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
*/
|
||||
final class PickCenterType extends AbstractType
|
||||
{
|
||||
public const CENTERS_IDENTIFIERS = 'c';
|
||||
|
||||
public function __construct(
|
||||
private readonly ExportManager $exportManager,
|
||||
private readonly RegroupmentRepository $regroupmentRepository,
|
||||
private readonly AuthorizationHelperForCurrentUserInterface $authorizationHelper,
|
||||
private readonly RegroupementFiltering $regroupementFiltering,
|
||||
) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$export = $this->exportManager->getExport($options['export_alias']);
|
||||
$centers = $this->authorizationHelper->getReachableCenters(
|
||||
$export->requiredRole()
|
||||
$export->requiredRole(),
|
||||
);
|
||||
|
||||
$centersActive = array_filter($centers, fn (Center $c) => $c->getIsActive());
|
||||
|
||||
// order alphabetically
|
||||
usort($centersActive, fn (Center $a, Center $b) => $a->getCenter() <=> $b->getName());
|
||||
usort($centersActive, fn (Center $a, Center $b) => $a->getName() <=> $b->getName());
|
||||
|
||||
$builder->add('center', EntityType::class, [
|
||||
$builder->add('centers', EntityType::class, [
|
||||
'class' => Center::class,
|
||||
'choices' => $centersActive,
|
||||
'label' => 'center',
|
||||
@@ -56,18 +55,22 @@ final class PickCenterType extends AbstractType
|
||||
'choice_label' => static fn (Center $c) => $c->getName(),
|
||||
]);
|
||||
|
||||
if (\count($this->regroupmentRepository->findAllActive()) > 0) {
|
||||
$builder->add('regroupment', EntityType::class, [
|
||||
$groups = $this->regroupementFiltering
|
||||
->filterContainsAtLeastOneCenter($this->regroupmentRepository->findAllActive(), $centersActive);
|
||||
|
||||
// order alphabetically
|
||||
usort($groups, fn (Regroupment $a, Regroupment $b) => $a->getName() <=> $b->getName());
|
||||
|
||||
if (\count($groups) > 0) {
|
||||
$builder->add('regroupments', EntityType::class, [
|
||||
'class' => Regroupment::class,
|
||||
'label' => 'regroupment',
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
'choices' => $this->regroupmentRepository->findAllActive(),
|
||||
'choices' => $groups,
|
||||
'choice_label' => static fn (Regroupment $r) => $r->getName(),
|
||||
]);
|
||||
}
|
||||
|
||||
$builder->setDataMapper(new ExportPickCenterDataMapper());
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
|
@@ -0,0 +1,63 @@
|
||||
<?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\NotificationFlagDataMapper;
|
||||
use Chill\MainBundle\Notification\NotificationFlagManager;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class NotificationFlagsType extends AbstractType
|
||||
{
|
||||
private readonly array $notificationFlagProviders;
|
||||
|
||||
public function __construct(NotificationFlagManager $notificationFlagManager)
|
||||
{
|
||||
$this->notificationFlagProviders = $notificationFlagManager->getAllNotificationFlagProviders();
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->setDataMapper(new NotificationFlagDataMapper($this->notificationFlagProviders));
|
||||
|
||||
foreach ($this->notificationFlagProviders as $flagProvider) {
|
||||
$flag = $flagProvider->getFlag();
|
||||
$builder->add($flag, FormType::class, [
|
||||
'label' => $flagProvider->getLabel(),
|
||||
'required' => false,
|
||||
]);
|
||||
|
||||
$builder->get($flag)
|
||||
->add('immediate_email', CheckboxType::class, [
|
||||
'label' => false,
|
||||
'required' => false,
|
||||
'mapped' => false,
|
||||
])
|
||||
->add('daily_email', CheckboxType::class, [
|
||||
'label' => false,
|
||||
'required' => false,
|
||||
'mapped' => false,
|
||||
])
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => null,
|
||||
]);
|
||||
}
|
||||
}
|
@@ -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\Form\Type;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Form\Type\DataTransformer\EntityToJsonTransformer;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
/**
|
||||
* Pick user dymically, using vuejs module "AddPerson".
|
||||
*
|
||||
* Possible options:
|
||||
*
|
||||
* - `multiple`: pick one or more users
|
||||
* - `suggested`: a list of suggested users
|
||||
* - `suggest_myself`: append the current user to the list of suggested
|
||||
* - `as_id`: only the id will be set in the returned data
|
||||
* - `submit_on_adding_new_entity`: the browser will immediately submit the form when new users are checked
|
||||
*/
|
||||
class PickUserOrMeDynamicType extends AbstractType
|
||||
{
|
||||
public function __construct(
|
||||
private readonly DenormalizerInterface $denormalizer,
|
||||
private readonly SerializerInterface $serializer,
|
||||
private readonly NormalizerInterface $normalizer,
|
||||
) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder->addViewTransformer(new EntityToJsonTransformer($this->denormalizer, $this->serializer, $options['multiple'], 'user'));
|
||||
}
|
||||
|
||||
public function buildView(FormView $view, FormInterface $form, array $options)
|
||||
{
|
||||
$view->vars['multiple'] = $options['multiple'];
|
||||
$view->vars['types'] = ['user'];
|
||||
$view->vars['uniqid'] = uniqid('pick_user_or_me_dyn');
|
||||
$view->vars['suggested'] = [];
|
||||
$view->vars['as_id'] = true === $options['as_id'] ? '1' : '0';
|
||||
$view->vars['submit_on_adding_new_entity'] = true === $options['submit_on_adding_new_entity'] ? '1' : '0';
|
||||
|
||||
foreach ($options['suggested'] as $user) {
|
||||
$view->vars['suggested'][] = $this->normalizer->normalize($user, 'json', ['groups' => 'read']);
|
||||
}
|
||||
// $user = /* should come from context */ $options['context'];
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver
|
||||
->setDefault('multiple', false)
|
||||
->setAllowedTypes('multiple', ['bool'])
|
||||
->setDefault('compound', false)
|
||||
->setDefault('suggested', [])
|
||||
// if set to true, only the id will be set inside the content. The denormalization will not work.
|
||||
->setDefault('as_id', false)
|
||||
->setAllowedTypes('as_id', ['bool'])
|
||||
->setDefault('submit_on_adding_new_entity', false)
|
||||
->setAllowedTypes('submit_on_adding_new_entity', ['bool']);
|
||||
}
|
||||
|
||||
public function getBlockPrefix()
|
||||
{
|
||||
return 'pick_entity_dynamic';
|
||||
}
|
||||
}
|
@@ -11,6 +11,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Form;
|
||||
|
||||
use Chill\MainBundle\Entity\UserGroup;
|
||||
use Chill\MainBundle\Form\Type\PickUserDynamicType;
|
||||
use Chill\MainBundle\Form\Type\TranslatableStringFormType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
@@ -23,6 +24,9 @@ class UserGroupType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
/** @var UserGroup $userGroup */
|
||||
$userGroup = $options['data'];
|
||||
|
||||
$builder
|
||||
->add('label', TranslatableStringFormType::class, [
|
||||
'label' => 'user_group.Label',
|
||||
@@ -46,20 +50,25 @@ class UserGroupType extends AbstractType
|
||||
'help' => 'user_group.ExcludeKeyHelp',
|
||||
'required' => false,
|
||||
'empty_data' => '',
|
||||
])
|
||||
->add('users', PickUserDynamicType::class, [
|
||||
'label' => 'user_group.Users',
|
||||
'multiple' => true,
|
||||
'required' => false,
|
||||
'empty_data' => [],
|
||||
])
|
||||
->add('adminUsers', PickUserDynamicType::class, [
|
||||
'label' => 'user_group.adminUsers',
|
||||
'multiple' => true,
|
||||
'required' => false,
|
||||
'empty_data' => [],
|
||||
'help' => 'user_group.adminUsersHelp',
|
||||
])
|
||||
;
|
||||
]);
|
||||
|
||||
if (!$userGroup->hasUserJob()) {
|
||||
$builder
|
||||
->add('users', PickUserDynamicType::class, [
|
||||
'label' => 'user_group.Users',
|
||||
'multiple' => true,
|
||||
'required' => false,
|
||||
'empty_data' => [],
|
||||
])
|
||||
->add('adminUsers', PickUserDynamicType::class, [
|
||||
'label' => 'user_group.adminUsers',
|
||||
'multiple' => true,
|
||||
'required' => false,
|
||||
'empty_data' => [],
|
||||
'help' => 'user_group.adminUsersHelp',
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
41
src/Bundle/ChillMainBundle/Form/UserProfileType.php
Normal file
41
src/Bundle/ChillMainBundle/Form/UserProfileType.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?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\Form\Type\ChillPhoneNumberType;
|
||||
use Chill\MainBundle\Form\Type\NotificationFlagsType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class UserProfileType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->add('phonenumber', ChillPhoneNumberType::class, [
|
||||
'required' => false,
|
||||
])
|
||||
->add('notificationFlags', NotificationFlagsType::class, [
|
||||
'label' => false,
|
||||
'mapped' => false,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => \Chill\MainBundle\Entity\User::class,
|
||||
]);
|
||||
}
|
||||
}
|
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Notification\Email;
|
||||
|
||||
use Chill\MainBundle\Cron\CronJobInterface;
|
||||
use Chill\MainBundle\Entity\CronJobExecution;
|
||||
use Chill\MainBundle\Notification\Email\NotificationEmailMessages\ScheduleDailyNotificationDigestMessage;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Exception;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
|
||||
readonly class DailyNotificationDigestCronjob implements CronJobInterface
|
||||
{
|
||||
public function __construct(
|
||||
private ClockInterface $clock,
|
||||
private Connection $connection,
|
||||
private MessageBusInterface $messageBus,
|
||||
private LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
public function canRun(?CronJobExecution $cronJobExecution): bool
|
||||
{
|
||||
$now = $this->clock->now();
|
||||
|
||||
if (null !== $cronJobExecution && $now->sub(new \DateInterval('PT23H45M')) < $cronJobExecution->getLastStart()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Run between 6 and 9 AM
|
||||
return in_array((int) $now->format('H'), [6, 7, 8], true);
|
||||
}
|
||||
|
||||
public function getKey(): string
|
||||
{
|
||||
return 'daily-notification-digest';
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \DateInvalidOperationException
|
||||
* @throws Exception
|
||||
*/
|
||||
public function run(array $lastExecutionData): ?array
|
||||
{
|
||||
$now = $this->clock->now();
|
||||
if (isset($lastExecutionData['last_execution'])) {
|
||||
$lastExecution = \DateTimeImmutable::createFromFormat(
|
||||
\DateTimeImmutable::ATOM,
|
||||
$lastExecutionData['last_execution']
|
||||
);
|
||||
} else {
|
||||
$lastExecution = $now->sub(new \DateInterval('P1D'));
|
||||
}
|
||||
|
||||
// Get distinct users who received notifications since the last execution
|
||||
$sql = <<<'SQL'
|
||||
SELECT DISTINCT cmnau.user_id
|
||||
FROM chill_main_notification cmn
|
||||
JOIN chill_main_notification_addresses_user cmnau ON cmnau.notification_id = cmn.id
|
||||
WHERE cmn.date >= :lastExecution AND cmn.date <= :now
|
||||
SQL;
|
||||
|
||||
$sqlStatement = $this->connection->prepare($sql);
|
||||
$sqlStatement->bindValue('lastExecution', $lastExecution->format(\DateTimeInterface::RFC3339));
|
||||
$sqlStatement->bindValue('now', $now->format(\DateTimeInterface::RFC3339));
|
||||
$result = $sqlStatement->executeQuery();
|
||||
|
||||
$count = 0;
|
||||
foreach ($result->fetchAllAssociative() as $row) {
|
||||
$userId = (int) $row['user_id'];
|
||||
|
||||
$message = new ScheduleDailyNotificationDigestMessage(
|
||||
$userId,
|
||||
$lastExecution,
|
||||
$now
|
||||
);
|
||||
|
||||
$this->messageBus->dispatch($message);
|
||||
++$count;
|
||||
}
|
||||
|
||||
$this->logger->info('[DailyNotificationDigestCronjob] Dispatched daily digest messages', [
|
||||
'user_count' => $count,
|
||||
'last_execution' => $lastExecution->format('Y-m-d-H:i:s.u e'),
|
||||
'current_time' => $now->format('Y-m-d-H:i:s.u e'),
|
||||
]);
|
||||
|
||||
return [
|
||||
'last_execution' => $now->format('Y-m-d-H:i:s.u e'),
|
||||
];
|
||||
}
|
||||
}
|
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Notification\Email\NotificationEmailHandlers;
|
||||
|
||||
use Chill\MainBundle\Notification\Email\NotificationEmailMessages\ScheduleDailyNotificationDigestMessage;
|
||||
use Chill\MainBundle\Notification\Email\NotificationMailer;
|
||||
use Chill\MainBundle\Repository\NotificationRepository;
|
||||
use Chill\MainBundle\Repository\UserRepository;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
|
||||
#[AsMessageHandler]
|
||||
readonly class ScheduleDailyNotificationDigestHandler
|
||||
{
|
||||
public function __construct(
|
||||
private NotificationRepository $notificationRepository,
|
||||
private UserRepository $userRepository,
|
||||
private NotificationMailer $notificationMailer,
|
||||
private LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws TransportExceptionInterface
|
||||
*/
|
||||
public function __invoke(ScheduleDailyNotificationDigestMessage $message): void
|
||||
{
|
||||
$userId = $message->getUserId();
|
||||
$lastExecutionDate = $message->getLastExecutionDateTime();
|
||||
$currentDate = $message->getCurrentDateTime();
|
||||
|
||||
$user = $this->userRepository->find($userId);
|
||||
if (null === $user) {
|
||||
$this->logger->warning('[ScheduleDailyNotificationDigestHandler] User not found', [
|
||||
'user_id' => $userId,
|
||||
]);
|
||||
|
||||
throw new \InvalidArgumentException(sprintf('User with ID %s not found', $userId));
|
||||
}
|
||||
|
||||
// Get all notifications for this user between last execution and current date
|
||||
$notifications = $this->notificationRepository->findNotificationsForUserBetweenDates(
|
||||
$userId,
|
||||
$lastExecutionDate,
|
||||
$currentDate
|
||||
);
|
||||
|
||||
// Filter out notifications that should be sent in a daily digest
|
||||
$dailyNotifications = array_filter($notifications, fn ($notification) => $user->isNotificationDailyDigest($notification->getType()));
|
||||
|
||||
if ([] === $dailyNotifications) {
|
||||
$this->logger->info('[ScheduleDailyNotificationDigestHandler] No daily notifications found for user', [
|
||||
'user_id' => $userId,
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->notificationMailer->sendDailyDigest($user, $dailyNotifications);
|
||||
|
||||
$this->logger->info('[ScheduleDailyNotificationDigestHandler] Sent daily digest', [
|
||||
'user_id' => $userId,
|
||||
'notification_count' => count($dailyNotifications),
|
||||
]);
|
||||
}
|
||||
}
|
@@ -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\MainBundle\Notification\Email\NotificationEmailHandlers;
|
||||
|
||||
use Chill\MainBundle\Notification\Email\NotificationEmailMessages\SendImmediateNotificationEmailMessage;
|
||||
use Chill\MainBundle\Notification\Email\NotificationMailer;
|
||||
use Chill\MainBundle\Repository\NotificationRepository;
|
||||
use Chill\MainBundle\Repository\UserRepository;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
|
||||
#[AsMessageHandler]
|
||||
readonly class SendImmediateNotificationEmailHandler
|
||||
{
|
||||
public function __construct(
|
||||
private NotificationRepository $notificationRepository,
|
||||
private UserRepository $userRepository,
|
||||
private NotificationMailer $notificationMailer,
|
||||
private LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws TransportExceptionInterface
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __invoke(SendImmediateNotificationEmailMessage $message): void
|
||||
{
|
||||
$notification = $this->notificationRepository->find($message->getNotificationId());
|
||||
$addressee = $this->userRepository->find($message->getAddresseeId());
|
||||
|
||||
if (null === $notification) {
|
||||
$this->logger->error('[SendImmediateNotificationEmailHandler] Notification not found', [
|
||||
'notification_id' => $message->getNotificationId(),
|
||||
]);
|
||||
|
||||
throw new \InvalidArgumentException(sprintf('Notification with ID %s not found', $message->getNotificationId()));
|
||||
}
|
||||
|
||||
if (null === $addressee) {
|
||||
$this->logger->error('[SendImmediateNotificationEmailHandler] Addressee not found', [
|
||||
'addressee_id' => $message->getAddresseeId(),
|
||||
]);
|
||||
|
||||
throw new \InvalidArgumentException(sprintf('User with ID %s not found', $message->getAddresseeId()));
|
||||
}
|
||||
|
||||
try {
|
||||
$this->notificationMailer->sendEmailToAddressee($notification, $addressee);
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error('[SendImmediateNotificationEmailHandler] Failed to send email', [
|
||||
'notification_id' => $message->getNotificationId(),
|
||||
'addressee_id' => $message->getAddresseeId(),
|
||||
'stacktrace' => $e->getTraceAsString(),
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,36 @@
|
||||
<?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\Notification\Email\NotificationEmailMessages;
|
||||
|
||||
readonly class ScheduleDailyNotificationDigestMessage
|
||||
{
|
||||
public function __construct(
|
||||
private int $userId,
|
||||
private \DateTimeInterface $lastExecutionDate,
|
||||
private \DateTimeInterface $currentDate,
|
||||
) {}
|
||||
|
||||
public function getUserId(): int
|
||||
{
|
||||
return $this->userId;
|
||||
}
|
||||
|
||||
public function getLastExecutionDateTime(): \DateTimeInterface
|
||||
{
|
||||
return $this->lastExecutionDate;
|
||||
}
|
||||
|
||||
public function getCurrentDateTime(): \DateTimeInterface
|
||||
{
|
||||
return $this->currentDate;
|
||||
}
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
<?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\Notification\Email\NotificationEmailMessages;
|
||||
|
||||
readonly class SendImmediateNotificationEmailMessage
|
||||
{
|
||||
public function __construct(
|
||||
private int $notificationId,
|
||||
private int $addresseeId,
|
||||
) {}
|
||||
|
||||
public function getNotificationId(): int
|
||||
{
|
||||
return $this->notificationId;
|
||||
}
|
||||
|
||||
public function getAddresseeId(): int
|
||||
{
|
||||
return $this->addresseeId;
|
||||
}
|
||||
}
|
@@ -13,22 +13,32 @@ namespace Chill\MainBundle\Notification\Email;
|
||||
|
||||
use Chill\MainBundle\Entity\Notification;
|
||||
use Chill\MainBundle\Entity\NotificationComment;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Notification\Email\NotificationEmailMessages\SendImmediateNotificationEmailMessage;
|
||||
use Doctrine\ORM\Event\PostPersistEventArgs;
|
||||
use Doctrine\ORM\Event\PostUpdateEventArgs;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||
use Symfony\Component\Mailer\MailerInterface;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class NotificationMailer
|
||||
readonly class NotificationMailer
|
||||
{
|
||||
public function __construct(private readonly MailerInterface $mailer, private readonly LoggerInterface $logger, private readonly TranslatorInterface $translator) {}
|
||||
public function __construct(
|
||||
private MailerInterface $mailer,
|
||||
private LoggerInterface $logger,
|
||||
private MessageBusInterface $messageBus,
|
||||
private TranslatorInterface $translator,
|
||||
) {}
|
||||
|
||||
public function postPersistComment(NotificationComment $comment, PostPersistEventArgs $eventArgs): void
|
||||
{
|
||||
$dests = [$comment->getNotification()->getSender(), ...$comment->getNotification()->getAddressees()->toArray()];
|
||||
$dests = [
|
||||
$comment->getNotification()->getSender(),
|
||||
...$comment->getNotification()->getAddressees()->toArray(),
|
||||
];
|
||||
|
||||
$uniqueDests = [];
|
||||
foreach ($dests as $dest) {
|
||||
@@ -69,55 +79,147 @@ class NotificationMailer
|
||||
*/
|
||||
public function postPersistNotification(Notification $notification, PostPersistEventArgs $eventArgs): void
|
||||
{
|
||||
$this->sendNotificationEmailsToAddresses($notification);
|
||||
$this->sendNotificationEmailsToAddressees($notification);
|
||||
$this->sendNotificationEmailsToAddressesEmails($notification);
|
||||
}
|
||||
|
||||
public function postUpdateNotification(Notification $notification, PostUpdateEventArgs $eventArgs): void
|
||||
private function sendNotificationEmailsToAddressees(Notification $notification): void
|
||||
{
|
||||
$this->sendNotificationEmailsToAddressesEmails($notification);
|
||||
}
|
||||
if ('' === $notification->getType()) {
|
||||
$this->logger->warning('[NotificationMailer] Notification has no type, skipping email processing', [
|
||||
'notification_id' => $notification->getId(),
|
||||
]);
|
||||
|
||||
private function sendNotificationEmailsToAddresses(Notification $notification): void
|
||||
{
|
||||
foreach ($notification->getAddressees() as $addressee) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($notification->getAllAddressees() as $addressee) {
|
||||
if (null === $addressee->getEmail()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($notification->isSystem()) {
|
||||
$email = new Email();
|
||||
$email
|
||||
->text($notification->getMessage());
|
||||
} else {
|
||||
$email = new TemplatedEmail();
|
||||
$email
|
||||
->textTemplate('@ChillMain/Notification/email_non_system_notification_content.fr.md.twig')
|
||||
->context([
|
||||
'notification' => $notification,
|
||||
'dest' => $addressee,
|
||||
]);
|
||||
}
|
||||
$this->processNotificationForAddressee($notification, $addressee);
|
||||
}
|
||||
}
|
||||
|
||||
private function processNotificationForAddressee(Notification $notification, User $addressee): void
|
||||
{
|
||||
$notificationType = $notification->getType();
|
||||
|
||||
if ($addressee->isNotificationSendImmediately($notificationType)) {
|
||||
$this->scheduleImmediateEmail($notification, $addressee);
|
||||
}
|
||||
}
|
||||
|
||||
private function scheduleImmediateEmail(Notification $notification, User $addressee): void
|
||||
{
|
||||
$message = new SendImmediateNotificationEmailMessage(
|
||||
$notification->getId(),
|
||||
$addressee->getId()
|
||||
);
|
||||
|
||||
$this->messageBus->dispatch($message);
|
||||
|
||||
$this->logger->info('[NotificationMailer] Scheduled immediate email', [
|
||||
'notification_id' => $notification->getId(),
|
||||
'addressee_email' => $addressee->getEmail(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method sends the email but is now called by the immediate notification email message handler.
|
||||
*
|
||||
* @throws TransportExceptionInterface
|
||||
*/
|
||||
public function sendEmailToAddressee(Notification $notification, User $addressee): void
|
||||
{
|
||||
if (null === $addressee->getEmail()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($notification->isSystem()) {
|
||||
$email = new Email();
|
||||
$email->text($notification->getMessage());
|
||||
} else {
|
||||
$email = new TemplatedEmail();
|
||||
$email
|
||||
->subject($notification->getTitle())
|
||||
->to($addressee->getEmail());
|
||||
|
||||
try {
|
||||
$this->mailer->send($email);
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
$this->logger->warning('[NotificationMailer] could not send an email notification', [
|
||||
'to' => $addressee->getEmail(),
|
||||
'error_message' => $e->getMessage(),
|
||||
'error_trace' => $e->getTraceAsString(),
|
||||
->textTemplate('@ChillMain/Notification/email_non_system_notification_content.fr.md.twig')
|
||||
->context([
|
||||
'notification' => $notification,
|
||||
'dest' => $addressee,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$email
|
||||
->subject($notification->getTitle())
|
||||
->to($addressee->getEmail());
|
||||
|
||||
try {
|
||||
$this->mailer->send($email);
|
||||
$this->logger->info('[NotificationMailer] Email sent successfully', [
|
||||
'notification_id' => $notification->getId(),
|
||||
'addressee_email' => $addressee->getEmail(),
|
||||
]);
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
$this->logger->warning('[NotificationMailer] Could not send an email notification', [
|
||||
'to' => $addressee->getEmail(),
|
||||
'notification_id' => $notification->getId(),
|
||||
'error_message' => $e->getMessage(),
|
||||
'error_trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send daily digest email with multiple notifications to a user.
|
||||
*
|
||||
* @throws TransportExceptionInterface
|
||||
*/
|
||||
public function sendDailyDigest(User $user, array $notifications): void
|
||||
{
|
||||
if (null === $user->getEmail() || [] === $notifications) {
|
||||
return;
|
||||
}
|
||||
|
||||
$email = new TemplatedEmail();
|
||||
$email
|
||||
->htmlTemplate('@ChillMain/Notification/email_daily_digest.fr.md.twig')
|
||||
->context([
|
||||
'user' => $user,
|
||||
'notifications' => $notifications,
|
||||
'notification_count' => count($notifications),
|
||||
])
|
||||
->subject($this->translator->trans('notification.Daily Notification Digest'))
|
||||
->to($user->getEmail());
|
||||
|
||||
try {
|
||||
$this->mailer->send($email);
|
||||
$this->logger->info('[NotificationMailer] Daily digest email sent successfully', [
|
||||
'user_email' => $user->getEmail(),
|
||||
'notification_count' => count($notifications),
|
||||
]);
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
$this->logger->warning('[NotificationMailer] Could not send daily digest email', [
|
||||
'to' => $user->getEmail(),
|
||||
'notification_count' => count($notifications),
|
||||
'error_message' => $e->getMessage(),
|
||||
'error_trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function sendNotificationEmailsToAddressesEmails(Notification $notification): void
|
||||
{
|
||||
foreach ($notification->getAddressesEmailsAdded() as $emailAddress) {
|
||||
foreach ($notification->getAddresseeUserGroups() as $userGroup) {
|
||||
|
||||
if (!$userGroup->hasEmail()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$emailAddress = $userGroup->getEmail();
|
||||
|
||||
$email = new TemplatedEmail();
|
||||
$email
|
||||
->textTemplate('@ChillMain/Notification/email_non_system_notification_content_to_email.fr.md.twig')
|
||||
|
@@ -0,0 +1,30 @@
|
||||
<?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\Notification\FlagProviders;
|
||||
|
||||
use Symfony\Component\Translation\TranslatableMessage;
|
||||
use Symfony\Contracts\Translation\TranslatableInterface;
|
||||
|
||||
class NotificationByUserFlagProvider implements NotificationFlagProviderInterface
|
||||
{
|
||||
public const FLAG = 'notif-by-user';
|
||||
|
||||
public function getFlag(): string
|
||||
{
|
||||
return self::FLAG;
|
||||
}
|
||||
|
||||
public function getLabel(): TranslatableInterface
|
||||
{
|
||||
return new TranslatableMessage('notification.flags.by-user');
|
||||
}
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
<?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\Notification\FlagProviders;
|
||||
|
||||
use Symfony\Contracts\Translation\TranslatableInterface;
|
||||
|
||||
interface NotificationFlagProviderInterface
|
||||
{
|
||||
public function getFlag(): string;
|
||||
|
||||
public function getLabel(): TranslatableInterface;
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
<?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\Notification\FlagProviders;
|
||||
|
||||
use Symfony\Component\Translation\TranslatableMessage;
|
||||
use Symfony\Contracts\Translation\TranslatableInterface;
|
||||
|
||||
class WorkflowTransitionNotificationFlagProvider implements NotificationFlagProviderInterface
|
||||
{
|
||||
public const FLAG = 'workflow-trans-notif';
|
||||
|
||||
public function getFlag(): string
|
||||
{
|
||||
return self::FLAG;
|
||||
}
|
||||
|
||||
public function getLabel(): TranslatableInterface
|
||||
{
|
||||
return new TranslatableMessage('notification.flags.workflow-trans');
|
||||
}
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
<?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\Notification;
|
||||
|
||||
use Chill\MainBundle\Notification\FlagProviders\NotificationFlagProviderInterface;
|
||||
|
||||
final readonly class NotificationFlagManager
|
||||
{
|
||||
/**
|
||||
* @var array<NotificationFlagProviderInterface>
|
||||
*/
|
||||
private array $notificationFlagProviders;
|
||||
|
||||
public function __construct(
|
||||
iterable $notificationFlagProviders,
|
||||
) {
|
||||
$this->notificationFlagProviders = iterator_to_array($notificationFlagProviders);
|
||||
}
|
||||
|
||||
public function getAllNotificationFlagProviders(): array
|
||||
{
|
||||
return $this->notificationFlagProviders;
|
||||
}
|
||||
}
|
@@ -0,0 +1,85 @@
|
||||
<?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\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||
use Chill\MainBundle\Entity\ExportGeneration;
|
||||
use Chill\MainBundle\Entity\SavedExport;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<ExportGeneration>
|
||||
*
|
||||
* @implements AssociatedEntityToStoredObjectInterface<ExportGeneration>
|
||||
*/
|
||||
class ExportGenerationRepository extends ServiceEntityRepository implements AssociatedEntityToStoredObjectInterface
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, ExportGeneration::class);
|
||||
}
|
||||
|
||||
public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?ExportGeneration
|
||||
{
|
||||
return $this->createQueryBuilder('e')
|
||||
->where('e.storedObject = :storedObject')
|
||||
->setParameter('storedObject', $storedObject)
|
||||
->getQuery()
|
||||
->getOneOrNullResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<ExportGeneration>
|
||||
*/
|
||||
public function findExportGenerationByAliasAndUser(string $alias, User $user, int $limit = 100, int $offset = 0): array
|
||||
{
|
||||
return $this->createQueryBuilder('e')
|
||||
->where('e.createdBy = :user')
|
||||
->andWhere('e.exportAlias LIKE :alias')
|
||||
->orderBy('e.createdAt', 'DESC')
|
||||
->setParameter('user', $user)
|
||||
->setParameter('alias', $alias)
|
||||
->setFirstResult($offset)
|
||||
->setMaxResults($limit)
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<ExportGeneration>
|
||||
*/
|
||||
public function findExportGenerationBySavedExportAndUser(SavedExport $savedExport, User $user, int $limit = 100, int $offset = 0): array
|
||||
{
|
||||
return $this->createQueryBuilder('e')
|
||||
->where('e.createdBy = :user')
|
||||
->andWhere('e.savedExport = :savedExport')
|
||||
->orderBy('e.createdAt', 'DESC')
|
||||
->setParameter('user', $user)
|
||||
->setParameter('savedExport', $savedExport)
|
||||
->setFirstResult($offset)
|
||||
->setMaxResults($limit)
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
|
||||
public function findExpiredExportGeneration(\DateTimeImmutable $atDate): iterable
|
||||
{
|
||||
return $this->createQueryBuilder('e')
|
||||
->where('e.deleteAt < :atDate')
|
||||
->setParameter('atDate', $atDate)
|
||||
->getQuery()
|
||||
->toIterable();
|
||||
}
|
||||
}
|
@@ -13,6 +13,7 @@ namespace Chill\MainBundle\Repository;
|
||||
|
||||
use Chill\MainBundle\Entity\Address;
|
||||
use Chill\MainBundle\Entity\GeographicalUnit;
|
||||
use Chill\MainBundle\Entity\GeographicalUnit\SimpleGeographicalUnitDTO;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
@@ -42,7 +43,7 @@ final readonly class GeographicalUnitRepository implements GeographicalUnitRepos
|
||||
$qb = $this->buildQueryGeographicalUnitContainingAddress($address);
|
||||
|
||||
return $qb
|
||||
->select(sprintf('NEW %s(gu.id, gu.unitName, gu.unitRefId, IDENTITY(gu.layer))', GeographicalUnit\SimpleGeographicalUnitDTO::class))
|
||||
->select(sprintf('NEW %s(gu.id, gu.unitName, gu.unitRefId, IDENTITY(gu.layer))', SimpleGeographicalUnitDTO::class))
|
||||
->addOrderBy('IDENTITY(gu.layer)')
|
||||
->addOrderBy('gu.unitName')
|
||||
->getQuery()
|
||||
@@ -58,7 +59,7 @@ final readonly class GeographicalUnitRepository implements GeographicalUnitRepos
|
||||
;
|
||||
|
||||
return $qb
|
||||
->select(sprintf('NEW %s(gu.id, gu.unitName, gu.unitRefId, IDENTITY(gu.layer))', GeographicalUnit\SimpleGeographicalUnitDTO::class))
|
||||
->select(sprintf('NEW %s(gu.id, gu.unitName, gu.unitRefId, IDENTITY(gu.layer))', SimpleGeographicalUnitDTO::class))
|
||||
->innerJoin(Address::class, 'address', Join::WITH, 'ST_CONTAINS(gu.geom, address.point) = TRUE')
|
||||
->where($qb->expr()->eq('address', ':address'))
|
||||
->setParameter('address', $address)
|
||||
@@ -70,6 +71,19 @@ final readonly class GeographicalUnitRepository implements GeographicalUnitRepos
|
||||
return $this->repository->find($id);
|
||||
}
|
||||
|
||||
public function findSimpleGeographicalUnit(int $id): ?SimpleGeographicalUnitDTO
|
||||
{
|
||||
$qb = $this->repository
|
||||
->createQueryBuilder('gu');
|
||||
|
||||
return $qb
|
||||
->select(sprintf('NEW %s(gu.id, gu.unitName, gu.unitRefId, IDENTITY(gu.layer))', SimpleGeographicalUnitDTO::class))
|
||||
->where('gu.id = :id')
|
||||
->setParameter('id', $id)
|
||||
->getQuery()
|
||||
->getOneOrNullResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Will return only partial object, where the @see{GeographicalUnit::geom} property is not loaded.
|
||||
*
|
||||
@@ -79,7 +93,7 @@ final readonly class GeographicalUnitRepository implements GeographicalUnitRepos
|
||||
{
|
||||
return $this->repository
|
||||
->createQueryBuilder('gu')
|
||||
->select(sprintf('NEW %s(gu.id, gu.unitName, gu.unitRefId, IDENTITY(gu.layer))', GeographicalUnit\SimpleGeographicalUnitDTO::class))
|
||||
->select(sprintf('NEW %s(gu.id, gu.unitName, gu.unitRefId, IDENTITY(gu.layer))', SimpleGeographicalUnitDTO::class))
|
||||
->addOrderBy('IDENTITY(gu.layer)')
|
||||
->addOrderBy('gu.unitName')
|
||||
->getQuery()
|
||||
|
@@ -27,4 +27,6 @@ interface GeographicalUnitRepositoryInterface extends ObjectRepository
|
||||
public function findGeographicalUnitContainingAddress(Address $address, int $offset = 0, int $limit = 50): array;
|
||||
|
||||
public function countGeographicalUnitContainingAddress(Address $address): int;
|
||||
|
||||
public function findSimpleGeographicalUnit(int $id): ?SimpleGeographicalUnitDTO;
|
||||
}
|
||||
|
@@ -290,12 +290,19 @@ final class NotificationRepository implements ObjectRepository
|
||||
return $qb;
|
||||
}
|
||||
|
||||
private function queryByAddressee(User $addressee, bool $countQuery = false): QueryBuilder
|
||||
private function queryByAddressee(User $addressee): QueryBuilder
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('n');
|
||||
|
||||
$qb
|
||||
->where($qb->expr()->isMemberOf(':addressee', 'n.addressees'))
|
||||
->leftJoin('n.addresseeUserGroups', 'aug')
|
||||
->leftJoin('aug.users', 'ugu')
|
||||
->where(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->isMemberOf(':addressee', 'n.addressees'),
|
||||
$qb->expr()->eq('ugu.id', ':addressee')
|
||||
)
|
||||
)
|
||||
->setParameter('addressee', $addressee);
|
||||
|
||||
return $qb;
|
||||
@@ -393,4 +400,30 @@ final class NotificationRepository implements ObjectRepository
|
||||
|
||||
return $nq->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all notifications for a user that were created between two dates.
|
||||
*
|
||||
* @return array|Notification[]
|
||||
*/
|
||||
public function findNotificationsForUserBetweenDates(int $userId, \DateTimeInterface $startDate, \DateTimeInterface $endDate): array
|
||||
{
|
||||
$rsm = new Query\ResultSetMappingBuilder($this->em);
|
||||
$rsm->addRootEntityFromClassMetadata(Notification::class, 'cmn');
|
||||
|
||||
$sql = 'SELECT '.$rsm->generateSelectClause(['cmn' => 'cmn']).' '.
|
||||
'FROM chill_main_notification cmn '.
|
||||
'JOIN chill_main_notification_addresses_user cmnau ON cmnau.notification_id = cmn.id '.
|
||||
'WHERE cmnau.user_id = :userId '.
|
||||
'AND cmn.date >= :startDate '.
|
||||
'AND cmn.date <= :endDate '.
|
||||
'ORDER BY cmn.date DESC';
|
||||
|
||||
$nq = $this->em->createNativeQuery($sql, $rsm)
|
||||
->setParameter('userId', $userId)
|
||||
->setParameter('startDate', $startDate, Types::DATETIME_MUTABLE)
|
||||
->setParameter('endDate', $endDate, Types::DATETIME_MUTABLE);
|
||||
|
||||
return $nq->getResult();
|
||||
}
|
||||
}
|
||||
|
@@ -16,9 +16,8 @@ use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\NonUniqueResultException;
|
||||
use Doctrine\ORM\NoResultException;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
|
||||
final readonly class RegroupmentRepository implements ObjectRepository
|
||||
final readonly class RegroupmentRepository implements RegroupmentRepositoryInterface
|
||||
{
|
||||
private EntityRepository $repository;
|
||||
|
||||
|
@@ -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\Regroupment;
|
||||
use Doctrine\ORM\NonUniqueResultException;
|
||||
use Doctrine\ORM\NoResultException;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
|
||||
/**
|
||||
* @template-extends ObjectRepository<Regroupment>
|
||||
*/
|
||||
interface RegroupmentRepositoryInterface extends ObjectRepository
|
||||
{
|
||||
/**
|
||||
* @throws NonUniqueResultException
|
||||
* @throws NoResultException
|
||||
*/
|
||||
public function findOneByName(string $name): ?Regroupment;
|
||||
|
||||
/**
|
||||
* @return array<Regroupment>
|
||||
*/
|
||||
public function findRegroupmentAssociatedToNoCenter(): array;
|
||||
}
|
@@ -0,0 +1,32 @@
|
||||
<?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\ExportGeneration;
|
||||
use Chill\MainBundle\Entity\SavedExport;
|
||||
|
||||
readonly class SavedExportOrExportGenerationRepository
|
||||
{
|
||||
public function __construct(
|
||||
private SavedExportRepositoryInterface $savedExportRepository,
|
||||
private ExportGenerationRepository $exportGenerationRepository,
|
||||
) {}
|
||||
|
||||
public function findById(string $uuid): SavedExport|ExportGeneration|null
|
||||
{
|
||||
if (null !== $savedExport = $this->savedExportRepository->find($uuid)) {
|
||||
return $savedExport;
|
||||
}
|
||||
|
||||
return $this->exportGenerationRepository->find($uuid);
|
||||
}
|
||||
}
|
@@ -13,9 +13,12 @@ namespace Chill\MainBundle\Repository;
|
||||
|
||||
use Chill\MainBundle\Entity\SavedExport;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\UserGroup;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
use Symfony\Component\String\UnicodeString;
|
||||
|
||||
/**
|
||||
* @implements ObjectRepository<SavedExport>
|
||||
@@ -55,6 +58,51 @@ class SavedExportRepository implements SavedExportRepositoryInterface
|
||||
->where($qb->expr()->eq('se.user', ':user'))
|
||||
->setParameter('user', $user);
|
||||
|
||||
return $this->prepareResult($qb, $orderBy, $limit, $offset);
|
||||
}
|
||||
|
||||
public function findSharedWithUser(User $user, ?array $orderBy = [], ?int $limit = null, ?int $offset = null, array $filters = []): array
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('se');
|
||||
|
||||
$qb
|
||||
->where(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->eq('se.user', ':user'),
|
||||
$qb->expr()->isMemberOf(':user', 'se.sharedWithUsers'),
|
||||
$qb->expr()->exists(
|
||||
sprintf('SELECT 1 FROM %s ug WHERE ug MEMBER OF se.sharedWithGroups AND :user MEMBER OF ug.users', UserGroup::class)
|
||||
)
|
||||
)
|
||||
)
|
||||
->setParameter('user', $user);
|
||||
|
||||
foreach ($filters as $key => $filter) {
|
||||
if (self::FILTER_TITLE === ($key & self::FILTER_TITLE)
|
||||
|| self::FILTER_DESCRIPTION === ($key & self::FILTER_DESCRIPTION)) {
|
||||
$filter = new UnicodeString($filter);
|
||||
|
||||
$i = 0;
|
||||
foreach ($filter->split(' ') as $word) {
|
||||
$orx = $qb->expr()->orX();
|
||||
if (self::FILTER_TITLE === ($key & self::FILTER_TITLE)) {
|
||||
$orx->add($qb->expr()->like('LOWER(se.title)', 'LOWER(:qs'.$i.')'));
|
||||
}
|
||||
if (self::FILTER_DESCRIPTION === ($key & self::FILTER_DESCRIPTION)) {
|
||||
$orx->add($qb->expr()->like('LOWER(se.description)', 'LOWER(:qs'.$i.')'));
|
||||
}
|
||||
$qb->andWhere($orx);
|
||||
$qb->setParameter('qs'.$i, '%'.$word->trim().'%');
|
||||
++$i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->prepareResult($qb, $orderBy, $limit, $offset);
|
||||
}
|
||||
|
||||
private function prepareResult(QueryBuilder $qb, ?array $orderBy = [], ?int $limit = null, ?int $offset = null): array
|
||||
{
|
||||
if (null !== $limit) {
|
||||
$qb->setMaxResults($limit);
|
||||
}
|
||||
|
@@ -20,6 +20,9 @@ use Doctrine\Persistence\ObjectRepository;
|
||||
*/
|
||||
interface SavedExportRepositoryInterface extends ObjectRepository
|
||||
{
|
||||
public const FILTER_TITLE = 0x01;
|
||||
public const FILTER_DESCRIPTION = 0x10;
|
||||
|
||||
public function find($id): ?SavedExport;
|
||||
|
||||
/**
|
||||
@@ -34,6 +37,15 @@ interface SavedExportRepositoryInterface extends ObjectRepository
|
||||
*/
|
||||
public function findByUser(User $user, ?array $orderBy = [], ?int $limit = null, ?int $offset = null): array;
|
||||
|
||||
/**
|
||||
* Get the saved export created by and the user and the ones shared with the user.
|
||||
*
|
||||
* @param array<int, mixed> $filters filters where keys are one of the constant starting with FILTER_
|
||||
*
|
||||
* @return list<SavedExport>
|
||||
*/
|
||||
public function findSharedWithUser(User $user, ?array $orderBy = [], ?int $limit = null, ?int $offset = null, array $filters = []): array;
|
||||
|
||||
public function findOneBy(array $criteria): ?SavedExport;
|
||||
|
||||
public function getClassName(): string;
|
||||
|
@@ -11,6 +11,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Repository;
|
||||
|
||||
use Chill\MainBundle\Entity\UserGroup;
|
||||
use Chill\MainBundle\Entity\UserJob;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
@@ -30,9 +31,6 @@ readonly class UserJobRepository implements UserJobRepositoryInterface
|
||||
return $this->repository->find($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|UserJob[]
|
||||
*/
|
||||
public function findAll(): array
|
||||
{
|
||||
return $this->repository->findAll();
|
||||
@@ -56,12 +54,20 @@ readonly class UserJobRepository implements UserJobRepositoryInterface
|
||||
return $jobs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed|null $limit
|
||||
* @param mixed|null $offset
|
||||
*
|
||||
* @return array|object[]|UserJob[]
|
||||
*/
|
||||
public function findAllNotAssociatedWithUserGroup(): array
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('u');
|
||||
$qb->select('u');
|
||||
|
||||
$qb->where(
|
||||
$qb->expr()->not(
|
||||
$qb->expr()->exists(sprintf('SELECT 1 FROM %s ug WHERE ug.userJob = u', UserGroup::class))
|
||||
)
|
||||
);
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null)
|
||||
{
|
||||
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
|
||||
|
@@ -14,18 +14,15 @@ namespace Chill\MainBundle\Repository;
|
||||
use Chill\MainBundle\Entity\UserJob;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
|
||||
/**
|
||||
* @template-extends ObjectRepository<UserJob>
|
||||
*/
|
||||
interface UserJobRepositoryInterface extends ObjectRepository
|
||||
{
|
||||
public function find($id): ?UserJob;
|
||||
|
||||
/**
|
||||
* @return array|UserJob[]
|
||||
*/
|
||||
public function findAll(): array;
|
||||
|
||||
/**
|
||||
* @return array|UserJob[]
|
||||
*/
|
||||
public function findAllActive(): array;
|
||||
|
||||
/**
|
||||
@@ -36,11 +33,14 @@ interface UserJobRepositoryInterface extends ObjectRepository
|
||||
public function findAllOrderedByName(): array;
|
||||
|
||||
/**
|
||||
* @param mixed|null $limit
|
||||
* @param mixed|null $offset
|
||||
* Find all the user job which are not related to a UserGroup.
|
||||
*
|
||||
* @return array|object[]|UserJob[]
|
||||
* This is useful for synchronizing UserGroups with jobs.
|
||||
*
|
||||
* @return list<UserJob>
|
||||
*/
|
||||
public function findAllNotAssociatedWithUserGroup(): array;
|
||||
|
||||
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null);
|
||||
|
||||
public function findOneBy(array $criteria): ?UserJob;
|
||||
|
@@ -61,6 +61,9 @@ export interface ConflictHttpExceptionInterface
|
||||
|
||||
/**
|
||||
* Generic api method that can be adapted to any fetch request
|
||||
*
|
||||
* This method is suitable make a single fetch. When performing a GET to fetch a list of elements, always consider pagination
|
||||
* and use of the @link{fetchResults} method.
|
||||
*/
|
||||
export const makeFetch = <Input, Output>(
|
||||
method: "POST" | "GET" | "PUT" | "PATCH" | "DELETE",
|
||||
|
@@ -0,0 +1,18 @@
|
||||
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
|
||||
import { ExportGeneration } from "ChillMainAssets/types";
|
||||
|
||||
export const fetchExportGenerationStatus = async (
|
||||
exportGenerationId: string,
|
||||
): Promise<ExportGeneration> =>
|
||||
makeFetch(
|
||||
"GET",
|
||||
`/api/1.0/main/export-generation/${exportGenerationId}/object`,
|
||||
);
|
||||
|
||||
export const generateFromSavedExport = async (
|
||||
savedExportUuid: string,
|
||||
): Promise<ExportGeneration> =>
|
||||
makeFetch(
|
||||
"POST",
|
||||
`/api/1.0/main/export/export-generation/create-from-saved-export/${savedExportUuid}`,
|
||||
);
|
@@ -0,0 +1,3 @@
|
||||
export function buildReturnPath(location: Location): string {
|
||||
return location.pathname + location.search;
|
||||
}
|
@@ -12,6 +12,11 @@ function loadDynamicPicker(element) {
|
||||
let apps = element.querySelectorAll('[data-module="pick-dynamic"]');
|
||||
|
||||
apps.forEach(function (el) {
|
||||
let suggested;
|
||||
let as_id;
|
||||
let submit_on_adding_new_entity;
|
||||
let label;
|
||||
let isCurrentUserPicker;
|
||||
const isMultiple = parseInt(el.dataset.multiple) === 1,
|
||||
uniqId = el.dataset.uniqid,
|
||||
input = element.querySelector(
|
||||
@@ -22,12 +27,13 @@ function loadDynamicPicker(element) {
|
||||
? JSON.parse(input.value)
|
||||
: input.value === "[]" || input.value === ""
|
||||
? null
|
||||
: [JSON.parse(input.value)],
|
||||
suggested = JSON.parse(el.dataset.suggested),
|
||||
as_id = parseInt(el.dataset.asId) === 1,
|
||||
submit_on_adding_new_entity =
|
||||
parseInt(el.dataset.submitOnAddingNewEntity) === 1,
|
||||
label = el.dataset.label;
|
||||
: [JSON.parse(input.value)];
|
||||
suggested = JSON.parse(el.dataset.suggested);
|
||||
as_id = parseInt(el.dataset.asId) === 1;
|
||||
submit_on_adding_new_entity =
|
||||
parseInt(el.dataset.submitOnAddingNewEntity) === 1;
|
||||
label = el.dataset.label;
|
||||
isCurrentUserPicker = uniqId.startsWith("pick_user_or_me_dyn");
|
||||
|
||||
if (!isMultiple) {
|
||||
if (input.value === "[]") {
|
||||
@@ -44,6 +50,7 @@ function loadDynamicPicker(element) {
|
||||
':uniqid="uniqid" ' +
|
||||
':suggested="notPickedSuggested" ' +
|
||||
':label="label" ' +
|
||||
':isCurrentUserPicker="isCurrentUserPicker" ' +
|
||||
'@addNewEntity="addNewEntity" ' +
|
||||
'@removeEntity="removeEntity" ' +
|
||||
'@addNewEntityProcessEnded="addNewEntityProcessEnded"' +
|
||||
@@ -61,6 +68,7 @@ function loadDynamicPicker(element) {
|
||||
as_id,
|
||||
submit_on_adding_new_entity,
|
||||
label,
|
||||
isCurrentUserPicker,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -89,7 +97,8 @@ function loadDynamicPicker(element) {
|
||||
const ids = this.picked.map((el) => el.id);
|
||||
input.value = ids.join(",");
|
||||
}
|
||||
console.log(entity);
|
||||
console.log(this.picked);
|
||||
// console.log(entity);
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
|
@@ -1,14 +0,0 @@
|
||||
import { download_report } from "../../lib/download-report/download-report";
|
||||
|
||||
window.addEventListener("DOMContentLoaded", function (e) {
|
||||
const export_generate_url = window.export_generate_url;
|
||||
|
||||
if (typeof export_generate_url === "undefined") {
|
||||
console.error("Alias not found!");
|
||||
throw new Error("Alias not found!");
|
||||
}
|
||||
|
||||
const query = window.location.search,
|
||||
container = document.querySelector("#download_container");
|
||||
download_report(export_generate_url + query.toString(), container);
|
||||
});
|
@@ -1,4 +1,5 @@
|
||||
import { GenericDoc } from "ChillDocStoreAssets/types/generic_doc";
|
||||
import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types";
|
||||
|
||||
export interface DateTime {
|
||||
datetime: string;
|
||||
@@ -200,3 +201,17 @@ export interface WorkflowAttachment {
|
||||
updatedBy: User | null;
|
||||
genericDoc: null | GenericDoc;
|
||||
}
|
||||
|
||||
export interface ExportGeneration {
|
||||
id: string;
|
||||
type: "export_generation";
|
||||
exportAlias: string;
|
||||
createdBy: User | null;
|
||||
createdAt: DateTime | null;
|
||||
status: StoredObjectStatus;
|
||||
storedObject: StoredObject;
|
||||
}
|
||||
|
||||
export interface PrivateCommentEmbeddable {
|
||||
comments: Record<number, string>;
|
||||
}
|
||||
|
@@ -0,0 +1,141 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
trans,
|
||||
EXPORT_GENERATION_EXPORT_GENERATION_IS_PENDING,
|
||||
EXPORT_GENERATION_TOO_MANY_RETRIES,
|
||||
EXPORT_GENERATION_ERROR_WHILE_GENERATING_EXPORT,
|
||||
EXPORT_GENERATION_EXPORT_READY,
|
||||
} from "translator";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types";
|
||||
import { fetchExportGenerationStatus } from "ChillMainAssets/lib/api/export";
|
||||
import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue";
|
||||
import { ExportGeneration } from "ChillMainAssets/types";
|
||||
|
||||
interface AppProps {
|
||||
exportGenerationId: string;
|
||||
title: string;
|
||||
createdDate: string;
|
||||
}
|
||||
|
||||
const props = defineProps<AppProps>();
|
||||
|
||||
const exportGeneration = ref<ExportGeneration | null>(null);
|
||||
|
||||
const status = computed<StoredObjectStatus>(
|
||||
() => exportGeneration.value?.status ?? "pending",
|
||||
);
|
||||
const storedObject = computed<null | StoredObject>(() => {
|
||||
if (exportGeneration.value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return exportGeneration.value?.storedObject;
|
||||
});
|
||||
|
||||
const isPending = computed<boolean>(() => status.value === "pending");
|
||||
const isFetching = computed<boolean>(
|
||||
() => tryiesForReady.value < maxTryiesForReady,
|
||||
);
|
||||
const isReady = computed<boolean>(() => status.value === "ready");
|
||||
const isFailure = computed<boolean>(() => status.value === "failure");
|
||||
const filename = computed<string>(() => `${props.title}-${props.createdDate}`);
|
||||
|
||||
/**
|
||||
* counter for the number of times that we check for a new status
|
||||
*/
|
||||
let tryiesForReady = ref<number>(0);
|
||||
|
||||
/**
|
||||
* how many times we may check for a new status, once loaded
|
||||
*/
|
||||
const maxTryiesForReady = 120;
|
||||
|
||||
const checkForReady = function (): void {
|
||||
if (
|
||||
"ready" === status.value ||
|
||||
"empty" === status.value ||
|
||||
"failure" === status.value ||
|
||||
// stop reloading if the page stays opened for a long time
|
||||
tryiesForReady.value > maxTryiesForReady
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
tryiesForReady.value = tryiesForReady.value + 1;
|
||||
setTimeout(onObjectNewStatusCallback, 5000);
|
||||
};
|
||||
|
||||
const onObjectNewStatusCallback = async function (): Promise<void> {
|
||||
exportGeneration.value = await fetchExportGenerationStatus(
|
||||
props.exportGenerationId,
|
||||
);
|
||||
|
||||
if (isPending.value) {
|
||||
checkForReady();
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
onObjectNewStatusCallback();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="waiting-screen">
|
||||
<div
|
||||
v-if="isPending && isFetching"
|
||||
class="alert alert-danger text-center"
|
||||
>
|
||||
<div>
|
||||
<p>
|
||||
{{ trans(EXPORT_GENERATION_EXPORT_GENERATION_IS_PENDING) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<i class="fa fa-cog fa-spin fa-3x fa-fw"></i>
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isPending && !isFetching" class="alert alert-info">
|
||||
<div>
|
||||
<p>
|
||||
{{ trans(EXPORT_GENERATION_TOO_MANY_RETRIES) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isFailure" class="alert alert-danger text-center">
|
||||
<div>
|
||||
<p>
|
||||
{{ trans(EXPORT_GENERATION_ERROR_WHILE_GENERATING_EXPORT) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isReady" class="alert alert-success text-center">
|
||||
<div>
|
||||
<p>
|
||||
{{ trans(EXPORT_GENERATION_EXPORT_READY) }}
|
||||
</p>
|
||||
|
||||
<p v-if="storedObject !== null">
|
||||
<document-action-buttons-group
|
||||
:stored-object="storedObject"
|
||||
:filename="filename"
|
||||
></document-action-buttons-group>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
#waiting-screen {
|
||||
> .alert {
|
||||
min-height: 350px;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,15 @@
|
||||
import { createApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
|
||||
const el = document.getElementById("app");
|
||||
|
||||
if (null === el) {
|
||||
console.error("div element app was not found");
|
||||
throw new Error("div element app was not found");
|
||||
}
|
||||
|
||||
const exportGenerationId = el?.dataset.exportGenerationId as string;
|
||||
const title = el?.dataset.exportTitle as string;
|
||||
const createdDate = el?.dataset.exportGenerationDate as string;
|
||||
|
||||
createApp(App, { exportGenerationId, title, createdDate }).mount(el);
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user