mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
Merge branch 'signature-app/wp-625-duplicate-ro-element-in-workflows' into 'signature-app-master'
Make stored object read-only if a signature was added to the document and allow to duplicate the related entity in workflow See merge request Chill-Projet/chill-bundles!731
This commit is contained in:
commit
b350c0cfe8
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Controller;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
|
||||
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
|
||||
use Chill\DocStoreBundle\Workflow\AccompanyingCourseDocumentDuplicator;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Session\Session;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
final readonly class DocumentAccompanyingCourseDuplicateController
|
||||
{
|
||||
public function __construct(
|
||||
private Security $security,
|
||||
private AccompanyingCourseDocumentDuplicator $documentWorkflowDuplicator,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private UrlGeneratorInterface $urlGenerator,
|
||||
) {}
|
||||
|
||||
#[Route('/{_locale}/doc-store/accompanying-course-document/{id}/duplicate', name: 'chill_doc_store_accompanying_course_document_duplicate')]
|
||||
public function __invoke(AccompanyingCourseDocument $document, Request $request, Session $session): Response
|
||||
{
|
||||
if (!$this->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()])
|
||||
);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -73,8 +73,15 @@
|
||||
<li>
|
||||
{{ document.object|chill_document_button_group(document.title) }}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_DELETE', document) %}
|
||||
<li class="delete">
|
||||
<a href="{{ chill_return_path_or('chill_docstore_accompanying_course_document_delete', {'course': accompanyingCourse.id, 'id': document.id}) }}" class="btn btn-delete"></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_CREATE', document.course) %}
|
||||
<li>
|
||||
<a href="{{ chill_path_add_return_path('accompanying_course_document_show', {'course': accompanyingCourse.id, 'id': document.id}) }}" class="btn btn-show"></a>
|
||||
<a href="{{ chill_path_add_return_path('chill_doc_store_accompanying_course_document_duplicate', {'id': document.id}) }}" class="btn btn-duplicate" title="{{ 'Duplicate'|trans|e('html_attr') }}"></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE', document) %}
|
||||
@ -82,9 +89,9 @@
|
||||
<a href="{{ path('accompanying_course_document_edit', {'course': accompanyingCourse.id, 'id': document.id }) }}" class="btn btn-update"></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_DELETE', document) %}
|
||||
<li class="delete">
|
||||
<a href="{{ chill_return_path_or('chill_docstore_accompanying_course_document_delete', {'course': accompanyingCourse.id, 'id': document.id}) }}" class="btn btn-delete"></a>
|
||||
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS', document) %}
|
||||
<li>
|
||||
<a href="{{ chill_path_add_return_path('accompanying_course_document_show', {'course': accompanyingCourse.id, 'id': document.id}) }}" class="btn btn-show"></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
|
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Service;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
* Class which duplicate a stored object into a new one, recreating a stored object.
|
||||
*/
|
||||
class StoredObjectDuplicate
|
||||
{
|
||||
public function __construct(private readonly StoredObjectManagerInterface $storedObjectManager, private readonly LoggerInterface $logger) {}
|
||||
|
||||
public function duplicate(StoredObject|StoredObjectVersion $from): StoredObject
|
||||
{
|
||||
$fromVersion = $from instanceof StoredObjectVersion ? $from : $from->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;
|
||||
}
|
||||
}
|
@ -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) {}
|
||||
|
@ -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;
|
||||
|
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Tests\Service;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectDuplicate;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\NullLogger;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class StoredObjectDuplicateTest extends TestCase
|
||||
{
|
||||
public function testDuplicateHappyScenario(): void
|
||||
{
|
||||
$storedObject = new StoredObject();
|
||||
$version = $storedObject->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());
|
||||
}
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Tests\Service;
|
||||
|
||||
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class WorkflowStoredObjectPermissionHelperTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
/**
|
||||
* @dataProvider provideDataNotBlockByWorkflow
|
||||
*/
|
||||
public function testNotBlockByWorkflow(EntityWorkflow $entityWorkflow, User $user, bool $expected, string $message): void
|
||||
{
|
||||
$object = new \stdClass();
|
||||
$helper = $this->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'];
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Tests\Workflow;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
|
||||
use Chill\DocStoreBundle\Entity\DocumentCategory;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectDuplicate;
|
||||
use Chill\DocStoreBundle\Workflow\AccompanyingCourseDocumentDuplicator;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Clock\MockClock;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class AccompanyingCourseDocumentDuplicatorTest extends TestCase
|
||||
{
|
||||
public function testDuplicate(): void
|
||||
{
|
||||
$object = new StoredObject();
|
||||
|
||||
$document = new AccompanyingCourseDocument();
|
||||
$document
|
||||
->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
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Workflow;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectDuplicate;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* Stores the logic to duplicate an AccompanyingCourseDocument associated to a workflow.
|
||||
*/
|
||||
class AccompanyingCourseDocumentDuplicator
|
||||
{
|
||||
public function __construct(
|
||||
private readonly StoredObjectDuplicate $storedObjectDuplicate,
|
||||
private readonly TranslatorInterface $translator,
|
||||
private readonly ClockInterface $clock,
|
||||
) {}
|
||||
|
||||
public function duplicate(AccompanyingCourseDocument $document): AccompanyingCourseDocument
|
||||
{
|
||||
$newDoc = new AccompanyingCourseDocument();
|
||||
$newDoc
|
||||
->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;
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
acc_course_document:
|
||||
duplicated_at: >-
|
||||
Dupliqué le {at, date, long} à {at, time, short}
|
@ -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,
|
||||
};
|
@ -168,3 +168,8 @@ export interface NewsItemType {
|
||||
startDate: DateTime;
|
||||
endDate: DateTime | null;
|
||||
}
|
||||
|
||||
export interface WorkflowAvailable {
|
||||
name: string;
|
||||
text: string;
|
||||
}
|
||||
|
@ -1,63 +1,52 @@
|
||||
<template>
|
||||
<template v-if="workflowsAvailables.length >= 1">
|
||||
<template v-if="props.workflowsAvailables.length >= 1">
|
||||
<div class="dropdown d-grid gap-2">
|
||||
<button class="btn btn-primary dropdown-toggle" type="button" id="createWorkflowButton" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Créer un workflow
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="createWorkflowButton">
|
||||
<li v-for="w in workflowsAvailables" :key="w.name">
|
||||
<a class="dropdown-item" :href="makeLink(w.name)" @click.prevent="goToGenerateWorkflow($event, w.name)">{{ w.text }}</a>
|
||||
<li v-for="w in props.workflowsAvailables" :key="w.name">
|
||||
<button class="dropdown-item" type="button" @click.prevent="goToGenerateWorkflow($event, w.name)">{{ w.text }}</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
|
||||
import {buildLinkCreate} from 'ChillMainAssets/lib/entity-workflow/api.js';
|
||||
import {buildLinkCreate} from '../../../lib/entity-workflow/api';
|
||||
import {WorkflowAvailable} from "../../../types";
|
||||
|
||||
export default {
|
||||
name: "PickWorkflow",
|
||||
props: {
|
||||
relatedEntityClass: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
relatedEntityId: {
|
||||
type: Number,
|
||||
required: false,
|
||||
},
|
||||
workflowsAvailables: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
preventDefaultMoveToGenerate: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
goToGenerateWorkflowPayload: {
|
||||
required: false,
|
||||
default: {}
|
||||
},
|
||||
},
|
||||
emits: ['goToGenerateWorkflow'],
|
||||
methods: {
|
||||
makeLink(workflowName) {
|
||||
return buildLinkCreate(workflowName, this.relatedEntityClass, this.relatedEntityId);
|
||||
},
|
||||
goToGenerateWorkflow(event, workflowName) {
|
||||
console.log('goToGenerateWorkflow', event, workflowName);
|
||||
interface PickWorkflowConfig {
|
||||
relatedEntityClass: string;
|
||||
relatedEntityId: number;
|
||||
workflowsAvailables: WorkflowAvailable[];
|
||||
preventDefaultMoveToGenerate: boolean;
|
||||
goToGenerateWorkflowPayload: object;
|
||||
}
|
||||
|
||||
if (!this.$props.preventDefaultMoveToGenerate) {
|
||||
console.log('to go generate');
|
||||
window.location.assign(this.makeLink(workflowName));
|
||||
}
|
||||
const props = withDefaults(defineProps<PickWorkflowConfig>(), {preventDefaultMoveToGenerate: false, goToGenerateWorkflowPayload: {}});
|
||||
|
||||
this.$emit('goToGenerateWorkflow', {event, workflowName, link: this.makeLink(workflowName), payload: this.goToGenerateWorkflowPayload});
|
||||
}
|
||||
const emit = defineEmits<{
|
||||
(e: 'goToGenerateWorkflow', {event: MouseEvent, workflowName: string, link: string, payload: object}): void;
|
||||
}>();
|
||||
|
||||
const makeLink = (workflowName: string): string => buildLinkCreate(workflowName, props.relatedEntityClass, props.relatedEntityId);
|
||||
|
||||
const goToGenerateWorkflow = (event: MouseEvent, workflowName: string): void => {
|
||||
console.log('goToGenerateWorkflow', event, workflowName);
|
||||
|
||||
if (!props.preventDefaultMoveToGenerate) {
|
||||
console.log('to go generate');
|
||||
window.location.assign(makeLink(workflowName));
|
||||
}
|
||||
|
||||
emit('goToGenerateWorkflow', {event, workflowName, link: makeLink(workflowName), payload: props.goToGenerateWorkflowPayload});
|
||||
}
|
||||
|
||||
const goToDuplicateRelatedEntity = (event: MouseEvent, workflowName: string): void => {
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -13,7 +13,6 @@ namespace Chill\MainBundle\Workflow\Templating;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Chill\MainBundle\Workflow\Helper\MetadataExtractor;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
use Symfony\Component\Workflow\Registry;
|
||||
@ -23,7 +22,7 @@ use Twig\Extension\RuntimeExtensionInterface;
|
||||
|
||||
class WorkflowTwigExtensionRuntime implements RuntimeExtensionInterface
|
||||
{
|
||||
public function __construct(private readonly EntityWorkflowManager $entityWorkflowManager, private readonly Registry $registry, private readonly EntityWorkflowRepository $repository, private readonly MetadataExtractor $metadataExtractor, private readonly NormalizerInterface $normalizer) {}
|
||||
public function __construct(private readonly Registry $registry, private readonly EntityWorkflowRepository $repository, private readonly MetadataExtractor $metadataExtractor, private readonly NormalizerInterface $normalizer) {}
|
||||
|
||||
public function getTransitionByString(EntityWorkflow $entityWorkflow, string $key): ?Transition
|
||||
{
|
||||
|
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\PersonBundle\Controller;
|
||||
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
|
||||
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkVoter;
|
||||
use Chill\PersonBundle\Service\AccompanyingPeriodWorkEvaluationDocument\AccompanyingPeriodWorkEvaluationDocumentDuplicator;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
class AccompanyingPeriodWorkEvaluationDocumentDuplicateController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AccompanyingPeriodWorkEvaluationDocumentDuplicator $duplicator,
|
||||
private readonly Security $security,
|
||||
private readonly SerializerInterface $serializer,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
) {}
|
||||
|
||||
#[Route('/api/1.0/person/accompanying-course-work-evaluation-document/{id}/duplicate', methods: ['POST'])]
|
||||
public function __invoke(AccompanyingPeriodWorkEvaluationDocument $document): Response
|
||||
{
|
||||
$work = $document->getAccompanyingPeriodWorkEvaluation()->getAccompanyingPeriodWork();
|
||||
|
||||
if (!$this->security->isGranted(AccompanyingPeriodWorkVoter::UPDATE, $work)) {
|
||||
throw new AccessDeniedHttpException('not allowed to edit this accompanying period work');
|
||||
}
|
||||
|
||||
$duplicatedDocument = $this->duplicator->duplicate($document);
|
||||
|
||||
$this->entityManager->persist($duplicatedDocument);
|
||||
$this->entityManager->persist($duplicatedDocument->getStoredObject());
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new JsonResponse(
|
||||
$this->serializer->serialize($duplicatedDocument, 'json', [AbstractNormalizer::GROUPS => ['read']]),
|
||||
json: true
|
||||
);
|
||||
}
|
||||
}
|
@ -1,4 +1,12 @@
|
||||
import {Address, Center, Civility, DateTime} from "../../../ChillMainBundle/Resources/public/types";
|
||||
import {
|
||||
Address,
|
||||
Center,
|
||||
Civility,
|
||||
DateTime,
|
||||
User,
|
||||
WorkflowAvailable
|
||||
} from "../../../ChillMainBundle/Resources/public/types";
|
||||
import {StoredObject} from "../../../ChillDocStoreBundle/Resources/public/types";
|
||||
|
||||
export interface Person {
|
||||
id: number;
|
||||
@ -20,3 +28,16 @@ export interface Person {
|
||||
current_household_id: number;
|
||||
current_residential_addresses: Address[];
|
||||
}
|
||||
|
||||
export interface AccompanyingPeriodWorkEvaluationDocument {
|
||||
id: number;
|
||||
type: "accompanying_period_work_evaluation_document"
|
||||
storedObject: StoredObject;
|
||||
title: string;
|
||||
createdAt: DateTime | null;
|
||||
createdBy: User | null;
|
||||
updatedAt: DateTime | null;
|
||||
updatedBy: User | null;
|
||||
workflows_availables: WorkflowAvailable[];
|
||||
workflows: object[];
|
||||
}
|
||||
|
@ -342,7 +342,7 @@ import OnTheFly from 'ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue';
|
||||
import ListWorkflowModal from 'ChillMainAssets/vuejs/_components/EntityWorkflow/ListWorkflowModal.vue';
|
||||
import PickWorkflow from 'ChillMainAssets/vuejs/_components/EntityWorkflow/PickWorkflow.vue';
|
||||
import PersonText from 'ChillPersonAssets/vuejs/_components/Entity/PersonText.vue';
|
||||
import {buildLinkCreate} from 'ChillMainAssets/lib/entity-workflow/api.js';
|
||||
import {buildLinkCreate} from 'ChillMainAssets/lib/entity-workflow/api';
|
||||
import { makeFetch } from 'ChillMainAssets/lib/api/apiMethods';
|
||||
|
||||
const i18n = {
|
||||
|
@ -54,7 +54,7 @@
|
||||
import FormEvaluation from './FormEvaluation.vue';
|
||||
import Modal from 'ChillMainAssets/vuejs/_components/Modal';
|
||||
import ListWorkflowModal from 'ChillMainAssets/vuejs/_components/EntityWorkflow/ListWorkflowModal.vue';
|
||||
import {buildLinkCreate} from 'ChillMainAssets/lib/entity-workflow/api.js';
|
||||
import {buildLinkCreate} from 'ChillMainAssets/lib/entity-workflow/api';
|
||||
|
||||
const i18n = {
|
||||
messages: {
|
||||
|
@ -147,6 +147,9 @@
|
||||
<a class="btn btn-delete" @click="removeDocument(d)">
|
||||
</a>
|
||||
</li>
|
||||
<li v-if="Number.isInteger(d.id)">
|
||||
<button type="button" @click="duplicateDocument(d)" class="btn btn-duplicate" title="Dupliquer"></button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@ -189,7 +192,7 @@ import { mapGetters, mapState } from 'vuex';
|
||||
import PickTemplate from 'ChillDocGeneratorAssets/vuejs/_components/PickTemplate.vue';
|
||||
import {buildLink} from 'ChillDocGeneratorAssets/lib/document-generator';
|
||||
import ListWorkflowModal from 'ChillMainAssets/vuejs/_components/EntityWorkflow/ListWorkflowModal.vue';
|
||||
import {buildLinkCreate} from 'ChillMainAssets/lib/entity-workflow/api.js';
|
||||
import {buildLinkCreate} from 'ChillMainAssets/lib/entity-workflow/api';
|
||||
import {buildLinkCreate as buildLinkCreateNotification} from 'ChillMainAssets/lib/entity-notification/api';
|
||||
import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue";
|
||||
import DropFileModal from "ChillDocStoreAssets/vuejs/DropFileWidget/DropFileModal.vue";
|
||||
@ -396,6 +399,9 @@ export default {
|
||||
this.$store.commit('removeDocument', {key: this.evaluation.key, document: document});
|
||||
}
|
||||
},
|
||||
duplicateDocument(document) {
|
||||
this.$store.dispatch('duplicateDocument', {evaluation_key: this.evaluation.key, document: document});
|
||||
},
|
||||
onStatusDocumentChanged(newStatus) {
|
||||
console.log('onStatusDocumentChanged', newStatus);
|
||||
this.$store.commit('statusDocumentChanged', {key: this.evaluation.key, newStatus: newStatus});
|
||||
|
@ -4,6 +4,7 @@ import { findSocialActionsBySocialIssue } from 'ChillPersonAssets/vuejs/_api/Soc
|
||||
import { create } from 'ChillPersonAssets/vuejs/_api/AccompanyingCourseWork.js';
|
||||
import { fetchResults, makeFetch } from 'ChillMainAssets/lib/api/apiMethods.ts';
|
||||
import { fetchTemplates } from 'ChillDocGeneratorAssets/api/pickTemplate.js';
|
||||
import { duplicate } from '../_api/accompanyingCourseWorkEvaluationDocument';
|
||||
|
||||
const debug = process.env.NODE_ENV !== 'production';
|
||||
const evalFQDN = encodeURIComponent("Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluation");
|
||||
@ -238,6 +239,14 @@ const store = createStore({
|
||||
}
|
||||
evaluation.documents = evaluation.documents.filter(d => d.key !== document.key);
|
||||
},
|
||||
addDuplicatedDocument(state, {document, evaluation_key}) {
|
||||
console.log('add duplicated document', document);
|
||||
console.log('add duplicated dcuemnt - evaluation key', evaluation_key);
|
||||
|
||||
let evaluation = state.evaluationsPicked.find(e => e.key === evaluation_key);
|
||||
document.key = evaluation.documents.length + 1;
|
||||
evaluation.documents.splice(0, 0, document);
|
||||
},
|
||||
/**
|
||||
* Replaces a document in the state with a new document.
|
||||
*
|
||||
@ -506,6 +515,10 @@ const store = createStore({
|
||||
addDocument({commit}, payload) {
|
||||
commit('addDocument', payload);
|
||||
},
|
||||
async duplicateDocument({commit}, {document, evaluation_key}) {
|
||||
const newDoc = await duplicate(document.id);
|
||||
commit('addDuplicatedDocument', {document: newDoc, evaluation_key});
|
||||
},
|
||||
removeDocument({commit}, payload) {
|
||||
commit('removeDocument', payload);
|
||||
},
|
||||
|
@ -0,0 +1,6 @@
|
||||
import {AccompanyingPeriodWorkEvaluationDocument} from "../../types";
|
||||
import {makeFetch} from "../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
|
||||
|
||||
export const duplicate = async (id: number): Promise<AccompanyingPeriodWorkEvaluationDocument> => {
|
||||
return makeFetch<null, AccompanyingPeriodWorkEvaluationDocument>("POST", `/api/1.0/person/accompanying-course-work-evaluation-document/${id}/duplicate`);
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\PersonBundle\Service\AccompanyingPeriodWorkEvaluationDocument;
|
||||
|
||||
use Chill\DocStoreBundle\Service\StoredObjectDuplicate;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class AccompanyingPeriodWorkEvaluationDocumentDuplicator
|
||||
{
|
||||
public function __construct(
|
||||
private readonly StoredObjectDuplicate $storedObjectDuplicate,
|
||||
private readonly TranslatorInterface $translator,
|
||||
private readonly ClockInterface $clock,
|
||||
) {}
|
||||
|
||||
public function duplicate(AccompanyingPeriodWorkEvaluationDocument $document): AccompanyingPeriodWorkEvaluationDocument
|
||||
{
|
||||
$newDocument = new AccompanyingPeriodWorkEvaluationDocument();
|
||||
$newDocument
|
||||
->setTitle($document->getTitle().' ('.$this->translator->trans('accompanying_course_evaluation_document.duplicated_at', ['at' => $this->clock->now()]).')')
|
||||
->setStoredObject($this->storedObjectDuplicate->duplicate($document->getStoredObject()))
|
||||
;
|
||||
|
||||
$document->getAccompanyingPeriodWorkEvaluation()->addDocument($newDocument);
|
||||
|
||||
return $newDocument;
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\PersonBundle\Tests\Service\AccompanyingPeriodWorkEvaluationDocument;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectDuplicate;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
|
||||
use Chill\PersonBundle\Service\AccompanyingPeriodWorkEvaluationDocument\AccompanyingPeriodWorkEvaluationDocumentDuplicator;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Clock\MockClock;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class AccompanyingPeriodWorkEvaluationDocumentDuplicatorTest extends TestCase
|
||||
{
|
||||
public function testDuplicate()
|
||||
{
|
||||
$storedObject = new StoredObject();
|
||||
$evaluation = new AccompanyingPeriodWorkEvaluation();
|
||||
$document = new AccompanyingPeriodWorkEvaluationDocument();
|
||||
$document
|
||||
->setStoredObject($storedObject)
|
||||
->setTitle('test title')
|
||||
->setAccompanyingPeriodWorkEvaluation($evaluation);
|
||||
|
||||
$storedObjectDuplicator = $this->createMock(StoredObjectDuplicate::class);
|
||||
$storedObjectDuplicator->expects($this->once())->method('duplicate')->with($storedObject)->willReturn($newStoredObject = new StoredObject());
|
||||
|
||||
$translator = $this->createMock(TranslatorInterface::class);
|
||||
$translator->method('trans')->willReturn('test translated');
|
||||
|
||||
$clock = new MockClock();
|
||||
|
||||
$duplicator = new AccompanyingPeriodWorkEvaluationDocumentDuplicator($storedObjectDuplicator, $translator, $clock);
|
||||
|
||||
$actual = $duplicator->duplicate($document);
|
||||
|
||||
self::assertNotSame($document, $actual);
|
||||
self::assertStringStartsWith('test title', $actual->getTitle());
|
||||
self::assertSame($newStoredObject, $actual->getStoredObject());
|
||||
self::assertSame($evaluation, $actual->getAccompanyingPeriodWorkEvaluation());
|
||||
}
|
||||
}
|
@ -1948,3 +1948,25 @@ paths:
|
||||
responses:
|
||||
200:
|
||||
description: "OK"
|
||||
|
||||
/1.0/person/accompanying-course-work-evaluation-document/{id}/duplicate:
|
||||
post:
|
||||
tags:
|
||||
- accompanying-course-work-evaluation-document
|
||||
summary: Dupliate an an accompanying period work evaluation document
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
description: The document's id
|
||||
schema:
|
||||
type: integer
|
||||
format: integer
|
||||
minimum: 1
|
||||
responses:
|
||||
200:
|
||||
description: "OK"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
|
@ -163,8 +163,8 @@ exports:
|
||||
{ total, plural,
|
||||
=0 {Aucun usager ne correspond aux termes de recherche}
|
||||
one {Un usager correspond aux termes de recherche}
|
||||
many {# usagers correspondent aux terms de recherche}
|
||||
other {# usagers correspondent aux terms de recherche}
|
||||
many {# usagers correspondent aux termes de recherche}
|
||||
other {# usagers correspondent aux termes de recherche}
|
||||
}
|
||||
|
||||
'nb person with similar name. Please verify that this is a new person': >-
|
||||
@ -173,3 +173,7 @@ exports:
|
||||
one {Un usager a un nom similaire. Vérifiez qu'il ne s'agit pas de lui.}
|
||||
other {# usagers ont un nom similaire. Vérifiez qu'il ne s'agit pas de l'un d'eux}
|
||||
}
|
||||
|
||||
accompanying_course_evaluation_document:
|
||||
duplicated_at: >-
|
||||
Dupliqué le {at, date, long} à {at, time, short}
|
||||
|
Loading…
x
Reference in New Issue
Block a user