From 6aeda97b5f37fc7ce50b7fd1f1e0465c42ba0969 Mon Sep 17 00:00:00 2001 From: mahesh Date: Mon, 15 Jan 2018 17:40:06 +0530 Subject: Add shuffle testcases option to questions --- yaksh/models.py | 51 ++++++++++++++++++++++++++++++--- yaksh/templates/yaksh/add_question.html | 1 + yaksh/views.py | 2 +- 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/yaksh/models.py b/yaksh/models.py index f065190..6ba4589 100644 --- a/yaksh/models.py +++ b/yaksh/models.py @@ -815,8 +815,14 @@ class Question(models.Model): min_time = models.IntegerField("time in minutes", default=0) + #Solution for the question. solution = models.TextField(blank=True) + # Shuffle testcase order. + shuffle_testcases = models.BooleanField("Shuffle testcase for each user", + default=False + ) + def consolidate_answer_data(self, user_answer, user=None): question_data = {} metadata = {} @@ -928,6 +934,17 @@ class Question(models.Model): return test_case + def get_ordered_test_cases(self, answerpaper): + try: + order = TestcaseOrder.objects.get(answer_paper=answerpaper, + question = self + ).testcase_order.split(",") + return [self.get_test_cases(id=int(tc_id))[0]\ + for tc_id in order + ] + except TestcaseOrder.DoesNotExist: + return self.get_test_cases() + def get_maximum_test_case_weight(self, **kwargs): max_weight = 0.0 for test_case in self.get_test_cases(): @@ -1197,7 +1214,17 @@ class QuestionPaper(models.Model): ans_paper.save() questions = self._get_questions_for_answerpaper() ans_paper.questions.add(*questions) - question_ids = [str(que.id) for que in questions] + question_ids = [] + for question in questions: + question_ids.append(str(question.id)) + testcases = question.get_test_cases() + if question.shuffle_testcases: + random.shuffle(testcases) + testcases_ids = ",".join([str(tc.id) for tc in testcases]) + testcases_order = TestcaseOrder.objects.create( + answer_paper=ans_paper, + question=question, + testcase_order=testcases_ids) ans_paper.questions_order = ",".join(question_ids) ans_paper.save() ans_paper.questions_unanswered.add(*questions) @@ -1829,7 +1856,7 @@ class AnswerPaper(models.Model): .format(u.first_name, u.last_name, q.description) -################################################################################ +############################################################################## class AssignmentUploadManager(models.Manager): def get_assignments(self, qp, que_id=None, user_id=None): @@ -1851,7 +1878,7 @@ class AssignmentUploadManager(models.Manager): return assignment_files, file_name -################################################################################ +############################################################################## class AssignmentUpload(models.Model): user = models.ForeignKey(User) assignmentQuestion = models.ForeignKey(Question) @@ -1860,7 +1887,7 @@ class AssignmentUpload(models.Model): objects = AssignmentUploadManager() -############################################################################### +############################################################################## class TestCase(models.Model): question = models.ForeignKey(Question, blank=True, null=True) type = models.CharField(max_length=24, choices=test_case_types, null=True) @@ -1978,3 +2005,19 @@ class FloatTestCase(TestCase): return u'Testcase | Correct: {0} | Error Margin: +or- {1}'.format( self.correct, self.error_margin ) + + +############################################################################## +class TestcaseOrder(models.Model): + """Testcase order contains a set of ordered test cases for a given question + for each user. + """ + + # Answerpaper of the user. + answer_paper= models.ForeignKey(AnswerPaper,related_name="answer_paper") + + # Question in an answerpaper. + question = models.ForeignKey(Question) + + #Order of the test case for a question. + testcase_order = models.TextField() diff --git a/yaksh/templates/yaksh/add_question.html b/yaksh/templates/yaksh/add_question.html index ed69657..b02487c 100644 --- a/yaksh/templates/yaksh/add_question.html +++ b/yaksh/templates/yaksh/add_question.html @@ -29,6 +29,7 @@ Snippet: {{ qform.snippet }} Minimum Time(in minutes): {{ qform.min_time }} Partial Grading: {{ qform.partial_grading }} + Shuffle Testcases: {{ qform.shuffle_testcases }} Grade Assignment Upload: {{ qform.grade_assignment_upload }} File: {{ fileform.file_field }}{{ fileform.file_field.errors }} {% if uploaded_files %}
Uploaded files:
Check on delete to delete files, diff --git a/yaksh/views.py b/yaksh/views.py index 011b417..17cfb13 100644 --- a/yaksh/views.py +++ b/yaksh/views.py @@ -616,7 +616,7 @@ def show_question(request, question, paper, error_message=None, notification=Non if question.type == "code" else 'You have already attempted this question' ) - test_cases = question.get_test_cases() + test_cases = question.get_ordered_test_cases(paper) files = FileUpload.objects.filter(question_id=question.id, hide=False) course = Course.objects.get(id=course_id) module = course.learning_module.get(id=module_id) -- cgit From 98775e565c99fd6486fb5908a5ca9be491db9b1c Mon Sep 17 00:00:00 2001 From: mahesh Date: Tue, 16 Jan 2018 01:13:49 +0530 Subject: Add test case order in view_answerpaper --- yaksh/templates/yaksh/view_answerpaper.html | 5 +++-- yaksh/templatetags/custom_filters.py | 5 +++++ yaksh/views.py | 5 ++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/yaksh/templates/yaksh/view_answerpaper.html b/yaksh/templates/yaksh/view_answerpaper.html index 410b578..971ef77 100644 --- a/yaksh/templates/yaksh/view_answerpaper.html +++ b/yaksh/templates/yaksh/view_answerpaper.html @@ -34,7 +34,7 @@ Start time: {{ paper.start_time }}
End time : {{ paper.end_time }}
Percentage obtained: {{ paper.percent }}%
- {% if paper.passed == 0 %} + {% if paper.passed %} Status : Failed
{% else %} Status : Passed
@@ -55,7 +55,8 @@
Question:
{{ question.description|safe }} {% if question.type == "mcq" or question.type == "mcc" %}
Choices:
- {% for testcase in question.get_test_cases %} + {% get_ordered_testcases question paper as testcases %} + {% for testcase in testcases %} {% if testcase.correct %}
{{ forloop.counter }}. {{ testcase.options|safe }} diff --git a/yaksh/templatetags/custom_filters.py b/yaksh/templatetags/custom_filters.py index 3c2c6fd..fa0802f 100644 --- a/yaksh/templatetags/custom_filters.py +++ b/yaksh/templatetags/custom_filters.py @@ -62,3 +62,8 @@ def module_completion_percent(course, module, user): @register.simple_tag def course_completion_percent(course, user): return course.percent_completed(user) + + +@register.simple_tag +def get_ordered_testcases(question, answerpaper): + return question.get_ordered_test_cases(answerpaper) \ No newline at end of file diff --git a/yaksh/views.py b/yaksh/views.py index 17cfb13..27a07d2 100644 --- a/yaksh/views.py +++ b/yaksh/views.py @@ -616,7 +616,10 @@ def show_question(request, question, paper, error_message=None, notification=Non if question.type == "code" else 'You have already attempted this question' ) - test_cases = question.get_ordered_test_cases(paper) + if question.type in ['mcc', 'mcq']: + test_cases = question.get_ordered_test_cases(paper) + else: + test_cases = question.get_test_cases() files = FileUpload.objects.filter(question_id=question.id, hide=False) course = Course.objects.get(id=course_id) module = course.learning_module.get(id=module_id) -- cgit From 03e74a964504e98cb0f01678ce12a626581a406b Mon Sep 17 00:00:00 2001 From: mahesh Date: Fri, 19 Jan 2018 12:44:57 +0530 Subject: Add test cases for shuffle testcase --- .../evaluator_tests/test_simple_question_types.py | 203 ++++++++++++++++++--- yaksh/test_models.py | 3 +- 2 files changed, 178 insertions(+), 28 deletions(-) diff --git a/yaksh/evaluator_tests/test_simple_question_types.py b/yaksh/evaluator_tests/test_simple_question_types.py index b86a9d8..3fe27d4 100644 --- a/yaksh/evaluator_tests/test_simple_question_types.py +++ b/yaksh/evaluator_tests/test_simple_question_types.py @@ -4,49 +4,56 @@ from django.utils import timezone import pytz from yaksh.models import User, Profile, Question, Quiz, QuestionPaper,\ QuestionSet, AnswerPaper, Answer, Course, IntegerTestCase, FloatTestCase,\ - StringTestCase + StringTestCase, McqTestCase def setUpModule(): - # create user profile + # Create user profile + # Create User 1 user = User.objects.create_user(username='demo_user_100', password='demo', email='demo@test.com') + Profile.objects.create(user=user, roll_number=1, institute='IIT', department='Aerospace', position='Student') + # Create User 2 + user2 = User.objects.create_user(username='demo_user_101', + password='demo', + email='demo@test.com') - # create a course + Profile.objects.create(user=user2, roll_number=2, + institute='IIT', department='Aerospace', + position='Student') + + # Create a course course = Course.objects.create(name="Python Course 100", enrollment="Enroll Request", creator=user) - quiz = Quiz.objects.create(start_date_time=datetime(2015, 10, 9, 10, 8, 15, 0, - tzinfo=pytz.utc), - end_date_time=datetime(2199, 10, 9, 10, 8, 15, 0, - tzinfo=pytz.utc), + quiz = Quiz.objects.create(start_date_time=datetime\ + (2015, 10, 9, 10, 8, 15, 0, + tzinfo=pytz.utc), + end_date_time=datetime\ + (2199, 10, 9, 10, 8, 15, 0, + tzinfo=pytz.utc), duration=30, active=True, attempts_allowed=1, - time_between_attempts=0, description='demo quiz 100', - pass_criteria=0, + time_between_attempts=0, pass_criteria=0, + description='demo quiz 100', instructions="Demo Instructions" ) question_paper = QuestionPaper.objects.create(quiz=quiz, total_marks=1.0) - answerpaper = AnswerPaper.objects.create(user=user, user_ip='101.0.0.1', - start_time=timezone.now(), - question_paper=question_paper, - end_time=timezone.now() - +timedelta(minutes=5), - attempt_number=1, - course=course - ) - + def tearDownModule(): User.objects.get(username="demo_user_100").delete() + User.objects.get(username="demo_user_101").delete() class IntegerQuestionTestCases(unittest.TestCase): @classmethod def setUpClass(self): + # Creating Course + self.course = Course.objects.get(name="Python Course 100") # Creating Quiz self.quiz = Quiz.objects.get(description="demo quiz 100") # Creating Question paper @@ -65,9 +72,16 @@ class IntegerQuestionTestCases(unittest.TestCase): self.question1.save() #Creating answerpaper - self.answerpaper = AnswerPaper.objects.get(question_paper\ - =self.question_paper) - self.answerpaper.attempt_number = 1 + + self.answerpaper = AnswerPaper.objects.create(user=self.user, + user_ip='101.0.0.1', + start_time=timezone.now(), + question_paper=self.question_paper, + end_time=timezone.now() + +timedelta(minutes=5), + attempt_number=1, + course=self.course + ) self.answerpaper.questions.add(self.question1) self.answerpaper.save() # For question @@ -80,6 +94,7 @@ class IntegerQuestionTestCases(unittest.TestCase): @classmethod def tearDownClass(self): self.question1.delete() + self.answerpaper.delete() def test_validate_regrade_integer_correct_answer(self): # Given @@ -158,6 +173,8 @@ class IntegerQuestionTestCases(unittest.TestCase): class StringQuestionTestCases(unittest.TestCase): @classmethod def setUpClass(self): + # Creating Course + self.course = Course.objects.get(name="Python Course 100") # Creating Quiz self.quiz = Quiz.objects.get(description="demo quiz 100") # Creating Question paper @@ -182,9 +199,16 @@ class StringQuestionTestCases(unittest.TestCase): self.question2.save() #Creating answerpaper - self.answerpaper = AnswerPaper.objects.get(question_paper\ - =self.question_paper) - self.answerpaper.attempt_number = 1 + + self.answerpaper = AnswerPaper.objects.create(user=self.user, + user_ip='101.0.0.1', + start_time=timezone.now(), + question_paper=self.question_paper, + end_time=timezone.now() + +timedelta(minutes=5), + attempt_number=1, + course=self.course + ) self.answerpaper.questions.add(*[self.question1, self.question2]) self.answerpaper.save() @@ -207,6 +231,7 @@ class StringQuestionTestCases(unittest.TestCase): def tearDownClass(self): self.question1.delete() self.question2.delete() + self.answerpaper.delete() def test_validate_regrade_case_insensitive_string_correct_answer(self): # Given @@ -346,6 +371,8 @@ class StringQuestionTestCases(unittest.TestCase): class FloatQuestionTestCases(unittest.TestCase): @classmethod def setUpClass(self): + # Creating Course + self.course = Course.objects.get(name="Python Course 100") # Creating Quiz self.quiz = Quiz.objects.get(description="demo quiz 100") # Creating Question paper @@ -362,9 +389,16 @@ class FloatQuestionTestCases(unittest.TestCase): self.question1.save() #Creating answerpaper - self.answerpaper = AnswerPaper.objects.get(question_paper\ - =self.question_paper) - self.answerpaper.attempt_number = 1 + + self.answerpaper = AnswerPaper.objects.create(user=self.user, + user_ip='101.0.0.1', + start_time=timezone.now(), + question_paper=self.question_paper, + end_time=timezone.now() + +timedelta(minutes=5), + attempt_number=1, + course=self.course + ) self.answerpaper.questions.add(self.question1) self.answerpaper.save() # For question @@ -378,6 +412,7 @@ class FloatQuestionTestCases(unittest.TestCase): @classmethod def tearDownClass(self): self.question1.delete() + self.answerpaper.delete() def test_validate_regrade_float_correct_answer(self): # Given @@ -450,3 +485,117 @@ class FloatQuestionTestCases(unittest.TestCase): self.assertTrue(details[0]) self.assertEqual(self.answer.marks, 1) self.assertTrue(self.answer.correct) + +class MCQQuestionTestCases(unittest.TestCase): + @classmethod + def setUpClass(self): + #Creating User + self.user = User.objects.get(username='demo_user_100') + self.user2 = User.objects.get(username='demo_user_101') + self.user_ip = '127.0.0.1' + + #Creating Course + self.course = Course.objects.get(name="Python Course 100") + # Creating Quiz + self.quiz = Quiz.objects.get(description="demo quiz 100") + # Creating Question paper + self.question_paper = QuestionPaper.objects.get(quiz=self.quiz) + #Creating Question + self.question1 = Question.objects.create(summary='mcq1', points=1, + type='code', user=self.user, + ) + self.question1.language = 'python' + self.question1.type = "mcq" + self.question1.test_case_type = 'Mcqtestcase' + self.question1.description = 'Which option is Correct?' + self.question1.shuffle_testcases = True + self.question1.save() + + # For questions + self.mcq_based_testcase_1 = McqTestCase(question=self.question1, + options="Correct", + correct=True, + type='mcqtestcase', + ) + self.mcq_based_testcase_1.save() + + self.mcq_based_testcase_2 = McqTestCase(question=self.question1, + options="Incorrect", + correct=False, + type='mcqtestcase', + ) + self.mcq_based_testcase_2.save() + + self.mcq_based_testcase_3 = McqTestCase(question=self.question1, + options="Incorrect", + correct=False, + type='mcqtestcase', + ) + self.mcq_based_testcase_3.save() + + self.mcq_based_testcase_4 = McqTestCase(question=self.question1, + options="Incorrect", + correct=False, + type='mcqtestcase', + ) + self.mcq_based_testcase_4.save() + + self.question_paper.fixed_questions.add(self.question1) + + self.answerpaper = self.question_paper.make_answerpaper( + user=self.user, ip=self.user_ip, + attempt_num=1, + course_id=self.course.id + ) + + # Answerpaper for user 2 + self.answerpaper2 = self.question_paper.make_answerpaper( + user=self.user2, ip=self.user_ip, + attempt_num=1, + course_id=self.course.id + ) + + @classmethod + def tearDownClass(self): + self.question1.delete() + + def test_shuffle_test_cases(self): + # Given + # Answerpaper for user 1 + # When + user_testcase = self.question1.get_ordered_test_cases( + self.answerpaper + ) + order1 = [tc.id for tc in user_testcase] + user2_testcase = self.question1.get_ordered_test_cases( + self.answerpaper2 + ) + order2 = [tc.id for tc in user2_testcase] + + # Then + self.assertNotEqual(order1, order2) + + # def test_not_shuffle_test_cases(self): + # # Given + # self.question1.shuffle_testcases = False + # self.question1.save() + # answerpaper = self.question_paper.make_answerpaper( + # user=self.user, ip=self.user_ip, + # attempt_num=1, + # course_id=self.course.id + # ) + + # # Answerpaper for user 2 + # answerpaper2 = self.question_paper.make_answerpaper( + # user=self.user2, ip=self.user_ip, + # attempt_num=1, + # course_id=self.course.id + # ) + + # user_testcase = self.question1.get_ordered_test_cases(answerpaper) + # order1 = [tc.id for tc in user_testcase] + + # user2_testcase = self.question1.get_ordered_test_cases(answerpaper2) + # order2 = [tc.id for tc in user2_testcase] + # print(order2, order1) + # self.assertNotEqual(order1, order2) \ No newline at end of file diff --git a/yaksh/test_models.py b/yaksh/test_models.py index cd4279b..381a2f3 100644 --- a/yaksh/test_models.py +++ b/yaksh/test_models.py @@ -113,6 +113,7 @@ def tearDownModule(): Lesson.objects.all().delete() LearningUnit.objects.all().delete() LearningModule.objects.all().delete() + AnswerPaper.objects.all().delete() ############################################################################### @@ -848,7 +849,7 @@ class AnswerPaperTestCases(unittest.TestCase): ) self.qtn_paper_with_single_question.save() - all_questions = Question.objects.all() + all_questions = Question.objects.filter(user=self.user) self.questions = all_questions[0:3] self.start_time = timezone.now() self.end_time = self.start_time + timedelta(minutes=20) -- cgit From 1f06442fba1420e03f54604bffe8029bb098169c Mon Sep 17 00:00:00 2001 From: mahesh Date: Fri, 19 Jan 2018 13:13:25 +0530 Subject: Disable/enable shuffle testcase option based on question type --- yaksh/static/yaksh/js/add_question.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/yaksh/static/yaksh/js/add_question.js b/yaksh/static/yaksh/js/add_question.js index 346991a..579904b 100644 --- a/yaksh/static/yaksh/js/add_question.js +++ b/yaksh/static/yaksh/js/add_question.js @@ -126,8 +126,9 @@ function textareaformat() document.getElementById('my').innerHTML = document.getElementById('id_description').value ; document.getElementById('rend_solution').innerHTML = document.getElementById('id_solution').value ; + var question_type = document.getElementById('id_type').value if (document.getElementById('id_grade_assignment_upload').checked || - document.getElementById('id_type').value == 'upload'){ + question_type == 'upload'){ $("#id_grade_assignment_upload").prop("disabled", false); } else{ @@ -142,6 +143,23 @@ function textareaformat() $("#id_grade_assignment_upload").prop("disabled", true); } }); + + if (document.getElementById('id_shuffle_testcases').checked || + question_type == "mcc" || question_type == "mcq"){ + $("#id_shuffle_testcases").prop("disabled", false); + } + else{ + $("#id_shuffle_testcases").prop("disabled", true); + } + + $('#id_type').change(function() { + if ($(this).val() == "mcc" || $(this).val() == "mcq"){ + $("#id_shuffle_testcases").prop("disabled", false); + } + else{ + $("#id_shuffle_testcases").prop("disabled", true); + } + }); } function autosubmit() -- cgit From 51c8184a795942c9f85b14a8914d03ee788e5639 Mon Sep 17 00:00:00 2001 From: mahesh Date: Thu, 1 Feb 2018 15:05:47 +0530 Subject: Change TestcaseOrder to TestCaseOrder --- .../evaluator_tests/test_simple_question_types.py | 25 ----------- yaksh/models.py | 16 ++++---- yaksh/test_models.py | 48 +++++++++++++--------- 3 files changed, 37 insertions(+), 52 deletions(-) diff --git a/yaksh/evaluator_tests/test_simple_question_types.py b/yaksh/evaluator_tests/test_simple_question_types.py index 3fe27d4..d2996e4 100644 --- a/yaksh/evaluator_tests/test_simple_question_types.py +++ b/yaksh/evaluator_tests/test_simple_question_types.py @@ -574,28 +574,3 @@ class MCQQuestionTestCases(unittest.TestCase): # Then self.assertNotEqual(order1, order2) - - # def test_not_shuffle_test_cases(self): - # # Given - # self.question1.shuffle_testcases = False - # self.question1.save() - # answerpaper = self.question_paper.make_answerpaper( - # user=self.user, ip=self.user_ip, - # attempt_num=1, - # course_id=self.course.id - # ) - - # # Answerpaper for user 2 - # answerpaper2 = self.question_paper.make_answerpaper( - # user=self.user2, ip=self.user_ip, - # attempt_num=1, - # course_id=self.course.id - # ) - - # user_testcase = self.question1.get_ordered_test_cases(answerpaper) - # order1 = [tc.id for tc in user_testcase] - - # user2_testcase = self.question1.get_ordered_test_cases(answerpaper2) - # order2 = [tc.id for tc in user2_testcase] - # print(order2, order1) - # self.assertNotEqual(order1, order2) \ No newline at end of file diff --git a/yaksh/models.py b/yaksh/models.py index 6ba4589..f823cda 100644 --- a/yaksh/models.py +++ b/yaksh/models.py @@ -936,13 +936,13 @@ class Question(models.Model): def get_ordered_test_cases(self, answerpaper): try: - order = TestcaseOrder.objects.get(answer_paper=answerpaper, + order = TestCaseOrder.objects.get(answer_paper=answerpaper, question = self - ).testcase_order.split(",") + ).order.split(",") return [self.get_test_cases(id=int(tc_id))[0]\ for tc_id in order ] - except TestcaseOrder.DoesNotExist: + except TestCaseOrder.DoesNotExist: return self.get_test_cases() def get_maximum_test_case_weight(self, **kwargs): @@ -1221,10 +1221,10 @@ class QuestionPaper(models.Model): if question.shuffle_testcases: random.shuffle(testcases) testcases_ids = ",".join([str(tc.id) for tc in testcases]) - testcases_order = TestcaseOrder.objects.create( + testcases_order = TestCaseOrder.objects.create( answer_paper=ans_paper, question=question, - testcase_order=testcases_ids) + order=testcases_ids) ans_paper.questions_order = ",".join(question_ids) ans_paper.save() ans_paper.questions_unanswered.add(*questions) @@ -2008,16 +2008,16 @@ class FloatTestCase(TestCase): ############################################################################## -class TestcaseOrder(models.Model): +class TestCaseOrder(models.Model): """Testcase order contains a set of ordered test cases for a given question for each user. """ # Answerpaper of the user. - answer_paper= models.ForeignKey(AnswerPaper,related_name="answer_paper") + answer_paper = models.ForeignKey(AnswerPaper, related_name="answer_paper") # Question in an answerpaper. question = models.ForeignKey(Question) #Order of the test case for a question. - testcase_order = models.TextField() + order = models.TextField() diff --git a/yaksh/test_models.py b/yaksh/test_models.py index 381a2f3..a0ccd49 100644 --- a/yaksh/test_models.py +++ b/yaksh/test_models.py @@ -626,8 +626,9 @@ class QuestionPaperTestCases(unittest.TestCase): @classmethod def setUpClass(self): self.course = Course.objects.get(name="Python Course") + self.user= User.objects.get(username='creator') # All active questions - self.questions = Question.objects.filter(active=True) + self.questions = Question.objects.filter(active=True, user=self.user) self.quiz = Quiz.objects.get(description="demo quiz 1") # create question paper with only fixed questions @@ -849,7 +850,7 @@ class AnswerPaperTestCases(unittest.TestCase): ) self.qtn_paper_with_single_question.save() - all_questions = Question.objects.filter(user=self.user) + all_questions = Question.objects.filter(user=self.user).order_by("id") self.questions = all_questions[0:3] self.start_time = timezone.now() self.end_time = self.start_time + timedelta(minutes=20) @@ -875,6 +876,9 @@ class AnswerPaperTestCases(unittest.TestCase): self.answerpaper.attempt_number = already_attempted + 1 self.answerpaper.save() self.answerpaper.questions.add(*self.questions) + self.answerpaper.questions_order = ",".join( + [str(q.id) for q in self.questions] + ) self.answerpaper.questions_unanswered.add(*self.questions) self.answerpaper.save() # answers for the Answer Paper @@ -929,17 +933,17 @@ class AnswerPaperTestCases(unittest.TestCase): self.question1.language = 'python' self.question1.test_case_type = 'standardtestcase' - self.question1.summary = "Question1" + self.question1.summary = "Q1" self.question1.save() self.question2.language = 'python' self.question2.type = 'mcq' self.question2.test_case_type = 'mcqtestcase' - self.question2.summary = "Question2" + self.question2.summary = "Q2" self.question2.save() self.question3.language = 'python' self.question3.type = 'mcc' self.question3.test_case_type = 'mcqtestcase' - self.question3.summary = "Question3" + self.question3.summary = "Q3" self.question3.save() self.assertion_testcase = StandardTestCase( question=self.question1, @@ -1098,7 +1102,8 @@ class AnswerPaperTestCases(unittest.TestCase): details = self.answerpaper.regrade(self.question3.id) # Then - self.answer = self.answerpaper.answers.filter(question=self.question3).last() + self.answer = self.answerpaper.answers.filter( + question=self.question3).last() self.assertTrue(details[0]) self.assertEqual(self.answer.marks, 0) self.assertFalse(self.answer.correct) @@ -1238,9 +1243,9 @@ class AnswerPaperTestCases(unittest.TestCase): """ Test Answer Paper""" self.assertEqual(self.answerpaper.user.username, 'creator') self.assertEqual(self.answerpaper.user_ip, self.ip) - questions = self.answerpaper.get_questions() + questions = [q.id for q in self.answerpaper.get_questions()] num_questions = len(questions) - self.assertSequenceEqual(list(questions), list(self.questions)) + self.assertEqual(set(questions), set([q.id for q in self.questions])) self.assertEqual(num_questions, 3) self.assertEqual(self.answerpaper.question_paper, self.question_paper) self.assertEqual(self.answerpaper.start_time, self.start_time) @@ -1251,7 +1256,7 @@ class AnswerPaperTestCases(unittest.TestCase): self.assertEqual(self.answerpaper.questions_left(), 3) # Test current_question() method of Answer Paper current_question = self.answerpaper.current_question() - self.assertEqual(current_question.summary, "Question1") + self.assertEqual(current_question.summary, "Q1") # Test completed_question() method of Answer Paper question = self.answerpaper.add_completed_question(self.question1.id) @@ -1260,14 +1265,14 @@ class AnswerPaperTestCases(unittest.TestCase): # Test next_question() method of Answer Paper current_question = self.answerpaper.current_question() - self.assertEqual(current_question.summary, "Question2") + self.assertEqual(current_question.summary, "Q2") # When next_question_id = self.answerpaper.next_question(current_question.id) # Then self.assertTrue(next_question_id is not None) - self.assertEqual(next_question_id.summary, "Question3") + self.assertEqual(next_question_id.summary, "Q3") # Given, here question is already answered current_question_id = self.question1.id @@ -1277,7 +1282,7 @@ class AnswerPaperTestCases(unittest.TestCase): # Then self.assertTrue(next_question_id is not None) - self.assertEqual(next_question_id.summary, "Question2") + self.assertEqual(next_question_id.summary, "Q2") # Given, wrong question id current_question_id = 12 @@ -1287,7 +1292,7 @@ class AnswerPaperTestCases(unittest.TestCase): # Then self.assertTrue(next_question_id is not None) - self.assertEqual(next_question_id.summary, "Question1") + self.assertEqual(next_question_id.summary, "Q1") # Given, last question in the list current_question_id = self.question3.id @@ -1298,7 +1303,7 @@ class AnswerPaperTestCases(unittest.TestCase): # Then self.assertTrue(next_question_id is not None) - self.assertEqual(next_question_id.summary, "Question1") + self.assertEqual(next_question_id.summary, "Q1") # Test get_questions_answered() method # When @@ -1313,8 +1318,11 @@ class AnswerPaperTestCases(unittest.TestCase): # Then self.assertEqual(questions_unanswered.count(), 2) - self.assertSequenceEqual(questions_unanswered, - [self.questions[1], self.questions[2]]) + self.assertEqual(set([q.id for q in questions_unanswered]), + set([self.questions[1].id, + self.questions[2].id] + ) + ) # Test completed_question and next_question # When all questions are answered @@ -1326,7 +1334,7 @@ class AnswerPaperTestCases(unittest.TestCase): # Then self.assertEqual(self.answerpaper.questions_left(), 1) self.assertIsNotNone(current_question) - self.assertEqual(current_question.summary, "Question3") + self.assertEqual(current_question.summary, "Q3") # When current_question = self.answerpaper.add_completed_question( @@ -1336,7 +1344,7 @@ class AnswerPaperTestCases(unittest.TestCase): # Then self.assertEqual(self.answerpaper.questions_left(), 0) self.assertIsNotNone(current_question) - self.assertTrue(current_question == self.answerpaper.questions.all()[0]) + self.assertTrue(current_question == self.answerpaper.get_all_ordered_questions()[0]) # When next_question_id = self.answerpaper.next_question(current_question_id) @@ -1427,7 +1435,9 @@ class CourseTestCases(unittest.TestCase): self.student2 = User.objects.get(username="demo_user3") self.quiz1 = Quiz.objects.get(description='demo quiz 1') self.quiz2 = Quiz.objects.get(description='demo quiz 2') - self.questions = Question.objects.filter(active=True) + self.questions = Question.objects.filter(active=True, + user=self.creator + ) self.modules = LearningModule.objects.filter(creator=self.creator) # create courses with disabled enrollment -- cgit From 337daeef8954a1be20164c5fb27050e67597b8a2 Mon Sep 17 00:00:00 2001 From: mahesh Date: Thu, 8 Feb 2018 15:19:27 +0530 Subject: Improve test cases for TestCaseOrder model --- yaksh/evaluator_tests/test_simple_question_types.py | 15 ++++++++++++++- yaksh/models.py | 2 +- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/yaksh/evaluator_tests/test_simple_question_types.py b/yaksh/evaluator_tests/test_simple_question_types.py index d2996e4..2d991a7 100644 --- a/yaksh/evaluator_tests/test_simple_question_types.py +++ b/yaksh/evaluator_tests/test_simple_question_types.py @@ -554,6 +554,15 @@ class MCQQuestionTestCases(unittest.TestCase): attempt_num=1, course_id=self.course.id ) + self.answerpaper3 = AnswerPaper.objects.create( + user=self.user, + question_paper=self.question_paper, + course=self.course, + attempt_number=self.answerpaper.attempt_number+1, + start_time=timezone.now(), + end_time=timezone.now()+timedelta(minutes=5), + user_ip="127.0.0.1" + ) @classmethod def tearDownClass(self): @@ -571,6 +580,10 @@ class MCQQuestionTestCases(unittest.TestCase): self.answerpaper2 ) order2 = [tc.id for tc in user2_testcase] - + not_ordered_testcase = self.question1.get_ordered_test_cases( + self.answerpaper3 + ) + get_test_cases = self.question1.get_test_cases() # Then self.assertNotEqual(order1, order2) + self.assertEqual(get_test_cases, not_ordered_testcase) diff --git a/yaksh/models.py b/yaksh/models.py index f823cda..0bb1e66 100644 --- a/yaksh/models.py +++ b/yaksh/models.py @@ -939,7 +939,7 @@ class Question(models.Model): order = TestCaseOrder.objects.get(answer_paper=answerpaper, question = self ).order.split(",") - return [self.get_test_cases(id=int(tc_id))[0]\ + return [self.get_test_case(id=int(tc_id)) for tc_id in order ] except TestCaseOrder.DoesNotExist: -- cgit From f531b9ca9c088263a53aff8574e93b74945b468f Mon Sep 17 00:00:00 2001 From: mahesh Date: Fri, 16 Feb 2018 16:30:55 +0530 Subject: Add shuffle_testcases to QuestionPaper model instead of Question --- .../evaluator_tests/test_simple_question_types.py | 38 +++++++++++----------- yaksh/forms.py | 2 +- yaksh/models.py | 12 ++++--- yaksh/static/yaksh/js/add_question.js | 17 ---------- yaksh/templates/yaksh/add_question.html | 1 - yaksh/templates/yaksh/design_questionpaper.html | 8 +++-- 6 files changed, 33 insertions(+), 45 deletions(-) diff --git a/yaksh/evaluator_tests/test_simple_question_types.py b/yaksh/evaluator_tests/test_simple_question_types.py index 2d991a7..cbf2abd 100644 --- a/yaksh/evaluator_tests/test_simple_question_types.py +++ b/yaksh/evaluator_tests/test_simple_question_types.py @@ -485,7 +485,6 @@ class FloatQuestionTestCases(unittest.TestCase): self.assertTrue(details[0]) self.assertEqual(self.answer.marks, 1) self.assertTrue(self.answer.correct) - class MCQQuestionTestCases(unittest.TestCase): @classmethod def setUpClass(self): @@ -493,13 +492,15 @@ class MCQQuestionTestCases(unittest.TestCase): self.user = User.objects.get(username='demo_user_100') self.user2 = User.objects.get(username='demo_user_101') self.user_ip = '127.0.0.1' - + #Creating Course self.course = Course.objects.get(name="Python Course 100") # Creating Quiz self.quiz = Quiz.objects.get(description="demo quiz 100") # Creating Question paper self.question_paper = QuestionPaper.objects.get(quiz=self.quiz) + self.question_paper.shuffle_testcases = True + self.question_paper.save() #Creating Question self.question1 = Question.objects.create(summary='mcq1', points=1, type='code', user=self.user, @@ -508,7 +509,6 @@ class MCQQuestionTestCases(unittest.TestCase): self.question1.type = "mcq" self.question1.test_case_type = 'Mcqtestcase' self.question1.description = 'Which option is Correct?' - self.question1.shuffle_testcases = True self.question1.save() # For questions @@ -518,21 +518,21 @@ class MCQQuestionTestCases(unittest.TestCase): type='mcqtestcase', ) self.mcq_based_testcase_1.save() - + self.mcq_based_testcase_2 = McqTestCase(question=self.question1, options="Incorrect", correct=False, type='mcqtestcase', ) self.mcq_based_testcase_2.save() - + self.mcq_based_testcase_3 = McqTestCase(question=self.question1, options="Incorrect", correct=False, type='mcqtestcase', ) self.mcq_based_testcase_3.save() - + self.mcq_based_testcase_4 = McqTestCase(question=self.question1, options="Incorrect", correct=False, @@ -544,7 +544,7 @@ class MCQQuestionTestCases(unittest.TestCase): self.answerpaper = self.question_paper.make_answerpaper( user=self.user, ip=self.user_ip, - attempt_num=1, + attempt_num=1, course_id=self.course.id ) @@ -554,24 +554,16 @@ class MCQQuestionTestCases(unittest.TestCase): attempt_num=1, course_id=self.course.id ) - self.answerpaper3 = AnswerPaper.objects.create( - user=self.user, - question_paper=self.question_paper, - course=self.course, - attempt_number=self.answerpaper.attempt_number+1, - start_time=timezone.now(), - end_time=timezone.now()+timedelta(minutes=5), - user_ip="127.0.0.1" - ) - @classmethod def tearDownClass(self): self.question1.delete() + self.answerpaper.delete() + self.answerpaper2.delete() def test_shuffle_test_cases(self): # Given - # Answerpaper for user 1 # When + user_testcase = self.question1.get_ordered_test_cases( self.answerpaper ) @@ -580,10 +572,18 @@ class MCQQuestionTestCases(unittest.TestCase): self.answerpaper2 ) order2 = [tc.id for tc in user2_testcase] + self.question_paper.shuffle_testcases = False + self.question_paper.save() + answerpaper3 = self.question_paper.make_answerpaper( + user=self.user2, ip=self.user_ip, + attempt_num=self.answerpaper.attempt_number+1, + course_id=self.course.id + ) not_ordered_testcase = self.question1.get_ordered_test_cases( - self.answerpaper3 + answerpaper3 ) get_test_cases = self.question1.get_test_cases() # Then self.assertNotEqual(order1, order2) self.assertEqual(get_test_cases, not_ordered_testcase) + answerpaper3.delete() diff --git a/yaksh/forms.py b/yaksh/forms.py index 258a1ee..e53eda3 100644 --- a/yaksh/forms.py +++ b/yaksh/forms.py @@ -308,7 +308,7 @@ class UploadFileForm(forms.Form): class QuestionPaperForm(forms.ModelForm): class Meta: model = QuestionPaper - fields = ['shuffle_questions'] + fields = ['shuffle_questions', 'shuffle_testcases'] class LessonForm(forms.ModelForm): diff --git a/yaksh/models.py b/yaksh/models.py index 0bb1e66..8e0bb4d 100644 --- a/yaksh/models.py +++ b/yaksh/models.py @@ -818,10 +818,6 @@ class Question(models.Model): #Solution for the question. solution = models.TextField(blank=True) - # Shuffle testcase order. - shuffle_testcases = models.BooleanField("Shuffle testcase for each user", - default=False - ) def consolidate_answer_data(self, user_answer, user=None): question_data = {} @@ -1151,6 +1147,11 @@ class QuestionPaper(models.Model): # Sequence or Order of fixed questions fixed_question_order = models.CharField(max_length=255, blank=True) + # Shuffle testcase order. + shuffle_testcases = models.BooleanField("Shuffle testcase for each user", + default=True + ) + objects = QuestionPaperManager() def get_question_bank(self): @@ -1218,7 +1219,8 @@ class QuestionPaper(models.Model): for question in questions: question_ids.append(str(question.id)) testcases = question.get_test_cases() - if question.shuffle_testcases: + if self.shuffle_testcases and \ + question.type in ["mcq", "mcc"]: random.shuffle(testcases) testcases_ids = ",".join([str(tc.id) for tc in testcases]) testcases_order = TestCaseOrder.objects.create( diff --git a/yaksh/static/yaksh/js/add_question.js b/yaksh/static/yaksh/js/add_question.js index 579904b..0f02aab 100644 --- a/yaksh/static/yaksh/js/add_question.js +++ b/yaksh/static/yaksh/js/add_question.js @@ -143,23 +143,6 @@ function textareaformat() $("#id_grade_assignment_upload").prop("disabled", true); } }); - - if (document.getElementById('id_shuffle_testcases').checked || - question_type == "mcc" || question_type == "mcq"){ - $("#id_shuffle_testcases").prop("disabled", false); - } - else{ - $("#id_shuffle_testcases").prop("disabled", true); - } - - $('#id_type').change(function() { - if ($(this).val() == "mcc" || $(this).val() == "mcq"){ - $("#id_shuffle_testcases").prop("disabled", false); - } - else{ - $("#id_shuffle_testcases").prop("disabled", true); - } - }); } function autosubmit() diff --git a/yaksh/templates/yaksh/add_question.html b/yaksh/templates/yaksh/add_question.html index b02487c..ed69657 100644 --- a/yaksh/templates/yaksh/add_question.html +++ b/yaksh/templates/yaksh/add_question.html @@ -29,7 +29,6 @@ Snippet: {{ qform.snippet }} Minimum Time(in minutes): {{ qform.min_time }} Partial Grading: {{ qform.partial_grading }} - Shuffle Testcases: {{ qform.shuffle_testcases }} Grade Assignment Upload: {{ qform.grade_assignment_upload }} File: {{ fileform.file_field }}{{ fileform.file_field.errors }} {% if uploaded_files %}
Uploaded files:
Check on delete to delete files, diff --git a/yaksh/templates/yaksh/design_questionpaper.html b/yaksh/templates/yaksh/design_questionpaper.html index 1656e2b..d982d27 100644 --- a/yaksh/templates/yaksh/design_questionpaper.html +++ b/yaksh/templates/yaksh/design_questionpaper.html @@ -192,10 +192,14 @@ select
-
Almost finished creating your question paper
+
Almost finished creating your question paper


+


-- cgit From 21181922ee2b4d69fa62ae9725fcda759b475b80 Mon Sep 17 00:00:00 2001 From: mahesh Date: Tue, 20 Feb 2018 12:41:59 +0530 Subject: TestCaseOrder created only mcc/mcq questions --- yaksh/models.py | 8 +++++--- yaksh/templates/yaksh/user_data.html | 3 +-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/yaksh/models.py b/yaksh/models.py index 8e0bb4d..ecc0fc4 100644 --- a/yaksh/models.py +++ b/yaksh/models.py @@ -1218,15 +1218,17 @@ class QuestionPaper(models.Model): question_ids = [] for question in questions: question_ids.append(str(question.id)) - testcases = question.get_test_cases() if self.shuffle_testcases and \ question.type in ["mcq", "mcc"]: + testcases = question.get_test_cases() random.shuffle(testcases) - testcases_ids = ",".join([str(tc.id) for tc in testcases]) - testcases_order = TestCaseOrder.objects.create( + testcases_ids = ",".join([str(tc.id) for tc in testcases] + ) + testcases_order = TestCaseOrder.objects.create( answer_paper=ans_paper, question=question, order=testcases_ids) + ans_paper.questions_order = ",".join(question_ids) ans_paper.save() ans_paper.questions_unanswered.add(*questions) diff --git a/yaksh/templates/yaksh/user_data.html b/yaksh/templates/yaksh/user_data.html index 45867d2..ce2533e 100644 --- a/yaksh/templates/yaksh/user_data.html +++ b/yaksh/templates/yaksh/user_data.html @@ -74,8 +74,7 @@ User IP address: {{ paper.user_ip }} {% endif %} {% endfor %} - {% elif question.type == "integer" or question.type == "string" - or question.type == "float" %} + {% elif question.type == "integer" or question.type == "string" or question.type == "float" %}
Correct Answer:
{% for testcase in question.get_test_cases %} {{ testcase.correct|safe }} -- cgit From 1c8bc4aaadb307b14cb4e673485bf2405e921543 Mon Sep 17 00:00:00 2001 From: mahesh Date: Thu, 8 Feb 2018 15:19:27 +0530 Subject: Improve test cases for TestCaseOrder model --- yaksh/evaluator_tests/test_simple_question_types.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yaksh/evaluator_tests/test_simple_question_types.py b/yaksh/evaluator_tests/test_simple_question_types.py index cbf2abd..c952884 100644 --- a/yaksh/evaluator_tests/test_simple_question_types.py +++ b/yaksh/evaluator_tests/test_simple_question_types.py @@ -554,6 +554,7 @@ class MCQQuestionTestCases(unittest.TestCase): attempt_num=1, course_id=self.course.id ) + @classmethod def tearDownClass(self): self.question1.delete() @@ -580,8 +581,7 @@ class MCQQuestionTestCases(unittest.TestCase): course_id=self.course.id ) not_ordered_testcase = self.question1.get_ordered_test_cases( - answerpaper3 - ) + answerpaper3 ) get_test_cases = self.question1.get_test_cases() # Then self.assertNotEqual(order1, order2) -- cgit From 9c9e505e79abce0cae6b341880ed9f8a4e31a8be Mon Sep 17 00:00:00 2001 From: mahesh Date: Fri, 2 Feb 2018 15:22:25 +0530 Subject: Add jumble question type --- yaksh/models.py | 24 ++ yaksh/static/yaksh/js/jquery-sortable.js | 693 +++++++++++++++++++++++++++++++ yaksh/static/yaksh/js/requesthandler.js | 11 +- yaksh/templates/yaksh/add_question.html | 1 + yaksh/templates/yaksh/question.html | 23 +- yaksh/views.py | 3 + 6 files changed, 752 insertions(+), 3 deletions(-) create mode 100644 yaksh/static/yaksh/js/jquery-sortable.js diff --git a/yaksh/models.py b/yaksh/models.py index ecc0fc4..7c5bb85 100644 --- a/yaksh/models.py +++ b/yaksh/models.py @@ -54,6 +54,8 @@ question_types = ( ("integer", "Answer in Integer"), ("string", "Answer in String"), ("float", "Answer in Float"), + ("arrange", "Arrange Options in Order"), + ) enrollment_methods = ( @@ -69,6 +71,7 @@ test_case_types = ( ("integertestcase", "Integer Testcase"), ("stringtestcase", "String Testcase"), ("floattestcase", "Float Testcase"), + ("arrangetestcase", "Arrange Options Testcase"), ) string_check_type = ( @@ -1791,6 +1794,13 @@ class AnswerPaper(models.Model): result['success'] = True result['error'] = ['Correct answer'] + elif question.type == 'arrange': + tc_list = sorted([ids.id for ids in question.get_test_cases()]) + if user_answer == tc_list: + result['success'] = True + result['error'] = ['Correct answer'] + + elif question.type == 'code' or question.type == "upload": user_dir = self.user.profile.get_user_dir() url = '{0}:{1}'.format(SERVER_HOST_NAME, server_port) @@ -2011,6 +2021,18 @@ class FloatTestCase(TestCase): ) +class ArrangeTestCase(TestCase): + + options = models.TextField(default=None) + + def get_field_value(self): + return {"test_case_type": "arrangetestcase", + "options": self.options} + + def __str__(self): + return u'Arrange Testcase | Option: {0}'.format(self.options) + + ############################################################################## class TestCaseOrder(models.Model): """Testcase order contains a set of ordered test cases for a given question @@ -2025,3 +2047,5 @@ class TestCaseOrder(models.Model): #Order of the test case for a question. order = models.TextField() + + diff --git a/yaksh/static/yaksh/js/jquery-sortable.js b/yaksh/static/yaksh/js/jquery-sortable.js new file mode 100644 index 0000000..376880c --- /dev/null +++ b/yaksh/static/yaksh/js/jquery-sortable.js @@ -0,0 +1,693 @@ +/* =================================================== + * jquery-sortable.js v0.9.13 + * http://johnny.github.com/jquery-sortable/ + * =================================================== + * Copyright (c) 2012 Jonas von Andrian + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * ========================================================== */ + + +!function ( $, window, pluginName, undefined){ + var containerDefaults = { + // If true, items can be dragged from this container + drag: true, + // If true, items can be droped onto this container + drop: true, + // Exclude items from being draggable, if the + // selector matches the item + exclude: "", + // If true, search for nested containers within an item.If you nest containers, + // either the original selector with which you call the plugin must only match the top containers, + // or you need to specify a group (see the bootstrap nav example) + nested: true, + // If true, the items are assumed to be arranged vertically + vertical: true + }, // end container defaults + groupDefaults = { + // This is executed after the placeholder has been moved. + // $closestItemOrContainer contains the closest item, the placeholder + // has been put at or the closest empty Container, the placeholder has + // been appended to. + afterMove: function ($placeholder, container, $closestItemOrContainer) { + }, + // The exact css path between the container and its items, e.g. "> tbody" + containerPath: "", + // The css selector of the containers + containerSelector: "ol, ul", + // Distance the mouse has to travel to start dragging + distance: 0, + // Time in milliseconds after mousedown until dragging should start. + // This option can be used to prevent unwanted drags when clicking on an element. + delay: 0, + // The css selector of the drag handle + handle: "", + // The exact css path between the item and its subcontainers. + // It should only match the immediate items of a container. + // No item of a subcontainer should be matched. E.g. for ol>div>li the itemPath is "> div" + itemPath: "", + // The css selector of the items + itemSelector: "li", + // The class given to "body" while an item is being dragged + bodyClass: "dragging", + // The class giving to an item while being dragged + draggedClass: "dragged", + // Check if the dragged item may be inside the container. + // Use with care, since the search for a valid container entails a depth first search + // and may be quite expensive. + isValidTarget: function ($item, container) { + return true + }, + // Executed before onDrop if placeholder is detached. + // This happens if pullPlaceholder is set to false and the drop occurs outside a container. + onCancel: function ($item, container, _super, event) { + }, + // Executed at the beginning of a mouse move event. + // The Placeholder has not been moved yet. + onDrag: function ($item, position, _super, event) { + $item.css(position) + }, + // Called after the drag has been started, + // that is the mouse button is being held down and + // the mouse is moving. + // The container is the closest initialized container. + // Therefore it might not be the container, that actually contains the item. + onDragStart: function ($item, container, _super, event) { + $item.css({ + height: $item.outerHeight(), + width: $item.outerWidth() + }) + $item.addClass(container.group.options.draggedClass) + $("body").addClass(container.group.options.bodyClass) + }, + // Called when the mouse button is being released + onDrop: function ($item, container, _super, event) { + $item.removeClass(container.group.options.draggedClass).removeAttr("style") + $("body").removeClass(container.group.options.bodyClass) + }, + // Called on mousedown. If falsy value is returned, the dragging will not start. + // Ignore if element clicked is input, select or textarea + onMousedown: function ($item, _super, event) { + if (!event.target.nodeName.match(/^(input|select|textarea)$/i)) { + event.preventDefault() + return true + } + }, + // The class of the placeholder (must match placeholder option markup) + placeholderClass: "placeholder", + // Template for the placeholder. Can be any valid jQuery input + // e.g. a string, a DOM element. + // The placeholder must have the class "placeholder" + placeholder: '
  • ', + // If true, the position of the placeholder is calculated on every mousemove. + // If false, it is only calculated when the mouse is above a container. + pullPlaceholder: true, + // Specifies serialization of the container group. + // The pair $parent/$children is either container/items or item/subcontainers. + serialize: function ($parent, $children, parentIsContainer) { + var result = $.extend({}, $parent.data()) + + if(parentIsContainer) + return [$children] + else if ($children[0]){ + result.children = $children + } + + delete result.subContainers + delete result.sortable + + return result + }, + // Set tolerance while dragging. Positive values decrease sensitivity, + // negative values increase it. + tolerance: 0 + }, // end group defaults + containerGroups = {}, + groupCounter = 0, + emptyBox = { + left: 0, + top: 0, + bottom: 0, + right:0 + }, + eventNames = { + start: "touchstart.sortable mousedown.sortable", + drop: "touchend.sortable touchcancel.sortable mouseup.sortable", + drag: "touchmove.sortable mousemove.sortable", + scroll: "scroll.sortable" + }, + subContainerKey = "subContainers" + + /* + * a is Array [left, right, top, bottom] + * b is array [left, top] + */ + function d(a,b) { + var x = Math.max(0, a[0] - b[0], b[0] - a[1]), + y = Math.max(0, a[2] - b[1], b[1] - a[3]) + return x+y; + } + + function setDimensions(array, dimensions, tolerance, useOffset) { + var i = array.length, + offsetMethod = useOffset ? "offset" : "position" + tolerance = tolerance || 0 + + while(i--){ + var el = array[i].el ? array[i].el : $(array[i]), + // use fitting method + pos = el[offsetMethod]() + pos.left += parseInt(el.css('margin-left'), 10) + pos.top += parseInt(el.css('margin-top'),10) + dimensions[i] = [ + pos.left - tolerance, + pos.left + el.outerWidth() + tolerance, + pos.top - tolerance, + pos.top + el.outerHeight() + tolerance + ] + } + } + + function getRelativePosition(pointer, element) { + var offset = element.offset() + return { + left: pointer.left - offset.left, + top: pointer.top - offset.top + } + } + + function sortByDistanceDesc(dimensions, pointer, lastPointer) { + pointer = [pointer.left, pointer.top] + lastPointer = lastPointer && [lastPointer.left, lastPointer.top] + + var dim, + i = dimensions.length, + distances = [] + + while(i--){ + dim = dimensions[i] + distances[i] = [i,d(dim,pointer), lastPointer && d(dim, lastPointer)] + } + distances = distances.sort(function (a,b) { + return b[1] - a[1] || b[2] - a[2] || b[0] - a[0] + }) + + // last entry is the closest + return distances + } + + function ContainerGroup(options) { + this.options = $.extend({}, groupDefaults, options) + this.containers = [] + + if(!this.options.rootGroup){ + this.scrollProxy = $.proxy(this.scroll, this) + this.dragProxy = $.proxy(this.drag, this) + this.dropProxy = $.proxy(this.drop, this) + this.placeholder = $(this.options.placeholder) + + if(!options.isValidTarget) + this.options.isValidTarget = undefined + } + } + + ContainerGroup.get = function (options) { + if(!containerGroups[options.group]) { + if(options.group === undefined) + options.group = groupCounter ++ + + containerGroups[options.group] = new ContainerGroup(options) + } + + return containerGroups[options.group] + } + + ContainerGroup.prototype = { + dragInit: function (e, itemContainer) { + this.$document = $(itemContainer.el[0].ownerDocument) + + // get item to drag + var closestItem = $(e.target).closest(this.options.itemSelector); + // using the length of this item, prevents the plugin from being started if there is no handle being clicked on. + // this may also be helpful in instantiating multidrag. + if (closestItem.length) { + this.item = closestItem; + this.itemContainer = itemContainer; + if (this.item.is(this.options.exclude) || !this.options.onMousedown(this.item, groupDefaults.onMousedown, e)) { + return; + } + this.setPointer(e); + this.toggleListeners('on'); + this.setupDelayTimer(); + this.dragInitDone = true; + } + }, + drag: function (e) { + if(!this.dragging){ + if(!this.distanceMet(e) || !this.delayMet) + return + + this.options.onDragStart(this.item, this.itemContainer, groupDefaults.onDragStart, e) + this.item.before(this.placeholder) + this.dragging = true + } + + this.setPointer(e) + // place item under the cursor + this.options.onDrag(this.item, + getRelativePosition(this.pointer, this.item.offsetParent()), + groupDefaults.onDrag, + e) + + var p = this.getPointer(e), + box = this.sameResultBox, + t = this.options.tolerance + + if(!box || box.top - t > p.top || box.bottom + t < p.top || box.left - t > p.left || box.right + t < p.left) + if(!this.searchValidTarget()){ + this.placeholder.detach() + this.lastAppendedItem = undefined + } + }, + drop: function (e) { + this.toggleListeners('off') + + this.dragInitDone = false + + if(this.dragging){ + // processing Drop, check if placeholder is detached + if(this.placeholder.closest("html")[0]){ + this.placeholder.before(this.item).detach() + } else { + this.options.onCancel(this.item, this.itemContainer, groupDefaults.onCancel, e) + } + this.options.onDrop(this.item, this.getContainer(this.item), groupDefaults.onDrop, e) + + // cleanup + this.clearDimensions() + this.clearOffsetParent() + this.lastAppendedItem = this.sameResultBox = undefined + this.dragging = false + } + }, + searchValidTarget: function (pointer, lastPointer) { + if(!pointer){ + pointer = this.relativePointer || this.pointer + lastPointer = this.lastRelativePointer || this.lastPointer + } + + var distances = sortByDistanceDesc(this.getContainerDimensions(), + pointer, + lastPointer), + i = distances.length + + while(i--){ + var index = distances[i][0], + distance = distances[i][1] + + if(!distance || this.options.pullPlaceholder){ + var container = this.containers[index] + if(!container.disabled){ + if(!this.$getOffsetParent()){ + var offsetParent = container.getItemOffsetParent() + pointer = getRelativePosition(pointer, offsetParent) + lastPointer = getRelativePosition(lastPointer, offsetParent) + } + if(container.searchValidTarget(pointer, lastPointer)) + return true + } + } + } + if(this.sameResultBox) + this.sameResultBox = undefined + }, + movePlaceholder: function (container, item, method, sameResultBox) { + var lastAppendedItem = this.lastAppendedItem + if(!sameResultBox && lastAppendedItem && lastAppendedItem[0] === item[0]) + return; + + item[method](this.placeholder) + this.lastAppendedItem = item + this.sameResultBox = sameResultBox + this.options.afterMove(this.placeholder, container, item) + }, + getContainerDimensions: function () { + if(!this.containerDimensions) + setDimensions(this.containers, this.containerDimensions = [], this.options.tolerance, !this.$getOffsetParent()) + return this.containerDimensions + }, + getContainer: function (element) { + return element.closest(this.options.containerSelector).data(pluginName) + }, + $getOffsetParent: function () { + if(this.offsetParent === undefined){ + var i = this.containers.length - 1, + offsetParent = this.containers[i].getItemOffsetParent() + + if(!this.options.rootGroup){ + while(i--){ + if(offsetParent[0] != this.containers[i].getItemOffsetParent()[0]){ + // If every container has the same offset parent, + // use position() which is relative to this parent, + // otherwise use offset() + // compare #setDimensions + offsetParent = false + break; + } + } + } + + this.offsetParent = offsetParent + } + return this.offsetParent + }, + setPointer: function (e) { + var pointer = this.getPointer(e) + + if(this.$getOffsetParent()){ + var relativePointer = getRelativePosition(pointer, this.$getOffsetParent()) + this.lastRelativePointer = this.relativePointer + this.relativePointer = relativePointer + } + + this.lastPointer = this.pointer + this.pointer = pointer + }, + distanceMet: function (e) { + var currentPointer = this.getPointer(e) + return (Math.max( + Math.abs(this.pointer.left - currentPointer.left), + Math.abs(this.pointer.top - currentPointer.top) + ) >= this.options.distance) + }, + getPointer: function(e) { + var o = e.originalEvent || e.originalEvent.touches && e.originalEvent.touches[0] + return { + left: e.pageX || o.pageX, + top: e.pageY || o.pageY + } + }, + setupDelayTimer: function () { + var that = this + this.delayMet = !this.options.delay + + // init delay timer if needed + if (!this.delayMet) { + clearTimeout(this._mouseDelayTimer); + this._mouseDelayTimer = setTimeout(function() { + that.delayMet = true + }, this.options.delay) + } + }, + scroll: function (e) { + this.clearDimensions() + this.clearOffsetParent() // TODO is this needed? + }, + toggleListeners: function (method) { + var that = this, + events = ['drag','drop','scroll'] + + $.each(events,function (i,event) { + that.$document[method](eventNames[event], that[event + 'Proxy']) + }) + }, + clearOffsetParent: function () { + this.offsetParent = undefined + }, + // Recursively clear container and item dimensions + clearDimensions: function () { + this.traverse(function(object){ + object._clearDimensions() + }) + }, + traverse: function(callback) { + callback(this) + var i = this.containers.length + while(i--){ + this.containers[i].traverse(callback) + } + }, + _clearDimensions: function(){ + this.containerDimensions = undefined + }, + _destroy: function () { + containerGroups[this.options.group] = undefined + } + } + + function Container(element, options) { + this.el = element + this.options = $.extend( {}, containerDefaults, options) + + this.group = ContainerGroup.get(this.options) + this.rootGroup = this.options.rootGroup || this.group + this.handle = this.rootGroup.options.handle || this.rootGroup.options.itemSelector + + var itemPath = this.rootGroup.options.itemPath + this.target = itemPath ? this.el.find(itemPath) : this.el + + this.target.on(eventNames.start, this.handle, $.proxy(this.dragInit, this)) + + if(this.options.drop) + this.group.containers.push(this) + } + + Container.prototype = { + dragInit: function (e) { + var rootGroup = this.rootGroup + + if( !this.disabled && + !rootGroup.dragInitDone && + this.options.drag && + this.isValidDrag(e)) { + rootGroup.dragInit(e, this) + } + }, + isValidDrag: function(e) { + return e.which == 1 || + e.type == "touchstart" && e.originalEvent.touches.length == 1 + }, + searchValidTarget: function (pointer, lastPointer) { + var distances = sortByDistanceDesc(this.getItemDimensions(), + pointer, + lastPointer), + i = distances.length, + rootGroup = this.rootGroup, + validTarget = !rootGroup.options.isValidTarget || + rootGroup.options.isValidTarget(rootGroup.item, this) + + if(!i && validTarget){ + rootGroup.movePlaceholder(this, this.target, "append") + return true + } else + while(i--){ + var index = distances[i][0], + distance = distances[i][1] + if(!distance && this.hasChildGroup(index)){ + var found = this.getContainerGroup(index).searchValidTarget(pointer, lastPointer) + if(found) + return true + } + else if(validTarget){ + this.movePlaceholder(index, pointer) + return true + } + } + }, + movePlaceholder: function (index, pointer) { + var item = $(this.items[index]), + dim = this.itemDimensions[index], + method = "after", + width = item.outerWidth(), + height = item.outerHeight(), + offset = item.offset(), + sameResultBox = { + left: offset.left, + right: offset.left + width, + top: offset.top, + bottom: offset.top + height + } + if(this.options.vertical){ + var yCenter = (dim[2] + dim[3]) / 2, + inUpperHalf = pointer.top <= yCenter + if(inUpperHalf){ + method = "before" + sameResultBox.bottom -= height / 2 + } else + sameResultBox.top += height / 2 + } else { + var xCenter = (dim[0] + dim[1]) / 2, + inLeftHalf = pointer.left <= xCenter + if(inLeftHalf){ + method = "before" + sameResultBox.right -= width / 2 + } else + sameResultBox.left += width / 2 + } + if(this.hasChildGroup(index)) + sameResultBox = emptyBox + this.rootGroup.movePlaceholder(this, item, method, sameResultBox) + }, + getItemDimensions: function () { + if(!this.itemDimensions){ + this.items = this.$getChildren(this.el, "item").filter( + ":not(." + this.group.options.placeholderClass + ", ." + this.group.options.draggedClass + ")" + ).get() + setDimensions(this.items, this.itemDimensions = [], this.options.tolerance) + } + return this.itemDimensions + }, + getItemOffsetParent: function () { + var offsetParent, + el = this.el + // Since el might be empty we have to check el itself and + // can not do something like el.children().first().offsetParent() + if(el.css("position") === "relative" || el.css("position") === "absolute" || el.css("position") === "fixed") + offsetParent = el + else + offsetParent = el.offsetParent() + return offsetParent + }, + hasChildGroup: function (index) { + return this.options.nested && this.getContainerGroup(index) + }, + getContainerGroup: function (index) { + var childGroup = $.data(this.items[index], subContainerKey) + if( childGroup === undefined){ + var childContainers = this.$getChildren(this.items[index], "container") + childGroup = false + + if(childContainers[0]){ + var options = $.extend({}, this.options, { + rootGroup: this.rootGroup, + group: groupCounter ++ + }) + childGroup = childContainers[pluginName](options).data(pluginName).group + } + $.data(this.items[index], subContainerKey, childGroup) + } + return childGroup + }, + $getChildren: function (parent, type) { + var options = this.rootGroup.options, + path = options[type + "Path"], + selector = options[type + "Selector"] + + parent = $(parent) + if(path) + parent = parent.find(path) + + return parent.children(selector) + }, + _serialize: function (parent, isContainer) { + var that = this, + childType = isContainer ? "item" : "container", + + children = this.$getChildren(parent, childType).not(this.options.exclude).map(function () { + return that._serialize($(this), !isContainer) + }).get() + + return this.rootGroup.options.serialize(parent, children, isContainer) + }, + traverse: function(callback) { + $.each(this.items || [], function(item){ + var group = $.data(this, subContainerKey) + if(group) + group.traverse(callback) + }); + + callback(this) + }, + _clearDimensions: function () { + this.itemDimensions = undefined + }, + _destroy: function() { + var that = this; + + this.target.off(eventNames.start, this.handle); + this.el.removeData(pluginName) + + if(this.options.drop) + this.group.containers = $.grep(this.group.containers, function(val){ + return val != that + }) + + $.each(this.items || [], function(){ + $.removeData(this, subContainerKey) + }) + } + } + + var API = { + enable: function() { + this.traverse(function(object){ + object.disabled = false + }) + }, + disable: function (){ + this.traverse(function(object){ + object.disabled = true + }) + }, + serialize: function () { + return this._serialize(this.el, true) + }, + refresh: function() { + this.traverse(function(object){ + object._clearDimensions() + }) + }, + destroy: function () { + this.traverse(function(object){ + object._destroy(); + }) + } + } + + $.extend(Container.prototype, API) + + /** + * jQuery API + * + * Parameters are + * either options on init + * or a method name followed by arguments to pass to the method + */ + $.fn[pluginName] = function(methodOrOptions) { + var args = Array.prototype.slice.call(arguments, 1) + + return this.map(function(){ + var $t = $(this), + object = $t.data(pluginName) + + if(object && API[methodOrOptions]) + return API[methodOrOptions].apply(object, args) || this + else if(!object && (methodOrOptions === undefined || + typeof methodOrOptions === "object")) + $t.data(pluginName, new Container($t, methodOrOptions)) + + return this + }); + }; + +}(jQuery, window, 'sortable'); diff --git a/yaksh/static/yaksh/js/requesthandler.js b/yaksh/static/yaksh/js/requesthandler.js index ec2391a..a215ce4 100644 --- a/yaksh/static/yaksh/js/requesthandler.js +++ b/yaksh/static/yaksh/js/requesthandler.js @@ -177,6 +177,13 @@ if (question_type == 'upload' || question_type == 'code') { global_editor.editor.setValue(init_val); global_editor.editor.clearHistory(); } - - }); +function user_arranged_options(){ + var temp_array = [] + var add_array = document.getElementById("arrange_order"); + var ans_array = order_array.children().get() + var answer_is = $.each(ans_array, function( index, value ) { + temp_array.push(value.id); + }); + add_array.value = temp_array +} diff --git a/yaksh/templates/yaksh/add_question.html b/yaksh/templates/yaksh/add_question.html index ed69657..79c132c 100644 --- a/yaksh/templates/yaksh/add_question.html +++ b/yaksh/templates/yaksh/add_question.html @@ -64,6 +64,7 @@ +

    diff --git a/yaksh/templates/yaksh/question.html b/yaksh/templates/yaksh/question.html index 9d6ce48..ed04963 100644 --- a/yaksh/templates/yaksh/question.html +++ b/yaksh/templates/yaksh/question.html @@ -21,6 +21,7 @@ + + {% endif %} + + {% if question.type == "code" %}
    @@ -269,6 +287,9 @@ question_type = "{{ question.type }}"
       {% elif question.type == "upload" %}
       + {% elif question.type == "arrange" %} +
       + {% else %} {% if question in paper.get_questions_unanswered or quiz.is_exercise %} diff --git a/yaksh/views.py b/yaksh/views.py index 27a07d2..5c88391 100644 --- a/yaksh/views.py +++ b/yaksh/views.py @@ -726,6 +726,9 @@ def check(request, q_id, attempt_num=None, questionpaper_id=None, elif current_question.type == 'mcc': user_answer = request.POST.getlist('answer') + elif current_question.type == 'arrange': + user_answer_list = request.POST.get('answer').split(',') + user_answer = [int(ids) for ids in user_answer_list] elif current_question.type == 'upload': # if time-up at upload question then the form is submitted without # validation -- cgit From 453a5d8f191b62af45426d5c98a99552c34ba396 Mon Sep 17 00:00:00 2001 From: maheshgudi Date: Mon, 5 Feb 2018 17:06:09 +0530 Subject: Include Rebase changes from shuffle testcases --- yaksh/forms.py | 3 +++ yaksh/templates/yaksh/question.html | 7 +++++-- yaksh/views.py | 4 +++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/yaksh/forms.py b/yaksh/forms.py index e53eda3..7eae673 100644 --- a/yaksh/forms.py +++ b/yaksh/forms.py @@ -35,6 +35,9 @@ question_types = ( ("integer", "Answer in Integer"), ("string", "Answer in String"), ("float", "Answer in Float"), + ("arrange", "Arrange Options in Order"), + + ) test_case_types = ( diff --git a/yaksh/templates/yaksh/question.html b/yaksh/templates/yaksh/question.html index ed04963..e67a260 100644 --- a/yaksh/templates/yaksh/question.html +++ b/yaksh/templates/yaksh/question.html @@ -257,11 +257,14 @@ question_type = "{{ question.type }}" {% if question.type == "arrange" %} +
      {% for test_case in test_cases %} -
    1. {{test_case.options| safe }}
    2. +
    3. {{test_case.options| safe }}
    4. {% endfor %} -
    + +
    +