diff --git a/.changes/unreleased/Feature-20240208-123318.yaml b/.changes/unreleased/Feature-20240208-123318.yaml
new file mode 100644
index 000000000..b883306e8
--- /dev/null
+++ b/.changes/unreleased/Feature-20240208-123318.yaml
@@ -0,0 +1,6 @@
+kind: Feature
+body: Prevent social work to be saved when another user edited conccurently the social
+ work
+time: 2024-02-08T12:33:18.319442398+01:00
+custom:
+ Issue: "115"
diff --git a/src/Bundle/ChillMainBundle/CRUD/Controller/AbstractCRUDController.php b/src/Bundle/ChillMainBundle/CRUD/Controller/AbstractCRUDController.php
index f3982d0d5..9ee0d9e19 100644
--- a/src/Bundle/ChillMainBundle/CRUD/Controller/AbstractCRUDController.php
+++ b/src/Bundle/ChillMainBundle/CRUD/Controller/AbstractCRUDController.php
@@ -15,10 +15,14 @@ use Chill\MainBundle\CRUD\Resolver\Resolver;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Pagination\PaginatorInterface;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
+use Doctrine\DBAL\LockMode;
+use Doctrine\ORM\EntityManagerInterface;
+use Doctrine\ORM\OptimisticLockException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -173,6 +177,21 @@ abstract class AbstractCRUDController extends AbstractController
if (null === $e) {
throw $this->createNotFoundException(sprintf('The object %s for id %s is not found', $this->getEntityClass(), $id));
}
+ if ($request->query->has('entity_version')) {
+ $expectedVersion = $request->query->getInt('entity_version');
+
+ try {
+ $manager = $this->getDoctrine()->getManagerForClass($this->getEntityClass());
+
+ if ($manager instanceof EntityManagerInterface) {
+ $manager->lock($e, LockMode::OPTIMISTIC, $expectedVersion);
+ } else {
+ throw new \LogicException('This manager does not allow locking.');
+ }
+ } catch (OptimisticLockException $e) {
+ throw new ConflictHttpException('Sorry, but someone else has already changed this entity. Please refresh the page and apply the changes again', $e);
+ }
+ }
return $e;
}
diff --git a/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php b/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php
index 78bdc96d5..a6675424e 100644
--- a/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php
+++ b/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php
@@ -135,7 +135,7 @@ class ApiController extends AbstractCRUDController
try {
$entity = $this->deserialize($action, $request, $_format, $entity);
} catch (NotEncodableValueException $e) {
- throw new BadRequestHttpException('invalid json', 400, $e);
+ throw new BadRequestHttpException('invalid json', $e, 400);
}
$errors = $this->validate($action, $request, $_format, $entity);
@@ -153,7 +153,7 @@ class ApiController extends AbstractCRUDController
return $response;
}
- $this->getDoctrine()->getManager()->flush();
+ $this->getDoctrine()->getManagerForClass($this->getEntityClass())->flush();
$response = $this->onAfterFlush($action, $request, $_format, $entity, $errors);
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