Skip to content

Commit

Permalink
Add Celery task for running MOSS; #1091 (#1118)
Browse files Browse the repository at this point in the history
  • Loading branch information
Ninjaclasher authored and Xyene committed Oct 20, 2019
1 parent 86ac77e commit 0ebbd6d
Show file tree
Hide file tree
Showing 9 changed files with 295 additions and 9 deletions.
2 changes: 2 additions & 0 deletions dmoj/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,8 @@ def paged_list_view(view, name):

url(r'^contest/(?P<contest>\w+)', include([
url(r'^$', contests.ContestDetail.as_view(), name='contest_view'),
url(r'^/moss$', contests.ContestMossView.as_view(), name='contest_moss'),
url(r'^/moss/delete$', contests.ContestMossDelete.as_view(), name='contest_moss_delete'),
url(r'^/clone$', contests.ContestClone.as_view(), name='contest_clone'),
url(r'^/ranking/$', contests.ContestRanking.as_view(), name='contest_ranking'),
url(r'^/ranking/ajax$', contests.contest_ranking_ajax, name='contest_ranking_ajax'),
Expand Down
45 changes: 45 additions & 0 deletions judge/migrations/0093_contest_moss.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Generated by Django 2.1.12 on 2019-10-17 20:52

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('judge', '0092_contest_clone'),
]

operations = [
migrations.CreateModel(
name='ContestMoss',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('language', models.CharField(max_length=10)),
('submission_count', models.PositiveIntegerField(default=0)),
('url', models.URLField(blank=True, null=True)),
],
options={
'verbose_name': 'contest moss result',
'verbose_name_plural': 'contest moss results',
},
),
migrations.AlterModelOptions(
name='contest',
options={'permissions': (('see_private_contest', 'See private contests'), ('edit_own_contest', 'Edit own contests'), ('edit_all_contest', 'Edit all contests'), ('clone_contest', 'Clone contest'), ('moss_contest', 'MOSS contest'), ('contest_rating', 'Rate contests'), ('contest_access_code', 'Contest access codes'), ('create_private_contest', 'Create private contests')), 'verbose_name': 'contest', 'verbose_name_plural': 'contests'},
),
migrations.AddField(
model_name='contestmoss',
name='contest',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='moss', to='judge.Contest', verbose_name='contest'),
),
migrations.AddField(
model_name='contestmoss',
name='problem',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='moss', to='judge.Problem', verbose_name='problem'),
),
migrations.AlterUniqueTogether(
name='contestmoss',
unique_together={('contest', 'problem', 'language')},
),
]
3 changes: 2 additions & 1 deletion judge/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

from judge.models.choices import ACE_THEMES, EFFECTIVE_MATH_ENGINES, MATH_ENGINES_CHOICES, TIMEZONE
from judge.models.comment import Comment, CommentLock, CommentVote
from judge.models.contest import Contest, ContestParticipation, ContestProblem, ContestSubmission, ContestTag, Rating
from judge.models.contest import Contest, ContestMoss, ContestParticipation, ContestProblem, ContestSubmission, \
ContestTag, Rating
from judge.models.interface import BlogPost, MiscConfig, NavigationBar, validate_regex
from judge.models.message import PrivateMessage, PrivateMessageThread
from judge.models.problem import LanguageLimit, License, Problem, ProblemClarification, ProblemGroup, \
Expand Down
34 changes: 32 additions & 2 deletions judge/models/contest.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from django.utils.functional import cached_property
from django.utils.translation import gettext, gettext_lazy as _
from jsonfield import JSONField
from moss import MOSS_LANG_C, MOSS_LANG_CC, MOSS_LANG_JAVA, MOSS_LANG_PYTHON

from judge import contest_format
from judge.models.problem import Problem
Expand Down Expand Up @@ -186,6 +187,8 @@ def update_user_count(self):
self.user_count = self.users.filter(virtual=0).count()
self.save()

update_user_count.alters_data = True

@cached_property
def show_scoreboard(self):
if self.hide_scoreboard and not self.ended:
Expand All @@ -210,21 +213,28 @@ def is_accessible_by(self, user):
if user.has_perm('judge.see_private_contest'):
return True

# User can edit the contest
return self.is_editable_by(user)

def is_editable_by(self, user):
# If the user can edit all contests
if user.has_perm('judge.edit_all_contest'):
return True

# If the user is a contest organizer
if user.has_perm('judge.edit_own_contest') and \
self.organizers.filter(id=user.profile.id).exists():
return True

return False

update_user_count.alters_data = True

class Meta:
permissions = (
('see_private_contest', _('See private contests')),
('edit_own_contest', _('Edit own contests')),
('edit_all_contest', _('Edit all contests')),
('clone_contest', _('Clone contest')),
('moss_contest', _('MOSS contest')),
('contest_rating', _('Rate contests')),
('contest_access_code', _('Contest access codes')),
('create_private_contest', _('Create private contests')),
Expand Down Expand Up @@ -355,3 +365,23 @@ class Meta:
unique_together = ('user', 'contest')
verbose_name = _('contest rating')
verbose_name_plural = _('contest ratings')


class ContestMoss(models.Model):
LANG_MAPPING = [
('C', MOSS_LANG_C),
('C++', MOSS_LANG_CC),
('Java', MOSS_LANG_JAVA),
('Python', MOSS_LANG_PYTHON),
]

contest = models.ForeignKey(Contest, verbose_name=_('contest'), related_name='moss', on_delete=CASCADE)
problem = models.ForeignKey(Problem, verbose_name=_('problem'), related_name='moss', on_delete=CASCADE)
language = models.CharField(max_length=10)
submission_count = models.PositiveIntegerField(default=0)
url = models.URLField(null=True, blank=True)

class Meta:
unique_together = ('contest', 'problem', 'language')
verbose_name = _('contest moss result')
verbose_name_plural = _('contest moss results')
1 change: 1 addition & 0 deletions judge/tasks/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from judge.tasks.demo import *
from judge.tasks.moss import *
from judge.tasks.submission import *
57 changes: 57 additions & 0 deletions judge/tasks/moss.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from celery import shared_task
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.utils.translation import gettext as _
from moss import MOSS

from judge.models import Contest, ContestMoss, ContestParticipation, Submission
from judge.utils.celery import Progress

__all__ = ('run_moss',)


@shared_task(bind=True)
def run_moss(self, contest_key):
moss_api_key = settings.MOSS_API_KEY
if moss_api_key is None:
raise ImproperlyConfigured('No MOSS API Key supplied')

contest = Contest.objects.get(key=contest_key)
ContestMoss.objects.filter(contest=contest).delete()

length = len(ContestMoss.LANG_MAPPING) * contest.problems.count()
moss_results = []

with Progress(self, length, stage=_('Running MOSS')) as p:
for problem in contest.problems.all():
for dmoj_lang, moss_lang in ContestMoss.LANG_MAPPING:
result = ContestMoss(contest=contest, problem=problem, language=dmoj_lang)

subs = Submission.objects.filter(
contest__participation__virtual__in=(ContestParticipation.LIVE, ContestParticipation.SPECTATE),
contest_object=contest,
problem=problem,
language__common_name=dmoj_lang,
).order_by('-points').values_list('user__user__username', 'source__source')

if subs.exists():
moss_call = MOSS(moss_api_key, language=moss_lang, matching_file_limit=100,
comment='%s - %s' % (contest.key, problem.code))

users = set()

for username, source in subs:
if username in users:
continue
users.add(username)
moss_call.add_file_from_memory(username, source.encode('utf-8'))

result.url = moss_call.process()
result.submission_count = len(users)

moss_results.append(result)
p.did(1)

ContestMoss.objects.bulk_create(moss_results)

return len(moss_results)
70 changes: 65 additions & 5 deletions judge/views/contests.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
from datetime import date, datetime, time, timedelta
from functools import partial
from itertools import chain
from operator import attrgetter
from operator import attrgetter, itemgetter

from django import forms
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.core.cache import cache
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
Expand All @@ -21,19 +22,22 @@
from django.utils.timezone import make_aware
from django.utils.translation import gettext as _, gettext_lazy
from django.views.generic import ListView, TemplateView
from django.views.generic.detail import BaseDetailView, DetailView
from django.views.generic.detail import BaseDetailView, DetailView, SingleObjectMixin, View

from judge import event_poster as event
from judge.comments import CommentedDetailView
from judge.forms import ContestCloneForm
from judge.models import Contest, ContestParticipation, ContestProblem, ContestTag, Problem, Profile
from judge.models import Contest, ContestMoss, ContestParticipation, ContestProblem, ContestTag, \
Problem, Profile
from judge.tasks import run_moss
from judge.utils.celery import redirect_to_task_status
from judge.utils.opengraph import generate_opengraph
from judge.utils.ranker import ranker
from judge.utils.views import DiggPaginatorMixin, SingleObjectFormView, TitleMixin, generic_message

__all__ = ['ContestList', 'ContestDetail', 'ContestRanking', 'ContestJoin', 'ContestLeave', 'ContestCalendar',
'ContestClone', 'contest_ranking_ajax', 'ContestParticipationList', 'get_contest_ranking_list',
'base_contest_ranking_list']
'ContestClone', 'ContestMossView', 'ContestMossDelete', 'contest_ranking_ajax', 'ContestParticipationList',
'get_contest_ranking_list', 'base_contest_ranking_list']


def _find_contest(request, key, private_check=True):
Expand Down Expand Up @@ -162,6 +166,7 @@ def get_context_data(self, **kwargs):
self.object.description, 'contest')
context['meta_description'] = self.object.summary or metadata[0]
context['og_image'] = self.object.og_image or metadata[1]
context['has_moss_api_key'] = settings.MOSS_API_KEY is not None

return context

Expand Down Expand Up @@ -626,6 +631,61 @@ def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs)


class ContestMossMixin(ContestMixin, PermissionRequiredMixin):
permission_required = 'judge.moss_contest'

def get_object(self, queryset=None):
contest = super().get_object(queryset)
if settings.MOSS_API_KEY is None:
raise Http404()
if not contest.is_editable_by(self.request.user):
raise Http404()
return contest


class ContestMossView(ContestMossMixin, TitleMixin, DetailView):
template_name = 'contest/moss.html'

def get_title(self):
return _('%s MOSS Results') % self.object.name

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)

problems = list(map(attrgetter('problem'), self.object.contest_problems.order_by('order')
.select_related('problem')))
languages = list(map(itemgetter(0), ContestMoss.LANG_MAPPING))

results = ContestMoss.objects.filter(contest=self.object)
moss_results = defaultdict(list)
for result in results:
moss_results[result.problem].append(result)

for result_list in moss_results.values():
result_list.sort(key=lambda x: languages.index(x.language))

context['languages'] = languages
context['has_results'] = results.exists()
context['moss_results'] = [(problem, moss_results[problem]) for problem in problems]

return context

def post(self, request, *args, **kwargs):
self.object = self.get_object()
status = run_moss.delay(self.object.key)
return redirect_to_task_status(
status, message=_('Running MOSS for %s...') % (self.object.name,),
redirect=reverse('contest_moss', args=(self.object.key,)),
)


class ContestMossDelete(ContestMossMixin, SingleObjectMixin, View):
def post(self, request, *args, **kwargs):
self.object = self.get_object()
ContestMoss.objects.filter(contest=self.object).delete()
return HttpResponseRedirect(reverse('contest_moss', args=(self.object.key,)))


class ContestTagDetailAjax(DetailView):
model = ContestTag
slug_field = slug_url_kwarg = 'name'
Expand Down
5 changes: 4 additions & 1 deletion templates/contest/contest-tabs.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
{{ make_tab('participation', 'fa-users', url('contest_participation_own', contest.key), _('Participation')) }}
{% endif %}
{% endif %}
{% if perms.judge.change_contest or is_organizer %}
{% if contest.is_editable_by(request.user) %}
{% if perms.judge.moss_contest and has_moss_api_key %}
{{ make_tab('moss', 'fa-gavel', url('contest_moss', contest.key), _('MOSS')) }}
{% endif %}
{{ make_tab('edit', 'fa-edit', url('admin:judge_contest_change', contest.id), _('Edit')) }}
{% endif %}
{% if perms.judge.clone_contest %}
Expand Down
Loading

0 comments on commit 0ebbd6d

Please sign in to comment.