diff --git a/judge/forms.py b/judge/forms.py index 9b7b205..ad65a87 100644 --- a/judge/forms.py +++ b/judge/forms.py @@ -65,6 +65,14 @@ class NewContestForm(forms.Form): is_public = forms.BooleanField(label='Is this contest public?', required=False) """Contest is_public property""" + enable_linter_score = forms.BooleanField(label='Enable linter scoring', + required=False, initial=True) + """Contest enable_linter_score property""" + + enable_poster_score = forms.BooleanField(label='Enable poster scoring', + required=False, initial=True) + """Contest enable_poster_score property""" + def clean(self): cleaned_data = super().clean() cont_start = cleaned_data.get("contest_start") @@ -84,7 +92,7 @@ class AddPersonToContestForm(forms.Form): emails = MultiEmailField( label='Emails', widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 3}), - help_text='Enter emails seperated using commas') + help_text='Enter emails separated using commas') """Email ID of the person""" @@ -266,3 +274,14 @@ class NewCommentForm(forms.Form): comment = forms.CharField(label='Comment', required=True, widget=forms.Textarea( attrs={'class': 'form-control', 'rows': 2})) """Comment content""" + + +class AddPosterScoreForm(forms.Form): + """ + Form to add poster score for a submission + """ + + score = forms.IntegerField( + label='Poster Score', widget=forms.NumberInput(attrs={'class': 'form-control'}), + initial=0) + """Score field""" diff --git a/judge/handler.py b/judge/handler.py index e0ff4b3..1c5bc25 100644 --- a/judge/handler.py +++ b/judge/handler.py @@ -1,4 +1,5 @@ import os +import pickle from re import compile from io import StringIO @@ -8,7 +9,6 @@ from datetime import timedelta, datetime from traceback import print_exc from csv import writer as csvwriter -from pickle import load as pickle_load from typing import Tuple, Optional, Dict, Any, List from django.utils import timezone @@ -16,7 +16,8 @@ def process_contest(name: str, start_datetime: datetime, soft_end_datetime: datetime, - hard_end_datetime: datetime, penalty: float, public: bool) -> Tuple[bool, str]: + hard_end_datetime: datetime, penalty: float, public: bool, + enable_linter_score: bool, enable_poster_score: bool) -> Tuple[bool, str]: """ Process a New Contest Only penalty can be None in which case Penalty will be set to 0 @@ -28,7 +29,9 @@ def process_contest(name: str, start_datetime: datetime, soft_end_datetime: date c = models.Contest(name=name, start_datetime=start_datetime, soft_end_datetime=soft_end_datetime, hard_end_datetime=hard_end_datetime, - penalty=penalty, public=public) + penalty=penalty, public=public, + enable_linter_score=enable_linter_score, + enable_poster_score=enable_poster_score) c.save() # Successfully added to Database return (True, str(c.pk)) @@ -320,6 +323,40 @@ def process_solution(problem_id: str, participant: str, file_type, return (True, None) +def update_poster_score(submission_id: str, new_score: int): + """ + Updates the poster score (tascore) for a submission. + Input pk of submission and the new poster score. + Leaderboard is updated if the new score for the person-problem pair has changed. + + Returns: + (True, None) or (False, Exception string) + """ + try: + submission = models.Submission.objects.get(pk=submission_id) + submission.final_score -= submission.ta_score + submission.ta_score = new_score + submission.final_score += submission.ta_score + submission.save() + + highest_scoring_submission = models.Submission.objects.filter( + problem=submission.problem.pk, participant=submission.participant.pk).\ + order_by('-final_score').first() + ppf, _ = models.PersonProblemFinalScore.objects.get_or_create( + person=submission.participant, problem=submission.problem) + old_highscore = ppf.score + ppf.score = highest_scoring_submission.final_score + ppf.save() + + if old_highscore != ppf.score: + # Update the leaderboard only if submission imporved the final score + update_leaderboard(submission.problem.contest.pk, + submission.participant.email) + return (True, None) + except Exception as e: + return (False, e.__str__()) + + def add_person_to_contest(person: str, contest: str, permission: bool) -> Tuple[bool, Optional[str]]: """ @@ -448,7 +485,7 @@ def get_personcontest_permission(person: Optional[str], contest: int) -> Optiona return None p = models.Person.objects.get(email=person) c = models.Contest.objects.get(pk=contest) - # partcipant and Current datetime < C.date_time -> None + # participant and Current datetime < C.date_time -> None try: cp = models.ContestPerson.objects.get(person=p, contest=c) if cp.role is False and curr < c.start_datetime: @@ -715,13 +752,51 @@ def get_leaderboard(contest: int) -> Tuple[bool, Any]: return (False, 'Leaderboard not yet initialized for this contest.') try: with open(leaderboard_path, 'rb') as f: - data = pickle_load(f) + data = pickle.load(f) return (True, data) except Exception as e: print_exc() return (False, e.__str__()) +def update_leaderboard(contest: int, person: str): + """ + Updates the leaderboard for the passed contest for the rank of the person + Pass pk for contest and email for person + Only call this function when some submission for some problem of the contest + has scored more than its previous submission. + Remember to call this function whenever PersonProblemFinalScore is updated. + Returns True if update was successfull else returns False + """ + + os.makedirs(os.path.join('content', 'contests'), exist_ok=True) + pickle_path = os.path.join('content', 'contests', str(contest) + '.lb') + + status, score = get_personcontest_score(person, contest) + + if status: + if not os.path.exists(pickle_path): + with open(pickle_path, 'wb') as f: + data = [[person, score]] + pickle.dump(data, f) + return True + else: + with open(pickle_path, 'rb') as f: + data = pickle.load(f) + with open(pickle_path, 'wb') as f: + for i in range(len(data)): + if data[i][0] == person: + data[i][1] = score + break + else: + data.append([person, score]) + data = sorted(data, key=lambda x: x[1]) + pickle.dump(data, f) + return True + else: + return False + + def process_comment(problem: str, person: str, commenter: str, timestamp, comment: str) -> Tuple[bool, Optional[str]]: """ diff --git a/judge/leaderboard.py b/judge/leaderboard.py deleted file mode 100644 index e178507..0000000 --- a/judge/leaderboard.py +++ /dev/null @@ -1,48 +0,0 @@ -import os -import pickle - -from .handler import get_personcontest_score - - -def update_leaderboard(contest: int, person: str): - """ - Updates the leaderboard for the passed contest for the rank of the person - Pass pk for contest and email for person - Only call this function when some submission for some problem of the contest - has scored more than its previous submission. - Remember to call this function whenever PersonProblemFinalScore is updated. - Returns True if update was successfull else returns False - """ - - os.makedirs(os.path.join('content', 'contests'), exist_ok=True) - pickle_path = os.path.join('content', 'contests', str(contest) + '.lb') - - status, score = get_personcontest_score(person, contest) - - if status: - if not os.path.exists(pickle_path): - with open(pickle_path, 'wb') as f: - data = [[person, score]] - pickle.dump(data, f) - return True - else: - with open(pickle_path, 'rb') as f: - data = pickle.load(f) - with open(pickle_path, 'wb') as f: - for i in range(len(data)): - if data[i][0] == person: - data[i][1] = score - pos = i - break - else: - data.append([person, score]) - pos = len(data) - 1 - for i in range(pos, 0, -1): - if data[i][1] > data[i-1][1]: - data[i], data[i-1] = data[i-1], data[i] - else: - break - pickle.dump(data, f) - return True - else: - return False diff --git a/judge/migrations/0001_initial.py b/judge/migrations/0001_initial.py index 5bdec72..b944773 100644 --- a/judge/migrations/0001_initial.py +++ b/judge/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2 on 2019-04-27 16:18 +# Generated by Django 2.2 on 2019-05-01 06:08 import datetime from django.db import migrations, models @@ -26,7 +26,9 @@ class Migration(migrations.Migration): ('soft_end_datetime', models.DateTimeField()), ('hard_end_datetime', models.DateTimeField()), ('penalty', models.FloatField(default=0.0)), - ('public', models.BooleanField()), + ('public', models.BooleanField(default=False)), + ('enable_linter_score', models.BooleanField(default=True)), + ('enable_poster_score', models.BooleanField(default=True)), ], ), migrations.CreateModel( @@ -65,8 +67,8 @@ class Migration(migrations.Migration): ('timestamp', models.DateTimeField()), ('judge_score', models.PositiveSmallIntegerField(default=0)), ('ta_score', models.PositiveSmallIntegerField(default=0)), - ('final_score', models.FloatField(default=0.0)), ('linter_score', models.FloatField(default=0.0)), + ('final_score', models.FloatField(default=0.0)), ('participant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.Person')), ('problem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.Problem')), ], diff --git a/judge/models.py b/judge/models.py index f7585c8..d03f695 100644 --- a/judge/models.py +++ b/judge/models.py @@ -61,9 +61,15 @@ class Contest(models.Model): penalty = models.FloatField(default=0.0) """Penalty for late-submission""" - public = models.BooleanField() + public = models.BooleanField(default=False) """Is the contest public?""" + enable_linter_score = models.BooleanField(default=True) + """Enable linter scoring""" + + enable_poster_score = models.BooleanField(default=True) + """Enable poster scoring""" + def __str__(self): return self.name @@ -179,7 +185,7 @@ class Submission(models.Model): judge_score = models.PositiveSmallIntegerField(default=0) """Judge score""" - ta_score = models.PositiveSmallIntegerField(default=0) + ta_score = models.SmallIntegerField(default=0) """TA score""" linter_score = models.FloatField(default=0.0) diff --git a/judge/templates/judge/contest_detail.html b/judge/templates/judge/contest_detail.html index 25ac45f..c0886c8 100644 --- a/judge/templates/judge/contest_detail.html +++ b/judge/templates/judge/contest_detail.html @@ -26,7 +26,7 @@