refactor: refactored quiz api and added quiz.js

This commit is contained in:
Shivam Mishra 2019-06-03 12:57:38 +05:30
parent 46b3446da0
commit d1a252190b
7 changed files with 350 additions and 180 deletions

View File

@ -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)

View File

@ -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:

View File

@ -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
}

View File

@ -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:

View File

@ -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 = `<div class="row">
<div class="col-md-8">
<h4>${message}</h4>
<h5 class="text-muted"><span class="indicator ${indicator}">Score: ${score}/100</span></h5>
</div>
<div class="col-md-4">
<a href="${this.next_url}" class="btn btn-primary pull-right">${this.quiz_exit_button}</a>
</div>
</div>`
this.wrapper.appendChild(div)
}
disable() {
this.questions.forEach(que => que.disable())
}
get_selected() {
let que = {}
this.questions.forEach(question => {
que[question.name] = question.get_selected()
})
return que
}
}
class Question {
constructor(opts) {
Object.assign(this, opts);
this.make();
}
make() {
this.make_question()
this.make_options()
}
get_selected() {
let selected = this.options.filter(opt => opt.input.checked)
if (this.type == 'Single Correct Answer') {
if (selected[0]) return selected[0].name
}
if (this.type == 'Multiple Correct Answer') {
return selected.map(opt => opt.name)
}
return null
}
disable() {
let selected = this.options.forEach(opt => opt.input.disabled = true)
}
make_question() {
let question_wrapper = document.createElement('h5');
question_wrapper.classList.add('mt-3');
question_wrapper.innerText = this.question;
this.wrapper.appendChild(question_wrapper);
}
make_options() {
let make_input = (name, value) => {
let input = document.createElement('input');
input.id = name;
input.name = this.name;
input.value = value;
input.type = 'radio';
if (this.type == 'Multiple Correct Answer')
input.type = 'checkbox';
input.classList.add('form-check-input');
return input;
}
let make_label = function(name, value) {
let label = document.createElement('label');
label.classList.add('form-check-label');
label.htmlFor = name;
label.innerText = value;
return label
}
let make_option = function (wrapper, option) {
let option_div = document.createElement('div')
option_div.classList.add('form-check', 'pb-1')
let input = make_input(option.name, option.option);
let label = make_label(option.name, option.option);
option_div.appendChild(input)
option_div.appendChild(label)
wrapper.appendChild(option_div)
return {input: input, ...option}
}
let options_wrapper = document.createElement('div')
options_wrapper.classList.add('ml-2')
let option_list = []
this.options.forEach(opt => option_list.push(make_option(options_wrapper, opt)))
this.options = option_list
this.wrapper.appendChild(options_wrapper)
}
}

View File

@ -36,7 +36,7 @@
<div class="col-md-7">
<h1>{{ content.name }} <span class="small text-muted">({{ position + 1 }}/{{length}})</span></h1>
</div>
<div class="col-md-5 text-right">
<div id="nav-buttons" class="col-md-5 text-right" {{ 'hidden' if content_type=='Quiz' }}>
{% if previous %}
<a href="/lms/content?program={{ program }}&course={{ course }}&topic={{ topic }}&type={{ previous.content_type }}&content={{ previous.content }}" class='btn btn-outline-secondary'>Previous</a>
{% else %}
@ -70,7 +70,7 @@
</div>
</div>
<div id="player" data-plyr-provider="{{ content.provider|lower }}" data-plyr-embed-id="{{ content.url }}"></div>
<div class="my-5">
<div class="my-5" style="line-height: 1.8em;">
{{ content.description }}
</div>
{% endmacro %}
@ -95,6 +95,18 @@
</div>
{% endmacro %}
{% macro quiz() %}
<div class="mb-5">
<div class="row">
<div class="col-md-7">
<h1>{{ content.name }} <span class="small text-muted">({{ position + 1 }}/{{length}})</span></h1>
</div>
</div>
</div>
<div id="quiz-wrapper">
</div>
{% endmacro %}
{% block content %}
<section class="section">
<div>
@ -104,7 +116,7 @@
{% elif content_type=='Article'%}
{{ article() }}
{% elif content_type=='Quiz' %}
<h2>Quiz: {{ content.name }}</h2>
{{ quiz() }}
{% endif %}
</div>
</div>
@ -113,20 +125,41 @@
{% block script %}
{% if content_type=='Video' %}
<script src="https://cdn.plyr.io/3.5.3/plyr.js"></script>
<script src="https://cdn.plyr.io/3.5.3/plyr.js"></script>
{% elif content_type == 'Quiz' %}
<script src='/assets/erpnext/js/education/lms/quiz.js'></script>
{% endif %}
<script>
{% if content_type=='Video' %}
{% if content_type == 'Video' %}
const player = new Plyr('#player');
{% elif content_type == 'Quiz' %}
{% if next %}
const quiz_exit_button = 'Next'
const next_url = '/lms/content?program={{ program }}&course={{ course }}&topic={{ topic }}&type={{ next.content_type }}&content={{ next.content }}'
{% else %}
const quiz_exit_button = 'Finish Course'
const next_url = '/lms/course?name={{ course }}&program={{ program }}'
{% endif %}
frappe.ready(() => {
const quiz = new Quiz(document.getElementById('quiz-wrapper'), {
name: '{{ content.name }}',
course: '{{ course }}',
quiz_exit_button: quiz_exit_button,
next_url: next_url
})
window.quiz = quiz;
})
{% endif %}
{% if content_type != 'Quiz' %}
frappe.ready(() => {
next = document.getElementById('nextButton')
next.disabled = false;
})
function handle(url) {
function handle(url) {
opts = {
method: "erpnext.education.utils.add_activity",
args: {
@ -139,5 +172,7 @@
window.location.href = url;
})
}
{% endif %}
</script>
{% endblock %}

View File

@ -27,7 +27,7 @@ def get_context(context):
# Set context for content to be displayer
context.content = frappe.get_doc(content_type, content)
context.content = frappe.get_doc(content_type, content).as_dict()
context.content_type = content_type
context.program = program
context.course = course