Skip to content

Commit

Permalink
Merge pull request #252 from adityacp/assignment_upload_check
Browse files Browse the repository at this point in the history
Assignment upload Evaluation
  • Loading branch information
prabhuramachandran authored Mar 20, 2017
2 parents 6a09816 + e0beba1 commit a522432
Show file tree
Hide file tree
Showing 10 changed files with 155 additions and 51 deletions.
56 changes: 49 additions & 7 deletions yaksh/evaluator_tests/test_python_evaluation.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ def assert_correct_output(self, expected_output, actual_output):

class PythonAssertionEvaluationTestCases(EvaluatorBaseTest):
def setUp(self):
with open('/tmp/test.txt', 'wb') as f:
self.tmp_file = os.path.join(tempfile.gettempdir(), "test.txt")
with open(self.tmp_file, 'wb') as f:
f.write('2'.encode('ascii'))
tmp_in_dir_path = tempfile.mkdtemp()
self.in_dir = tmp_in_dir_path
Expand All @@ -33,7 +34,7 @@ def setUp(self):
self.file_paths = None

def tearDown(self):
os.remove('/tmp/test.txt')
os.remove(self.tmp_file)
shutil.rmtree(self.in_dir)

def test_correct_answer(self):
Expand Down Expand Up @@ -343,7 +344,7 @@ def add(a, b):
def test_file_based_assert(self):
# Given
self.test_case_data = [{"test_case_type": "standardtestcase", "test_case": "assert(ans()=='2')", "weight": 0.0}]
self.file_paths = [('/tmp/test.txt', False)]
self.file_paths = [(self.tmp_file, False)]
user_answer = dedent("""
def ans():
with open("test.txt") as f:
Expand Down Expand Up @@ -479,12 +480,17 @@ def strchar(s):

class PythonStdIOEvaluationTestCases(EvaluatorBaseTest):
def setUp(self):
with open('/tmp/test.txt', 'wb') as f:
self.tmp_file = os.path.join(tempfile.gettempdir(), "test.txt")
with open(self.tmp_file, 'wb') as f:
f.write('2'.encode('ascii'))
self.file_paths = None
tmp_in_dir_path = tempfile.mkdtemp()
self.in_dir = tmp_in_dir_path

def teardown(self):
os.remove(self.tmp_file)
shutil.rmtree(self.in_dir)

def test_correct_answer_integer(self):
# Given
self.test_case_data = [{"test_case_type": "stdiobasedtestcase",
Expand Down Expand Up @@ -618,7 +624,7 @@ def test_file_based_answer(self):
"expected_output": "2",
"weight": 0.0
}]
self.file_paths = [('/tmp/test.txt', False)]
self.file_paths = [(self.tmp_file, False)]

user_answer = dedent("""
with open("test.txt") as f:
Expand Down Expand Up @@ -702,7 +708,8 @@ def test_unicode_literal_bug(self):
class PythonHookEvaluationTestCases(EvaluatorBaseTest):

def setUp(self):
with open('/tmp/test.txt', 'wb') as f:
self.tmp_file = os.path.join(tempfile.gettempdir(), "test.txt")
with open(self.tmp_file, 'wb') as f:
f.write('2'.encode('ascii'))
tmp_in_dir_path = tempfile.mkdtemp()
self.in_dir = tmp_in_dir_path
Expand All @@ -712,7 +719,7 @@ def setUp(self):
self.file_paths = None

def tearDown(self):
os.remove('/tmp/test.txt')
os.remove(self.tmp_file)
shutil.rmtree(self.in_dir)

def test_correct_answer(self):
Expand Down Expand Up @@ -910,6 +917,41 @@ def check_answer(user_answer):
self.assertFalse(result.get('success'))
self.assert_correct_output(self.timeout_msg, result.get('error'))

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()
8 changes: 8 additions & 0 deletions yaksh/hook_evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@
class HookEvaluator(BaseEvaluator):
def __init__(self, metadata, test_case_data):
self.files = []
self.assign_files = []

# Set metadata values
self.user_answer = metadata.get('user_answer')
self.file_paths = metadata.get('file_paths')
self.partial_grading = metadata.get('partial_grading')
self.assignment_files = metadata.get('assign_files')

# Set test case data values
self.hook_code = test_case_data.get('hook_code')
Expand All @@ -26,6 +28,8 @@ def teardown(self):
# Delete the created file.
if self.files:
delete_files(self.files)
if self.assign_files:
delete_files(self.assign_files)

def check_code(self):
""" Function evaluates user answer by running a python based hook code
Expand All @@ -47,6 +51,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)
success = False
mark_fraction = 0.0
try:
Expand Down
7 changes: 4 additions & 3 deletions yaksh/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.5 on 2017-03-14 08:33
# Generated by Django 1.9.5 on 2017-03-17 16:42
from __future__ import unicode_literals

import datetime
Expand Down Expand Up @@ -114,6 +114,7 @@ class Migration(migrations.Migration):
('active', models.BooleanField(default=True)),
('snippet', models.CharField(blank=True, max_length=256)),
('partial_grading', models.BooleanField(default=False)),
('grade_assignment_upload', models.BooleanField(default=False)),
('tags', taggit.managers.TaggableManager(blank=True, help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user', to=settings.AUTH_USER_MODEL)),
],
Expand Down Expand Up @@ -171,7 +172,7 @@ class Migration(migrations.Migration):
name='HookTestCase',
fields=[
('testcase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='yaksh.TestCase')),
('hook_code', models.TextField(default='def check_answer(user_answer):\n \'\'\' Evaluates user answer to return -\n success - Boolean, indicating if code was executed correctly\n mark_fraction - Float, indicating fraction of the\n weight to a test case\n error - String, error message if success is false\'\'\'\n success = False\n err = "Incorrect Answer" # Please make this more specific\n mark_fraction = 0.0\n\n # write your code here\n\n return success, err, mark_fraction\n\n')),
('hook_code', models.TextField(default='def check_answer(user_answer):\n \'\'\' Evaluates user answer to return -\n success - Boolean, indicating if code was executed correctly\n mark_fraction - Float, indicating fraction of the\n weight to a test case\n error - String, error message if success is false\n In case of assignment upload there will be no user answer \'\'\'\n success = False\n err = "Incorrect Answer" # Please make this more specific\n mark_fraction = 0.0\n\n # write your code here\n\n return success, err, mark_fraction\n\n')),
('weight', models.FloatField(default=1.0)),
],
bases=('yaksh.testcase',),
Expand Down Expand Up @@ -233,7 +234,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='assignmentupload',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='yaksh.Profile'),
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='answerpaper',
Expand Down
21 changes: 16 additions & 5 deletions yaksh/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@

def get_assignment_dir(instance, filename):
return os.sep.join((
instance.user.user.username, str(instance.assignmentQuestion.id), filename
instance.user.username, str(instance.assignmentQuestion.id), filename
))


Expand Down Expand Up @@ -266,7 +266,10 @@ class Question(models.Model):
# Does this question allow partial grading
partial_grading = models.BooleanField(default=False)

def consolidate_answer_data(self, user_answer):
# Check assignment upload based question
grade_assignment_upload = models.BooleanField(default=False)

def consolidate_answer_data(self, user_answer, user=None):
question_data = {}
metadata = {}
test_case_data = []
Expand All @@ -285,6 +288,13 @@ def consolidate_answer_data(self, user_answer):
if files:
metadata['file_paths'] = [(file.file.path, file.extract)
for file in files]
if self.type == "upload":
assignment_files = AssignmentUpload.objects.filter(
assignmentQuestion=self, user=user
)
if assignment_files:
metadata['assign_files'] = [(file.assignmentFile.path, False)
for file in assignment_files]
question_data['metadata'] = metadata

return json.dumps(question_data)
Expand Down Expand Up @@ -1168,7 +1178,7 @@ def validate_answer(self, user_answer, question, json_data=None):
if set(user_answer) == set(expected_answers):
result['success'] = True
result['error'] = ['Correct answer']
elif question.type == 'code':
elif question.type == 'code' or question.type == "upload":
user_dir = self.user.profile.get_user_dir()
json_result = code_server.run_code(
question.language, json_data, user_dir
Expand Down Expand Up @@ -1231,7 +1241,7 @@ def __str__(self):

###############################################################################
class AssignmentUpload(models.Model):
user = models.ForeignKey(Profile)
user = models.ForeignKey(User)
assignmentQuestion = models.ForeignKey(Question)
assignmentFile = models.FileField(upload_to=get_assignment_dir)

Expand Down Expand Up @@ -1295,7 +1305,8 @@ def check_answer(user_answer):
success - Boolean, indicating if code was executed correctly
mark_fraction - Float, indicating fraction of the
weight to a test case
error - String, error message if success is false'''
error - String, error message if success is false
In case of assignment upload there will be no user answer '''
success = False
err = "Incorrect Answer" # Please make this more specific
mark_fraction = 0.0
Expand Down
36 changes: 27 additions & 9 deletions yaksh/static/yaksh/js/add_question.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,16 +111,34 @@ function textareaformat()
});


$('#id_type').bind('focus', function(event){
var type = document.getElementById('id_type');
type.style.border = '1px solid #ccc';
});
$('#id_type').bind('focus', function(event){
var type = document.getElementById('id_type');
type.style.border = '1px solid #ccc';
});

$('#id_language').bind('focus', function(event){
var language = document.getElementById('id_language');
language.style.border = '1px solid #ccc';
});
document.getElementById('my').innerHTML = document.getElementById('id_description').value ;

$('#id_language').bind('focus', function(event){
var language = document.getElementById('id_language');
language.style.border = '1px solid #ccc';
});
document.getElementById('my').innerHTML = document.getElementById('id_description').value ;

if (document.getElementById('id_grade_assignment_upload').checked ||
document.getElementById('id_type').val() == 'upload'){
$("#id_grade_assignment_upload").prop("disabled", false);
}
else{
$("#id_grade_assignment_upload").prop("disabled", true);
}

$('#id_type').change(function() {
if ($(this).val() == "upload"){
$("#id_grade_assignment_upload").prop("disabled", false);
}
else{
$("#id_grade_assignment_upload").prop("disabled", true);
}
});
}

function autosubmit()
Expand Down
2 changes: 1 addition & 1 deletion yaksh/templates/exam.html
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
{% block main %}
{% endblock %}
</div>
{% if question.type == 'code' %}
{% if question.type == 'code' or question.type == 'upload' %}
{% if error_message %}
<div class="row" id="error_panel">
{% for error in error_message %}
Expand Down
1 change: 1 addition & 0 deletions yaksh/templates/yaksh/add_question.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<tr><td>Tags: <td>{{ qform.tags }}
<tr><td>Snippet: <td>{{ qform.snippet }}
<tr><td>Partial Grading: <td>{{ qform.partial_grading }}
<tr><td>Grade Assignment Upload:<td> {{ qform.grade_assignment_upload }}
<tr><td> File: <td> {{ fileform.file_field }}{{ fileform.file_field.errors }}
{% if uploaded_files %}<br><b>Uploaded files:</b><br>Check on delete to delete files,
extract to extract files and hide to hide files from student(if required)<br>
Expand Down
2 changes: 1 addition & 1 deletion yaksh/templates/yaksh/question.html
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ <h5><a href="{{f_name.file.url}}">{{f_name.file.name}}</a></h5>
{% endif %}
{% if question.type == "upload" %}
<p>Upload assignment file for the said question<p>
<input type=file id="assignment" name="assignment">
<input type=file id="assignment" name="assignment" multiple="">
<hr>
{% endif %}
{% if question.type == "code" %}
Expand Down
34 changes: 21 additions & 13 deletions yaksh/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,20 +57,15 @@ def setUpModule():
description='demo quiz', pass_criteria=40,
language='Python', prerequisite=quiz,
course=course, instructions="Demo Instructions")

with open('/tmp/test.txt', 'wb') as f:
tmp_file1 = os.path.join(tempfile.gettempdir(), "test.txt")
with open(tmp_file1, 'wb') as f:
f.write('2'.encode('ascii'))


def tearDownModule():
User.objects.all().delete()
Question.objects.all().delete()
Quiz.objects.all().delete()

que_id_list = ["25", "22", "24", "27"]
for que_id in que_id_list:
dir_path = os.path.join(os.getcwd(), "yaksh", "data","question_{0}".format(que_id))
if os.path.exists(dir_path):
shutil.rmtree(dir_path)

###############################################################################
class ProfileTestCases(unittest.TestCase):
Expand Down Expand Up @@ -117,7 +112,7 @@ def setUp(self):
self.question2.save()

# create a temp directory and add files for loading questions test
file_path = "/tmp/test.txt"
file_path = os.path.join(tempfile.gettempdir(), "test.txt")
self.load_tmp_path = tempfile.mkdtemp()
shutil.copy(file_path, self.load_tmp_path)
file1 = os.path.join(self.load_tmp_path, "test.txt")
Expand All @@ -126,9 +121,11 @@ def setUp(self):
self.dump_tmp_path = tempfile.mkdtemp()
shutil.copy(file_path, self.dump_tmp_path)
file2 = os.path.join(self.dump_tmp_path, "test.txt")
file = open(file2, "r")
django_file = File(file)
file = FileUpload.objects.create(file=django_file, question=self.question2)
upload_file = open(file2, "r")
django_file = File(upload_file)
file = FileUpload.objects.create(file=django_file,
question=self.question2
)

self.question1.tags.add('python', 'function')
self.assertion_testcase = StandardTestCase(question=self.question1,
Expand Down Expand Up @@ -158,6 +155,15 @@ def setUp(self):
def tearDown(self):
shutil.rmtree(self.load_tmp_path)
shutil.rmtree(self.dump_tmp_path)
uploaded_files = FileUpload.objects.all()
que_id_list = [file.question.id for file in uploaded_files]
for que_id in que_id_list:
dir_path = os.path.join(os.getcwd(), "yaksh", "data",
"question_{0}".format(que_id)
)
if os.path.exists(dir_path):
shutil.rmtree(dir_path)
uploaded_files.delete()

def test_question(self):
""" Test question """
Expand Down Expand Up @@ -214,7 +220,9 @@ def test_load_questions(self):
self.assertTrue(question_data.active)
self.assertEqual(question_data.snippet, 'def fact()')
self.assertEqual(os.path.basename(file.file.path), "test.txt")
self.assertEqual([case.get_field_value() for case in test_case], self.test_case_upload_data)
self.assertEqual([case.get_field_value() for case in test_case],
self.test_case_upload_data
)


###############################################################################
Expand Down
Loading

0 comments on commit a522432

Please sign in to comment.