From 37227a3aeb2df1dfb74dc3e35cc0a1489f550ed6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 3 Feb 2025 21:15:00 +0000 Subject: [PATCH] Add attachments to workflow --- config/routes/chill_assets_dev.yaml | 19 ++ config/routes/chill_swagger.yaml | 12 + package.json | 8 +- .../GenericDoc/activity_document.html.twig | 82 +---- .../activity_document_row.html.twig | 81 +++++ ...yingPeriodActivityGenericDocNormalizer.php | 54 ++++ ...anyingPeriodActivityGenericDocProvider.php | 41 +++ ...anyingPeriodActivityGenericDocRenderer.php | 7 +- .../Repository/CalendarDocRepository.php | 18 ++ .../CalendarDocRepositoryInterface.php | 3 + .../store/modules/calendarRanges.ts | 19 +- .../GenericDoc/calendar_document.html.twig | 68 +---- .../calendar_document_row.html.twig | 75 +++++ ...yingPeriodCalendarGenericDocNormalizer.php | 51 ++++ ...anyingPeriodCalendarGenericDocProvider.php | 34 ++- ...anyingPeriodCalendarGenericDocRenderer.php | 7 +- ...ccompanyingPeriodListApiControllerTest.php | 82 +++++ .../ChillDocStoreBundle.php | 3 + ...ericDocForAccompanyingPeriodController.php | 7 +- ...ForAccompanyingPeriodListApiController.php | 57 ++++ .../Controller/GenericDocForPerson.php | 4 +- .../ChillDocStoreBundle/Entity/Document.php | 20 +- .../AssociatedStoredObjectNotFound.php | 20 ++ .../NotNormalizableGenericDocException.php | 14 + .../Exception/UnexpectedValueException.php | 14 + ...ForAccompanyingPeriodProviderInterface.php | 2 +- .../GenericDocNormalizerInterface.php | 30 ++ .../GenericDocProviderInterface.php | 38 +++ .../GenericDoc/Manager.php | 73 ++++- .../GenericDoc/ManagerInterface.php | 64 ++++ ...yingCourseDocumentGenericDocNormalizer.php | 56 ++++ .../PersonDocumentGenericDocNormalizer.php | 51 ++++ ...anyingCourseDocumentGenericDocProvider.php | 52 +++- .../PersonDocumentGenericDocProvider.php | 33 ++ ...anyingCourseDocumentGenericDocRenderer.php | 9 + .../Twig/GenericDocRendererInterface.php | 14 + .../PersonDocumentACLAwareRepository.php | 16 +- .../Resources/public/js/generic-doc-api.ts | 10 + .../document_action_buttons_group/index.ts | 2 +- .../Resources/public/types/generic_doc.ts | 71 +++++ .../public/{types.ts => types/index.ts} | 14 +- .../StoredObjectButton/DownloadButton.vue | 2 +- .../Resources/views/List/list_item.html.twig | 119 +------- .../views/List/list_item_row.html.twig | 119 ++++++++ ...public_view_with_document_render.html.twig | 20 +- .../Authorization/StoredObjectVoter.php | 19 +- .../Normalizer/GenericDocNormalizer.php | 67 +++++ .../Tests/GenericDoc/ManagerTest.php | 76 +++++ ...ngCourseDocumentGenericDocProviderTest.php | 4 +- .../PersonDocumentGenericDocProviderTest.php | 7 +- .../Authorization/StoredObjectVoterTest.php | 3 +- .../Normalizer/GenericDocNormalizerTest.php | 75 +++++ ...ompanyingCourseDocumentWorkflowHandler.php | 10 + .../WorkflowWithPublicViewDocumentHelper.php | 1 + .../ChillDocStoreBundle/chill.api.specs.yaml | 122 +++++--- .../ChillDocStoreBundle/config/services.yaml | 7 + .../migrations/Version20241212112733.php | 45 +++ .../translations/messages.fr.yml | 1 + .../WorkflowAttachmentController.php | 101 +++++++ .../Controller/WorkflowController.php | 1 + .../Entity/Workflow/EntityWorkflow.php | 49 ++- .../Workflow/EntityWorkflowAttachment.php | 80 +++++ .../Entity/Workflow/EntityWorkflowComment.php | 5 + .../Entity/Workflow/EntityWorkflowStep.php | 49 +-- .../Workflow/EntityWorkflowStepSignature.php | 1 + .../EntityWorkflowAttachmentRepository.php | 67 +++++ .../Resources/public/chill/chillmain.scss | 2 +- .../Resources/public/lib/api/apiMethods.ts | 27 +- .../public/lib/workflow/attachments.ts | 22 ++ .../ChillMainBundle/Resources/public/types.ts | 13 + .../public/vuejs/WorkflowAttachment/App.vue | 70 +++++ .../Component/AttachmentList.vue | 52 ++++ .../Component/GenericDocItemBox.vue | 18 ++ .../Component/PickGenericDoc.vue | 281 ++++++++++++++++++ .../Component/PickGenericDocItem.vue | 95 ++++++ .../Component/PickGenericDocModal.vue | 113 +++++++ .../public/vuejs/WorkflowAttachment/index.ts | 109 +++++++ .../views/Workflow/_attachment.html.twig | 124 +------- .../Resources/views/Workflow/index.html.twig | 4 + .../EntityWorkflowAttachmentNormalizer.php | 54 ++++ .../WorkflowViewSendPublicControllerTest.php | 6 + .../Attachment/AddAttachmentActionTest.php | 66 ++++ .../Attachment/AddAttachmentAction.php | 42 +++ .../Attachment/AddAttachmentRequestDTO.php | 30 ++ .../EntityWorkflowHandlerInterface.php | 6 + .../Workflow/EntityWorkflowManager.php | 6 + .../WorkflowRelatedEntityPermissionHelper.php | 11 +- .../ChillMainBundle/chill.api.specs.yaml | 111 +++++++ .../ChillMainBundle/chill.webpack.config.js | 4 + .../ChillMainBundle/config/services.yaml | 2 + .../migrations/Version20241129112740.php | 51 ++++ .../translations/messages.fr.yml | 3 + ...companyingPeriodWorkEvaluationDocument.php | 23 +- .../GenericDoc/evaluation_document.html.twig | 82 +---- .../evaluation_document_row.html.twig | 82 +++++ ...riodWorkEvaluationGenericDocNormalizer.php | 51 ++++ ...PeriodWorkEvaluationGenericDocProvider.php | 35 ++- ...PeriodWorkEvaluationGenericDocRenderer.php | 7 +- ...ngPeriodCalendarGenericDocProviderTest.php | 10 +- ...odWorkEvaluationGenericDocProviderTest.php | 9 +- ...dWorkEvaluationDocumentWorkflowHandler.php | 7 + ...ingPeriodWorkEvaluationWorkflowHandler.php | 6 + .../AccompanyingPeriodWorkWorkflowHandler.php | 6 + .../migrations/Version20241212120202.php | 35 +++ tsconfig.json | 6 +- yarn.lock | 8 +- 106 files changed, 3455 insertions(+), 619 deletions(-) create mode 100644 config/routes/chill_assets_dev.yaml create mode 100644 config/routes/chill_swagger.yaml create mode 100644 src/Bundle/ChillActivityBundle/Resources/views/GenericDoc/activity_document_row.html.twig create mode 100644 src/Bundle/ChillActivityBundle/Service/GenericDoc/Normalizer/AccompanyingPeriodActivityGenericDocNormalizer.php create mode 100644 src/Bundle/ChillCalendarBundle/Resources/views/GenericDoc/calendar_document_row.html.twig create mode 100644 src/Bundle/ChillCalendarBundle/Service/GenericDoc/Normalizer/AccompanyingPeriodCalendarGenericDocNormalizer.php create mode 100644 src/Bundle/ChillDocGeneratorBundle/Tests/Controller/GenericDocForAccompanyingPeriodListApiControllerTest.php create mode 100644 src/Bundle/ChillDocStoreBundle/Controller/GenericDocForAccompanyingPeriodListApiController.php create mode 100644 src/Bundle/ChillDocStoreBundle/GenericDoc/Exception/AssociatedStoredObjectNotFound.php create mode 100644 src/Bundle/ChillDocStoreBundle/GenericDoc/Exception/NotNormalizableGenericDocException.php create mode 100644 src/Bundle/ChillDocStoreBundle/GenericDoc/Exception/UnexpectedValueException.php create mode 100644 src/Bundle/ChillDocStoreBundle/GenericDoc/GenericDocNormalizerInterface.php create mode 100644 src/Bundle/ChillDocStoreBundle/GenericDoc/GenericDocProviderInterface.php create mode 100644 src/Bundle/ChillDocStoreBundle/GenericDoc/ManagerInterface.php create mode 100644 src/Bundle/ChillDocStoreBundle/GenericDoc/Normalizer/AccompanyingCourseDocumentGenericDocNormalizer.php create mode 100644 src/Bundle/ChillDocStoreBundle/GenericDoc/Normalizer/PersonDocumentGenericDocNormalizer.php create mode 100644 src/Bundle/ChillDocStoreBundle/Resources/public/js/generic-doc-api.ts create mode 100644 src/Bundle/ChillDocStoreBundle/Resources/public/types/generic_doc.ts rename src/Bundle/ChillDocStoreBundle/Resources/public/{types.ts => types/index.ts} (90%) create mode 100644 src/Bundle/ChillDocStoreBundle/Resources/views/List/list_item_row.html.twig create mode 100644 src/Bundle/ChillDocStoreBundle/Serializer/Normalizer/GenericDocNormalizer.php create mode 100644 src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/GenericDocNormalizerTest.php create mode 100644 src/Bundle/ChillDocStoreBundle/migrations/Version20241212112733.php create mode 100644 src/Bundle/ChillMainBundle/Controller/WorkflowAttachmentController.php create mode 100644 src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowAttachment.php create mode 100644 src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowAttachmentRepository.php create mode 100644 src/Bundle/ChillMainBundle/Resources/public/lib/workflow/attachments.ts create mode 100644 src/Bundle/ChillMainBundle/Resources/public/vuejs/WorkflowAttachment/App.vue create mode 100644 src/Bundle/ChillMainBundle/Resources/public/vuejs/WorkflowAttachment/Component/AttachmentList.vue create mode 100644 src/Bundle/ChillMainBundle/Resources/public/vuejs/WorkflowAttachment/Component/GenericDocItemBox.vue create mode 100644 src/Bundle/ChillMainBundle/Resources/public/vuejs/WorkflowAttachment/Component/PickGenericDoc.vue create mode 100644 src/Bundle/ChillMainBundle/Resources/public/vuejs/WorkflowAttachment/Component/PickGenericDocItem.vue create mode 100644 src/Bundle/ChillMainBundle/Resources/public/vuejs/WorkflowAttachment/Component/PickGenericDocModal.vue create mode 100644 src/Bundle/ChillMainBundle/Resources/public/vuejs/WorkflowAttachment/index.ts create mode 100644 src/Bundle/ChillMainBundle/Serializer/Normalizer/EntityWorkflowAttachmentNormalizer.php create mode 100644 src/Bundle/ChillMainBundle/Tests/Workflow/Attachment/AddAttachmentActionTest.php create mode 100644 src/Bundle/ChillMainBundle/Workflow/Attachment/AddAttachmentAction.php create mode 100644 src/Bundle/ChillMainBundle/Workflow/Attachment/AddAttachmentRequestDTO.php create mode 100644 src/Bundle/ChillMainBundle/migrations/Version20241129112740.php create mode 100644 src/Bundle/ChillPersonBundle/Resources/views/GenericDoc/evaluation_document_row.html.twig create mode 100644 src/Bundle/ChillPersonBundle/Service/GenericDoc/Normalizer/AccompanyingPeriodWorkEvaluationGenericDocNormalizer.php create mode 100644 src/Bundle/ChillPersonBundle/migrations/Version20241212120202.php diff --git a/config/routes/chill_assets_dev.yaml b/config/routes/chill_assets_dev.yaml new file mode 100644 index 000000000..ea4a9c424 --- /dev/null +++ b/config/routes/chill_assets_dev.yaml @@ -0,0 +1,19 @@ +when@dev: + sass_assets: + path: /_dev/assets + controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController + defaults: + template: '@ChillMain/Dev/dev.assets.html.twig' + + sass_assets_test1: + path: /_dev/assets_test1 + controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController + defaults: + template: '@ChillMain/Dev/dev.assets.test1.html.twig' + + sass_assets_test2: + path: /_dev/assets_test2 + controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController + defaults: + template: '@ChillMain/Dev/dev.assets.test2.html.twig' + diff --git a/config/routes/chill_swagger.yaml b/config/routes/chill_swagger.yaml new file mode 100644 index 000000000..fc2d6b0b2 --- /dev/null +++ b/config/routes/chill_swagger.yaml @@ -0,0 +1,12 @@ +when@dev: + swagger_ui: + path: /_dev/swagger + controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController + defaults: + template: '@ChillMain/Dev/swagger-ui/index.html.twig' + + swagger_specs: + path: /_dev/specs.yaml + controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController + defaults: + template: api/specs.yaml diff --git a/package.json b/package.json index 6cdc61668..397676b1b 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "@eslint/js": "^9.14.0", "@luminateone/eslint-baseline": "^1.0.9", "@symfony/webpack-encore": "^4.1.0", - "@tsconfig/node14": "^1.0.1", + "@tsconfig/node20": "^20.1.4", "@types/dompurify": "^3.0.5", "@types/eslint__js": "^8.42.3", "@typescript-eslint/parser": "^8.12.2", @@ -30,7 +30,6 @@ "eslint-plugin-vue": "^9.30.0", "fork-awesome": "^1.1.7", "jquery": "^3.6.0", - "marked": "^12.0.1", "node-sass": "^8.0.0", "popper.js": "^1.16.1", "postcss-loader": "^7.0.2", @@ -80,6 +79,11 @@ "dev": "encore dev", "watch": "encore dev --watch", "build": "encore production --progress", + "specs-build": "yaml-merge src/Bundle/ChillMainBundle/chill.api.specs.yaml src/Bundle/ChillPersonBundle/chill.api.specs.yaml src/Bundle/ChillCalendarBundle/chill.api.specs.yaml src/Bundle/ChillThirdPartyBundle/chill.api.specs.yaml src/Bundle/ChillDocStoreBundle/chill.api.specs.yaml> templates/api/specs.yaml", + "specs-validate": "swagger-cli validate templates/api/specs.yaml", + "specs-create-dir": "mkdir -p templates/api", + "specs": "yarn run specs-create-dir && yarn run specs-build && yarn run specs-validate", + "version": "node --version", "eslint": "npx eslint-baseline --fix \"src/**/*.{js,ts,vue}\"" }, "private": true diff --git a/src/Bundle/ChillActivityBundle/Resources/views/GenericDoc/activity_document.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/GenericDoc/activity_document.html.twig index 5b9547675..1da058332 100644 --- a/src/Bundle/ChillActivityBundle/Resources/views/GenericDoc/activity_document.html.twig +++ b/src/Bundle/ChillActivityBundle/Resources/views/GenericDoc/activity_document.html.twig @@ -1,83 +1,3 @@ -{% import "@ChillDocStore/Macro/macro.html.twig" as m %} -{% import "@ChillDocStore/Macro/macro_mimeicon.html.twig" as mm %} -{% import '@ChillPerson/Macro/updatedBy.html.twig' as mmm %} - -{% set person_id = null %} -{% if activity.person %} - {% set person_id = activity.person.id %} -{% endif %} - -{% set accompanying_course_id = null %} -{% if activity.accompanyingPeriod %} - {% set accompanying_course_id = activity.accompanyingPeriod.id %} -{% endif %} -
-
-
- {% if document.isPending %} -
{{ 'docgen.Doc generation is pending'|trans }}
- {% elseif document.isFailure %} -
{{ 'docgen.Doc generation failed'|trans }}
- {% endif %} - -
- {% if activity.accompanyingPeriod is not null and context == 'person' %} - - {{ activity.accompanyingPeriod.id }} -   - {% endif %} -
- - - {{ activity.type.name | localize_translatable_string }} - {% if activity.emergency %} - {{ 'Emergency'|trans|upper }} - {% endif %} - -
-
-
- {{ document.title|chill_print_or_message("No title") }} -
- {% if document.hasTemplate %} -
-

{{ document.template.name|localize_translatable_string }}

-
- {% endif %} -
- -
-
-
- {{ document.createdAt|format_date('short') }} -
-
-
-
- - -
-
- {{ mmm.createdBy(document) }} -
-
    - {% if is_granted('CHILL_ACTIVITY_SEE_DETAILS', activity) %} -
  • - {{ document|chill_document_button_group(document.title, is_granted('CHILL_ACTIVITY_UPDATE', activity), {small: false}) }} -
  • - {% endif %} - {% if is_granted('CHILL_ACTIVITY_SEE', activity)%} -
  • - -
  • - {% endif %} - {% if is_granted('CHILL_ACTIVITY_UPDATE', activity) %} -
  • - -
  • - {% endif %} -
- -
+ {{ include('@ChillActivity/GenericDoc/activity_document_row.html.twig') }}
diff --git a/src/Bundle/ChillActivityBundle/Resources/views/GenericDoc/activity_document_row.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/GenericDoc/activity_document_row.html.twig new file mode 100644 index 000000000..6ca871de1 --- /dev/null +++ b/src/Bundle/ChillActivityBundle/Resources/views/GenericDoc/activity_document_row.html.twig @@ -0,0 +1,81 @@ +{% import "@ChillDocStore/Macro/macro.html.twig" as m %} +{% import "@ChillDocStore/Macro/macro_mimeicon.html.twig" as mm %} +{% import '@ChillPerson/Macro/updatedBy.html.twig' as mmm %} + +{% set person_id = null %} +{% if activity.person %} + {% set person_id = activity.person.id %} +{% endif %} + +{% set accompanying_course_id = null %} +{% if activity.accompanyingPeriod %} + {% set accompanying_course_id = activity.accompanyingPeriod.id %} +{% endif %} + +
+
+ {% if document.isPending %} +
{{ 'docgen.Doc generation is pending'|trans }}
+ {% elseif document.isFailure %} +
{{ 'docgen.Doc generation failed'|trans }}
+ {% endif %} + +
+ {% if activity.accompanyingPeriod is not null and context == 'person' %} + + {{ activity.accompanyingPeriod.id }} +   + {% endif %} +
+ + + {{ activity.type.name | localize_translatable_string }} + {% if activity.emergency %} + {{ 'Emergency'|trans|upper }} + {% endif %} + +
+
+
+ {{ document.title|chill_print_or_message("No title") }} +
+ {% if document.hasTemplate %} +
+

{{ document.template.name|localize_translatable_string }}

+
+ {% endif %} +
+ +
+
+
+ {{ document.createdAt|format_date('short') }} +
+
+
+
+ +{% if show_actions %} +
+
+ {{ mmm.createdBy(document) }} +
+ +
+{% endif %} diff --git a/src/Bundle/ChillActivityBundle/Service/GenericDoc/Normalizer/AccompanyingPeriodActivityGenericDocNormalizer.php b/src/Bundle/ChillActivityBundle/Service/GenericDoc/Normalizer/AccompanyingPeriodActivityGenericDocNormalizer.php new file mode 100644 index 000000000..7fdbd99d9 --- /dev/null +++ b/src/Bundle/ChillActivityBundle/Service/GenericDoc/Normalizer/AccompanyingPeriodActivityGenericDocNormalizer.php @@ -0,0 +1,54 @@ +key + && 'json' == $format; + } + + public function normalize(GenericDocDTO $genericDocDTO, string $format, array $context = []): array + { + $storedObject = $this->storedObjectRepository->find($genericDocDTO->identifiers['id']); + + if (null === $storedObject) { + return ['title' => $this->translator->trans('generic_doc.document removed'), 'isPresent' => false]; + } + + return [ + 'isPresent' => true, + 'title' => $storedObject->getTitle(), + 'html' => $this->twig->render( + $this->renderer->getTemplate($genericDocDTO, ['show-actions' => false, 'row-only' => true]), + $this->renderer->getTemplateData($genericDocDTO, ['show-actions' => false, 'row-only' => true]), + ), + ]; + } +} diff --git a/src/Bundle/ChillActivityBundle/Service/GenericDoc/Providers/AccompanyingPeriodActivityGenericDocProvider.php b/src/Bundle/ChillActivityBundle/Service/GenericDoc/Providers/AccompanyingPeriodActivityGenericDocProvider.php index d29e08ef5..eed136ac8 100644 --- a/src/Bundle/ChillActivityBundle/Service/GenericDoc/Providers/AccompanyingPeriodActivityGenericDocProvider.php +++ b/src/Bundle/ChillActivityBundle/Service/GenericDoc/Providers/AccompanyingPeriodActivityGenericDocProvider.php @@ -13,10 +13,12 @@ namespace Chill\ActivityBundle\Service\GenericDoc\Providers; use Chill\ActivityBundle\Entity\Activity; use Chill\ActivityBundle\Repository\ActivityDocumentACLAwareRepositoryInterface; +use Chill\ActivityBundle\Repository\ActivityRepository; use Chill\ActivityBundle\Security\Authorization\ActivityVoter; use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\GenericDoc\FetchQuery; use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface; +use Chill\DocStoreBundle\GenericDoc\GenericDocDTO; use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface; use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; @@ -34,8 +36,47 @@ final readonly class AccompanyingPeriodActivityGenericDocProvider implements Gen private EntityManagerInterface $em, private Security $security, private ActivityDocumentACLAwareRepositoryInterface $activityDocumentACLAwareRepository, + private ActivityRepository $activityRepository, ) {} + public function fetchAssociatedStoredObject(GenericDocDTO $genericDocDTO): ?StoredObject + { + if (null === $activity = $this->getRelatedEntity($genericDocDTO->key, $genericDocDTO->identifiers)) { + return null; + } + + return $activity->getDocuments()->findFirst(fn (int $key, StoredObject $storedObject) => $storedObject->getId() === $genericDocDTO->identifiers['id']); + } + + public function supportsGenericDoc(GenericDocDTO $genericDocDTO): bool + { + return $this->supportsKeyAndIdentifiers($genericDocDTO->key, $genericDocDTO->identifiers); + } + + public function supportsKeyAndIdentifiers(string $key, array $identifiers): bool + { + return self::KEY === $key && array_key_exists('activity_id', $identifiers); + } + + private function getRelatedEntity(string $key, array $identifiers): ?Activity + { + return $this->activityRepository->find($identifiers['activity_id']); + } + + public function buildOneGenericDoc(string $key, array $identifiers): ?GenericDocDTO + { + if (null === $activity = $this->getRelatedEntity($key, $identifiers)) { + return null; + } + + return new GenericDocDTO( + self::KEY, + $identifiers, + \DateTimeImmutable::createFromInterface($activity->getDate()), + $activity->getAccompanyingPeriod(), + ); + } + public function buildFetchQueryForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface { $storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class); diff --git a/src/Bundle/ChillActivityBundle/Service/GenericDoc/Renderers/AccompanyingPeriodActivityGenericDocRenderer.php b/src/Bundle/ChillActivityBundle/Service/GenericDoc/Renderers/AccompanyingPeriodActivityGenericDocRenderer.php index 93649cea8..9e388f1b4 100644 --- a/src/Bundle/ChillActivityBundle/Service/GenericDoc/Renderers/AccompanyingPeriodActivityGenericDocRenderer.php +++ b/src/Bundle/ChillActivityBundle/Service/GenericDoc/Renderers/AccompanyingPeriodActivityGenericDocRenderer.php @@ -18,6 +18,9 @@ use Chill\DocStoreBundle\GenericDoc\GenericDocDTO; use Chill\DocStoreBundle\GenericDoc\Twig\GenericDocRendererInterface; use Chill\DocStoreBundle\Repository\StoredObjectRepository; +/** + * @implements GenericDocRendererInterface + */ final readonly class AccompanyingPeriodActivityGenericDocRenderer implements GenericDocRendererInterface { public function __construct(private StoredObjectRepository $objectRepository, private ActivityRepository $activityRepository) {} @@ -29,7 +32,8 @@ final readonly class AccompanyingPeriodActivityGenericDocRenderer implements Gen public function getTemplate(GenericDocDTO $genericDocDTO, $options = []): string { - return '@ChillActivity/GenericDoc/activity_document.html.twig'; + return ($options['row-only'] ?? false) ? '@ChillActivity/GenericDoc/activity_document_row.html.twig' : + '@ChillActivity/GenericDoc/activity_document.html.twig'; } public function getTemplateData(GenericDocDTO $genericDocDTO, $options = []): array @@ -38,6 +42,7 @@ final readonly class AccompanyingPeriodActivityGenericDocRenderer implements Gen 'activity' => $this->activityRepository->find($genericDocDTO->identifiers['activity_id']), 'document' => $this->objectRepository->find($genericDocDTO->identifiers['id']), 'context' => $genericDocDTO->getContext(), + 'show_actions' => $options['show-actions'] ?? true, ]; } } diff --git a/src/Bundle/ChillCalendarBundle/Repository/CalendarDocRepository.php b/src/Bundle/ChillCalendarBundle/Repository/CalendarDocRepository.php index dd593bf3c..af7705c82 100644 --- a/src/Bundle/ChillCalendarBundle/Repository/CalendarDocRepository.php +++ b/src/Bundle/ChillCalendarBundle/Repository/CalendarDocRepository.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\CalendarBundle\Repository; use Chill\CalendarBundle\Entity\CalendarDoc; +use Chill\DocStoreBundle\Entity\StoredObject; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; use Doctrine\Persistence\ObjectRepository; @@ -49,4 +50,21 @@ class CalendarDocRepository implements ObjectRepository, CalendarDocRepositoryIn { return CalendarDoc::class; } + + /** + * @param StoredObject|int $storedObject the StoredObject instance, or the id of the stored object + */ + public function findOneByStoredObject(StoredObject|int $storedObject): ?CalendarDoc + { + $storedObjectId = $storedObject instanceof StoredObject ? $storedObject->getId() : $storedObject; + + $qb = $this->repository->createQueryBuilder('c'); + $qb->where( + $qb->expr()->eq(':storedObject', 'c.storedObject') + ); + + $qb->setParameter('storedObject', $storedObjectId); + + return $qb->getQuery()->getOneOrNullResult(); + } } diff --git a/src/Bundle/ChillCalendarBundle/Repository/CalendarDocRepositoryInterface.php b/src/Bundle/ChillCalendarBundle/Repository/CalendarDocRepositoryInterface.php index d2b1951df..78737c8b6 100644 --- a/src/Bundle/ChillCalendarBundle/Repository/CalendarDocRepositoryInterface.php +++ b/src/Bundle/ChillCalendarBundle/Repository/CalendarDocRepositoryInterface.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\CalendarBundle\Repository; use Chill\CalendarBundle\Entity\CalendarDoc; +use Chill\DocStoreBundle\Entity\StoredObject; interface CalendarDocRepositoryInterface { @@ -29,5 +30,7 @@ interface CalendarDocRepositoryInterface public function findOneBy(array $criteria): ?CalendarDoc; + public function findOneByStoredObject(StoredObject|int $storedObject): ?CalendarDoc; + public function getClassName(); } diff --git a/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/store/modules/calendarRanges.ts b/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/store/modules/calendarRanges.ts index 5d1876d05..26984c051 100644 --- a/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/store/modules/calendarRanges.ts +++ b/src/Bundle/ChillCalendarBundle/Resources/public/vuejs/MyCalendarRange/store/modules/calendarRanges.ts @@ -106,7 +106,10 @@ export default { }); state.key = state.key + toAdd.length; }, - addExternals(state, externalEvents: (EventInput & { id: string })[]) { + addExternals( + state: CalendarRangesState, + externalEvents: (EventInput & { id: string })[], + ) { const toAdd = externalEvents.filter( (r) => !state.rangesIndex.has(r.id), ); @@ -160,7 +163,7 @@ export default { state.key = state.key + 1; } }, - updateRange(state, range: CalendarRange) { + updateRange(state: CalendarRangesState, range: CalendarRange) { const found = state.ranges.find( (r) => r.calendarRangeId === range.id && r.is === "range", ); @@ -207,7 +210,7 @@ export default { }); }, createRange( - ctx, + ctx: Context, { start, end, @@ -253,10 +256,10 @@ export default { throw error; }); }, - deleteRange(ctx, calendarRangeId: number) { + deleteRange(ctx: Context, calendarRangeId: number) { const url = `/api/1.0/calendar/calendar-range/${calendarRangeId}.json`; - makeFetch("DELETE", url).then((_) => { + makeFetch("DELETE", url).then(() => { ctx.commit("removeRange", calendarRangeId); }); }, @@ -347,10 +350,10 @@ export default { ); } - return Promise.all(promises).then((_) => Promise.resolve(null)); + return Promise.all(promises).then(() => Promise.resolve(null)); }, copyFromWeekToAnotherWeek( - ctx, + ctx: Context, { fromMonday, toMonday }: { fromMonday: Date; toMonday: Date }, ): Promise { const rangesToCopy: EventInputCalendarRange[] = @@ -371,7 +374,7 @@ export default { ); } - return Promise.all(promises).then((_) => Promise.resolve(null)); + return Promise.all(promises).then(() => Promise.resolve(null)); }, }, } as Module; diff --git a/src/Bundle/ChillCalendarBundle/Resources/views/GenericDoc/calendar_document.html.twig b/src/Bundle/ChillCalendarBundle/Resources/views/GenericDoc/calendar_document.html.twig index facf5be50..f35a25a73 100644 --- a/src/Bundle/ChillCalendarBundle/Resources/views/GenericDoc/calendar_document.html.twig +++ b/src/Bundle/ChillCalendarBundle/Resources/views/GenericDoc/calendar_document.html.twig @@ -5,71 +5,5 @@ {% set c = document.calendar %}
-
-
- {% if document.storedObject.isPending %} -
{{ 'docgen.Doc generation is pending'|trans }}
- {% elseif document.storedObject.isFailure %} -
{{ 'docgen.Doc generation failed'|trans }}
- {% endif %} - -
- {% if c.accompanyingPeriod is not null and context == 'person' %} - - {{ c.accompanyingPeriod.id }} -   - {% endif %} - - - - - {{ 'Calendar'|trans }} - {% if c.endDate.diff(c.startDate).days >= 1 %} - {{ c.startDate|format_datetime('short', 'short') }} - - {{ c.endDate|format_datetime('short', 'short') }} - {% else %} - {{ c.startDate|format_datetime('short', 'short') }} - - {{ c.endDate|format_datetime('none', 'short') }} - {% endif %} - - -
- -
- {{ document.storedObject.title|chill_print_or_message("No title") }} -
- {% if document.storedObject.hasTemplate %} -
-

{{ document.storedObject.template.name|localize_translatable_string }}

-
- {% endif %} -
- -
-
-
- {{ document.storedObject.createdAt|format_date('short') }} -
-
-
-
- -
-
- {{ mmm.createdBy(document) }} -
-
    - {% if is_granted('CHILL_CALENDAR_DOC_SEE', document) %} -
  • - {{ document.storedObject|chill_document_button_group(document.storedObject.title, is_granted('CHILL_CALENDAR_CALENDAR_EDIT', c)) }} -
  • - {% endif %} - {% if is_granted('CHILL_CALENDAR_CALENDAR_EDIT', c) %} -
  • - -
  • - {% endif %} -
- -
+ {{ include('@ChillCalendar/GenericDoc/calendar_document_row.html.twig') }}
diff --git a/src/Bundle/ChillCalendarBundle/Resources/views/GenericDoc/calendar_document_row.html.twig b/src/Bundle/ChillCalendarBundle/Resources/views/GenericDoc/calendar_document_row.html.twig new file mode 100644 index 000000000..8cf5bc4ac --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Resources/views/GenericDoc/calendar_document_row.html.twig @@ -0,0 +1,75 @@ +{% import "@ChillDocStore/Macro/macro.html.twig" as m %} +{% import "@ChillDocStore/Macro/macro_mimeicon.html.twig" as mm %} +{% import '@ChillPerson/Macro/updatedBy.html.twig' as mmm %} + +{% set c = document.calendar %} + + +
+
+ {% if document.storedObject.isPending %} +
{{ 'docgen.Doc generation is pending'|trans }}
+ {% elseif document.storedObject.isFailure %} +
{{ 'docgen.Doc generation failed'|trans }}
+ {% endif %} + +
+ {% if c.accompanyingPeriod is not null and context == 'person' %} + + {{ c.accompanyingPeriod.id }} +   + {% endif %} + + + + + {{ 'Calendar'|trans }} + {% if c.endDate.diff(c.startDate).days >= 1 %} + {{ c.startDate|format_datetime('short', 'short') }} + - {{ c.endDate|format_datetime('short', 'short') }} + {% else %} + {{ c.startDate|format_datetime('short', 'short') }} + - {{ c.endDate|format_datetime('none', 'short') }} + {% endif %} + + +
+ +
+ {{ document.storedObject.title|chill_print_or_message("No title") }} +
+ {% if document.storedObject.hasTemplate %} +
+

{{ document.storedObject.template.name|localize_translatable_string }}

+
+ {% endif %} +
+ +
+
+
+ {{ document.storedObject.createdAt|format_date('short') }} +
+
+
+
+ +{% if show_actions %} +
+
+ {{ mmm.createdBy(document) }} +
+
    + {% if is_granted('CHILL_CALENDAR_DOC_SEE', document) %} +
  • + {{ document.storedObject|chill_document_button_group(document.storedObject.title, is_granted('CHILL_CALENDAR_CALENDAR_EDIT', c)) }} +
  • + {% endif %} + {% if is_granted('CHILL_CALENDAR_CALENDAR_EDIT', c) %} +
  • + +
  • + {% endif %} +
+
+{% endif %} diff --git a/src/Bundle/ChillCalendarBundle/Service/GenericDoc/Normalizer/AccompanyingPeriodCalendarGenericDocNormalizer.php b/src/Bundle/ChillCalendarBundle/Service/GenericDoc/Normalizer/AccompanyingPeriodCalendarGenericDocNormalizer.php new file mode 100644 index 000000000..61a6e1370 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Service/GenericDoc/Normalizer/AccompanyingPeriodCalendarGenericDocNormalizer.php @@ -0,0 +1,51 @@ +key && 'json' === $format; + } + + public function normalize(GenericDocDTO $genericDocDTO, string $format, array $context = []): array + { + if (null === $calendarDoc = $this->calendarDocRepository->find($genericDocDTO->identifiers['id'])) { + return ['title' => $this->translator->trans('generic_doc.document removed'), 'isPresent' => false]; + } + + return [ + 'isPresent' => true, + 'title' => $calendarDoc->getStoredObject()->getTitle(), + 'html' => $this->twig->render( + $this->renderer->getTemplate($genericDocDTO, ['show-actions' => false, 'row-only' => true]), + $this->renderer->getTemplateData($genericDocDTO, ['show-actions' => false, 'row-only' => true]) + ), + ]; + } +} diff --git a/src/Bundle/ChillCalendarBundle/Service/GenericDoc/Providers/AccompanyingPeriodCalendarGenericDocProvider.php b/src/Bundle/ChillCalendarBundle/Service/GenericDoc/Providers/AccompanyingPeriodCalendarGenericDocProvider.php index ce851a31f..fe71f18a2 100644 --- a/src/Bundle/ChillCalendarBundle/Service/GenericDoc/Providers/AccompanyingPeriodCalendarGenericDocProvider.php +++ b/src/Bundle/ChillCalendarBundle/Service/GenericDoc/Providers/AccompanyingPeriodCalendarGenericDocProvider.php @@ -13,10 +13,12 @@ namespace Chill\CalendarBundle\Service\GenericDoc\Providers; use Chill\CalendarBundle\Entity\Calendar; use Chill\CalendarBundle\Entity\CalendarDoc; +use Chill\CalendarBundle\Repository\CalendarDocRepositoryInterface; use Chill\CalendarBundle\Security\Voter\CalendarVoter; use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\GenericDoc\FetchQuery; use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface; +use Chill\DocStoreBundle\GenericDoc\GenericDocDTO; use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface; use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; @@ -38,8 +40,38 @@ final readonly class AccompanyingPeriodCalendarGenericDocProvider implements Gen public function __construct( private Security $security, private EntityManagerInterface $em, + private CalendarDocRepositoryInterface $calendarRepository, ) {} + public function fetchAssociatedStoredObject(GenericDocDTO $genericDocDTO): ?StoredObject + { + return $this->calendarRepository->find($genericDocDTO->identifiers['id'])?->getStoredObject(); + } + + public function supportsGenericDoc(GenericDocDTO $genericDocDTO): bool + { + return $this->supportsKeyAndIdentifiers($genericDocDTO->key, $genericDocDTO->identifiers); + } + + public function supportsKeyAndIdentifiers(string $key, array $identifiers): bool + { + return self::KEY === $key && array_key_exists('id', $identifiers); + } + + public function buildOneGenericDoc(string $key, array $identifiers): ?GenericDocDTO + { + if (null === $calendarDoc = $this->calendarRepository->find($identifiers['id'])) { + return null; + } + + return new GenericDocDTO( + self::KEY, + $identifiers, + \DateTimeImmutable::createFromInterface($calendarDoc->getCreatedAt() ?? new \DateTimeImmutable('now')), + $calendarDoc->getCalendar()->getAccompanyingPeriod() ?? $calendarDoc->getCalendar()->getPerson() + ); + } + /** * @throws MappingException */ @@ -82,7 +114,7 @@ final readonly class AccompanyingPeriodCalendarGenericDocProvider implements Gen [Types::INTEGER] ); - return $query; + return $this->addWhereClausesToQuery($query, $startDate, $endDate, $content); } public function isAllowedForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod): bool diff --git a/src/Bundle/ChillCalendarBundle/Service/GenericDoc/Renderers/AccompanyingPeriodCalendarGenericDocRenderer.php b/src/Bundle/ChillCalendarBundle/Service/GenericDoc/Renderers/AccompanyingPeriodCalendarGenericDocRenderer.php index 123afc164..217578be4 100644 --- a/src/Bundle/ChillCalendarBundle/Service/GenericDoc/Renderers/AccompanyingPeriodCalendarGenericDocRenderer.php +++ b/src/Bundle/ChillCalendarBundle/Service/GenericDoc/Renderers/AccompanyingPeriodCalendarGenericDocRenderer.php @@ -17,6 +17,9 @@ use Chill\CalendarBundle\Service\GenericDoc\Providers\PersonCalendarGenericDocPr use Chill\DocStoreBundle\GenericDoc\GenericDocDTO; use Chill\DocStoreBundle\GenericDoc\Twig\GenericDocRendererInterface; +/** + * @implements GenericDocRendererInterface + */ final readonly class AccompanyingPeriodCalendarGenericDocRenderer implements GenericDocRendererInterface { public function __construct(private CalendarDocRepository $repository) {} @@ -28,7 +31,8 @@ final readonly class AccompanyingPeriodCalendarGenericDocRenderer implements Gen public function getTemplate(GenericDocDTO $genericDocDTO, $options = []): string { - return '@ChillCalendar/GenericDoc/calendar_document.html.twig'; + return $options['row-only'] ?? false ? '@ChillCalendar/GenericDoc/calendar_document_row.html.twig' + : '@ChillCalendar/GenericDoc/calendar_document.html.twig'; } public function getTemplateData(GenericDocDTO $genericDocDTO, $options = []): array @@ -36,6 +40,7 @@ final readonly class AccompanyingPeriodCalendarGenericDocRenderer implements Gen return [ 'document' => $this->repository->find($genericDocDTO->identifiers['id']), 'context' => $genericDocDTO->getContext(), + 'show_actions' => $options['show-actions'] ?? true, ]; } } diff --git a/src/Bundle/ChillDocGeneratorBundle/Tests/Controller/GenericDocForAccompanyingPeriodListApiControllerTest.php b/src/Bundle/ChillDocGeneratorBundle/Tests/Controller/GenericDocForAccompanyingPeriodListApiControllerTest.php new file mode 100644 index 000000000..231240d27 --- /dev/null +++ b/src/Bundle/ChillDocGeneratorBundle/Tests/Controller/GenericDocForAccompanyingPeriodListApiControllerTest.php @@ -0,0 +1,82 @@ + 9], new \DateTimeImmutable('2024-08-01'), $accompanyingPeriod), + new GenericDocDTO('dummy', ['id' => 1], new \DateTimeImmutable('2024-09-01'), $accompanyingPeriod), + ]; + + + $manager = $this->createMock(ManagerInterface::class); + $manager->method('findDocForAccompanyingPeriod')->with($accompanyingPeriod)->willReturn($docs); + $manager->method('countDocForAccompanyingPeriod')->with($accompanyingPeriod)->willReturn(2); + + $paginatorFactory = $this->createMock(PaginatorFactoryInterface::class); + $paginatorFactory->method('create')->with(2)->willReturn(new Paginator( + 2, + 20, + 1, + '/route', + [], + $this->createMock(UrlGeneratorInterface::class), + 'page', + 'item-per-page' + )); + + $serializer = $this->createMock(SerializerInterface::class); + $serializer->method('serialize')->with($this->isInstanceOf(Collection::class))->willReturn( + json_encode(['docs' => []]) + ); + + $security = $this->createMock(Security::class); + $security->expects($this->once())->method('isGranted') + ->with(AccompanyingCourseDocumentVoter::SEE, $accompanyingPeriod)->willReturn(true); + + $controller = new GenericDocForAccompanyingPeriodListApiController( + $manager, + $security, + $paginatorFactory, + $serializer, + ); + + $response = $controller($accompanyingPeriod); + + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertEquals('{"docs":[]}', $response->getContent()); + } +} diff --git a/src/Bundle/ChillDocStoreBundle/ChillDocStoreBundle.php b/src/Bundle/ChillDocStoreBundle/ChillDocStoreBundle.php index 1cd203c35..6910871de 100644 --- a/src/Bundle/ChillDocStoreBundle/ChillDocStoreBundle.php +++ b/src/Bundle/ChillDocStoreBundle/ChillDocStoreBundle.php @@ -14,6 +14,7 @@ namespace Chill\DocStoreBundle; use Chill\DocStoreBundle\DependencyInjection\Compiler\StorageConfigurationCompilerPass; use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface; use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface; +use Chill\DocStoreBundle\GenericDoc\GenericDocNormalizerInterface; use Chill\DocStoreBundle\GenericDoc\Twig\GenericDocRendererInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; @@ -28,6 +29,8 @@ class ChillDocStoreBundle extends Bundle ->addTag('chill_doc_store.generic_doc_person_provider'); $container->registerForAutoconfiguration(GenericDocRendererInterface::class) ->addTag('chill_doc_store.generic_doc_renderer'); + $container->registerForAutoconfiguration(GenericDocNormalizerInterface::class) + ->addTag('chill_doc_store.generic_doc_metadata_normalizer'); $container->addCompilerPass(new StorageConfigurationCompilerPass()); } diff --git a/src/Bundle/ChillDocStoreBundle/Controller/GenericDocForAccompanyingPeriodController.php b/src/Bundle/ChillDocStoreBundle/Controller/GenericDocForAccompanyingPeriodController.php index f68158552..9c88f861d 100644 --- a/src/Bundle/ChillDocStoreBundle/Controller/GenericDocForAccompanyingPeriodController.php +++ b/src/Bundle/ChillDocStoreBundle/Controller/GenericDocForAccompanyingPeriodController.php @@ -11,7 +11,7 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\Controller; -use Chill\DocStoreBundle\GenericDoc\Manager; +use Chill\DocStoreBundle\GenericDoc\ManagerInterface; use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter; use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactory; @@ -25,7 +25,7 @@ final readonly class GenericDocForAccompanyingPeriodController { public function __construct( private FilterOrderHelperFactory $filterOrderHelperFactory, - private Manager $manager, + private ManagerInterface $manager, private PaginatorFactory $paginator, private Security $security, private \Twig\Environment $twig, @@ -68,6 +68,9 @@ final readonly class GenericDocForAccompanyingPeriodController ); $paginator = $this->paginator->create($nb); + // restrict the number of items for performance reasons + $paginator->setItemsPerPage(20); + $documents = $this->manager->findDocForAccompanyingPeriod( $accompanyingPeriod, $paginator->getCurrentPageFirstItemNumber(), diff --git a/src/Bundle/ChillDocStoreBundle/Controller/GenericDocForAccompanyingPeriodListApiController.php b/src/Bundle/ChillDocStoreBundle/Controller/GenericDocForAccompanyingPeriodListApiController.php new file mode 100644 index 000000000..92ea8c203 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Controller/GenericDocForAccompanyingPeriodListApiController.php @@ -0,0 +1,57 @@ +security->isGranted(AccompanyingCourseDocumentVoter::SEE, $accompanyingPeriod)) { + throw new AccessDeniedHttpException('not allowed to see the documents for accompanying period'); + } + + $nb = $this->manager->countDocForAccompanyingPeriod($accompanyingPeriod); + $paginator = $this->paginator->create($nb); + + $docs = $this->manager->findDocForAccompanyingPeriod($accompanyingPeriod, $paginator->getCurrentPageFirstItemNumber(), $paginator->getItemsPerPage()); + + $collection = new Collection($docs, $paginator); + + return new JsonResponse( + $this->serializer->serialize($collection, 'json', [AbstractNormalizer::GROUPS => ['read']]), + json: true, + ); + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Controller/GenericDocForPerson.php b/src/Bundle/ChillDocStoreBundle/Controller/GenericDocForPerson.php index c47b2c02a..2a07d0d26 100644 --- a/src/Bundle/ChillDocStoreBundle/Controller/GenericDocForPerson.php +++ b/src/Bundle/ChillDocStoreBundle/Controller/GenericDocForPerson.php @@ -11,7 +11,7 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\Controller; -use Chill\DocStoreBundle\GenericDoc\Manager; +use Chill\DocStoreBundle\GenericDoc\ManagerInterface; use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter; use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactory; @@ -25,7 +25,7 @@ final readonly class GenericDocForPerson { public function __construct( private FilterOrderHelperFactory $filterOrderHelperFactory, - private Manager $manager, + private ManagerInterface $manager, private PaginatorFactory $paginator, private Security $security, private \Twig\Environment $twig, diff --git a/src/Bundle/ChillDocStoreBundle/Entity/Document.php b/src/Bundle/ChillDocStoreBundle/Entity/Document.php index b98159919..089941f75 100644 --- a/src/Bundle/ChillDocStoreBundle/Entity/Document.php +++ b/src/Bundle/ChillDocStoreBundle/Entity/Document.php @@ -46,9 +46,10 @@ class Document implements TrackCreationInterface, TrackUpdateInterface #[ORM\ManyToOne(targetEntity: DocGeneratorTemplate::class)] private ?DocGeneratorTemplate $template = null; - #[Assert\Length(min: 2, max: 250)] - #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT)] - private string $title = ''; + /** + * Store the title of the document, if the title is set before the document. + */ + private string $proxyTitle = ''; #[ORM\ManyToOne(targetEntity: \Chill\MainBundle\Entity\User::class)] private ?\Chill\MainBundle\Entity\User $user = null; @@ -78,9 +79,10 @@ class Document implements TrackCreationInterface, TrackUpdateInterface return $this->template; } + #[Assert\Length(min: 2, max: 250)] public function getTitle(): string { - return $this->title; + return (string) $this->getObject()?->getTitle(); } public function getUser() @@ -113,6 +115,10 @@ class Document implements TrackCreationInterface, TrackUpdateInterface { $this->object = $object; + if ('' !== $this->proxyTitle) { + $this->object->setTitle($this->proxyTitle); + } + return $this; } @@ -125,7 +131,11 @@ class Document implements TrackCreationInterface, TrackUpdateInterface public function setTitle(string $title): self { - $this->title = $title; + if (null !== $this->getObject()) { + $this->getObject()->setTitle($title); + } else { + $this->proxyTitle = $title; + } return $this; } diff --git a/src/Bundle/ChillDocStoreBundle/GenericDoc/Exception/AssociatedStoredObjectNotFound.php b/src/Bundle/ChillDocStoreBundle/GenericDoc/Exception/AssociatedStoredObjectNotFound.php new file mode 100644 index 000000000..ea0c3fe77 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/GenericDoc/Exception/AssociatedStoredObjectNotFound.php @@ -0,0 +1,20 @@ + */ private iterable $providersForPerson, + + /** + * @var iterable + */ + private iterable $genericDocNormalizers, private Connection $connection, ) { $this->builder = new FetchQueryToSqlBuilder(); } - /** - * @param list $places - * - * @throws Exception - */ public function countDocForAccompanyingPeriod( AccompanyingPeriod $accompanyingPeriod, ?\DateTimeImmutable $startDate = null, @@ -83,13 +86,6 @@ final readonly class Manager return $this->countDoc($sql, $params, $types); } - /** - * @param list $places places to search. When empty, search in all places - * - * @return iterable - * - * @throws Exception - */ public function findDocForAccompanyingPeriod( AccompanyingPeriod $accompanyingPeriod, int $offset = 0, @@ -129,10 +125,35 @@ final readonly class Manager } /** - * @param list $places places to search. When empty, search in all places + * Fetch a generic doc, if it does exists. * - * @return iterable + * Currently implemented only on generic docs linked with accompanying period */ + public function buildOneGenericDoc(string $key, array $identifiers): ?GenericDocDTO + { + foreach ($this->providersForAccompanyingPeriod as $provider) { + if ($provider->supportsKeyAndIdentifiers($key, $identifiers)) { + return $provider->buildOneGenericDoc($key, $identifiers); + } + } + + return null; + } + + /** + * @throws AssociatedStoredObjectNotFound if no stored object can be found + */ + public function fetchStoredObject(GenericDocDTO $genericDocDTO): StoredObject + { + foreach ($this->providersForAccompanyingPeriod as $provider) { + if ($provider->supportsGenericDoc($genericDocDTO)) { + return $provider->fetchAssociatedStoredObject($genericDocDTO); + } + } + + throw new AssociatedStoredObjectNotFound($genericDocDTO->key, $genericDocDTO->identifiers); + } + public function findDocForPerson( Person $person, int $offset = 0, @@ -161,6 +182,28 @@ final readonly class Manager return $this->places($sql, $params, $types); } + public function isGenericDocNormalizable(GenericDocDTO $genericDocDTO, string $format, array $context = []): bool + { + foreach ($this->genericDocNormalizers as $genericDocNormalizer) { + if ($genericDocNormalizer->supportsNormalization($genericDocDTO, $format, $context)) { + return true; + } + } + + return false; + } + + public function normalizeGenericDoc(GenericDocDTO $genericDocDTO, string $format, array $context = []): array + { + foreach ($this->genericDocNormalizers as $genericDocNormalizer) { + if ($genericDocNormalizer->supportsNormalization($genericDocDTO, $format, $context)) { + return $genericDocNormalizer->normalize($genericDocDTO, $format, $context); + } + } + + throw new NotNormalizableGenericDocException(); + } + private function places(string $sql, array $params, array $types): array { if ('' === $sql) { diff --git a/src/Bundle/ChillDocStoreBundle/GenericDoc/ManagerInterface.php b/src/Bundle/ChillDocStoreBundle/GenericDoc/ManagerInterface.php new file mode 100644 index 000000000..a3b0165e0 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/GenericDoc/ManagerInterface.php @@ -0,0 +1,64 @@ + $places + * + * @throws Exception + */ + public function countDocForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, array $places = []): int; + + public function countDocForPerson(Person $person, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, array $places = []): int; + + /** + * @param list $places places to search. When empty, search in all places + * + * @return iterable + * + * @throws Exception + */ + public function findDocForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod, int $offset = 0, int $limit = 20, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, array $places = []): iterable; + + /** + * @param list $places places to search. When empty, search in all places + * + * @return iterable + */ + public function findDocForPerson(Person $person, int $offset = 0, int $limit = 20, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, array $places = []): iterable; + + public function placesForPerson(Person $person): array; + + public function placesForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod): array; + + public function isGenericDocNormalizable(GenericDocDTO $genericDocDTO, string $format, array $context = []): bool; + + /** + * @return array{title: string, html?: string} + */ + public function normalizeGenericDoc(GenericDocDTO $genericDocDTO, string $format, array $context = []): array; + + public function buildOneGenericDoc(string $key, array $identifiers): ?GenericDocDTO; + + /** + * @throws AssociatedStoredObjectNotFound if no stored object can be found + */ + public function fetchStoredObject(GenericDocDTO $genericDocDTO): StoredObject; +} diff --git a/src/Bundle/ChillDocStoreBundle/GenericDoc/Normalizer/AccompanyingCourseDocumentGenericDocNormalizer.php b/src/Bundle/ChillDocStoreBundle/GenericDoc/Normalizer/AccompanyingCourseDocumentGenericDocNormalizer.php new file mode 100644 index 000000000..3cc46ac05 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/GenericDoc/Normalizer/AccompanyingCourseDocumentGenericDocNormalizer.php @@ -0,0 +1,56 @@ +key; + } + + public function normalize(GenericDocDTO $genericDocDTO, string $format, array $context = []): array + { + if (!array_key_exists('id', $genericDocDTO->identifiers)) { + throw new UnexpectedValueException('key id not found in identifier'); + } + + $document = $this->repository->find($genericDocDTO->identifiers['id']); + + if (null === $document) { + throw new UnexpectedValueException('document not found with id '.$genericDocDTO->identifiers['id']); + } + + return [ + 'isPresent' => true, + 'title' => $document->getTitle(), + 'html' => $this->twig->render( + $this->renderer->getTemplate($genericDocDTO, ['show-actions' => false, 'row-only' => true]), + $this->renderer->getTemplateData($genericDocDTO, ['show-actions' => false, 'row-only' => true]) + ), + ]; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/GenericDoc/Normalizer/PersonDocumentGenericDocNormalizer.php b/src/Bundle/ChillDocStoreBundle/GenericDoc/Normalizer/PersonDocumentGenericDocNormalizer.php new file mode 100644 index 000000000..57f30b604 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/GenericDoc/Normalizer/PersonDocumentGenericDocNormalizer.php @@ -0,0 +1,51 @@ +key && 'json' === $format; + } + + public function normalize(GenericDocDTO $genericDocDTO, string $format, array $context = []): array + { + if (null === $personDocument = $this->personDocumentRepository->find($genericDocDTO->identifiers['id'])) { + return ['title' => $this->translator->trans('generic_doc.document removed'), 'isPresent' => false]; + } + + return [ + 'isPresent' => true, + 'title' => $personDocument->getTitle(), + 'html' => $this->twig->render( + $this->renderer->getTemplate($genericDocDTO, ['show-actions' => false, 'row-only' => true]), + $this->renderer->getTemplateData($genericDocDTO, ['show-actions' => false, 'row-only' => true]) + ), + ]; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/GenericDoc/Providers/AccompanyingCourseDocumentGenericDocProvider.php b/src/Bundle/ChillDocStoreBundle/GenericDoc/Providers/AccompanyingCourseDocumentGenericDocProvider.php index 7522c5d3b..defe2a643 100644 --- a/src/Bundle/ChillDocStoreBundle/GenericDoc/Providers/AccompanyingCourseDocumentGenericDocProvider.php +++ b/src/Bundle/ChillDocStoreBundle/GenericDoc/Providers/AccompanyingCourseDocumentGenericDocProvider.php @@ -12,10 +12,13 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\GenericDoc\Providers; use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument; +use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\GenericDoc\FetchQuery; use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface; +use Chill\DocStoreBundle\GenericDoc\GenericDocDTO; use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface; use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface; +use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository; use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\Person; @@ -31,17 +34,47 @@ final readonly class AccompanyingCourseDocumentGenericDocProvider implements Gen public function __construct( private Security $security, private EntityManagerInterface $entityManager, + private AccompanyingCourseDocumentRepository $accompanyingCourseDocumentRepository, ) {} + public function fetchAssociatedStoredObject(GenericDocDTO $genericDocDTO): ?StoredObject + { + return $this->accompanyingCourseDocumentRepository->find($genericDocDTO->identifiers['id'])?->getObject(); + } + + public function supportsGenericDoc(GenericDocDTO $genericDocDTO): bool + { + return $this->supportsKeyAndIdentifiers($genericDocDTO->key, $genericDocDTO->identifiers); + } + + public function supportsKeyAndIdentifiers(string $key, array $identifiers): bool + { + return self::KEY === $key; + } + + public function buildOneGenericDoc(string $key, array $identifiers): ?GenericDocDTO + { + if (null === $accompanyingCourseDocument = $this->accompanyingCourseDocumentRepository->find($identifiers['id'])) { + return null; + } + + return new GenericDocDTO( + self::KEY, + $identifiers, + \DateTimeImmutable::createFromInterface($accompanyingCourseDocument->getDate()), + $accompanyingCourseDocument->getCourse(), + ); + } + public function buildFetchQueryForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface { $classMetadata = $this->entityManager->getClassMetadata(AccompanyingCourseDocument::class); $query = new FetchQuery( self::KEY, - sprintf('jsonb_build_object(\'id\', %s)', $classMetadata->getIdentifierColumnNames()[0]), + sprintf('jsonb_build_object(\'id\', acc_course_document.%s)', $classMetadata->getIdentifierColumnNames()[0]), $classMetadata->getColumnName('date'), - $classMetadata->getSchemaName().'.'.$classMetadata->getTableName() + $classMetadata->getSchemaName().'.'.$classMetadata->getTableName().' AS acc_course_document' ); $query->addWhereClause( @@ -64,7 +97,7 @@ final readonly class AccompanyingCourseDocumentGenericDocProvider implements Gen $query = new FetchQuery( self::KEY, - sprintf('jsonb_build_object(\'id\', %s)', $classMetadata->getIdentifierColumnNames()[0]), + sprintf('jsonb_build_object(\'id\', acc_course_document.%s)', $classMetadata->getIdentifierColumnNames()[0]), $classMetadata->getColumnName('date'), $classMetadata->getSchemaName().'.'.$classMetadata->getTableName().' AS acc_course_document' ); @@ -110,6 +143,7 @@ final readonly class AccompanyingCourseDocumentGenericDocProvider implements Gen private function addWhereClause(FetchQuery $query, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null): FetchQuery { $classMetadata = $this->entityManager->getClassMetadata(AccompanyingCourseDocument::class); + $storedObjectMetadata = $this->entityManager->getClassMetadata(StoredObject::class); if (null !== $startDate) { $query->addWhereClause( @@ -128,9 +162,19 @@ final readonly class AccompanyingCourseDocumentGenericDocProvider implements Gen } if (null !== $content and '' !== $content) { + // add join clause to stored_object table + $query->addJoinClause( + sprintf( + 'JOIN %s AS doc_store ON doc_store.%s = acc_course_document.%s', + $storedObjectMetadata->getSchemaName().'.'.$storedObjectMetadata->getTableName(), + $storedObjectMetadata->getSingleIdentifierColumnName(), + $classMetadata->getSingleAssociationJoinColumnName('object') + ) + ); + $query->addWhereClause( sprintf( - '(%s ilike ? OR %s ilike ?)', + '(doc_store.%s ilike ? OR acc_course_document.%s ilike ?)', $classMetadata->getColumnName('title'), $classMetadata->getColumnName('description') ), diff --git a/src/Bundle/ChillDocStoreBundle/GenericDoc/Providers/PersonDocumentGenericDocProvider.php b/src/Bundle/ChillDocStoreBundle/GenericDoc/Providers/PersonDocumentGenericDocProvider.php index e5c99b258..8d079fa1a 100644 --- a/src/Bundle/ChillDocStoreBundle/GenericDoc/Providers/PersonDocumentGenericDocProvider.php +++ b/src/Bundle/ChillDocStoreBundle/GenericDoc/Providers/PersonDocumentGenericDocProvider.php @@ -11,10 +11,13 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\GenericDoc\Providers; +use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface; +use Chill\DocStoreBundle\GenericDoc\GenericDocDTO; use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface; use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface; use Chill\DocStoreBundle\Repository\PersonDocumentACLAwareRepositoryInterface; +use Chill\DocStoreBundle\Repository\PersonDocumentRepository; use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\Person; @@ -27,8 +30,38 @@ final readonly class PersonDocumentGenericDocProvider implements GenericDocForPe public function __construct( private Security $security, private PersonDocumentACLAwareRepositoryInterface $personDocumentACLAwareRepository, + private PersonDocumentRepository $personDocumentRepository, ) {} + public function fetchAssociatedStoredObject(GenericDocDTO $genericDocDTO): ?StoredObject + { + return $this->personDocumentRepository->find($genericDocDTO->identifiers['id'])?->getObject(); + } + + public function supportsGenericDoc(GenericDocDTO $genericDocDTO): bool + { + return $this->supportsKeyAndIdentifiers($genericDocDTO->key, $genericDocDTO->identifiers); + } + + public function supportsKeyAndIdentifiers(string $key, array $identifiers): bool + { + return self::KEY === $key && array_key_exists('id', $identifiers); + } + + public function buildOneGenericDoc(string $key, array $identifiers): ?GenericDocDTO + { + if (null === $document = $this->personDocumentRepository->find($identifiers['id'])) { + return null; + } + + return new GenericDocDTO( + self::KEY, + $identifiers, + \DateTimeImmutable::createFromInterface($document->getDate()), + $document->getPerson() + ); + } + public function buildFetchQueryForPerson( Person $person, ?\DateTimeImmutable $startDate = null, diff --git a/src/Bundle/ChillDocStoreBundle/GenericDoc/Renderer/AccompanyingCourseDocumentGenericDocRenderer.php b/src/Bundle/ChillDocStoreBundle/GenericDoc/Renderer/AccompanyingCourseDocumentGenericDocRenderer.php index 70175ee1b..af9b53a50 100644 --- a/src/Bundle/ChillDocStoreBundle/GenericDoc/Renderer/AccompanyingCourseDocumentGenericDocRenderer.php +++ b/src/Bundle/ChillDocStoreBundle/GenericDoc/Renderer/AccompanyingCourseDocumentGenericDocRenderer.php @@ -18,6 +18,9 @@ use Chill\DocStoreBundle\GenericDoc\Providers\AccompanyingCourseDocumentGenericD use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository; use Chill\DocStoreBundle\Repository\PersonDocumentRepository; +/** + * @implements GenericDocRendererInterface + */ final readonly class AccompanyingCourseDocumentGenericDocRenderer implements GenericDocRendererInterface { public function __construct( @@ -33,6 +36,10 @@ final readonly class AccompanyingCourseDocumentGenericDocRenderer implements Gen public function getTemplate(GenericDocDTO $genericDocDTO, $options = []): string { + if ($options['row-only'] ?? false) { + return '@ChillDocStore/List/list_item_row.html.twig'; + } + return '@ChillDocStore/List/list_item.html.twig'; } @@ -44,6 +51,7 @@ final readonly class AccompanyingCourseDocumentGenericDocRenderer implements Gen 'accompanyingCourse' => $doc->getCourse(), 'options' => $options, 'context' => $genericDocDTO->getContext(), + 'show_actions' => $options['show-actions'] ?? true, ]; } @@ -53,6 +61,7 @@ final readonly class AccompanyingCourseDocumentGenericDocRenderer implements Gen 'person' => $doc->getPerson(), 'options' => $options, 'context' => $genericDocDTO->getContext(), + 'show_actions' => $options['show-actions'] ?? true, ]; } } diff --git a/src/Bundle/ChillDocStoreBundle/GenericDoc/Twig/GenericDocRendererInterface.php b/src/Bundle/ChillDocStoreBundle/GenericDoc/Twig/GenericDocRendererInterface.php index cdd28ac70..3f2628956 100644 --- a/src/Bundle/ChillDocStoreBundle/GenericDoc/Twig/GenericDocRendererInterface.php +++ b/src/Bundle/ChillDocStoreBundle/GenericDoc/Twig/GenericDocRendererInterface.php @@ -13,11 +13,25 @@ namespace Chill\DocStoreBundle\GenericDoc\Twig; use Chill\DocStoreBundle\GenericDoc\GenericDocDTO; +/** + * Render a generic doc, to display it into a page. + * + * @template T of array + */ interface GenericDocRendererInterface { + /** + * @param T $options the options defined by the renderer + */ public function supports(GenericDocDTO $genericDocDTO, $options = []): bool; + /** + * @param T $options the options defined by the renderer + */ public function getTemplate(GenericDocDTO $genericDocDTO, $options = []): string; + /** + * @param T $options the options defined by the renderer + */ public function getTemplateData(GenericDocDTO $genericDocDTO, $options = []): array; } diff --git a/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentACLAwareRepository.php b/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentACLAwareRepository.php index 016a61dab..d00899053 100644 --- a/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentACLAwareRepository.php +++ b/src/Bundle/ChillDocStoreBundle/Repository/PersonDocumentACLAwareRepository.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\Repository; use Chill\DocStoreBundle\Entity\PersonDocument; +use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\GenericDoc\FetchQuery; use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface; use Chill\DocStoreBundle\GenericDoc\Providers\PersonDocumentGenericDocProvider; @@ -136,6 +137,7 @@ final readonly class PersonDocumentACLAwareRepository implements PersonDocumentA private function addFilterClauses(FetchQuery $query, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null): FetchQuery { $personDocMetadata = $this->em->getClassMetadata(PersonDocument::class); + $storedObjectMetadata = $this->em->getClassMetadata(StoredObject::class); if (null !== $startDate) { $query->addWhereClause( @@ -154,10 +156,20 @@ final readonly class PersonDocumentACLAwareRepository implements PersonDocumentA } if (null !== $content and '' !== $content) { + + $query->addJoinClause( + sprintf( + 'JOIN %s AS doc_store ON doc_store.%s = person_document.%s', + $storedObjectMetadata->getSchemaName().'.'.$storedObjectMetadata->getTableName(), + $storedObjectMetadata->getSingleIdentifierColumnName(), + $personDocMetadata->getSingleAssociationJoinColumnName('object') + ) + ); + $query->addWhereClause( sprintf( - '(%s ilike ? OR %s ilike ?)', - $personDocMetadata->getColumnName('title'), + '(doc_store.%s ilike ? OR person_document.%s ilike ?)', + $storedObjectMetadata->getColumnName('title'), $personDocMetadata->getColumnName('description') ), ['%'.$content.'%', '%'.$content.'%'], diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/js/generic-doc-api.ts b/src/Bundle/ChillDocStoreBundle/Resources/public/js/generic-doc-api.ts new file mode 100644 index 000000000..c15eff711 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/js/generic-doc-api.ts @@ -0,0 +1,10 @@ +import { fetchResults } from "ChillMainAssets/lib/api/apiMethods"; +import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc"; + +export function fetch_generic_docs_by_accompanying_period( + periodId: number, +): Promise { + return fetchResults( + `/api/1.0/doc-store/generic-doc/by-period/${periodId}/index`, + ); +} 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 d83c64e4a..5874e88fe 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,4 +1,4 @@ -import { _createI18n } from "../../../../../ChillMainBundle/Resources/public/vuejs/_js/i18n"; +import { _createI18n } from "ChillMainAssets/vuejs/_js/i18n"; import DocumentActionButtonsGroup from "../../vuejs/DocumentActionButtonsGroup.vue"; import { createApp } from "vue"; import { StoredObject, StoredObjectStatusChange } from "../../types"; diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/types/generic_doc.ts b/src/Bundle/ChillDocStoreBundle/Resources/public/types/generic_doc.ts new file mode 100644 index 000000000..facc55e58 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/types/generic_doc.ts @@ -0,0 +1,71 @@ +import { DateTime } from "ChillMainAssets/types"; +import { StoredObject } from "ChillDocStoreAssets/types/index"; + +export interface GenericDocMetadata { + isPresent: boolean; +} + +/** + * Empty metadata for a GenericDoc + */ +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface EmptyMetadata extends GenericDocMetadata {} + +/** + * Minimal Metadata for a GenericDoc with a normalizer + */ +export interface BaseMetadata extends GenericDocMetadata { + title: string; +} + +/** + * A generic doc is a document attached to a Person or an AccompanyingPeriod. + */ +export interface GenericDoc { + type: "doc_store_generic_doc"; + uniqueKey: string; + key: string; + identifiers: object; + context: "person" | "accompanying-period"; + doc_date: DateTime; + metadata: GenericDocMetadata; + storedObject: StoredObject | null; +} + +export interface GenericDocForAccompanyingPeriod extends GenericDoc { + context: "accompanying-period"; +} + +interface BaseMetadataWithHtml extends BaseMetadata { + html: string; +} + +export interface GenericDocForAccompanyingCourseDocument + extends GenericDocForAccompanyingPeriod { + key: "accompanying_course_document"; + metadata: BaseMetadataWithHtml; +} + +export interface GenericDocForAccompanyingCourseActivityDocument + extends GenericDocForAccompanyingPeriod { + key: "accompanying_course_activity_document"; + metadata: BaseMetadataWithHtml; +} + +export interface GenericDocForAccompanyingCourseCalendarDocument + extends GenericDocForAccompanyingPeriod { + key: "accompanying_course_calendar_document"; + metadata: BaseMetadataWithHtml; +} + +export interface GenericDocForAccompanyingCoursePersonDocument + extends GenericDocForAccompanyingPeriod { + key: "person_document"; + metadata: BaseMetadataWithHtml; +} + +export interface GenericDocForAccompanyingCourseWorkEvaluationDocument + extends GenericDocForAccompanyingPeriod { + key: "accompanying_period_work_evaluation_document"; + metadata: BaseMetadataWithHtml; +} diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/types.ts b/src/Bundle/ChillDocStoreBundle/Resources/public/types/index.ts similarity index 90% rename from src/Bundle/ChillDocStoreBundle/Resources/public/types.ts rename to src/Bundle/ChillDocStoreBundle/Resources/public/types/index.ts index c2752f7b6..5b34b8f73 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/public/types.ts +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/types/index.ts @@ -1,8 +1,5 @@ -import { - DateTime, - User, -} from "../../../ChillMainBundle/Resources/public/types"; -import { SignedUrlGet } from "./vuejs/StoredObjectButton/helpers"; +import { DateTime, User } from "ChillMainAssets/types"; +import { SignedUrlGet } from "ChillDocStoreAssets/vuejs/StoredObjectButton/helpers"; export type StoredObjectStatus = "empty" | "ready" | "failure" | "pending"; @@ -138,3 +135,10 @@ export interface ZoomLevel { nl?: string; }; } + +export interface GenericDoc { + type: "doc_store_generic_doc"; + key: string; + context: "person" | "accompanying-period"; + doc_date: DateTime; +} diff --git a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/DownloadButton.vue b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/DownloadButton.vue index 8fc88d729..d28b49cdf 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/DownloadButton.vue +++ b/src/Bundle/ChillDocStoreBundle/Resources/public/vuejs/StoredObjectButton/DownloadButton.vue @@ -66,7 +66,7 @@ const open_button = ref(null); function buildDocumentName(): string { let document_name = props.filename ?? props.storedObject.title; - if ("" === document_name) { + if ("" === document_name || null === document_name) { document_name = "document"; } diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/List/list_item.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/List/list_item.html.twig index 7ec761464..487322f8d 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/views/List/list_item.html.twig +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/List/list_item.html.twig @@ -1,120 +1,3 @@ -{% import "@ChillDocStore/Macro/macro.html.twig" as m %} -{% import "@ChillDocStore/Macro/macro_mimeicon.html.twig" as mm %} -{% import '@ChillPerson/Macro/updatedBy.html.twig' as mmm %} -
-
-
- {% if document.object.isPending %} -
{{ 'docgen.Doc generation is pending'|trans }}
- {% elseif document.object.isFailure %} -
{{ 'docgen.Doc generation failed'|trans }}
- {% endif %} - - {% if context == 'person' and accompanyingCourse is defined %} -
- - {{ accompanyingCourse.id }} -   -
- {% elseif context == 'accompanying-period' and person is defined %} -
- - {{ 'Document from person %name%'|trans({ '%name%': document.person|chill_entity_render_string }) }} -   -
- - {% endif %} -
- {{ document.title|chill_print_or_message("No title") }} -
- {% if document.object.type is not empty %} -
- {{ mm.mimeIcon(document.object.type) }} -
- {% endif %} -
-

{{ document.category.name|localize_translatable_string }}

-
- {% if document.object.hasTemplate %} -
-

{{ document.object.template.name|localize_translatable_string }}

-
- {% endif %} -
- -
-
- {% if document.date is not null %} -
- {{ document.date|format_date('short') }} -
- {% endif %} -
-
-
- {% if document.description is not empty %} -
-
- {{ document.description|chill_markdown_to_html }} -
-
- {% endif %} -
-
- {{ mmm.createdBy(document) }} -
-
    - {% if document.course is defined %} -
  • - {{ chill_entity_workflow_list('Chill\\DocStoreBundle\\Entity\\AccompanyingCourseDocument', document.id) }} -
  • - {% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS', document) %} -
  • - {{ document.object|chill_document_button_group(document.title) }} -
  • - {% endif %} - {% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_DELETE', document) %} -
  • - -
  • - {% endif %} - {% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_CREATE', document.course) %} -
  • - -
  • - {% endif %} - {% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE', document) %} -
  • - -
  • - {% endif %} - {% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS', document) %} -
  • - -
  • - {% endif %} - {% else %} - {% if is_granted('CHILL_PERSON_DOCUMENT_SEE_DETAILS', document) %} -
  • - {{ document.object|chill_document_button_group(document.title) }} -
  • -
  • - -
  • - {% endif %} - {% if is_granted('CHILL_PERSON_DOCUMENT_UPDATE', document) %} -
  • - -
  • - {% endif %} - {% if is_granted('CHILL_PERSON_DOCUMENT_DELETE', document) %} -
  • - -
  • - {% endif %} - {% endif %} -
- -
+ {% include '@ChillDocStore/List/list_item_row.html.twig'%}
diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/List/list_item_row.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/List/list_item_row.html.twig new file mode 100644 index 000000000..21812c903 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/List/list_item_row.html.twig @@ -0,0 +1,119 @@ +{% import "@ChillDocStore/Macro/macro.html.twig" as m %} +{% import "@ChillDocStore/Macro/macro_mimeicon.html.twig" as mm %} +{% import '@ChillPerson/Macro/updatedBy.html.twig' as mmm %} + +
+
+ {% if document.object.isPending %} +
{{ 'docgen.Doc generation is pending'|trans }}
+ {% elseif document.object.isFailure %} +
{{ 'docgen.Doc generation failed'|trans }}
+ {% endif %} + + {% if context == 'person' and accompanyingCourse is defined %} +
+ + {{ accompanyingCourse.id }} +   +
+ {% elseif context == 'accompanying-period' and person is defined %} +
+ + {{ 'Document from person %name%'|trans({ '%name%': document.person|chill_entity_render_string }) }} +   +
+ + {% endif %} +
+ {{ document.title|chill_print_or_message("No title") }} +
+ {% if document.object.type is not empty %} +
+ {{ mm.mimeIcon(document.object.type) }} +
+ {% endif %} +
+

{{ document.category.name|localize_translatable_string }}

+
+ {% if document.object.hasTemplate %} +
+

{{ document.object.template.name|localize_translatable_string }}

+
+ {% endif %} +
+ +
+
+ {% if document.date is not null %} +
+ {{ document.date|format_date('short') }} +
+ {% endif %} +
+
+
+{% if document.description is not empty %} +
+
+ {{ document.description|chill_markdown_to_html }} +
+
+{% endif %} + {% if show_actions %} +
+
+ {{ mmm.createdBy(document) }} +
+
    + {% if document.course is defined %} +
  • + {{ chill_entity_workflow_list('Chill\\DocStoreBundle\\Entity\\AccompanyingCourseDocument', document.id) }} +
  • + {% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS', document) %} +
  • + {{ document.object|chill_document_button_group(document.title) }} +
  • + {% endif %} + {% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_DELETE', document) %} +
  • + +
  • + {% endif %} + {% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_CREATE', document.course) %} +
  • + +
  • + {% endif %} + {% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE', document) %} +
  • + +
  • + {% endif %} + {% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS', document) %} +
  • + +
  • + {% endif %} + {% else %} + {% if is_granted('CHILL_PERSON_DOCUMENT_SEE_DETAILS', document) %} +
  • + {{ document.object|chill_document_button_group(document.title) }} +
  • +
  • + +
  • + {% endif %} + {% if is_granted('CHILL_PERSON_DOCUMENT_UPDATE', document) %} +
  • + +
  • + {% endif %} + {% if is_granted('CHILL_PERSON_DOCUMENT_DELETE', document) %} +
  • + +
  • + {% endif %} + {% endif %} +
+
+ {% endif %} diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/Workflow/public_view_with_document_render.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/Workflow/public_view_with_document_render.html.twig index c286642d9..57aa53d51 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/views/Workflow/public_view_with_document_render.html.twig +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/Workflow/public_view_with_document_render.html.twig @@ -24,9 +24,9 @@ {% endif %} {% endif %} -
+
-
+

{{ title }}

{{ 'workflow.public_link.main_document'|trans }}

@@ -39,5 +39,21 @@
+ {% for attachment in attachments %} +
+
+
+

{{ attachment.proxyStoredObject.title }}

+

{{ 'workflow.public_link.attachment'|trans }}

+ +
    +
  • + {{ attachment.proxyStoredObject|chill_document_download_only_button(storedObject.title(), false) }} +
  • +
+
+
+
+ {% endfor %}
{% endblock %} diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php index da5e4118b..f23db17c3 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter.php @@ -12,6 +12,8 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\Security\Authorization; use Chill\DocStoreBundle\Entity\StoredObject; +use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository; use Psr\Log\LoggerInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; @@ -26,7 +28,12 @@ class StoredObjectVoter extends Voter { public const LOG_PREFIX = '[stored object voter] '; - public function __construct(private readonly Security $security, private readonly iterable $storedObjectVoters, private readonly LoggerInterface $logger) {} + public function __construct( + private readonly Security $security, + private readonly iterable $storedObjectVoters, + private readonly LoggerInterface $logger, + private readonly EntityWorkflowAttachmentRepository $entityWorkflowAttachmentRepository, + ) {} protected function supports($attribute, $subject): bool { @@ -39,6 +46,16 @@ class StoredObjectVoter extends Voter /** @var StoredObject $subject */ $attributeAsEnum = StoredObjectRoleEnum::from($attribute); + // check if the stored object is attached to any workflow + $user = $token->getUser(); + if ($user instanceof User && StoredObjectRoleEnum::SEE === $attributeAsEnum) { + foreach ($this->entityWorkflowAttachmentRepository->findByStoredObject($subject) as $workflowAttachment) { + if ($workflowAttachment->getEntityWorkflow()->isUserInvolved($user)) { + return true; + } + } + } + // Loop through context-specific voters foreach ($this->storedObjectVoters as $storedObjectVoter) { if ($storedObjectVoter->supports($attributeAsEnum, $subject)) { diff --git a/src/Bundle/ChillDocStoreBundle/Serializer/Normalizer/GenericDocNormalizer.php b/src/Bundle/ChillDocStoreBundle/Serializer/Normalizer/GenericDocNormalizer.php new file mode 100644 index 000000000..f3aa33243 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Serializer/Normalizer/GenericDocNormalizer.php @@ -0,0 +1,67 @@ +manager->fetchStoredObject($object); + } catch (AssociatedStoredObjectNotFound) { + $storedObject = null; + } + + $data = [ + 'type' => 'doc_store_generic_doc', + 'key' => $object->key, + 'uniqueKey' => $object->key.implode('', array_keys($object->identifiers)).implode('', array_values($object->identifiers)), + 'identifiers' => $object->identifiers, + 'context' => $object->getContext(), + 'doc_date' => $this->normalizer->normalize($object->docDate, $format, $context), + 'metadata' => [], + 'storedObject' => $this->normalizer->normalize($storedObject, $format, $context), + ]; + + if ($this->manager->isGenericDocNormalizable($object, $format, $context)) { + $data['metadata'] = $this->manager->normalizeGenericDoc($object, $format, $context); + } + + return $data; + } + + public function supportsNormalization($data, ?string $format = null): bool + { + return 'json' === $format && $data instanceof GenericDocDTO; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Tests/GenericDoc/ManagerTest.php b/src/Bundle/ChillDocStoreBundle/Tests/GenericDoc/ManagerTest.php index eed078aa3..0839019ae 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/GenericDoc/ManagerTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/GenericDoc/ManagerTest.php @@ -11,10 +11,13 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\Tests\GenericDoc; +use Chill\DocStoreBundle\Entity\StoredObject; +use Chill\DocStoreBundle\GenericDoc\Exception\NotNormalizableGenericDocException; use Chill\DocStoreBundle\GenericDoc\FetchQuery; use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface; use Chill\DocStoreBundle\GenericDoc\GenericDocDTO; use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface; +use Chill\DocStoreBundle\GenericDoc\GenericDocNormalizerInterface; use Chill\DocStoreBundle\GenericDoc\Manager; use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; @@ -58,6 +61,7 @@ class ManagerTest extends KernelTestCase $manager = new Manager( [new SimpleGenericDocAccompanyingPeriodProvider()], [new SimpleGenericDocPersonProvider()], + [], $this->connection, ); @@ -79,6 +83,7 @@ class ManagerTest extends KernelTestCase $manager = new Manager( [new SimpleGenericDocAccompanyingPeriodProvider()], [new SimpleGenericDocPersonProvider()], + [], $this->connection, ); @@ -100,6 +105,7 @@ class ManagerTest extends KernelTestCase $manager = new Manager( [new SimpleGenericDocAccompanyingPeriodProvider()], [new SimpleGenericDocPersonProvider()], + [], $this->connection, ); @@ -121,6 +127,7 @@ class ManagerTest extends KernelTestCase $manager = new Manager( [new SimpleGenericDocAccompanyingPeriodProvider()], [new SimpleGenericDocPersonProvider()], + [], $this->connection, ); @@ -142,6 +149,7 @@ class ManagerTest extends KernelTestCase $manager = new Manager( [new SimpleGenericDocAccompanyingPeriodProvider()], [new SimpleGenericDocPersonProvider()], + [], $this->connection, ); @@ -163,6 +171,7 @@ class ManagerTest extends KernelTestCase $manager = new Manager( [new SimpleGenericDocAccompanyingPeriodProvider()], [new SimpleGenericDocPersonProvider()], + [], $this->connection, ); @@ -170,10 +179,77 @@ class ManagerTest extends KernelTestCase self::assertEquals(['accompanying_course_document_dummy'], $places); } + + public function testIsGenericDocNormalizable(): void + { + $genericDoc = new GenericDocDTO('test', ['id' => 1], new \DateTimeImmutable(), new AccompanyingPeriod()); + + $manager = new Manager([], [], [$this->buildNormalizer(true)], $this->connection); + self::assertTrue($manager->isGenericDocNormalizable($genericDoc, 'json')); + + $manager = new Manager([], [], [$this->buildNormalizer(false)], $this->connection); + self::assertFalse($manager->isGenericDocNormalizable($genericDoc, 'json')); + } + + public function testNormalizeGenericDocMetadata(): void + { + $genericDoc = new GenericDocDTO('test', ['id' => 1], new \DateTimeImmutable(), new AccompanyingPeriod()); + + $manager = new Manager([], [], [$this->buildNormalizer(false), $this->buildNormalizer(true)], $this->connection); + self::assertEquals(['title' => 'Some title'], $manager->normalizeGenericDoc($genericDoc, 'json')); + } + + public function testNormalizeGenericDocMetadataNoNormalizer(): void + { + $genericDoc = new GenericDocDTO('test', ['id' => 1], new \DateTimeImmutable(), new AccompanyingPeriod()); + + $manager = new Manager([], [], [$this->buildNormalizer(false)], $this->connection); + + $this->expectException(NotNormalizableGenericDocException::class); + + self::assertEquals(['title' => 'Some title'], $manager->normalizeGenericDoc($genericDoc, 'json')); + } + + public function buildNormalizer(bool $supports): GenericDocNormalizerInterface + { + return new class ($supports) implements GenericDocNormalizerInterface { + public function __construct(private readonly bool $supports) {} + + public function supportsNormalization(GenericDocDTO $genericDocDTO, string $format, array $context = []): bool + { + return $this->supports; + } + + public function normalize(GenericDocDTO $genericDocDTO, string $format, array $context = []): array + { + return ['title' => 'Some title']; + } + }; + } } final readonly class SimpleGenericDocAccompanyingPeriodProvider implements GenericDocForAccompanyingPeriodProviderInterface { + public function fetchAssociatedStoredObject(GenericDocDTO $genericDocDTO): ?StoredObject + { + throw new \BadMethodCallException('not implemented'); + } + + public function supportsGenericDoc(GenericDocDTO $genericDocDTO): bool + { + throw new \BadMethodCallException('not implemented'); + } + + public function supportsKeyAndIdentifiers(string $key, array $identifiers): bool + { + return 'accompanying_course_document_dummy' === $key; + } + + public function buildOneGenericDoc(string $key, array $identifiers): ?GenericDocDTO + { + return new GenericDocDTO('accompanying_course_document_dummy', $identifiers, new \DateTimeImmutable(), new AccompanyingPeriod()); + } + public function buildFetchQueryForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface { $query = new FetchQuery( diff --git a/src/Bundle/ChillDocStoreBundle/Tests/GenericDoc/Providers/AccompanyingCourseDocumentGenericDocProviderTest.php b/src/Bundle/ChillDocStoreBundle/Tests/GenericDoc/Providers/AccompanyingCourseDocumentGenericDocProviderTest.php index a4f5234d5..638d9ae56 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/GenericDoc/Providers/AccompanyingCourseDocumentGenericDocProviderTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/GenericDoc/Providers/AccompanyingCourseDocumentGenericDocProviderTest.php @@ -13,6 +13,7 @@ namespace Chill\DocStoreBundle\Tests\GenericDoc\Providers; use Chill\DocStoreBundle\GenericDoc\FetchQueryToSqlBuilder; use Chill\DocStoreBundle\GenericDoc\Providers\AccompanyingCourseDocumentGenericDocProvider; +use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository; use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Doctrine\ORM\EntityManagerInterface; @@ -56,7 +57,8 @@ class AccompanyingCourseDocumentGenericDocProviderTest extends KernelTestCase $provider = new AccompanyingCourseDocumentGenericDocProvider( $security->reveal(), - $this->entityManager + $this->entityManager, + $this->prophesize(AccompanyingCourseDocumentRepository::class)->reveal() ); $query = $provider->buildFetchQueryForAccompanyingPeriod($period, $startDate, $endDate, $content); diff --git a/src/Bundle/ChillDocStoreBundle/Tests/GenericDoc/Providers/PersonDocumentGenericDocProviderTest.php b/src/Bundle/ChillDocStoreBundle/Tests/GenericDoc/Providers/PersonDocumentGenericDocProviderTest.php index 602dd7f4c..0e6c98538 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/GenericDoc/Providers/PersonDocumentGenericDocProviderTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/GenericDoc/Providers/PersonDocumentGenericDocProviderTest.php @@ -14,6 +14,7 @@ namespace Chill\DocStoreBundle\Tests\GenericDoc\Providers; use Chill\DocStoreBundle\GenericDoc\FetchQueryToSqlBuilder; use Chill\DocStoreBundle\GenericDoc\Providers\PersonDocumentGenericDocProvider; use Chill\DocStoreBundle\Repository\PersonDocumentACLAwareRepositoryInterface; +use Chill\DocStoreBundle\Repository\PersonDocumentRepository; use Chill\PersonBundle\Entity\Person; use Doctrine\ORM\EntityManagerInterface; use Prophecy\PhpUnit\ProphecyTrait; @@ -33,11 +34,14 @@ class PersonDocumentGenericDocProviderTest extends KernelTestCase private PersonDocumentACLAwareRepositoryInterface $personDocumentACLAwareRepository; + private PersonDocumentRepository $personDocumentRepository; + protected function setUp(): void { self::bootKernel(); $this->entityManager = self::getContainer()->get(EntityManagerInterface::class); $this->personDocumentACLAwareRepository = self::getContainer()->get(PersonDocumentACLAwareRepositoryInterface::class); + $this->personDocumentRepository = self::getContainer()->get(PersonDocumentRepository::class); } /** @@ -60,7 +64,8 @@ class PersonDocumentGenericDocProviderTest extends KernelTestCase $provider = new PersonDocumentGenericDocProvider( $security->reveal(), - $this->personDocumentACLAwareRepository + $this->personDocumentACLAwareRepository, + $this->personDocumentRepository, ); $query = $provider->buildFetchQueryForPerson($person, $startDate, $endDate, $content); diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/StoredObjectVoterTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/StoredObjectVoterTest.php index b9a025f45..7180a14d8 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/StoredObjectVoterTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/StoredObjectVoterTest.php @@ -16,6 +16,7 @@ use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter; use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface; use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository; use PHPUnit\Framework\TestCase; use Psr\Log\NullLogger; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; @@ -44,7 +45,7 @@ class StoredObjectVoterTest extends TestCase ->with($this->logicalOr($this->identicalTo('ROLE_USER'), $this->identicalTo('ROLE_ADMIN'))) ->willReturn($securityIsGrantedResult); - $voter = new StoredObjectVoter($security, $storedObjectVoters, new NullLogger()); + $voter = new StoredObjectVoter($security, $storedObjectVoters, new NullLogger(), $this->createMock(EntityWorkflowAttachmentRepository::class)); self::assertEquals($expected, $voter->vote($token, $subject, [$attribute])); } diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/GenericDocNormalizerTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/GenericDocNormalizerTest.php new file mode 100644 index 000000000..0d5eb818c --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Tests/Serializer/Normalizer/GenericDocNormalizerTest.php @@ -0,0 +1,75 @@ +manager = $this->createMock(ManagerInterface::class); + + $this->normalizer = new GenericDocNormalizer($this->manager); + + $innerNormalizer = $this->createMock(NormalizerInterface::class); + $innerNormalizer->method('normalize') + ->willReturnCallback(fn ($date) => $date instanceof \DateTimeImmutable ? $date->format(DATE_ATOM) : null); + + $this->normalizer->setNormalizer($innerNormalizer); + } + + public function testNormalize() + { + $docDate = new \DateTimeImmutable('2023-10-01T15:03:01.012345Z'); + + $object = new GenericDocDTO( + 'some_key', + ['id' => 'id1', 'other_id' => 'id2'], + $docDate, + new AccompanyingPeriod(), + ); + + $expected = [ + 'type' => 'doc_store_generic_doc', + 'key' => 'some_key', + 'identifiers' => ['id' => 'id1', 'other_id' => 'id2'], + 'context' => 'accompanying-period', + 'doc_date' => $docDate->format(DATE_ATOM), + 'uniqueKey' => 'some_keyidother_idid1id2', + 'metadata' => [], + 'storedObject' => null, + ]; + + $this->manager->expects($this->once())->method('isGenericDocNormalizable') + ->with($object, 'json', []) + ->willReturn(true); + + $actual = $this->normalizer->normalize($object, 'json', []); + + $this->assertEquals($expected, $actual); + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php b/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php index 193568286..9b91d8aef 100644 --- a/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php +++ b/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php @@ -21,6 +21,7 @@ use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository; use Chill\MainBundle\Workflow\EntityWorkflowWithPublicViewInterface; use Chill\MainBundle\Workflow\EntityWorkflowWithStoredObjectHandlerInterface; use Chill\MainBundle\Workflow\Templating\EntityWorkflowViewMetadataDTO; +use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation; use Chill\PersonBundle\Service\AccompanyingPeriod\ProvideThirdPartiesAssociated; use Chill\PersonBundle\Service\AccompanyingPeriod\ProvidePersonsAssociated; @@ -78,6 +79,15 @@ final readonly class AccompanyingCourseDocumentWorkflowHandler implements Entity return $this->repository->find($entityWorkflow->getRelatedEntityId()); } + public function getRelatedAccompanyingPeriod(EntityWorkflow $entityWorkflow): ?AccompanyingPeriod + { + if (null === $document = $this->getRelatedEntity($entityWorkflow)) { + return null; + } + + return $document->getCourse(); + } + /** * @return array[] */ diff --git a/src/Bundle/ChillDocStoreBundle/Workflow/WorkflowWithPublicViewDocumentHelper.php b/src/Bundle/ChillDocStoreBundle/Workflow/WorkflowWithPublicViewDocumentHelper.php index 5d23150e0..84e647df4 100644 --- a/src/Bundle/ChillDocStoreBundle/Workflow/WorkflowWithPublicViewDocumentHelper.php +++ b/src/Bundle/ChillDocStoreBundle/Workflow/WorkflowWithPublicViewDocumentHelper.php @@ -39,6 +39,7 @@ class WorkflowWithPublicViewDocumentHelper 'storedObject' => $storedObject, 'send' => $send, 'metadata' => $metadata, + 'attachments' => $entityWorkflow->getAttachments(), ] ); } diff --git a/src/Bundle/ChillDocStoreBundle/chill.api.specs.yaml b/src/Bundle/ChillDocStoreBundle/chill.api.specs.yaml index 127430c70..8570116aa 100644 --- a/src/Bundle/ChillDocStoreBundle/chill.api.specs.yaml +++ b/src/Bundle/ChillDocStoreBundle/chill.api.specs.yaml @@ -19,6 +19,22 @@ components: type: string type: type: string + GenericDoc: + type: object + properties: + type: + type: string + enum: + - doc_store_generic_doc + key: + type: string + context: + type: string + enum: + - person + - accompanying-period + doc_date: + $ref: '#/components/schemas/Date' paths: /1.0/doc-store/stored-object/create: @@ -69,30 +85,30 @@ paths: - storedobject summary: Get a signed route to get a stored object parameters: - - in: path - name: uuid - required: true - allowEmptyValue: false - description: The UUID of the storedObjeect - schema: - type: string - format: uuid - - in: path - name: method - required: true - allowEmptyValue: false - description: the method of the signed url (get or head) - schema: - type: string - enum: [get, head] - - in: query - name: version - required: false - allowEmptyValue: false - description: the version's filename of the stored object - schema: - type: string - minLength: 2 + - in: path + name: uuid + required: true + allowEmptyValue: false + description: The UUID of the storedObjeect + schema: + type: string + format: uuid + - in: path + name: method + required: true + allowEmptyValue: false + description: the method of the signed url (get or head) + schema: + type: string + enum: [ get, head ] + - in: query + name: version + required: false + allowEmptyValue: false + description: the version's filename of the stored object + schema: + type: string + minLength: 2 responses: 200: description: "OK" @@ -111,14 +127,14 @@ paths: - storedobject summary: Get a signed route to post stored object parameters: - - in: path - name: uuid - required: true - allowEmptyValue: false - description: The UUID of the storedObjeect - schema: - type: string - format: uuid + - in: path + name: uuid + required: true + allowEmptyValue: false + description: The UUID of the storedObjeect + schema: + type: string + format: uuid responses: 200: description: "OK" @@ -137,13 +153,13 @@ paths: - storedobject summary: Restore an old version of a stored object parameters: - - in: path - name: id - required: true - allowEmptyValue: false - description: The id of the stored object version - schema: - type: integer + - in: path + name: id + required: true + allowEmptyValue: false + description: The id of the stored object version + schema: + type: integer responses: 200: description: "OK" @@ -151,4 +167,32 @@ paths: application/json: schema: type: object + /1.0/doc-store/generic-doc/by-period/{id}/index: + get: + tags: + - storedobject + summary: A list of generic doc associated with the accompanying period + parameters: + - in: path + name: id + required: true + allowEmptyValue: false + description: The id of the accompanying period + schema: + type: integer + responses: + 200: + description: "OK" + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/PaginatedResult' + - type: object + properties: + results: + type: array + items: + $ref: '#/components/schemas/GenericDoc' + type: object diff --git a/src/Bundle/ChillDocStoreBundle/config/services.yaml b/src/Bundle/ChillDocStoreBundle/config/services.yaml index 070731b54..caecc216d 100644 --- a/src/Bundle/ChillDocStoreBundle/config/services.yaml +++ b/src/Bundle/ChillDocStoreBundle/config/services.yaml @@ -31,6 +31,10 @@ services: arguments: $providersForAccompanyingPeriod: !tagged_iterator chill_doc_store.generic_doc_accompanying_period_provider $providersForPerson: !tagged_iterator chill_doc_store.generic_doc_person_provider + $genericDocNormalizers: !tagged_iterator chill_doc_store.generic_doc_metadata_normalizer + + Chill\DocStoreBundle\GenericDoc\ManagerInterface: + alias: Chill\DocStoreBundle\GenericDoc\Manager Chill\DocStoreBundle\GenericDoc\Twig\GenericDocExtension: ~ @@ -44,6 +48,9 @@ services: Chill\DocStoreBundle\GenericDoc\Renderer\: resource: '../GenericDoc/Renderer/' + Chill\DocStoreBundle\GenericDoc\Normalizer\: + resource: '../GenericDoc/Normalizer/' + Chill\DocStoreBundle\Validator\: resource: '../Validator' diff --git a/src/Bundle/ChillDocStoreBundle/migrations/Version20241212112733.php b/src/Bundle/ChillDocStoreBundle/migrations/Version20241212112733.php new file mode 100644 index 000000000..994a33ef3 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/migrations/Version20241212112733.php @@ -0,0 +1,45 @@ +addSql('UPDATE chill_doc.stored_object SET title = ac_doc.title FROM chill_doc.accompanyingcourse_document ac_doc WHERE ac_doc.object_id = stored_object.id'); + $this->addSql('ALTER TABLE chill_doc.accompanyingcourse_document DROP scope_id'); + $this->addSql('ALTER TABLE chill_doc.accompanyingcourse_document DROP title'); + $this->addSql('UPDATE chill_doc.stored_object SET title = p_doc.title FROM chill_doc.person_document p_doc WHERE p_doc.object_id = stored_object.id'); + $this->addSql('ALTER TABLE chill_doc.person_document DROP title'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE chill_doc.accompanyingcourse_document ADD scope_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE chill_doc.accompanyingcourse_document ADD title TEXT DEFAULT \'\' NOT NULL'); + $this->addSql('UPDATE chill_doc.accompanyingcourse_document SET title = so.title FROM chill_doc.stored_object so WHERE object_id = so.id'); + $this->addSql('ALTER TABLE chill_doc.accompanyingcourse_document ADD CONSTRAINT fk_a45098f6682b5931 FOREIGN KEY (scope_id) REFERENCES scopes (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('CREATE INDEX idx_a45098f6682b5931 ON chill_doc.accompanyingcourse_document (scope_id)'); + + + $this->addSql('ALTER TABLE chill_doc.person_document ADD title TEXT DEFAULT \'\' NOT NULL'); + $this->addSql('UPDATE chill_doc.person_document SET title = so.title FROM chill_doc.stored_object so WHERE object_id = so.id'); + } +} diff --git a/src/Bundle/ChillDocStoreBundle/translations/messages.fr.yml b/src/Bundle/ChillDocStoreBundle/translations/messages.fr.yml index 05713ff28..da946a34c 100644 --- a/src/Bundle/ChillDocStoreBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillDocStoreBundle/translations/messages.fr.yml @@ -86,6 +86,7 @@ workflow: shared_doc: Document partagé title: Document partagé main_document: Document principal + attachment: Pièce jointe # ROLES accompanyingCourseDocument: Documents dans les parcours d'accompagnement diff --git a/src/Bundle/ChillMainBundle/Controller/WorkflowAttachmentController.php b/src/Bundle/ChillMainBundle/Controller/WorkflowAttachmentController.php new file mode 100644 index 000000000..7a8a45950 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/WorkflowAttachmentController.php @@ -0,0 +1,101 @@ +security->isGranted(EntityWorkflowVoter::SEE, $entityWorkflow)) { + throw new AccessDeniedHttpException(); + } + + $dto = new AddAttachmentRequestDTO($entityWorkflow); + $this->serializer->deserialize($request->getContent(), AddAttachmentRequestDTO::class, 'json', [ + AbstractNormalizer::OBJECT_TO_POPULATE => $dto, AbstractNormalizer::GROUPS => ['write'], + ]); + + $errors = $this->validator->validate($dto); + + if (count($errors) > 0) { + return new JsonResponse( + $this->serializer->serialize($errors, 'json'), + Response::HTTP_UNPROCESSABLE_ENTITY, + json: true + ); + } + + $attachment = ($this->addAttachmentAction)($dto); + + $this->entityManager->flush(); + + return new JsonResponse( + $this->serializer->serialize($attachment, 'json', [AbstractNormalizer::GROUPS => ['read']]), + json: true + ); + } + + #[Route('/api/1.0/main/workflow/attachment/{id}', methods: ['DELETE'])] + public function removeAttachment(EntityWorkflowAttachment $attachment): Response + { + if (!$this->security->isGranted(EntityWorkflowVoter::SEE, $attachment->getEntityWorkflow())) { + throw new AccessDeniedHttpException(); + } + + $this->entityManager->remove($attachment); + $this->entityManager->flush(); + + return new Response(null, Response::HTTP_NO_CONTENT); + } + + #[Route('/api/1.0/main/workflow/{id}/attachment', methods: ['GET'])] + public function listAttachmentsForEntityWorkflow(EntityWorkflow $entityWorkflow): JsonResponse + { + if (!$this->security->isGranted(EntityWorkflowVoter::SEE, $entityWorkflow)) { + throw new AccessDeniedHttpException(); + } + + return new JsonResponse( + $this->serializer->serialize( + $entityWorkflow->getAttachments(), + 'json', + [AbstractNormalizer::GROUPS => ['read']] + ), + json: true + ); + } +} diff --git a/src/Bundle/ChillMainBundle/Controller/WorkflowController.php b/src/Bundle/ChillMainBundle/Controller/WorkflowController.php index 1f1372cd2..248bd286e 100644 --- a/src/Bundle/ChillMainBundle/Controller/WorkflowController.php +++ b/src/Bundle/ChillMainBundle/Controller/WorkflowController.php @@ -351,6 +351,7 @@ class WorkflowController extends AbstractController 'entity_workflow' => $entityWorkflow, 'transition_form_errors' => $errors, 'signatures' => $signatures, + 'related_accompanying_period' => $this->entityWorkflowManager->getRelatedAccompanyingPeriod($entityWorkflow), ] ); } diff --git a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php index 1c4413294..ad5fa9e95 100644 --- a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php +++ b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php @@ -87,12 +87,19 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT)] private string $workflowName; + /** + * @var Collection + */ + #[ORM\OneToMany(mappedBy: 'entityWorkflow', targetEntity: EntityWorkflowAttachment::class, cascade: ['remove'], orphanRemoval: true)] + private Collection $attachments; + public function __construct() { $this->subscriberToFinal = new ArrayCollection(); $this->subscriberToStep = new ArrayCollection(); $this->comments = new ArrayCollection(); $this->steps = new ArrayCollection(); + $this->attachments = new ArrayCollection(); $initialStep = new EntityWorkflowStep(); $initialStep @@ -142,6 +149,35 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface return $this; } + /** + * @return $this + * + * @internal use @{EntityWorkflowAttachement::__construct} instead + */ + public function addAttachment(EntityWorkflowAttachment $attachment): self + { + if (!$this->attachments->contains($attachment)) { + $this->attachments[] = $attachment; + } + + return $this; + } + + /** + * @return Collection + */ + public function getAttachments(): Collection + { + return $this->attachments; + } + + public function removeAttachment(EntityWorkflowAttachment $attachment): self + { + $this->attachments->removeElement($attachment); + + return $this; + } + public function getComments(): Collection { return $this->comments; @@ -356,6 +392,17 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface return $this->getCurrentStep()->isOnHoldByUser($user); } + public function isUserInvolved(User $user): bool + { + foreach ($this->getSteps() as $step) { + if ($step->getAllDestUser()->contains($user)) { + return true; + } + } + + return false; + } + public function isUserSubscribedToFinal(User $user): bool { return $this->subscriberToFinal->contains($user); @@ -420,7 +467,7 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface } /** - * Method use by marking store. + * Method used by marking store. * * @return $this */ diff --git a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowAttachment.php b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowAttachment.php new file mode 100644 index 000000000..83f5b8c50 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowAttachment.php @@ -0,0 +1,80 @@ + true])] + private array $relatedGenericDocIdentifiers, + #[ORM\ManyToOne(targetEntity: EntityWorkflow::class, inversedBy: 'attachments')] + #[ORM\JoinColumn(nullable: false, name: 'entityworkflow_id')] + private EntityWorkflow $entityWorkflow, + + /** + * Stored object related to the generic doc. + * + * This is a story to keep track more easily to stored object + */ + #[ORM\ManyToOne(targetEntity: StoredObject::class)] + #[ORM\JoinColumn(nullable: false, name: 'storedobject_id')] + private StoredObject $proxyStoredObject, + ) { + $this->entityWorkflow->addAttachment($this); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getEntityWorkflow(): EntityWorkflow + { + return $this->entityWorkflow; + } + + public function getRelatedGenericDocIdentifiers(): array + { + return $this->relatedGenericDocIdentifiers; + } + + public function getRelatedGenericDocKey(): string + { + return $this->relatedGenericDocKey; + } + + public function getProxyStoredObject(): StoredObject + { + return $this->proxyStoredObject; + } +} diff --git a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowComment.php b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowComment.php index 2943e5cff..6269a4d11 100644 --- a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowComment.php +++ b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowComment.php @@ -17,6 +17,11 @@ use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface; use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait; use Doctrine\ORM\Mapping as ORM; +/** + * Contains comment for entity workflow. + * + * **NOTE**: for now, this class is not in used. Comments are, for now, stored in the EntityWorkflowStep. + */ #[ORM\Entity] #[ORM\Table('chill_main_workflow_entity_comment')] class EntityWorkflowComment implements TrackCreationInterface, TrackUpdateInterface diff --git a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStep.php b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStep.php index f9556c33a..24acc4f78 100644 --- a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStep.php +++ b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStep.php @@ -16,9 +16,18 @@ use Chill\MainBundle\Entity\UserGroup; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; -use Symfony\Component\Validator\Constraints as Assert; -use Symfony\Component\Validator\Context\ExecutionContextInterface; +/** + * A step for each EntityWorkflow. + * + * The step contains the history of position. The current one is the one which transitionAt or transitionAfter is NULL. + * + * The comments field is populated by the comment of the one who apply the transition, it means that the comment for the + * "next" step is stored in the EntityWorkflowStep in the previous step. + * + * DestUsers are the one added at the transition. DestUserByAccessKey are the users who obtained permission after having + * clicked on a link to get access (email notification to groups). + */ #[ORM\Entity] #[ORM\Table('chill_main_workflow_entity_step')] class EntityWorkflowStep @@ -80,6 +89,11 @@ class EntityWorkflowStep #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)] private ?int $id = null; + /** + * If this is the final step. + * + * This property is filled by a listener. + */ #[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, options: ['default' => false])] private bool $isFinal = false; @@ -254,6 +268,11 @@ class EntityWorkflowStep return $this->ccUser; } + /** + * This is the comment from the one who apply the transition. + * + * It means that it must be saved when the user apply a transition. + */ public function getComment(): string { return $this->comment; @@ -346,6 +365,9 @@ class EntityWorkflowStep return $this->transitionByEmail; } + /** + * @return bool true if this is the end of the EntityWorkflow + */ public function isFinal(): bool { return $this->isFinal; @@ -367,6 +389,9 @@ class EntityWorkflowStep return false; } + /** + * @return bool if the EntityWorkflowStep is waiting for a transition, and is not the final step + */ public function isWaitingForTransition(): bool { if (null !== $this->transitionAfter) { @@ -506,26 +531,6 @@ class EntityWorkflowStep return $this->holdsOnStep; } - #[Assert\Callback] - public function validateOnCreation(ExecutionContextInterface $context, mixed $payload): void - { - return; - - if ($this->isFinalizeAfter()) { - if (0 !== \count($this->getDestUser())) { - $context->buildViolation('workflow.No dest users when the workflow is finalized') - ->atPath('finalizeAfter') - ->addViolation(); - } - } else { - if (0 === \count($this->getDestUser())) { - $context->buildViolation('workflow.The next step must count at least one dest') - ->atPath('finalizeAfter') - ->addViolation(); - } - } - } - public function addOnHold(EntityWorkflowStepHold $onHold): self { if (!$this->holdsOnStep->contains($onHold)) { diff --git a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStepSignature.php b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStepSignature.php index 945e0d024..8ac9b5a02 100644 --- a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStepSignature.php +++ b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStepSignature.php @@ -53,6 +53,7 @@ class EntityWorkflowStepSignature implements TrackCreationInterface, TrackUpdate public function __construct( #[ORM\ManyToOne(targetEntity: EntityWorkflowStep::class, inversedBy: 'signatures')] + #[ORM\JoinColumn(nullable: false)] private EntityWorkflowStep $step, User|Person $signer, ) { diff --git a/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowAttachmentRepository.php b/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowAttachmentRepository.php new file mode 100644 index 000000000..9d7884123 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowAttachmentRepository.php @@ -0,0 +1,67 @@ + + */ +class EntityWorkflowAttachmentRepository implements ObjectRepository +{ + private readonly EntityRepository $repository; + + public function __construct(EntityManagerInterface $registry) + { + $this->repository = $registry->getRepository(EntityWorkflowAttachment::class); + } + + public function find($id): ?EntityWorkflowAttachment + { + return $this->repository->find($id); + } + + public function findAll(): array + { + return $this->repository->findAll(); + } + + public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array + { + return $this->repository->findBy($criteria, $orderBy, $limit, $offset); + } + + public function findOneBy(array $criteria) + { + return $this->repository->findOneBy($criteria); + } + + /** + * @return array + */ + public function findByStoredObject(StoredObject $storedObject): array + { + $qb = $this->repository->createQueryBuilder('a'); + $qb->where('a.proxyStoredObject = :storedObject')->setParameter('storedObject', $storedObject); + + return $qb->getQuery()->getResult(); + } + + public function getClassName() + { + return EntityWorkflowAttachment::class; + } +} diff --git a/src/Bundle/ChillMainBundle/Resources/public/chill/chillmain.scss b/src/Bundle/ChillMainBundle/Resources/public/chill/chillmain.scss index 4f215859f..1dc56dded 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/chill/chillmain.scss +++ b/src/Bundle/ChillMainBundle/Resources/public/chill/chillmain.scss @@ -480,7 +480,7 @@ div.workflow { section.step { border: 1px solid $chill-l-gray; padding: 1em 2em; - div.flex-table { + > div.flex-table { margin: 1.5em -2em; } } diff --git a/src/Bundle/ChillMainBundle/Resources/public/lib/api/apiMethods.ts b/src/Bundle/ChillMainBundle/Resources/public/lib/api/apiMethods.ts index ea51c85c2..b50bb5534 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/lib/api/apiMethods.ts +++ b/src/Bundle/ChillMainBundle/Resources/public/lib/api/apiMethods.ts @@ -83,6 +83,10 @@ export const makeFetch = ( opts = Object.assign(opts, options); } return fetch(url, opts).then((response) => { + if (response.status === 204) { + return Promise.resolve(); + } + if (response.ok) { return response.json(); } @@ -173,18 +177,26 @@ function _fetchAction( throw new Error("other network error"); }) - .catch((reason: any) => { - console.error(reason); - throw new Error(reason); - }); + .catch( + ( + reason: + | NotFoundExceptionInterface + | ServerExceptionInterface + | ValidationExceptionInterface + | TransportExceptionInterface, + ) => { + console.error(reason); + throw reason; + }, + ); } export const fetchResults = async ( uri: string, params?: FetchParams, ): Promise => { - let promises: Promise[] = [], - page = 1; + const promises: Promise[] = []; + let page = 1; const firstData: PaginationResponse = (await _fetchAction( page, uri, @@ -229,6 +241,7 @@ const ValidationException = ( return error; }; +// eslint-disable-next-line @typescript-eslint/no-unused-vars const AccessException = (response: Response): AccessExceptionInterface => { const error = {} as AccessExceptionInterface; error.name = "AccessException"; @@ -237,6 +250,7 @@ const AccessException = (response: Response): AccessExceptionInterface => { return error; }; +// eslint-disable-next-line @typescript-eslint/no-unused-vars const NotFoundException = (response: Response): NotFoundExceptionInterface => { const error = {} as NotFoundExceptionInterface; error.name = "NotFoundException"; @@ -257,6 +271,7 @@ const ServerException = ( }; const ConflictHttpException = ( + // eslint-disable-next-line @typescript-eslint/no-unused-vars response: Response, ): ConflictHttpExceptionInterface => { const error = {} as ConflictHttpExceptionInterface; diff --git a/src/Bundle/ChillMainBundle/Resources/public/lib/workflow/attachments.ts b/src/Bundle/ChillMainBundle/Resources/public/lib/workflow/attachments.ts new file mode 100644 index 000000000..7e2f23f57 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/lib/workflow/attachments.ts @@ -0,0 +1,22 @@ +import { WorkflowAttachment } from "ChillMainAssets/types"; +import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc"; +import { makeFetch } from "ChillMainAssets/lib/api/apiMethods"; + +export const find_attachments_by_workflow = async ( + workflowId: number, +): Promise => + makeFetch("GET", `/api/1.0/main/workflow/${workflowId}/attachment`); + +export const create_attachment = async ( + workflowId: number, + genericDoc: GenericDocForAccompanyingPeriod, +): Promise => + makeFetch("POST", `/api/1.0/main/workflow/${workflowId}/attachment`, { + relatedGenericDocKey: genericDoc.key, + relatedGenericDocIdentifiers: genericDoc.identifiers, + }); + +export const delete_attachment = async ( + attachment: WorkflowAttachment, +): Promise => + makeFetch("DELETE", `/api/1.0/main/workflow/attachment/${attachment.id}`); diff --git a/src/Bundle/ChillMainBundle/Resources/public/types.ts b/src/Bundle/ChillMainBundle/Resources/public/types.ts index 7fa1c69dd..b7b62980b 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/types.ts +++ b/src/Bundle/ChillMainBundle/Resources/public/types.ts @@ -1,3 +1,5 @@ +import { GenericDoc } from "ChillDocStoreAssets/types/generic_doc"; + export interface DateTime { datetime: string; datetime8601: string; @@ -190,3 +192,14 @@ export interface WorkflowAvailable { name: string; text: string; } + +export interface WorkflowAttachment { + id: number; + relatedGenericDocKey: string; + relatedGenericDocIdentifiers: object; + createdAt: DateTime | null; + createdBy: User | null; + updatedAt: DateTime | null; + updatedBy: User | null; + genericDoc: null | GenericDoc; +} diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/WorkflowAttachment/App.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/WorkflowAttachment/App.vue new file mode 100644 index 000000000..eeea00d29 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/WorkflowAttachment/App.vue @@ -0,0 +1,70 @@ + + + + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/WorkflowAttachment/Component/AttachmentList.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/WorkflowAttachment/Component/AttachmentList.vue new file mode 100644 index 000000000..06e6ca6f4 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/WorkflowAttachment/Component/AttachmentList.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/WorkflowAttachment/Component/GenericDocItemBox.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/WorkflowAttachment/Component/GenericDocItemBox.vue new file mode 100644 index 000000000..8b96b4eea --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/WorkflowAttachment/Component/GenericDocItemBox.vue @@ -0,0 +1,18 @@ + + + + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/WorkflowAttachment/Component/PickGenericDoc.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/WorkflowAttachment/Component/PickGenericDoc.vue new file mode 100644 index 000000000..8d0eda735 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/WorkflowAttachment/Component/PickGenericDoc.vue @@ -0,0 +1,281 @@ + + + + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/WorkflowAttachment/Component/PickGenericDocItem.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/WorkflowAttachment/Component/PickGenericDocItem.vue new file mode 100644 index 000000000..e8e35573c --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/WorkflowAttachment/Component/PickGenericDocItem.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/WorkflowAttachment/Component/PickGenericDocModal.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/WorkflowAttachment/Component/PickGenericDocModal.vue new file mode 100644 index 000000000..cd3e9253d --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/WorkflowAttachment/Component/PickGenericDocModal.vue @@ -0,0 +1,113 @@ + + + + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/WorkflowAttachment/index.ts b/src/Bundle/ChillMainBundle/Resources/public/vuejs/WorkflowAttachment/index.ts new file mode 100644 index 000000000..872e6c251 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/WorkflowAttachment/index.ts @@ -0,0 +1,109 @@ +import { createApp } from "vue"; +import App from "./App.vue"; +import { _createI18n } from "../_js/i18n"; +import { WorkflowAttachment } from "ChillMainAssets/types"; +import { + create_attachment, + delete_attachment, + find_attachments_by_workflow, +} from "ChillMainAssets/lib/workflow/attachments"; +import { GenericDocForAccompanyingPeriod } from "ChillDocStoreAssets/types/generic_doc"; +import ToastPlugin from "vue-toast-notification"; +import "vue-toast-notification/dist/theme-bootstrap.css"; + +window.addEventListener("DOMContentLoaded", () => { + const attachments = document.querySelectorAll( + 'div[data-app="workflow_attachments"]', + ); + + attachments.forEach(async (el) => { + const workflowId = parseInt(el.dataset.entityWorkflowId || ""); + const accompanyingPeriodId = parseInt( + el.dataset.relatedAccompanyingPeriodId || "", + ); + const attachments = await find_attachments_by_workflow(workflowId); + + const app = createApp({ + template: + '', + components: { App }, + data: function () { + return { workflowId, accompanyingPeriodId, attachments }; + }, + methods: { + onRemoveAttachment: async function ({ + attachment, + }: { + attachment: WorkflowAttachment; + }): Promise { + const index = this.$data.attachments.findIndex( + (el: WorkflowAttachment) => el.id === attachment.id, + ); + if (-1 === index) { + console.warn( + "this attachment is not associated with the workflow", + attachment, + ); + this.$toast.error( + "This attachment is not associated with the workflow", + ); + return; + } + + try { + await delete_attachment(attachment); + } catch (error) { + console.error(error); + this.$toast.error("Error while removing element"); + throw error; + } + this.$data.attachments.splice(index, 1); + this.$toast.success("Pièce jointe supprimée"); + }, + onPickGenericDoc: async function ({ + genericDoc, + }: { + genericDoc: GenericDocForAccompanyingPeriod; + }): Promise { + console.log("picked generic doc", genericDoc); + + // prevent to create double attachment: + if ( + -1 !== + this.$data.attachments.findIndex( + (el: WorkflowAttachment) => + el.genericDoc?.key === genericDoc.key && + JSON.stringify(el.genericDoc?.identifiers) == + JSON.stringify(genericDoc.identifiers), + ) + ) { + console.warn( + "this document is already attached to the workflow", + genericDoc, + ); + this.$toast.error( + "Ce document est déjà attaché au workflow", + ); + return; + } + + try { + const attachment = await create_attachment( + workflowId, + genericDoc, + ); + this.$data.attachments.push(attachment); + } catch (error) { + console.error(error); + throw error; + } + }, + }, + }); + const i18n = _createI18n({}); + app.use(i18n); + app.use(ToastPlugin); + + app.mount(el); + }); +}); diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_attachment.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_attachment.html.twig index 2a1041c6f..d06622ece 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_attachment.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_attachment.html.twig @@ -1,123 +1,5 @@ -{# TODO - Check if this template is used - Adapt condition or Delete it -#} -{% if random(1) == 0 %} - - {# For a document #} -

{{ 'Document'|trans ~ 'target'|trans }}

- -
-
- -
-
-

Imprimé unique, parcours n°14635

- Document PDF (6.2 Mo) -

- Description du document. Sed euismod nisi porta lorem mollis aliquam. Non curabitur gravida arcu ac tortor. -

-
-
- -{% else %} - - {# For an action #} -

{{ 'Accompanying Course Action'|trans ~ 'target'|trans }}

- -
- {# dynamic insertion - ::: TODO delete all static insertion, remove condition and pass work object in inclusion - #}{% if dynamic is defined %} - - {% set work = '' %} - {% include '@ChillPerson/AccompanyingCourseWork/_item.html.twig' with { 'w': work } %} - - {% else %} - - {# BEGIN static insertion #} -
-
-

- - Exercer un AEB > Conclure l'AEB -
    -
  • Date de début : 25/11/2021
  • -
  • Date de fin : 10/03/2022
  • -
-
-

-
-
-
-
-

Référent

-

Fred

-
-
-

Usagers du parcours

- -
-
-

Problématique sociale

-
- -
-
-
-
-
- - - - - - - - - - - -

Objectif - motif - dispositif

Résultats - orientations

-

Aucun objectif - motif - dispositif

-
-
    -
  • Résultat : Arrêt à l'initiative du ménage pour déménagement
  • -
  • Orientation vers une MASP
  • -
-
-
-
-
- Dernière mise à jour par - Fred(Responsable tous les territoires)(ASE),
- le 3 décembre 2021 à 15:19 -
-
-
- {# END static insertion #} - - {% endif %} -
+{% if related_accompanying_period is not null %} +

{{ 'workflow.attachments.title'|trans }}

+
{% endif %} - -
    -
  • - -
  • -
  • - {% set x = random(1) %} - -
  • -
diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/index.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/index.html.twig index f469e7f66..989f2fa72 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Workflow/index.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Workflow/index.html.twig @@ -11,6 +11,7 @@ {{ encore_entry_script_tags('page_workflow_show') }} {{ encore_entry_script_tags('mod_wopi_link') }} {{ encore_entry_script_tags('mod_document_action_buttons_group') }} + {{ encore_entry_script_tags('mod_workflow_attachment') }} {% endblock %} {% block css %} @@ -20,6 +21,7 @@ {{ encore_entry_link_tags('page_workflow_show') }} {{ encore_entry_link_tags('mod_wopi_link') }} {{ encore_entry_link_tags('mod_document_action_buttons_group') }} + {{ encore_entry_link_tags('mod_workflow_attachment') }} {% endblock %} {% import '@ChillMain/Workflow/macro_breadcrumb.html.twig' as macro %} @@ -58,6 +60,8 @@ {% endif %} +
{% include '@ChillMain/Workflow/_attachment.html.twig' %}
+
{% include '@ChillMain/Workflow/_follow.html.twig' %}
{% if signatures|length > 0 %}
{% include '@ChillMain/Workflow/_signature.html.twig' %}
diff --git a/src/Bundle/ChillMainBundle/Serializer/Normalizer/EntityWorkflowAttachmentNormalizer.php b/src/Bundle/ChillMainBundle/Serializer/Normalizer/EntityWorkflowAttachmentNormalizer.php new file mode 100644 index 000000000..38b9f19ec --- /dev/null +++ b/src/Bundle/ChillMainBundle/Serializer/Normalizer/EntityWorkflowAttachmentNormalizer.php @@ -0,0 +1,54 @@ +manager->buildOneGenericDoc($object->getRelatedGenericDocKey(), $object->getRelatedGenericDocIdentifiers()); + + return [ + 'id' => $object->getId(), + 'relatedGenericDocKey' => $object->getRelatedGenericDocKey(), + 'relatedGenericDocIdentifiers' => $object->getRelatedGenericDocIdentifiers(), + 'createdAt' => $this->normalizer->normalize($object->getCreatedAt(), $format, $context), + 'createdBy' => $this->normalizer->normalize($object->getCreatedBy(), $format, $context), + 'updatedAt' => $this->normalizer->normalize($object->getUpdatedAt(), $format, $context), + 'updatedBy' => $this->normalizer->normalize($object->getUpdatedBy(), $format, $context), + 'genericDoc' => $this->normalizer->normalize($genericDoc, $format, [ + GenericDocNormalizer::ATTACHED_STORED_OBJECT_PROXY => $object->getProxyStoredObject(), ...$context, + ]), + ]; + } + + public function supportsNormalization($data, ?string $format = null) + { + return 'json' === $format && $data instanceof EntityWorkflowAttachment; + } +} diff --git a/src/Bundle/ChillMainBundle/Tests/Controller/WorkflowViewSendPublicControllerTest.php b/src/Bundle/ChillMainBundle/Tests/Controller/WorkflowViewSendPublicControllerTest.php index d5a4d6561..1f48e79c1 100644 --- a/src/Bundle/ChillMainBundle/Tests/Controller/WorkflowViewSendPublicControllerTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Controller/WorkflowViewSendPublicControllerTest.php @@ -20,6 +20,7 @@ use Chill\MainBundle\Workflow\EntityWorkflowManager; use Chill\MainBundle\Workflow\EntityWorkflowWithPublicViewInterface; use Chill\MainBundle\Workflow\Messenger\PostPublicViewMessage; use Chill\MainBundle\Workflow\Templating\EntityWorkflowViewMetadataDTO; +use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\ThirdPartyBundle\Entity\ThirdParty; use Doctrine\ORM\EntityManagerInterface; use PHPUnit\Framework\TestCase; @@ -189,6 +190,11 @@ class WorkflowViewSendPublicControllerTest extends TestCase throw new \BadMethodCallException('not implemented'); } + public function getRelatedAccompanyingPeriod(EntityWorkflow $entityWorkflow): ?AccompanyingPeriod + { + throw new \BadMethodCallException('not implemented'); + } + public function getRelatedObjects(object $object): array { throw new \BadMethodCallException('not implemented'); diff --git a/src/Bundle/ChillMainBundle/Tests/Workflow/Attachment/AddAttachmentActionTest.php b/src/Bundle/ChillMainBundle/Tests/Workflow/Attachment/AddAttachmentActionTest.php new file mode 100644 index 000000000..6baff49b2 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Workflow/Attachment/AddAttachmentActionTest.php @@ -0,0 +1,66 @@ +entityManagerMock = $this->createMock(EntityManagerInterface::class); + $this->manager = $this->createMock(ManagerInterface::class); + $this->addAttachmentAction = new AddAttachmentAction($this->entityManagerMock, $this->manager); + } + + public function testInvokeCreatesAndPersistsEntityWorkflowAttachment(): void + { + $entityWorkflow = new EntityWorkflow(); + $dto = new AddAttachmentRequestDTO($entityWorkflow); + $dto->relatedGenericDocKey = 'doc_key'; + $dto->relatedGenericDocIdentifiers = ['id' => 1]; + $this->manager->method('buildOneGenericDoc')->willReturn($g = new GenericDocDTO('doc_key', ['id' => 1], new \DateTimeImmutable(), new AccompanyingPeriod())); + $this->manager->method('fetchStoredObject')->with($g)->willReturn($storedObject = new StoredObject()); + + $this->entityManagerMock + ->expects($this->once()) + ->method('persist') + ->with($this->isInstanceOf(EntityWorkflowAttachment::class)); + + $result = $this->addAttachmentAction->__invoke($dto); + + $this->assertInstanceOf(EntityWorkflowAttachment::class, $result); + $this->assertSame('doc_key', $result->getRelatedGenericDocKey()); + $this->assertSame(['id' => 1], $result->getRelatedGenericDocIdentifiers()); + $this->assertSame($entityWorkflow, $result->getEntityWorkflow()); + $this->assertSame($storedObject, $result->getProxyStoredObject()); + } +} diff --git a/src/Bundle/ChillMainBundle/Workflow/Attachment/AddAttachmentAction.php b/src/Bundle/ChillMainBundle/Workflow/Attachment/AddAttachmentAction.php new file mode 100644 index 000000000..9f2220ab3 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Workflow/Attachment/AddAttachmentAction.php @@ -0,0 +1,42 @@ +manager->buildOneGenericDoc($dto->relatedGenericDocKey, $dto->relatedGenericDocIdentifiers); + + if (null === $genericDoc) { + throw new \RuntimeException(sprintf('could not build any generic doc, %s key and %s identifiers', $dto->relatedGenericDocKey, json_encode($dto->relatedGenericDocIdentifiers))); + } + + $storedObject = $this->manager->fetchStoredObject($genericDoc); + + $attachement = new EntityWorkflowAttachment($dto->relatedGenericDocKey, $dto->relatedGenericDocIdentifiers, $dto->entityWorkflow, $storedObject); + + $this->em->persist($attachement); + + return $attachement; + } +} diff --git a/src/Bundle/ChillMainBundle/Workflow/Attachment/AddAttachmentRequestDTO.php b/src/Bundle/ChillMainBundle/Workflow/Attachment/AddAttachmentRequestDTO.php new file mode 100644 index 000000000..3d93d2f52 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Workflow/Attachment/AddAttachmentRequestDTO.php @@ -0,0 +1,30 @@ +getHandler($entityWorkflow)->getSuggestedThirdParties($entityWorkflow); } + + public function getRelatedAccompanyingPeriod(EntityWorkflow $entityWorkflow): ?AccompanyingPeriod + { + return $this->getHandler($entityWorkflow)->getRelatedAccompanyingPeriod($entityWorkflow); + } } diff --git a/src/Bundle/ChillMainBundle/Workflow/Helper/WorkflowRelatedEntityPermissionHelper.php b/src/Bundle/ChillMainBundle/Workflow/Helper/WorkflowRelatedEntityPermissionHelper.php index cd890154e..b0694549d 100644 --- a/src/Bundle/ChillMainBundle/Workflow/Helper/WorkflowRelatedEntityPermissionHelper.php +++ b/src/Bundle/ChillMainBundle/Workflow/Helper/WorkflowRelatedEntityPermissionHelper.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Chill\MainBundle\Workflow\Helper; +use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\Workflow\EntityWorkflow; use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum; use Chill\MainBundle\Workflow\EntityWorkflowManager; @@ -146,12 +147,14 @@ class WorkflowRelatedEntityPermissionHelper { $currentUser = $this->security->getUser(); + if (!$currentUser instanceof User) { + return false; + } + foreach ($entityWorkflows as $entityWorkflow) { // so, the workflow is running... We return true if the current user is involved - foreach ($entityWorkflow->getSteps() as $step) { - if ($step->getAllDestUser()->contains($currentUser)) { - return true; - } + if ($entityWorkflow->isUserInvolved($currentUser)) { + return true; } } diff --git a/src/Bundle/ChillMainBundle/chill.api.specs.yaml b/src/Bundle/ChillMainBundle/chill.api.specs.yaml index ea1ae10bd..6236ba206 100644 --- a/src/Bundle/ChillMainBundle/chill.api.specs.yaml +++ b/src/Bundle/ChillMainBundle/chill.api.specs.yaml @@ -10,6 +10,50 @@ servers: components: schemas: + EntityWorkflowAttachment: + type: object + properties: + id: + type: number + format: u64 + minimum: 0 + relatedGenericDocKey: + type: string + relatedGenericDocIdentifiers: + type: object + AddAttachmentRequest: + description: "A request to add attachment in an entity workflow" + type: object + properties: + relatedGenericDocKey: + type: string + relatedGenericDocIdentifiers: + type: object + PaginatedResult: + type: object + properties: + count: + type: number + format: u64 + pagination: + type: object + properties: + first: + type: number + format: u64 + minimum: 0 + items_per_page: + type: number + format: u64 + minimum: 0 + next: + type: string + nullable: true + previous: + type: string + nullable: true + more: + type: boolean Date: type: object properties: @@ -990,3 +1034,70 @@ paths: $ref: '#/components/schemas/UserGroup' 403: description: "Unauthorized" + /1.0/main/workflow/{id}/attachment: + get: + tags: + - workflow + summary: Get a list of attachements for a given workflow + parameters: + - name: id + in: path + required: true + description: The entity workflow id + schema: + type: integer + format: integer + minimum: 1 + responses: + 200: + description: "ok" + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/EntityWorkflowAttachment' + post: + tags: + - workflow + summary: Create a new attachment + parameters: + - name: id + in: path + required: true + description: The entity workflow id + schema: + type: integer + format: integer + minimum: 1 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AddAttachmentRequest' + responses: + 200: + description: "ok" + content: + application/json: + schema: + $ref: '#/components/schemas/EntityWorkflowAttachment' + /1.0/main/workflow/attachment/{id}: + delete: + tags: + - workflow + summary: Remove an attachment + parameters: + - name: id + in: path + required: true + description: The entity workflow 's attachment id + schema: + type: integer + format: integer + minimum: 1 + responses: + 204: + description: "resource was deleted successfully" + diff --git a/src/Bundle/ChillMainBundle/chill.webpack.config.js b/src/Bundle/ChillMainBundle/chill.webpack.config.js index 36f34a2fd..ee640e677 100644 --- a/src/Bundle/ChillMainBundle/chill.webpack.config.js +++ b/src/Bundle/ChillMainBundle/chill.webpack.config.js @@ -150,6 +150,10 @@ module.exports = function (encore, entries) { "mod_news", __dirname + "/Resources/public/module/news/index.js", ); + encore.addEntry( + "mod_workflow_attachment", + __dirname + "/Resources/public/vuejs/WorkflowAttachment/index", + ); // Vue entrypoints encore.addEntry( diff --git a/src/Bundle/ChillMainBundle/config/services.yaml b/src/Bundle/ChillMainBundle/config/services.yaml index 86f15097b..a9829f99d 100644 --- a/src/Bundle/ChillMainBundle/config/services.yaml +++ b/src/Bundle/ChillMainBundle/config/services.yaml @@ -33,6 +33,8 @@ services: # workflow related Chill\MainBundle\Workflow\: resource: '../Workflow/' + exclude: + - '../Workflow/Attachment/AddAttachmentRequestDTO.php' Chill\MainBundle\Workflow\EntityWorkflowManager: arguments: diff --git a/src/Bundle/ChillMainBundle/migrations/Version20241129112740.php b/src/Bundle/ChillMainBundle/migrations/Version20241129112740.php new file mode 100644 index 000000000..b33993209 --- /dev/null +++ b/src/Bundle/ChillMainBundle/migrations/Version20241129112740.php @@ -0,0 +1,51 @@ +addSql('CREATE SEQUENCE chill_main_workflow_entity_attachment_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE chill_main_workflow_entity_attachment (id INT NOT NULL, relatedGenericDocKey VARCHAR(255) NOT NULL, relatedGenericDocIdentifiers JSONB NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, entityWorkflow_id INT NOT NULL, createdBy_id INT DEFAULT NULL, updatedBy_id INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_279415FFFB054143 ON chill_main_workflow_entity_attachment (entityWorkflow_id)'); + $this->addSql('CREATE INDEX IDX_279415FF3174800F ON chill_main_workflow_entity_attachment (createdBy_id)'); + $this->addSql('CREATE INDEX IDX_279415FF65FF1AEC ON chill_main_workflow_entity_attachment (updatedBy_id)'); + $this->addSql('CREATE UNIQUE INDEX unique_generic_doc_by_workflow ON chill_main_workflow_entity_attachment (relatedGenericDocKey, relatedGenericDocIdentifiers, entityworkflow_id)'); + $this->addSql('COMMENT ON COLUMN chill_main_workflow_entity_attachment.createdAt IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN chill_main_workflow_entity_attachment.updatedAt IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE chill_main_workflow_entity_attachment ADD CONSTRAINT FK_279415FFFB054143 FOREIGN KEY (entityWorkflow_id) REFERENCES chill_main_workflow_entity (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_workflow_entity_attachment ADD CONSTRAINT FK_279415FF3174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_workflow_entity_attachment ADD CONSTRAINT FK_279415FF65FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_workflow_entity_attachment ADD storedobject_id INT NOT NULL'); + $this->addSql('ALTER TABLE chill_main_workflow_entity_attachment ADD CONSTRAINT FK_279415FFEE684399 FOREIGN KEY (storedobject_id) REFERENCES chill_doc.stored_object (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('CREATE INDEX IDX_279415FFEE684399 ON chill_main_workflow_entity_attachment (storedobject_id)'); + $this->addSql('ALTER INDEX idx_279415fffb054143 RENAME TO IDX_279415FF7D99CE94'); + } + + public function down(Schema $schema): void + { + $this->addSql('DROP SEQUENCE chill_main_workflow_entity_attachment_id_seq CASCADE'); + $this->addSql('ALTER TABLE chill_main_workflow_entity_attachment DROP CONSTRAINT FK_279415FFFB054143'); + $this->addSql('ALTER TABLE chill_main_workflow_entity_attachment DROP CONSTRAINT FK_279415FF3174800F'); + $this->addSql('ALTER TABLE chill_main_workflow_entity_attachment DROP CONSTRAINT FK_279415FF65FF1AEC'); + $this->addSql('DROP TABLE chill_main_workflow_entity_attachment'); + } +} diff --git a/src/Bundle/ChillMainBundle/translations/messages.fr.yml b/src/Bundle/ChillMainBundle/translations/messages.fr.yml index d2a760d53..d05ab595c 100644 --- a/src/Bundle/ChillMainBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillMainBundle/translations/messages.fr.yml @@ -612,6 +612,9 @@ workflow: reject_signature_of: Rejet de la signature de %signer% reject_are_you_sure: Êtes-vous sûr de vouloir rejeter la signature de %signer% + attachments: + title: Pièces jointes + Subscribe final: Recevoir une notification à l'étape finale Subscribe all steps: Recevoir une notification à chaque étape CHILL_MAIN_WORKFLOW_APPLY_ALL_TRANSITION: Appliquer les transitions sur tous les workflows diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocument.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocument.php index ae0546516..01c3ed69d 100644 --- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocument.php +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationDocument.php @@ -60,9 +60,10 @@ class AccompanyingPeriodWorkEvaluationDocument implements \Chill\MainBundle\Doct #[ORM\ManyToOne(targetEntity: DocGeneratorTemplate::class)] private ?DocGeneratorTemplate $template = null; - #[Serializer\Groups(['read', 'write', 'accompanying_period_work_evaluation:create'])] - #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])] - private ?string $title = ''; + /** + * Store the title only if the storedObject is not yet associated with the instance. + */ + private string $proxyTitle = ''; public function getAccompanyingPeriodWorkEvaluation(): ?AccompanyingPeriodWorkEvaluation { @@ -89,9 +90,10 @@ class AccompanyingPeriodWorkEvaluationDocument implements \Chill\MainBundle\Doct return $this->template; } + #[Serializer\Groups(['read'])] public function getTitle(): ?string { - return $this->title; + return (string) $this->getStoredObject()?->getTitle(); } public function setAccompanyingPeriodWorkEvaluation(?AccompanyingPeriodWorkEvaluation $accompanyingPeriodWorkEvaluation): AccompanyingPeriodWorkEvaluationDocument @@ -125,6 +127,10 @@ class AccompanyingPeriodWorkEvaluationDocument implements \Chill\MainBundle\Doct { $this->storedObject = $storedObject; + if ('' !== $this->proxyTitle) { + $this->storedObject->setTitle($this->proxyTitle); + } + return $this; } @@ -135,9 +141,14 @@ class AccompanyingPeriodWorkEvaluationDocument implements \Chill\MainBundle\Doct return $this; } - public function setTitle(?string $title): AccompanyingPeriodWorkEvaluationDocument + #[Serializer\Groups(['write', 'accompanying_period_work_evaluation:create'])] + public function setTitle(?string $proxyTitle): AccompanyingPeriodWorkEvaluationDocument { - $this->title = $title; + if (null !== $this->storedObject) { + $this->storedObject->setTitle((string) $proxyTitle); + } else { + $this->proxyTitle = (string) $proxyTitle; + } return $this; } diff --git a/src/Bundle/ChillPersonBundle/Resources/views/GenericDoc/evaluation_document.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/GenericDoc/evaluation_document.html.twig index 70ff82b88..7d7852f2e 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/GenericDoc/evaluation_document.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/GenericDoc/evaluation_document.html.twig @@ -1,83 +1,3 @@ -{% import "@ChillDocStore/Macro/macro.html.twig" as m %} -{% import "@ChillDocStore/Macro/macro_mimeicon.html.twig" as mm %} -{% import '@ChillPerson/Macro/updatedBy.html.twig' as mmm %} - -{% set w = document.accompanyingPeriodWorkEvaluation.accompanyingPeriodWork %} -
-
-
- {% if document.storedObject.isPending %} -
{{ 'docgen.Doc generation is pending'|trans }}
- {% elseif document.storedObject.isFailure %} -
{{ 'docgen.Doc generation failed'|trans }}
- {% endif %} -
- {% if context == 'person' %} - - {{ w.accompanyingPeriod.id }} -   - {% endif %} -
- - {{ w.socialAction|chill_entity_render_string }} > {{ document.accompanyingPeriodWorkEvaluation.evaluation.title|localize_translatable_string }} -
-
-
- {{ document.title|chill_print_or_message("No title") }} -
- {% if document.storedObject.type is not empty %} -
- {{ mm.mimeIcon(document.storedObject.type) }} -
- {% endif %} - {% if document.storedObject.hasTemplate %} -
-

{{ document.storedObject.template.name|localize_translatable_string }}

-
- {% endif %} -
- -
-
-
- {{ document.storedObject.createdAt|format_date('short') }} -
-
-
-
- -
-
- {{ mmm.createdBy(document) }} -
-
    -
  • - {{ chill_entity_workflow_list('Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument', document.id) }} -
  • - {% if is_granted('CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_EVALUATION_DOCUMENT_SHOW', document) %} -
  • - {{ document.storedObject|chill_document_button_group(document.title, is_granted('CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_UPDATE', document.accompanyingPeriodWorkEvaluation.accompanyingPeriodWork)) }} -
  • - {% endif %} - {% if is_granted('CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_UPDATE', w) %} -
  • -
    - -
    -
  • - {% endif %} - {% if is_granted('CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_UPDATE', w) %} -
  • - -
  • - {% endif %} - {% if is_granted('CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_SEE', w)%} -
  • - -
  • - {% endif %} -
- -
+ {{ include('@ChillPerson/GenericDoc/evaluation_document_row.html.twig') }}
diff --git a/src/Bundle/ChillPersonBundle/Resources/views/GenericDoc/evaluation_document_row.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/GenericDoc/evaluation_document_row.html.twig new file mode 100644 index 000000000..451857628 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/views/GenericDoc/evaluation_document_row.html.twig @@ -0,0 +1,82 @@ +{% import "@ChillDocStore/Macro/macro.html.twig" as m %} +{% import "@ChillDocStore/Macro/macro_mimeicon.html.twig" as mm %} +{% import '@ChillPerson/Macro/updatedBy.html.twig' as mmm %} + +{% set w = document.accompanyingPeriodWorkEvaluation.accompanyingPeriodWork %} + +
+
+ {% if document.storedObject.isPending %} +
{{ 'docgen.Doc generation is pending'|trans }}
+ {% elseif document.storedObject.isFailure %} +
{{ 'docgen.Doc generation failed'|trans }}
+ {% endif %} +
+ {% if context == 'person' %} + + {{ w.accompanyingPeriod.id }} +   + {% endif %} +
+ + {{ w.socialAction|chill_entity_render_string }} > {{ document.accompanyingPeriodWorkEvaluation.evaluation.title|localize_translatable_string }} +
+
+
+ {{ document.title|chill_print_or_message("No title") }} +
+ {% if document.storedObject.type is not empty %} +
+ {{ mm.mimeIcon(document.storedObject.type) }} +
+ {% endif %} + {% if document.storedObject.hasTemplate %} +
+

{{ document.storedObject.template.name|localize_translatable_string }}

+
+ {% endif %} +
+ +
+
+
+ {{ document.storedObject.createdAt|format_date('short') }} +
+
+
+
+ +{% if show_actions %} +
+
+ {{ mmm.createdBy(document) }} +
+
    +
  • + {{ chill_entity_workflow_list('Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument', document.id) }} +
  • + {% if is_granted('CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_EVALUATION_DOCUMENT_SHOW', document) %} +
  • + {{ document.storedObject|chill_document_button_group(document.title, is_granted('CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_UPDATE', document.accompanyingPeriodWorkEvaluation.accompanyingPeriodWork)) }} +
  • + {% endif %} + {% if is_granted('CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_UPDATE', w) %} +
  • +
    + +
    +
  • + {% endif %} + {% if is_granted('CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_UPDATE', w) %} +
  • + +
  • + {% endif %} + {% if is_granted('CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_SEE', w)%} +
  • + +
  • + {% endif %} +
+
+{% endif %} diff --git a/src/Bundle/ChillPersonBundle/Service/GenericDoc/Normalizer/AccompanyingPeriodWorkEvaluationGenericDocNormalizer.php b/src/Bundle/ChillPersonBundle/Service/GenericDoc/Normalizer/AccompanyingPeriodWorkEvaluationGenericDocNormalizer.php new file mode 100644 index 000000000..c2536811f --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Service/GenericDoc/Normalizer/AccompanyingPeriodWorkEvaluationGenericDocNormalizer.php @@ -0,0 +1,51 @@ +key; + } + + public function normalize(GenericDocDTO $genericDocDTO, string $format, array $context = []): array + { + if (null === $evaluationDoc = $this->accompanyingPeriodWorkEvaluationDocumentRepository->find($genericDocDTO->identifiers['id'])) { + return ['title' => $this->translator->trans('generic_doc.document removed'), 'isPresent' => false]; + } + + return [ + 'title' => $evaluationDoc->getTitle(), + 'html' => $this->twig->render( + $this->renderer->getTemplate($genericDocDTO, ['show-actions' => false, 'row-only' => true]), + $this->renderer->getTemplateData($genericDocDTO, ['show-actions' => false, 'row-only' => true]) + ), + 'isPresent' => true, + ]; + } +} diff --git a/src/Bundle/ChillPersonBundle/Service/GenericDoc/Providers/AccompanyingPeriodWorkEvaluationGenericDocProvider.php b/src/Bundle/ChillPersonBundle/Service/GenericDoc/Providers/AccompanyingPeriodWorkEvaluationGenericDocProvider.php index a9617e3c9..3f69c1f1c 100644 --- a/src/Bundle/ChillPersonBundle/Service/GenericDoc/Providers/AccompanyingPeriodWorkEvaluationGenericDocProvider.php +++ b/src/Bundle/ChillPersonBundle/Service/GenericDoc/Providers/AccompanyingPeriodWorkEvaluationGenericDocProvider.php @@ -14,10 +14,12 @@ namespace Chill\PersonBundle\Service\GenericDoc\Providers; use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\GenericDoc\FetchQuery; use Chill\DocStoreBundle\GenericDoc\FetchQueryInterface; +use Chill\DocStoreBundle\GenericDoc\GenericDocDTO; use Chill\DocStoreBundle\GenericDoc\GenericDocForAccompanyingPeriodProviderInterface; use Chill\DocStoreBundle\GenericDoc\GenericDocForPersonProviderInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\Person; +use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocumentRepository; use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkVoter; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\EntityManagerInterface; @@ -30,8 +32,38 @@ final readonly class AccompanyingPeriodWorkEvaluationGenericDocProvider implemen public function __construct( private Security $security, private EntityManagerInterface $entityManager, + private AccompanyingPeriodWorkEvaluationDocumentRepository $accompanyingPeriodWorkEvaluationDocumentRepository, ) {} + public function fetchAssociatedStoredObject(GenericDocDTO $genericDocDTO): ?StoredObject + { + return $this->accompanyingPeriodWorkEvaluationDocumentRepository->find($genericDocDTO->identifiers['id'])?->getStoredObject(); + } + + public function supportsGenericDoc(GenericDocDTO $genericDocDTO): bool + { + return $this->supportsKeyAndIdentifiers($genericDocDTO->key, $genericDocDTO->identifiers); + } + + public function supportsKeyAndIdentifiers(string $key, array $identifiers): bool + { + return self::KEY === $key; + } + + public function buildOneGenericDoc(string $key, array $identifiers): ?GenericDocDTO + { + if (null === $document = $this->accompanyingPeriodWorkEvaluationDocumentRepository->find($identifiers['id'])) { + return null; + } + + return new GenericDocDTO( + $key, + $identifiers, + $document->getAccompanyingPeriodWorkEvaluation()->getCreatedAt(), + $document->getAccompanyingPeriodWorkEvaluation()->getAccompanyingPeriodWork()->getAccompanyingPeriod() + ); + } + public function buildFetchQueryForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null, ?string $origin = null): FetchQueryInterface { $accompanyingPeriodWorkMetadata = $this->entityManager->getClassMetadata(AccompanyingPeriod\AccompanyingPeriodWork::class); @@ -53,7 +85,6 @@ final readonly class AccompanyingPeriodWorkEvaluationGenericDocProvider implemen private function addWhereClausesToQuery(FetchQuery $query, ?\DateTimeImmutable $startDate = null, ?\DateTimeImmutable $endDate = null, ?string $content = null): FetchQuery { - $classMetadata = $this->entityManager->getClassMetadata(AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument::class); $storedObjectMetadata = $this->entityManager->getClassMetadata(StoredObject::class); if (null !== $startDate) { @@ -74,7 +105,7 @@ final readonly class AccompanyingPeriodWorkEvaluationGenericDocProvider implemen if (null !== $content) { $query->addWhereClause( - sprintf('apwed.%s ilike ?', $classMetadata->getColumnName('title')), + sprintf('doc_store.%s ilike ?', $storedObjectMetadata->getColumnName('title')), ['%'.$content.'%'], [Types::STRING] ); diff --git a/src/Bundle/ChillPersonBundle/Service/GenericDoc/Renderer/AccompanyingPeriodWorkEvaluationGenericDocRenderer.php b/src/Bundle/ChillPersonBundle/Service/GenericDoc/Renderer/AccompanyingPeriodWorkEvaluationGenericDocRenderer.php index 7c6f2264f..42fc5b658 100644 --- a/src/Bundle/ChillPersonBundle/Service/GenericDoc/Renderer/AccompanyingPeriodWorkEvaluationGenericDocRenderer.php +++ b/src/Bundle/ChillPersonBundle/Service/GenericDoc/Renderer/AccompanyingPeriodWorkEvaluationGenericDocRenderer.php @@ -16,6 +16,9 @@ use Chill\DocStoreBundle\GenericDoc\Twig\GenericDocRendererInterface; use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocumentRepository; use Chill\PersonBundle\Service\GenericDoc\Providers\AccompanyingPeriodWorkEvaluationGenericDocProvider; +/** + * @implements GenericDocRendererInterface + */ final readonly class AccompanyingPeriodWorkEvaluationGenericDocRenderer implements GenericDocRendererInterface { public function __construct( @@ -29,7 +32,8 @@ final readonly class AccompanyingPeriodWorkEvaluationGenericDocRenderer implemen public function getTemplate(GenericDocDTO $genericDocDTO, $options = []): string { - return '@ChillPerson/GenericDoc/evaluation_document.html.twig'; + return ($options['row-only'] ?? false) ? '@ChillPerson/GenericDoc/evaluation_document_row.html.twig' + : '@ChillPerson/GenericDoc/evaluation_document.html.twig'; } public function getTemplateData(GenericDocDTO $genericDocDTO, $options = []): array @@ -37,6 +41,7 @@ final readonly class AccompanyingPeriodWorkEvaluationGenericDocRenderer implemen return [ 'document' => $this->accompanyingPeriodWorkEvaluationDocumentRepository->find($genericDocDTO->identifiers['id']), 'context' => $genericDocDTO->getContext(), + 'show_actions' => $options['show-actions'] ?? true, ]; } } diff --git a/src/Bundle/ChillPersonBundle/Tests/Service/GenericDoc/Providers/AccompanyingPeriodCalendarGenericDocProviderTest.php b/src/Bundle/ChillPersonBundle/Tests/Service/GenericDoc/Providers/AccompanyingPeriodCalendarGenericDocProviderTest.php index cae53d9a5..06d4b37f9 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Service/GenericDoc/Providers/AccompanyingPeriodCalendarGenericDocProviderTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Service/GenericDoc/Providers/AccompanyingPeriodCalendarGenericDocProviderTest.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Service\GenericDoc\Providers; +use Chill\CalendarBundle\Repository\CalendarDocRepositoryInterface; use Chill\CalendarBundle\Security\Voter\CalendarVoter; use Chill\CalendarBundle\Service\GenericDoc\Providers\AccompanyingPeriodCalendarGenericDocProvider; use Chill\DocStoreBundle\GenericDoc\FetchQueryToSqlBuilder; @@ -35,11 +36,14 @@ class AccompanyingPeriodCalendarGenericDocProviderTest extends KernelTestCase private EntityManagerInterface $entityManager; + private CalendarDocRepositoryInterface $calendarDocRepository; + protected function setUp(): void { self::bootKernel(); $this->security = self::getContainer()->get(Security::class); $this->entityManager = self::getContainer()->get(EntityManagerInterface::class); + $this->calendarDocRepository = self::getContainer()->get(CalendarDocRepositoryInterface::class); } /** @@ -47,7 +51,7 @@ class AccompanyingPeriodCalendarGenericDocProviderTest extends KernelTestCase */ public function testBuildFetchQueryForAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod, ?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate, ?string $content): void { - $provider = new AccompanyingPeriodCalendarGenericDocProvider($this->security, $this->entityManager); + $provider = new AccompanyingPeriodCalendarGenericDocProvider($this->security, $this->entityManager, $this->calendarDocRepository); $query = $provider->buildFetchQueryForAccompanyingPeriod($accompanyingPeriod, $startDate, $endDate, $content); @@ -66,7 +70,7 @@ class AccompanyingPeriodCalendarGenericDocProviderTest extends KernelTestCase $security = $this->prophesize(Security::class); $security->isGranted(CalendarVoter::SEE, Argument::any())->willReturn(true); - $provider = new AccompanyingPeriodCalendarGenericDocProvider($security->reveal(), $this->entityManager); + $provider = new AccompanyingPeriodCalendarGenericDocProvider($security->reveal(), $this->entityManager, $this->calendarDocRepository); $query = $provider->buildFetchQueryForPerson($person, $startDate, $endDate, $content); @@ -87,7 +91,7 @@ class AccompanyingPeriodCalendarGenericDocProviderTest extends KernelTestCase $security = $this->prophesize(Security::class); $security->isGranted(CalendarVoter::SEE, Argument::any())->willReturn(false); - $provider = new AccompanyingPeriodCalendarGenericDocProvider($security->reveal(), $this->entityManager); + $provider = new AccompanyingPeriodCalendarGenericDocProvider($security->reveal(), $this->entityManager, $this->calendarDocRepository); $query = $provider->buildFetchQueryForPerson($person, $startDate, $endDate, $content); diff --git a/src/Bundle/ChillPersonBundle/Tests/Service/GenericDoc/Providers/AccompanyingPeriodWorkEvaluationGenericDocProviderTest.php b/src/Bundle/ChillPersonBundle/Tests/Service/GenericDoc/Providers/AccompanyingPeriodWorkEvaluationGenericDocProviderTest.php index a90d5f751..7bf2b6bd8 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Service/GenericDoc/Providers/AccompanyingPeriodWorkEvaluationGenericDocProviderTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Service/GenericDoc/Providers/AccompanyingPeriodWorkEvaluationGenericDocProviderTest.php @@ -9,10 +9,11 @@ declare(strict_types=1); * the LICENSE file that was distributed with this source code. */ -namespace Service\GenericDoc\Providers; +namespace Chill\PersonBundle\Tests\Service\GenericDoc\Providers; use Chill\DocStoreBundle\GenericDoc\FetchQueryToSqlBuilder; use Chill\PersonBundle\Entity\AccompanyingPeriod; +use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocumentRepository; use Chill\PersonBundle\Service\GenericDoc\Providers\AccompanyingPeriodWorkEvaluationGenericDocProvider; use Doctrine\ORM\EntityManagerInterface; use Prophecy\PhpUnit\ProphecyTrait; @@ -29,10 +30,13 @@ class AccompanyingPeriodWorkEvaluationGenericDocProviderTest extends KernelTestC use ProphecyTrait; private EntityManagerInterface $entityManager; + private AccompanyingPeriodWorkEvaluationDocumentRepository $repository; + protected function setUp(): void { self::bootKernel(); $this->entityManager = self::getContainer()->get(EntityManagerInterface::class); + $this->repository = self::getContainer()->get(AccompanyingPeriodWorkEvaluationDocumentRepository::class); } /** @@ -55,7 +59,8 @@ class AccompanyingPeriodWorkEvaluationGenericDocProviderTest extends KernelTestC $provider = new AccompanyingPeriodWorkEvaluationGenericDocProvider( $security->reveal(), - $this->entityManager + $this->entityManager, + $this->repository, ); $query = $provider->buildFetchQueryForAccompanyingPeriod($period, $startDate, $endDate, $content); diff --git a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php index 780b81c1d..9d3b30df3 100644 --- a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php +++ b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler.php @@ -20,6 +20,7 @@ use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\MainBundle\Workflow\EntityWorkflowWithPublicViewInterface; use Chill\MainBundle\Workflow\EntityWorkflowWithStoredObjectHandlerInterface; use Chill\MainBundle\Workflow\Templating\EntityWorkflowViewMetadataDTO; +use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument; use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocumentRepository; use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkEvaluationDocumentVoter; @@ -85,6 +86,12 @@ class AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler implements EntityW return $this->repository->find($entityWorkflow->getRelatedEntityId()); } + public function getRelatedAccompanyingPeriod(EntityWorkflow $entityWorkflow): ?AccompanyingPeriod + { + return $this->getRelatedEntity($entityWorkflow)?->getAccompanyingPeriodWorkEvaluation()->getAccompanyingPeriodWork() + ->getAccompanyingPeriod(); + } + /** * @return array[] */ diff --git a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationWorkflowHandler.php b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationWorkflowHandler.php index 3e72e65bd..3943cb5d4 100644 --- a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationWorkflowHandler.php +++ b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationWorkflowHandler.php @@ -15,6 +15,7 @@ use Chill\MainBundle\Entity\Workflow\EntityWorkflow; use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface; +use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument; use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationRepository; @@ -66,6 +67,11 @@ readonly class AccompanyingPeriodWorkEvaluationWorkflowHandler implements Entity return $this->repository->find($entityWorkflow->getRelatedEntityId()); } + public function getRelatedAccompanyingPeriod(EntityWorkflow $entityWorkflow): ?AccompanyingPeriod + { + return $this->getRelatedEntity($entityWorkflow)?->getAccompanyingPeriodWork()->getAccompanyingPeriod(); + } + public function getRelatedObjects(object $object): array { $relateds = []; diff --git a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkWorkflowHandler.php b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkWorkflowHandler.php index 58d3c9875..6f67c5c59 100644 --- a/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkWorkflowHandler.php +++ b/src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkWorkflowHandler.php @@ -15,6 +15,7 @@ use Chill\MainBundle\Entity\Workflow\EntityWorkflow; use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface; +use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument; @@ -68,6 +69,11 @@ readonly class AccompanyingPeriodWorkWorkflowHandler implements EntityWorkflowHa return $this->repository->find($entityWorkflow->getRelatedEntityId()); } + public function getRelatedAccompanyingPeriod(EntityWorkflow $entityWorkflow): ?AccompanyingPeriod + { + return $this->getRelatedEntity($entityWorkflow)?->getAccompanyingPeriod(); + } + public function getRelatedObjects(object $object): array { $relateds = []; diff --git a/src/Bundle/ChillPersonBundle/migrations/Version20241212120202.php b/src/Bundle/ChillPersonBundle/migrations/Version20241212120202.php new file mode 100644 index 000000000..dbf42ccff --- /dev/null +++ b/src/Bundle/ChillPersonBundle/migrations/Version20241212120202.php @@ -0,0 +1,35 @@ +addSql('UPDATE chill_doc.stored_object SET title = doc.title FROM chill_person_accompanying_period_work_evaluation_document doc WHERE storedobject_id = stored_object.id'); + $this->addSql('ALTER TABLE chill_person_accompanying_period_work_evaluation_document DROP title'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE chill_person_accompanying_period_work_evaluation_document ADD title TEXT DEFAULT \'\' NOT NULL'); + $this->addSql('UPDATE chill_person_accompanying_period_work_evaluation_document SET title = so.title FROM chill_doc.stored_object so WHERE storedobject_id = so.id'); + } +} diff --git a/tsconfig.json b/tsconfig.json index f4eec6d81..34cea65e0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,9 +1,9 @@ { - "extends": "@tsconfig/node14/tsconfig.json", + "extends": "@tsconfig/node20/tsconfig.json", "compilerOptions": { "paths": { - "ChillMainAssets": ["./src/Bundle/ChillMainBundle/Resources/public"], - "ChillDocStoreAssets": ["./src/Bundle/ChillDocStoreBundle/Resources/public"] + "ChillMainAssets/*": ["./src/Bundle/ChillMainBundle/Resources/public/*"], + "ChillDocStoreAssets/*": ["./src/Bundle/ChillDocStoreBundle/Resources/public/*"] }, "lib": [ "es2020", diff --git a/yarn.lock b/yarn.lock index 152ba9185..9539fd93f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2065,10 +2065,10 @@ resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== -"@tsconfig/node14@^1.0.1": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" - integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== +"@tsconfig/node20@^20.1.4": + version "20.1.4" + resolved "https://registry.yarnpkg.com/@tsconfig/node20/-/node20-20.1.4.tgz#3457d42eddf12d3bde3976186ab0cd22b85df928" + integrity sha512-sqgsT69YFeLWf5NtJ4Xq/xAF8p4ZQHlmGW74Nu2tD4+g5fAsposc4ZfaaPixVu4y01BEiDCWLRDCvDM5JOsRxg== "@types/body-parser@*": version "1.19.5"