Add export generation creation from saved export endpoint

Introduce a new controller for creating export generations from saved exports using a POST endpoint. Updates include a new route, serialization groups, and a corresponding test to validate functionality.
This commit is contained in:
Julien Fastré 2025-03-16 22:47:57 +01:00
parent 6a2aa77ecc
commit b2d8d21f04
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
4 changed files with 174 additions and 1 deletions

View File

@ -0,0 +1,62 @@
<?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\MainBundle\Controller;
use Chill\MainBundle\Entity\ExportGeneration;
use Chill\MainBundle\Entity\SavedExport;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessage;
use Chill\MainBundle\Security\Authorization\SavedExportVoter;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\SerializerInterface;
class ExportGenerationCreateFromSavedExportController
{
public function __construct(
private Security $security,
private EntityManagerInterface $entityManager,
private MessageBusInterface $messageBus,
private ClockInterface $clock,
private SerializerInterface $serializer,
) {}
#[Route('/api/1.0/main/export/export-generation/create-from-saved-export/{id}', methods: ['POST'])]
public function __invoke(SavedExport $export): JsonResponse
{
if (!$this->security->isGranted(SavedExportVoter::GENERATE, $export)) {
throw new AccessDeniedHttpException('Not allowed to generate an export from this saved export');
}
$user = $this->security->getUser();
if (!$user instanceof User) {
throw new AccessDeniedHttpException('Only users can create exports');
}
$exportGeneration = ExportGeneration::fromSavedExport($export, $this->clock->now()->add(new \DateInterval('P6M')));
$this->entityManager->persist($exportGeneration);
$this->entityManager->flush();
$this->messageBus->dispatch(new ExportRequestGenerationMessage($exportGeneration, $user));
return new JsonResponse(
$this->serializer->serialize($exportGeneration, 'json', ['groups' => ['read']]),
json: true,
);
}
}

View File

@ -17,6 +17,7 @@ use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
use Doctrine\ORM\Mapping as ORM;
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;
use Symfony\Component\Serializer\Annotation as Serializer;
/**
* Contains the single execution of an export.
@ -29,21 +30,25 @@ use Ramsey\Uuid\UuidInterface;
*/
#[ORM\Entity()]
#[ORM\Table('chill_main_export_generation')]
#[Serializer\DiscriminatorMap('type', ['export_generation' => ExportGeneration::class])]
class ExportGeneration implements TrackCreationInterface
{
use TrackCreationTrait;
#[ORM\Id]
#[ORM\Column(type: 'uuid', unique: true)]
#[Serializer\Groups(['read'])]
private UuidInterface $id;
#[ORM\ManyToOne(targetEntity: StoredObject::class, cascade: ['persist', 'refresh'])]
#[ORM\JoinColumn(nullable: false)]
#[Serializer\Groups(['read'])]
private StoredObject $storedObject;
public function __construct(
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])]
#[Serializer\Groups(['read'])]
private string $exportAlias,
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]'])]
private array $options = [],
@ -103,6 +108,9 @@ class ExportGeneration implements TrackCreationInterface
public static function fromSavedExport(SavedExport $savedExport, ?\DateTimeImmutable $deletedAt = null): self
{
return new self($savedExport->getExportAlias(), $savedExport->getOptions(), $deletedAt, $savedExport);
$generation = new self($savedExport->getExportAlias(), $savedExport->getOptions(), $deletedAt, $savedExport);
$generation->getStoredObject()->setTitle($savedExport->getTitle());
return $generation;
}
}

View File

@ -0,0 +1,81 @@
<?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\MainBundle\Tests\Authorization;
use Chill\MainBundle\Controller\ExportGenerationCreateFromSavedExportController;
use Chill\MainBundle\Entity\ExportGeneration;
use Chill\MainBundle\Entity\SavedExport;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessage;
use Chill\MainBundle\Security\Authorization\SavedExportVoter;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\Clock\MockClock;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\SerializerInterface;
/**
* @internal
*
* @coversNothing
*/
class ExportGenerationCreateFromSavedExportControllerTest extends TestCase
{
use ProphecyTrait;
public function testInvoke(): void
{
$savedExport = new SavedExport();
$savedExport->setOptions($exportOptions = ['test' => 'content'])->setExportAlias('dummy_export_alias');
$user = new User();
$reflection = new \ReflectionClass($user);
$id = $reflection->getProperty('id');
$id->setValue($user, 1);
$security = $this->prophesize(Security::class);
$security->isGranted(SavedExportVoter::GENERATE, $savedExport)->shouldBeCalled()->willReturn(true);
$security->getUser()->willReturn($user);
$entityManager = $this->prophesize(EntityManagerInterface::class);
$entityManager->persist(Argument::that(
static fn ($arg) => $arg instanceof ExportGeneration && $arg->getOptions() === $exportOptions && $arg->getSavedExport() === $savedExport,
))->shouldBeCalled();
$entityManager->flush()->shouldBeCalled();
$messenger = $this->prophesize(MessageBusInterface::class);
$messenger->dispatch(Argument::type(ExportRequestGenerationMessage::class))->shouldBeCalled()->will(
static fn (array $args): Envelope => new Envelope($args[0]),
);
$serializer = $this->prophesize(SerializerInterface::class);
$serializer->serialize(Argument::type(ExportGeneration::class), 'json', ['groups' => ['read']])->shouldBeCalled()->willReturn('{"test": "export-generation"}');
$controller = new ExportGenerationCreateFromSavedExportController(
$security->reveal(),
$entityManager->reveal(),
$messenger->reveal(),
new MockClock(),
$serializer->reveal()
);
$response = $controller($savedExport);
self::assertInstanceOf(JsonResponse::class, $response);
self::assertEquals('{"test": "export-generation"}', $response->getContent());
}
}

View File

@ -1123,3 +1123,25 @@ paths:
application/json:
schema:
type: object
/1.0/main/export/export-generation/create-from-saved-export/{id}:
post:
tags:
- export
summary: Create an export generation from an existing saved export
parameters:
- name: id
in: path
required: true
description: The entity saved export's id
schema:
type: string
format: uuid
responses:
403:
description: Access denied
200:
description: "ok"
content:
application/json:
schema:
type: object