diff --git a/lms/lms/doctype/lms_certificate/lms_certificate.js b/lms/lms/doctype/lms_certificate/lms_certificate.js
index 6a341a63..8d6f5e12 100644
--- a/lms/lms/doctype/lms_certificate/lms_certificate.js
+++ b/lms/lms/doctype/lms_certificate/lms_certificate.js
@@ -2,7 +2,7 @@
// For license information, please see license.txt
frappe.ui.form.on('LMS Certificate', {
- onload: function (frm) {
+ onload: (frm) => {
frm.set_query("member", function (doc) {
return {
filters: {
@@ -10,5 +10,8 @@ frappe.ui.form.on('LMS Certificate', {
}
};
});
+ },
+ refresh: (frm) => {
+ if (frm.doc.name) frm.add_web_link(`/courses/${frm.doc.course}/${frm.doc.name}`, 'See on Website')
}
});
diff --git a/lms/lms/doctype/lms_certificate/lms_certificate.py b/lms/lms/doctype/lms_certificate/lms_certificate.py
index 0a6e9b19..93487cad 100644
--- a/lms/lms/doctype/lms_certificate/lms_certificate.py
+++ b/lms/lms/doctype/lms_certificate/lms_certificate.py
@@ -10,16 +10,15 @@ from lms.lms.utils import is_certified
class LMSCertificate(Document):
- def validate(self):
+ def before_insert(self):
certificates = frappe.get_all("LMS Certificate", {
- "member": self.member,
- "course": self.course,
- "expiry_date": [">", nowdate()]
- })
+ "member": self.member,
+ "course": self.course
+ })
if len(certificates):
full_name = frappe.db.get_value("User", self.member, "full_name")
course_name = frappe.db.get_value("LMS Course", self.course, "title")
- frappe.throw(_("There is already a valid certificate for user {0} for the course {1}").format(full_name, course_name))
+ frappe.throw(_("{0} is already certified for the course {1}").format(full_name, course_name))
@frappe.whitelist()
def create_certificate(course):
@@ -35,11 +34,11 @@ def create_certificate(course):
expiry_date = add_years(nowdate(), expires_after_yrs)
certificate = frappe.get_doc({
- "doctype": "LMS Certificate",
- "member": frappe.session.user,
- "course": course,
- "issue_date": nowdate(),
- "expiry_date": expiry_date
- })
+ "doctype": "LMS Certificate",
+ "member": frappe.session.user,
+ "course": course,
+ "issue_date": nowdate(),
+ "expiry_date": expiry_date
+ })
certificate.save(ignore_permissions=True)
return certificate
diff --git a/lms/lms/doctype/lms_quiz/lms_quiz.json b/lms/lms/doctype/lms_quiz/lms_quiz.json
index 129291a4..e68caf67 100644
--- a/lms/lms/doctype/lms_quiz/lms_quiz.json
+++ b/lms/lms/doctype/lms_quiz/lms_quiz.json
@@ -10,7 +10,9 @@
"field_order": [
"title",
"questions",
- "lesson"
+ "lesson",
+ "max_attempts",
+ "time"
],
"fields": [
{
@@ -31,11 +33,23 @@
"label": "Lesson",
"options": "Course Lesson",
"read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "max_attempts",
+ "fieldtype": "Int",
+ "label": "Max Attempts"
+ },
+ {
+ "default": "0",
+ "fieldname": "time",
+ "fieldtype": "Int",
+ "label": "Time Per Question (in Seconds)"
}
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-09-30 13:10:06.929358",
+ "modified": "2022-05-16 14:47:55.364743",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Quiz",
@@ -57,5 +71,6 @@
],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/lms/lms/doctype/lms_quiz/lms_quiz.py b/lms/lms/doctype/lms_quiz/lms_quiz.py
index dd0b4809..098fa7e4 100644
--- a/lms/lms/doctype/lms_quiz/lms_quiz.py
+++ b/lms/lms/doctype/lms_quiz/lms_quiz.py
@@ -46,13 +46,14 @@ class LMSQuiz(Document):
@frappe.whitelist()
def quiz_summary(quiz, results):
score = 0
- results = json.loads(results)
+ results = results and json.loads(results)
for result in results:
correct = result["is_correct"][0]
result["question"] = frappe.db.get_value("LMS Quiz Question",
- {"parent": quiz, "idx": result["question_index"]},
- ["question"])
+ {"parent": quiz,
+ "idx": result["question_index"]},
+ ["question"])
for point in result["is_correct"]:
correct = correct and point
diff --git a/lms/lms/doctype/lms_settings/lms_settings.json b/lms/lms/doctype/lms_settings/lms_settings.json
index c2b8b03b..afbbbc1a 100644
--- a/lms/lms/doctype/lms_settings/lms_settings.json
+++ b/lms/lms/doctype/lms_settings/lms_settings.json
@@ -10,6 +10,7 @@
"force_profile_completion",
"column_break_2",
"search_placeholder",
+ "custom_certificate_template",
"livecode_url",
"signup_settings_section",
"terms_of_use",
@@ -63,7 +64,7 @@
"depends_on": "show_search",
"fieldname": "search_placeholder",
"fieldtype": "Data",
- "label": "Search Field Placeholder"
+ "label": "Course List Search Bar Placeholder"
},
{
"default": "0",
@@ -137,12 +138,18 @@
"fieldname": "user_category",
"fieldtype": "Check",
"label": "Ask User Category during Signup"
+ },
+ {
+ "fieldname": "custom_certificate_template",
+ "fieldtype": "Link",
+ "label": "Custom Certificate Template",
+ "options": "Web Template"
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2022-04-20 09:09:12.369728",
+ "modified": "2022-05-09 09:55:24.519269",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Settings",
diff --git a/lms/plugins.py b/lms/plugins.py
index cd03d2a6..354fe281 100644
--- a/lms/plugins.py
+++ b/lms/plugins.py
@@ -103,7 +103,25 @@ def set_mandatory_fields_for_profile():
def quiz_renderer(quiz_name):
quiz = frappe.get_doc("LMS Quiz", quiz_name)
- context = dict(quiz=quiz)
+
+ context = {
+ "quiz": quiz
+ }
+
+ no_of_attempts = frappe.db.count("LMS Quiz Submission", {
+ "owner": frappe.session.user,
+ "quiz": quiz_name})
+
+ if quiz.max_attempts and no_of_attempts >= quiz.max_attempts:
+ last_attempt_score = frappe.db.get_value("LMS Quiz Submission", {
+ "owner": frappe.session.user,
+ "quiz": quiz_name
+ }, ["score"])
+
+ context.update({
+ "attempts_exceeded": True,
+ "last_attempt_score": last_attempt_score
+ })
return frappe.render_template("templates/quiz.html", context)
def exercise_renderer(argument):
diff --git a/lms/public/css/style.css b/lms/public/css/style.css
index 2cc604ec..835d909f 100644
--- a/lms/public/css/style.css
+++ b/lms/public/css/style.css
@@ -576,7 +576,7 @@ input[type=checkbox] {
}
.lesson-content-card {
- margin-top: 1rem;
+ margin: 3rem 0;
}
.lesson-page {
@@ -837,15 +837,22 @@ pre {
}
.certificate-content {
- padding: 2.5rem 3rem;
- background-color: #FFFFFF;
+ background-color: #FFFFFF;
+ border-width: 100px;
+ border-style: solid;
+}
+
+@media (max-width: 500px) {
+ .certificate-content {
+ border-width: 50px;
+ }
}
.certificate-footer {
display: flex;
- justify-content: space-between;
- width: 30%;
- margin: 5rem auto 0;
+ justify-content: center;
+ margin: 4rem auto 0;
+ width: fit-content;
}
.certificate-price {
@@ -854,15 +861,15 @@ pre {
}
.certificate-ribbon {
- background-color: var(--primary-color);
- padding: 0.5rem;
- border-radius: var(--border-radius-md);
+ background-color: var(--primary-color);
+ padding: 0.5rem;
+ border-radius: var(--border-radius-md);
}
.certificate-heading {
- font-size: 1.5rem;
- font-weight: 500;
- color: var(--text-color);
+ font-size: 2rem;
+ font-weight: 500;
+ color: var(--text-color);
}
.certificate-para {
@@ -876,24 +883,19 @@ pre {
box-shadow: var(--shadow-sm);
padding: 1rem;
text-align: center;
- margin: 0 6rem;
}
.certificate-footer-item {
color: var(--text-color);
- font-weight: 500;
+ font-weight: bold;
+ font-family: cursive;
+ font-size: 1.25rem;
}
.certificate-logo {
height: 1.5rem;
}
-@media (max-width: 1050px) {
- .certificate-footer {
- width: 50%;
- }
-}
-
@media (max-width: 768px) {
.certificate-card {
margin: 0;
@@ -904,10 +906,6 @@ pre {
.certificate-content {
padding: 1rem;
}
-
- .certificate-footer {
- width: 100%;
- }
}
.profile-card {
@@ -1341,8 +1339,7 @@ pre {
font-size: var(--text-lg);
color: var(--gray-900);
font-weight: 600;
- flex-grow: 1;
- margin-left: 1rem;
+ width: 75%;
}
.profile-page-body {
diff --git a/lms/public/images/border.png b/lms/public/images/border.png
new file mode 100644
index 00000000..f3c6e63b
Binary files /dev/null and b/lms/public/images/border.png differ
diff --git a/lms/templates/certificate.html b/lms/templates/certificate.html
index b16ff782..8064b42c 100644
--- a/lms/templates/certificate.html
+++ b/lms/templates/certificate.html
@@ -1,50 +1,33 @@
-
-
-

-
- {{ _("This certifies that") }}
+
+

+
+ {{ _("This certifies that") }}
+
+
{{ member.full_name }}
+
{{ _("has successfully completed the course on") }}
+ {{ course.title }} on {{ frappe.utils.format_date(certificate.issue_date, "medium") }}.
+
+
-
diff --git a/lms/templates/quiz.html b/lms/templates/quiz.html
index 1e68d995..8674f1ce 100644
--- a/lms/templates/quiz.html
+++ b/lms/templates/quiz.html
@@ -1,18 +1,35 @@
-
{{ quiz.title }}
+{% if attempts_exceeded %}
+
+
{{ quiz.title }}
+
{{ _("You have already exceeded the maximum number of attempts allowed for this quiz.") }}
+
{{ _("Your latest score is {0}.").format(last_attempt_score) }}
+
+{% else %}
+
{{ quiz.title }}
diff --git a/lms/www/batch/learn.js b/lms/www/batch/learn.js
index b44d5af9..dc533e7d 100644
--- a/lms/www/batch/learn.js
+++ b/lms/www/batch/learn.js
@@ -45,6 +45,18 @@ frappe.ready(() => {
clear_work(e);
});
+ $(".btn-start-quiz").click((e) => {
+ $("#start-banner").addClass("hide");
+ $("#quiz-form").removeClass("hide");
+ mark_active_question();
+ });
+
+ if ($("#quiz-title").data("max-attempts")) {
+ window.addEventListener("beforeunload", (e) => {
+ e.returnValue = "";
+ $(".active-question").length && quiz_summary();
+ });
+ }
});
const save_current_lesson = () => {
@@ -65,19 +77,19 @@ const enable_check = (e) => {
};
const mark_active_question = (e = undefined) => {
- var current_index;
- var next_index = 1;
- if (e) {
- e.preventDefault();
- current_index = $(".active-question").attr("data-qt-index");
- next_index = parseInt(current_index) + 1;
- }
- $(".question").addClass("hide").removeClass("active-question");
- $(`.question[data-qt-index='${next_index}']`).removeClass("hide").addClass("active-question");
- $(".current-question").text(`${next_index}`);
- $("#check").removeClass("hide").attr("disabled", true);
- $("#next").addClass("hide");
- $(".explanation").addClass("hide");
+ $(".timer").addClass("hide");
+ calculate_and_display_time(100);
+ $(".timer").removeClass("hide");
+
+ let current_index = $(".active-question").attr("data-qt-index") || 0;
+ let next_index = parseInt(current_index) + 1;
+ $(".question").addClass("hide").removeClass("active-question");
+ $(`.question[data-qt-index='${next_index}']`).removeClass("hide").addClass("active-question");
+ $(".current-question").text(`${next_index}`);
+ $("#check").removeClass("hide").attr("disabled", true);
+ $("#next").addClass("hide");
+ $(".explanation").addClass("hide");
+ initialize_timer();
};
const mark_progress = (e) => {
@@ -85,7 +97,7 @@ const mark_progress = (e) => {
if ($(e.currentTarget).prop("nodeName") != "INPUT")
e.preventDefault();
else
- return
+ return;
const target = $(e.currentTarget).attr("data-progress") ? $(e.currentTarget) : $("input.mark-progress");
const current_status = $(".lesson-progress").hasClass("hide") ? "Incomplete": "Complete";
@@ -152,57 +164,56 @@ const move_to_next_lesson = (status, e) => {
}
};
-const quiz_summary = (e) => {
- e.preventDefault();
- var quiz_name = $("#quiz-title").text();
- var total_questions = $(".question").length;
-
- frappe.call({
- method: "lms.lms.doctype.lms_quiz.lms_quiz.quiz_summary",
- args: {
- "quiz": quiz_name,
- "results": localStorage.getItem(quiz_name)
- },
- callback: (data) => {
- var message = data.message == total_questions ? "Excellent Work" : "You were almost there."
- $(".question").addClass("hide");
- $("#summary").addClass("hide");
- $("#quiz-form").parent().prepend(
- `
${message} 👏
-
${data.message}/${total_questions} correct.
`);
- $("#try-again").removeClass("hide");
- }
- })
+const quiz_summary = (e=undefined) => {
+ e && e.preventDefault();
+ var quiz_name = $("#quiz-title").text();
+ var total_questions = $(".question").length;
+ frappe.call({
+ method: "lms.lms.doctype.lms_quiz.lms_quiz.quiz_summary",
+ args: {
+ "quiz": quiz_name,
+ "results": localStorage.getItem(quiz_name)
+ },
+ callback: (data) => {
+ var message = data.message == total_questions ? __("Excellent Work 👏") : __("Better luck next time")
+ $(".question").addClass("hide");
+ $("#summary").addClass("hide");
+ $("#quiz-form").parent().prepend(
+ `
${message}
+
${data.message}/${total_questions}
`);
+ $("#try-again").removeClass("hide");
+ }
+ })
};
const try_quiz_again = (e) => {
window.location.reload();
};
-const check_answer = (e) => {
- e.preventDefault();
+const check_answer = (e=undefined) => {
+ e && e.preventDefault();
+ clearInterval(self.timer);
+ $(".timer").addClass("hide");
+ var quiz_name = $("#quiz-title").text();
+ var total_questions = $(".question").length;
+ var current_index = $(".active-question").attr("data-qt-index");
- var quiz_name = $("#quiz-title").text();
- var total_questions = $(".question").length;
- var current_index = $(".active-question").attr("data-qt-index");
+ $(".explanation").removeClass("hide");
+ $("#check").addClass("hide");
- $(".explanation").removeClass("hide");
- $("#check").addClass("hide");
-
- if (current_index == total_questions) {
- if ($(".eligible-for-submission").length) {
- $("#summary").removeClass("hide")
+ if (current_index == total_questions) {
+ if ($(".eligible-for-submission").length) {
+ $("#summary").removeClass("hide");
+ }
+ else {
+ $("#submission-message").removeClass("hide");
+ }
}
else {
- $("#submission-message").removeClass("hide");
+ $("#next").removeClass("hide");
}
- }
- else {
- $("#next").removeClass("hide")
- }
-
- var [answer, is_correct] = parse_options();
- add_to_local_storage(quiz_name, current_index, answer, is_correct)
+ var [answer, is_correct] = parse_options();
+ add_to_local_storage(quiz_name, current_index, answer, is_correct);
};
const parse_options = () => {
@@ -381,3 +392,35 @@ const fetch_assignments = () => {
}
});
};
+
+const initialize_timer = () => {
+ this.time_left = $(".timer").data("time");
+ calculate_and_display_time(100, this.time_left);
+ $(".timer").removeClass("hide");
+ const total_time = $(".timer").data("time");
+ this.start_time = new Date().getTime();
+ const self = this;
+ let old_diff;
+
+ this.timer = setInterval(function () {
+ var diff = (new Date().getTime() - self.start_time)/1000;
+ var variation = old_diff ? diff - old_diff : diff;
+ old_diff = diff;
+ self.time_left -= variation;
+ let percent_time = (self.time_left / total_time) * 100;
+ calculate_and_display_time(percent_time);
+ if (self.time_left <= 0) {
+ clearInterval(self.timer);
+ $(".timer").addClass("hide");
+ check_answer();
+ }
+ }, 100);
+};
+
+const calculate_and_display_time = (percent_time) => {
+ $(".timer .progress-bar").attr("aria-valuenow", percent_time);
+ $(".timer .progress-bar").attr("aria-valuemax", percent_time);
+ $(".timer .progress-bar").css("width", `${percent_time}%`);
+ let progress_color = percent_time < 20 ? "red" : "var(--primary-color)";
+ $(".timer .progress-bar").css("background-color", progress_color);
+};
diff --git a/lms/www/courses/certificate.html b/lms/www/courses/certificate.html
index dbc3cc06..dd5e873c 100644
--- a/lms/www/courses/certificate.html
+++ b/lms/www/courses/certificate.html
@@ -5,18 +5,23 @@
- {% if certificate.member == frappe.session.user %}
+
-
+
+ {% if custom_template %}
+ {{ custom_template }}
+ {% else %}
{% include "lms/templates/certificate.html" %}
+ {% endif %}
+
+
{% endblock %}
diff --git a/lms/www/courses/certificate.js b/lms/www/courses/certificate.js
index 4b8b66a2..f8fe5933 100644
--- a/lms/www/courses/certificate.js
+++ b/lms/www/courses/certificate.js
@@ -1,16 +1,16 @@
frappe.ready(() => {
- $("#export-as-pdf").click((e) => {
- export_as_pdf(e);
- })
+ $("#export-as-pdf").click((e) => {
+ export_as_pdf(e);
+ })
-})
+});
-var export_as_pdf = (e) => {
- var button = $(e.currentTarget);
- button.text(__("Exporting..."));
+const export_as_pdf = (e) => {
+ let button = $(e.currentTarget);
+ button.text(__("Exporting..."));
- html2canvas(document.querySelector('.common-card-style'), {
+ html2canvas(document.querySelector('.certificate-card'), {
scrollY: -window.scrollY,
scrollX: 0
}).then(function(canvas) {
diff --git a/lms/www/courses/certificate.py b/lms/www/courses/certificate.py
index fc2fa0be..bf0d2ec6 100644
--- a/lms/www/courses/certificate.py
+++ b/lms/www/courses/certificate.py
@@ -1,4 +1,6 @@
import frappe
+from frappe.utils.jinja import render_template
+from lms.lms.utils import get_instructors
def get_context(context):
context.no_cache = 1
@@ -15,16 +17,21 @@ def get_context(context):
if context.certificate.course != course_name:
redirect_to_course_list()
- context.course = frappe.db.get_value("LMS Course", course_name,
- ["instructor", "title", "name"], as_dict=True)
-
- context.instructor = frappe.db.get_value("User", context.course.instructor,
- ["full_name", "username"], as_dict=True)
-
+ context.course = frappe.db.get_value("LMS Course", course_name, ["title", "name", "image"], as_dict=True)
+ context.instructors = (", ").join([x.full_name for x in get_instructors(course_name)])
context.member = frappe.db.get_value("User", context.certificate.member,
["full_name"], as_dict=True)
context.logo = frappe.db.get_single_value("Website Settings", "banner_image")
+ template_name = frappe.db.get_single_value("LMS Settings", "custom_certificate_template")
+ context.custom_certificate_template = frappe.db.get_value("Web Template", template_name, "template")
+ context.custom_template = render_template(context.custom_certificate_template, context)
+
+ context.metatags = {
+ "title": f"{member.full_name} - {course.title}",
+ "image": course.image,
+ "keywords": course.title, member.full_name
+ }
def redirect_to_course_list():
frappe.local.flags.redirect_location = "/courses"