From 71575d0a517d7073b8b3c54a381b969612296872 Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Wed, 24 Jul 2024 19:17:12 -0700 Subject: [PATCH] Add teams functionality Also commit missed redo of the evaluator loop --- comptest/web/management/commands/evaluator.py | 15 ++++ comptest/web/models.py | 13 ++- comptest/web/templates/teams/add-member.html | 14 ++++ comptest/web/templates/teams/create.html | 14 ++++ comptest/web/templates/teams/list.html | 14 ++++ comptest/web/templates/teams/view.html | 16 ++++ comptest/web/urls.py | 12 ++- comptest/web/views/__init__.py | 0 comptest/web/{views.py => views/default.py} | 9 +-- comptest/web/views/teams.py | 81 +++++++++++++++++++ 10 files changed, 176 insertions(+), 12 deletions(-) create mode 100644 comptest/web/templates/teams/add-member.html create mode 100644 comptest/web/templates/teams/create.html create mode 100644 comptest/web/templates/teams/list.html create mode 100644 comptest/web/templates/teams/view.html create mode 100644 comptest/web/views/__init__.py rename comptest/web/{views.py => views/default.py} (87%) create mode 100644 comptest/web/views/teams.py diff --git a/comptest/web/management/commands/evaluator.py b/comptest/web/management/commands/evaluator.py index cc0acfe..0dbcae9 100644 --- a/comptest/web/management/commands/evaluator.py +++ b/comptest/web/management/commands/evaluator.py @@ -89,6 +89,20 @@ async def process_running_evaluation(self, evaluator: DockerEvaluator, evaluatio async def ahandle(self): evaluator = DockerEvaluator() while True: + # Create evaluation objects when they do not exist + submissions_without_evaluations = Submission.objects.filter( + Q(status=Submission.Status.UPLOADED), + ~Exists( + Evaluation.objects.filter( + submission=OuterRef("pk") + ) + ) + ) + async for s in submissions_without_evaluations: + e = Evaluation(submission=s) + await e.asave() + + # Start Evaluations when they have not been started yet unstarted_evaluations = Evaluation.objects.select_related("submission").filter( status=Evaluation.Status.NOT_STARTED ) @@ -96,6 +110,7 @@ async def ahandle(self): async for e in unstarted_evaluations: await self.start_evaluation(evaluator, e) + # Check running evaluations running_evaluations = Evaluation.objects.select_related("submission").filter( status=Evaluation.Status.EVALUATING ) diff --git a/comptest/web/models.py b/comptest/web/models.py index e20fd2f..3ef98b2 100644 --- a/comptest/web/models.py +++ b/comptest/web/models.py @@ -37,4 +37,15 @@ class Status(models.TextChoices): def __str__(self): - return f"({self.status}) {self.result} {self.submission.data_uri}" \ No newline at end of file + return f"({self.status}) {self.result} {self.submission.data_uri}" + + + +class Team(models.Model): + name = models.CharField(max_length=1024) + members = models.ManyToManyField(User, through='TeamMembership', related_name='teams') + +class TeamMembership(models.Model): + is_admin = models.BooleanField() + user = models.ForeignKey(User, on_delete=models.CASCADE) + team = models.ForeignKey(Team, on_delete=models.CASCADE) \ No newline at end of file diff --git a/comptest/web/templates/teams/add-member.html b/comptest/web/templates/teams/add-member.html new file mode 100644 index 0000000..c1e3631 --- /dev/null +++ b/comptest/web/templates/teams/add-member.html @@ -0,0 +1,14 @@ +{% extends "page.html" %} + + +{% block body %} + +

Add someone to team {{team.name}}

+ +
+ {% csrf_token %} + {{ form }} + +
+ +{% endblock %} \ No newline at end of file diff --git a/comptest/web/templates/teams/create.html b/comptest/web/templates/teams/create.html new file mode 100644 index 0000000..380c6d1 --- /dev/null +++ b/comptest/web/templates/teams/create.html @@ -0,0 +1,14 @@ +{% extends "page.html" %} + + +{% block body %} + +

Create a team

+ +
+ {% csrf_token %} + {{ form }} + +
+ +{% endblock %} \ No newline at end of file diff --git a/comptest/web/templates/teams/list.html b/comptest/web/templates/teams/list.html new file mode 100644 index 0000000..e8bde88 --- /dev/null +++ b/comptest/web/templates/teams/list.html @@ -0,0 +1,14 @@ +{% extends "page.html" %} + +{% block body %} + +

List of Teams you are a part of

+Create a new team + + + +{% endblock %} \ No newline at end of file diff --git a/comptest/web/templates/teams/view.html b/comptest/web/templates/teams/view.html new file mode 100644 index 0000000..fa4c371 --- /dev/null +++ b/comptest/web/templates/teams/view.html @@ -0,0 +1,16 @@ +{% extends "page.html" %} + +{% block body %} + +

{{ team.name }}

+ +

Members

+Add users to this team + + + +{% endblock %} \ No newline at end of file diff --git a/comptest/web/urls.py b/comptest/web/urls.py index d065a74..2114d3d 100644 --- a/comptest/web/urls.py +++ b/comptest/web/urls.py @@ -1,9 +1,13 @@ from django.urls import path -from . import views +from .views import default, teams urlpatterns = [ - path("upload", views.upload, name="upload"), - path("results", views.results, name="results"), - path("", views.home, name="home") + 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"), + path("teams//add-member", teams.add_member, name="teams-add-member"), + path("", default.home, name="home") ] \ No newline at end of file diff --git a/comptest/web/views/__init__.py b/comptest/web/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/comptest/web/views.py b/comptest/web/views/default.py similarity index 87% rename from comptest/web/views.py rename to comptest/web/views/default.py index 3190ba7..7d7a7cb 100644 --- a/comptest/web/views.py +++ b/comptest/web/views/default.py @@ -4,7 +4,7 @@ from django.http import HttpRequest, HttpResponse, HttpResponseRedirect, JsonResponse from django import forms from django.contrib.auth.decorators import login_required -from .models import Submission, Evaluation +from ..models import Submission, Evaluation from django.conf import settings @@ -24,12 +24,7 @@ def upload(request: HttpRequest) -> HttpResponse: status=Submission.Status.UPLOADED, data_uri=f"file:///{filepath}" ) - # FIXME: Figure out transactions s.save() - e = Evaluation( - submission=s, - ) - e.save() return HttpResponseRedirect("/") else: form = UploadForm() @@ -53,4 +48,4 @@ def results(request: HttpRequest) -> HttpResponse: }) def home(request: HttpRequest) -> HttpResponse: - return render(request, "results.html") \ No newline at end of file + return render(request, "results.html") diff --git a/comptest/web/views/teams.py b/comptest/web/views/teams.py new file mode 100644 index 0000000..6783d7a --- /dev/null +++ b/comptest/web/views/teams.py @@ -0,0 +1,81 @@ +import os +import tempfile +from django.shortcuts import render +from django.http import HttpRequest, HttpResponse, HttpResponseRedirect, Http404 +from django.urls import reverse +from django import forms +from django.contrib.auth.decorators import login_required +from django.contrib.auth.models import User +from ..models import Team, TeamMembership +from django.conf import settings + +@login_required +def list(request: HttpRequest) -> HttpResponse: + teams = request.user.teams.all() + + return render(request, 'teams/list.html', { + 'teams': teams + }) + + +class TeamForm(forms.Form): + name = forms.CharField(max_length=1024) + + +@login_required +def create(request: HttpRequest) -> HttpResponse: + if request.method == "POST": + form = TeamForm(request.POST) + if form.is_valid(): + team = Team() + team.name = form.cleaned_data['name'] + # FIXME: Transactions? + team.save() + membership = TeamMembership() + membership.user = request.user + membership.team = team + membership.is_admin = True + membership.save() + return HttpResponseRedirect(reverse('teams-view', args=(team.id, ))) + else: + form = TeamForm() + return render(request, "teams/create.html", {"form": form}) + +# Intentionally not authenticated, as anyone should be able to view team membership +def view(request: HttpRequest, id: int) -> HttpResponse: + try: + team = Team.objects.filter(id=id).get() + except Team.DoesNotExist: + raise Http404("The requested team does not exist") + + return render(request, "teams/view.html", {"team": team}) + + + +class AddMemberForm(forms.Form): + username = forms.CharField(max_length=1024) + is_admin = forms.BooleanField(required=False) + +@login_required +def add_member(request: HttpRequest, id: int) -> HttpRequest: + try: + team = Team.objects.filter(id=id).get() + except Team.DoesNotExist: + raise Http404("The requested team does not exist") + + # FIXME: Validate that we are admin on the team so we can add people + if request.method == "POST": + form = AddMemberForm(request.POST) + if form.is_valid(): + # FIXME: Handle missing users + # FIXME: Handle an 'invitation acceptance' flow for users + user = User.objects.filter(username=form.cleaned_data['username']).get() + membership = TeamMembership() + membership.user = user + membership.team = team + membership.is_admin = form.cleaned_data['is_admin'] + membership.save() + return HttpResponseRedirect(reverse('teams-view', args=(team.id, ))) + else: + form = AddMemberForm() + return render(request, "teams/add-member.html", {"form": form, "team": team}) \ No newline at end of file