From 3bb911b4d0f10a452785dd455574bda244c38134 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 29 Nov 2023 16:14:19 +0100 Subject: [PATCH] Update version within PUT request Try to add api logic check for version being the same instead of smaller implementing optimistic locking and displaying correct message in frontend rector fixes adjust violation message and add translation in translation.yaml add translator in apiController --- .../Resources/public/lib/api/apiMethods.ts | 25 ++++++++++++++++- .../AccompanyingCourseWorkApiController.php | 28 +++++++++++++++++-- .../AccompanyingPeriodWork.php | 10 ++++--- .../vuejs/AccompanyingCourseWorkEdit/App.vue | 2 +- .../vuejs/AccompanyingCourseWorkEdit/store.js | 8 ++++-- .../migrations/Version20231129113816.php | 7 +++++ .../translations/validators.fr.yml | 1 + 7 files changed, 70 insertions(+), 11 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Resources/public/lib/api/apiMethods.ts b/src/Bundle/ChillMainBundle/Resources/public/lib/api/apiMethods.ts index 17ac8879e..24f518ca3 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/lib/api/apiMethods.ts +++ b/src/Bundle/ChillMainBundle/Resources/public/lib/api/apiMethods.ts @@ -55,11 +55,20 @@ export interface ServerExceptionInterface extends TransportExceptionInterface { body: string; } +export interface ConflictHttpExceptionInterface extends TransportExceptionInterface { + name: 'ConflictHttpException'; + violations: string[]; +} /** * Generic api method that can be adapted to any fetch request */ -export const makeFetch = (method: 'POST'|'GET'|'PUT'|'PATCH'|'DELETE', url: string, body?: body | Input | null, options?: FetchParams): Promise => { +export const makeFetch = ( + method: 'POST'|'GET'|'PUT'|'PATCH'|'DELETE', + url: string, body?: body | Input | null, + options?: FetchParams +): Promise => { + let opts = { method: method, headers: { @@ -67,6 +76,7 @@ export const makeFetch = (method: 'POST'|'GET'|'PUT'|'PATCH'|'DEL }, }; + if (body !== null && typeof body !== 'undefined') { Object.assign(opts, {body: JSON.stringify(body)}) } @@ -90,6 +100,10 @@ export const makeFetch = (method: 'POST'|'GET'|'PUT'|'PATCH'|'DEL throw AccessException(response); } + if (response.status === 409) { + throw ConflictHttpException(response); + } + throw { name: 'Exception', sta: response.status, @@ -220,3 +234,12 @@ const ServerException = (code: number, body: string): ServerExceptionInterface = return error; } + +const ConflictHttpException = (response: Response): ConflictHttpExceptionInterface => { + const error = {} as ConflictHttpExceptionInterface; + + error.name = 'ConflictHttpException'; + error.violations = ['Sorry, but someone else has already changed this entity. Please refresh the page and apply the changes again'] + + return error; +} diff --git a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkApiController.php b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkApiController.php index 47e46cb6b..00d2f9026 100644 --- a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkApiController.php +++ b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkApiController.php @@ -15,15 +15,23 @@ use Chill\MainBundle\CRUD\Controller\ApiController; use Chill\MainBundle\Serializer\Model\Collection; use Chill\MainBundle\Serializer\Model\Counter; use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepository; +use Doctrine\DBAL\LockMode; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\OptimisticLockException; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\ConflictHttpException; use Symfony\Component\Routing\Annotation\Route; +use Symfony\Contracts\Translation\TranslatorInterface; class AccompanyingCourseWorkApiController extends ApiController { - public function __construct(private readonly AccompanyingPeriodWorkRepository $accompanyingPeriodWorkRepository) - { - } + public function __construct( + private readonly AccompanyingPeriodWorkRepository $accompanyingPeriodWorkRepository, + private readonly EntityManagerInterface $em, + private readonly TranslatorInterface $translator, + ) {} /** * @Route("/api/1.0/person/accompanying-period/work/my-near-end") @@ -67,4 +75,18 @@ class AccompanyingCourseWorkApiController extends ApiController return parent::getContextForSerialization($action, $request, $_format, $entity); } + + public function entityPut($action, Request $request, $id, string $_format): Response + { + $entity = $this->accompanyingPeriodWorkRepository->findBy(['id' => $id])[0]; + $expectedVersion = $entity->getVersion(); + + try { + $this->em->lock($entity, LockMode::OPTIMISTIC, $expectedVersion); + + return parent::entityPut($action, $request, $id, $_format); + } catch (OptimisticLockException) { + throw new ConflictHttpException($this->translator->trans('Sorry, but someone else has already changed this entity. Please refresh the page and apply the changes again')); + } + } } diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/AccompanyingPeriodWork.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/AccompanyingPeriodWork.php index be385c664..618918695 100644 --- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/AccompanyingPeriodWork.php +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/AccompanyingPeriodWork.php @@ -245,10 +245,12 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues private ?User $updatedBy = null; /** - * @ORM\Column(type="integer", nullable=false, options={"default": 1}) - * - * @Serializer\Groups({"read", "accompanying_period_work:edit"}) - */ + * @ORM\Column(type="integer", nullable=false, options={"default": 1}) + * + * @Serializer\Groups({"read", "accompanying_period_work:edit"}) + * + * @ORM\Version + */ private int $version = 1; public function __construct() diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/App.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/App.vue index dd4ddb664..e50b42c2c 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/App.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/App.vue @@ -599,7 +599,7 @@ export default { }, submit() { this.$store.dispatch('submit').catch((error) => { - if (error.name === 'ValidationException' || error.name === 'AccessException') { + if (error.name === 'ValidationException' || error.name === 'AccessException' || error.name === 'ConflictHttpException') { error.violations.forEach((violation) => this.$toast.open({message: violation})); } else { this.$toast.open({message: 'An error occurred'}); diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/store.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/store.js index 5175ba70c..bec89d088 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/store.js +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/store.js @@ -75,6 +75,7 @@ const store = createStore({ return { type: 'accompanying_period_work', id: state.work.id, + version: state.version, startDate: state.startDate === null || state.startDate === '' ? null : { datetime: datetimeToISO(ISOToDate(state.startDate)) }, @@ -505,19 +506,22 @@ const store = createStore({ url = `/api/1.0/person/accompanying-course/work/${state.work.id}.json`, errors = [] ; + commit('setIsPosting', true); - console.log('the social action', payload); + // console.log('the social action', payload); return makeFetch('PUT', url, payload) .then(data => { if (typeof(callback) !== 'undefined') { return callback(data); } else { - console.info('nothing to do here, bye bye'); + // console.log('payload', payload.privateComment) + // console.info('nothing to do here, bye bye'); window.location.assign(`/fr/person/accompanying-period/${state.work.accompanyingPeriod.id}/work`); } }).catch(error => { + console.log('error', error) commit('setIsPosting', false); throw error; }); diff --git a/src/Bundle/ChillPersonBundle/migrations/Version20231129113816.php b/src/Bundle/ChillPersonBundle/migrations/Version20231129113816.php index 51731db8a..20bc3b446 100644 --- a/src/Bundle/ChillPersonBundle/migrations/Version20231129113816.php +++ b/src/Bundle/ChillPersonBundle/migrations/Version20231129113816.php @@ -2,6 +2,13 @@ declare(strict_types=1); +/* + * Chill is a software for social workers + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + namespace Chill\Migrations\Person; use Doctrine\DBAL\Schema\Schema; diff --git a/src/Bundle/ChillPersonBundle/translations/validators.fr.yml b/src/Bundle/ChillPersonBundle/translations/validators.fr.yml index f005755b1..691fef833 100644 --- a/src/Bundle/ChillPersonBundle/translations/validators.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/validators.fr.yml @@ -21,6 +21,7 @@ The firstname cannot be empty: Le prénom ne peut pas être vide The lastname cannot be empty: Le nom de famille ne peut pas être vide The gender must be set: Le genre doit être renseigné You are not allowed to perform this action: Vous n'avez pas le droit de changer cette valeur. +Sorry, but someone else has already changed this entity. Please refresh the page and apply the changes again: Désolé, mais quelqu'un d'autre a déjà modifié cette entité. Veuillez actualiser la page et appliquer à nouveau les modifications A center is required: Un centre est requis