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
This commit is contained in:
Julie Lenaerts 2023-11-29 16:14:19 +01:00 committed by Julien Fastré
parent f00b39980c
commit 3bb911b4d0
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
7 changed files with 70 additions and 11 deletions

View File

@ -55,11 +55,20 @@ export interface ServerExceptionInterface extends TransportExceptionInterface {
body: string; body: string;
} }
export interface ConflictHttpExceptionInterface extends TransportExceptionInterface {
name: 'ConflictHttpException';
violations: string[];
}
/** /**
* Generic api method that can be adapted to any fetch request * Generic api method that can be adapted to any fetch request
*/ */
export const makeFetch = <Input, Output>(method: 'POST'|'GET'|'PUT'|'PATCH'|'DELETE', url: string, body?: body | Input | null, options?: FetchParams): Promise<Output> => { export const makeFetch = <Input, Output>(
method: 'POST'|'GET'|'PUT'|'PATCH'|'DELETE',
url: string, body?: body | Input | null,
options?: FetchParams
): Promise<Output> => {
let opts = { let opts = {
method: method, method: method,
headers: { headers: {
@ -67,6 +76,7 @@ export const makeFetch = <Input, Output>(method: 'POST'|'GET'|'PUT'|'PATCH'|'DEL
}, },
}; };
if (body !== null && typeof body !== 'undefined') { if (body !== null && typeof body !== 'undefined') {
Object.assign(opts, {body: JSON.stringify(body)}) Object.assign(opts, {body: JSON.stringify(body)})
} }
@ -90,6 +100,10 @@ export const makeFetch = <Input, Output>(method: 'POST'|'GET'|'PUT'|'PATCH'|'DEL
throw AccessException(response); throw AccessException(response);
} }
if (response.status === 409) {
throw ConflictHttpException(response);
}
throw { throw {
name: 'Exception', name: 'Exception',
sta: response.status, sta: response.status,
@ -220,3 +234,12 @@ const ServerException = (code: number, body: string): ServerExceptionInterface =
return error; 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;
}

View File

@ -15,15 +15,23 @@ use Chill\MainBundle\CRUD\Controller\ApiController;
use Chill\MainBundle\Serializer\Model\Collection; use Chill\MainBundle\Serializer\Model\Collection;
use Chill\MainBundle\Serializer\Model\Counter; use Chill\MainBundle\Serializer\Model\Counter;
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepository; 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\JsonResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
class AccompanyingCourseWorkApiController extends ApiController 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") * @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); 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'));
}
}
} }

View File

@ -245,10 +245,12 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
private ?User $updatedBy = null; private ?User $updatedBy = null;
/** /**
* @ORM\Column(type="integer", nullable=false, options={"default": 1}) * @ORM\Column(type="integer", nullable=false, options={"default": 1})
* *
* @Serializer\Groups({"read", "accompanying_period_work:edit"}) * @Serializer\Groups({"read", "accompanying_period_work:edit"})
*/ *
* @ORM\Version
*/
private int $version = 1; private int $version = 1;
public function __construct() public function __construct()

View File

@ -599,7 +599,7 @@ export default {
}, },
submit() { submit() {
this.$store.dispatch('submit').catch((error) => { 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})); error.violations.forEach((violation) => this.$toast.open({message: violation}));
} else { } else {
this.$toast.open({message: 'An error occurred'}); this.$toast.open({message: 'An error occurred'});

View File

@ -75,6 +75,7 @@ const store = createStore({
return { return {
type: 'accompanying_period_work', type: 'accompanying_period_work',
id: state.work.id, id: state.work.id,
version: state.version,
startDate: state.startDate === null || state.startDate === '' ? null : { startDate: state.startDate === null || state.startDate === '' ? null : {
datetime: datetimeToISO(ISOToDate(state.startDate)) datetime: datetimeToISO(ISOToDate(state.startDate))
}, },
@ -505,19 +506,22 @@ const store = createStore({
url = `/api/1.0/person/accompanying-course/work/${state.work.id}.json`, url = `/api/1.0/person/accompanying-course/work/${state.work.id}.json`,
errors = [] errors = []
; ;
commit('setIsPosting', true); commit('setIsPosting', true);
console.log('the social action', payload); // console.log('the social action', payload);
return makeFetch('PUT', url, payload) return makeFetch('PUT', url, payload)
.then(data => { .then(data => {
if (typeof(callback) !== 'undefined') { if (typeof(callback) !== 'undefined') {
return callback(data); return callback(data);
} else { } 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`); window.location.assign(`/fr/person/accompanying-period/${state.work.accompanyingPeriod.id}/work`);
} }
}).catch(error => { }).catch(error => {
console.log('error', error)
commit('setIsPosting', false); commit('setIsPosting', false);
throw error; throw error;
}); });

View File

@ -2,6 +2,13 @@
declare(strict_types=1); 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; namespace Chill\Migrations\Person;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;

View File

@ -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 lastname cannot be empty: Le nom de famille ne peut pas être vide
The gender must be set: Le genre doit être renseigné 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. 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 A center is required: Un centre est requis