Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Quality

on:
push:
branches: [ master, develop ]
branches: [ master ]
pull_request:
branches: [ master, develop ]
Comment thread
LukaMrt marked this conversation as resolved.

Expand Down
28 changes: 28 additions & 0 deletions backend/migrations/Version20260522120000.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

final class Version20260522120000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add person_link table for free-form links on a person profile';
}

public function up(Schema $schema): void
{
$this->addSql('CREATE TABLE person_link (id INT AUTO_INCREMENT NOT NULL, person_id INT NOT NULL, title VARCHAR(255) NOT NULL, url VARCHAR(2048) NOT NULL, INDEX IDX_person_link_person (person_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci`');
$this->addSql('ALTER TABLE person_link ADD CONSTRAINT FK_person_link_person FOREIGN KEY (person_id) REFERENCES person (id) ON DELETE CASCADE');
}

public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE person_link DROP FOREIGN KEY FK_person_link_person');
$this->addSql('DROP TABLE person_link');
}
}
35 changes: 35 additions & 0 deletions backend/migrations/Version20260522154347.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260522154347 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}

public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE person_link DROP FOREIGN KEY `FK_person_link_person`');
$this->addSql('ALTER TABLE person_link ADD CONSTRAINT FK_BC4A1DDA217BBB47 FOREIGN KEY (person_id) REFERENCES person (id)');
Comment thread
LukaMrt marked this conversation as resolved.
$this->addSql('ALTER TABLE person_link RENAME INDEX idx_person_link_person TO IDX_BC4A1DDA217BBB47');
}

public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE person_link DROP FOREIGN KEY FK_BC4A1DDA217BBB47');
$this->addSql('ALTER TABLE person_link ADD CONSTRAINT `FK_person_link_person` FOREIGN KEY (person_id) REFERENCES person (id) ON UPDATE NO ACTION ON DELETE CASCADE');
$this->addSql('ALTER TABLE person_link RENAME INDEX idx_bc4a1dda217bbb47 TO IDX_person_link_person');
}
}
2 changes: 2 additions & 0 deletions backend/src/Controller/Admin/DashboardController.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ public function configureMenuItems(): iterable
yield MenuItem::section('Demandes');
yield MenuItem::linkTo(ContactCrudController::class, 'Contacts', 'fa fa-envelope');
yield MenuItem::section('Outils');
$mergeUrl = $this->adminUrlGenerator->setRoute('admin_merge')->generateUrl();
yield MenuItem::linkToUrl('Fusionner référentiels', 'fa fa-code-merge', $mergeUrl);
yield MenuItem::linkToUrl('Test mail', 'fa fa-paper-plane', $this->adminUrlGenerator->setRoute('admin_test_mail')->generateUrl());
yield MenuItem::section();
yield MenuItem::linkToUrl('Retour au site', 'fa fa-arrow-left', '/');
Expand Down
121 changes: 121 additions & 0 deletions backend/src/Controller/Admin/MergeAdminController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?php

declare(strict_types=1);

namespace App\Controller\Admin;

use Symfony\Component\HttpFoundation\RedirectResponse;
use App\Entity\Person\Association;
use App\Entity\Person\Filiere;
use App\Entity\Person\Role;
use App\Entity\Person\School;
use App\Repository\Person\AssociationRepository;
use App\Repository\Person\FiliereRepository;
use App\Repository\Person\SchoolRepository;
use App\Service\MergeService;
use EasyCorp\Bundle\EasyAdminBundle\Router\AdminUrlGenerator;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;

#[IsGranted(Role::ADMIN->value)]
final class MergeAdminController extends AbstractController
{
public function __construct(
private readonly MergeService $mergeService,
private readonly FiliereRepository $filiereRepository,
private readonly AssociationRepository $associationRepository,
private readonly SchoolRepository $schoolRepository,
private readonly AdminUrlGenerator $adminUrlGenerator,
) {
}

#[Route('/admin/merge', name: 'admin_merge', methods: ['GET', 'POST'])]
public function index(Request $request): Response
{
$mergeUrl = $this->adminUrlGenerator->setRoute('admin_merge')->generateUrl();

if ($request->isMethod('POST')) {
return $this->handleMerge($request, $mergeUrl);
}

return $this->render('admin/merge.html.twig', [
'merge_url' => $mergeUrl,
'filieres' => $this->filiereRepository->findAllOrderedByName(),
'associations' => $this->associationRepository->findAllOrderedByName(),
'schools' => $this->schoolRepository->findAllOrderedByName(),
]);
}

private function handleMerge(Request $request, string $mergeUrl): RedirectResponse
{
if (!$this->isCsrfTokenValid('admin_merge', $request->request->getString('_token'))) {
$this->addFlash('danger', 'Token CSRF invalide.');
return $this->redirect($mergeUrl);
}

$type = $request->request->getString('type');
$sourceId = $request->request->getInt('source_id');
$targetId = $request->request->getInt('target_id');

if ($sourceId === $targetId) {
$this->addFlash('warning', 'La source et la cible sont identiques.');
return $this->redirect($mergeUrl);
}

try {
$count = match ($type) {
'filiere' => $this->mergeFiliere($sourceId, $targetId),
'association' => $this->mergeAssociation($sourceId, $targetId),
'school' => $this->mergeSchool($sourceId, $targetId),
default => throw new \InvalidArgumentException('Type inconnu : ' . $type),
};

$this->addFlash('success', sprintf('Fusion effectuée : %d enregistrement(s) réassigné(s).', $count));
} catch (\InvalidArgumentException $invalidArgumentException) {
$this->addFlash('danger', $invalidArgumentException->getMessage());
Comment thread
LukaMrt marked this conversation as resolved.
} catch (\Throwable) {
$this->addFlash('danger', 'Une erreur inattendue est survenue lors de la fusion.');
}

return $this->redirect($mergeUrl);
}

private function mergeFiliere(int $sourceId, int $targetId): int
{
$source = $this->filiereRepository->find($sourceId);
$target = $this->filiereRepository->find($targetId);

if (!$source instanceof Filiere || !$target instanceof Filiere) {
throw new \InvalidArgumentException('Filière source ou cible introuvable.');
}

return $this->mergeService->mergeFiliere($source, $target);
}

private function mergeAssociation(int $sourceId, int $targetId): int
{
$source = $this->associationRepository->find($sourceId);
$target = $this->associationRepository->find($targetId);

if (!$source instanceof Association || !$target instanceof Association) {
throw new \InvalidArgumentException('Association source ou cible introuvable.');
}

return $this->mergeService->mergeAssociation($source, $target);
}

private function mergeSchool(int $sourceId, int $targetId): int
{
$source = $this->schoolRepository->find($sourceId);
$target = $this->schoolRepository->find($targetId);

if (!$source instanceof School || !$target instanceof School) {
throw new \InvalidArgumentException('École source ou cible introuvable.');
}

return $this->mergeService->mergeSchool($source, $target);
}
}
36 changes: 36 additions & 0 deletions backend/src/Controller/Api/CharacteristicTypeApiController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace App\Controller\Api;

use App\Entity\Characteristic\CharacteristicType;
use App\Api\ApiResponse;
use App\Repository\CharacteristicTypeRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;

final class CharacteristicTypeApiController extends AbstractController
{
public function __construct(
private readonly CharacteristicTypeRepository $characteristicTypeRepository,
) {
}

#[Route('/api/characteristic-types', name: 'api_characteristic_types_list', methods: ['GET'])]
public function list(): JsonResponse
{
$types = $this->characteristicTypeRepository->getAll();

return ApiResponse::success(array_map(
static fn(CharacteristicType $type): array => [
'id' => $type->getId(),
'title' => $type->getTitle(),
'url' => $type->getUrl(),
'image' => $type->getImage(),
],
$types,
));
}
}
8 changes: 8 additions & 0 deletions backend/src/Controller/Api/PersonApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@ public function update(

$this->personService->syncFilieres($person, $dto->filieres ?? []);
$this->personService->syncAssociations($person, $dto->associations ?? []);
if ($dto->characteristics !== null) {
$this->personService->syncCharacteristics($person, $dto->characteristics);
}

if ($dto->links !== null) {
$this->personService->syncLinks($person, $dto->links);
}

$this->personService->update($person);

return ApiResponse::success($this->personService->mapToResponseDto($person));
Expand Down
16 changes: 16 additions & 0 deletions backend/src/Dto/Person/CharacteristicRequestDto.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace App\Dto\Person;

final readonly class CharacteristicRequestDto
{
public function __construct(
public ?int $id = null,
public ?int $typeId = null,
public ?string $value = null,
public bool $visible = false,
) {
}
}
15 changes: 15 additions & 0 deletions backend/src/Dto/Person/PersonLinkDto.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace App\Dto\Person;

final readonly class PersonLinkDto
{
public function __construct(
public int $id,
public string $title,
public string $url,
) {
}
}
21 changes: 21 additions & 0 deletions backend/src/Dto/Person/PersonLinkRequestDto.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace App\Dto\Person;

use Symfony\Component\Validator\Constraints as Assert;

final readonly class PersonLinkRequestDto
{
public function __construct(
#[Assert\NotBlank]
#[Assert\Length(max: 255)]
public string $title,
#[Assert\NotBlank]
#[Assert\Url]
#[Assert\Length(max: 2048)]
public string $url,
) {
}
}
10 changes: 10 additions & 0 deletions backend/src/Dto/Person/PersonRequestDto.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ public function __construct(
*/
#[Assert\Valid]
public ?array $associations = null,
/**
* @var CharacteristicRequestDto[]|null
*/
#[Assert\Valid]
public ?array $characteristics = null,
/**
* @var PersonLinkRequestDto[]|null
*/
#[Assert\Valid]
public ?array $links = null,
) {
}
}
2 changes: 2 additions & 0 deletions backend/src/Dto/Person/PersonResponseDto.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* @param CharacteristicDto[] $characteristics
* @param FiliereResponseDto[] $filieres
* @param AssociationResponseDto[] $associations
* @param PersonLinkDto[] $links
*/
public function __construct(
public int $id,
Expand All @@ -30,6 +31,7 @@ public function __construct(
public array $characteristics,
public array $filieres,
public array $associations,
public array $links = [],
) {
}
}
36 changes: 36 additions & 0 deletions backend/src/Entity/Person/Person.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@ class Person implements \Stringable
#[ORM\OneToMany(targetEntity: PersonAssociation::class, mappedBy: 'person', cascade: ['persist'], orphanRemoval: true)]
private Collection $associations;

/**
* @var Collection<int, PersonLink>
*/
#[ORM\OneToMany(targetEntity: PersonLink::class, mappedBy: 'person', cascade: ['persist'], orphanRemoval: true)]
private Collection $links;

public function __construct()
{
$this->godFathers = new ArrayCollection();
Expand All @@ -117,6 +123,7 @@ public function __construct()
$this->createdAt = new \DateTime();
$this->filieres = new ArrayCollection();
$this->associations = new ArrayCollection();
$this->links = new ArrayCollection();
}

public function getId(): int
Expand Down Expand Up @@ -463,4 +470,33 @@ public function replaceAssociations(array $personAssociations): void
$this->addAssociation($pa);
}
}

/**
* @return Collection<int, PersonLink>
*/
public function getLinks(): Collection
{
return $this->links;
}

public function addLink(PersonLink $link): static
{
if (!$this->links->contains($link)) {
$this->links->add($link);
$link->setPerson($this);
}

return $this;
}

/**
* @param PersonLink[] $links
*/
public function replaceLinks(array $links): void
{
$this->links->clear();
foreach ($links as $link) {
$this->addLink($link);
}
}
}
Loading
Loading