diff --git a/yaksh/models.py b/yaksh/models.py index fd483189f..27ee9dd4c 100644 --- a/yaksh/models.py +++ b/yaksh/models.py @@ -1367,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) @@ -1732,10 +1740,16 @@ class Answer(models.Model): comment = models.TextField(null=True, blank=True) def set_marks(self, marks): - if marks > self.question.points: - self.marks = self.question.points - else: - self.marks = 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): self.comment = comments @@ -2629,16 +2643,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/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 019e2fc52..6ead1f320 100644 --- a/yaksh/test_models.py +++ b/yaksh/test_models.py @@ -1790,6 +1790,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) @@ -1818,6 +1874,61 @@ 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_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) diff --git a/yaksh/views.py b/yaksh/views.py index 4a6f462d2..35b010aef 100644 --- a/yaksh/views.py +++ b/yaksh/views.py @@ -987,22 +987,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' \