Merge branch 'main' of https://github.com/frappe/lms into ui-fixes

This commit is contained in:
Jannat Patel
2022-05-18 11:13:26 +05:30
15 changed files with 282 additions and 184 deletions

View File

@@ -2,7 +2,7 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on('LMS Certificate', { frappe.ui.form.on('LMS Certificate', {
onload: function (frm) { onload: (frm) => {
frm.set_query("member", function (doc) { frm.set_query("member", function (doc) {
return { return {
filters: { 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')
} }
}); });

View File

@@ -10,16 +10,15 @@ from lms.lms.utils import is_certified
class LMSCertificate(Document): class LMSCertificate(Document):
def validate(self): def before_insert(self):
certificates = frappe.get_all("LMS Certificate", { certificates = frappe.get_all("LMS Certificate", {
"member": self.member, "member": self.member,
"course": self.course, "course": self.course
"expiry_date": [">", nowdate()] })
})
if len(certificates): if len(certificates):
full_name = frappe.db.get_value("User", self.member, "full_name") full_name = frappe.db.get_value("User", self.member, "full_name")
course_name = frappe.db.get_value("LMS Course", self.course, "title") 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() @frappe.whitelist()
def create_certificate(course): def create_certificate(course):
@@ -35,11 +34,11 @@ def create_certificate(course):
expiry_date = add_years(nowdate(), expires_after_yrs) expiry_date = add_years(nowdate(), expires_after_yrs)
certificate = frappe.get_doc({ certificate = frappe.get_doc({
"doctype": "LMS Certificate", "doctype": "LMS Certificate",
"member": frappe.session.user, "member": frappe.session.user,
"course": course, "course": course,
"issue_date": nowdate(), "issue_date": nowdate(),
"expiry_date": expiry_date "expiry_date": expiry_date
}) })
certificate.save(ignore_permissions=True) certificate.save(ignore_permissions=True)
return certificate return certificate

View File

@@ -10,7 +10,9 @@
"field_order": [ "field_order": [
"title", "title",
"questions", "questions",
"lesson" "lesson",
"max_attempts",
"time"
], ],
"fields": [ "fields": [
{ {
@@ -31,11 +33,23 @@
"label": "Lesson", "label": "Lesson",
"options": "Course Lesson", "options": "Course Lesson",
"read_only": 1 "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, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-09-30 13:10:06.929358", "modified": "2022-05-16 14:47:55.364743",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Quiz", "name": "LMS Quiz",
@@ -57,5 +71,6 @@
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -46,13 +46,14 @@ class LMSQuiz(Document):
@frappe.whitelist() @frappe.whitelist()
def quiz_summary(quiz, results): def quiz_summary(quiz, results):
score = 0 score = 0
results = json.loads(results) results = results and json.loads(results)
for result in results: for result in results:
correct = result["is_correct"][0] correct = result["is_correct"][0]
result["question"] = frappe.db.get_value("LMS Quiz Question", result["question"] = frappe.db.get_value("LMS Quiz Question",
{"parent": quiz, "idx": result["question_index"]}, {"parent": quiz,
["question"]) "idx": result["question_index"]},
["question"])
for point in result["is_correct"]: for point in result["is_correct"]:
correct = correct and point correct = correct and point

View File

@@ -10,6 +10,7 @@
"force_profile_completion", "force_profile_completion",
"column_break_2", "column_break_2",
"search_placeholder", "search_placeholder",
"custom_certificate_template",
"livecode_url", "livecode_url",
"signup_settings_section", "signup_settings_section",
"terms_of_use", "terms_of_use",
@@ -63,7 +64,7 @@
"depends_on": "show_search", "depends_on": "show_search",
"fieldname": "search_placeholder", "fieldname": "search_placeholder",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Search Field Placeholder" "label": "Course List Search Bar Placeholder"
}, },
{ {
"default": "0", "default": "0",
@@ -137,12 +138,18 @@
"fieldname": "user_category", "fieldname": "user_category",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Ask User Category during Signup" "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, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2022-04-20 09:09:12.369728", "modified": "2022-05-09 09:55:24.519269",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Settings", "name": "LMS Settings",

View File

@@ -103,7 +103,25 @@ def set_mandatory_fields_for_profile():
def quiz_renderer(quiz_name): def quiz_renderer(quiz_name):
quiz = frappe.get_doc("LMS Quiz", 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) return frappe.render_template("templates/quiz.html", context)
def exercise_renderer(argument): def exercise_renderer(argument):

View File

@@ -576,7 +576,7 @@ input[type=checkbox] {
} }
.lesson-content-card { .lesson-content-card {
margin-top: 1rem; margin: 3rem 0;
} }
.lesson-page { .lesson-page {
@@ -837,15 +837,22 @@ pre {
} }
.certificate-content { .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 { .certificate-footer {
display: flex; display: flex;
justify-content: space-between; justify-content: center;
width: 30%; margin: 4rem auto 0;
margin: 5rem auto 0; width: fit-content;
} }
.certificate-price { .certificate-price {
@@ -854,15 +861,15 @@ pre {
} }
.certificate-ribbon { .certificate-ribbon {
background-color: var(--primary-color); background-color: var(--primary-color);
padding: 0.5rem; padding: 0.5rem;
border-radius: var(--border-radius-md); border-radius: var(--border-radius-md);
} }
.certificate-heading { .certificate-heading {
font-size: 1.5rem; font-size: 2rem;
font-weight: 500; font-weight: 500;
color: var(--text-color); color: var(--text-color);
} }
.certificate-para { .certificate-para {
@@ -876,24 +883,19 @@ pre {
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-sm);
padding: 1rem; padding: 1rem;
text-align: center; text-align: center;
margin: 0 6rem;
} }
.certificate-footer-item { .certificate-footer-item {
color: var(--text-color); color: var(--text-color);
font-weight: 500; font-weight: bold;
font-family: cursive;
font-size: 1.25rem;
} }
.certificate-logo { .certificate-logo {
height: 1.5rem; height: 1.5rem;
} }
@media (max-width: 1050px) {
.certificate-footer {
width: 50%;
}
}
@media (max-width: 768px) { @media (max-width: 768px) {
.certificate-card { .certificate-card {
margin: 0; margin: 0;
@@ -904,10 +906,6 @@ pre {
.certificate-content { .certificate-content {
padding: 1rem; padding: 1rem;
} }
.certificate-footer {
width: 100%;
}
} }
.profile-card { .profile-card {
@@ -1341,8 +1339,7 @@ pre {
font-size: var(--text-lg); font-size: var(--text-lg);
color: var(--gray-900); color: var(--gray-900);
font-weight: 600; font-weight: 600;
flex-grow: 1; width: 75%;
margin-left: 1rem;
} }
.profile-page-body { .profile-page-body {

Binary file not shown.

After

Width:  |  Height:  |  Size: 812 KiB

View File

@@ -1,50 +1,33 @@
<div class="certificate-card"> <div class="certificate-card">
<div class="certificate-ribbon"> <div class="certificate-content" style="border-image: url(/assets/lms/images/border.png) 13% round;">
<div class="certificate-content"> <img src="{{ logo }}" class="certificate-logo">
<img src="{{ logo }}" class="certificate-logo"> <div class="mt-16">
<div class="mt-20"> {{ _("This certifies that") }}
{{ _("This certifies that") }} </div>
<div class="certificate-heading"> {{ member.full_name }} </div>
<div class="mt-2"> {{ _("has successfully completed the course on") }}
<b> {{ course.title }} </b> on {{ frappe.utils.format_date(certificate.issue_date, "medium") }}. </div>
<div class="certificate-footer">
{% if instructors %}
<div class="">
<span class="certificate-footer-item"> {{ instructors }} </span>
<hr class="mt-2 mb-2">
<div class=""> {{ _("Course Instructor") }} </div>
</div> </div>
<div class="certificate-heading"> {{ member.full_name }} </div> {% endif %}
<div class="mt-2"> {{ _("has successfully completed the course on") }} </div>
<div class="certificate-heading"> {{ course.title }} </div>
<div class="certificate-footer">
{% if certificate.issue_date %}
<div class="">
<div class="certificate-footer-item">
{{ frappe.utils.format_date(certificate.issue_date, "medium") }}
</div>
<div>
{{ _("Issue date") }}
</div>
</div>
{% endif %}
{% if certificate.expiry_date %}
<div class="">
<div class="certificate-footer-item">
{{ frappe.utils.format_date(certificate.expiry_date, "medium") }}
</div>
<div>
{{ _("Expiry date") }}
</div>
</div>
{% endif %}
{% if instructor.full_name %}
<div class="">
<div class="certificate-footer-item">
{{ instructor.full_name }}
</div>
<div>
{{ _("Instructor") }}
</div>
</div>
{% endif %}
{% if certificate.expiry_date %}
<div class="ml-8">
<div class="certificate-footer-item">
{{ frappe.utils.format_date(certificate.expiry_date, "medium") }}
</div> </div>
<hr class="mt-2 mb-2">
<div class=""> {{ _("Expiry date") }} </div>
</div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
<script src="/assets/lms/js/html2canvas.js"></script>

View File

@@ -1,18 +1,35 @@
<div id="quiz-title" class="hide">{{ quiz.title }}</div> {% if attempts_exceeded %}
<div class="common-card-style text-center p-5" style="flex-direction: column;">
<div class="font-weight-bold mb-4" style="font-size: var(--text-lg);">{{ quiz.title }}</div>
<div> {{ _("You have already exceeded the maximum number of attempts allowed for this quiz.") }} </div>
<div> {{ _("Your latest score is {0}.").format(last_attempt_score) }} </div>
</div>
{% else %}
<div id="quiz-title" class="hide" data-max-attempts="{{ quiz.max_attempts }}">{{ quiz.title }}</div>
<div class="common-card-style question-card"> <div class="common-card-style question-card">
<form id="quiz-form"> <div id="start-banner" class="text-center">
<div class="font-weight-bold mb-3" style="font-size: var(--text-lg);"> {{ quiz.title }} </div>
<div class="mb-3">{{ _("There are {0} questions in this quiz.").format(quiz.questions | length) }}{% if quiz.max_attempts %}
{% set suffix = "times" if quiz.max_attempts > 1 else "time" %} {{ _("This quiz can only be taken {0} {1}. If you attempt the quiz and leave the page before submitting, the quiz will be automatically submitted.").format(quiz.max_attempts, suffix) }}{% endif %}
{% if quiz.time %}{{ _("The quiz has a time limit. Each question will be given {0} seconds.").format(quiz.time) }}{% endif %}
</div>
<button class="button is-default m-auto btn-start-quiz"> {{ _("Start") }} </bu>
</div>
<form id="quiz-form" class="hide">
<div class="questions"> <div class="questions">
{% for question in quiz.questions %} {% for question in quiz.questions %}
{% set instruction = _("Choose all answers that apply") if question.multiple else _("Choose 1 answer") %} {% set instruction = _("Choose all answers that apply") if question.multiple else _("Choose 1 answer") %}
<div class="question {% if loop.index == 1 %} active-question {% else %} hide {% endif %}" <div class="question hide"
data-question="{{ question.question }}" data-multi="{{ question.multiple}}" data-qt-index="{{ loop.index }}"> data-question="{{ question.question }}" data-multi="{{ question.multiple }}" data-qt-index="{{ loop.index }}">
<div class="question-header"> <div class="question-header">
<div class="question-number">{{ loop.index }}</div> <div class="question-number">{{ loop.index }}</div>
<div class="question-text"> <div class="question-text">
<div class="course-meta pull-right ml-2"> {{ instruction }} </div> {{ frappe.utils.md_to_html(question.question) }}
{{ frappe.utils.md_to_html(question.question) }} </div>
</div> <div class="course-meta"> {{ instruction }} </div>
</div> </div>
{% set options = [question.option_1, question.option_2, question.option_3, question.option_4] %} {% set options = [question.option_1, question.option_2, question.option_3, question.option_4] %}
@@ -40,15 +57,23 @@
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
<div class="quiz-footer"> <div class="quiz-footer">
<span class="font-weight-bold"> <span class="current-question">1</span> of {{ quiz.questions | length }}</span> <span class="font-weight-bold"> <span class="current-question">1</span> of {{ quiz.questions | length }}</span>
<button class="button pull-right is-default" id="check" disabled>{{ _("Check") }}</button>
<div class="button is-secondary hide" id="next">{{ _("Next Question") }}</div> {% if quiz.time %}
<div class="button is-secondary is-default hide" id="summary">{{ _("Summary") }}</div> <div class="progress timer w-75" data-time="{{ quiz.time }}">
<small id="submission-message" class="font-weight-bold hide"> {{ _("Please join the course to submit the Quiz.") }} </small> <div class="progress-bar" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width:100%">
<div class="button is-secondary hide" id="try-again">{{ _("Try Again") }}</div> </div>
</div>
{% endif %}
<button class="button pull-right is-default" id="check" disabled>{{ _("Check") }}</button>
<div class="button is-secondary hide" id="next">{{ _("Next Question") }}</div>
<div class="button is-secondary is-default hide" id="summary">{{ _("Summary") }}</div>
<small id="submission-message" class="font-weight-bold hide"> {{ _("Please join the course to submit the Quiz.") }} </small>
<div class="button is-secondary hide" id="try-again">{{ _("Try Again") }}</div>
</div> </div>
<h4 class="success-message"></h4>
<h5 class="score text-muted"></h5>
</form> </form>
</div> </div>
{% endif %}

View File

@@ -139,11 +139,6 @@
{% if next_url %} {{ _("Next") }} {% else %} {{ _("Mark as Complete") }} {% endif %} {% if next_url %} {{ _("Next") }} {% else %} {{ _("Mark as Complete") }} {% endif %}
<img class="ml-2" src="/assets/lms/icons/side-arrow-white.svg"> <img class="ml-2" src="/assets/lms/icons/side-arrow-white.svg">
</a> </a>
{% if course.enable_certification %}
<div class="button is-primary {% if membership.progress|int != 100 or next_url %} hide {% endif %}" id="certification">
{{ _("Get Certificate") }}
</div>
{% endif %}
</div> </div>
</div> </div>

View File

@@ -45,6 +45,18 @@ frappe.ready(() => {
clear_work(e); 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 = () => { const save_current_lesson = () => {
@@ -65,19 +77,19 @@ const enable_check = (e) => {
}; };
const mark_active_question = (e = undefined) => { const mark_active_question = (e = undefined) => {
var current_index; $(".timer").addClass("hide");
var next_index = 1; calculate_and_display_time(100);
if (e) { $(".timer").removeClass("hide");
e.preventDefault();
current_index = $(".active-question").attr("data-qt-index"); let current_index = $(".active-question").attr("data-qt-index") || 0;
next_index = parseInt(current_index) + 1; let next_index = parseInt(current_index) + 1;
} $(".question").addClass("hide").removeClass("active-question");
$(".question").addClass("hide").removeClass("active-question"); $(`.question[data-qt-index='${next_index}']`).removeClass("hide").addClass("active-question");
$(`.question[data-qt-index='${next_index}']`).removeClass("hide").addClass("active-question"); $(".current-question").text(`${next_index}`);
$(".current-question").text(`${next_index}`); $("#check").removeClass("hide").attr("disabled", true);
$("#check").removeClass("hide").attr("disabled", true); $("#next").addClass("hide");
$("#next").addClass("hide"); $(".explanation").addClass("hide");
$(".explanation").addClass("hide"); initialize_timer();
}; };
const mark_progress = (e) => { const mark_progress = (e) => {
@@ -85,7 +97,7 @@ const mark_progress = (e) => {
if ($(e.currentTarget).prop("nodeName") != "INPUT") if ($(e.currentTarget).prop("nodeName") != "INPUT")
e.preventDefault(); e.preventDefault();
else else
return return;
const target = $(e.currentTarget).attr("data-progress") ? $(e.currentTarget) : $("input.mark-progress"); const target = $(e.currentTarget).attr("data-progress") ? $(e.currentTarget) : $("input.mark-progress");
const current_status = $(".lesson-progress").hasClass("hide") ? "Incomplete": "Complete"; const current_status = $(".lesson-progress").hasClass("hide") ? "Incomplete": "Complete";
@@ -152,57 +164,56 @@ const move_to_next_lesson = (status, e) => {
} }
}; };
const quiz_summary = (e) => { const quiz_summary = (e=undefined) => {
e.preventDefault(); e && e.preventDefault();
var quiz_name = $("#quiz-title").text(); var quiz_name = $("#quiz-title").text();
var total_questions = $(".question").length; var total_questions = $(".question").length;
frappe.call({
frappe.call({ method: "lms.lms.doctype.lms_quiz.lms_quiz.quiz_summary",
method: "lms.lms.doctype.lms_quiz.lms_quiz.quiz_summary", args: {
args: { "quiz": quiz_name,
"quiz": quiz_name, "results": localStorage.getItem(quiz_name)
"results": localStorage.getItem(quiz_name) },
}, callback: (data) => {
callback: (data) => { var message = data.message == total_questions ? __("Excellent Work 👏") : __("Better luck next time")
var message = data.message == total_questions ? "Excellent Work" : "You were almost there." $(".question").addClass("hide");
$(".question").addClass("hide"); $("#summary").addClass("hide");
$("#summary").addClass("hide"); $("#quiz-form").parent().prepend(
$("#quiz-form").parent().prepend( `<div class="text-center summary"><h2> ${message} </h2>
`<div class="text-center summary"><h2>${message} 👏 </h2> <div class="font-weight-bold">${data.message}/${total_questions}</div></div>`);
<div class="font-weight-bold">${data.message}/${total_questions} correct.</div></div>`); $("#try-again").removeClass("hide");
$("#try-again").removeClass("hide"); }
} })
})
}; };
const try_quiz_again = (e) => { const try_quiz_again = (e) => {
window.location.reload(); window.location.reload();
}; };
const check_answer = (e) => { const check_answer = (e=undefined) => {
e.preventDefault(); 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(); $(".explanation").removeClass("hide");
var total_questions = $(".question").length; $("#check").addClass("hide");
var current_index = $(".active-question").attr("data-qt-index");
$(".explanation").removeClass("hide"); if (current_index == total_questions) {
$("#check").addClass("hide"); if ($(".eligible-for-submission").length) {
$("#summary").removeClass("hide");
if (current_index == total_questions) { }
if ($(".eligible-for-submission").length) { else {
$("#summary").removeClass("hide") $("#submission-message").removeClass("hide");
}
} }
else { else {
$("#submission-message").removeClass("hide"); $("#next").removeClass("hide");
} }
} var [answer, is_correct] = parse_options();
else { add_to_local_storage(quiz_name, current_index, answer, is_correct);
$("#next").removeClass("hide")
}
var [answer, is_correct] = parse_options();
add_to_local_storage(quiz_name, current_index, answer, is_correct)
}; };
const parse_options = () => { 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);
};

View File

@@ -5,18 +5,23 @@
<div class="common-page-style"> <div class="common-page-style">
<div class="container certificate-page"> <div class="container certificate-page">
{% if certificate.member == frappe.session.user %} <!-- {% if certificate.member == frappe.session.user %}
<div class="button is-secondary pull-right mt-4" id="export-as-pdf" data-certificate="{{ certificate.name }}" <div class="button is-secondary pull-right mt-4" id="export-as-pdf" data-certificate="{{ certificate.name }}"
data-certificate-name="{{ member.full_name }} - {{ course.title }}">{{ _("Export") }}</div> data-certificate-name="{{ member.full_name }} - {{ course.title }}">{{ _("Export") }}</div>
{% endif %} {% endif %} -->
<div class="breadcrumb"> <div class="breadcrumb">
<a class="dark-links" href="/courses">{{ _("All Courses") }}</a> <a class="dark-links" href="/courses">{{ _("All Courses") }}</a>
<img class="ml-1 mr-1" src="/assets/lms/icons/chevron-right.svg"> <img class="ml-1 mr-1" src="/assets/lms/icons/chevron-right.svg">
<a class="dark-links" href="/courses/{{ course.name }}">{{ course.title }}</a> <a class="dark-links" href="/courses/{{ course.name }}">{{ course.title }}</a>
</div> </div>
{% if custom_template %}
{{ custom_template }}
{% else %}
{% include "lms/templates/certificate.html" %} {% include "lms/templates/certificate.html" %}
{% endif %}
<script src="/assets/lms/js/html2canvas.js"></script>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -1,16 +1,16 @@
frappe.ready(() => { frappe.ready(() => {
$("#export-as-pdf").click((e) => { $("#export-as-pdf").click((e) => {
export_as_pdf(e); export_as_pdf(e);
}) })
}) });
var export_as_pdf = (e) => { const export_as_pdf = (e) => {
var button = $(e.currentTarget); let button = $(e.currentTarget);
button.text(__("Exporting...")); button.text(__("Exporting..."));
html2canvas(document.querySelector('.common-card-style'), { html2canvas(document.querySelector('.certificate-card'), {
scrollY: -window.scrollY, scrollY: -window.scrollY,
scrollX: 0 scrollX: 0
}).then(function(canvas) { }).then(function(canvas) {

View File

@@ -1,4 +1,6 @@
import frappe import frappe
from frappe.utils.jinja import render_template
from lms.lms.utils import get_instructors
def get_context(context): def get_context(context):
context.no_cache = 1 context.no_cache = 1
@@ -15,16 +17,21 @@ def get_context(context):
if context.certificate.course != course_name: if context.certificate.course != course_name:
redirect_to_course_list() redirect_to_course_list()
context.course = frappe.db.get_value("LMS Course", course_name, context.course = frappe.db.get_value("LMS Course", course_name, ["title", "name", "image"], as_dict=True)
["instructor", "title", "name"], as_dict=True) context.instructors = (", ").join([x.full_name for x in get_instructors(course_name)])
context.instructor = frappe.db.get_value("User", context.course.instructor,
["full_name", "username"], as_dict=True)
context.member = frappe.db.get_value("User", context.certificate.member, context.member = frappe.db.get_value("User", context.certificate.member,
["full_name"], as_dict=True) ["full_name"], as_dict=True)
context.logo = frappe.db.get_single_value("Website Settings", "banner_image") 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(): def redirect_to_course_list():
frappe.local.flags.redirect_location = "/courses" frappe.local.flags.redirect_location = "/courses"