From c8f8121961f8eabb26c1ce8e1c3496a2f9abc940 Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Fri, 1 Nov 2024 17:00:03 -0700 Subject: [PATCH] Query and display 'best' evaluation for any given submission - Make the leaderboard show Submissions, rather than Evaluations - Simplify leaderboard to avoid REST API calls. No pagination will be done for now. - Add "ordering" and "ordering_priority" to EVALUATION_DISPLAY_CONFIG to determine the ordering of evaluations when scoring submission versions - Add datatables for dynamic sorting of both submissions & versions - Display the 'best' evaluation for any given submission in submission list Ref https://github.com/2i2c-org/frx-challenges/issues/177 --- frx_challenges/frx_challenges/settings.py | 15 ++- frx_challenges/web/models.py | 46 +++++++- frx_challenges/web/templates/results.html | 109 +++++++++--------- .../web/templates/submission/detail.html | 26 ++++- .../web/templates/submission/list.html | 36 +++++- frx_challenges/web/urls.py | 1 - frx_challenges/web/views/default.py | 54 +++++---- frx_challenges/web/views/submissions.py | 2 +- 8 files changed, 196 insertions(+), 93 deletions(-) diff --git a/frx_challenges/frx_challenges/settings.py b/frx_challenges/frx_challenges/settings.py index 54a8ccc..b782038 100644 --- a/frx_challenges/frx_challenges/settings.py +++ b/frx_challenges/frx_challenges/settings.py @@ -262,8 +262,19 @@ } EVALUATION_DISPLAY_CONFIG = [ - {"result_key": "chars", "display_name": "Characters"}, - {"result_key": "lines", "display_name": "Lines"}, + { + "result_key": "chars", + "display_name": "Characters", + # ordering options are "smaller_is_better" and "bigger_is_better" + "ordering": "smaller_is_better", + "ordering_priority": 2 + }, + { + "result_key": "lines", + "display_name": "Lines", + "ordering": "bigger_is_better", + "ordering_priority": 1 + } ] django_yamlconf.load() diff --git a/frx_challenges/web/models.py b/frx_challenges/web/models.py index 27e75f9..0b85f12 100644 --- a/frx_challenges/web/models.py +++ b/frx_challenges/web/models.py @@ -6,7 +6,8 @@ from django.contrib.auth.models import User from django.db import models, transaction from django_jsonform.models.fields import JSONField - +from django.db.models.fields.json import KT +from typing import Optional, List # Create your models here. @@ -40,6 +41,47 @@ class Submission(models.Model): blank=True, null=True, schema=settings.SITE_SUBMISSION_FORM_SCHEMA ) + @property + def best_version(self) -> Version: + """ + Return the 'best' evaluated version for this Submission + """ + # Construct a query that returns evaluations that: + # 1. Belong to a version that belong to this submission + # 2. Have been succesfully evaluated + # 3. Have results that contain all the keys we use for ordering + # 4. Ordered by the ordering criteria expressed by EVALUATION_DISPLAY_CONFIG + + + # Sort the display_config by ordering_priority (smaller numbers go first) + # This primarily is used for tiebreaking, since we only want the 'best' + sorted_display_config = sorted(settings.EVALUATION_DISPLAY_CONFIG, key=lambda dc: -dc["ordering_priority"]) + + # Ordering criteria for querying our results + ordering_criteria = [] + # List of keys that *must* be present in a result for it to count + result_must_have_keys = [] + + for dc in sorted_display_config: + result_must_have_keys.append(dc['result_key']) + k = KT(f"result__{dc['result_key']}") + if dc["ordering"] == "smaller_is_better": + k = k.asc() + elif dc["ordering"] == "bigger_is_better": + k = k.desc() + else: + raise ValueError(f"Invalid ordering {dc['ordering']} found for result_key {dc['result_key']}") + ordering_criteria.append(k) + + best_evaluation = Evaluation.objects.filter( + version__submission=self, + status=Evaluation.Status.EVALUATED, + result__has_keys=result_must_have_keys + ).order_by(*ordering_criteria).first() + + if best_evaluation: + return best_evaluation.version + class Version(models.Model): """ @@ -52,7 +94,7 @@ class Status(models.TextChoices): UPLOADED = "UPLOADED" CLEARED = "CLEARED" - submission = models.ForeignKey(Submission, on_delete=models.CASCADE) + submission = models.ForeignKey(Submission, on_delete=models.CASCADE, related_name="versions") date_created = models.DateTimeField(auto_now=True) # FIXME: Cascade is probably not quite right? user = models.ForeignKey(User, on_delete=models.CASCADE) diff --git a/frx_challenges/web/templates/results.html b/frx_challenges/web/templates/results.html index 0ae3fc6..39261ad 100644 --- a/frx_challenges/web/templates/results.html +++ b/frx_challenges/web/templates/results.html @@ -25,65 +25,60 @@

Explanatory Headline

- - - - - -
-
+ + + + + + + + {% for dc in evaluation_display_config %} + + {% endfor %} + + + + {% for result in results %} + + + + + + {% for r in result.best_version.latest_evaluation.ordered_results %} + + {% endfor %} + + {% endfor %} + +
IDNameDescriptionDate created{{ dc.display_name }}
{{ result.submission.id }} + {{ result.submission.name }} + {{ result.submission.description }}{{ result.submission.date_created|date:"c" }} + {% if r %} + {{ r }} + {% endif %} +
+
+ main(); + {% endblock body %} diff --git a/frx_challenges/web/templates/submission/detail.html b/frx_challenges/web/templates/submission/detail.html index 8315934..a0cb0d9 100644 --- a/frx_challenges/web/templates/submission/detail.html +++ b/frx_challenges/web/templates/submission/detail.html @@ -1,11 +1,13 @@ {% extends "page.html" %} {% block head %} + integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" + crossorigin="anonymous"> + href="https://cdn.datatables.net/2.0.8/css/dataTables.dataTables.css" /> + + {% endblock head %} {% block body %}
@@ -39,7 +41,7 @@

{{ submission.name }}

{{ v.id }} {{ v.filename }} - {{ v.date_created|date:"M j Y" }} + {{ v.date_created|date:"c" }} {{ v.latest_evaluation.status }} {% for r in v.latest_evaluation.ordered_results %} @@ -61,7 +63,21 @@

{{ submission.name }}

{% endblock body %} diff --git a/frx_challenges/web/urls.py b/frx_challenges/web/urls.py index 49a4b89..3c21089 100644 --- a/frx_challenges/web/urls.py +++ b/frx_challenges/web/urls.py @@ -4,7 +4,6 @@ urlpatterns = [ path("upload/", default.upload, name="upload"), - path("results", default.results, name="results"), path("teams/list", teams.list, name="teams-list"), path("teams/create", teams.create, name="teams-create"), path("teams/", teams.view, name="teams-view"), diff --git a/frx_challenges/web/views/default.py b/frx_challenges/web/views/default.py index 266864f..f9b83d8 100644 --- a/frx_challenges/web/views/default.py +++ b/frx_challenges/web/views/default.py @@ -54,32 +54,40 @@ def upload(request: HttpRequest, id: int) -> HttpResponse: ) -def results(request: HttpRequest) -> HttpResponse: - evaluations = Evaluation.objects.all() - - evaluations_resp = { - "display_config": settings.EVALUATION_DISPLAY_CONFIG, - "results": [], - } - - for ev in evaluations: - evaluations_resp["results"].append( - { - "evaluation_id": ev.id, - "username": ev.version.user.username, - "status": ev.status, - "last_updated": ev.last_updated.isoformat(), - "result": ev.result, - } - ) - - return JsonResponse(evaluations_resp) - - def leaderboard(request: HttpRequest) -> HttpResponse: if settings.CHALLENGE_STATE != "RUNNING": return HttpResponse( "Challenge hasn't started, so leaderboard is not available", status=400 ) - return render(request, "results.html") + sorted_display_config = sorted(settings.EVALUATION_DISPLAY_CONFIG, key=lambda dc: -dc["ordering_priority"]) + results = [] + all_submissions = Submission.objects.all() + for sub in all_submissions: + bv = sub.best_version + if not bv: + # Only display submissions with at least one 'best version' + continue + results.append({ + "submission": sub, + "best_version": bv + }) + + def sort_key_func(r): + bv: Version = r["best_version"] + sort_key = [] + for dc in sorted_display_config: + if dc["ordering"] == "smaller_is_better": + sort_key.append(-bv.latest_evaluation.result[dc["result_key"]]) + elif dc["ordering"] == "bigger_is_better": + sort_key.append(bv.latest_evaluation.result[dc["result_key"]]) + else: + raise ValueError(f"Invalid ordering {dc['ordering']} found for result_key {dc['result_key']}") + + return sort_key + + results = sorted( + results, + key=sort_key_func + ) + return render(request, "results.html", {"results":results}) diff --git a/frx_challenges/web/views/submissions.py b/frx_challenges/web/views/submissions.py index fac20df..f770e57 100644 --- a/frx_challenges/web/views/submissions.py +++ b/frx_challenges/web/views/submissions.py @@ -60,7 +60,7 @@ def detail(request: HttpRequest, id: int) -> HttpResponse: submission = queryset.get(id=id) except Submission.DoesNotExist: raise Http404("Submission does not exist") - versions = submission.version_set.all() + versions = submission.versions.all() return render( request, "submission/detail.html",