diff --git a/erpnext/education/doctype/course_enrollment/course_enrollment.py b/erpnext/education/doctype/course_enrollment/course_enrollment.py index 064b0757093..b082be2aa21 100644 --- a/erpnext/education/doctype/course_enrollment/course_enrollment.py +++ b/erpnext/education/doctype/course_enrollment/course_enrollment.py @@ -35,7 +35,7 @@ class CourseEnrollment(Document): if enrollment: frappe.throw(_("Student is already enrolled.")) - def add_quiz_activity(self, quiz_name, quiz_response,answers, score, status): + def add_quiz_activity(self, quiz_name, quiz_response, answers, score, status): result = {k: ('Correct' if v else 'Wrong') for k,v in answers.items()} result_data = [] for key in answers: @@ -43,7 +43,9 @@ class CourseEnrollment(Document): item['question'] = key item['quiz_result'] = result[key] try: - if isinstance(quiz_response[key], list): + if not quiz_response[key]: + item['selected_option'] = "Unattempted" + elif isinstance(quiz_response[key], list): item['selected_option'] = ', '.join(frappe.get_value('Options', res, 'option') for res in quiz_response[key]) else: item['selected_option'] = frappe.get_value('Options', quiz_response[key], 'option') @@ -59,7 +61,7 @@ class CourseEnrollment(Document): "result": result_data, "score": score, "status": status - }).insert() + }).insert(ignore_permissions = True) def add_activity(self, content_type, content): activity = check_activity_exists(self.name, content_type, content) diff --git a/erpnext/education/doctype/quiz/quiz.py b/erpnext/education/doctype/quiz/quiz.py index 6d00d333723..8e54745464b 100644 --- a/erpnext/education/doctype/quiz/quiz.py +++ b/erpnext/education/doctype/quiz/quiz.py @@ -11,50 +11,43 @@ class Quiz(Document): if self.passing_score > 100: frappe.throw("Passing Score value should be between 0 and 100") - def validate_quiz_attempts(self, enrollment, quiz_name): - if self.max_attempts > 0: - try: - if len(frappe.get_all("Quiz Activity", {'enrollment': enrollment.name, 'quiz': quiz_name})) >= self.max_attempts: - frappe.throw('Maximum attempts reached!') - except Exception as e: - pass + def allowed_attempt(self, enrollment, quiz_name): + if self.max_attempts == 0: + return True + + try: + if len(frappe.get_all("Quiz Activity", {'enrollment': enrollment.name, 'quiz': quiz_name})) >= self.max_attempts: + frappe.msgprint("Maximum attempts for this quiz reached!") + return False + else: + return True + except Exception as e: + return False def evaluate(self, response_dict, quiz_name): - # self.validate_quiz_attempts(enrollment, quiz_name) questions = [frappe.get_doc('Question', question.question_link) for question in self.question] answers = {q.name:q.get_answer() for q in questions} - correct_answers = {} + result = {} for key in answers: try: if isinstance(response_dict[key], list): - result = compare_list_elementwise(response_dict[key], answers[key]) + is_correct = compare_list_elementwise(response_dict[key], answers[key]) else: - result = (response_dict[key] == answers[key]) - except: - result = False - correct_answers[key] = result - score = (sum(correct_answers.values()) * 100 ) / len(answers) + is_correct = (response_dict[key] == answers[key]) + except Exception as e: + is_correct = False + result[key] = is_correct + score = (sum(result.values()) * 100 ) / len(answers) if score >= self.passing_score: status = "Pass" else: status = "Fail" - return correct_answers, score, status + return result, score, status def get_questions(self): - quiz_question = self.get_all_children() - if quiz_question: - questions = [frappe.get_doc('Question', question.question_link).as_dict() for question in quiz_question] - for question in questions: - correct_options = [option.is_correct for option in question.options] - if sum(correct_options) > 1: - question['type'] = "MultipleChoice" - else: - question['type'] = "SingleChoice" - return questions - else: - return None + return [frappe.get_doc('Question', question.question_link) for question in self.question] def compare_list_elementwise(*args): try: diff --git a/erpnext/education/doctype/quiz_result/quiz_result.json b/erpnext/education/doctype/quiz_result/quiz_result.json index 86505ac756a..67c7e2d4492 100644 --- a/erpnext/education/doctype/quiz_result/quiz_result.json +++ b/erpnext/education/doctype/quiz_result/quiz_result.json @@ -1,145 +1,52 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2018-10-15 15:52:25.766374", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "creation": "2018-10-15 15:52:25.766374", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "question", + "selected_option", + "quiz_result" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "question", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Question", - "length": 0, - "no_copy": 0, - "options": "Question", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 1, - "translatable": 0, - "unique": 0 - }, + "fieldname": "question", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Question", + "options": "Question", + "read_only": 1, + "reqd": 1, + "set_only_once": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "selected_option", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Selected Option", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 1, - "translatable": 0, - "unique": 0 - }, + "fieldname": "selected_option", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Selected Option", + "read_only": 1, + "set_only_once": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "quiz_result", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Result", - "length": 0, - "no_copy": 0, - "options": "\nCorrect\nWrong", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 1, - "translatable": 0, - "unique": 0 + "fieldname": "quiz_result", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Result", + "options": "\nCorrect\nWrong", + "read_only": 1, + "reqd": 1, + "set_only_once": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2019-03-27 17:58:54.388848", - "modified_by": "Administrator", - "module": "Education", - "name": "Quiz Result", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "istable": 1, + "modified": "2019-06-03 12:52:32.267392", + "modified_by": "Administrator", + "module": "Education", + "name": "Quiz Result", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/education/utils.py b/erpnext/education/utils.py index a4b71e310ea..53f02f5f2f7 100644 --- a/erpnext/education/utils.py +++ b/erpnext/education/utils.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- # Copyright (c) 2015, Frappe Technologies and contributors -# For lice from __future__ import unicode_literals, division import frappe @@ -173,7 +172,7 @@ def has_super_access(): """Check if user has a role that allows full access to LMS Returns: - bool: true if user has access to all lms content + bool: true if user has access to all lms content """ current_user = frappe.get_doc('User', frappe.session.user) roles = set([role.role for role in current_user.roles]) @@ -189,7 +188,6 @@ def add_activity(course, content_type, content): return frappe.throw("Student with email {0} does not exist".format(frappe.session.user), frappe.DoesNotExistError) course_enrollment = get_enrollment("course", course, student.name) - print(course_enrollment) if not course_enrollment: return None @@ -199,6 +197,56 @@ def add_activity(course, content_type, content): else: return enrollment.add_activity(content_type, content) +@frappe.whitelist() +def evaluate_quiz(quiz_response, quiz_name, course): + import json + + student = get_current_student() + + quiz_response = json.loads(quiz_response) + quiz = frappe.get_doc("Quiz", quiz_name) + result, score, status = quiz.evaluate(quiz_response, quiz_name) + + if has_super_access(): + return {'result': result, 'score': score, 'status': status} + + if student: + course_enrollment = get_enrollment("course", course, student.name) + if course_enrollment: + enrollment = frappe.get_doc('Course Enrollment', course_enrollment) + if quiz.allowed_attempt(enrollment, quiz_name): + enrollment.add_quiz_activity(quiz_name, quiz_response, result, score, status) + return {'result': result, 'score': score, 'status': status} + else: + return None + else: + frappe.throw("Something went wrong. Pleae contact the administrator.") + +@frappe.whitelist() +def get_quiz(quiz_name, course): + try: + quiz = frappe.get_doc("Quiz", quiz_name) + questions = quiz.get_questions() + except: + frappe.throw("Quiz {0} does not exist".format(quiz_name)) + return None + + questions = [{ + 'name': question.name, + 'question': question.question, + 'type': question.question_type, + 'options': [{'name': option.name, 'option': option.option} + for option in question.options], + } for question in questions] + + if has_super_access(): + return {'questions': questions, 'activity': None} + + student = get_current_student() + course_enrollment = get_enrollment("course", course, student.name) + status, score, result = check_quiz_completion(quiz, course_enrollment) + return {'questions': questions, 'activity': {'is_complete': status, 'score': score, 'result': result}} + def create_student_from_current_user(): user = frappe.get_doc("User", frappe.session.user) @@ -226,7 +274,7 @@ def check_content_completion(content_name, content_type, enrollment_name): def check_quiz_completion(quiz, enrollment_name): attempts = frappe.get_all("Quiz Activity", filters={'enrollment': enrollment_name, 'quiz': quiz.name}, fields=["name", "activity_date", "score", "status"]) - status = False if quiz.max_attempts == 0 else bool(len(attempts) == quiz.max_attempts) + status = False if quiz.max_attempts == 0 else bool(len(attempts) >= quiz.max_attempts) score = None result = None if attempts: diff --git a/erpnext/public/js/education/lms/quiz.js b/erpnext/public/js/education/lms/quiz.js new file mode 100644 index 00000000000..f6dc4d08d54 --- /dev/null +++ b/erpnext/public/js/education/lms/quiz.js @@ -0,0 +1,185 @@ +class Quiz { + constructor(wrapper, options) { + this.wrapper = wrapper; + Object.assign(this, options); + this.questions = [] + this.refresh(); + } + + refresh() { + this.get_quiz(); + } + + get_quiz() { + frappe.call('erpnext.education.utils.get_quiz', { + quiz_name: this.name, + course: this.course + }).then(res => { + this.make(res.message) + }); + } + + make(data) { + data.questions.forEach(question_data => { + let question_wrapper = document.createElement('div'); + let question = new Question({ + wrapper: question_wrapper, + ...question_data + }); + this.questions.push(question) + this.wrapper.appendChild(question_wrapper); + }) + if (data.activity.is_complete) { + this.disable() + let indicator = 'red' + let message = 'Your are not allowed to attempt the quiz again.' + if (data.activity.result == 'Pass') { + indicator = 'green' + message = 'You have already cleared the quiz.' + } + + this.set_quiz_footer(message, indicator, data.activity.score) + } + else { + this.make_actions(); + } + } + + make_actions() { + const button = document.createElement("button"); + button.classList.add("btn", "btn-primary", "mt-5", "mr-2"); + + button.id = 'submit-button'; + button.innerText = 'Submit'; + button.onclick = () => this.submit(); + this.submit_btn = button + this.wrapper.appendChild(button); + } + + submit() { + this.submit_btn.innerText = 'Evaluating..' + this.submit_btn.disabled = true + this.disable() + frappe.call('erpnext.education.utils.evaluate_quiz', { + quiz_name: this.name, + quiz_response: this.get_selected(), + course: this.course + }).then(res => { + this.submit_btn.remove() + if (!res.message) { + frappe.throw("Something went wrong while evaluating the quiz.") + } + + let indicator = 'red' + let message = 'Fail' + if (res.message.status == 'Pass') { + indicator = 'green' + message = 'Congratulations, you cleared the quiz.' + } + + this.set_quiz_footer(message, indicator, res.message.score) + }); + } + + set_quiz_footer(message, indicator, score) { + const div = document.createElement("div"); + div.classList.add("mt-5"); + div.innerHTML = `