Skip to content

Commit 2222a86

Browse files
committed
Add multi-delete option for problems.
Part of #229
1 parent bc181f7 commit 2222a86

File tree

5 files changed

+139
-6
lines changed

5 files changed

+139
-6
lines changed

webapp/src/Controller/BaseController.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,7 @@ protected function buildDeleteTree(array $entities, array $relations): array {
372372
}
373373
$primaryKeyData[] = $primaryKeyDataTemp;
374374
}
375-
return [$isError, $primaryKeyData, $messages];
375+
return [$isError, $primaryKeyData, array_values(array_unique($messages))];
376376
}
377377

378378
/**
@@ -445,6 +445,7 @@ protected function deleteEntities(
445445
'showModalSubmit' => !$isError,
446446
'modalUrl' => $request->getRequestUri(),
447447
'redirectUrl' => $redirectUrl,
448+
'count' => count($entities),
448449
];
449450
if ($request->isXmlHttpRequest()) {
450451
return $this->render('jury/delete_modal.html.twig', $data);

webapp/src/Controller/Jury/ProblemController.php

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,13 @@ public function indexAction(): Response
8888
'type' => ['title' => 'type', 'sort' => true],
8989
];
9090

91+
if ($this->isGranted('ROLE_ADMIN')) {
92+
$table_fields = array_merge(
93+
['checkbox' => ['title' => '<input type="checkbox" class="select-all" title="Select all problems">', 'sort' => false, 'search' => false, 'raw' => true]],
94+
$table_fields
95+
);
96+
}
97+
9198
$contestCountData = $this->em->createQueryBuilder()
9299
->from(ContestProblem::class, 'cp')
93100
->select('COUNT(cp.shortname) AS count', 'p.probid')
@@ -109,6 +116,28 @@ public function indexAction(): Response
109116
$p = $row[0];
110117
$problemdata = [];
111118
$problemactions = [];
119+
120+
if ($this->isGranted('ROLE_ADMIN')) {
121+
$problemIsLocked = false;
122+
foreach ($p->getContestProblems() as $contestProblem) {
123+
if ($contestProblem->getContest()->isLocked()) {
124+
$problemIsLocked = true;
125+
break;
126+
}
127+
}
128+
129+
if (!$problemIsLocked) {
130+
$problemdata['checkbox'] = [
131+
'value' => sprintf(
132+
'<input type="checkbox" name="ids[]" value="%s" class="problem-checkbox">',
133+
$p->getProbid()
134+
)
135+
];
136+
} else {
137+
$problemdata['checkbox'] = ['value' => ''];
138+
}
139+
}
140+
112141
// Get whatever fields we can from the problem object itself.
113142
foreach ($table_fields as $k => $v) {
114143
if ($propertyAccessor->isReadable($p, $k)) {
@@ -999,6 +1028,39 @@ public function editAction(Request $request, int $probId): Response
9991028
]);
10001029
}
10011030

1031+
#[IsGranted('ROLE_ADMIN')]
1032+
#[Route(path: '/delete-multiple', name: 'jury_problem_delete_multiple', methods: ['GET', 'POST'])]
1033+
public function deleteMultipleAction(Request $request): Response
1034+
{
1035+
$ids = $request->query->all('ids');
1036+
if (empty($ids)) {
1037+
throw new BadRequestHttpException('No IDs specified for deletion');
1038+
}
1039+
1040+
$problems = $this->em->getRepository(Problem::class)->findBy(['probid' => $ids]);
1041+
1042+
$deletableProblems = [];
1043+
foreach ($problems as $problem) {
1044+
$isLocked = false;
1045+
foreach ($problem->getContestProblems() as $contestProblem) {
1046+
if ($contestProblem->getContest()->isLocked()) {
1047+
$isLocked = true;
1048+
break;
1049+
}
1050+
}
1051+
if (!$isLocked) {
1052+
$deletableProblems[] = $problem;
1053+
}
1054+
}
1055+
1056+
if (empty($deletableProblems)) {
1057+
$this->addFlash('warning', 'No problems could be deleted (they might be locked).');
1058+
return $this->redirectToRoute('jury_problems');
1059+
}
1060+
1061+
return $this->deleteEntities($request, $deletableProblems, $this->generateUrl('jury_problems'));
1062+
}
1063+
10021064
#[IsGranted('ROLE_ADMIN')]
10031065
#[Route(path: '/{probId<\d+>}/delete', name: 'jury_problem_delete')]
10041066
public function deleteAction(Request $request, int $probId): Response

webapp/templates/jury/delete_modal.html.twig

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
{% extends "partials/modal.html.twig" %}
22

3-
{% block title %}Delete {{ type }} {{ primaryKey }} - "{{ description }}"{% endblock %}
3+
{% block title %}
4+
{% if count > 1 %}
5+
Delete {{ count }} {{ type }}s
6+
{% else %}
7+
Delete {{ type }} {{ primaryKey }} - "{{ description }}"
8+
{% endif %}
9+
{% endblock %}
410

511
{% block content %}
612

@@ -9,7 +15,16 @@
915
<strong>Error: {{ messages.0 }}</strong>
1016
</div>
1117
{% else %}
12-
<p>You're about to delete {{ type }} {{ primaryKey }} "{{ description }}".</p>
18+
{% if count > 1 %}
19+
<p>You're about to delete the following {{ count }} {{ type }}s:</p>
20+
<ul>
21+
{% for desc in description|split(',') %}
22+
<li>{{ desc|trim }}</li>
23+
{% endfor %}
24+
</ul>
25+
{% else %}
26+
<p>You're about to delete {{ type }} {{ primaryKey }} "{{ description }}".</p>
27+
{% endif %}
1328

1429
{% if messages is not empty %}
1530
<p>

webapp/templates/jury/jury_macros.twig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@
104104
<th scope="col" class="
105105
{%- if column.sort is defined and column.sort %}sortable{%- endif %}
106106
{%- if (column.search is not defined) or column.search %} searchable{%- endif %}">
107-
{{- column.title|nl2br -}}
107+
{%- if column.raw|default(false) %}{{- column.title|raw -}}{% else %}{{- column.title|nl2br -}}{% endif -%}
108108
</th>
109109
{%- endfor %}
110110
{%- if num_actions > 0 %}
@@ -149,7 +149,7 @@
149149
{% elseif (column.render | default('')) == "entity_id_badge" %}
150150
{% if item.value %}{{- item.value|entityIdBadge(item.idPrefix|default('')) -}}{% endif %}
151151
{% else %}
152-
{{- (item.value|default(item.default|default(''))) -}}
152+
{%- if column.raw|default(false) %}{{- (item.value|default(item.default|default('')))|raw -}}{% else %}{{- (item.value|default(item.default|default(''))) -}}{% endif -%}
153153
{% endif %}
154154
{% if item.icon is defined %}<i class="fas fa-{{ item.icon }}"></i>{%- endif %}
155155
{%- if item.link is defined or (row.link is defined and not item.toggle_partial is defined) -%}</a>{% endif %}

webapp/templates/jury/problems.html.twig

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,64 @@
2626
{% endif %}
2727

2828
{% if is_granted('ROLE_ADMIN') %}
29-
<p>
29+
<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 %}
3033
{{ button(path('jury_problem_add'), 'Add new problem', 'primary', 'plus') }}
3134
{{ button(path('jury_import_export', {'_fragment':'problemarchive'}), 'Import problem', 'primary', 'upload') }}
3235
</p>
3336
{% endif %}
37+
38+
39+
{% endblock %}
40+
41+
{% block extrafooter %}
42+
{{ parent() }}
43+
<script>
44+
$(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();
86+
});
87+
});
88+
</script>
3489
{% endblock %}

0 commit comments

Comments
 (0)