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 @@

{{ contest.name }}

{% if type == 'Poster' %} - {% if now < contest.start_datetime %}Add Problem{% endif %} Download Scores {% endif %} diff --git a/judge/templates/judge/problem_detail.html b/judge/templates/judge/problem_detail.html index 8ec41f5..019f7bc 100644 --- a/judge/templates/judge/problem_detail.html +++ b/judge/templates/judge/problem_detail.html @@ -118,7 +118,7 @@

{{ problem.name }}

- {% if now < problem.contest.start_datetime %} + {% if curr_time < problem.contest.start_datetime %}
{% csrf_token %} @@ -169,7 +169,7 @@

Output Format

Public test cases

{% for test in public_tests %}
Test Case {{ forloop.counter }}
- {% if type == 'Poster' and now < problem.contest.start_datetime %} + {% if type == 'Poster' and curr_time < problem.contest.start_datetime %}
{% csrf_token %} @@ -200,7 +200,7 @@
Output

Private test cases

{% for test in private_tests %}
Test Case {{ forloop.counter }}
- {% if now < problem.contest.start_datetime %} + {% if curr_time < problem.contest.start_datetime %}
{% csrf_token %} @@ -304,7 +304,7 @@

Submit Solution

{% if type == 'Poster' %}
- {% if now < problem.contest.start_datetime %} + {% if curr_time < problem.contest.start_datetime %}
diff --git a/judge/templates/judge/problem_submissions.html b/judge/templates/judge/problem_submissions.html index 3129459..3661a8a 100644 --- a/judge/templates/judge/problem_submissions.html +++ b/judge/templates/judge/problem_submissions.html @@ -75,11 +75,6 @@
-
-
- -
-
{% endfor %} {% endblock %} diff --git a/judge/templates/judge/submission_detail.html b/judge/templates/judge/submission_detail.html index 3f10de8..6b8f485 100644 --- a/judge/templates/judge/submission_detail.html +++ b/judge/templates/judge/submission_detail.html @@ -10,6 +10,16 @@ {% endblock %} +{% block scripts %} +{% if form.errors %} + +{% endif %} +{% endblock %} + {% block content %}
@@ -18,6 +28,56 @@ Download + {% if type == 'Poster' and problem.contest.enable_poster_score %} + + + {% endif %}
@@ -27,14 +87,18 @@ Judge Score {{ judge_score }} + {% if problem.contest.enable_poster_score %} Poster Score {{ ta_score }} + {% endif %} + {% if problem.contest.enable_linter_score %} Linter Score {{ linter_score }} + {% endif %} Final Score {{ final_score }} diff --git a/judge/tests.py b/judge/tests.py index 9813854..b67935d 100644 --- a/judge/tests.py +++ b/judge/tests.py @@ -65,7 +65,8 @@ def test_process_and_delete_contest(self): status, pk = handler.process_contest(name='Test Contest', start_datetime='2019-04-25T12:30', soft_end_datetime='2019-04-26T12:30', hard_end_datetime='2019-04-27T12:30', - penalty=0, public=True) + penalty=0, public=True, enable_linter_score=True, + enable_poster_score=True) self.assertTrue(status) c = models.Contest.objects.filter(pk=int(pk)) self.assertEqual(len(c), 1) diff --git a/judge/views.py b/judge/views.py index eb89b39..beab76d 100644 --- a/judge/views.py +++ b/judge/views.py @@ -10,7 +10,7 @@ from .models import Contest, Problem, TestCase, Submission from .forms import NewContestForm, AddPersonToContestForm, DeletePersonFromContest from .forms import NewProblemForm, EditProblemForm, NewSubmissionForm, AddTestCaseForm -from .forms import NewCommentForm +from .forms import NewCommentForm, AddPosterScoreForm from . import handler @@ -78,13 +78,15 @@ def new_contest(request): contest_soft_end = form.cleaned_data['contest_soft_end'] contest_hard_end = form.cleaned_data['contest_hard_end'] penalty = form.cleaned_data['penalty'] + enable_linter_score = form.cleaned_data['enable_linter_score'] + enable_poster_score = form.cleaned_data['enable_poster_score'] is_public = form.cleaned_data['is_public'] if penalty < 0 or penalty > 1: form.add_error('penalty', 'Penalty should be between 0 and 1.') else: status, msg = handler.process_contest( contest_name, contest_start, contest_soft_end, contest_hard_end, - penalty, is_public) + penalty, is_public, enable_linter_score, enable_poster_score) if status: handler.add_person_to_contest(user.email, msg, True) return redirect(reverse('judge:index')) @@ -147,7 +149,7 @@ def get_participants(request, contest_id): Renders the page for posters of a contest. Dispatches to get_people with role=False. """ - return get_posters(request, contest_id, False) + return get_people(request, contest_id, False) def add_person(request, contest_id, role): @@ -212,6 +214,7 @@ def contest_detail(request, contest_id): 'problems': problems, 'leaderboard_status': status, 'leaderboard': leaderboard, + 'curr_time': timezone.now(), }) @@ -359,6 +362,7 @@ def problem_detail(request, problem_id): context['private_tests'].append((input_file.file.read(), output_file.file.read(), t.pk)) input_file.close() output_file.close() + context['curr_time'] = timezone.now() return render(request, 'judge/problem_detail.html', context) @@ -578,6 +582,18 @@ def submission_detail(request, submission_id: str): if user is None: return handler404(request) if perm or user.email == submission.participant.pk: + context['type'] = 'Poster' if perm else 'Participant' + if perm and submission.problem.contest.enable_poster_score: + if request.method == 'POST': + form = AddPosterScoreForm(request.POST) + if form.is_valid(): + status, err = handler.update_poster_score(submission.pk, + form.cleaned_data['score']) + if not status: + form.add_error(None, err) + else: + form = AddPosterScoreForm(initial={'score': submission.ta_score}) + context['form'] = form status, msg = handler.get_submission_status_mini(submission_id) if status: # TODO Fix this @@ -591,6 +607,7 @@ def submission_detail(request, submission_id: str): else: logging.debug(msg) return handler404(request) + return render(request, 'judge/submission_detail.html', context) else: return handler404(request) diff --git a/submission_watcher_saver.py b/submission_watcher_saver.py index 0d751e0..d815c6e 100644 --- a/submission_watcher_saver.py +++ b/submission_watcher_saver.py @@ -1,7 +1,7 @@ import os import django -from pylint.lint import Run +from pycodestyle import Checker from datetime import timedelta from subprocess import call from typing import List, Any @@ -9,7 +9,7 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pdpjudge.settings") django.setup() -from judge import models, leaderboard # noqa: E402 +from judge import models, handler # noqa: E402 CONTENT_DIRECTORY = 'content' TMP_DIRECTORY = 'tmp' @@ -21,14 +21,10 @@ REFRESH_LS_TRIGGER = 10 -def _compute_lint_score(error_dict): - if error_dict['statement'] > 0: - high = 10.0 - penalty = (5 * error_dict['error'] + error_dict['warning']) / error_dict['statement'] - high -= 10 * penalty - return max(0.0, high) - else: - return 0.0 +def _compute_lint_score(report): + if len(report.lines) > 0: + score = 10.0 * (1 - report.total_errors / len(report.lines)) + return max(0.0, score) def saver(sub_id): @@ -73,11 +69,15 @@ def saver(sub_id): st.save() s.judge_score = score_received - if s.file_type == '.py': - penalty = Run([os.path.join(CONTENT_DIRECTORY, 'submissions', - 'submission_{}.py'.format(submission))], do_exit=False) - s.linter_score = _compute_lint_score( - penalty.linter.stats['by_module']['submission_{}'.format(submission)]) + + if s.problem.contest.enable_linter_score: + if s.file_type == '.py': + checker = Checker( + os.path.join(CONTENT_DIRECTORY, + 'submissions', 'submission_{}.py'.format(submission)), + quiet=True) + checker.check_all() + s.linter_score = _compute_lint_score(checker.report) current_final_score = s.judge_score + s.ta_score + s.linter_score penalty_multiplier = 1.0 @@ -105,10 +105,9 @@ def saver(sub_id): update_lb = True ppf.save() - if update_lb and remaining_time.days >= 0: - # Update the leaderboard only if not a late submission - # and the submission imporved the final score - leaderboard.update_leaderboard(problem.contest.pk, s.participant.email) + if update_lb: + # Update the leaderboard only if the submission imporved the final score + handler.update_leaderboard(problem.contest.pk, s.participant.email) return True