Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix assignment upload grading #833

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ def post(self, request, answerpaper_id, question_id, format=None):
answerpaper.save()
json_data = None
if question.type in ['code', 'upload']:
json_data = question.consolidate_answer_data(user_answer, user)
json_data = question.consolidate_answer_data(user_answer, user, answerpaper.id)
result = answerpaper.validate_answer(user_answer, question, json_data,
answer.id)

Expand Down
1 change: 1 addition & 0 deletions requirements/requirements-common.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ qrcode
more-itertools==8.4.0
django-storages==1.11.1
boto3==1.17.17
aiohttp==3.7.4.post0
25 changes: 25 additions & 0 deletions yaksh/code_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
# Local imports
from .settings import N_CODE_SERVERS, SERVER_POOL_PORT
from .grader import Grader
from .file_utils import delete_files, Downloader


MY_DIR = abspath(dirname(__file__))
Expand Down Expand Up @@ -91,6 +92,7 @@ def __init__(self, n, pool_port=50000):

def _make_app(self):
app = Application([
(r"/files", FileHandler),
(r"/.*", MainHandler, dict(server=self)),
])
app.listen(self.my_port)
Expand Down Expand Up @@ -188,6 +190,29 @@ def post(self):
self.write('OK')


class FileHandler(RequestHandler):

def get(self):
self.write("Submit the file urls and path to download/delete")

def post(self):
try:
files = self.get_arguments("files")
path = self.get_argument("path")
# Action is download or delete
action = self.get_argument("action")
if action == "download":
Downloader().main(files, path)
elif action == "delete":
delete_files(files, path)
else:
self.write(
"Please add action download or delete in the post data"
)
except Exception as e:
self.write(e)


def submit(url, uid, json_data, user_dir):
'''Submit a job to the code server.

Expand Down
51 changes: 6 additions & 45 deletions yaksh/evaluator_tests/test_python_evaluation.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,8 @@ def setUp(self):
"test_case": 'assert(add(-1,-2)==-3)',
'weight': 0.0, 'hidden': True},
]
self.timeout_msg = ("Code took more than {0} seconds to run. "
"You probably have an infinite loop in"
" your code.").format(SERVER_TIMEOUT)
self.timeout_msg = ("You probably have an infinite loop in"
" your code.")
self.file_paths = None

def tearDown(self):
Expand Down Expand Up @@ -650,9 +649,8 @@ def test_infinite_loop(self):
"expected_output": "3",
"weight": 0.0
}]
timeout_msg = ("Code took more than {0} seconds to run. "
"You probably have an infinite loop in"
" your code.").format(SERVER_TIMEOUT)
timeout_msg = ("You probably have an infinite loop in"
" your code.")
user_answer = "while True:\n\tpass"

kwargs = {'metadata': {
Expand Down Expand Up @@ -731,9 +729,8 @@ def setUp(self):
f.write('2'.encode('ascii'))
tmp_in_dir_path = tempfile.mkdtemp()
self.in_dir = tmp_in_dir_path
self.timeout_msg = ("Code took more than {0} seconds to run. "
"You probably have an infinite loop in"
" your code.").format(SERVER_TIMEOUT)
self.timeout_msg = ("You probably have an infinite loop in"
" your code.")
self.file_paths = None

def tearDown(self):
Expand Down Expand Up @@ -930,41 +927,5 @@ def check_answer(user_answer):
result.get("error")[0]["message"]
)

def test_assignment_upload(self):
# Given
user_answer = "Assignment Upload"
hook_code = dedent("""\
def check_answer(user_answer):
success = False
err = "Incorrect Answer"
mark_fraction = 0.0
with open("test.txt") as f:
data = f.read()
if data == '2':
success, err, mark_fraction = True, "", 1.0
return success, err, mark_fraction
"""
)

test_case_data = [{"test_case_type": "hooktestcase",
"hook_code": hook_code, "weight": 1.0
}]
kwargs = {'metadata': {
'user_answer': user_answer,
'file_paths': self.file_paths,
'assign_files': [(self.tmp_file, False)],
'partial_grading': False,
'language': 'python'},
'test_case_data': test_case_data,
}

# When
grader = Grader(self.in_dir)
result = grader.evaluate(kwargs)

# Then
self.assertTrue(result.get('success'))


if __name__ == '__main__':
unittest.main()
5 changes: 2 additions & 3 deletions yaksh/evaluator_tests/test_r_evaluation.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,8 @@ def setUp(self):
"test_case_type": "standardtestcase",
"weight": 0.0, "hidden": True
}]
self.timeout_msg = ("Code took more than {0} seconds to run. "
"You probably have an infinite loop in"
" your code.").format(SERVER_TIMEOUT)
self.timeout_msg = ("You probably have an infinite loop in"
" your code.")
self.file_paths = None

def tearDown(self):
Expand Down
29 changes: 28 additions & 1 deletion yaksh/file_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
import zipfile
import tempfile
import csv
import asyncio
import os
import aiohttp
import async_timeout


def copy_files(file_paths):
Expand All @@ -28,7 +32,7 @@ def delete_files(files, file_path=None):
if file_path:
file = os.path.join(file_path, file_name)
else:
file = file_name
file = os.path.join(os.getcwd(), file_name)
if os.path.exists(file):
if os.path.isfile(file):
os.remove(file)
Expand Down Expand Up @@ -66,3 +70,26 @@ def is_csv(document):
except (csv.Error, UnicodeDecodeError):
return False, None
return True, dialect


class Downloader:
async def get_url(self, url, path, session):
file_name = url.split("/")[-1]
if not os.path.exists(path):
os.makedirs(path)
async with async_timeout.timeout(120):
async with session.get(url) as response:
with open(os.path.join(path, file_name), 'wb') as fd:
async for data in response.content.iter_chunked(1024):
fd.write(data)
return file_name

async def download(self, urls, path):
async with aiohttp.ClientSession() as session:
tasks = [self.get_url(url, path, session) for url in urls]
return await asyncio.gather(*tasks)

def main(self, urls, download_path):
loop = asyncio.get_event_loop()
return loop.run_until_complete(self.download(urls, download_path))

8 changes: 4 additions & 4 deletions yaksh/hook_evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import psutil

# Local imports
from .file_utils import copy_files, delete_files
from .file_utils import copy_files, delete_files, Downloader
from .base_evaluator import BaseEvaluator
from .grader import TimeoutException
from .error_messages import prettify_exceptions
Expand Down Expand Up @@ -55,10 +55,10 @@ def check_code(self):
Returns (False, error_msg, 0.0): If mandatory arguments are not files
or if the required permissions are not given to the file(s).
"""
if self.file_paths:
self.files = copy_files(self.file_paths)
if self.assignment_files:
self.assign_files = copy_files(self.assignment_files)
self.assign_files = Downloader().main(
self.assignment_files, os.getcwd()
)
success = False
mark_fraction = 0.0
try:
Expand Down
62 changes: 32 additions & 30 deletions yaksh/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import pandas as pd
import qrcode
import hashlib
import urllib

# Django Imports
from django.db import models, IntegrityError
Expand All @@ -47,6 +48,7 @@
from django.db.models import Count
from django.db.models.signals import pre_delete
from django.db.models.fields.files import FieldFile
from django.core.exceptions import SuspiciousFileOperation
from django.core.files.base import ContentFile
# Local Imports
from yaksh.code_server import (
Expand Down Expand Up @@ -284,7 +286,10 @@ def file_cleanup(sender, instance, *args, **kwargs):
for field_name, _ in instance.__dict__.items():
field = getattr(instance, field_name)
if issubclass(field.__class__, FieldFile) and field.name:
field.delete(save=False)
try:
field.delete(save=False)
except SuspiciousFileOperation:
pass

###############################################################################
class CourseManager(models.Manager):
Expand Down Expand Up @@ -1432,7 +1437,7 @@ class Question(models.Model):
]
}

def consolidate_answer_data(self, user_answer, user=None, regrade=False):
def consolidate_answer_data(self, user_answer, user, paper_id):
question_data = {}
metadata = {}
test_case_data = []
Expand All @@ -1446,36 +1451,18 @@ def consolidate_answer_data(self, user_answer, user=None, regrade=False):
metadata['user_answer'] = user_answer
metadata['language'] = self.language
metadata['partial_grading'] = self.partial_grading
files = FileUpload.objects.filter(question=self)
if files:
if settings.USE_AWS:
metadata['file_paths'] = [
(file.file.url, file.extract)
for file in files
]
else:
metadata['file_paths'] = [
(self.get_file_url(file.file.url), file.extract)
for file in files
]
if self.type == "upload" and regrade:
file = AssignmentUpload.objects.only(
if self.type == "upload" and self.grade_assignment_upload:
assignments = AssignmentUpload.objects.only(
"assignmentFile").filter(
assignmentQuestion_id=self.id, answer_paper__user_id=user.id
).order_by("-id").first()
if file:
if settings.USE_AWS:
metadata['assign_files'] = [file.assignmentFile.url]
else:
metadata['assign_files'] = [
self.get_file_url(file.assignmentFile.url)
]
assignmentQuestion_id=self.id, answer_paper_id=paper_id
)
if assignments.exists():
metadata['assign_files'] = [
assignment.get_file_url() for assignment in assignments
]
question_data['metadata'] = metadata
return json.dumps(question_data)

def get_file_url(self, path):
return f'{settings.DOMAIN_HOST}{path}'

def dump_questions(self, question_ids, user):
questions = Question.objects.filter(id__in=question_ids,
user_id=user.id, active=True
Expand Down Expand Up @@ -1705,6 +1692,12 @@ def toggle_hide_status(self):
def get_filename(self):
return os.path.basename(self.file.name)

def get_file_url(self):
url = self.file.url
if not settings.USE_AWS:
url = urllib.parse.urljoin(settings.DOMAIN_HOST, url)
return url

pre_delete.connect(file_cleanup, sender=FileUpload)
###############################################################################
class Answer(models.Model):
Expand Down Expand Up @@ -2611,8 +2604,11 @@ def regrade(self, question_id, server_port=SERVER_POOL_PORT):
return (False, f'{msg} {question.type} answer submission error')
else:
answer = user_answer.answer
json_data = question.consolidate_answer_data(answer, self.user, True) \
if question.type == 'code' else None
if question.type in ['code', 'upload']:
json_data = question.consolidate_answer_data(
answer, self.user, self.id)
else:
json_data = None
result = self.validate_answer(answer, question,
json_data, user_answer.id,
server_port=server_port
Expand Down Expand Up @@ -2689,6 +2685,12 @@ class AssignmentUpload(models.Model):

objects = AssignmentUploadManager()

def get_file_url(self):
url = self.assignmentFile.url
if not settings.USE_AWS:
url = urllib.parse.urljoin(settings.DOMAIN_HOST, url)
return url

def __str__(self):
return f'Assignment File of the user {self.answer_paper.user}'

Expand Down
2 changes: 1 addition & 1 deletion yaksh/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
SERVER_HOST_NAME = config('SERVER_HOST_NAME', default='http://localhost')

# Timeout for the code to run in seconds. This is an integer!
SERVER_TIMEOUT = config('SERVER_TIMEOUT', default=4, cast=int)
SERVER_TIMEOUT = config('SERVER_TIMEOUT', default=10, cast=int)

# The root of the URL, for example you might be in the situation where you
# are not hosted as host.org/exam/ but as host.org/foo/exam/ for whatever
Expand Down
Loading