feat: quiz-creation-ui

This commit is contained in:
Jannat Patel
2022-08-18 19:52:27 +05:30
parent eb50f6fd8f
commit e1b16e9ae3
12 changed files with 246 additions and 17 deletions

View File

@@ -137,8 +137,8 @@ website_route_rules = [
{"from_route": "/courses/<course>/<certificate>", "to_route": "courses/certificate"},
{"from_route": "/courses/<course>/learn", "to_route": "batch/learn"},
{"from_route": "/courses/<course>/learn/<int:chapter>.<int:lesson>", "to_route": "batch/learn"},
{"from_route": "/courses/quiz-list", "to_route": "batch/quiz_list"},
{"from_route": "/courses/quiz/<quiz-name>", "to_route": "batch/quiz"},
{"from_route": "/quizzes", "to_route": "batch/quiz_list"},
{"from_route": "/quizzes/<quizname>", "to_route": "batch/quiz"},
{"from_route": "/courses/<course>/progress", "to_route": "batch/progress"},
{"from_route": "/courses/<course>/join", "to_route": "batch/join"},
{"from_route": "/courses/<course>/manage", "to_route": "cohorts"},

View File

@@ -10,6 +10,7 @@ class LMSQuiz(Document):
def validate(self):
self.validate_correct_answers()
def validate_correct_answers(self):
for question in self.questions:
correct_options = self.get_correct_options(question)
@@ -20,10 +21,12 @@ class LMSQuiz(Document):
if not len(correct_options):
frappe.throw(_("At least one answer must be correct for this question: {0}").format(frappe.bold(question.question)))
def get_correct_options(self, question):
correct_option_fields = ["is_correct_1", "is_correct_2", "is_correct_3", "is_correct_4"]
return list(filter(lambda x: question.get(x) == 1, correct_option_fields))
def get_last_submission_details(self):
"""Returns the latest submission for this user.
"""
@@ -43,6 +46,7 @@ class LMSQuiz(Document):
if result:
return result[0]
@frappe.whitelist()
def quiz_summary(quiz, results):
score = 0

View File

@@ -1,14 +1,14 @@
<div class="course-home-outline">
{% if course.edit_mode and course.name %}
<div>
<button class="btn btn-md btn-secondary btn-chapter pull-right"> {{ _("New Chapter") }} </button>
</div>
<button class="btn btn-md btn-secondary btn-chapter pull-right"> {{ _("New Chapter") }} </button>
{% endif %}
<div class="course-home-headings">
{% if course.name and (course.edit_mode or get_chapters(course.name) | length) %}
<div class="course-home-headings" id="outline-heading">
{{ _("Course Content") }}
</div>
{% endif %}
{% if get_chapters(course.name) | length %}

View File

@@ -910,8 +910,9 @@ pre {
}
.empty-state-text {
flex: 1;
margin-left: 1.25rem;
flex: 1;
margin-left: 1.25rem;
text-align: center;
}
.empty-state-heading {
@@ -1546,6 +1547,7 @@ li {
border-radius: var(--border-radius);
border: 1px dashed var(--gray-600);
padding: 0.5rem 0.75rem;
color: var(--gray-900);
}
[contenteditable]:empty:before {
@@ -1599,3 +1601,22 @@ li {
.table {
margin-bottom: 0;
}
.quiz-card {
border: 1px solid var(--dark-border-color);
border-radius: var(--border-radius);
padding: 1.25rem;
margin-top: 1.25rem;
font-size: var(--text-base);
}
.option-input {
width: 45%;
margin-right: 1rem;
}
.option-checkbox {
width: 15%;
display: flex;
align-items: center;
}

View File

@@ -109,14 +109,16 @@ const add_chapter = (e) => {
return;
}
let next_index = $("[data-index]").last().data("index");
let next_index = $("[data-index]").last().data("index") || 1;
let add_after = $(`.chapter-parent:last`).length ? $(`.chapter-parent:last`) : $("#outline-heading");
console.log(add_after)
$(`<div class="chapter-parent chapter-edit new-chapter">
<div contenteditable="true" data-placeholder="${__('Chapter Name')}" class="chapter-title-main"></div>
<div class="small my-2" contenteditable="true" data-placeholder="${__('Short Description')}"
class="chapter-description"></div>
<div class="chapter-description small my-2" contenteditable="true"
data-placeholder="${__('Short Description')}"></div>
<button class="btn btn-sm btn-secondary d-block btn-save-chapter"
data-index="${next_index}"> ${__('Save')} </button>
</div>`).insertAfter(`.chapter-parent:last`);
</div>`).insertAfter(add_after);
scroll_to_chapter_container();
};
@@ -133,7 +135,9 @@ const scroll_to_chapter_container = () => {
const save_chapter = (e) => {
let target = $(e.currentTarget);
let parent = target.closest(".chapter-parent");
console.log(parent)
console.log(parent.find(".chapter-description"))
debugger;
frappe.call({
method: "lms.lms.doctype.lms_course.lms_course.save_chapter",
args: {

View File

@@ -186,7 +186,7 @@
<div class="mt-4">
<button class="btn btn-primary btn-sm btn-lesson pull-right ml-2"> {{ _("Save") }} </button>
<button class="btn btn-secondary btn-sm pull-right btn-back ml-2"> {{ _("Back to Lesson") }} </button>
<a class="btn btn-secondary btn-sm pull-right" href="/courses/quiz-list"> {{ _("Create a Quiz") }} </a>
<a class="btn btn-secondary btn-sm pull-right" href="/quizzes"> {{ _("Create a Quiz") }} </a>
<div class="attachments-parent">
<div class="attachment-controls">

View File

@@ -0,0 +1,74 @@
{% extends "templates/base.html" %}
{% block title %}
{{ _("Quiz List") }}
{% endblock %}
{% block head_include %}
{% include "public/icons/symbol-defs.svg" %}
{% endblock %}
{% block content %}
<div class="common-page-style" style="background-color: var(--fg-color);">
<div class="container">
{{ BreadCrumb(quiz) }}
{{ QuizCard(quiz) }}
</div>
</div>
{% endblock %}
{% macro BreadCrumb(quiz) %}
<div class="breadcrumb">
<a class="dark-links" href="/courses">{{ _("Quizzes") }}</a>
<img class="ml-1 mr-1" src="/assets/lms/icons/chevron-right.svg">
<span class="breadcrumb-destination">{{ quiz.title if quiz.title else _("New Quiz") }}</span>
</div>
{% endmacro %}
{% macro QuizCard(quiz) %}
<div style="width: 60%;">
<div class="course-home-headings mb-2" data-placeholder="{{ _('Quiz Title') }}" id="quiz-title"
contenteditable="true">{% if quiz.title %}{{ quiz.title }}{% endif %}</div>
<div class="mt-4">
<button class="btn btn-secondary btn-sm btn-question"> {{ _("New Question") }} </button>
<button class="btn btn-primary btn-sm btn-save-question ml-2 hide"> {{ _("Save") }} </button>
</div>
{% if quiz.question %}
{% for question in quiz.questions %}
<div class="quiz-card">
<div contenteditable="true" data-placeholder="{{ _('Question') }}"
class="mb-4">{% if question.question %} {{ question.question }} {% endif %}</div>
{% for num in range(1,5) %}
{% set option = question["option_" + frappe.utils.cstr(num)] %}
{% set explanation = question["explanation_" + frappe.utils.cstr(num)] %}
<div class="mt-4">
<label class=""> {{ _("Option") }} {{ frappe.utils.cstr(num) }} </label>
<div class="d-flex justify-content-between">
<div contenteditable="true" data-placeholder="{{ _('Option') }}"
class="option-input">{% if option %}{{ option }}{% endif %}</div>
<div contenteditable="true" data-placeholder="{{ _('Explanation') }}"
class="option-input">{% if explanation %}{{ explanation }}{% endif %}</div>
<div class="option-checkbox">
<input type="checkbox" {% if question['is_correct_' + frappe.utils.cstr(num)] %} checked {% endif %}>
<label class="mb-0"> {{ _("Is Correct") }} </label>
</div>
</div>
</div>
{% endfor %}
</div>
{% endfor %}
{% endif %}
</div>
{% endmacro %}

91
lms/www/batch/quiz.js Normal file
View File

@@ -0,0 +1,91 @@
frappe.ready(() => {
$(".btn-question").click((e) => {
add_question(e);
});
$(".btn-save-question").click((e) => {
save_question(e);
});
});
const add_question = (e) => {
let add_after = $(".quiz-card").length ? $(".quiz-card") : $("#quiz-title");
let question_template = `<div class="quiz-card">
<div contenteditable="true" data-placeholder="${__("Question")}" class="question mb-4"></div>
</div>`;
$(question_template).insertAfter(add_after);
get_question_template();
$(".btn-save-question").removeClass("hide");
};
const get_question_template = () => {
Array.from({length: 4}, (x, num) => {
let option_template = get_option_template(num + 1);
let add_after = $(".quiz-card:last .option-group").length ? $(".quiz-card:last .option-group").last() : $(".question:last");
question_template = $(option_template).insertAfter(add_after);
});
};
const get_option_template = (num) => {
return `<div class="option-group mt-4">
<label class="">${__("Option")} ${num}</label>
<div class="d-flex justify-content-between option-${num}">
<div contenteditable="true" data-placeholder="${ __("Option") }"
class="option-input"></div>
<div contenteditable="true" data-placeholder="${ __('Explanation') }"
class="option-input"></div>
<div class="option-checkbox">
<input type="checkbox">
<label class="mb-0"> ${ __("Is Correct") } </label>
</div>
</div>
</div>`;
};
const save_question = (e) => {
if (!$("#quiz-title").text()) {
frappe.throw(__("Quiz Title is mandatory."));
}
console.log(get_questions());
debugger;
frappe.call({
method: lms.lms.doctype.lms_quiz.lms_quiz.save_quiz,
args: {
"quiz-title": $("#quiz-title"),
"questions": get_questions()
},
callback: (data) => {
}
});
};
const get_questions = () => {
let questions = [];
$(".quiz-card").each((i, elem) => {
if (!$(elem).find(".question").text())
return;
let question_details = {};
question_details["question"] = $(elem).find(".question").text();
Array.from({length: 4}, (x, i) => {
let num = i + 1;
question_details[`option_${num}`] = $(`.option-${num} .option-input:first`).text();
question_details[`explanation_${num}`] = $(`.option-${num} .option-input:last`).text();
question_details[`is_correct_${num}`] = $(`.option-${num} .option-checkbox`).find("input").prop("checked");
});
questions.push(question_details);
});
return questions
};

View File

@@ -0,0 +1,20 @@
import frappe
from frappe.utils import cstr
def get_context(context):
quizname = frappe.form_dict["quizname"]
if quizname == "new-quiz":
context.quiz = frappe._dict()
context.quiz.edit_mode = 1
else:
fields_arr = []
for num in range(1,5):
fields_arr.append("option_" + cstr(num))
fields_arr.append("is_correct_" + cstr(num))
fields_arr.append("explanation_" + cstr(num))
fields_arr.append("question")
context.quiz = frappe.db.get_value("LMS Quiz", quizname, ["title"], as_dict=1)
context.quiz.questions = frappe.get_all("LMS Quiz Question", {
"parent": quizname
}, fields_arr)

View File

@@ -12,7 +12,8 @@
{% block content %}
<div class="common-page-style">
<div class="container">
<a class="btn btn-primary btn-sm pull-right"> {{ _("Add Quiz") }} </a>
{% if quiz_list | length %}
<a class="btn btn-secondary btn-sm pull-right" href="/quizzes/new-quiz"> {{ _("Add Quiz") }} </a>
<div class="course-home-headings"> {{ _("Quiz List") }} </div>
<div class="common-card-style">
<table class="table">
@@ -24,12 +25,23 @@
<tr style="position: relative; color: var(--text-color);">
<td> {{ loop.index }} </td>
<td>
<a class="button-links" href="/courses/quiz/{{ quiz.name }}">{{ quiz.name }}</a>
<a class="button-links" href="/quizzes/{{ quiz.name }}">{{ quiz.name }}</a>
</td>
</tr>
{% endfor %}
</table>
</div>
{% else %}
<div class="empty-state">
<div class="empty-state-text">
<div class="empty-state-heading">{{ _("You have not created any quiz yet.") }}</div>
<div class="course-meta mb-6">{{ _("Create a quiz and add it to your course to engage users.") }}</div>
<a class="btn btn-secondary btn-sm"
href="{% if frappe.session.user == 'Guest' %} /login?redirect-to=/quizzes {% else %} /quizzes/new-quiz {% endif %}">
{{ _("Add Quiz") }} </a>
</div>
</div>
{% endif %}
</div>
</div>
{% endblock %}

View File

@@ -336,7 +336,7 @@ const save_course = (e) => {
"video_link": $("#video-link").text(),
"image": $("#image").attr("href"),
"description": $("#description").text(),
"course": $("#title").data("course")
"course": $("#title").data("course") ? $("#title").data("course") : ""
},
callback: (data) => {
window.location.href = `/courses/${data.message}?edit=1`;
@@ -344,6 +344,7 @@ const save_course = (e) => {
});
};
const remove_tag = (e) => {
$(e.currentTarget).closest(".course-card-pills").remove();
};

View File

@@ -11,6 +11,8 @@ def get_context(context):
redirect_to_courses_list()
if course_name == "new-course":
if frappe.session.user == "Guest":
redirect_to_courses_list()
context.course = frappe._dict()
context.course.edit_mode = True
context.membership = None