-
Notifications
You must be signed in to change notification settings - Fork 1
feat: merge tool for filières, associations and schools in admin #31
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
c8c5660
feat: directory filters, editable links and free links on profiles
LukaMrt 6dc65ce
fix: align filter dropdown items to the left
LukaMrt 0a81fd9
feat: add merge tool for filières, associations and schools in admin
LukaMrt 6d44f18
Fix e2e tests
LukaMrt 42645be
fix: address Copilot review feedback on PR #31
LukaMrt 4563011
fix: simplify e2e filter reset by reusing open dropdown
LukaMrt File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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'); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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)'); | ||
|
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'); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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()); | ||
|
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
36
backend/src/Controller/Api/CharacteristicTypeApiController.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | ||
| )); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | ||
| ) { | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | ||
| ) { | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | ||
| ) { | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.