diff options
-rw-r--r-- | yaksh/evaluator_tests/test_python_evaluation.py | 56 | ||||
-rw-r--r-- | yaksh/evaluator_tests/test_simple_question_types.py | 30 | ||||
-rw-r--r-- | yaksh/hook_evaluator.py | 8 | ||||
-rw-r--r-- | yaksh/migrations/0001_initial.py | 7 | ||||
-rw-r--r-- | yaksh/migrations/0002_questionpaper_fixed_question_order.py | 20 | ||||
-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.py | 96 | ||||
-rw-r--r-- | yaksh/static/yaksh/js/add_question.js | 36 | ||||
-rw-r--r-- | yaksh/static/yaksh/js/question_paper_creation.js | 14 | ||||
-rw-r--r-- | yaksh/templates/exam.html | 2 | ||||
-rw-r--r-- | yaksh/templates/yaksh/add_question.html | 1 | ||||
-rw-r--r-- | yaksh/templates/yaksh/design_questionpaper.html | 8 | ||||
-rw-r--r-- | yaksh/templates/yaksh/grade_user.html | 3 | ||||
-rw-r--r-- | yaksh/templates/yaksh/question.html | 2 | ||||
-rw-r--r-- | yaksh/test_models.py | 99 | ||||
-rw-r--r-- | yaksh/test_views.py | 2 | ||||
-rw-r--r-- | yaksh/views.py | 92 |
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} |