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
43 changes: 43 additions & 0 deletions webapp/public/js/multi-delete.js
Original file line number Diff line number Diff line change
@@ -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 = $('<a>', {
'href': url,
'data-ajax-modal': ''
}).hide().appendTo('body');

$tempLink.trigger('click');
$tempLink.remove();
});
}
66 changes: 66 additions & 0 deletions webapp/src/Controller/BaseController.php
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,39 @@ protected function deleteEntities(
return $this->render('jury/delete.html.twig', $data);
}

/**
* @template T of object
* @param class-string<T> $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<T> $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<string, array<array{'target': string, 'targetColumn': string, 'type': string}>> $relations
* @return string[]
Expand Down Expand Up @@ -486,6 +519,39 @@ protected function getDependentEntities(string $entityClass, array $relations):
return $result;
}

/**
* @param array<string, array<string, mixed>> $table_fields
*/
protected function addSelectAllCheckbox(array &$table_fields, string $title): void
{
if ($this->isGranted('ROLE_ADMIN')) {
$table_fields = array_merge(
['checkbox' => ['title' => sprintf('<input type="checkbox" class="select-all" title="Select all %s">', $title), 'sort' => false, 'search' => false, 'raw' => true]],
$table_fields
);
}
}

/**
* @param array<string, mixed> $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(
'<input type="checkbox" name="ids[]" value="%s" class="%s">',
$identifierValue,
$checkboxClass
)
];
}
}

/**
* Get the contests that an event for the given entity should be triggered on
*
Expand Down
63 changes: 10 additions & 53 deletions webapp/src/Controller/Jury/ProblemController.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,7 @@ public function indexAction(): Response
'type' => ['title' => 'type', 'sort' => true],
];

if ($this->isGranted('ROLE_ADMIN')) {
$table_fields = array_merge(
['checkbox' => ['title' => '<input type="checkbox" class="select-all" title="Select all problems">', 'sort' => false, 'search' => false, 'raw' => true]],
$table_fields
);
}
$this->addSelectAllCheckbox($table_fields, 'problems');

$contestCountData = $this->em->createQueryBuilder()
->from(ContestProblem::class, 'cp')
Expand All @@ -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(
'<input type="checkbox" name="ids[]" value="%s" class="problem-checkbox">',
$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) {
Expand Down Expand Up @@ -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')]
Expand Down
19 changes: 19 additions & 0 deletions webapp/src/Controller/Jury/TeamAffiliationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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];
Expand All @@ -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)) {
Expand Down Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions webapp/src/Controller/Jury/TeamCategoryController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -65,13 +66,18 @@ 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) {
/** @var TeamCategory $teamCategory */
$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)) {
Expand Down Expand Up @@ -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
{
Expand Down
19 changes: 19 additions & 0 deletions webapp/src/Controller/Jury/TeamController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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)) {
Expand Down Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions webapp/src/Controller/Jury/UserController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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];
}
Expand All @@ -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)) {
Expand Down Expand Up @@ -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()
);
}
}
Loading
Loading