diff --git a/src/Bundle/ChillDocStoreBundle/Controller/DocumentAccompanyingCourseDuplicateController.php b/src/Bundle/ChillDocStoreBundle/Controller/DocumentAccompanyingCourseDuplicateController.php new file mode 100644 index 000000000..129109634 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Controller/DocumentAccompanyingCourseDuplicateController.php @@ -0,0 +1,55 @@ +security->isGranted(AccompanyingCourseDocumentVoter::SEE, $document)) { + throw new AccessDeniedHttpException('not allowed to see this document'); + } + + if (!$this->security->isGranted(AccompanyingCourseDocumentVoter::CREATE, $document->getCourse())) { + throw new AccessDeniedHttpException('not allowed to create this document'); + } + + $duplicated = $this->documentWorkflowDuplicator->duplicate($document); + $this->entityManager->persist($duplicated); + $this->entityManager->flush(); + + return new RedirectResponse( + $this->urlGenerator->generate('accompanying_course_document_edit', ['id' => $duplicated->getId(), 'course' => $duplicated->getCourse()->getId()]) + ); + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php b/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php index 7033a53c9..3590aae7f 100644 --- a/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php +++ b/src/Bundle/ChillDocStoreBundle/Repository/AccompanyingCourseDocumentRepository.php @@ -23,7 +23,7 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository, Associat { private readonly EntityRepository $repository; - public function __construct(private readonly EntityManagerInterface $em) + public function __construct(EntityManagerInterface $em) { $this->repository = $em->getRepository(AccompanyingCourseDocument::class); } 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 2a3592d73..7ec761464 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/views/List/list_item.html.twig +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/List/list_item.html.twig @@ -73,8 +73,15 @@
  • {{ 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) %} @@ -82,9 +89,9 @@ {% endif %} - {% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_DELETE', document) %} -
  • - + {% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS', document) %} +
  • +
  • {% endif %} {% else %} diff --git a/src/Bundle/ChillDocStoreBundle/Service/StoredObjectDuplicate.php b/src/Bundle/ChillDocStoreBundle/Service/StoredObjectDuplicate.php new file mode 100644 index 000000000..b1c7e7a87 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Service/StoredObjectDuplicate.php @@ -0,0 +1,47 @@ +getCurrentVersion(); + + $oldContent = $this->storedObjectManager->read($fromVersion); + + $storedObject = new StoredObject(); + + $newVersion = $this->storedObjectManager->write($storedObject, $oldContent, $fromVersion->getType()); + + $newVersion->setCreatedFrom($fromVersion); + + $this->logger->info('[StoredObjectDuplicate] Duplicated stored object from a version of a previous stored object', [ + 'from_stored_object_uuid' => $fromVersion->getStoredObject()->getUuid(), + 'to_stored_object_uuid' => $storedObject->getUuid(), + 'old_version_id' => $fromVersion->getId(), + 'old_version_version' => $fromVersion->getVersion(), + 'new_version_id' => $newVersion->getVersion(), + ]); + + return $storedObject; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Service/StoredObjectRestore.php b/src/Bundle/ChillDocStoreBundle/Service/StoredObjectRestore.php index 3c6469185..bad032346 100644 --- a/src/Bundle/ChillDocStoreBundle/Service/StoredObjectRestore.php +++ b/src/Bundle/ChillDocStoreBundle/Service/StoredObjectRestore.php @@ -14,6 +14,9 @@ namespace Chill\DocStoreBundle\Service; use Chill\DocStoreBundle\Entity\StoredObjectVersion; use Psr\Log\LoggerInterface; +/** + * Class responsible for restoring stored object versions into the same stored object. + */ final readonly class StoredObjectRestore implements StoredObjectRestoreInterface { public function __construct(private readonly StoredObjectManagerInterface $storedObjectManager, private readonly LoggerInterface $logger) {} diff --git a/src/Bundle/ChillDocStoreBundle/Service/WorkflowStoredObjectPermissionHelper.php b/src/Bundle/ChillDocStoreBundle/Service/WorkflowStoredObjectPermissionHelper.php index b27b6d96a..545325121 100644 --- a/src/Bundle/ChillDocStoreBundle/Service/WorkflowStoredObjectPermissionHelper.php +++ b/src/Bundle/ChillDocStoreBundle/Service/WorkflowStoredObjectPermissionHelper.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\Service; +use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum; use Chill\MainBundle\Workflow\EntityWorkflowManager; use Symfony\Component\Security\Core\Security; @@ -31,6 +32,16 @@ class WorkflowStoredObjectPermissionHelper if (!$workflow->getCurrentStep()->getAllDestUser()->contains($currentUser)) { return false; } + + // as soon as there is one signatured applyied, we are not able to + // edit the document any more + foreach ($workflow->getSteps() as $step) { + foreach ($step->getSignatures() as $signature) { + if (EntityWorkflowSignatureStateEnum::SIGNED === $signature->getState()) { + return false; + } + } + } } return true; diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Service/StoredObjectDuplicateTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Service/StoredObjectDuplicateTest.php new file mode 100644 index 000000000..2e9da0bab --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Tests/Service/StoredObjectDuplicateTest.php @@ -0,0 +1,48 @@ +registerVersion(type: $type = 'application/test'); + + $manager = $this->createMock(StoredObjectManagerInterface::class); + $manager->method('read')->with($version)->willReturn('1234'); + $manager + ->expects($this->once()) + ->method('write') + ->with($this->isInstanceOf(StoredObject::class), '1234', 'application/test') + ->willReturnCallback(fn (StoredObject $so, $content, $type) => $so->registerVersion(type: $type)); + + $storedObjectDuplicate = new StoredObjectDuplicate($manager, new NullLogger()); + + $actual = $storedObjectDuplicate->duplicate($storedObject); + + self::assertNotNull($actual->getCurrentVersion()); + self::assertNotNull($actual->getCurrentVersion()->getCreatedFrom()); + self::assertSame($version, $actual->getCurrentVersion()->getCreatedFrom()); + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Service/WorkflowStoredObjectPermissionHelperTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Service/WorkflowStoredObjectPermissionHelperTest.php new file mode 100644 index 000000000..e87ea4653 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Tests/Service/WorkflowStoredObjectPermissionHelperTest.php @@ -0,0 +1,101 @@ +buildHelper($object, $entityWorkflow, $user); + + self::assertEquals($expected, $helper->notBlockedByWorkflow($entityWorkflow), $message); + } + + private function buildHelper(object $relatedEntity, EntityWorkflow $entityWorkflow, User $user): WorkflowStoredObjectPermissionHelper + { + $security = $this->prophesize(Security::class); + $security->getUser()->willReturn($user); + + $entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class); + $entityWorkflowManager->findByRelatedEntity(Argument::type('object'))->willReturn([$entityWorkflow]); + + return new WorkflowStoredObjectPermissionHelper($security->reveal(), $entityWorkflowManager->reveal()); + } + + public static function provideDataNotBlockByWorkflow(): iterable + { + $entityWorkflow = new EntityWorkflow(); + $dto = new WorkflowTransitionContextDTO($entityWorkflow); + $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable()); + + yield [$entityWorkflow, new User(), false, 'blocked because the user is not present as a dest user']; + + $entityWorkflow = new EntityWorkflow(); + $dto = new WorkflowTransitionContextDTO($entityWorkflow); + $dto->futureDestUsers[] = $user = new User(); + $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), $user); + + yield [$entityWorkflow, $user, true, 'allowed because the user is present as a dest user']; + + $entityWorkflow = new EntityWorkflow(); + $dto = new WorkflowTransitionContextDTO($entityWorkflow); + $dto->futureDestUsers[] = $user = new User(); + $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), $user); + $entityWorkflow->getCurrentStep()->setIsFinal(true); + + yield [$entityWorkflow, $user, false, 'blocked because the step is final']; + + $entityWorkflow = new EntityWorkflow(); + $dto = new WorkflowTransitionContextDTO($entityWorkflow); + $dto->futureDestUsers[] = $user = new User(); + $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), $user); + $step = $entityWorkflow->getCurrentStep(); + new EntityWorkflowStepSignature($step, new Person()); + + yield [$entityWorkflow, $user, true, 'allow, a signature is present but still pending']; + + $entityWorkflow = new EntityWorkflow(); + $dto = new WorkflowTransitionContextDTO($entityWorkflow); + $dto->futureDestUsers[] = $user = new User(); + $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), $user); + $step = $entityWorkflow->getCurrentStep(); + $signature = new EntityWorkflowStepSignature($step, new Person()); + $signature->setState(EntityWorkflowSignatureStateEnum::SIGNED); + + yield [$entityWorkflow, $user, false, 'blocked, a signature is present and signed']; + + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Workflow/AccompanyingCourseDocumentDuplicatorTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Workflow/AccompanyingCourseDocumentDuplicatorTest.php new file mode 100644 index 000000000..71df75d20 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Tests/Workflow/AccompanyingCourseDocumentDuplicatorTest.php @@ -0,0 +1,70 @@ +setDate($date = new \DateTimeImmutable()) + ->setObject($object) + ->setTitle('Title') + ->setUser($user = new User()) + ->setCategory($category = new DocumentCategory('bundle', 10)) + ->setDescription($description = 'Description'); + + $actual = $this->buildDuplicator()->duplicate($document); + + self::assertSame($date, $actual->getDate()); + // FYI, the duplication of object is checked by the mock + self::assertNotNull($actual->getObject()); + self::assertStringStartsWith('Title', $actual->getTitle()); + self::assertSame($user, $actual->getUser()); + self::assertSame($category, $actual->getCategory()); + self::assertEquals($description, $actual->getDescription()); + } + + private function buildDuplicator(): AccompanyingCourseDocumentDuplicator + { + $storedObjectDuplicate = $this->createMock(StoredObjectDuplicate::class); + $storedObjectDuplicate->expects($this->once())->method('duplicate') + ->with($this->isInstanceOf(StoredObject::class))->willReturn(new StoredObject()); + $translator = $this->createMock(TranslatorInterface::class); + $translator->method('trans')->withAnyParameters()->willReturn('duplicated'); + $clock = new MockClock(); + + return new AccompanyingCourseDocumentDuplicator( + $storedObjectDuplicate, + $translator, + $clock + ); + } +} diff --git a/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentDuplicator.php b/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentDuplicator.php new file mode 100644 index 000000000..22ee65cc3 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentDuplicator.php @@ -0,0 +1,45 @@ +setCourse($document->getCourse()) + ->setTitle($document->getTitle().' ('.$this->translator->trans('acc_course_document.duplicated_at', ['at' => $this->clock->now()]).')') + ->setDate($document->getDate()) + ->setDescription($document->getDescription()) + ->setCategory($document->getCategory()) + ->setUser($document->getUser()) + ->setObject($this->storedObjectDuplicate->duplicate($document->getObject())) + ; + + return $newDoc; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/translations/messages+intl-icu.fr.yml b/src/Bundle/ChillDocStoreBundle/translations/messages+intl-icu.fr.yml new file mode 100644 index 000000000..0132cdc4f --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/translations/messages+intl-icu.fr.yml @@ -0,0 +1,3 @@ +acc_course_document: + duplicated_at: >- + Dupliqué le {at, date, long} à {at, time, short} diff --git a/src/Bundle/ChillMainBundle/Resources/public/lib/entity-workflow/api.js b/src/Bundle/ChillMainBundle/Resources/public/lib/entity-workflow/api.ts similarity index 52% rename from src/Bundle/ChillMainBundle/Resources/public/lib/entity-workflow/api.js rename to src/Bundle/ChillMainBundle/Resources/public/lib/entity-workflow/api.ts index a89dd66f5..486421904 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/lib/entity-workflow/api.js +++ b/src/Bundle/ChillMainBundle/Resources/public/lib/entity-workflow/api.ts @@ -1,12 +1,8 @@ -const buildLinkCreate = function(workflowName, relatedEntityClass, relatedEntityId) { +export const buildLinkCreate = (workflowName: string, relatedEntityClass: string, relatedEntityId: number): string => { let params = new URLSearchParams(); params.set('entityClass', relatedEntityClass); - params.set('entityId', relatedEntityId); + params.set('entityId', relatedEntityId.toString(10)); params.set('workflow', workflowName); return `/fr/main/workflow/create?`+params.toString(); }; - -export { - buildLinkCreate, -}; diff --git a/src/Bundle/ChillMainBundle/Resources/public/types.ts b/src/Bundle/ChillMainBundle/Resources/public/types.ts index 2e33b8248..304ebd8e4 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/types.ts +++ b/src/Bundle/ChillMainBundle/Resources/public/types.ts @@ -168,3 +168,8 @@ export interface NewsItemType { startDate: DateTime; endDate: DateTime | null; } + +export interface WorkflowAvailable { + name: string; + text: string; +} diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/EntityWorkflow/PickWorkflow.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/EntityWorkflow/PickWorkflow.vue index 9ed1ef6cc..e429adf31 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/EntityWorkflow/PickWorkflow.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/EntityWorkflow/PickWorkflow.vue @@ -1,63 +1,52 @@