From f98ad4867f9f2eb5a8b2797cb2c6f871aff47f5e Mon Sep 17 00:00:00 2001 From: prathamesh Date: Fri, 15 May 2020 15:22:51 +0530 Subject: [PATCH 1/2] Allow negative marks for a question Moderator can set negative marks for a question. This handles the partial marks situation as well. --- yaksh/models.py | 23 +++++++---- yaksh/test_models.py | 93 +++++++++++++++++++++++++++++++++++++++++++- yaksh/views.py | 10 +++-- 3 files changed, 113 insertions(+), 13 deletions(-) diff --git a/yaksh/models.py b/yaksh/models.py index 64489b892..45032adf4 100644 --- a/yaksh/models.py +++ b/yaksh/models.py @@ -1288,6 +1288,13 @@ class Question(models.Model): # Number of points for the question. points = models.FloatField(default=1.0) + # Negative marks for the question + negative_marks = models.FloatField( + default=0.0, + help_text='In Percentage eg: 25, ' + 'means 0.25 will be negative marks if question is for 1 mark' + ) + # The language for question. language = models.CharField(max_length=24, choices=languages) @@ -1650,9 +1657,11 @@ class Answer(models.Model): def set_marks(self, marks): if marks > self.question.points: - self.marks = self.question.points - else: - self.marks = marks + marks = self.question.points + wrong_fraction = self.question.points - marks + negative_marks = wrong_fraction * (self.question.negative_marks/100) + self.marks = marks - negative_marks + self.save() def __str__(self): return "Answer for question {0}".format(self.question.summary) @@ -2460,16 +2469,16 @@ def regrade(self, question_id, server_port=SERVER_POOL_PORT): if question.partial_grading and question.type == 'code': max_weight = question.get_maximum_test_case_weight() factor = result['weight']/max_weight - user_answer.marks = question.points * factor + user_answer.set_marks(question.points * factor) else: - user_answer.marks = question.points + user_answer.set_marks(question.points) else: if question.partial_grading and question.type == 'code': max_weight = question.get_maximum_test_case_weight() factor = result['weight']/max_weight - user_answer.marks = question.points * factor + user_answer.set_marks(question.points * factor) else: - user_answer.marks = 0 + user_answer.set_marks(0) user_answer.save() self.update_marks('completed') return True, msg diff --git a/yaksh/test_models.py b/yaksh/test_models.py index 4e6b1ae01..13ec97243 100644 --- a/yaksh/test_models.py +++ b/yaksh/test_models.py @@ -28,7 +28,6 @@ def setUpModule(): Group.objects.create(name='moderator') - # create user profile user = User.objects.create_user(username='creator', password='demo', @@ -1636,6 +1635,62 @@ def test_update_marks(self): self.assertTrue(self.answerpaper.passed) self.assertFalse(self.answerpaper.is_attempt_inprogress()) + def test_update_marks_with_negative_marks(self): + # Given + question = self.answer_wrong.question + question.negative_marks = 25 + question.save() + + # When + self.answer_wrong.set_marks(1) + self.answerpaper.update_marks() + + # Then + self.assertEqual(self.answerpaper.marks_obtained, 2) + self.assertEqual(self.answerpaper.percent, 66.67) + + # When + answers = self.answerpaper.answers.filter(question=question) + for answer in answers: + answer.set_marks(0) + self.answerpaper.update_marks() + + # Then + self.assertEqual(self.answerpaper.marks_obtained, 0.75) + self.assertEqual(self.answerpaper.percent, 25) + + # When + for answer in answers: + self.answer_wrong.set_marks(-1) + self.answer_wrong.set_marks(0) + self.answerpaper.update_marks() + + # Then + self.assertEqual(self.answerpaper.marks_obtained, 0.75) + self.assertEqual(self.answerpaper.percent, 25) + + # When + for answer in answers: + self.answer_wrong.set_marks(0.5) + self.answerpaper.update_marks() + + # Then + self.assertEqual(self.answerpaper.marks_obtained, 1.375) + self.assertEqual(self.answerpaper.percent, 45.83) + + # Given + question.negative_marks = 0 + question.save() + + # When + for answer in answers: + self.answer_wrong.set_marks(0) + self.answerpaper.update_marks() + + # Then + self.assertEqual(self.answerpaper.marks_obtained, 1) + self.assertEqual(self.answerpaper.percent, 33.33) + def test_set_end_time(self): current_time = timezone.now() self.answerpaper.set_end_time(current_time) @@ -1675,6 +1730,40 @@ def test_set_marks(self): self.assertEqual(self.answer_wrong.marks, 0.5) self.answer_wrong.set_marks(10.0) self.assertEqual(self.answer_wrong.marks, 1.0) + self.answer_wrong.set_marks(0) + self.assertEqual(self.answer_wrong.marks, 0) + + def test_set_marks_with_negative_marks(self): + # Given + question = self.answer_wrong.question + question.negative_marks = 25 + question.save() + # When + self.answer_wrong.set_marks(1) + # Then + self.assertEqual(self.answer_wrong.marks, 1) + + # When + self.answer_wrong.set_marks(0) + # Then + self.assertEqual(self.answer_wrong.marks, -0.25) + + # When + self.answer_wrong.set_marks(0.5) + # Then + self.assertEqual(self.answer_wrong.marks, 0.375) + + # Given + question.negative_marks = 0 + # When + self.answer_wrong.set_marks(1) + # Then + self.assertEqual(self.answer_wrong.marks, 1.0) + + # When + self.answer_wrong.set_marks(0) + # Then + self.assertEqual(self.answer_wrong.marks, 0) def test_get_latest_answer(self): latest_answer = self.answerpaper.get_latest_answer(self.question1.id) @@ -2363,4 +2452,4 @@ def tearDown(self): self.user1.delete() self.course.delete() self.post1.delete() - self.comment1.delete() \ No newline at end of file + self.comment1.delete() diff --git a/yaksh/views.py b/yaksh/views.py index 397e7c8a9..a55e1a1d2 100644 --- a/yaksh/views.py +++ b/yaksh/views.py @@ -903,22 +903,24 @@ def _update_paper(request, uid, result): paper = new_answer.answerpaper_set.first() if result.get('success'): - new_answer.marks = (current_question.points * result['weight'] / - current_question.get_maximum_test_case_weight()) \ + marks = (current_question.points * result['weight'] / + current_question.get_maximum_test_case_weight()) \ if current_question.partial_grading and \ current_question.type == 'code' or \ current_question.type == 'upload' else current_question.points + new_answer.set_marks(marks) new_answer.correct = result.get('success') error_message = None new_answer.error = json.dumps(result.get('error')) next_question = paper.add_completed_question(current_question.id) else: - new_answer.marks = (current_question.points * result['weight'] / - current_question.get_maximum_test_case_weight()) \ + marks = (current_question.points * result['weight'] / + current_question.get_maximum_test_case_weight()) \ if current_question.partial_grading and \ current_question.type == 'code' or \ current_question.type == 'upload' \ else 0 + new_answer.set_marks(marks) error_message = result.get('error') \ if current_question.type == 'code' or \ current_question.type == 'upload' \ From 72e7b765702c25090cb0baf6334fcc529bf4bb26 Mon Sep 17 00:00:00 2001 From: prathamesh Date: Fri, 16 Apr 2021 07:07:40 +0530 Subject: [PATCH 2/2] Add negative marks only for mcq and mcc question types. Negative marks will be available only for mcq and mcc questions on the add question interface. Also, on the safer side the negative marks are given only to the mcc and mcq questions at the backend. --- yaksh/models.py | 29 ++++++++++++++++----------- yaksh/static/yaksh/js/add_question.js | 14 +++++++++++-- yaksh/test_models.py | 23 ++++++++++++++++++++- 3 files changed, 51 insertions(+), 15 deletions(-) diff --git a/yaksh/models.py b/yaksh/models.py index e39f169dc..27ee9dd4c 100644 --- a/yaksh/models.py +++ b/yaksh/models.py @@ -1358,13 +1358,6 @@ class Question(models.Model): # Number of points for the question. points = models.FloatField(default=1.0) - # Negative marks for the question - negative_marks = models.FloatField( - default=0.0, - help_text='In Percentage eg: 25, ' - 'means 0.25 will be negative marks if question is for 1 mark' - ) - # The language for question. language = models.CharField(max_length=24, choices=languages) @@ -1374,6 +1367,14 @@ class Question(models.Model): # The type of question. type = models.CharField(max_length=24, choices=question_types) + # Negative marks for the question + negative_marks = models.FloatField( + default=0.0, + help_text='In Percentage eg: 25, ' + 'means 0.25 will be negative marks if question is for 1 mark' + ) + + # Is this question active or not. If it is inactive it will not be used # when creating a QuestionPaper. active = models.BooleanField(default=True) @@ -1739,11 +1740,15 @@ class Answer(models.Model): comment = models.TextField(null=True, blank=True) def set_marks(self, marks): - if marks > self.question.points: - marks = self.question.points - wrong_fraction = self.question.points - marks - negative_marks = wrong_fraction * (self.question.negative_marks/100) - self.marks = marks - negative_marks + points = self.question.points + qtype = self.question.type + if marks > points: + marks = points + if qtype == 'mcq' or qtype == 'mcc': + wrong_fraction = points - marks + negative_marks = wrong_fraction * (self.question.negative_marks/100) + marks = marks - negative_marks + self.marks = marks self.save() def set_comment(self, comments): diff --git a/yaksh/static/yaksh/js/add_question.js b/yaksh/static/yaksh/js/add_question.js index 551c61101..ec6a48227 100644 --- a/yaksh/static/yaksh/js/add_question.js +++ b/yaksh/static/yaksh/js/add_question.js @@ -183,6 +183,11 @@ function autosubmit() } $(document).ready(() => { + $("#id_negative_marks").closest("tr").hide() + let qtype = $("#id_type").val(); + if (qtype === "mcq" || qtype === "mcc") { + $("#id_negative_marks").closest("tr").show() + } let option = $('#id_language').val(); if(option === 'other') { $('#id_topic').closest('tr').show(); @@ -210,5 +215,10 @@ $(document).ready(() => { } else { $('#id_language').children("option[value='other']").show(); } - }) -}); \ No newline at end of file + if (value === "mcq" || value === "mcc") { + $("#id_negative_marks").closest("tr").show() + } else { + $("#id_negative_marks").closest("tr").hide() + } + }); +}); diff --git a/yaksh/test_models.py b/yaksh/test_models.py index d5d0fce21..6ead1f320 100644 --- a/yaksh/test_models.py +++ b/yaksh/test_models.py @@ -1882,6 +1882,7 @@ def test_set_marks_with_negative_marks(self): question = self.answer_wrong.question question.negative_marks = 25 question.save() + # When self.answer_wrong.set_marks(1) # Then @@ -1909,6 +1910,26 @@ def test_set_marks_with_negative_marks(self): # Then self.assertEqual(self.answer_wrong.marks, 0) + def test_no_negative_marks_except_mcq_mcc(self): + # Given + code_question = self.answer1.question + marks = self.answer1.marks + code_question.negative_marks = 25 + code_question.save() + + # When + self.answer1.set_marks(1) + # Then + self.assertEqual(self.answer1.marks, 1) + + # When + self.answer1.set_marks(0) + # Then + self.assertEqual(self.answer1.marks, 0) + + self.answer1.marks = marks + self.answer1.save() + def test_get_latest_answer(self): latest_answer = self.answerpaper.get_latest_answer(self.question1.id) self.assertEqual(latest_answer.id, self.answer1.id) @@ -2637,7 +2658,7 @@ def test_active(self): self.assertTrue(qrcode.is_active()) # When - qrcode.deactivate() + qrcode.deactivate() qrcode.save() # Then