Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
vCra committed Apr 13, 2019
1 parent 1b4208a commit 7d547d9
Show file tree
Hide file tree
Showing 45 changed files with 1,007 additions and 168 deletions.
3 changes: 2 additions & 1 deletion config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,4 +229,5 @@
#


OPEN_URLS = ["/admin/login/"]
OPEN_URLS = ["/accounts/login/"]
STUDENT_VIEWS = ["workshop_student_waiting", "problem_student", "workshop_auth"]
5 changes: 4 additions & 1 deletion config/settings/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,7 @@
'ENGINE': 'django.db.backends.sqlite3',
'NAME': 'testdb.db',
}
}
}
ALLOWED_HOSTS = ["localhost"]

JUDGE0_ENDPOINT = "https://api.judge0.com"
2 changes: 1 addition & 1 deletion manage.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@
current_path = os.path.dirname(os.path.abspath(__file__))
sys.path.append(os.path.join(current_path, "programdom"))

execute_from_command_line(sys.argv)
execute_from_command_line(sys.argv)
1 change: 1 addition & 0 deletions programdom/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from .models import *

admin.site.register(Problem)
admin.site.register(ProblemTest)
admin.site.register(Workshop)
admin.site.register(Submission)
admin.site.register(SubmissionTestResult)
Expand Down
1 change: 1 addition & 0 deletions programdom/api/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

router = routers.DefaultRouter()
router.register(r'submissions', views.SubmissionView)
router.register(r'problems', views.ProblemView)


urlpatterns = [
Expand Down
10 changes: 7 additions & 3 deletions programdom/api/serializers.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
from rest_framework import serializers

from programdom.models import Submission
from programdom.models import Submission, Problem


class SubmissionSerializer(serializers.ModelSerializer):
class Meta:
model = Submission
fields = ('id', 'problem', 'user', 'code', 'options')
fields = ('id', 'problem', 'user', 'code', 'workshop', 'options')


class ProblemSerializer(serializers.ModelSerializer):
class Meta:
model = Problem
fields = ('id', 'title', 'skeleton', 'language')
14 changes: 8 additions & 6 deletions programdom/api/views.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from rest_framework import viewsets, mixins
from rest_framework.generics import CreateAPIView
from rest_framework.views import APIView

from programdom.api.serializers import SubmissionSerializer
from programdom.models import Submission
from rest_framework import viewsets
from .serializers import SubmissionSerializer, ProblemSerializer
from programdom.models import Submission, Problem


class SubmissionView(viewsets.ModelViewSet):
queryset = Submission.objects.all()
serializer_class = SubmissionSerializer


class ProblemView(viewsets.ModelViewSet):
queryset = Problem.objects.all()
serializer_class = ProblemSerializer
4 changes: 4 additions & 0 deletions programdom/bridge/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@

client = api.Client(settings.JUDGE0_ENDPOINT)

# Do not wait for the program to
client.wait = False

# This was needed with mooshak, but is no longer, as Judge0 sessions do not expire.

# """
# Use asyncio rather than threads, in order to reduce complexity
# """
Expand Down
110 changes: 56 additions & 54 deletions programdom/bridge/consumers.py
Original file line number Diff line number Diff line change
@@ -1,83 +1,85 @@
import asyncio
import os
from pprint import pprint
import logging

import aiofiles as aiofiles
from asgiref.sync import sync_to_async
from channels.consumer import AsyncConsumer
from channels.db import database_sync_to_async
from django.core.cache import cache

from channels.layers import get_channel_layer
from django.conf import settings
import judge0api as api
from programdom.bridge.client import client
from programdom.models import Problem, SubmissionTestResult
from programdom.models import SubmissionTestResult, Submission

channel_layer = get_channel_layer()

class ProgramdomBridgeConsumer(AsyncConsumer):
logger = logging.getLogger(__name__)


class ProgramdomBridgeConsumer(AsyncConsumer):
submission = None
problem = None
workshop_id = None
session_id = None
channel_name = None
file = None

async def evaluate(self, message):
"""
Sends a message to judge0api, and then processes the result
:param message: a dict containing the following:
message = {
"type": "solution.evaluate", # To match to this method
"problem_id": The Mooshak ID of the problem this solution is for
"code_url": The URL of the code for this submission
"submission_id": the PK of the submission object
"session_id": The clients session_id - used to send messages back to the user
"workshop_id": the PK of the workshop associated with this submission - used for stats tracking
}
"""

self.problem = Problem.objects.get(id=message["problem_id"])
self.workshop_id = message["workshop_id"]
self.submission = await database_sync_to_async(Submission.objects.get)(pk=message["submission_id"])
self.problem = self.submission.problem
self.workshop_id = self.submission.workshop.id
self.session_id = message["session_id"]
self.channel_name = cache.get(f"session_{self.session_id}_channel-name")

url = message["code_url"]


# Our old system downloaded files and then sent them off. We will probably go back to this once in prod and have
# an actual object storage system working. However, ATM we can just use the local files.

# async with aiohttp.ClientSession() as session:
# # TODO: Check file is accessable (aka http 200)
# async with session.get(f"{url}") as response:
# data = await response.read()

# TODO: This is horrible, and should be changed
async with aiofiles.open(str(settings.APPS_DIR(url[1:])), mode='rb') as f:
self.file = await f.read()

for test in self.problem.problemtest_set.all():
submission = sync_to_async(api.submission.submit)(client, self.file, self.problem.language.judge_zero_id, stdin=test.std_in, expected_output=test.std_out)

client_result = dict(vars(submission))

await self.handle_state(client_result)

test_result = SubmissionTestResult()

client_result.update({"type": "submission.status"})
client_result.update({"test": test})


async def handle_state(self, message):
# Sends a message to the end user saying what is happening
await channel_layer.send(self.channel_name, message)
# Update the lecturers graph
await channel_layer.group_send(f"workshop_{self.workshop_id}_control", {"type": "graph.update"})

def submit_allowed(self):
"""
Checks if the current session is able to submit, by sesing if their answer has been approved
"""
return True

self.channel_name = await sync_to_async(cache.get)(f"session_{self.session_id}_channel-name")

logger.debug(f"Evaluating {self.submission}")

with await sync_to_async(open)(self.submission.code.path, 'rb') as f:
source_code = await sync_to_async(f.read)()

loop = asyncio.get_event_loop()
for test in await database_sync_to_async(self.problem.problemtest_set.all)():
loop.create_task(self.test_submit(source_code, test))

async def test_submit(self, source_code, test):
submission = await sync_to_async(api.submission.submit)(
client,
source_code,
self.problem.language.judge_zero_id,
stdin=test.std_in.encode(),
expected_output=test.std_out.encode()
)
await sync_to_async(submission.load)(client)
logger.debug(f"Running test {test} for {self.submission}")

# TODO: Cleanup
test_result = await database_sync_to_async(SubmissionTestResult)(submission=self.submission, test=test, result_data=dict(submission))
await database_sync_to_async(test_result.save)()
await sync_to_async(test_result.send_user_status)(self.channel_name)

# If we don't have an actual result yet, then
if test_result.result_data["status"]["id"] in [1, 2]:
await self.test_schedule(test_result)

async def test_reload(self, test):
data = await sync_to_async(api.submission.get)(client, test.result_data["token"])
if data.status["id"] != test.result_data["status"]["id"]:
test.result_data.update(**dict(data))
await database_sync_to_async(test.save)()
await sync_to_async(test.send_user_status)(self.channel_name)
if data.status["id"] in [2]:
await self.test_schedule(test)
else:
await self.test_schedule(test)

async def test_schedule(self, test):
await asyncio.sleep(0.5)
await self.test_reload(test)
20 changes: 0 additions & 20 deletions programdom/conftest.py

This file was deleted.

11 changes: 11 additions & 0 deletions programdom/fixtures/problem_tests.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[
{
"model": "programdom.ProblemTest",
"fields": {
"name": "New Test",
"std_in": "Bob",
"std_out": "Hello Bob",
"problem": 1
}
}
]
10 changes: 10 additions & 0 deletions programdom/fixtures/problems.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[
{
"model": "programdom.Problem",
"fields": {
"title": "Test Problem",
"skeleton": "print('this is a test')",
"language": 3
}
}
]
8 changes: 8 additions & 0 deletions programdom/fixtures/workshops.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[
{
"model": "programdom.Workshop",
"fields": {
"title": "Test Workshop"
}
}
]
10 changes: 9 additions & 1 deletion programdom/middleware/login_required.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
from django.http import HttpResponseForbidden
from django.shortcuts import redirect
from django.conf import settings
from django.urls import resolve


class LoginRequiredMiddleware:
def __init__(self, get_response):
self.get_response = get_response
self.login_url = settings.LOGIN_URL
self.student_views = settings.STUDENT_VIEWS
self.open_urls = [self.login_url] + \
getattr(settings, 'OPEN_URLS', [])

def __call__(self, request):
if not (request.user.is_authenticated or request.session.get("current_workshop_id")) and not request.path_info in self.open_urls:
return redirect(self.login_url+'?next='+request.path)
else:
if request.session.get("current_workshop_id") and not self._is_student_url(request.path_info):
return HttpResponseForbidden()
return self.get_response(request)

return self.get_response(request)
def _is_student_url(self, path):
return resolve(path).view_name in self.student_views
9 changes: 7 additions & 2 deletions programdom/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 2.1.7 on 2019-04-02 13:37
# Generated by Django 2.1.7 on 2019-04-02 18:53

from django.conf import settings
import django.contrib.postgres.fields.jsonb
Expand Down Expand Up @@ -50,7 +50,7 @@ class Migration(migrations.Migration):
('code', models.FileField(upload_to='')),
('options', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict)),
('problem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='programdom.Problem')),
('user', models.ForeignKey(blank=-2, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
Expand All @@ -71,6 +71,11 @@ class Migration(migrations.Migration):
('problems', models.ManyToManyField(blank=True, to='programdom.Problem')),
],
),
migrations.AddField(
model_name='submission',
name='workshop',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='programdom.Workshop'),
),
migrations.AddField(
model_name='problem',
name='language',
Expand Down
18 changes: 18 additions & 0 deletions programdom/migrations/0002_problemtest_name.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 2.1.7 on 2019-04-07 15:06

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('programdom', '0001_initial'),
]

operations = [
migrations.AddField(
model_name='problemtest',
name='name',
field=models.CharField(default='New Test', max_length=100),
),
]
20 changes: 20 additions & 0 deletions programdom/migrations/0003_submission_date.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 2.1.7 on 2019-04-08 15:05

from django.db import migrations, models
import django.utils.timezone


class Migration(migrations.Migration):

dependencies = [
('programdom', '0002_problemtest_name'),
]

operations = [
migrations.AddField(
model_name='submission',
name='date',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
]
Loading

0 comments on commit 7d547d9

Please sign in to comment.