diff --git a/webapp/public/js/multi-delete.js b/webapp/public/js/multi-delete.js new file mode 100644 index 0000000000..aab3e6ea07 --- /dev/null +++ b/webapp/public/js/multi-delete.js @@ -0,0 +1,43 @@ +function initializeMultiDelete(options) { + var $deleteButton = $(options.buttonSelector); + var checkboxClass = options.checkboxClass; + var deleteUrl = options.deleteUrl; + + function toggleDeleteButton() { + var checkedCount = $('.' + checkboxClass + ':checked').length; + $deleteButton.prop('disabled', checkedCount === 0); + } + + $(document).on('change', '.' + checkboxClass, function() { + var table = $(this).closest('table'); + var $tableCheckboxes = table.find('.' + checkboxClass); + var $selectAllInTable = table.find('.select-all'); + $selectAllInTable.prop('checked', $tableCheckboxes.length > 0 && $tableCheckboxes.length === $tableCheckboxes.filter(':checked').length); + toggleDeleteButton(); + }); + + $(document).on('change', '.select-all', function() { + var table = $(this).closest('table'); + table.find('.' + checkboxClass).prop('checked', $(this).is(':checked')); + toggleDeleteButton(); + }); + + toggleDeleteButton(); + + $deleteButton.on('click', function () { + var ids = $('.' + checkboxClass + ':checked').map(function () { + return 'ids[]=' + $(this).val(); + }).get(); + if (ids.length === 0) return; + + var url = deleteUrl + '?' + ids.join('&'); + + var $tempLink = $('', { + 'href': url, + 'data-ajax-modal': '' + }).hide().appendTo('body'); + + $tempLink.trigger('click'); + $tempLink.remove(); + }); +} diff --git a/webapp/src/Controller/BaseController.php b/webapp/src/Controller/BaseController.php index 78eb67a4ca..00b17e4424 100644 --- a/webapp/src/Controller/BaseController.php +++ b/webapp/src/Controller/BaseController.php @@ -454,6 +454,39 @@ protected function deleteEntities( return $this->render('jury/delete.html.twig', $data); } + /** + * @template T of object + * @param class-string $entityClass + */ + protected function deleteMultiple( + Request $request, + string $entityClass, + string $idProperty, + string $redirectRoute, + string $warningMessage, + ?callable $filter = null + ): Response { + $ids = $request->query->all('ids'); + if (empty($ids)) { + throw new BadRequestHttpException('No IDs specified for deletion'); + } + + /** @var \Doctrine\ORM\EntityRepository $repository */ + $repository = $this->em->getRepository($entityClass); + $entities = $repository->findBy([$idProperty => $ids]); + + if ($filter) { + $entities = array_filter($entities, $filter); + } + + if (empty($entities)) { + $this->addFlash('warning', $warningMessage); + return $this->redirectToRoute($redirectRoute); + } + + return $this->deleteEntities($request, $entities, $this->generateUrl($redirectRoute)); + } + /** * @param array> $relations * @return string[] @@ -486,6 +519,39 @@ protected function getDependentEntities(string $entityClass, array $relations): return $result; } + /** + * @param array> $table_fields + */ + protected function addSelectAllCheckbox(array &$table_fields, string $title): void + { + if ($this->isGranted('ROLE_ADMIN')) { + $table_fields = array_merge( + ['checkbox' => ['title' => sprintf('', $title), 'sort' => false, 'search' => false, 'raw' => true]], + $table_fields + ); + } + } + + /** + * @param array $data + */ + protected function addEntityCheckbox(array &$data, object $entity, mixed $identifierValue, string $checkboxClass, ?callable $condition = null): void + { + if ($this->isGranted('ROLE_ADMIN')) { + if ($condition !== null && !$condition($entity)) { + $data['checkbox'] = ['value' => '']; + return; + } + $data['checkbox'] = [ + 'value' => sprintf( + '', + $identifierValue, + $checkboxClass + ) + ]; + } + } + /** * Get the contests that an event for the given entity should be triggered on * diff --git a/webapp/src/Controller/Jury/ProblemController.php b/webapp/src/Controller/Jury/ProblemController.php index 4cd07fee96..c3371b43dc 100644 --- a/webapp/src/Controller/Jury/ProblemController.php +++ b/webapp/src/Controller/Jury/ProblemController.php @@ -88,12 +88,7 @@ public function indexAction(): Response 'type' => ['title' => 'type', 'sort' => true], ]; - if ($this->isGranted('ROLE_ADMIN')) { - $table_fields = array_merge( - ['checkbox' => ['title' => '', 'sort' => false, 'search' => false, 'raw' => true]], - $table_fields - ); - } + $this->addSelectAllCheckbox($table_fields, 'problems'); $contestCountData = $this->em->createQueryBuilder() ->from(ContestProblem::class, 'cp') @@ -117,26 +112,7 @@ public function indexAction(): Response $problemdata = []; $problemactions = []; - if ($this->isGranted('ROLE_ADMIN')) { - $problemIsLocked = false; - foreach ($p->getContestProblems() as $contestProblem) { - if ($contestProblem->getContest()->isLocked()) { - $problemIsLocked = true; - break; - } - } - - if (!$problemIsLocked) { - $problemdata['checkbox'] = [ - 'value' => sprintf( - '', - $p->getProbid() - ) - ]; - } else { - $problemdata['checkbox'] = ['value' => '']; - } - } + $this->addEntityCheckbox($problemdata, $p, $p->getProbid(), 'problem-checkbox', fn(Problem $problem) => !$problem->isLocked()); // Get whatever fields we can from the problem object itself. foreach ($table_fields as $k => $v) { @@ -1032,33 +1008,14 @@ public function editAction(Request $request, int $probId): Response #[Route(path: '/delete-multiple', name: 'jury_problem_delete_multiple', methods: ['GET', 'POST'])] public function deleteMultipleAction(Request $request): Response { - $ids = $request->query->all('ids'); - if (empty($ids)) { - throw new BadRequestHttpException('No IDs specified for deletion'); - } - - $problems = $this->em->getRepository(Problem::class)->findBy(['probid' => $ids]); - - $deletableProblems = []; - foreach ($problems as $problem) { - $isLocked = false; - foreach ($problem->getContestProblems() as $contestProblem) { - if ($contestProblem->getContest()->isLocked()) { - $isLocked = true; - break; - } - } - if (!$isLocked) { - $deletableProblems[] = $problem; - } - } - - if (empty($deletableProblems)) { - $this->addFlash('warning', 'No problems could be deleted (they might be locked).'); - return $this->redirectToRoute('jury_problems'); - } - - return $this->deleteEntities($request, $deletableProblems, $this->generateUrl('jury_problems')); + return $this->deleteMultiple( + $request, + Problem::class, + 'probid', + 'jury_problems', + 'No problems could be deleted (they might be locked).', + fn(Problem $problem) => !$problem->isLocked() + ); } #[IsGranted('ROLE_ADMIN')] diff --git a/webapp/src/Controller/Jury/TeamAffiliationController.php b/webapp/src/Controller/Jury/TeamAffiliationController.php index 2042f718cb..ef0ab44ee5 100644 --- a/webapp/src/Controller/Jury/TeamAffiliationController.php +++ b/webapp/src/Controller/Jury/TeamAffiliationController.php @@ -14,6 +14,7 @@ use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\PropertyAccess\PropertyAccess; @@ -59,6 +60,8 @@ public function indexAction( 'name' => ['title' => 'name', 'sort' => true, 'default_sort' => true], ]; + $this->addSelectAllCheckbox($table_fields, 'affiliations'); + if ($showFlags) { $table_fields['country'] = ['title' => 'country', 'sort' => true]; $table_fields['affiliation_logo'] = ['title' => 'logo', 'sort' => false]; @@ -73,6 +76,9 @@ public function indexAction( $teamAffiliation = $teamAffiliationData[0]; $affiliationdata = []; $affiliationactions = []; + + $this->addEntityCheckbox($affiliationdata, $teamAffiliation, $teamAffiliation->getAffilid(), 'affiliation-checkbox'); + // Get whatever fields we can from the affiliation object itself. foreach ($table_fields as $k => $v) { if ($propertyAccessor->isReadable($teamAffiliation, $k)) { @@ -201,6 +207,19 @@ public function deleteAction(Request $request, int $affilId): Response return $this->deleteEntities($request, [$teamAffiliation], $this->generateUrl('jury_team_affiliations')); } + #[IsGranted('ROLE_ADMIN')] + #[Route(path: '/delete-multiple', name: 'jury_team_affiliation_delete_multiple', methods: ['GET', 'POST'])] + public function deleteMultipleAction(Request $request): Response + { + return $this->deleteMultiple( + $request, + TeamAffiliation::class, + 'affilid', + 'jury_team_affiliations', + 'No affiliations could be deleted.' + ); + } + #[IsGranted('ROLE_ADMIN')] #[Route(path: '/add', name: 'jury_team_affiliation_add')] public function addAction(Request $request): Response diff --git a/webapp/src/Controller/Jury/TeamCategoryController.php b/webapp/src/Controller/Jury/TeamCategoryController.php index dc56442b4a..19f845c94f 100644 --- a/webapp/src/Controller/Jury/TeamCategoryController.php +++ b/webapp/src/Controller/Jury/TeamCategoryController.php @@ -20,6 +20,7 @@ use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\PropertyAccess\PropertyAccess; @@ -65,6 +66,8 @@ public function indexAction(): Response 'allow_self_registration' => ['title' => 'self-registration', 'sort' => true], ]; + $this->addSelectAllCheckbox($table_fields, 'categories'); + $propertyAccessor = PropertyAccess::createPropertyAccessor(); $team_categories_table = []; foreach ($teamCategories as $teamCategoryData) { @@ -72,6 +75,9 @@ public function indexAction(): Response $teamCategory = $teamCategoryData[0]; $categorydata = []; $categoryactions = []; + + $this->addEntityCheckbox($categorydata, $teamCategory, $teamCategory->getCategoryid(), 'category-checkbox'); + // Get whatever fields we can from the category object itself. foreach ($table_fields as $k => $v) { if ($propertyAccessor->isReadable($teamCategory, $k)) { @@ -230,6 +236,19 @@ public function addAction(Request $request): Response ]); } + #[IsGranted('ROLE_ADMIN')] + #[Route(path: '/delete-multiple', name: 'jury_team_category_delete_multiple', methods: ['GET', 'POST'])] + public function deleteMultipleAction(Request $request): Response + { + return $this->deleteMultiple( + $request, + TeamCategory::class, + 'categoryid', + 'jury_team_categories', + 'No categories could be deleted.' + ); + } + #[Route(path: '/{categoryId<\d+>}/request-remaining', name: 'jury_team_category_request_remaining')] public function requestRemainingRunsWholeTeamCategoryAction(string $categoryId): RedirectResponse { diff --git a/webapp/src/Controller/Jury/TeamController.php b/webapp/src/Controller/Jury/TeamController.php index 87399655e9..fbc4f2485c 100644 --- a/webapp/src/Controller/Jury/TeamController.php +++ b/webapp/src/Controller/Jury/TeamController.php @@ -102,6 +102,8 @@ public function indexAction(): Response 'stats' => ['title' => 'stats', 'sort' => true,], ]; + $this->addSelectAllCheckbox($table_fields, 'teams'); + $userDataPerTeam = $this->em->createQueryBuilder() ->from(Team::class, 't', 't.teamid') ->leftJoin('t.users', 'u') @@ -115,6 +117,9 @@ public function indexAction(): Response foreach ($teams as $t) { $teamdata = []; $teamactions = []; + + $this->addEntityCheckbox($teamdata, $t, $t->getTeamid(), 'team-checkbox', fn(Team $team) => !$team->isLocked()); + // Get whatever fields we can from the team object itself. foreach ($table_fields as $k => $v) { if ($propertyAccessor->isReadable($t, $k)) { @@ -346,6 +351,20 @@ public function deleteAction(Request $request, int $teamId): Response return $this->deleteEntities($request, [$team], $this->generateUrl('jury_teams')); } + #[IsGranted('ROLE_ADMIN')] + #[Route(path: '/delete-multiple', name: 'jury_team_delete_multiple', methods: ['GET', 'POST'])] + public function deleteMultipleAction(Request $request): Response + { + return $this->deleteMultiple( + $request, + Team::class, + 'teamid', + 'jury_teams', + 'No teams could be deleted (they might be in a locked contest).', + fn(Team $team) => !$team->isLocked() + ); + } + #[IsGranted('ROLE_ADMIN')] #[Route(path: '/add', name: 'jury_team_add')] public function addAction(Request $request): Response diff --git a/webapp/src/Controller/Jury/UserController.php b/webapp/src/Controller/Jury/UserController.php index 498373a020..4a91099e16 100644 --- a/webapp/src/Controller/Jury/UserController.php +++ b/webapp/src/Controller/Jury/UserController.php @@ -22,6 +22,7 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\ResponseHeaderBag; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\PropertyAccess\PropertyAccess; @@ -70,6 +71,9 @@ public function indexAction(): Response 'teamid' => ['title' => '', 'sort' => false, 'render' => 'entity_id_badge'], 'team' => ['title' => 'team', 'sort' => true], ]; + + $this->addSelectAllCheckbox($table_fields, 'users'); + if (in_array('ipaddress', $this->config->get('auth_methods'))) { $table_fields['ip_address'] = ['title' => 'autologin IP', 'sort' => true]; } @@ -83,6 +87,9 @@ public function indexAction(): Response /** @var User $u */ $userdata = []; $useractions = []; + + $this->addEntityCheckbox($userdata, $u, $u->getUserid(), 'user-checkbox', fn(User $user) => $user->getUserid() !== $this->dj->getUser()->getUserid()); + // Get whatever fields we can from the user object itself. foreach ($table_fields as $k => $v) { if ($propertyAccessor->isReadable($u, $k)) { @@ -386,4 +393,18 @@ public function resetTeamLoginStatus(Request $request): Response $this->addFlash('success', 'Reset login status all ' . $count . ' users with the team role.'); return $this->redirectToRoute('jury_users'); } + + #[IsGranted('ROLE_ADMIN')] + #[Route(path: '/delete-multiple', name: 'jury_user_delete_multiple', methods: ['GET', 'POST'])] + public function deleteMultipleAction(Request $request): Response + { + return $this->deleteMultiple( + $request, + User::class, + 'userid', + 'jury_users', + 'No users could be deleted (you cannot delete your own account).', + fn(User $user) => $user->getUserid() !== $this->dj->getUser()->getUserid() + ); + } } diff --git a/webapp/src/Entity/Problem.php b/webapp/src/Entity/Problem.php index 1c47973a21..a715cd9fdc 100644 --- a/webapp/src/Entity/Problem.php +++ b/webapp/src/Entity/Problem.php @@ -682,4 +682,14 @@ public function removeLanguage(Language $language): Problem $this->languages->removeElement($language); return $this; } + + public function isLocked(): bool + { + foreach ($this->getContestProblems() as $contestProblem) { + if ($contestProblem->getContest()->isLocked()) { + return true; + } + } + return false; + } } diff --git a/webapp/src/Entity/Team.php b/webapp/src/Entity/Team.php index b08b485446..fd9d4e4ef2 100644 --- a/webapp/src/Entity/Team.php +++ b/webapp/src/Entity/Team.php @@ -661,4 +661,21 @@ public function getPhotoForApi(): array { return array_filter([$this->photoForApi]); } + + public function isLocked(): bool + { + foreach ($this->getContests() as $contest) { + if ($contest->isLocked()) { + return true; + } + } + if ($this->getCategory()) { + foreach ($this->getCategory()->getContests() as $contest) { + if ($contest->isLocked()) { + return true; + } + } + } + return false; + } } diff --git a/webapp/templates/jury/delete_modal.html.twig b/webapp/templates/jury/delete_modal.html.twig index eb13b79b40..924a216a7a 100644 --- a/webapp/templates/jury/delete_modal.html.twig +++ b/webapp/templates/jury/delete_modal.html.twig @@ -2,7 +2,7 @@ {% block title %} {% if count > 1 %} - Delete {{ count }} {{ type }}s + Delete {{ count }} {% if type|slice(-1) == 'y' %}{{ type|slice(0, -1) }}ies{% else %}{{ type }}s{% endif %} {% else %} Delete {{ type }} {{ primaryKey }} - "{{ description }}" {% endif %} @@ -16,7 +16,7 @@ {% else %} {% if count > 1 %} -

You're about to delete the following {{ count }} {{ type }}s:

+

You're about to delete the following {{ count }} {% if type|slice(-1) == 'y' %}{{ type|slice(0, -1) }}ies{% else %}{{ type }}s{% endif %}:

    {% for desc in description|split(',') %}
  • {{ desc|trim }}
  • @@ -46,4 +46,4 @@ {% if isError %}OK{% else %}Cancel{% endif %} {% endblock %} -{% block buttonText %}Delete{% endblock %} +{% block buttonText %}Delete{% endblock %} \ No newline at end of file diff --git a/webapp/templates/jury/partials/_delete_button.html.twig b/webapp/templates/jury/partials/_delete_button.html.twig new file mode 100644 index 0000000000..829786d404 --- /dev/null +++ b/webapp/templates/jury/partials/_delete_button.html.twig @@ -0,0 +1,3 @@ +{% if entities is not empty %} + +{% endif %} diff --git a/webapp/templates/jury/problems.html.twig b/webapp/templates/jury/problems.html.twig index 8e7d07db2e..7483d04485 100644 --- a/webapp/templates/jury/problems.html.twig +++ b/webapp/templates/jury/problems.html.twig @@ -27,9 +27,7 @@ {% if is_granted('ROLE_ADMIN') %}

    - {% if problems_current is not empty or problems_other is not empty %} - - {% endif %} + {% include 'jury/partials/_delete_button.html.twig' with {'entities': problems_current} %} {{ button(path('jury_problem_add'), 'Add new problem', 'primary', 'plus') }} {{ button(path('jury_import_export', {'_fragment':'problemarchive'}), 'Import problem', 'primary', 'upload') }}

    @@ -40,49 +38,13 @@ {% block extrafooter %} {{ parent() }} + diff --git a/webapp/templates/jury/team_affiliations.html.twig b/webapp/templates/jury/team_affiliations.html.twig index 0241c263b0..98db04cb4e 100644 --- a/webapp/templates/jury/team_affiliations.html.twig +++ b/webapp/templates/jury/team_affiliations.html.twig @@ -15,10 +15,24 @@ {{ macros.table(team_affiliations, table_fields) }} {%- if is_granted('ROLE_ADMIN') %} - -

    +

    + {% include 'jury/partials/_delete_button.html.twig' with {'entities': team_affiliations} %} {{ button(path('jury_team_affiliation_add'), 'Add new affiliation', 'primary', 'plus') }}

    {%- endif %} {% endblock %} + +{% block extrafooter %} + {{ parent() }} + + +{% endblock %} \ No newline at end of file diff --git a/webapp/templates/jury/team_categories.html.twig b/webapp/templates/jury/team_categories.html.twig index 91e7a95b96..db7bba96e7 100644 --- a/webapp/templates/jury/team_categories.html.twig +++ b/webapp/templates/jury/team_categories.html.twig @@ -15,9 +15,24 @@ {{ macros.table(team_categories, table_fields) }} {% if is_granted('ROLE_ADMIN') %} -

    +

    + {% include 'jury/partials/_delete_button.html.twig' with {'entities': team_categories} %} {{ button(path('jury_team_category_add'), 'Add new category', 'primary', 'plus') }}

    {% endif %} {% endblock %} + +{% block extrafooter %} + {{ parent() }} + + +{% endblock %} \ No newline at end of file diff --git a/webapp/templates/jury/teams.html.twig b/webapp/templates/jury/teams.html.twig index b2501cf750..1632b02fb0 100644 --- a/webapp/templates/jury/teams.html.twig +++ b/webapp/templates/jury/teams.html.twig @@ -15,10 +15,24 @@ {{ macros.table(teams, table_fields) }} {%- if is_granted('ROLE_ADMIN') %} - -

    +

    + {% include 'jury/partials/_delete_button.html.twig' with {'entities': teams} %} {{ button(path('jury_team_add'), 'Add new team', 'primary', 'plus') }} {{ button(path('jury_import_export', {'_fragment':'teams'}), 'Import teams', 'primary', 'upload') }}

    {%- endif %} {% endblock %} + +{% block extrafooter %} + {{ parent() }} + + +{% endblock %} \ No newline at end of file diff --git a/webapp/templates/jury/users.html.twig b/webapp/templates/jury/users.html.twig index e5e291843d..9de27bd207 100644 --- a/webapp/templates/jury/users.html.twig +++ b/webapp/templates/jury/users.html.twig @@ -15,9 +15,24 @@ {{ macros.table(users, table_fields) }} {% if is_granted('ROLE_ADMIN') %} -

    +

    + {% include 'jury/partials/_delete_button.html.twig' with {'entities': users} %} {{ button(path('jury_user_add'), 'Add new user', 'primary', 'plus') }} {{ button(path('jury_reset_login_status'), 'Reset login status of all team users', 'secondary', 'refresh') }}

    {% endif %} {% endblock %} + +{% block extrafooter %} + {{ parent() }} + + +{% endblock %} diff --git a/webapp/tests/Unit/Controller/Jury/ProblemControllerTest.php b/webapp/tests/Unit/Controller/Jury/ProblemControllerTest.php index 2611549ded..410dc4f859 100644 --- a/webapp/tests/Unit/Controller/Jury/ProblemControllerTest.php +++ b/webapp/tests/Unit/Controller/Jury/ProblemControllerTest.php @@ -6,6 +6,7 @@ use App\Entity\Contest; use App\Entity\Problem; use App\Entity\ProblemAttachment; +use App\Entity\ContestProblem; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\DomCrawler\Crawler; @@ -114,4 +115,84 @@ public function testLockedContest(): void ]; self::assertTrue(array_intersect($titles, $unexpectedTitles) == []); } + + public function testMultiDeleteProblems(): void + { + $this->roles = ['admin']; + $this->logOut(); + $this->logIn(); + + /** @var EntityManagerInterface $em */ + $em = static::getContainer()->get(EntityManagerInterface::class); + + // Get a contest to associate problems with + $contest = $em->getRepository(Contest::class)->findOneBy(['shortname' => 'demo']); + self::assertNotNull($contest, 'Demo contest not found.'); + + // Create some problems to delete + $problemsData = [ + ['name' => 'Problem 1 for multi-delete', 'externalid' => 'prob1md', 'shortname' => 'MDA'], + ['name' => 'Problem 2 for multi-delete', 'externalid' => 'prob2md', 'shortname' => 'MDB'], + ['name' => 'Problem 3 for multi-delete', 'externalid' => 'prob3md', 'shortname' => 'MDC'], + ]; + + $problemIds = []; + $createdProblems = []; + + foreach ($problemsData as $index => $data) { + $problem = new Problem(); + $problem->setName($data['name']); + $problem->setExternalid($data['externalid']); + $em->persist($problem); + + $contestProblem = new ContestProblem(); + $contestProblem->setProblem($problem); + $contestProblem->setContest($contest); + $contestProblem->setShortname($data['shortname']); + $em->persist($contestProblem); + + $createdProblems[] = $problem; + } + + $em->flush(); + + // Get the IDs of the newly created problems + foreach ($createdProblems as $problem) { + $problemIds[] = $problem->getProbid(); + } + + $problem1Id = $problemIds[0]; + $problem2Id = $problemIds[1]; + $problem3Id = $problemIds[2]; + + // Verify problems exist before deletion + $this->verifyPageResponse('GET', static::$baseUrl, 200); + foreach ([1, 2, 3] as $i) { + self::assertSelectorExists('body:contains("Problem ' . $i . ' for multi-delete")'); + } + + // Simulate multi-delete POST request + $this->client->request( + 'POST', + static::getContainer()->get('router')->generate('jury_problem_delete_multiple', ['ids' => [$problem1Id, $problem2Id]]), + [ + 'submit' => 'delete' // Assuming a submit button with name 'submit' and value 'delete' + ] + ); + + $this->checkStatusAndFollowRedirect(); + + // Verify problems are deleted + $this->verifyPageResponse('GET', static::$baseUrl, 200); + self::assertSelectorNotExists('body:contains("Problem 1 for multi-delete")'); + self::assertSelectorNotExists('body:contains("Problem 2 for multi-delete")'); + // Problem 3 should still exist + self::assertSelectorExists('body:contains("Problem 3 for multi-delete")'); + + // Verify problem 3 can still be deleted individually + $this->verifyPageResponse('GET', static::$baseUrl . '/' . $problem3Id . static::$delete, 200); + $this->client->submitForm('Delete', []); + $this->checkStatusAndFollowRedirect(); + $this->verifyPageResponse('GET', static::$baseUrl, 200); + } } diff --git a/webapp/tests/Unit/Controller/Jury/TeamAffiliationControllerTest.php b/webapp/tests/Unit/Controller/Jury/TeamAffiliationControllerTest.php index 7903adf473..0db62b576c 100644 --- a/webapp/tests/Unit/Controller/Jury/TeamAffiliationControllerTest.php +++ b/webapp/tests/Unit/Controller/Jury/TeamAffiliationControllerTest.php @@ -4,6 +4,7 @@ use App\DataFixtures\Test\SampleAffiliationsFixture; use App\Entity\TeamAffiliation; +use Doctrine\ORM\EntityManagerInterface; use App\Service\ConfigurationService; class TeamAffiliationControllerTest extends JuryControllerTestCase @@ -49,6 +50,76 @@ class TeamAffiliationControllerTest extends JuryControllerTestCase ['icpcid' => '|viol']], 'Only letters, numbers, dashes, underscores and dots are allowed.' => [['externalid' => '()']]]; + public function testMultiDeleteTeamAffiliations(): void + { + $this->roles = ['admin']; + $this->logOut(); + $this->logIn(); + + /** @var EntityManagerInterface $em */ + $em = static::getContainer()->get(EntityManagerInterface::class); + + // Create some team affiliations to delete + $affiliationsData = [ + ['name' => 'Affiliation 1 for multi-delete', 'shortname' => 'affil1md'], + ['name' => 'Affiliation 2 for multi-delete', 'shortname' => 'affil2md'], + ['name' => 'Affiliation 3 for multi-delete', 'shortname' => 'affil3md'], + ]; + + $affiliationIds = []; + $createdAffiliations = []; + + foreach ($affiliationsData as $data) { + $affiliation = new TeamAffiliation(); + $affiliation + ->setName($data['name']) + ->setShortname($data['shortname']); + $em->persist($affiliation); + $createdAffiliations[] = $affiliation; + } + + $em->flush(); + + // Get the IDs of the newly created affiliations + foreach ($createdAffiliations as $affiliation) { + $affiliationIds[] = $affiliation->getAffilid(); + } + + $affiliation1Id = $affiliationIds[0]; + $affiliation2Id = $affiliationIds[1]; + $affiliation3Id = $affiliationIds[2]; + + // Verify affiliations exist before deletion + $this->verifyPageResponse('GET', static::$baseUrl, 200); + foreach ([1, 2, 3] as $i) { + self::assertSelectorExists(sprintf('body:contains("Affiliation %d for multi-delete")', $i)); + } + + // Simulate multi-delete POST request + $this->client->request( + 'POST', + static::getContainer()->get('router')->generate('jury_team_affiliation_delete_multiple', ['ids' => [$affiliation1Id, $affiliation2Id]]), + [ + 'submit' => 'delete' + ] + ); + + $this->checkStatusAndFollowRedirect(); + + // Verify affiliations are deleted + $this->verifyPageResponse('GET', static::$baseUrl, 200); + self::assertSelectorNotExists('body:contains("Affiliation 1 for multi-delete")'); + self::assertSelectorNotExists('body:contains("Affiliation 2 for multi-delete")'); + // Affiliation 3 should still exist + self::assertSelectorExists('body:contains("Affiliation 3 for multi-delete")'); + + // Verify affiliation 3 can still be deleted individually + $this->verifyPageResponse('GET', static::$baseUrl . '/' . $affiliation3Id . static::$delete, 200); + $this->client->submitForm('Delete', []); + $this->checkStatusAndFollowRedirect(); + $this->verifyPageResponse('GET', static::$baseUrl, 200); + } + protected function helperProvideTranslateAddEntity(array $entity, array $expected): array { $config = static::getContainer()->get(ConfigurationService::class); diff --git a/webapp/tests/Unit/Controller/Jury/TeamCategoryControllerTest.php b/webapp/tests/Unit/Controller/Jury/TeamCategoryControllerTest.php index b2478852c9..95ba0ce4a4 100644 --- a/webapp/tests/Unit/Controller/Jury/TeamCategoryControllerTest.php +++ b/webapp/tests/Unit/Controller/Jury/TeamCategoryControllerTest.php @@ -3,6 +3,7 @@ namespace App\Tests\Unit\Controller\Jury; use App\Entity\TeamCategory; +use Doctrine\ORM\EntityManagerInterface; class TeamCategoryControllerTest extends JuryControllerTestCase { @@ -55,6 +56,74 @@ class TeamCategoryControllerTest extends JuryControllerTestCase 'Only letters, numbers, dashes, underscores and dots are allowed.' => [['externalid' => 'yes|']], 'This value should not be blank.' => [['name' => '']]]; + public function testMultiDeleteTeamCategories(): void + { + $this->roles = ['admin']; + $this->logOut(); + $this->logIn(); + + /** @var EntityManagerInterface $em */ + $em = static::getContainer()->get(EntityManagerInterface::class); + + // Create some team categories to delete + $categoriesData = [ + ['name' => 'Category 1 for multi-delete'], + ['name' => 'Category 2 for multi-delete'], + ['name' => 'Category 3 for multi-delete'], + ]; + + $categoryIds = []; + $createdCategories = []; + + foreach ($categoriesData as $data) { + $category = new TeamCategory(); + $category->setName($data['name']); + $em->persist($category); + $createdCategories[] = $category; + } + + $em->flush(); + + // Get the IDs of the newly created categories + foreach ($createdCategories as $category) { + $categoryIds[] = $category->getCategoryid(); + } + + $category1Id = $categoryIds[0]; + $category2Id = $categoryIds[1]; + $category3Id = $categoryIds[2]; + + // Verify categories exist before deletion + $this->verifyPageResponse('GET', static::$baseUrl, 200); + foreach ([1, 2, 3] as $i) { + self::assertSelectorExists(sprintf('body:contains("Category %d for multi-delete")', $i)); + } + + // Simulate multi-delete POST request + $this->client->request( + 'POST', + static::getContainer()->get('router')->generate('jury_team_category_delete_multiple', ['ids' => [$category1Id, $category2Id]]), + [ + 'submit' => 'delete' + ] + ); + + $this->checkStatusAndFollowRedirect(); + + // Verify categories are deleted + $this->verifyPageResponse('GET', static::$baseUrl, 200); + self::assertSelectorNotExists('body:contains("Category 1 for multi-delete")'); + self::assertSelectorNotExists('body:contains("Category 2 for multi-delete")'); + // Category 3 should still exist + self::assertSelectorExists('body:contains("Category 3 for multi-delete")'); + + // Verify category 3 can still be deleted individually + $this->verifyPageResponse('GET', static::$baseUrl . '/' . $category3Id . static::$delete, 200); + $this->client->submitForm('Delete', []); + $this->checkStatusAndFollowRedirect(); + $this->verifyPageResponse('GET', static::$baseUrl, 200); + } + protected function helperProvideTranslateAddEntity(array $entity, array $expected): array { return [$entity, $expected]; diff --git a/webapp/tests/Unit/Controller/Jury/TeamControllerTest.php b/webapp/tests/Unit/Controller/Jury/TeamControllerTest.php index fb914e5586..54d6ec5696 100644 --- a/webapp/tests/Unit/Controller/Jury/TeamControllerTest.php +++ b/webapp/tests/Unit/Controller/Jury/TeamControllerTest.php @@ -94,6 +94,74 @@ class TeamControllerTest extends JuryControllerTestCase ['icpcid' => '&viol', 'name' => 'icpcid violation-2']], 'This value should not be blank.' => [['name' => '', 'displayName' => 'Teams should have a name']]]; + public function testMultiDeleteTeams(): void + { + $this->roles = ['admin']; + $this->logOut(); + $this->logIn(); + + /** @var EntityManagerInterface $em */ + $em = static::getContainer()->get(EntityManagerInterface::class); + + // Create some teams to delete + $teamsData = [ + ['name' => 'Team 1 for multi-delete'], + ['name' => 'Team 2 for multi-delete'], + ['name' => 'Team 3 for multi-delete'], + ]; + + $teamIds = []; + $createdTeams = []; + + foreach ($teamsData as $data) { + $team = new Team(); + $team->setName($data['name']); + $em->persist($team); + $createdTeams[] = $team; + } + + $em->flush(); + + // Get the IDs of the newly created teams + foreach ($createdTeams as $team) { + $teamIds[] = $team->getTeamid(); + } + + $team1Id = $teamIds[0]; + $team2Id = $teamIds[1]; + $team3Id = $teamIds[2]; + + // Verify teams exist before deletion + $this->verifyPageResponse('GET', static::$baseUrl, 200); + foreach ([1, 2, 3] as $i) { + self::assertSelectorExists(sprintf('body:contains("Team %d for multi-delete")', $i)); + } + + // Simulate multi-delete POST request + $this->client->request( + 'POST', + static::getContainer()->get('router')->generate('jury_team_delete_multiple', ['ids' => [$team1Id, $team2Id]]), + [ + 'submit' => 'delete' + ] + ); + + $this->checkStatusAndFollowRedirect(); + + // Verify teams are deleted + $this->verifyPageResponse('GET', static::$baseUrl, 200); + self::assertSelectorNotExists('body:contains("Team 1 for multi-delete")'); + self::assertSelectorNotExists('body:contains("Team 2 for multi-delete")'); + // Team 3 should still exist + self::assertSelectorExists('body:contains("Team 3 for multi-delete")'); + + // Verify team 3 can still be deleted individually + $this->verifyPageResponse('GET', static::$baseUrl . '/' . $team3Id . static::$delete, 200); + $this->client->submitForm('Delete', []); + $this->checkStatusAndFollowRedirect(); + $this->verifyPageResponse('GET', static::$baseUrl, 200); + } + /** * Test that adding a team without a user and then editing it to add a user works. */ diff --git a/webapp/tests/Unit/Controller/Jury/UserControllerTest.php b/webapp/tests/Unit/Controller/Jury/UserControllerTest.php index 5fcead413c..1843de2e25 100644 --- a/webapp/tests/Unit/Controller/Jury/UserControllerTest.php +++ b/webapp/tests/Unit/Controller/Jury/UserControllerTest.php @@ -3,6 +3,7 @@ namespace App\Tests\Unit\Controller\Jury; use App\Entity\User; +use Doctrine\ORM\EntityManagerInterface; class UserControllerTest extends JuryControllerTestCase { @@ -89,4 +90,75 @@ class UserControllerTest extends JuryControllerTestCase ['ipAddress' => '1.1.1.256'], ['ipAddress' => '1.1.1.1.1'], ['ipAddress' => '::g']]]; + + public function testMultiDeleteUsers(): void + { + $this->roles = ['admin']; + $this->logOut(); + $this->logIn(); + + /** @var EntityManagerInterface $em */ + $em = static::getContainer()->get(EntityManagerInterface::class); + + // Create some users to delete + $usersData = [ + ['name' => 'User 1 for multi-delete', 'username' => 'user1md'], + ['name' => 'User 2 for multi-delete', 'username' => 'user2md'], + ['name' => 'User 3 for multi-delete', 'username' => 'user3md'], + ]; + + $userIds = []; + $createdUsers = []; + + foreach ($usersData as $data) { + $user = new User(); + $user + ->setName($data['name']) + ->setUsername($data['username']) + ->setPlainPassword('password'); + $em->persist($user); + $createdUsers[] = $user; + } + + $em->flush(); + + // Get the IDs of the newly created users + foreach ($createdUsers as $user) { + $userIds[] = $user->getUserid(); + } + + $user1Id = $userIds[0]; + $user2Id = $userIds[1]; + $user3Id = $userIds[2]; + + // Verify users exist before deletion + $this->verifyPageResponse('GET', static::$baseUrl, 200); + foreach ([1, 2, 3] as $i) { + self::assertSelectorExists(sprintf('body:contains("User %d for multi-delete")', $i)); + } + + // Simulate multi-delete POST request + $this->client->request( + 'POST', + static::getContainer()->get('router')->generate('jury_user_delete_multiple', ['ids' => [$user1Id, $user2Id]]), + [ + 'submit' => 'delete' + ] + ); + + $this->checkStatusAndFollowRedirect(); + + // Verify users are deleted + $this->verifyPageResponse('GET', static::$baseUrl, 200); + self::assertSelectorNotExists('body:contains("User 1 for multi-delete")'); + self::assertSelectorNotExists('body:contains("User 2 for multi-delete")'); + // User 3 should still exist + self::assertSelectorExists('body:contains("User 3 for multi-delete")'); + + // Verify user 3 can still be deleted individually + $this->verifyPageResponse('GET', static::$baseUrl . '/' . $user3Id . static::$delete, 200); + $this->client->submitForm('Delete', []); + $this->checkStatusAndFollowRedirect(); + $this->verifyPageResponse('GET', static::$baseUrl, 200); + } }