summaryrefslogtreecommitdiff
path: root/yaksh
diff options
context:
space:
mode:
authormahesh2017-03-21 16:38:31 +0530
committermahesh2017-03-21 16:38:31 +0530
commit72061d5f4ceae8e848a1fb12ab245be93db75435 (patch)
tree0cee9facaa3cd8d7701eff36de044ceac960acfe /yaksh
parent977399542df00349cc87917191af343226c44596 (diff)
parent49a4cbac480f8a9e3fafcd50e6ce2fa41a5d8699 (diff)
downloadonline_test-72061d5f4ceae8e848a1fb12ab245be93db75435.tar.gz
online_test-72061d5f4ceae8e848a1fb12ab245be93db75435.tar.bz2
online_test-72061d5f4ceae8e848a1fb12ab245be93db75435.zip
rebase changes for simple question types
Diffstat (limited to 'yaksh')
-rw-r--r--yaksh/evaluator_tests/test_python_evaluation.py56
-rw-r--r--yaksh/evaluator_tests/test_simple_question_types.py30
-rw-r--r--yaksh/hook_evaluator.py8
-rw-r--r--yaksh/migrations/0001_initial.py7
-rw-r--r--yaksh/migrations/0002_questionpaper_fixed_question_order.py20
-rw-r--r--yaksh/migrations/0003_auto_20170321_0917.py (renamed from yaksh/migrations/0002_auto_20170320_1135.py)4
-rw-r--r--yaksh/models.py96
-rw-r--r--yaksh/static/yaksh/js/add_question.js36
-rw-r--r--yaksh/static/yaksh/js/question_paper_creation.js14
-rw-r--r--yaksh/templates/exam.html2
-rw-r--r--yaksh/templates/yaksh/add_question.html1
-rw-r--r--yaksh/templates/yaksh/design_questionpaper.html8
-rw-r--r--yaksh/templates/yaksh/grade_user.html3
-rw-r--r--yaksh/templates/yaksh/question.html2
-rw-r--r--yaksh/test_models.py99
-rw-r--r--yaksh/test_views.py2
-rw-r--r--yaksh/views.py92
17 files changed, 351 insertions, 129 deletions
diff --git a/yaksh/evaluator_tests/test_python_evaluation.py b/yaksh/evaluator_tests/test_python_evaluation.py
index 51f9bea..a751c40 100644
--- a/yaksh/evaluator_tests/test_python_evaluation.py
+++ b/yaksh/evaluator_tests/test_python_evaluation.py
@@ -19,7 +19,8 @@ class EvaluatorBaseTest(unittest.TestCase):
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
@@ -33,7 +34,7 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest):
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):
@@ -343,7 +344,7 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest):
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:
@@ -479,12 +480,17 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest):
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",
@@ -618,7 +624,7 @@ class PythonStdIOEvaluationTestCases(EvaluatorBaseTest):
"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:
@@ -702,7 +708,8 @@ class PythonStdIOEvaluationTestCases(EvaluatorBaseTest):
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
@@ -712,7 +719,7 @@ class PythonHookEvaluationTestCases(EvaluatorBaseTest):
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):
@@ -910,6 +917,41 @@ class PythonHookEvaluationTestCases(EvaluatorBaseTest):
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()
diff --git a/yaksh/evaluator_tests/test_simple_question_types.py b/yaksh/evaluator_tests/test_simple_question_types.py
index 1d0a1e2..fb1c220 100644
--- a/yaksh/evaluator_tests/test_simple_question_types.py
+++ b/yaksh/evaluator_tests/test_simple_question_types.py
@@ -15,10 +15,6 @@ def setUpModule():
Profile.objects.create(user=user, roll_number=1,
institute='IIT', department='Aerospace',
position='Student')
- # create 2 questions
- for i in range(101, 103):
- Question.objects.create(summary='Q%d' % (i), points=1,
- type='code', user=user)
# create a course
course = Course.objects.create(name="Python Course 100",
@@ -48,7 +44,6 @@ def setUpModule():
def tearDownModule():
User.objects.get(username="demo_user_100").delete()
-
class IntegerQuestionTestCases(unittest.TestCase):
@classmethod
def setUpClass(self):
@@ -61,7 +56,8 @@ class IntegerQuestionTestCases(unittest.TestCase):
self.user = User.objects.get(username='demo_user_100')
#Creating Question
- self.question1 = Question.objects.get(summary='Q101')
+ self.question1 = Question.objects.create(summary='int1', points=1,
+ type='code', user=self.user)
self.question1.language = 'python'
self.question1.type = "integer"
self.question1.test_case_type = 'integertestcase'
@@ -80,6 +76,10 @@ class IntegerQuestionTestCases(unittest.TestCase):
)
self.integer_based_testcase.save()
+ @classmethod
+ def tearDownClass(self):
+ self.question1.delete()
+
def test_integer_correct_answer(self):
# Given
integer_answer = 25
@@ -127,14 +127,16 @@ class StringQuestionTestCases(unittest.TestCase):
#Creating User
self.user = User.objects.get(username='demo_user_100')
#Creating Question
- self.question1 = Question.objects.get(summary='Q101')
+ self.question1 = Question.objects.create(summary='str1', points=1,
+ type='code', user=self.user)
self.question1.language = 'python'
self.question1.type = "string"
self.question1.test_case_type = 'stringtestcase'
self.question1.description = 'Write Hello, EARTH!'
self.question1.save()
- self.question2 = Question.objects.get(summary='Q102')
+ self.question2 = Question.objects.create(summary='str2', points=1,
+ type='code', user=self.user)
self.question2.language = 'python'
self.question2.type = "string"
self.question2.test_case_type = 'stringtestcase'
@@ -162,6 +164,11 @@ class StringQuestionTestCases(unittest.TestCase):
)
self.exact_string_testcase.save()
+ @classmethod
+ def tearDownClass(self):
+ self.question1.delete()
+ self.question2.delete()
+
def test_case_insensitive_string_correct_answer(self):
# Given
string_answer = "hello, earth!"
@@ -236,7 +243,8 @@ class FloatQuestionTestCases(unittest.TestCase):
#Creating User
self.user = User.objects.get(username='demo_user_100')
#Creating Question
- self.question1 = Question.objects.get(summary='Q101')
+ self.question1 = Question.objects.create(summary='flt1', points=1,
+ type='code', user=self.user)
self.question1.language = 'python'
self.question1.type = "float"
self.question1.test_case_type = 'floattestcase'
@@ -255,6 +263,10 @@ class FloatQuestionTestCases(unittest.TestCase):
)
self.float_based_testcase.save()
+ @classmethod
+ def tearDownClass(self):
+ self.question1.delete()
+
def test_float_correct_answer(self):
# Given
float_answer = 99.9
diff --git a/yaksh/hook_evaluator.py b/yaksh/hook_evaluator.py
index 2cc4578..0819ec9 100644
--- a/yaksh/hook_evaluator.py
+++ b/yaksh/hook_evaluator.py
@@ -12,11 +12,13 @@ from .grader import TimeoutException
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')
@@ -26,6 +28,8 @@ class HookEvaluator(BaseEvaluator):
# 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
@@ -47,6 +51,10 @@ class HookEvaluator(BaseEvaluator):
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:
diff --git a/yaksh/migrations/0001_initial.py b/yaksh/migrations/0001_initial.py
index 8ee8c6a..8770a72 100644
--- a/yaksh/migrations/0001_initial.py
+++ b/yaksh/migrations/0001_initial.py
@@ -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
@@ -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)),
],
@@ -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',),
@@ -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',
diff --git a/yaksh/migrations/0002_questionpaper_fixed_question_order.py b/yaksh/migrations/0002_questionpaper_fixed_question_order.py
new file mode 100644
index 0000000..3cc46ed
--- /dev/null
+++ b/yaksh/migrations/0002_questionpaper_fixed_question_order.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.5 on 2017-03-20 13:32
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('yaksh', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='questionpaper',
+ name='fixed_question_order',
+ field=models.CharField(blank=True, max_length=255),
+ ),
+ ]
diff --git a/yaksh/migrations/0002_auto_20170320_1135.py b/yaksh/migrations/0003_auto_20170321_0917.py
index 81c7572..5a575c7 100644
--- a/yaksh/migrations/0002_auto_20170320_1135.py
+++ b/yaksh/migrations/0003_auto_20170321_0917.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# Generated by Django 1.9.5 on 2017-03-20 11:35
+# Generated by Django 1.9.5 on 2017-03-21 09:17
from __future__ import unicode_literals
from django.db import migrations, models
@@ -9,7 +9,7 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
- ('yaksh', '0001_initial'),
+ ('yaksh', '0002_questionpaper_fixed_question_order'),
]
operations = [
diff --git a/yaksh/models.py b/yaksh/models.py
index be741b1..9e05af0 100644
--- a/yaksh/models.py
+++ b/yaksh/models.py
@@ -79,7 +79,7 @@ test_status = (
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
))
@@ -277,7 +277,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 = []
@@ -296,6 +299,13 @@ class Question(models.Model):
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)
@@ -642,8 +652,8 @@ class QuestionPaperManager(models.Manager):
def _create_trial_from_questionpaper(self, original_quiz_id):
"""Creates a copy of the original questionpaper"""
trial_questionpaper = self.get(quiz_id=original_quiz_id)
- trial_questions = {"fixed_questions": trial_questionpaper
- .fixed_questions.all(),
+ fixed_ques = trial_questionpaper.get_ordered_questions()
+ trial_questions = {"fixed_questions": fixed_ques,
"random_questions": trial_questionpaper
.random_questions.all()
}
@@ -658,6 +668,7 @@ class QuestionPaperManager(models.Manager):
trial_questionpaper = self.create(quiz=trial_quiz,
total_marks=10,
)
+ trial_questionpaper.fixed_question_order = ",".join(questions_list)
trial_questionpaper.fixed_questions.add(*questions_list)
return trial_questionpaper
@@ -695,6 +706,10 @@ class QuestionPaper(models.Model):
# Total marks for the question paper.
total_marks = models.FloatField(default=0.0, blank=True)
+
+ # Sequence or Order of fixed questions
+ fixed_question_order = models.CharField(max_length=255, blank=True)
+
objects = QuestionPaperManager()
def update_total_marks(self):
@@ -709,7 +724,7 @@ class QuestionPaper(models.Model):
def _get_questions_for_answerpaper(self):
""" Returns fixed and random questions for the answer paper"""
- questions = list(self.fixed_questions.filter(active=True))
+ questions = self.get_ordered_questions()
for question_set in self.random_questions.all():
questions += question_set.get_random_questions()
return questions
@@ -727,8 +742,10 @@ class QuestionPaper(models.Model):
ans_paper.question_paper = self
ans_paper.save()
questions = self._get_questions_for_answerpaper()
- ans_paper.questions.add(*questions)
- ans_paper.questions_unanswered.add(*questions)
+ for question in questions:
+ ans_paper.questions.add(question)
+ for question in questions:
+ ans_paper.questions_unanswered.add(question)
return ans_paper
def _is_questionpaper_passed(self, user):
@@ -769,9 +786,21 @@ class QuestionPaper(models.Model):
questions = Question.objects.filter(active=True,
summary="Yaksh Demo Question",
user=user)
+ q_order = [str(que.id) for que in questions]
+ question_paper.fixed_question_order = ",".join(q_order)
+ question_paper.save()
# add fixed set of questions to the question paper
- for question in questions:
- question_paper.fixed_questions.add(question)
+ question_paper.fixed_questions.add(*questions)
+
+ def get_ordered_questions(self):
+ ques = []
+ if self.fixed_question_order:
+ que_order = self.fixed_question_order.split(',')
+ for que_id in que_order:
+ ques.append(self.fixed_questions.get(id=que_id))
+ else:
+ ques = self.fixed_questions.all()
+ return ques
def __str__(self):
return "Question Paper for " + self.quiz.description
@@ -1153,6 +1182,7 @@ class AnswerPaper(models.Model):
if user_answer.strip() == expected_answer.strip():
result['success'] = True
result['error'] = ['Correct answer']
+
elif question.type == 'mcc':
expected_answers = []
for opt in question.get_test_cases(correct=True):
@@ -1162,31 +1192,38 @@ class AnswerPaper(models.Model):
result['error'] = ['Correct answer']
elif question.type == 'integer':
- expected_answer = question.get_test_case().correct
- if expected_answer == int(user_answer):
+ expected_answers = []
+ for tc in question.get_test_cases():
+ expected_answers.append(int(tc.correct))
+ if int(user_answer) in expected_answers:
result['success'] = True
result['error'] = ['Correct answer']
elif question.type == 'string':
- testcase = question.get_test_case()
- if testcase.string_check == "lower":
- if testcase.correct.lower().splitlines()\
- == user_answer.lower().splitlines():
- result['success'] = True
- result['error'] = ['Correct answer']
- else:
- if testcase.correct.splitlines()\
- == user_answer.splitlines():
- result['success'] = True
- result['error'] = ['Correct answer']
+ tc_status = []
+ for tc in question.get_test_cases():
+ if tc.string_check == "lower":
+ if tc.correct.lower().splitlines()\
+ == user_answer.lower().splitlines():
+ tc_status.append(True)
+ else:
+ if tc.correct.splitlines()\
+ == user_answer.splitlines():
+ tc_status.append(True)
+ if any(tc_status):
+ result['success'] = True
+ result['error'] = ['Correct answer']
elif question.type == 'float':
- testcase = question.get_test_case()
- if abs(testcase.correct - user_answer) <= testcase.error_margin:
- result['success'] = True
- result['error'] = ['Correct answer']
+ tc_status = []
+ for tc in question.get_test_cases():
+ if abs(tc.correct - user_answer) <= tc.error_margin:
+ tc_status.append(True)
+ if any(tc_status):
+ 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
@@ -1249,7 +1286,7 @@ class AnswerPaper(models.Model):
###############################################################################
class AssignmentUpload(models.Model):
- user = models.ForeignKey(Profile)
+ user = models.ForeignKey(User)
assignmentQuestion = models.ForeignKey(Question)
assignmentFile = models.FileField(upload_to=get_assignment_dir)
@@ -1313,7 +1350,8 @@ class HookTestCase(TestCase):
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
diff --git a/yaksh/static/yaksh/js/add_question.js b/yaksh/static/yaksh/js/add_question.js
index 8ca22eb..05752b4 100644
--- a/yaksh/static/yaksh/js/add_question.js
+++ b/yaksh/static/yaksh/js/add_question.js
@@ -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()
diff --git a/yaksh/static/yaksh/js/question_paper_creation.js b/yaksh/static/yaksh/js/question_paper_creation.js
index 898e491..430ec4b 100644
--- a/yaksh/static/yaksh/js/question_paper_creation.js
+++ b/yaksh/static/yaksh/js/question_paper_creation.js
@@ -46,4 +46,18 @@ $(document).ready(function(){
$("#random").click();
});
+ var checked_vals = [];
+ $('input:checkbox[name="questions"]').click(function() {
+ if($(this).prop("checked") == true){
+ checked_vals.push(parseInt($(this).val()));
+ }
+ else{
+ checked_vals.pop(parseInt($(this).val()));
+ }
+ });
+ $('#design_q').submit(function(eventObj) {
+ $(this).append('<input type="hidden" name="checked_ques" value='+checked_vals+'>');
+ return true;
+});
+
});//document
diff --git a/yaksh/templates/exam.html b/yaksh/templates/exam.html
index 02ff70a..a18a962 100644
--- a/yaksh/templates/exam.html
+++ b/yaksh/templates/exam.html
@@ -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 %}
diff --git a/yaksh/templates/yaksh/add_question.html b/yaksh/templates/yaksh/add_question.html
index 5a3b551..a33950a 100644
--- a/yaksh/templates/yaksh/add_question.html
+++ b/yaksh/templates/yaksh/add_question.html
@@ -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>
diff --git a/yaksh/templates/yaksh/design_questionpaper.html b/yaksh/templates/yaksh/design_questionpaper.html
index 4418c27..829e27f 100644
--- a/yaksh/templates/yaksh/design_questionpaper.html
+++ b/yaksh/templates/yaksh/design_questionpaper.html
@@ -23,7 +23,7 @@ select
{% block content %}
<input type=hidden id="url_root" value={{ URL_ROOT }}>
-<form action="{{ URL_ROOT }}/exam/manage/designquestionpaper/{{ qpaper.quiz.id }}/{{ qpaper.id }}/" method="POST">
+<form action="{{ URL_ROOT }}/exam/manage/designquestionpaper/{{ qpaper.quiz.id }}/{{ qpaper.id }}/" method="POST" id="design_q">
<input class ="btn primary small" type="submit" name="back" id="back" value="Cancel">
{% csrf_token %}
<input type=hidden name="is_active" id="is_active" value="{{ state }}">
@@ -95,8 +95,10 @@ select
{% for question in fixed_questions %}
<li>
<label>
- <input type="checkbox" name="added-questions" data-qid="{{question.id}}" value={{question.id}}>
- <span> {{ question.summary }} </span> <span> {{ question.points }} </span>
+ <input type="checkbox" name="added-questions"
+ data-qid="{{question.id}}" value={{question.id}}>
+ <span> {{ question.summary }} </span>
+ <span> {{ question.points }} </span>
</label>
</li>
{% endfor %}
diff --git a/yaksh/templates/yaksh/grade_user.html b/yaksh/templates/yaksh/grade_user.html
index 73d7060..1cb1f99 100644
--- a/yaksh/templates/yaksh/grade_user.html
+++ b/yaksh/templates/yaksh/grade_user.html
@@ -149,6 +149,9 @@ Status : <b style="color: green;"> Passed </b><br/>
<h5> <u>Correct Answer:</u></h5>
{% for testcase in question.get_test_cases %}
<strong>{{ testcase.correct|safe }}</strong>
+ {% if testcase.error_margin %}
+ <strong>{{ testcase.error_margin|safe }}</strong>
+ {% endif %}
{% endfor %}
{% else %}
diff --git a/yaksh/templates/yaksh/question.html b/yaksh/templates/yaksh/question.html
index 8eb7d4e..45a524d 100644
--- a/yaksh/templates/yaksh/question.html
+++ b/yaksh/templates/yaksh/question.html
@@ -220,7 +220,7 @@ function call_skip(url)
{% 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" %}
diff --git a/yaksh/test_models.py b/yaksh/test_models.py
index e6e4f74..f8f506a 100644
--- a/yaksh/test_models.py
+++ b/yaksh/test_models.py
@@ -57,10 +57,11 @@ def setUpModule():
description='demo quiz 2', 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()
@@ -117,7 +118,7 @@ class QuestionTestCases(unittest.TestCase):
)
# 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")
@@ -126,9 +127,11 @@ class QuestionTestCases(unittest.TestCase):
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,
@@ -158,6 +161,15 @@ class QuestionTestCases(unittest.TestCase):
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 """
@@ -214,7 +226,9 @@ class QuestionTestCases(unittest.TestCase):
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
+ )
###############################################################################
@@ -327,9 +341,13 @@ class QuestionPaperTestCases(unittest.TestCase):
shuffle_questions=True
)
+ self.question_paper.fixed_question_order = "{0}, {1}".format(
+ self.questions[3].id, self.questions[5].id
+ )
# add fixed set of questions to the question paper
self.question_paper.fixed_questions.add(self.questions[3],
- self.questions[5])
+ self.questions[5]
+ )
# create two QuestionSet for random questions
# QuestionSet 1
self.question_set_1 = QuestionSet.objects.create(marks=2,
@@ -371,14 +389,15 @@ class QuestionPaperTestCases(unittest.TestCase):
# For Trial case
self.questions_list = [self.questions[3].id, self.questions[5].id]
- trial_course = Course.objects.create_trial_course(self.user)
- trial_quiz = Quiz.objects.create_trial_quiz(trial_course, self.user)
+ self.trial_course = Course.objects.create_trial_course(self.user)
+ self.trial_quiz = Quiz.objects.create_trial_quiz(self.trial_course, self.user)
def test_questionpaper(self):
""" Test question paper"""
self.assertEqual(self.question_paper.quiz.description, 'demo quiz 1')
self.assertSequenceEqual(self.question_paper.fixed_questions.all(),
- [self.questions[3], self.questions[5]])
+ [self.questions[3], self.questions[5]]
+ )
self.assertTrue(self.question_paper.shuffle_questions)
def test_update_total_marks(self):
@@ -426,29 +445,41 @@ class QuestionPaperTestCases(unittest.TestCase):
# test can_attempt_now(self):
self.assertFalse(self.question_paper.can_attempt_now(self.user))
- def test_create_trial_paper_to_test_quiz(self):
- trial_paper = QuestionPaper.objects.create_trial_paper_to_test_quiz\
- (trial_quiz,
- self.question_paper.id
- )
- self.assertEqual(trial_paper.quiz, trial_quiz)
- self.assertEqual(trial_paper.fixed_questions.all(),
- self.question_paper.fixed_questions.all()
- )
- self.assertEqual(trial_paper.random_questions.all(),
- self.question_paper.random_questions.all()
- )
-
- def test_create_trial_paper_to_test_questions(self):
- trial_paper = QuestionPaper.objects.\
- create_trial_paper_to_test_questions(
- trial_quiz, self.questions_list
- )
- self.assertEqual(trial_paper.quiz, trial_quiz)
- self.assertEqual(self.questions_list,
- self.question_paper.fixed_questions
- .values_list("id", flat=True)
- )
+ def test_create_trial_paper_to_test_quiz(self):
+ qu_list = [str(self.questions_list[0]), str(self.questions_list[1])]
+ trial_paper = QuestionPaper.objects.create_trial_paper_to_test_quiz\
+ (self.trial_quiz,
+ self.quiz.id
+ )
+ trial_paper.random_questions.add(self.question_set_1)
+ trial_paper.random_questions.add(self.question_set_2)
+ trial_paper.fixed_question_order = ",".join(qu_list)
+ self.assertEqual(trial_paper.quiz, self.trial_quiz)
+ self.assertSequenceEqual(trial_paper.get_ordered_questions(),
+ self.question_paper.get_ordered_questions()
+ )
+ trial_paper_ran = [q_set.id for q_set in
+ trial_paper.random_questions.all()]
+ qp_ran = [q_set.id for q_set in
+ self.question_paper.random_questions.all()]
+
+ self.assertSequenceEqual(trial_paper_ran, qp_ran)
+
+ def test_create_trial_paper_to_test_questions(self):
+ qu_list = [str(self.questions_list[0]), str(self.questions_list[1])]
+ trial_paper = QuestionPaper.objects.\
+ create_trial_paper_to_test_questions(
+ self.trial_quiz, qu_list
+ )
+ self.assertEqual(trial_paper.quiz, self.trial_quiz)
+ fixed_q = self.question_paper.fixed_questions.values_list(
+ 'id', flat=True)
+ self.assertSequenceEqual(self.questions_list, fixed_q)
+
+ def test_fixed_order_questions(self):
+ fixed_ques = self.question_paper.get_ordered_questions()
+ actual_ques = [self.questions[3], self.questions[5]]
+ self.assertSequenceEqual(fixed_ques, actual_ques)
###############################################################################
diff --git a/yaksh/test_views.py b/yaksh/test_views.py
index 7757f70..aa6561a 100644
--- a/yaksh/test_views.py
+++ b/yaksh/test_views.py
@@ -1161,7 +1161,6 @@ class TestViewAnswerPaper(TestCase):
self.question_paper = QuestionPaper.objects.create(quiz=self.quiz,
total_marks=1.0)
-
self.question_paper.fixed_questions.add(self.question)
self.question_paper.save()
@@ -1446,7 +1445,6 @@ class TestGrader(TestCase):
self.question_paper = QuestionPaper.objects.create(quiz=self.quiz,
total_marks=1.0)
-
self.question_paper.fixed_questions.add(self.question)
self.question_paper.save()
diff --git a/yaksh/views.py b/yaksh/views.py
index ef24f06..bdb6f49 100644
--- a/yaksh/views.py
+++ b/yaksh/views.py
@@ -25,8 +25,9 @@ import six
# Local imports.
from yaksh.models import get_model_class, Quiz, Question, QuestionPaper, QuestionSet, Course
from yaksh.models import Profile, Answer, AnswerPaper, User, TestCase, FileUpload,\
- has_profile, StandardTestCase, McqTestCase,\
- StdIOBasedTestCase, HookTestCase, IntegerTestCase
+ has_profile, StandardTestCase, McqTestCase,\
+ StdIOBasedTestCase, HookTestCase, IntegerTestCase,\
+ FloatTestCase, StringTestCase
from yaksh.forms import UserRegisterForm, UserLoginForm, QuizForm,\
QuestionForm, RandomQuestionForm,\
QuestionFilterForm, CourseForm, ProfileForm, UploadFileForm,\
@@ -266,7 +267,7 @@ def show_all_questionpapers(request, questionpaper_id=None):
else:
qu_papers = QuestionPaper.objects.get(id=questionpaper_id)
quiz = qu_papers.quiz
- fixed_questions = qu_papers.fixed_questions.all()
+ fixed_questions = qu_papers.get_ordered_questions()
random_questions = qu_papers.random_questions.all()
context = {'quiz': quiz, 'fixed_questions': fixed_questions,
'random_questions': random_questions}
@@ -358,7 +359,8 @@ def start(request, questionpaper_id=None, attempt_num=None):
msg = 'Quiz not found, please contact your '\
'instructor/administrator.'
return complete(request, msg, attempt_num, questionpaper_id=None)
- if not quest_paper.fixed_questions.all() and not quest_paper.random_questions.all():
+ if not quest_paper.get_ordered_questions() and not \
+ quest_paper.random_questions.all():
msg = 'Quiz does not have Questions, please contact your '\
'instructor/administrator.'
return complete(request, msg, attempt_num, questionpaper_id=None)
@@ -469,31 +471,43 @@ def check(request, q_id, attempt_num=None, questionpaper_id=None):
try:
user_answer = int(request.POST.get('answer'))
except ValueError:
- msg = ["Please enter an Integer Value"]
- return show_question(request, current_question, paper, msg)
+ msg = "Please enter an Integer Value"
+ return show_question(request, current_question,
+ paper, notification=msg
+ )
elif current_question.type == 'float':
try:
user_answer = float(request.POST.get('answer'))
except ValueError:
- msg = ["Please enter a Decimal Value"]
- return show_question(request, current_question, paper, msg)
+ msg = "Please enter a Float Value"
+ return show_question(request, current_question,
+ paper, notification=msg)
elif current_question.type == 'string':
user_answer = str(request.POST.get('answer'))
elif current_question.type == 'mcc':
user_answer = request.POST.getlist('answer')
elif current_question.type == 'upload':
- assign = AssignmentUpload()
- assign.user = user.profile
- assign.assignmentQuestion = current_question
# if time-up at upload question then the form is submitted without
# validation
if 'assignment' in request.FILES:
- assign.assignmentFile = request.FILES['assignment']
- assign.save()
+ assignment_filename = request.FILES.getlist('assignment')
+ for fname in assignment_filename:
+ if AssignmentUpload.objects.filter(
+ assignmentQuestion=current_question,
+ assignmentFile__icontains=fname, user=user).exists():
+ assign_file = AssignmentUpload.objects.get(
+ assignmentQuestion=current_question,
+ assignmentFile__icontains=fname, user=user)
+ os.remove(assign_file.assignmentFile.path)
+ assign_file.delete()
+ AssignmentUpload.objects.create(user=user,
+ assignmentQuestion=current_question, assignmentFile=fname
+ )
user_answer = 'ASSIGNMENT UPLOADED'
- next_q = paper.add_completed_question(current_question.id)
- return show_question(request, next_q, paper)
+ if not current_question.grade_assignment_upload:
+ next_q = paper.add_completed_question(current_question.id)
+ return show_question(request, next_q, paper)
else:
user_code = request.POST.get('answer')
user_answer = snippet_code + "\n" + user_code if snippet_code else user_code
@@ -507,17 +521,18 @@ def check(request, q_id, attempt_num=None, questionpaper_id=None):
# If we were not skipped, we were asked to check. For any non-mcq
# questions, we obtain the results via XML-RPC with the code executed
# safely in a separate process (the code_server.py) running as nobody.
- try:
- json_data = current_question.consolidate_answer_data(user_answer) \
- if current_question.type == 'code' else None
- result = paper.validate_answer(user_answer, current_question, json_data)
- except MultipleObjectsReturned:
- msg = ["Multiple objects returned. Contact admin."]
- return show_question(request, current_question, paper, msg)
+ json_data = current_question.consolidate_answer_data(user_answer, user) \
+ if current_question.type == 'code' or \
+ current_question.type == 'upload' else None
+ result = paper.validate_answer(user_answer, current_question,
+ json_data
+ )
if result.get('success'):
new_answer.marks = (current_question.points * result['weight'] /
current_question.get_maximum_test_case_weight()) \
- if current_question.partial_grading and current_question.type == 'code' else current_question.points
+ if current_question.partial_grading and \
+ current_question.type == 'code' or current_question.type == 'upload' \
+ else current_question.points
new_answer.correct = result.get('success')
error_message = None
new_answer.error = json.dumps(result.get('error'))
@@ -525,11 +540,14 @@ def check(request, q_id, attempt_num=None, questionpaper_id=None):
else:
new_answer.marks = (current_question.points * result['weight'] /
current_question.get_maximum_test_case_weight()) \
- if current_question.partial_grading and current_question.type == 'code' else 0
+ if current_question.partial_grading and \
+ current_question.type == 'code' or current_question.type == 'upload' \
+ else 0
error_message = result.get('error') if current_question.type == 'code' \
- else None
+ or current_question.type == 'upload' else None
new_answer.error = json.dumps(result.get('error'))
next_question = current_question if current_question.type == 'code' \
+ or current_question.type == 'upload' \
else paper.add_completed_question(current_question.id)
new_answer.save()
paper.update_marks('inprogress')
@@ -859,12 +877,28 @@ def design_questionpaper(request, quiz_id, questionpaper_id=None):
state = request.POST.get('is_active', None)
if 'add-fixed' in request.POST:
- question_ids = request.POST.getlist('questions', None)
- for question in Question.objects.filter(id__in=question_ids):
- question_paper.fixed_questions.add(question)
+ question_ids = request.POST.get('checked_ques', None)
+ if question_paper.fixed_question_order:
+ ques_order = question_paper.fixed_question_order.split(",") +\
+ question_ids.split(",")
+ questions_order = ",".join(ques_order)
+ else:
+ questions_order = question_ids
+ questions = Question.objects.filter(id__in=question_ids.split(','))
+ question_paper.fixed_question_order = questions_order
+ question_paper.save()
+ question_paper.fixed_questions.add(*questions)
if 'remove-fixed' in request.POST:
question_ids = request.POST.getlist('added-questions', None)
+ que_order = question_paper.fixed_question_order.split(",")
+ for qid in question_ids:
+ que_order.remove(qid)
+ if que_order:
+ question_paper.fixed_question_order = ",".join(que_order)
+ else:
+ question_paper.fixed_question_order = ""
+ question_paper.save()
question_paper.fixed_questions.remove(*question_ids)
if 'add-random' in request.POST:
@@ -892,7 +926,7 @@ def design_questionpaper(request, quiz_id, questionpaper_id=None):
question_paper.update_total_marks()
question_paper.save()
random_sets = question_paper.random_questions.all()
- fixed_questions = question_paper.fixed_questions.all()
+ fixed_questions = question_paper.get_ordered_questions()
context = {'qpaper_form': qpaper_form, 'filter_form': filter_form, 'qpaper':
question_paper, 'questions': questions, 'fixed_questions': fixed_questions,
'state': state, 'random_sets': random_sets}