feat: quiz-creation-ui
This commit is contained in:
@@ -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"},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
91
lms/www/batch/quiz.js
Normal 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
|
||||
};
|
||||
@@ -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)
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user