diff --git a/.gitignore b/.gitignore index ebdc16e56..26802dca0 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ composer.lock docs/build/ node_modules/* .php_cs.cache +.cache/* ###> symfony/framework-bundle ### /.env.local diff --git a/phpstan-types.neon b/phpstan-types.neon index 1aae06880..b11cbd153 100644 --- a/phpstan-types.neon +++ b/phpstan-types.neon @@ -340,11 +340,6 @@ parameters: count: 1 path: src/Bundle/ChillPersonBundle/Form/Type/PersonPhoneType.php - - - message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#" - count: 3 - path: src/Bundle/ChillPersonBundle/Search/PersonSearch.php - - message: "#^Method Chill\\\\PersonBundle\\\\Search\\\\PersonSearch\\:\\:renderResult\\(\\) should return string but return statement is missing\\.$#" count: 1 diff --git a/src/Bundle/ChillActivityBundle/Entity/Activity.php b/src/Bundle/ChillActivityBundle/Entity/Activity.php index 8fcae3e0b..2a5ae6acb 100644 --- a/src/Bundle/ChillActivityBundle/Entity/Activity.php +++ b/src/Bundle/ChillActivityBundle/Entity/Activity.php @@ -257,12 +257,10 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac /** * Add a social issue. * - * Note: the social issue consistency (the fact that only yougest social issues + * Note: the social issue consistency (the fact that only youngest social issues * are kept) is processed by an entity listener: * * @see{\Chill\PersonBundle\AccompanyingPeriod\SocialIssueConsistency\AccompanyingPeriodSocialIssueConsistencyEntityListener} - * - * @return $this */ public function addSocialIssue(SocialIssue $socialIssue): self { @@ -270,6 +268,10 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac $this->socialIssues[] = $socialIssue; } + if ($this->getAccompanyingPeriod() !== null) { + $this->getAccompanyingPeriod()->addSocialIssue($socialIssue); + } + return $this; } @@ -550,6 +552,10 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac { $this->accompanyingPeriod = $accompanyingPeriod; + foreach ($this->getSocialIssues() as $issue) { + $this->accompanyingPeriod->addSocialIssue($issue); + } + return $this; } diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/LocationFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/LocationFilter.php index ed591e957..5cdce5b31 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/LocationFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/LocationFilter.php @@ -50,7 +50,7 @@ class LocationFilter implements FilterInterface { $builder->add('accepted_location', PickUserLocationType::class, [ 'multiple' => true, - 'label' => 'pick location' + 'label' => 'pick location', ]); } diff --git a/src/Bundle/ChillActivityBundle/Resources/public/chill/chillactivity.scss b/src/Bundle/ChillActivityBundle/Resources/public/chill/chillactivity.scss index 5c1c83d06..d70bd28c3 100644 --- a/src/Bundle/ChillActivityBundle/Resources/public/chill/chillactivity.scss +++ b/src/Bundle/ChillActivityBundle/Resources/public/chill/chillactivity.scss @@ -88,3 +88,11 @@ div.flex-bloc.concerned-groups { font-size: 120%; } } + +/// DOCUMENT LIST IN ACTIVITY ITEM +li.document-list-item { + display: flex; + width: 100%; + justify-content: space-between; + margin-bottom: 0.3rem; +} diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/_list_item.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/_list_item.html.twig index 3a4749f3c..885125aa6 100644 --- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/_list_item.html.twig +++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/_list_item.html.twig @@ -68,7 +68,7 @@

{{ 'Referrer'|trans }}

- {{ activity.user|chill_entity_render_box }} + {{ activity.user|chill_entity_render_box }}

@@ -137,19 +137,42 @@ {{ activity.comment|chill_entity_render_box({ 'disable_markdown': false, 'limit_lines': 3, - 'metadata': false + 'metadata': false, }) }} {% endif %} - {# Only if ACL SEE_DETAILS AND/OR only on template SHOW ?? - durationTime - travelTime - comment - documents - attendee - #} + {% if is_granted('CHILL_ACTIVITY_SEE_DETAILS', activity) and activity.privateComment.hasCommentForUser(app.user) %} +
+
+

{{ 'Private comment'|trans }}

+
+
+
+
+ {{ activity.privateComment.comments[app.user.id]|chill_markdown_to_html }} +
+
+
+
+ {% endif %} + + {% if is_granted('CHILL_ACTIVITY_SEE_DETAILS', activity) and activity.documents|length > 0 %} +
+
+

{{ 'Documents'|trans }}

+
+
+ +
+
+ {% endif %} + diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/listAccompanyingCourse.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/listAccompanyingCourse.html.twig index a666f183d..bdf55d86f 100644 --- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/listAccompanyingCourse.html.twig +++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/listAccompanyingCourse.html.twig @@ -8,11 +8,13 @@ {% block js %} {{ parent() }} {{ encore_entry_script_tags('mod_notification_toggle_read_status') }} + {{ encore_entry_script_tags('mod_document_action_buttons_group') }} {% endblock %} {% block css %} {{ parent() }} {{ encore_entry_link_tags('mod_notification_toggle_read_status') }} + {{ encore_entry_link_tags('mod_document_action_buttons_group') }} {% endblock %} {% block content %} diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/listPerson.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/listPerson.html.twig index 0514284a2..0c1afdac9 100644 --- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/listPerson.html.twig +++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/listPerson.html.twig @@ -23,11 +23,13 @@ {% block js %} {{ parent() }} {{ encore_entry_script_tags('mod_notification_toggle_read_status') }} + {{ encore_entry_script_tags('mod_document_action_buttons_group') }} {% endblock %} {% block css %} {{ parent() }} {{ encore_entry_link_tags('mod_notification_toggle_read_status') }} + {{ encore_entry_link_tags('mod_document_action_buttons_group') }} {% endblock %} {% block content %} diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/list_recent.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/list_recent.html.twig index e4184da36..aa0bbea0c 100644 --- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/list_recent.html.twig +++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/list_recent.html.twig @@ -41,7 +41,7 @@ {% if activity.user and t.userVisible %}
  • {{ 'Referrer'|trans ~ ': ' }} - {{ activity.user|chill_entity_render_box}} + {{ activity.user|chill_entity_render_box }}
  • {% endif %} diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/show.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/show.html.twig index 1ec0ab274..acda43b97 100644 --- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/show.html.twig +++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/show.html.twig @@ -35,7 +35,9 @@
    {{ 'Referrer'|trans|capitalize }}
    -
    {{ entity.user|chill_entity_render_box }}
    +
    + {{ entity.user|chill_entity_render_box }} +
    {%- if entity.scope -%}
    {{ 'Scope'|trans }}
    @@ -168,7 +170,7 @@ {% if entity.documents|length > 0 %} {% else %} diff --git a/src/Bundle/ChillActivityBundle/Service/DocGenerator/ActivityContext.php b/src/Bundle/ChillActivityBundle/Service/DocGenerator/ActivityContext.php index 4e2970138..3df0d886d 100644 --- a/src/Bundle/ChillActivityBundle/Service/DocGenerator/ActivityContext.php +++ b/src/Bundle/ChillActivityBundle/Service/DocGenerator/ActivityContext.php @@ -22,6 +22,7 @@ use Chill\DocStoreBundle\Repository\DocumentCategoryRepository; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\Person; +use Chill\PersonBundle\Repository\PersonRepository; use Chill\PersonBundle\Templating\Entity\PersonRenderInterface; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bridge\Doctrine\Form\Type\EntityType; @@ -45,6 +46,8 @@ class ActivityContext implements private PersonRenderInterface $personRender; + private PersonRepository $personRepository; + private TranslatableStringHelperInterface $translatableStringHelper; private TranslatorInterface $translator; @@ -55,6 +58,7 @@ class ActivityContext implements TranslatableStringHelperInterface $translatableStringHelper, EntityManagerInterface $em, PersonRenderInterface $personRender, + PersonRepository $personRepository, TranslatorInterface $translator, BaseContextData $baseContextData ) { @@ -63,6 +67,7 @@ class ActivityContext implements $this->translatableStringHelper = $translatableStringHelper; $this->em = $em; $this->personRender = $personRender; + $this->personRepository = $personRepository; $this->translator = $translator; $this->baseContextData = $baseContextData; } @@ -147,7 +152,7 @@ class ActivityContext implements $options = $template->getOptions(); $data = []; - $data = array_merge($data, $this->baseContextData->getData()); + $data = array_merge($data, $this->baseContextData->getData($contextGenerationData['creator'] ?? null)); $data['activity'] = $this->normalizer->normalize($entity, 'docgen', ['docgen:expects' => Activity::class, 'groups' => 'docgen:read']); $data['course'] = $this->normalizer->normalize($entity->getAccompanyingPeriod(), 'docgen', ['docgen:expects' => AccompanyingPeriod::class, 'groups' => 'docgen:read']); @@ -206,6 +211,32 @@ class ActivityContext implements return $options['mainPerson'] || $options['person1'] || $options['person2']; } + public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array + { + $normalized = []; + + foreach (['mainPerson', 'person1', 'person2'] as $k) { + $normalized[$k] = null === $data[$k] ? null : $data[$k]->getId(); + } + + return $normalized; + } + + public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array + { + $denormalized = []; + + foreach (['mainPerson', 'person1', 'person2'] as $k) { + if (null !== ($id = ($data[$k] ?? null))) { + $denormalized[$k] = $this->personRepository->find($id); + } else { + $denormalized[$k] = null; + } + } + + return $denormalized; + } + /** * @param Activity $entity */ diff --git a/src/Bundle/ChillActivityBundle/Service/DocGenerator/ListActivitiesByAccompanyingPeriodContext.php b/src/Bundle/ChillActivityBundle/Service/DocGenerator/ListActivitiesByAccompanyingPeriodContext.php index 3189307f9..7e1873710 100644 --- a/src/Bundle/ChillActivityBundle/Service/DocGenerator/ListActivitiesByAccompanyingPeriodContext.php +++ b/src/Bundle/ChillActivityBundle/Service/DocGenerator/ListActivitiesByAccompanyingPeriodContext.php @@ -146,6 +146,16 @@ class ListActivitiesByAccompanyingPeriodContext implements return $this->accompanyingPeriodContext->hasPublicForm($template, $entity); } + public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array + { + return $this->accompanyingPeriodContext->contextGenerationDataNormalize($template, $entity, $data); + } + + public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array + { + return $this->accompanyingPeriodContext->contextGenerationDataDenormalize($template, $entity, $data); + } + public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void { $this->accompanyingPeriodContext->storeGenerated($template, $storedObject, $entity, $contextGenerationData); diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Export/ListAsideActivity.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Export/ListAsideActivity.php index bf370f71b..976f4171c 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Export/Export/ListAsideActivity.php +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Export/ListAsideActivity.php @@ -1,10 +1,18 @@ format('H:i:s'); } @@ -118,7 +122,7 @@ final class ListAsideActivity implements ListInterface, GroupedExportInterface case 'createdAt': case 'updatedAt': case 'date': - return $this->dateTimeHelper->getLabel('export.aside_activity.'.$key); + return $this->dateTimeHelper->getLabel('export.aside_activity.' . $key); case 'agent_id': case 'creator_id': @@ -165,7 +169,7 @@ final class ListAsideActivity implements ListInterface, GroupedExportInterface }; default: - throw new \LogicException('this key is not supported : ' . $key); + throw new LogicException('this key is not supported : ' . $key); } } @@ -182,7 +186,7 @@ final class ListAsideActivity implements ListInterface, GroupedExportInterface 'aside_activity_type', 'date', 'duration', - 'note' + 'note', ]; } @@ -195,6 +199,11 @@ final class ListAsideActivity implements ListInterface, GroupedExportInterface return $query->getQuery()->getResult(AbstractQuery::HYDRATE_ARRAY); } + public function getTitle() + { + return 'export.aside_activity.List of aside activities'; + } + public function getType(): string { return Declarations::ASIDE_ACTIVITY_TYPE; @@ -204,8 +213,7 @@ final class ListAsideActivity implements ListInterface, GroupedExportInterface { $qb = $this->em->createQueryBuilder() ->from(AsideActivity::class, 'aside') - ->leftJoin('aside.agent', 'agent') - ; + ->leftJoin('aside.agent', 'agent'); $qb ->addSelect('aside.id AS id') @@ -218,8 +226,7 @@ final class ListAsideActivity implements ListInterface, GroupedExportInterface ->addSelect('IDENTITY(aside.type) AS aside_activity_type') ->addSelect('aside.date') ->addSelect('aside.duration') - ->addSelect('aside.note') - ; + ->addSelect('aside.note'); return $qb; } diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByDateFilter.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByDateFilter.php index 2d49b3d57..7a1b6f4dc 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByDateFilter.php +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByDateFilter.php @@ -17,7 +17,6 @@ use Chill\MainBundle\Form\Type\Export\FilterType; use Chill\MainBundle\Form\Type\PickRollingDateType; use Chill\MainBundle\Service\RollingDate\RollingDate; use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface; -use Doctrine\ORM\Query\Expr\Andx; use Doctrine\ORM\QueryBuilder; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormError; diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserFilter.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserFilter.php index c2a3b4c54..795c813cd 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserFilter.php +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserFilter.php @@ -15,7 +15,6 @@ use Chill\AsideActivityBundle\Export\Declarations; use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Form\Type\PickUserDynamicType; use Chill\MainBundle\Templating\Entity\UserRender; -use Doctrine\ORM\Query\Expr\Andx; use Doctrine\ORM\QueryBuilder; use Symfony\Component\Form\FormBuilderInterface; diff --git a/src/Bundle/ChillBudgetBundle/Calculator/CalculatorManager.php b/src/Bundle/ChillBudgetBundle/Calculator/CalculatorManager.php index a37ac68dc..934c22e36 100644 --- a/src/Bundle/ChillBudgetBundle/Calculator/CalculatorManager.php +++ b/src/Bundle/ChillBudgetBundle/Calculator/CalculatorManager.php @@ -29,10 +29,10 @@ class CalculatorManager public function addCalculator(CalculatorInterface $calculator, bool $default) { - $this->calculators[$calculator::getAlias()] = $calculator; + $this->calculators[$calculator->getAlias()] = $calculator; if ($default) { - $this->defaultCalculator[] = $calculator::getAlias(); + $this->defaultCalculator[] = $calculator->getAlias(); } } @@ -50,7 +50,7 @@ class CalculatorManager $result = $calculator->calculate($elements); if (null !== $result) { - $results[$calculator::getAlias()] = $result; + $results[$calculator->getAlias()] = $result; } } diff --git a/src/Bundle/ChillBudgetBundle/Form/Admin/ChargeKindType.php b/src/Bundle/ChillBudgetBundle/Form/Admin/ChargeKindType.php index 59adf49af..c3c2a66bf 100644 --- a/src/Bundle/ChillBudgetBundle/Form/Admin/ChargeKindType.php +++ b/src/Bundle/ChillBudgetBundle/Form/Admin/ChargeKindType.php @@ -30,7 +30,7 @@ class ChargeKindType extends AbstractType ]) ->add('kind', TextType::class, [ 'label' => 'budget.admin.form.Charge_kind_key', - 'help' => 'budget.admin.form.This kind must contains only alphabeticals characters, and dashes. This string is in use during document generation. Changes may have side effect on document' + 'help' => 'budget.admin.form.This kind must contains only alphabeticals characters, and dashes. This string is in use during document generation. Changes may have side effect on document', ]) ->add('ordering', NumberType::class) ->add('isActive', CheckboxType::class, [ diff --git a/src/Bundle/ChillBudgetBundle/Form/Admin/ResourceKindType.php b/src/Bundle/ChillBudgetBundle/Form/Admin/ResourceKindType.php index 0605b8731..41d3a8b53 100644 --- a/src/Bundle/ChillBudgetBundle/Form/Admin/ResourceKindType.php +++ b/src/Bundle/ChillBudgetBundle/Form/Admin/ResourceKindType.php @@ -30,7 +30,7 @@ class ResourceKindType extends AbstractType ]) ->add('kind', TextType::class, [ 'label' => 'budget.admin.form.Resource_kind_key', - 'help' => 'budget.admin.form.This kind must contains only alphabeticals characters, and dashes. This string is in use during document generation. Changes may have side effect on document' + 'help' => 'budget.admin.form.This kind must contains only alphabeticals characters, and dashes. This string is in use during document generation. Changes may have side effect on document', ]) ->add('ordering', NumberType::class) ->add('isActive', CheckboxType::class, [ diff --git a/src/Bundle/ChillBudgetBundle/Repository/ChargeKindRepository.php b/src/Bundle/ChillBudgetBundle/Repository/ChargeKindRepository.php index 10d02749a..01f5cd737 100644 --- a/src/Bundle/ChillBudgetBundle/Repository/ChargeKindRepository.php +++ b/src/Bundle/ChillBudgetBundle/Repository/ChargeKindRepository.php @@ -49,8 +49,7 @@ final class ChargeKindRepository implements ChargeKindRepositoryInterface ->where($qb->expr()->eq('c.isActive', 'true')) ->orderBy('c.ordering', 'ASC') ->getQuery() - ->getResult() - ; + ->getResult(); } /** diff --git a/src/Bundle/ChillBudgetBundle/Repository/ChargeKindRepositoryInterface.php b/src/Bundle/ChillBudgetBundle/Repository/ChargeKindRepositoryInterface.php index 5099a5674..fb8c9dc35 100644 --- a/src/Bundle/ChillBudgetBundle/Repository/ChargeKindRepositoryInterface.php +++ b/src/Bundle/ChillBudgetBundle/Repository/ChargeKindRepositoryInterface.php @@ -28,8 +28,6 @@ interface ChargeKindRepositoryInterface extends ObjectRepository */ public function findAllActive(): array; - public function findOneByKind(string $kind): ?ChargeKind; - /** * @return ChargeType[] */ @@ -45,5 +43,7 @@ interface ChargeKindRepositoryInterface extends ObjectRepository public function findOneBy(array $criteria): ?ChargeKind; + public function findOneByKind(string $kind): ?ChargeKind; + public function getClassName(): string; } diff --git a/src/Bundle/ChillBudgetBundle/Repository/ResourceKindRepository.php b/src/Bundle/ChillBudgetBundle/Repository/ResourceKindRepository.php index 140bb7cc7..f7935c5b7 100644 --- a/src/Bundle/ChillBudgetBundle/Repository/ResourceKindRepository.php +++ b/src/Bundle/ChillBudgetBundle/Repository/ResourceKindRepository.php @@ -49,8 +49,7 @@ final class ResourceKindRepository implements ResourceKindRepositoryInterface ->where($qb->expr()->eq('r.isActive', 'true')) ->orderBy('r.ordering', 'ASC') ->getQuery() - ->getResult() - ; + ->getResult(); } /** diff --git a/src/Bundle/ChillBudgetBundle/Repository/ResourceRepository.php b/src/Bundle/ChillBudgetBundle/Repository/ResourceRepository.php index b3f0a41da..12f9fd52f 100644 --- a/src/Bundle/ChillBudgetBundle/Repository/ResourceRepository.php +++ b/src/Bundle/ChillBudgetBundle/Repository/ResourceRepository.php @@ -34,7 +34,7 @@ class ResourceRepository extends EntityRepository //->andWhere('c.startDate < :date') // TODO: there is a misconception here, the end date must be lower or null. startDate are never null //->andWhere('c.startDate < :date OR c.startDate IS NULL'); - ; +; if (null !== $sort) { $qb->orderBy($sort); diff --git a/src/Bundle/ChillBudgetBundle/Service/Summary/SummaryBudget.php b/src/Bundle/ChillBudgetBundle/Service/Summary/SummaryBudget.php index 2f401f1ec..ad2a014ed 100644 --- a/src/Bundle/ChillBudgetBundle/Service/Summary/SummaryBudget.php +++ b/src/Bundle/ChillBudgetBundle/Service/Summary/SummaryBudget.php @@ -13,9 +13,7 @@ namespace Chill\BudgetBundle\Service\Summary; use Chill\BudgetBundle\Entity\ChargeKind; use Chill\BudgetBundle\Entity\ResourceKind; -use Chill\BudgetBundle\Repository\ChargeKindRepository; use Chill\BudgetBundle\Repository\ChargeKindRepositoryInterface; -use Chill\BudgetBundle\Repository\ResourceKindRepository; use Chill\BudgetBundle\Repository\ResourceKindRepositoryInterface; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\PersonBundle\Entity\Household\Household; diff --git a/src/Bundle/ChillBudgetBundle/Tests/Service/Summary/SummaryBudgetTest.php b/src/Bundle/ChillBudgetBundle/Tests/Service/Summary/SummaryBudgetTest.php index cf8c00efe..7fcda6b11 100644 --- a/src/Bundle/ChillBudgetBundle/Tests/Service/Summary/SummaryBudgetTest.php +++ b/src/Bundle/ChillBudgetBundle/Tests/Service/Summary/SummaryBudgetTest.php @@ -20,12 +20,15 @@ use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\PersonBundle\Entity\Household\Household; use Chill\PersonBundle\Entity\Household\HouseholdMember; use Chill\PersonBundle\Entity\Person; +use DateTimeImmutable; use Doctrine\ORM\AbstractQuery; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Query; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; +use ReflectionClass; +use RuntimeException; /** * @internal @@ -47,10 +50,9 @@ final class SummaryBudgetTest extends TestCase ], ]); $queryCharges->setParameters(Argument::type('array')) - ->will(function ($args, $query) { + ->will(static function ($args, $query) { return $query; - }) - ; + }); $queryResources = $this->prophesize(AbstractQuery::class); $queryResources->getResult()->willReturn([ @@ -61,23 +63,23 @@ final class SummaryBudgetTest extends TestCase ], ]); $queryResources->setParameters(Argument::type('array')) - ->will(function ($args, $query) { + ->will(static function ($args, $query) { return $query; - }) - ; + }); $em = $this->prophesize(EntityManagerInterface::class); $em->createNativeQuery(Argument::type('string'), Argument::type(Query\ResultSetMapping::class)) - ->will(function ($args) use ($queryResources, $queryCharges) { + ->will(static function ($args) use ($queryResources, $queryCharges) { if (false !== strpos($args[0], 'chill_budget.resource')) { return $queryResources->reveal(); } + if (false !== strpos($args[0], 'chill_budget.charge')) { return $queryCharges->reveal(); } - throw new \RuntimeException('this query does not have a stub counterpart: '.$args[0]); - }) - ; + + throw new RuntimeException('this query does not have a stub counterpart: ' . $args[0]); + }); $chargeRepository = $this->prophesize(ChargeKindRepositoryInterface::class); $chargeRepository->findAll()->willReturn([ @@ -98,24 +100,23 @@ final class SummaryBudgetTest extends TestCase $resourceRepository->findOneByKind('misc')->willReturn($misc); $translatableStringHelper = $this->prophesize(TranslatableStringHelperInterface::class); - $translatableStringHelper->localize(Argument::type('array'))->will(function ($arg) { + $translatableStringHelper->localize(Argument::type('array'))->will(static function ($arg) { return $arg[0]['fr']; }); $person = new Person(); - $personReflection = new \ReflectionClass($person); + $personReflection = new ReflectionClass($person); $personIdReflection = $personReflection->getProperty('id'); $personIdReflection->setAccessible(true); $personIdReflection->setValue($person, 1); $household = new Household(); - $householdReflection = new \ReflectionClass($household); + $householdReflection = new ReflectionClass($household); $householdId = $householdReflection->getProperty('id'); $householdId->setAccessible(true); $householdId->setValue($household, 1); $householdMember = (new HouseholdMember())->setPerson($person) - ->setStartDate(new \DateTimeImmutable('1 month ago')) - ; + ->setStartDate(new DateTimeImmutable('1 month ago')); $household->addMember($householdMember); $summaryBudget = new SummaryBudget( diff --git a/src/Bundle/ChillBudgetBundle/migrations/Version20230209161546.php b/src/Bundle/ChillBudgetBundle/migrations/Version20230209161546.php index 618b0c982..4495db8a7 100644 --- a/src/Bundle/ChillBudgetBundle/migrations/Version20230209161546.php +++ b/src/Bundle/ChillBudgetBundle/migrations/Version20230209161546.php @@ -2,6 +2,13 @@ declare(strict_types=1); +/* + * Chill is a software for social workers + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace Chill\Migrations\Budget; use Doctrine\DBAL\Schema\Schema; @@ -9,6 +16,12 @@ use Doctrine\Migrations\AbstractMigration; final class Version20230209161546 extends AbstractMigration { + public function down(Schema $schema): void + { + $this->addSql('DROP INDEX resource_kind_unique_type_idx'); + $this->addSql('DROP INDEX charge_kind_unique_type_idx'); + } + public function getDescription(): string { return 'Budget: add unique constraint on kind for charge_kind and resource_kind'; @@ -21,10 +34,4 @@ final class Version20230209161546 extends AbstractMigration $this->addSql('CREATE UNIQUE INDEX resource_kind_unique_type_idx ON chill_budget.resource_type (kind);'); $this->addSql('CREATE UNIQUE INDEX charge_kind_unique_type_idx ON chill_budget.charge_type (kind);'); } - - public function down(Schema $schema): void - { - $this->addSql('DROP INDEX resource_kind_unique_type_idx'); - $this->addSql('DROP INDEX charge_kind_unique_type_idx'); - } } diff --git a/src/Bundle/ChillBudgetBundle/translations/messages.nl.yml b/src/Bundle/ChillBudgetBundle/translations/messages.nl.yml index 5fd21520d..de334f79f 100644 --- a/src/Bundle/ChillBudgetBundle/translations/messages.nl.yml +++ b/src/Bundle/ChillBudgetBundle/translations/messages.nl.yml @@ -2,8 +2,8 @@ Budget: Budget Resource: Inkomsten Charge: Onkosten Budget for %name%: Budget van %name% -Budget for household %household%: Budget van gezin -Current budget household members: Actuele budget van gezinsleden +Budget for household %household%: Budget van huishouden +Current budget household members: Actuele budget van leden huishouden Show budget of %name%: Toon budget van %name% See complete budget: Toon volledige budget Hide budget: Verbergen diff --git a/src/Bundle/ChillCalendarBundle/DataFixtures/ORM/LoadCalendarRange.php b/src/Bundle/ChillCalendarBundle/DataFixtures/ORM/LoadCalendarRange.php index a1226ca6a..805386669 100644 --- a/src/Bundle/ChillCalendarBundle/DataFixtures/ORM/LoadCalendarRange.php +++ b/src/Bundle/ChillCalendarBundle/DataFixtures/ORM/LoadCalendarRange.php @@ -61,7 +61,7 @@ class LoadCalendarRange extends Fixture implements FixtureGroupInterface, Ordere ->setEmail('centreA@test.chill.social') ->setLocationType($type = new LocationType()) ->setPhonenumber1(PhoneNumberUtil::getInstance()->parse('+3287653812')); - $type->setTitle('Service'); + $type->setTitle(['fr' => 'Service']); $address->setStreet('Rue des Épaules')->setStreetNumber('14') ->setPostcode($postCode = new PostalCode()); $postCode->setCode('4145')->setName('Houte-Si-Plout')->setCountry( diff --git a/src/Bundle/ChillCalendarBundle/DataFixtures/ORM/LoadInvite.php b/src/Bundle/ChillCalendarBundle/DataFixtures/ORM/LoadInvite.php index 25b8ae8a8..ba325e296 100644 --- a/src/Bundle/ChillCalendarBundle/DataFixtures/ORM/LoadInvite.php +++ b/src/Bundle/ChillCalendarBundle/DataFixtures/ORM/LoadInvite.php @@ -12,6 +12,8 @@ declare(strict_types=1); namespace Chill\CalendarBundle\DataFixtures\ORM; use Chill\CalendarBundle\Entity\Invite; +use Chill\MainBundle\DataFixtures\ORM\LoadUsers; +use Chill\MainBundle\Entity\User; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Bundle\FixturesBundle\FixtureGroupInterface; use Doctrine\Persistence\ObjectManager; @@ -33,14 +35,21 @@ class LoadInvite extends Fixture implements FixtureGroupInterface public function load(ObjectManager $manager): void { $arr = [ - ['name' => ['fr' => 'Rendez-vous décliné']], - ['name' => ['fr' => 'Rendez-vous accepté']], + [ + 'name' => ['fr' => 'Rendez-vous décliné'], + 'status' => Invite::DECLINED, + ], + [ + 'name' => ['fr' => 'Rendez-vous accepté'], + 'status' => Invite::ACCEPTED, + ], ]; foreach ($arr as $a) { echo 'Creating calendar invite : ' . $a['name']['fr'] . "\n"; $invite = (new Invite()) - ->setStatus($a['name']); + ->setStatus($a['status']) + ->setUser($this->getRandomUser()); $manager->persist($invite); $reference = 'Invite_' . $a['name']['fr']; $this->addReference($reference, $invite); @@ -49,4 +58,11 @@ class LoadInvite extends Fixture implements FixtureGroupInterface $manager->flush(); } + + private function getRandomUser(): User + { + $userRef = array_rand(LoadUsers::$refs); + + return $this->getReference($userRef); + } } diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MapCalendarToUser.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MapCalendarToUser.php index 504d48ffc..563bd3a38 100644 --- a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MapCalendarToUser.php +++ b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MapCalendarToUser.php @@ -22,6 +22,7 @@ use Chill\MainBundle\Entity\User; use DateTimeImmutable; use LogicException; use Psr\Log\LoggerInterface; +use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; use function array_key_exists; @@ -74,9 +75,18 @@ class MapCalendarToUser public function getDefaultUserCalendar(string $idOrUserPrincipalName): ?array { - $value = $this->machineHttpClient->request('GET', "users/{$idOrUserPrincipalName}/calendars", [ - 'query' => ['$filter' => 'isDefaultCalendar eq true'], - ])->toArray()['value']; + try { + $value = $this->machineHttpClient->request('GET', "users/{$idOrUserPrincipalName}/calendars", [ + 'query' => ['$filter' => 'isDefaultCalendar eq true'], + ])->toArray()['value']; + } catch (ClientExceptionInterface $e) { + $this->logger->error('[MapCalendarToUser] Error while listing calendars for a user', [ + 'http_status_code' => $e->getResponse()->getStatusCode(), + 'id_user' => $idOrUserPrincipalName, + ]); + + return null; + } return $value[0] ?? null; } diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraphRemoteCalendarConnector.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraphRemoteCalendarConnector.php index c376f9680..d409e6f03 100644 --- a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraphRemoteCalendarConnector.php +++ b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraphRemoteCalendarConnector.php @@ -34,6 +34,7 @@ use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Security\Core\Security; use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\Translation\TranslatorInterface; @@ -64,6 +65,8 @@ class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface private OnBehalfOfUserHttpClient $userHttpClient; + private Security $security; + public function __construct( CalendarRepository $calendarRepository, CalendarRangeRepository $calendarRangeRepository, @@ -74,7 +77,8 @@ class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface OnBehalfOfUserHttpClient $userHttpClient, RemoteEventConverter $remoteEventConverter, TranslatorInterface $translator, - UrlGeneratorInterface $urlGenerator + UrlGeneratorInterface $urlGenerator, + Security $security ) { $this->calendarRepository = $calendarRepository; $this->calendarRangeRepository = $calendarRangeRepository; @@ -86,6 +90,7 @@ class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface $this->translator = $translator; $this->urlGenerator = $urlGenerator; $this->userHttpClient = $userHttpClient; + $this->security = $security; } public function countEventsForUser(User $user, DateTimeImmutable $startDate, DateTimeImmutable $endDate): int @@ -133,6 +138,24 @@ class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface public function isReady(): bool { + $user = $this->security->getUser(); + + if (!$user instanceof User) { + // this is not a user from chill. This is not the role of this class to + // restrict access, so we will just say that we do not have to do anything more + // here... + return true; + } + + if (null === $this->mapCalendarToUser->getUserId($user)) { + // this user is not mapped with remote calendar. The user will have to wait for + // the next calendar subscription iteration + $this->logger->debug('mark user ready for msgraph calendar as he does not have any mapping', [ + 'userId' => $user->getId(), + ]); + return true; + } + return $this->tokenStorage->hasToken(); } diff --git a/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/_documents.twig.html b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/_documents.twig.html index 499fb0a83..09824b5cc 100644 --- a/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/_documents.twig.html +++ b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/_documents.twig.html @@ -17,30 +17,20 @@ diff --git a/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/listByAccompanyingCourse.html.twig b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/listByAccompanyingCourse.html.twig index d85c5237e..7ce1003bc 100644 --- a/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/listByAccompanyingCourse.html.twig +++ b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/listByAccompanyingCourse.html.twig @@ -10,13 +10,13 @@ {% block js %} {{ parent() }} {{ encore_entry_script_tags('mod_answer') }} - {{ encore_entry_script_tags('mod_async_upload') }} + {{ encore_entry_script_tags('mod_document_action_buttons_group') }} {% endblock %} {% block css %} {{ parent() }} {{ encore_entry_link_tags('mod_answer') }} - {{ encore_entry_link_tags('mod_async_upload') }} + {{ encore_entry_link_tags('mod_document_action_buttons_group') }} {% endblock %} {% block content %} diff --git a/src/Bundle/ChillCalendarBundle/Service/DocGenerator/CalendarContext.php b/src/Bundle/ChillCalendarBundle/Service/DocGenerator/CalendarContext.php index 9ba9e36b4..cba7fc661 100644 --- a/src/Bundle/ChillCalendarBundle/Service/DocGenerator/CalendarContext.php +++ b/src/Bundle/ChillCalendarBundle/Service/DocGenerator/CalendarContext.php @@ -18,8 +18,10 @@ use Chill\DocGeneratorBundle\Service\Context\BaseContextData; use Chill\DocStoreBundle\Entity\StoredObject; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\PersonBundle\Entity\Person; +use Chill\PersonBundle\Repository\PersonRepository; use Chill\PersonBundle\Templating\Entity\PersonRender; use Chill\ThirdPartyBundle\Entity\ThirdParty; +use Chill\ThirdPartyBundle\Repository\ThirdPartyRepository; use Chill\ThirdPartyBundle\Templating\Entity\ThirdPartyRender; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bridge\Doctrine\Form\Type\EntityType; @@ -39,6 +41,10 @@ final class CalendarContext implements CalendarContextInterface private PersonRender $personRender; + private PersonRepository $personRepository; + + private ThirdPartyRepository $thirdPartyRepository; + private ThirdPartyRender $thirdPartyRender; private TranslatableStringHelperInterface $translatableStringHelper; @@ -48,14 +54,18 @@ final class CalendarContext implements CalendarContextInterface EntityManagerInterface $entityManager, NormalizerInterface $normalizer, PersonRender $personRender, + PersonRepository $personRepository, ThirdPartyRender $thirdPartyRender, + ThirdPartyRepository $thirdPartyRepository, TranslatableStringHelperInterface $translatableStringHelper ) { $this->baseContextData = $baseContextData; $this->entityManager = $entityManager; $this->normalizer = $normalizer; $this->personRender = $personRender; + $this->personRepository = $personRepository; $this->thirdPartyRender = $thirdPartyRender; + $this->thirdPartyRepository = $thirdPartyRepository; $this->translatableStringHelper = $translatableStringHelper; } @@ -146,7 +156,7 @@ final class CalendarContext implements CalendarContextInterface $options = $this->getOptions($template); $data = array_merge( - $this->baseContextData->getData(), + $this->baseContextData->getData($contextGenerationData['creator'] ?? null), [ 'calendar' => $this->normalizer->normalize($entity, 'docgen', ['docgen:expects' => Calendar::class, 'groups' => ['docgen:read']]), ] @@ -226,8 +236,44 @@ final class CalendarContext implements CalendarContextInterface return true; } + public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array + { + $normalized = []; + $normalized['title'] = $data['title'] ?? ''; + + foreach (['mainPerson', 'thirdParty'] as $k) { + if (isset($data[$k])) { + $normalized[$k] = $data[$k]->getId(); + } + } + + return $normalized; + } + + public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array + { + $denormalized = []; + $denormalized['title'] = $data['title']; + + if (null !== ($data['mainPerson'] ?? null)) { + if (null === $person = $this->personRepository->find($data['mainPerson'])) { + throw new \RuntimeException('person not found'); + } + $denormalized['mainPerson'] = $person; + } + + if (null !== ($data['thirdParty'] ?? null)) { + if (null === $thirdParty = $this->thirdPartyRepository->find($data['thirdParty'])) { + throw new \RuntimeException('third party not found'); + } + $denormalized['thirdParty'] = $thirdParty; + } + + return $denormalized; + } + /** - * @param array{mainPerson?: Person, thirdParty?: ThirdParty, title: string} $contextGenerationData + * param array{mainPerson?: Person, thirdParty?: ThirdParty, title: string} $contextGenerationData */ public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void { diff --git a/src/Bundle/ChillCalendarBundle/Service/DocGenerator/CalendarContextInterface.php b/src/Bundle/ChillCalendarBundle/Service/DocGenerator/CalendarContextInterface.php index d02cdc2c2..eeef3b417 100644 --- a/src/Bundle/ChillCalendarBundle/Service/DocGenerator/CalendarContextInterface.php +++ b/src/Bundle/ChillCalendarBundle/Service/DocGenerator/CalendarContextInterface.php @@ -56,6 +56,10 @@ interface CalendarContextInterface extends DocGeneratorContextWithPublicFormInte */ public function hasPublicForm(DocGeneratorTemplate $template, $entity): bool; + public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array; + + public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array; + /** * @param Calendar $entity */ diff --git a/src/Bundle/ChillCalendarBundle/Tests/Service/DocGenerator/CalendarContextTest.php b/src/Bundle/ChillCalendarBundle/Tests/Service/DocGenerator/CalendarContextTest.php index be31485d4..cbb4ea3af 100644 --- a/src/Bundle/ChillCalendarBundle/Tests/Service/DocGenerator/CalendarContextTest.php +++ b/src/Bundle/ChillCalendarBundle/Tests/Service/DocGenerator/CalendarContextTest.php @@ -205,7 +205,7 @@ final class CalendarContextTest extends TestCase ?NormalizerInterface $normalizer = null ): CalendarContext { $baseContext = $this->prophesize(BaseContextData::class); - $baseContext->getData()->willReturn(['base_context' => 'data']); + $baseContext->getData(null)->willReturn(['base_context' => 'data']); $personRender = $this->prophesize(PersonRender::class); $personRender->renderString(Argument::type(Person::class), [])->willReturn('person name'); diff --git a/src/Bundle/ChillDocGeneratorBundle/Context/DocGeneratorContextWithPublicFormInterface.php b/src/Bundle/ChillDocGeneratorBundle/Context/DocGeneratorContextWithPublicFormInterface.php index f013c8435..4f58bd049 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Context/DocGeneratorContextWithPublicFormInterface.php +++ b/src/Bundle/ChillDocGeneratorBundle/Context/DocGeneratorContextWithPublicFormInterface.php @@ -23,6 +23,9 @@ interface DocGeneratorContextWithPublicFormInterface extends DocGeneratorContext */ public function buildPublicForm(FormBuilderInterface $builder, DocGeneratorTemplate $template, $entity): void; + /** + * Fill the form with initial data + */ public function getFormData(DocGeneratorTemplate $template, $entity): array; /** @@ -31,4 +34,14 @@ interface DocGeneratorContextWithPublicFormInterface extends DocGeneratorContext * @param mixed $entity */ public function hasPublicForm(DocGeneratorTemplate $template, $entity): bool; + + /** + * Transform the data from the form into serializable data, storable into messenger's message + */ + public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array; + + /** + * Reverse the data from the messenger's message into data usable for doc's generation + */ + public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array; } diff --git a/src/Bundle/ChillDocGeneratorBundle/Controller/DocGeneratorTemplateController.php b/src/Bundle/ChillDocGeneratorBundle/Controller/DocGeneratorTemplateController.php index d618f758a..eeab4932b 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Controller/DocGeneratorTemplateController.php +++ b/src/Bundle/ChillDocGeneratorBundle/Controller/DocGeneratorTemplateController.php @@ -16,67 +16,58 @@ use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithPublicFormInterface; use Chill\DocGeneratorBundle\Context\Exception\ContextNotFoundException; use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; use Chill\DocGeneratorBundle\GeneratorDriver\DriverInterface; -use Chill\DocGeneratorBundle\GeneratorDriver\Exception\TemplateException; use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository; +use Chill\DocGeneratorBundle\Service\Generator\GeneratorInterface; +use Chill\DocGeneratorBundle\Service\Messenger\RequestGenerationMessage; use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Service\StoredObjectManagerInterface; use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Serializer\Model\Collection; use Doctrine\ORM\EntityManagerInterface; -use Exception; use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\FileType; -use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; // TODO à mettre dans services use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; -use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Contracts\HttpClient\HttpClientInterface; -use Throwable; use function strlen; +use const JSON_PRETTY_PRINT; final class DocGeneratorTemplateController extends AbstractController { - private HttpClientInterface $client; - private ContextManager $contextManager; private DocGeneratorTemplateRepository $docGeneratorTemplateRepository; - private DriverInterface $driver; - private EntityManagerInterface $entityManager; - private LoggerInterface $logger; + private GeneratorInterface $generator; + + private MessageBusInterface $messageBus; private PaginatorFactory $paginatorFactory; - private StoredObjectManagerInterface $storedObjectManager; - public function __construct( ContextManager $contextManager, DocGeneratorTemplateRepository $docGeneratorTemplateRepository, - DriverInterface $driver, - LoggerInterface $logger, + GeneratorInterface $generator, + MessageBusInterface $messageBus, PaginatorFactory $paginatorFactory, - HttpClientInterface $client, - StoredObjectManagerInterface $storedObjectManager, EntityManagerInterface $entityManager ) { $this->contextManager = $contextManager; $this->docGeneratorTemplateRepository = $docGeneratorTemplateRepository; - $this->driver = $driver; - $this->logger = $logger; + $this->generator = $generator; + $this->messageBus = $messageBus; $this->paginatorFactory = $paginatorFactory; - $this->client = $client; - $this->storedObjectManager = $storedObjectManager; $this->entityManager = $entityManager; } @@ -94,7 +85,6 @@ final class DocGeneratorTemplateController extends AbstractController ): Response { return $this->generateDocFromTemplate( $template, - $entityClassName, $entityId, $request, true @@ -115,7 +105,6 @@ final class DocGeneratorTemplateController extends AbstractController ): Response { return $this->generateDocFromTemplate( $template, - $entityClassName, $entityId, $request, false @@ -185,7 +174,6 @@ final class DocGeneratorTemplateController extends AbstractController private function generateDocFromTemplate( DocGeneratorTemplate $template, - string $entityClassName, int $entityId, Request $request, bool $isTest @@ -206,7 +194,7 @@ final class DocGeneratorTemplateController extends AbstractController if (null === $entity) { throw new NotFoundHttpException( - sprintf('Entity with classname %s and id %s is not found', $entityClassName, $entityId) + sprintf('Entity with classname %s and id %s is not found', $context->getEntityClass(), $entityId) ); } @@ -259,99 +247,68 @@ final class DocGeneratorTemplateController extends AbstractController } } - $document = $template->getFile(); - - if ($isTest && ($contextGenerationData['test_file'] instanceof File)) { - $dataDecrypted = file_get_contents($contextGenerationData['test_file']->getPathname()); - } else { - try { - $dataDecrypted = $this->storedObjectManager->read($document); - } catch (Throwable $exception) { - throw $exception; - } - } + // transform context generation data + $contextGenerationDataSanitized = + $context instanceof DocGeneratorContextWithPublicFormInterface ? + $context->contextGenerationDataNormalize($template, $entity, $contextGenerationData) + : []; + // if is test, render the data or generate the doc if ($isTest && isset($form) && $form['show_data']->getData()) { return $this->render('@ChillDocGenerator/Generator/debug_value.html.twig', [ - 'datas' => json_encode($context->getData($template, $entity, $contextGenerationData), JSON_PRETTY_PRINT) + 'datas' => json_encode($context->getData($template, $entity, $contextGenerationData), JSON_PRETTY_PRINT), ]); - } - - try { - $generatedResource = $this - ->driver - ->generateFromString( - $dataDecrypted, - $template->getFile()->getType(), - $context->getData($template, $entity, $contextGenerationData), - $template->getFile()->getFilename() - ); - } catch (TemplateException $e) { - return new Response( - implode("\n", $e->getErrors()), - 400, - [ - 'Content-Type' => 'text/plain', - ] + } elseif ($isTest) { + $generated = $this->generator->generateDocFromTemplate( + $template, + $entityId, + $contextGenerationDataSanitized, + null, + true, + isset($form) ? $form['test_file']->getData() : null ); - } - if ($isTest) { return new Response( - $generatedResource, + $generated, Response::HTTP_OK, [ 'Content-Transfer-Encoding', 'binary', 'Content-Type' => 'application/vnd.oasis.opendocument.text', 'Content-Disposition' => 'attachment; filename="generated.odt"', - 'Content-Length' => strlen($generatedResource), + 'Content-Length' => strlen($generated), ], ); } - /** @var StoredObject $storedObject */ - $storedObject = (new ObjectNormalizer()) - ->denormalize( - [ - 'type' => $template->getFile()->getType(), - 'filename' => sprintf('%s_odt', uniqid('doc_', true)), - ], - StoredObject::class - ); - - try { - $this->storedObjectManager->write($storedObject, $generatedResource); - } catch (Throwable $exception) { - throw $exception; - } + // this is not a test + // we prepare the object to store the document + $storedObject = (new StoredObject()) + ->setStatus(StoredObject::STATUS_PENDING) + ; $this->entityManager->persist($storedObject); - try { - $context - ->storeGenerated( - $template, - $storedObject, - $entity, - $contextGenerationData - ); - } catch (Exception $e) { - $this - ->logger - ->error( - 'Unable to store the associated document to entity', - [ - 'entityClassName' => $entityClassName, - 'entityId' => $entityId, - 'contextKey' => $context->getName(), - ] - ); - - throw $e; - } + // we store the generated document + $context + ->storeGenerated( + $template, + $storedObject, + $entity, + $contextGenerationData + ); $this->entityManager->flush(); + $this->messageBus->dispatch( + new RequestGenerationMessage( + $this->getUser(), + $template, + $entityId, + $storedObject, + $contextGenerationDataSanitized, + ) + ); + return $this ->redirectToRoute( 'chill_wopi_file_edit', diff --git a/src/Bundle/ChillDocGeneratorBundle/Resources/views/Email/on_generation_failed_email.txt.twig b/src/Bundle/ChillDocGeneratorBundle/Resources/views/Email/on_generation_failed_email.txt.twig new file mode 100644 index 000000000..c4ca7079d --- /dev/null +++ b/src/Bundle/ChillDocGeneratorBundle/Resources/views/Email/on_generation_failed_email.txt.twig @@ -0,0 +1,16 @@ +{{ creator.label }}, + +{{ 'docgen.failure_email.The generation of the document {template_name} failed'|trans({'{template_name}': template.name|localize_translatable_string}) }} + +{{ 'docgen.failure_email.Forward this email to your administrator for solving'|trans }} + +{{ 'docgen.failure_email.References'|trans }}: +{% if errors|length > 0 %} +{{ 'docgen.failure_email.The following errors were encoutered'|trans }}: + +{% for error in errors %} +- {{ error }} +{% endfor %} +{% endif %} +- template_id: {{ template.id }} +- stored_object_destination_id: {{ stored_object_id }} diff --git a/src/Bundle/ChillDocGeneratorBundle/Service/Context/BaseContextData.php b/src/Bundle/ChillDocGeneratorBundle/Service/Context/BaseContextData.php index e7b56ed88..5aed8554b 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Service/Context/BaseContextData.php +++ b/src/Bundle/ChillDocGeneratorBundle/Service/Context/BaseContextData.php @@ -21,18 +21,14 @@ class BaseContextData { private NormalizerInterface $normalizer; - private Security $security; - - public function __construct(Security $security, NormalizerInterface $normalizer) + public function __construct(NormalizerInterface $normalizer) { - $this->security = $security; $this->normalizer = $normalizer; } - public function getData(): array + public function getData(?User $user = null): array { $data = []; - $user = $this->security->getUser(); $data['creator'] = $this->normalizer->normalize( $user instanceof User ? $user : null, diff --git a/src/Bundle/ChillDocGeneratorBundle/Service/Generator/Generator.php b/src/Bundle/ChillDocGeneratorBundle/Service/Generator/Generator.php index 755e608ba..4ddcb461c 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Service/Generator/Generator.php +++ b/src/Bundle/ChillDocGeneratorBundle/Service/Generator/Generator.php @@ -3,16 +3,18 @@ namespace Chill\DocGeneratorBundle\Service\Generator; use Chill\DocGeneratorBundle\Context\ContextManagerInterface; +use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithPublicFormInterface; use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; use Chill\DocGeneratorBundle\GeneratorDriver\DriverInterface; use Chill\DocGeneratorBundle\GeneratorDriver\Exception\TemplateException; use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Service\StoredObjectManagerInterface; +use Chill\MainBundle\Entity\User; use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\File\File; -class Generator +class Generator implements GeneratorInterface { private ContextManagerInterface $contextManager; @@ -24,6 +26,8 @@ class Generator private StoredObjectManagerInterface $storedObjectManager; + private const LOG_PREFIX = '[docgen generator] '; + public function __construct( ContextManagerInterface $contextManager, DriverInterface $driver, @@ -48,18 +52,24 @@ class Generator */ public function generateDocFromTemplate( DocGeneratorTemplate $template, - string $entityClassName, int $entityId, + array $contextGenerationDataNormalized, ?StoredObject $destinationStoredObject = null, bool $isTest = false, - ?File $testFile = null + ?File $testFile = null, + ?User $creator = null ): ?string { if ($destinationStoredObject instanceof StoredObject && StoredObject::STATUS_PENDING !== $destinationStoredObject->getStatus()) { + $this->logger->info(self::LOG_PREFIX.'Aborting generation of an already generated document'); throw new ObjectReadyException(); } + $this->logger->info(self::LOG_PREFIX.'Starting generation of a document', [ + 'entity_id' => $entityId, + 'destination_stored_object' => $destinationStoredObject === null ? null : $destinationStoredObject->getId() + ]); + $context = $this->contextManager->getContextByDocGeneratorTemplate($template); - $contextGenerationData = ['test_file' => $testFile]; $entity = $this ->entityManager @@ -67,22 +77,39 @@ class Generator ; if (null === $entity) { - throw new RelatedEntityNotFoundException($entityClassName, $entityId); + throw new RelatedEntityNotFoundException($template->getEntity(), $entityId); + } + + $contextGenerationDataNormalized = array_merge( + $contextGenerationDataNormalized, + ['creator' => $creator], + $context instanceof DocGeneratorContextWithPublicFormInterface ? + $context->contextGenerationDataDenormalize($template, $entity, $contextGenerationDataNormalized) + : [] + ); + + $data = $context->getData($template, $entity, $contextGenerationDataNormalized); + + $destinationStoredObjectId = $destinationStoredObject instanceof StoredObject ? $destinationStoredObject->getId() : null; + $this->entityManager->clear(); + gc_collect_cycles(); + if (null !== $destinationStoredObjectId) { + $destinationStoredObject = $this->entityManager->find(StoredObject::class, $destinationStoredObjectId); } if ($isTest && ($testFile instanceof File)) { - $dataDecrypted = file_get_contents($testFile->getPathname()); + $templateDecrypted = file_get_contents($testFile->getPathname()); } else { - $dataDecrypted = $this->storedObjectManager->read($template->getFile()); + $templateDecrypted = $this->storedObjectManager->read($template->getFile()); } try { $generatedResource = $this ->driver ->generateFromString( - $dataDecrypted, + $templateDecrypted, $template->getFile()->getType(), - $context->getData($template, $entity, $contextGenerationData), + $data, $template->getFile()->getFilename() ); } catch (TemplateException $e) { @@ -90,6 +117,11 @@ class Generator } if ($isTest) { + $this->logger->info(self::LOG_PREFIX.'Finished generation of a document', [ + 'is_test' => true, + 'entity_id' => $entityId, + 'destination_stored_object' => $destinationStoredObject === null ? null : $destinationStoredObject->getId() + ]); return $generatedResource; } @@ -102,31 +134,13 @@ class Generator $this->storedObjectManager->write($destinationStoredObject, $generatedResource); - try { - $context - ->storeGenerated( - $template, - $destinationStoredObject, - $entity, - $contextGenerationData - ); - } catch (\Exception $e) { - $this - ->logger - ->error( - 'Unable to store the associated document to entity', - [ - 'entityClassName' => $entityClassName, - 'entityId' => $entityId, - 'contextKey' => $context->getName(), - ] - ); - - throw $e; - } - $this->entityManager->flush(); + $this->logger->info(self::LOG_PREFIX.'Finished generation of a document', [ + 'entity_id' => $entityId, + 'destination_stored_object' => $destinationStoredObject->getId(), + ]); + return null; } } diff --git a/src/Bundle/ChillDocGeneratorBundle/Service/Generator/GeneratorException.php b/src/Bundle/ChillDocGeneratorBundle/Service/Generator/GeneratorException.php index 423d59dd4..4360413b4 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Service/Generator/GeneratorException.php +++ b/src/Bundle/ChillDocGeneratorBundle/Service/Generator/GeneratorException.php @@ -1,18 +1,41 @@ */ private array $errors; - public function __construct(array $errors = [], \Throwable $previous = null) + public function __construct(array $errors = [], ?Throwable $previous = null) { $this->errors = $errors; - parent::__construct("Could not generate the document", 15252, - $previous); + parent::__construct( + 'Could not generate the document', + 15252, + $previous + ); + } + + /** + * @return array + */ + public function getErrors(): array + { + return $this->errors; } } diff --git a/src/Bundle/ChillDocGeneratorBundle/Service/Generator/GeneratorInterface.php b/src/Bundle/ChillDocGeneratorBundle/Service/Generator/GeneratorInterface.php new file mode 100644 index 000000000..a8f01b97c --- /dev/null +++ b/src/Bundle/ChillDocGeneratorBundle/Service/Generator/GeneratorInterface.php @@ -0,0 +1,29 @@ +docGeneratorTemplateRepository = $docGeneratorTemplateRepository; + $this->entityManager = $entityManager; + $this->logger = $logger; + $this->mailer = $mailer; + $this->storedObjectRepository = $storedObjectRepository; + $this->translator = $translator; + $this->userRepository = $userRepository; + } + + + public static function getSubscribedEvents() + { + return [ + WorkerMessageFailedEvent::class => 'onMessageFailed' + ]; + } + + public function onMessageFailed(WorkerMessageFailedEvent $event): void + { + if ($event->willRetry()) { + return; + } + + if (!$event->getEnvelope()->getMessage() instanceof RequestGenerationMessage) { + return; + } + + /** @var \Chill\DocGeneratorBundle\Service\Messenger\RequestGenerationMessage $message */ + $message = $event->getEnvelope()->getMessage(); + + $this->logger->error(self::LOG_PREFIX.'Docgen failed', [ + 'stored_object_id' => $message->getDestinationStoredObjectId(), + 'entity_id' => $message->getEntityId(), + 'template_id' => $message->getTemplateId(), + 'creator_id' => $message->getCreatorId(), + 'throwable_class' => get_class($event->getThrowable()), + ]); + + $this->markObjectAsFailed($message); + $this->warnCreator($message, $event); + } + + private function markObjectAsFailed(RequestGenerationMessage $message): void + { + $object = $this->storedObjectRepository->find($message->getDestinationStoredObjectId()); + + if (null === $object) { + $this->logger->error(self::LOG_PREFIX.'Stored object not found', ['stored_object_id', $message->getDestinationStoredObjectId()]); + } + + $object->setStatus(StoredObject::STATUS_FAILURE); + + $this->entityManager->flush(); + } + + private function warnCreator(RequestGenerationMessage $message, WorkerMessageFailedEvent $event): void + { + if (null === $creatorId = $message->getCreatorId()) { + $this->logger->info(self::LOG_PREFIX.'creator id is null'); + return; + } + + if (null === $creator = $this->userRepository->find($creatorId)) { + $this->logger->error(self::LOG_PREFIX.'Creator not found with given id', ['creator_id', $creatorId]); + return; + } + + if (null === $creator->getEmail() || '' === $creator->getEmail()) { + $this->logger->info(self::LOG_PREFIX.'Creator does not have any email', ['user' => $creator->getUsernameCanonical()]); + return; + } + + // if the exception is not a GeneratorException, we try the previous one... + $throwable = $event->getThrowable(); + if (!$throwable instanceof GeneratorException) { + $throwable = $throwable->getPrevious(); + } + + if ($throwable instanceof GeneratorException) { + $errors = $throwable->getErrors(); + } else { + $errors = [$throwable->getTraceAsString()]; + } + + if (null === $template = $this->docGeneratorTemplateRepository->find($message->getTemplateId())) { + $this->logger->info(self::LOG_PREFIX.'Template not found', ['template_id' => $message->getTemplateId()]); + return; + } + + $email = (new TemplatedEmail()) + ->to($creator->getEmail()) + ->subject($this->translator->trans('docgen.failure_email.The generation of a document failed')) + ->textTemplate('@ChillDocGenerator/Email/on_generation_failed_email.txt.twig') + ->context([ + 'errors' => $errors, + 'template' => $template, + 'creator' => $creator, + 'stored_object_id' => $message->getDestinationStoredObjectId(), + ]); + + $this->mailer->send($email); + } +} diff --git a/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/RequestGenerationHandler.php b/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/RequestGenerationHandler.php new file mode 100644 index 000000000..8fef8cf41 --- /dev/null +++ b/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/RequestGenerationHandler.php @@ -0,0 +1,89 @@ +docGeneratorTemplateRepository = $docGeneratorTemplateRepository; + $this->entityManager = $entityManager; + $this->generator = $generator; + $this->logger = $logger; + $this->storedObjectRepository = $storedObjectRepository; + $this->userRepository = $userRepository; + } + + public function __invoke(RequestGenerationMessage $message) + { + if (null === $template = $this->docGeneratorTemplateRepository->find($message->getTemplateId())) { + throw new \RuntimeException('template not found: ' . $message->getTemplateId()); + } + + if (null === $destinationStoredObject = $this->storedObjectRepository->find($message->getDestinationStoredObjectId())) { + throw new \RuntimeException('destination stored object not found : ' . $message->getDestinationStoredObjectId()); + } + + if ($destinationStoredObject->getGenerationTrialsCounter() >= self::AUTHORIZED_TRIALS) { + throw new UnrecoverableMessageHandlingException('maximum number of retry reached'); + } + + $creator = $this->userRepository->find($message->getCreatorId()); + + $destinationStoredObject->addGenerationTrial(); + $this->entityManager->createQuery('UPDATE '.StoredObject::class.' s SET s.generationTrialsCounter = s.generationTrialsCounter + 1 WHERE s.id = :id') + ->setParameter('id', $destinationStoredObject->getId()) + ->execute(); + + $this->generator->generateDocFromTemplate( + $template, + $message->getEntityId(), + $message->getContextGenerationData(), + $destinationStoredObject, + false, + null, + $creator + ); + + $this->logger->info(self::LOG_PREFIX.'Request generation finished', [ + 'template_id' => $message->getTemplateId(), + 'destination_stored_object' => $message->getDestinationStoredObjectId(), + 'duration_int' => (new \DateTimeImmutable('now'))->getTimestamp() - $message->getCreatedAt()->getTimestamp(), + ]); + } +} diff --git a/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/RequestGenerationMessage.php b/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/RequestGenerationMessage.php index 1489e2686..8362e4ecc 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/RequestGenerationMessage.php +++ b/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/RequestGenerationMessage.php @@ -3,6 +3,7 @@ namespace Chill\DocGeneratorBundle\Service\Messenger; use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; +use Chill\DocStoreBundle\Entity\StoredObject; use Chill\MainBundle\Entity\User; class RequestGenerationMessage @@ -13,14 +14,25 @@ class RequestGenerationMessage private int $entityId; - private string $entityClassName; + private int $destinationStoredObjectId; - public function __construct(User $creator, DocGeneratorTemplate $template, int $entityId, string $entityClassName) - { + private array $contextGenerationData; + + private \DateTimeImmutable $createdAt; + + public function __construct( + User $creator, + DocGeneratorTemplate $template, + int $entityId, + StoredObject $destinationStoredObject, + array $contextGenerationData + ) { $this->creatorId = $creator->getId(); $this->templateId = $template->getId(); $this->entityId = $entityId; - $this->entityClassName = $entityClassName; + $this->destinationStoredObjectId = $destinationStoredObject->getId(); + $this->contextGenerationData = $contextGenerationData; + $this->createdAt = new \DateTimeImmutable('now'); } public function getCreatorId(): int @@ -28,6 +40,11 @@ class RequestGenerationMessage return $this->creatorId; } + public function getDestinationStoredObjectId(): int + { + return $this->destinationStoredObjectId; + } + public function getTemplateId(): int { return $this->templateId; @@ -38,8 +55,13 @@ class RequestGenerationMessage return $this->entityId; } - public function getEntityClassName(): string + public function getContextGenerationData(): array { - return $this->entityClassName; + return $this->contextGenerationData; + } + + public function getCreatedAt(): \DateTimeImmutable + { + return $this->createdAt; } } diff --git a/src/Bundle/ChillDocGeneratorBundle/config/services.yaml b/src/Bundle/ChillDocGeneratorBundle/config/services.yaml index 5bdfe2a11..5fef6fb22 100644 --- a/src/Bundle/ChillDocGeneratorBundle/config/services.yaml +++ b/src/Bundle/ChillDocGeneratorBundle/config/services.yaml @@ -20,10 +20,14 @@ services: resource: '../Serializer/Normalizer/' tags: - { name: 'serializer.normalizer', priority: -152 } + Chill\DocGeneratorBundle\Serializer\Normalizer\CollectionDocGenNormalizer: tags: - { name: 'serializer.normalizer', priority: -126 } + Chill\DocGeneratorBundle\Service\Context\: + resource: "../Service/Context" + Chill\DocGeneratorBundle\Controller\: resource: "../Controller" autowire: true @@ -34,18 +38,20 @@ services: autowire: true autoconfigure: true - Chill\DocGeneratorBundle\Service\Context\: - resource: "../Service/Context/" - autowire: true - autoconfigure: true - Chill\DocGeneratorBundle\GeneratorDriver\: resource: "../GeneratorDriver/" autowire: true autoconfigure: true + Chill\DocGeneratorBundle\Service\Messenger\: + resource: "../Service/Messenger/" + + Chill\DocGeneratorBundle\Service\Generator\Generator: ~ + Chill\DocGeneratorBundle\Service\Generator\GeneratorInterface: '@Chill\DocGeneratorBundle\Service\Generator\Generator' + Chill\DocGeneratorBundle\Driver\RelatorioDriver: '@Chill\DocGeneratorBundle\Driver\DriverInterface' Chill\DocGeneratorBundle\Context\ContextManager: arguments: $contexts: !tagged_iterator { tag: chill_docgen.context, default_index_method: getKey } + Chill\DocGeneratorBundle\Context\ContextManagerInterface: '@Chill\DocGeneratorBundle\Context\ContextManager' diff --git a/src/Bundle/ChillDocGeneratorBundle/migrations/Version20230214192558.php b/src/Bundle/ChillDocGeneratorBundle/migrations/Version20230214192558.php new file mode 100644 index 000000000..de536ca84 --- /dev/null +++ b/src/Bundle/ChillDocGeneratorBundle/migrations/Version20230214192558.php @@ -0,0 +1,47 @@ +addSql('ALTER TABLE chill_doc.stored_object ADD template_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE chill_doc.stored_object ADD status TEXT DEFAULT \'ready\' NOT NULL'); + $this->addSql('ALTER TABLE chill_doc.stored_object ADD createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL'); + $this->addSql('UPDATE chill_doc.stored_object SET createdAt = creation_date'); + $this->addSql('ALTER TABLE chill_doc.stored_object ADD createdBy_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE chill_doc.stored_object DROP creation_date;'); + $this->addSql('ALTER TABLE chill_doc.stored_object ALTER type SET DEFAULT \'\''); + $this->addSql('ALTER TABLE chill_doc.stored_object ALTER title DROP DEFAULT'); + $this->addSql('COMMENT ON COLUMN chill_doc.stored_object.createdAt IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE chill_doc.stored_object ADD CONSTRAINT FK_49604E365DA0FB8 FOREIGN KEY (template_id) REFERENCES chill_docgen_template (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_doc.stored_object ADD CONSTRAINT FK_49604E363174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('CREATE INDEX IDX_49604E365DA0FB8 ON chill_doc.stored_object (template_id)'); + $this->addSql('CREATE INDEX IDX_49604E363174800F ON chill_doc.stored_object (createdBy_id)'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE chill_doc.stored_object DROP CONSTRAINT FK_49604E365DA0FB8'); + $this->addSql('ALTER TABLE chill_doc.stored_object DROP CONSTRAINT FK_49604E363174800F'); + $this->addSql('ALTER TABLE chill_doc.stored_object DROP template_id'); + $this->addSql('ALTER TABLE chill_doc.stored_object DROP status'); + $this->addSql('ALTER TABLE chill_doc.stored_object ADD creation_date TIMESTAMP(0) DEFAULT NOW()'); + $this->addSql('UPDATE chill_doc.stored_object SET creation_date = createdAt'); + $this->addSql('ALTER TABLE chill_doc.stored_object DROP createdAt'); + $this->addSql('ALTER TABLE chill_doc.stored_object DROP createdBy_id'); + $this->addSql('ALTER TABLE chill_doc.stored_object ALTER title SET DEFAULT \'\''); + $this->addSql('ALTER TABLE chill_doc.stored_object ALTER type DROP DEFAULT'); + } +} diff --git a/src/Bundle/ChillDocGeneratorBundle/tests/Service/Context/Generator/GeneratorTest.php b/src/Bundle/ChillDocGeneratorBundle/tests/Service/Context/Generator/GeneratorTest.php index 2e68b443c..066715b60 100644 --- a/src/Bundle/ChillDocGeneratorBundle/tests/Service/Context/Generator/GeneratorTest.php +++ b/src/Bundle/ChillDocGeneratorBundle/tests/Service/Context/Generator/GeneratorTest.php @@ -26,13 +26,14 @@ class GeneratorTest extends TestCase $template = (new DocGeneratorTemplate())->setFile($templateStoredObject = (new StoredObject()) ->setType('application/test')); $destinationStoredObject = (new StoredObject())->setStatus(StoredObject::STATUS_PENDING); + $reflection = new \ReflectionClass($destinationStoredObject); + $reflection->getProperty('id')->setAccessible(true); + $reflection->getProperty('id')->setValue($destinationStoredObject, 1); $entity = new class {}; $data = []; $context = $this->prophesize(DocGeneratorContextInterface::class); $context->getData($template, $entity, Argument::type('array'))->willReturn($data); - $context->storeGenerated($template, $destinationStoredObject, $entity, Argument::type('array')) - ->shouldBeCalled(); $context->getName()->willReturn('dummy_context'); $context->getEntityClass()->willReturn('DummyClass'); $context = $context->reveal(); @@ -46,8 +47,11 @@ class GeneratorTest extends TestCase ->willReturn('generated'); $entityManager = $this->prophesize(EntityManagerInterface::class); - $entityManager->find(Argument::type('string'), Argument::type('int')) + $entityManager->find(StoredObject::class, 1) + ->willReturn($destinationStoredObject); + $entityManager->find('DummyClass', Argument::type('int')) ->willReturn($entity); + $entityManager->clear()->shouldBeCalled(); $entityManager->flush()->shouldBeCalled(); $storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class); @@ -65,8 +69,8 @@ class GeneratorTest extends TestCase $generator->generateDocFromTemplate( $template, - 'DummyEntity', 1, + [], $destinationStoredObject ); } @@ -89,8 +93,8 @@ class GeneratorTest extends TestCase $generator->generateDocFromTemplate( $template, - 'DummyEntity', 1, + [], $destinationStoredObject ); } @@ -102,6 +106,9 @@ class GeneratorTest extends TestCase $template = (new DocGeneratorTemplate())->setFile($templateStoredObject = (new StoredObject()) ->setType('application/test')); $destinationStoredObject = (new StoredObject())->setStatus(StoredObject::STATUS_PENDING); + $reflection = new \ReflectionClass($destinationStoredObject); + $reflection->getProperty('id')->setAccessible(true); + $reflection->getProperty('id')->setValue($destinationStoredObject, 1); $context = $this->prophesize(DocGeneratorContextInterface::class); $context->getName()->willReturn('dummy_context'); @@ -126,8 +133,8 @@ class GeneratorTest extends TestCase $generator->generateDocFromTemplate( $template, - 'DummyEntity', 1, + [], $destinationStoredObject ); } diff --git a/src/Bundle/ChillDocGeneratorBundle/translations/messages.fr.yml b/src/Bundle/ChillDocGeneratorBundle/translations/messages.fr.yml index bf37ff838..d0950482e 100644 --- a/src/Bundle/ChillDocGeneratorBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillDocGeneratorBundle/translations/messages.fr.yml @@ -10,6 +10,16 @@ docgen: test generate: Tester la génération With context %name%: 'Avec le contexte "%name%"' + Doc generation failed: La génération de ce document a échoué + Doc generation is pending: La génération de ce document est en cours + Come back later: Revenir plus tard + + failure_email: + The generation of a document failed: La génération d'un document a échoué + The generation of the document {template_name} failed: La génération d'un document à partir du modèle {{ template_name }} a échoué. + The following errors were encoutered: Les erreurs suivantes ont été rencontrées + Forward this email to your administrator for solving: Faites suivre ce message vers votre administrateur pour la résolution du problème. + References: Références crud: docgen_template: @@ -19,4 +29,4 @@ crud: Show data instead of generating: Montrer les données au lieu de générer le document -Template file: Fichier modèle \ No newline at end of file +Template file: Fichier modèle diff --git a/src/Bundle/ChillDocStoreBundle/Controller/StoredObjectApiController.php b/src/Bundle/ChillDocStoreBundle/Controller/StoredObjectApiController.php new file mode 100644 index 000000000..29b978ffb --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Controller/StoredObjectApiController.php @@ -0,0 +1,39 @@ +security = $security; + } + + /** + * @Route("/api/1.0/doc-store/stored-object/{uuid}/is-ready") + */ + public function isDocumentReady(StoredObject $storedObject): Response + { + if (!$this->security->isGranted('ROLE_USER')) { + throw new AccessDeniedHttpException(); + } + + return new JsonResponse( + [ + 'id' => $storedObject->getId(), + 'filename' => $storedObject->getFilename(), + 'status' => $storedObject->getStatus(), + 'type' => $storedObject->getType(), + ] + ); + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Entity/StoredObject.php b/src/Bundle/ChillDocStoreBundle/Entity/StoredObject.php index 21abc1f56..8821ead7c 100644 --- a/src/Bundle/ChillDocStoreBundle/Entity/StoredObject.php +++ b/src/Bundle/ChillDocStoreBundle/Entity/StoredObject.php @@ -41,12 +41,6 @@ class StoredObject implements AsyncFileInterface, Document, TrackCreationInterfa use TrackCreationTrait; - /** - * @ORM\Column(type="datetime", name="creation_date") - * @Serializer\Groups({"read", "write"}) - */ - private DateTimeInterface $creationDate; - /** * @ORM\Column(type="json", name="datas") * @Serializer\Groups({"read", "write"}) @@ -87,7 +81,7 @@ class StoredObject implements AsyncFileInterface, Document, TrackCreationInterfa private string $title = ''; /** - * @ORM\Column(type="text", name="type") + * @ORM\Column(type="text", name="type", options={"default": ""}) * @Serializer\Groups({"read", "write"}) */ private string $type = ''; @@ -105,9 +99,20 @@ class StoredObject implements AsyncFileInterface, Document, TrackCreationInterfa /** * @ORM\Column(type="text", options={"default": "ready"}) + * @Serializer\Groups({"read"}) */ private string $status; + /** + * Store the number of times a generation has been tryied for this StoredObject. + * + * This is a workaround, as generation consume lot of memory, and out-of-memory errors + * are not handled by messenger. + * + * @ORM\Column(type="integer", options={"default": 0}) + */ + private int $generationTrialsCounter = 0; + /** * @param StoredObject::STATUS_* $status */ @@ -117,8 +122,16 @@ class StoredObject implements AsyncFileInterface, Document, TrackCreationInterfa $this->status = $status; } + public function addGenerationTrial(): self + { + $this->generationTrialsCounter++; + + return $this; + } + /** * @Serializer\Groups({"read", "write"}) + * @deprecated */ public function getCreationDate(): DateTime { @@ -135,6 +148,11 @@ class StoredObject implements AsyncFileInterface, Document, TrackCreationInterfa return $this->filename; } + public function getGenerationTrialsCounter(): int + { + return $this->generationTrialsCounter; + } + public function getId(): ?int { return $this->id; @@ -158,6 +176,9 @@ class StoredObject implements AsyncFileInterface, Document, TrackCreationInterfa return $this->getFilename(); } + /** + * @return StoredObject::STATUS_* + */ public function getStatus(): string { return $this->status; @@ -185,6 +206,7 @@ class StoredObject implements AsyncFileInterface, Document, TrackCreationInterfa /** * @Serializer\Groups({"write"}) + * @deprecated */ public function setCreationDate(DateTime $creationDate): self { @@ -244,4 +266,30 @@ class StoredObject implements AsyncFileInterface, Document, TrackCreationInterfa return $this; } + + public function getTemplate(): ?DocGeneratorTemplate + { + return $this->template; + } + + public function hasTemplate(): bool + { + return null !== $this->template; + } + + public function setTemplate(?DocGeneratorTemplate $template): StoredObject + { + $this->template = $template; + return $this; + } + + public function isPending(): bool + { + return self::STATUS_PENDING === $this->getStatus(); + } + + public function isFailure(): bool + { + return self::STATUS_FAILURE === $this->getStatus(); + } } diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/module/document_action_buttons_group/index.ts b/src/Bundle/ChillDocStoreBundle/Resources/public/module/document_action_buttons_group/index.ts index ec1d50a86..4180808dd 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/public/module/document_action_buttons_group/index.ts +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/module/document_action_buttons_group/index.ts @@ -1,7 +1,8 @@ import {_createI18n} from "../../../../../ChillMainBundle/Resources/public/vuejs/_js/i18n"; import DocumentActionButtonsGroup from "../../vuejs/DocumentActionButtonsGroup.vue"; import {createApp} from "vue"; -import {StoredObject} from "../../types"; +import {StoredObject, StoredObjectStatusChange} from "../../types"; +import {is_object_ready} from "../../vuejs/StoredObjectButton/helpers"; const i18n = _createI18n({}); @@ -15,19 +16,32 @@ window.addEventListener('DOMContentLoaded', function (e) { filename: string, canEdit: string, storedObject: string, - small: string, + buttonSmall: string, }; const - storedObject = JSON.parse(datasets.storedObject), + storedObject = JSON.parse(datasets.storedObject) as StoredObject, filename = datasets.filename, canEdit = datasets.canEdit === '1', - small = datasets.small === '1' + small = datasets.buttonSmall === '1' ; return { storedObject, filename, canEdit, small }; }, - template: '', + template: '', + methods: { + onStoredObjectStatusChange: function(newStatus: StoredObjectStatusChange): void { + this.$data.storedObject.status = newStatus.status; + this.$data.storedObject.filename = newStatus.filename; + this.$data.storedObject.type = newStatus.type; + + // remove eventual div which inform pending status + document.querySelectorAll(`[data-docgen-is-pending="${this.$data.storedObject.id}"]`) + .forEach(function(el) { + el.remove(); + }); + } + } }); app.use(i18n).mount(el); diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/types.ts b/src/Bundle/ChillDocStoreBundle/Resources/public/types.ts index 918526117..825055973 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/public/types.ts +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/types.ts @@ -1,5 +1,7 @@ import {DateTime} from "../../../ChillMainBundle/Resources/public/types"; +export type StoredObjectStatus = "ready"|"failure"|"pending"; + export interface StoredObject { id: number, @@ -13,7 +15,15 @@ export interface StoredObject { keyInfos: object, title: string, type: string, - uuid: string + uuid: string, + status: StoredObjectStatus, +} + +export interface StoredObjectStatusChange { + id: number, + filename: string, + status: StoredObjectStatus, + type: string, } /** diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentActionButtonsGroup.vue b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentActionButtonsGroup.vue index 60b368cd3..88587e90f 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentActionButtonsGroup.vue +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/DocumentActionButtonsGroup.vue @@ -1,6 +1,6 @@