Skip to content

Commit 962f109

Browse files
committed
Add multi-delete for users.
Also generalize multi-delete code where it makes sense. More progress towards #229.
1 parent 2222a86 commit 962f109

File tree

5 files changed

+118
-45
lines changed

5 files changed

+118
-45
lines changed

webapp/public/js/multi-delete.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
function initializeMultiDelete(options) {
2+
var $deleteButton = $(options.buttonSelector);
3+
var checkboxClass = options.checkboxClass;
4+
var deleteUrl = options.deleteUrl;
5+
6+
function toggleDeleteButton() {
7+
var checkedCount = $('.' + checkboxClass + ':checked').length;
8+
$deleteButton.prop('disabled', checkedCount === 0);
9+
}
10+
11+
$(document).on('change', '.' + checkboxClass, function() {
12+
var table = $(this).closest('table');
13+
var $tableCheckboxes = table.find('.' + checkboxClass);
14+
var $selectAllInTable = table.find('.select-all');
15+
$selectAllInTable.prop('checked', $tableCheckboxes.length > 0 && $tableCheckboxes.length === $tableCheckboxes.filter(':checked').length);
16+
toggleDeleteButton();
17+
});
18+
19+
$(document).on('change', '.select-all', function() {
20+
var table = $(this).closest('table');
21+
table.find('.' + checkboxClass).prop('checked', $(this).is(':checked'));
22+
toggleDeleteButton();
23+
});
24+
25+
toggleDeleteButton();
26+
27+
$deleteButton.on('click', function () {
28+
var ids = $('.' + checkboxClass + ':checked').map(function () {
29+
return 'ids[]=' + $(this).val();
30+
}).get();
31+
if (ids.length === 0) return;
32+
33+
var url = deleteUrl + '?' + ids.join('&');
34+
35+
var $tempLink = $('<a>', {
36+
'href': url,
37+
'data-ajax-modal': ''
38+
}).hide().appendTo('body');
39+
40+
$tempLink.trigger('click');
41+
$tempLink.remove();
42+
});
43+
}

webapp/src/Controller/Jury/UserController.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Symfony\Component\HttpFoundation\Response;
2323
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
2424
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
25+
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
2526
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
2627
use Symfony\Component\HttpKernel\KernelInterface;
2728
use Symfony\Component\PropertyAccess\PropertyAccess;
@@ -70,6 +71,14 @@ public function indexAction(): Response
7071
'teamid' => ['title' => '', 'sort' => false, 'render' => 'entity_id_badge'],
7172
'team' => ['title' => 'team', 'sort' => true],
7273
];
74+
75+
if ($this->isGranted('ROLE_ADMIN')) {
76+
$table_fields = array_merge(
77+
['checkbox' => ['title' => '<input type="checkbox" class="select-all" title="Select all users">', 'sort' => false, 'search' => false, 'raw' => true]],
78+
$table_fields
79+
);
80+
}
81+
7382
if (in_array('ipaddress', $this->config->get('auth_methods'))) {
7483
$table_fields['ip_address'] = ['title' => 'autologin IP', 'sort' => true];
7584
}
@@ -83,6 +92,21 @@ public function indexAction(): Response
8392
/** @var User $u */
8493
$userdata = [];
8594
$useractions = [];
95+
96+
if ($this->isGranted('ROLE_ADMIN')) {
97+
$canBeDeleted = $u->getUserid() !== $this->dj->getUser()->getUserid();
98+
if ($canBeDeleted) {
99+
$userdata['checkbox'] = [
100+
'value' => sprintf(
101+
'<input type="checkbox" name="ids[]" value="%s" class="user-checkbox">',
102+
$u->getUserid()
103+
)
104+
];
105+
} else {
106+
$userdata['checkbox'] = ['value' => ''];
107+
}
108+
}
109+
86110
// Get whatever fields we can from the user object itself.
87111
foreach ($table_fields as $k => $v) {
88112
if ($propertyAccessor->isReadable($u, $k)) {
@@ -386,4 +410,30 @@ public function resetTeamLoginStatus(Request $request): Response
386410
$this->addFlash('success', 'Reset login status all ' . $count . ' users with the team role.');
387411
return $this->redirectToRoute('jury_users');
388412
}
413+
414+
#[IsGranted('ROLE_ADMIN')]
415+
#[Route(path: '/delete-multiple', name: 'jury_user_delete_multiple', methods: ['GET', 'POST'])]
416+
public function deleteMultipleAction(Request $request): Response
417+
{
418+
$ids = $request->query->all('ids');
419+
if (empty($ids)) {
420+
throw new BadRequestHttpException('No IDs specified for deletion');
421+
}
422+
423+
$users = $this->em->getRepository(User::class)->findBy(['userid' => $ids]);
424+
425+
$deletableUsers = [];
426+
foreach ($users as $user) {
427+
if ($user->getUserid() !== $this->dj->getUser()->getUserid()) {
428+
$deletableUsers[] = $user;
429+
}
430+
}
431+
432+
if (empty($deletableUsers)) {
433+
$this->addFlash('warning', 'No users could be deleted (you cannot delete your own account).');
434+
return $this->redirectToRoute('jury_users');
435+
}
436+
437+
return $this->deleteEntities($request, $deletableUsers, $this->generateUrl('jury_users'));
438+
}
389439
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{% if entities is not empty %}
2+
<button type="button" class="btn btn-danger me-2" id="delete-selected-button" disabled><i class="fas fa-trash-alt"></i> Delete selected</button>
3+
{% endif %}

webapp/templates/jury/problems.html.twig

Lines changed: 6 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,7 @@
2727

2828
{% if is_granted('ROLE_ADMIN') %}
2929
<p class="mt-4">
30-
{% if problems_current is not empty or problems_other is not empty %}
31-
<button type="button" class="btn btn-danger me-2" id="delete-selected-button" disabled><i class="fas fa-trash-alt"></i> Delete selected</button>
32-
{% endif %}
30+
{% include 'jury/partials/_delete_button.html.twig' with {'entities': problems_current} %}
3331
{{ button(path('jury_problem_add'), 'Add new problem', 'primary', 'plus') }}
3432
{{ button(path('jury_import_export', {'_fragment':'problemarchive'}), 'Import problem', 'primary', 'upload') }}
3533
</p>
@@ -40,49 +38,13 @@
4038

4139
{% block extrafooter %}
4240
{{ parent() }}
41+
<script src="{{ asset('js/multi-delete.js') }}"></script>
4342
<script>
4443
$(function() {
45-
var $deleteButton = $('#delete-selected-button');
46-
47-
function toggleDeleteButton() {
48-
var checkedCount = $('.problem-checkbox:checked').length;
49-
$deleteButton.prop('disabled', checkedCount === 0);
50-
}
51-
52-
// Use delegated events to handle clicks on checkboxes, as DataTables might redraw the table.
53-
$(document).on('change', '.problem-checkbox', function() {
54-
var table = $(this).closest('table');
55-
var $tableCheckboxes = table.find('.problem-checkbox');
56-
var $selectAllInTable = table.find('.select-all');
57-
$selectAllInTable.prop('checked', $tableCheckboxes.length > 0 && $tableCheckboxes.length === $tableCheckboxes.filter(':checked').length);
58-
toggleDeleteButton();
59-
});
60-
61-
$(document).on('change', '.select-all', function() {
62-
var table = $(this).closest('table');
63-
table.find('.problem-checkbox').prop('checked', $(this).is(':checked'));
64-
toggleDeleteButton();
65-
});
66-
67-
// Initial state on page load.
68-
toggleDeleteButton();
69-
70-
$deleteButton.on('click', function () {
71-
var ids = $('.problem-checkbox:checked').map(function () {
72-
return 'ids[]=' + $(this).val();
73-
}).get();
74-
if (ids.length === 0) return;
75-
76-
var url = "{{ path('jury_problem_delete_multiple') }}" + '?' + ids.join('&');
77-
78-
// Create a temporary link and click it to trigger the existing AJAX modal logic.
79-
var $tempLink = $('<a>', {
80-
'href': url,
81-
'data-ajax-modal': ''
82-
}).hide().appendTo('body');
83-
84-
$tempLink.trigger('click');
85-
$tempLink.remove();
44+
initializeMultiDelete({
45+
buttonSelector: '#delete-selected-button',
46+
checkboxClass: 'problem-checkbox',
47+
deleteUrl: '{{ path('jury_problem_delete_multiple') }}'
8648
});
8749
});
8850
</script>

webapp/templates/jury/users.html.twig

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,24 @@
1515
{{ macros.table(users, table_fields) }}
1616

1717
{% if is_granted('ROLE_ADMIN') %}
18-
<p>
18+
<p class="mt-4">
19+
{% include 'jury/partials/_delete_button.html.twig' with {'entities': users} %}
1920
{{ button(path('jury_user_add'), 'Add new user', 'primary', 'plus') }}
2021
{{ button(path('jury_reset_login_status'), 'Reset login status of all team users', 'secondary', 'refresh') }}
2122
</p>
2223
{% endif %}
2324
{% endblock %}
25+
26+
{% block extrafooter %}
27+
{{ parent() }}
28+
<script src="{{ asset('js/multi-delete.js') }}"></script>
29+
<script>
30+
$(function() {
31+
initializeMultiDelete({
32+
buttonSelector: '#delete-selected-button',
33+
checkboxClass: 'user-checkbox',
34+
deleteUrl: '{{ path('jury_user_delete_multiple') }}'
35+
});
36+
});
37+
</script>
38+
{% endblock %}

0 commit comments

Comments
 (0)