From 34956910420266e12e4b0eaea8845a7ed4231a48 Mon Sep 17 00:00:00 2001 From: ElijahFeldman7 Date: Fri, 29 May 2026 20:56:46 -0400 Subject: [PATCH] feat: filter content + move view all button --- autograder/apps/contests/views.py | 76 +++++++++- autograder/apps/runtests/views.py | 82 +++++++++- autograder/static/profile.css | 111 ++++++++++++++ autograder/templates/contest/status.html | 121 ++++++++++++++- .../partials/submission_filters.html | 53 +++++++ autograder/templates/runtests/status.html | 143 ++++++++++++++++-- 6 files changed, 565 insertions(+), 21 deletions(-) create mode 100644 autograder/templates/partials/submission_filters.html diff --git a/autograder/apps/contests/views.py b/autograder/apps/contests/views.py index 38584e8..f019c20 100644 --- a/autograder/apps/contests/views.py +++ b/autograder/apps/contests/views.py @@ -4,6 +4,7 @@ from django.conf import settings from django.utils import timezone from django.http import HttpResponse +from urllib.parse import urlencode from ..oauth.decorators import login_required, admin_required from .models import Contest from ..problems.models import Problem @@ -15,6 +16,25 @@ logger = logging.getLogger(__name__) +def _parse_multi_values(values): + items = [] + for raw in values: + if raw is None: + continue + for part in str(raw).split(","): + part = part.strip() + if part: + items.append(part) + seen = set() + deduped = [] + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + @login_required def contests_view(request): contests = Contest.objects.all().order_by("-start") @@ -144,7 +164,52 @@ def contest_status_view(request, cid, mine_only, page): if not request.user.is_staff: subs = subs.filter(timestamp__gte=contest.start) - subs = subs.order_by("-timestamp") + base_subs = subs + + problem_values = _parse_multi_values(request.GET.getlist("problem")) + verdict_values = _parse_multi_values(request.GET.getlist("verdict")) + language_values = _parse_multi_values(request.GET.getlist("language")) + + problem_ids = [] + for value in problem_values: + try: + problem_ids.append(int(value)) + except (TypeError, ValueError): + continue + + if problem_ids: + subs = subs.filter(problem_id__in=problem_ids) + + if verdict_values: + subs = subs.filter(verdict__in=verdict_values) + + if language_values: + subs = subs.filter(language__in=language_values) + + problem_options = ( + Problem.objects.filter( + id__in=base_subs.values_list("problem_id", flat=True).distinct() + ) + .select_related("contest") + .order_by("contest_letter", "id") + ) + verdict_options = list( + base_subs.values_list("verdict", flat=True).order_by("verdict").distinct() + ) + language_options = list( + base_subs.values_list("language", flat=True).order_by("language").distinct() + ) + + filter_params = {} + if problem_ids: + filter_params["problem"] = ",".join(str(pid) for pid in problem_ids) + if verdict_values: + filter_params["verdict"] = ",".join(verdict_values) + if language_values: + filter_params["language"] = ",".join(language_values) + filters_query = urlencode(filter_params) + + subs = subs.select_related("problem", "usr", "contest").order_by("-timestamp") # Pagination paginator = Paginator(subs, 25) # Show 25 submissions per page @@ -159,6 +224,15 @@ def contest_status_view(request, cid, mine_only, page): "submissions": page_obj.object_list, "contest_over": timezone.now() > contest.end, "mine_only": mine_only, + "problem_options": problem_options, + "verdict_options": verdict_options, + "language_options": language_options, + "filters": { + "problem": problem_ids, + "verdict": verdict_values, + "language": language_values, + }, + "filters_query": filters_query, } return render(request, "contest/status.html", context) diff --git a/autograder/apps/runtests/views.py b/autograder/apps/runtests/views.py index 4ec2866..352d7d4 100644 --- a/autograder/apps/runtests/views.py +++ b/autograder/apps/runtests/views.py @@ -5,6 +5,7 @@ from django.conf import settings from django.utils import timezone from datetime import timedelta +from urllib.parse import urlencode from ..oauth.decorators import login_required from ..problems.models import Problem from ..contests.models import Contest @@ -15,6 +16,25 @@ logger = logging.getLogger(__name__) +def _parse_multi_values(values): + items = [] + for raw in values: + if raw is None: + continue + for part in str(raw).split(","): + part = part.strip() + if part: + items.append(part) + seen = set() + deduped = [] + for item in items: + if item in seen: + continue + seen.add(item) + deduped.append(item) + return deduped + + def get_last_sub_lang(user): try: return Submission.objects.filter(usr=user).latest().language @@ -94,7 +114,58 @@ def status_view(request, page, cid=None, mine=False): elif request.user.is_tjioi: submissions = submissions.filter(usr__is_tjioi=True) - submissions = submissions.order_by("-timestamp") + base_submissions = submissions + + problem_values = _parse_multi_values(request.GET.getlist("problem")) + verdict_values = _parse_multi_values(request.GET.getlist("verdict")) + language_values = _parse_multi_values(request.GET.getlist("language")) + + problem_ids = [] + for value in problem_values: + try: + problem_ids.append(int(value)) + except (TypeError, ValueError): + continue + + if problem_ids: + submissions = submissions.filter(problem_id__in=problem_ids) + + if verdict_values: + submissions = submissions.filter(verdict__in=verdict_values) + + if language_values: + submissions = submissions.filter(language__in=language_values) + + problem_options = ( + Problem.objects.filter( + id__in=base_submissions.values_list("problem_id", flat=True).distinct() + ) + .select_related("contest") + .order_by("contest__name", "contest_letter", "id") + ) + verdict_options = list( + base_submissions.values_list("verdict", flat=True) + .order_by("verdict") + .distinct() + ) + language_options = list( + base_submissions.values_list("language", flat=True) + .order_by("language") + .distinct() + ) + + filter_params = {} + if problem_ids: + filter_params["problem"] = ",".join(str(pid) for pid in problem_ids) + if verdict_values: + filter_params["verdict"] = ",".join(verdict_values) + if language_values: + filter_params["language"] = ",".join(language_values) + filters_query = urlencode(filter_params) + + submissions = submissions.select_related("problem", "usr", "contest").order_by( + "-timestamp" + ) paginator = Paginator(submissions, 25) # 25 per page try: @@ -109,6 +180,15 @@ def status_view(request, page, cid=None, mine=False): "page": page, "cid": cid, "mine": mine, + "problem_options": problem_options, + "verdict_options": verdict_options, + "language_options": language_options, + "filters": { + "problem": problem_ids, + "verdict": verdict_values, + "language": language_values, + }, + "filters_query": filters_query, } return render(request, "runtests/status.html", context) diff --git a/autograder/static/profile.css b/autograder/static/profile.css index 1f509ea..f181e9f 100644 --- a/autograder/static/profile.css +++ b/autograder/static/profile.css @@ -343,6 +343,18 @@ li.slg { font-size: 1.5em; } +.title-bar { + display: flex; + justify-content: space-between; + align-items: center; + gap: 1em; +} + +.title-actions { + display: flex; + gap: 1em; +} + .button { padding: 0.5em; border-radius: 0.5em; @@ -360,6 +372,105 @@ li.slg { background-color: rgb(40, 61, 166); } +.inline-button { + width: auto; + margin: 0; + display: inline-flex; + align-items: center; + gap: 0.4em; +} + +.filter-form { + margin: 1% 2%; + display: flex; + flex-direction: column; + gap: 0.75em; +} + +.filter-toggle { + align-self: flex-start; + background-color: rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 255, 255, 0.2); + padding: 0.35em 0.6em; +} + +.filter-icon { + width: 1em; + height: 1em; + fill: currentColor; +} + +.filter-panel { + background-color: rgba(0, 0, 0, 0.12); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 0.5em; + padding: 0.75em; + display: flex; + flex-direction: column; + gap: 0.75em; +} + +.filter-panel[hidden] { + display: none; +} + + +.filter-section { + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 0.4em; + padding: 0.5em; + background-color: rgba(0, 0, 0, 0.16); +} + +.filter-summary { + cursor: pointer; + font-weight: bold; + list-style: none; +} + +.filter-section[open] .filter-summary { + margin-bottom: 0.4em; +} + +.filter-summary::-webkit-details-marker { + display: none; +} + +.filter-summary::marker { + content: ""; +} + +.filter-options { + display: flex; + flex-wrap: wrap; + gap: 0.5em; +} + +.filter-actions { + display: flex; + justify-content: flex-end; +} + +.filter-option { + background-color: rgba(255, 255, 255, 0.06); + border: 1px solid rgba(255, 255, 255, 0.12); + color: white; + padding: 0.2em 0.5em; + border-radius: 0.4em; + cursor: pointer; +} + +.filter-option:hover { + background-color: rgba(255, 255, 255, 0.14); +} + +.filter-option.active { + background-color: rgb(0, 157, 255); + border-color: rgb(0, 157, 255); + color: black; +} + + input[type="text"] { width: 5%; padding-top: 0.25%; diff --git a/autograder/templates/contest/status.html b/autograder/templates/contest/status.html index 2a216e5..5256e09 100644 --- a/autograder/templates/contest/status.html +++ b/autograder/templates/contest/status.html @@ -15,12 +15,14 @@ {% include "partials/contestNavbar.html" %}
+{% url "contests:status" cid=cid mine_only=mine_only page=1 as filter_action_url %} +{% include "partials/submission_filters.html" with filter_form_id="contestFilters" filter_panel_id="contestFilterPanel" filter_toggle_id="contestFilterToggle" filter_action_url=filter_action_url %} - + {% if user.is_staff %}{% endif %} @@ -66,17 +68,130 @@
Submission User ProblemLangaugeLanguage Runtime VerdictSkip
{% if page_obj.has_previous %} - +
Previous
{% endif %}
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
{% if page_obj.has_next %} - +
Next
{% endif %}
+ + {% endblock content %} \ No newline at end of file diff --git a/autograder/templates/partials/submission_filters.html b/autograder/templates/partials/submission_filters.html new file mode 100644 index 0000000..ee0392a --- /dev/null +++ b/autograder/templates/partials/submission_filters.html @@ -0,0 +1,53 @@ +
+ + + + + +
diff --git a/autograder/templates/runtests/status.html b/autograder/templates/runtests/status.html index 8ac8765..cfc8065 100644 --- a/autograder/templates/runtests/status.html +++ b/autograder/templates/runtests/status.html @@ -12,29 +12,29 @@ {% if user.particles_enabled %}{% include "partials/particles.html" %}{% endif %} {% include "partials/header.html" %}
-
- Submissions {% if admin %} - Viewing as Admin{% endif %} +
+
+ Submissions {% if admin %} - Viewing as Admin{% endif %} +
+
+ {% if mine %} + View All + {% else %} + View Mine Only + {% endif %} +
-
- {% if mine %} - -
View All
-
- {% else %} - -
View Mine Only
-
- {% endif %} -
+ {% url "runtests:status" mine=mine cid=cid page=1 as filter_action_url %} + {% include "partials/submission_filters.html" with filter_form_id="submissionFilters" filter_panel_id="filterPanel" filter_toggle_id="filterToggle" filter_action_url=filter_action_url %} - + @@ -69,13 +69,13 @@
Submission User ProblemLangaugeLanguage Runtime Verdict
{% if page_obj.has_previous %} - +
Previous
{% endif %}
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
{% if page_obj.has_next %} - +
Next
{% endif %} @@ -116,4 +116,115 @@ console.log('WebSocket connection closed.'); }; + + {% endblock content %}