mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
Add conflict resolution for generated API + add test
Implemented additional code to handle version conflicts when editing accompanying period work. By keeping track of the current version and returning an HTTP conflict response when it doesn't match with the provided entity version, users are properly alerted to update their entity before continuing. Furthermore, adjusted BadRequestHttpException to match correct arguments order and introduced entity version as query parameter for the URL. ensure kernel is shutdown after generating data
This commit is contained in:
parent
3bb911b4d0
commit
aa0cadfa84
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -15,23 +15,15 @@ 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,
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly TranslatorInterface $translator,
|
||||
) {}
|
||||
public function __construct(private readonly AccompanyingPeriodWorkRepository $accompanyingPeriodWorkRepository)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/api/1.0/person/accompanying-period/work/my-near-end")
|
||||
@ -75,18 +67,4 @@ 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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -503,7 +503,8 @@ const store = createStore({
|
||||
submit({ getters, state, commit }, callback) {
|
||||
let
|
||||
payload = getters.buildPayload,
|
||||
url = `/api/1.0/person/accompanying-course/work/${state.work.id}.json`,
|
||||
params = new URLSearchParams({'entity_version': state.version}),
|
||||
url = `/api/1.0/person/accompanying-course/work/${state.work.id}.json?${params}`,
|
||||
errors = []
|
||||
;
|
||||
|
||||
|
@ -0,0 +1,157 @@
|
||||
<?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 Tests\Controller\AccompanyingCoursWorkApiController;
|
||||
|
||||
use Chill\MainBundle\Repository\UserRepositoryInterface;
|
||||
use Chill\MainBundle\Test\PrepareClientTrait;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Entity\SocialWork\SocialAction;
|
||||
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class ConflictTest extends WebTestCase
|
||||
{
|
||||
use PrepareClientTrait;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::ensureKernelShutdown();
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
self::ensureKernelShutdown();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider generateAccompanyingPeriodWork
|
||||
*
|
||||
* @throws \JsonException
|
||||
*/
|
||||
public function testWhenEditionAccompanyingPeriodWorkWithCurrentVersionNoConflictOccurs(AccompanyingPeriod\AccompanyingPeriodWork $work, int $personId): void
|
||||
{
|
||||
$client = $this->getClientAuthenticated();
|
||||
|
||||
$currentVersion = $work->getVersion();
|
||||
|
||||
$client->request(
|
||||
'PUT',
|
||||
"/api/1.0/person/accompanying-course/work/{$work->getid()}.json?entity_version={$currentVersion}",
|
||||
content: json_encode([
|
||||
'type' => 'accompanying_period_work',
|
||||
'id' => $work->getId(),
|
||||
'startDate' => [
|
||||
'datetime' => '2023-12-15T00:00:00+01:00',
|
||||
],
|
||||
'endDate' => null,
|
||||
'note' => 'This is a note',
|
||||
'accompanyingPeriodWorkEvaluations' => [],
|
||||
'goals' => [],
|
||||
'handlingThirdParty' => null,
|
||||
'persons' => [[
|
||||
'type' => 'person',
|
||||
'id' => $personId,
|
||||
],
|
||||
],
|
||||
'privateComment' => '',
|
||||
'refferers' => [],
|
||||
'results' => [],
|
||||
'thirdParties' => [],
|
||||
], JSON_THROW_ON_ERROR)
|
||||
);
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
$w = json_decode($client->getResponse()->getContent(), true, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
self::assertEquals($work->getVersion() + 1, $w['version']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider generateAccompanyingPeriodWork
|
||||
*/
|
||||
public function testWhenEditingAccompanyingPeriodWorkWithPreviousVersionAnHttpConflictResponseOccurs(AccompanyingPeriod\AccompanyingPeriodWork $work, int $personId): void
|
||||
{
|
||||
$client = $this->getClientAuthenticated();
|
||||
|
||||
$previous = $work->getVersion() - 1;
|
||||
|
||||
$client->request(
|
||||
'PUT',
|
||||
"/api/1.0/person/accompanying-course/work/{$work->getid()}.json?entity_version={$previous}",
|
||||
content: json_encode([
|
||||
'type' => 'accompanying_period_work',
|
||||
'id' => $work->getId(),
|
||||
'startDate' => [
|
||||
'datetime' => '2023-12-15T00:00:00+01:00',
|
||||
],
|
||||
'endDate' => null,
|
||||
'note' => 'This is a note',
|
||||
'accompanyingPeriodWorkEvaluations' => [],
|
||||
'goals' => [],
|
||||
'handlingThirdParty' => null,
|
||||
'persons' => [[
|
||||
'type' => 'person',
|
||||
'id' => $personId,
|
||||
],
|
||||
],
|
||||
'privateComment' => '',
|
||||
'refferers' => [],
|
||||
'results' => [],
|
||||
'thirdParties' => [],
|
||||
], JSON_THROW_ON_ERROR)
|
||||
);
|
||||
|
||||
self::assertResponseStatusCodeSame(409);
|
||||
}
|
||||
|
||||
public function generateAccompanyingPeriodWork(): iterable
|
||||
{
|
||||
self::bootKernel();
|
||||
$em = self::$container->get(EntityManagerInterface::class);
|
||||
$userRepository = self::$container->get(UserRepositoryInterface::class);
|
||||
$user = $userRepository->findOneByUsernameOrEmail('center a_social');
|
||||
|
||||
$period = new AccompanyingPeriod();
|
||||
$em->persist($period);
|
||||
$period->addPerson(($p = new Person())->setFirstName('test')->setLastName('test')
|
||||
->setBirthdate(new \DateTime('1980-01-01'))->setGender(Person::BOTH_GENDER));
|
||||
$em->persist($p);
|
||||
$issue = (new SocialIssue())->setTitle(['fr' => 'test']);
|
||||
$em->persist($issue);
|
||||
$action = (new SocialAction())->setIssue($issue);
|
||||
$em->persist($action);
|
||||
|
||||
$work = new AccompanyingPeriod\AccompanyingPeriodWork();
|
||||
$work
|
||||
->setAccompanyingPeriod($period)
|
||||
->setStartDate(new \DateTimeImmutable())
|
||||
->addPerson($p)
|
||||
->setSocialAction($action)
|
||||
->setCreatedBy($user)
|
||||
->setUpdatedBy($user)
|
||||
;
|
||||
$em->persist($work);
|
||||
|
||||
$em->flush();
|
||||
|
||||
self::ensureKernelShutdown();
|
||||
|
||||
yield [$work, $p->getId()];
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user