feat: meta tags
This commit is contained in:
@@ -1,181 +0,0 @@
|
||||
{% extends "lms/templates/lms_base.html" %}
|
||||
{% block title %}
|
||||
{{ assignment.title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
<main class="common-page-style">
|
||||
{{ Header() }}
|
||||
<div class="container form-width">
|
||||
{{ SubmissionForm(assignment) }}
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
{% macro Header() %}
|
||||
<header class="sticky mb-5">
|
||||
<div class="container form-width">
|
||||
<div class="edit-header">
|
||||
<div>
|
||||
<div class="vertically-center">
|
||||
<div class="page-title">
|
||||
{{ assignment.title }}
|
||||
</div>
|
||||
{% if assignment.grade_assignment and submission.status %}
|
||||
{% set color = "green" if submission.status == "Pass" else "red" if submission.status == "Fail" else "orange" %}
|
||||
<div class="indicator-pill {{ color }} ml-2">
|
||||
{{ submission.status }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="vertically-center small">
|
||||
<a class="dark-links" href="/batches">
|
||||
{{ _("All Batches") }}
|
||||
</a>
|
||||
<img class="icon icon-sm mr-0" src="/assets/lms/icons/chevron-right.svg">
|
||||
<span class="breadcrumb-destination">{{ _("Assignment Submission") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if not assignment.show_answer or (assignment.show_answer and not submission) %}
|
||||
<div class="align-self-center">
|
||||
<button class="btn btn-primary btn-sm btn-save-assignment"
|
||||
data-assignment="{{ assignment.name }}" data-type="{{ assignment.type }}"
|
||||
{% if submission.name %} data-submission="{{ submission.name }}" {% endif %}>
|
||||
{{ _("Save") }}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro SubmissionForm(assignment) %}
|
||||
<article class="field-parent">
|
||||
{% if assignment.grade_assignment and submission.name %}
|
||||
<div class="alert alert-info">
|
||||
{{ _("You've successfully submitted the assignment. Once the moderator grades your submission, you'll find the details here. Feel free to make edits to your submission if needed.") }}
|
||||
</div>
|
||||
{% if is_moderator %}
|
||||
<div class="field-group">
|
||||
<div class="bold-heading">
|
||||
{{ _("Student Name") }}
|
||||
</div>
|
||||
{{ submission.member_name }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<div class="field-group">
|
||||
<div class="bold-heading">
|
||||
{{ _("Question")}}
|
||||
</div>
|
||||
{{ assignment.question }}
|
||||
</div>
|
||||
|
||||
{% if assignment.type not in ["URL", "Text"] %}
|
||||
<div class="field-group">
|
||||
<div class="bold-heading">
|
||||
{{ _("Submit")}}
|
||||
</div>
|
||||
<div class="field-description">
|
||||
{{ _("Upload assignment as {0}").format(assignment.type) }}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="btn btn-default btn-sm btn-upload mt-2 {% if submission.assignment_attachment %} hide {% endif %}" data-type="{{ assignment.type }}">
|
||||
{{ _("Browse").format(assignment.type) }}
|
||||
</div>
|
||||
<div class="field-input flex justify-between align-center overflow-auto
|
||||
{% if not submission.assignment_attachment %} hide {% endif %}" id="assignment-preview">
|
||||
<a class="clickable" {% if submission.assignment_attachment %} href="{{ submission.assignment_attachment }}" {% endif %}>
|
||||
{% if submission.assignment_attachment %} {{ submission.assignment_attachment }} {% endif %}
|
||||
</a>
|
||||
</div>
|
||||
<span class="btn btn-default btn-sm btn-close {% if not submission %} hide {% endif %} mt-2">
|
||||
{{ _("Clear") }}
|
||||
</span>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
<div class="field-group">
|
||||
<div class="bold-heading">
|
||||
{{ _("Submission")}}
|
||||
</div>
|
||||
<div class="field-description">
|
||||
{% if assignment.type == "URL" %}
|
||||
{{ _("Enter a {0}").format(assignment.type) }}
|
||||
{% else %}
|
||||
{{ _("Enter your response") }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if assignment.type == "URL" %}
|
||||
<input type="text" class="field-input assignment-answer" placeholder="https://"
|
||||
{% if submission.answer %} value="{{ submission.answer }}" {% endif %}>
|
||||
{% else %}
|
||||
<div class="assignment-text"></div>
|
||||
{% if submission.answer %}
|
||||
<div class="assignment-text-data hide">
|
||||
{{ submission.answer }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if assignment.show_answer and submission %}
|
||||
<div class="field-group">
|
||||
<div class="bold-heading">
|
||||
{{ _("Response by Instructor:") }}
|
||||
</div>
|
||||
<div>
|
||||
{{ assignment.answer }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if assignment.grade_assignment and is_moderator %}
|
||||
<div class="field-group">
|
||||
<div class="field-label">
|
||||
{{ _("Status") }}
|
||||
</div>
|
||||
<div class="field-input flex align-center">
|
||||
<select class="form-control" id="status">
|
||||
{% set statuses = ["Not Graded", "Pass", "Fail"] %}
|
||||
{% for status in statuses %}
|
||||
<option value="{{ status }}" {% if submission.status == status %} selected {% endif %}>
|
||||
{{ status }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="select-icon">
|
||||
<svg class="icon icon-sm">
|
||||
<use class="" href="#icon-select"></use>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<div class="field-label">
|
||||
{{ _("Comments by Mentor") }}
|
||||
</div>
|
||||
<textarea id="comments" type="text" class="field-input" height="300px"
|
||||
>{% if submission.comments %}{{ submission.comments }}{% endif %}</textarea>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if submission and submission.member == frappe.session.user and submission.comments %}
|
||||
<div class="field-group">
|
||||
<div class="field-label">
|
||||
{{ _("Comments by Mentor") }}
|
||||
</div>
|
||||
<div>
|
||||
{{ submission.comments }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</article>
|
||||
{% endmacro %}
|
||||
@@ -1,132 +0,0 @@
|
||||
frappe.ready(() => {
|
||||
if ($(".assignment-text").length) {
|
||||
frappe.require("controls.bundle.js", () => {
|
||||
make_text_editor();
|
||||
});
|
||||
}
|
||||
|
||||
$(".btn-upload").click((e) => {
|
||||
upload_file(e);
|
||||
});
|
||||
|
||||
$(".btn-save-assignment").click((e) => {
|
||||
save_assignment(e);
|
||||
});
|
||||
|
||||
$(".btn-close").click((e) => {
|
||||
clear_preview(e);
|
||||
});
|
||||
});
|
||||
|
||||
const upload_file = (e) => {
|
||||
let type = $(e.currentTarget).data("type");
|
||||
let mapper = {
|
||||
Image: ["image/*"],
|
||||
Document: [
|
||||
".doc",
|
||||
".docx",
|
||||
".xml",
|
||||
"application/msword",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
],
|
||||
PDF: [".pdf"],
|
||||
};
|
||||
|
||||
new frappe.ui.FileUploader({
|
||||
disable_file_browser: true,
|
||||
folder: "Home/Attachments",
|
||||
make_attachments_public: true,
|
||||
restrictions: {
|
||||
allowed_file_types: mapper[type],
|
||||
},
|
||||
on_success: (file_doc) => {
|
||||
$(e.currentTarget).addClass("hide");
|
||||
$("#assignment-preview").removeClass("hide");
|
||||
$("#assignment-preview .btn-close").removeClass("hide");
|
||||
$("#assignment-preview a").attr(
|
||||
"href",
|
||||
encodeURI(file_doc.file_url)
|
||||
);
|
||||
$("#assignment-preview a").text(file_doc.file_url);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const save_assignment = (e) => {
|
||||
let data = $(e.currentTarget).data("type");
|
||||
let answer,
|
||||
file = "";
|
||||
|
||||
if (data == "URL") {
|
||||
answer = $(".assignment-answer").val();
|
||||
if (!answer) {
|
||||
frappe.throw({
|
||||
title: __("No Submission"),
|
||||
message: __("Please enter a response."),
|
||||
});
|
||||
}
|
||||
} else if (data == "Text") {
|
||||
answer = this.text_editor.get_value("assignment_text");
|
||||
if (!answer) {
|
||||
frappe.throw({
|
||||
title: __("No Submission"),
|
||||
message: __("Please enter a response."),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
file = $("#assignment-preview a").attr("href");
|
||||
if (!file) {
|
||||
frappe.throw({
|
||||
title: __("No File"),
|
||||
message: __("Please upload a file."),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_assignment_submission.lms_assignment_submission.upload_assignment",
|
||||
args: {
|
||||
assignment: $(e.currentTarget).data("assignment"),
|
||||
submission: $(e.currentTarget).data("submission") || "",
|
||||
assignment_attachment: file,
|
||||
answer: answer,
|
||||
status: $("#status").val(),
|
||||
comments: $("#comments").val(),
|
||||
},
|
||||
callback: (data) => {
|
||||
frappe.show_alert({
|
||||
message: __("Saved"),
|
||||
indicator: "green",
|
||||
});
|
||||
setTimeout(() => {
|
||||
window.location.href = `/assignment-submission/${$(
|
||||
e.currentTarget
|
||||
).data("assignment")}/${data.message}`;
|
||||
}, 2000);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const clear_preview = (e) => {
|
||||
$(".btn-upload").removeClass("hide");
|
||||
$("#assignment-preview").addClass("hide");
|
||||
$("#assignment-preview a").attr("href", "");
|
||||
$("#assignment-preview .btn-close").addClass("hide");
|
||||
};
|
||||
|
||||
const make_text_editor = () => {
|
||||
this.text_editor = new frappe.ui.FieldGroup({
|
||||
fields: [
|
||||
{
|
||||
fieldname: "assignment_text",
|
||||
fieldtype: "Text Editor",
|
||||
default: $(".assignment-text-data").html(),
|
||||
},
|
||||
],
|
||||
body: $(".assignment-text").get(0),
|
||||
});
|
||||
this.text_editor.make();
|
||||
$(".assignment-text .form-section:last").removeClass("empty-section");
|
||||
$(".assignment-text .frappe-control").removeClass("hide-control");
|
||||
$(".assignment-text .form-column").addClass("p-0");
|
||||
};
|
||||
@@ -1,48 +0,0 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from lms.lms.utils import has_course_moderator_role
|
||||
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
|
||||
if frappe.session.user == "Guest":
|
||||
raise frappe.PermissionError(_("Please login to submit the assignment."))
|
||||
|
||||
context.is_moderator = has_course_moderator_role()
|
||||
submission = frappe.form_dict["submission"]
|
||||
assignment = frappe.form_dict["assignment"]
|
||||
|
||||
context.assignment = frappe.db.get_value(
|
||||
"LMS Assignment",
|
||||
assignment,
|
||||
["title", "name", "type", "question", "show_answer", "answer", "grade_assignment"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if submission == "new-submission":
|
||||
context.submission = frappe._dict()
|
||||
else:
|
||||
context.submission = frappe.db.get_value(
|
||||
"LMS Assignment Submission",
|
||||
submission,
|
||||
[
|
||||
"name",
|
||||
"assignment_attachment",
|
||||
"answer",
|
||||
"comments",
|
||||
"status",
|
||||
"member",
|
||||
"member_name",
|
||||
],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
if not context.submission:
|
||||
raise frappe.PermissionError(_("Invalid Submission URL"))
|
||||
|
||||
if not context.is_moderator and frappe.session.user != context.submission.member:
|
||||
raise frappe.PermissionError(_("You don't have permission to access this page."))
|
||||
|
||||
if not context.assignment or not context.submission:
|
||||
raise frappe.PermissionError(_("Invalid Submission URL"))
|
||||
@@ -1,99 +0,0 @@
|
||||
{% extends "lms/templates/lms_base.html" %}
|
||||
{% block title %}
|
||||
{{ assignment.title if assignment.name else _("Assignment Details") }}
|
||||
{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
<div class="common-page-style">
|
||||
{{ Header() }}
|
||||
<div class="container form-width">
|
||||
{{ AssignmentForm(assignment) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% macro Header() %}
|
||||
<header class="sticky">
|
||||
<div class="container form-width">
|
||||
<div class="edit-header">
|
||||
<div>
|
||||
<div class="page-title">
|
||||
{{ _("Assignment Details") }}
|
||||
</div>
|
||||
<div class="vertically-center small">
|
||||
<a class="dark-links" href="/assignments">
|
||||
{{ _("Assignment List") }}
|
||||
</a>
|
||||
<img class="icon icon-sm mr-0" src="/assets/lms/icons/chevron-right.svg">
|
||||
<span class="breadcrumb-destination">{{ assignment.title if assignment.title else _("New Assignment") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="align-self-center">
|
||||
<button class="btn btn-primary btn-sm btn-save-assignment" {% if assignment.name %} data-assignment="{{ assignment.name }}" {% endif %}>
|
||||
{{ _("Save") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro AssignmentForm(assignment) %}
|
||||
<article>
|
||||
<div class="field-parent">
|
||||
<div class="field-group">
|
||||
<div class="field-label reqd"> {{ _("Title") }} </div>
|
||||
<div class="field-description">
|
||||
{{ _("Give the assignment a title.") }}
|
||||
</div>
|
||||
<input type="text" id="title" class="field-input" {% if assignment.name %} value="{{ assignment.title }}" data-name="{{ assignment.name }}" {% endif %}>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<div class="field-label reqd"> {{ "Type" }} </div>
|
||||
<div class="field-description">
|
||||
{{ _("Select the format in which students will have to submit the assignment.") }}
|
||||
</div>
|
||||
<div class="field-input flex align-center">
|
||||
<select class="form-control" id="type">
|
||||
{% set types = ["Document", "PDF", "Image"] %}
|
||||
{% for type in types %}
|
||||
<option value="{{ type }}" {% if assignment.type == type %} selected {% endif %}>
|
||||
{{ type }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="select-icon">
|
||||
<svg class="icon icon-sm" style="">
|
||||
<use class="" href="#icon-select"></use>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<div class="field-label reqd">
|
||||
{{ _("Question") }}
|
||||
</div>
|
||||
<div class="field-description">
|
||||
{{ _("Enter an assignment question.") }}
|
||||
</div>
|
||||
<div id="question" class=""></div>
|
||||
{% if assignment.question %}
|
||||
<div id="question-data" class="hide">
|
||||
{{ assignment.question }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{%- block script %}
|
||||
{{ super() }}
|
||||
{{ include_script('controls.bundle.js') }}
|
||||
{% endblock %}
|
||||
@@ -1,47 +0,0 @@
|
||||
frappe.ready(() => {
|
||||
if ($("#question").length) {
|
||||
make_editor();
|
||||
}
|
||||
|
||||
$(".btn-save-assignment").click((e) => {
|
||||
save_assignment(e);
|
||||
});
|
||||
});
|
||||
|
||||
const make_editor = () => {
|
||||
this.question = new frappe.ui.FieldGroup({
|
||||
fields: [
|
||||
{
|
||||
fieldname: "question",
|
||||
fieldtype: "Text Editor",
|
||||
default: $("#question-data").html(),
|
||||
},
|
||||
],
|
||||
body: $("#question").get(0),
|
||||
});
|
||||
this.question.make();
|
||||
$("#question .form-section:last").removeClass("empty-section");
|
||||
$("#question .frappe-control").removeClass("hide-control");
|
||||
$("#question .form-column").addClass("p-0");
|
||||
};
|
||||
|
||||
const save_assignment = (e) => {
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_assignment.lms_assignment.save_assignment",
|
||||
args: {
|
||||
assignment: $(e.currentTarget).data("assignment") || "",
|
||||
title: $("#title").val(),
|
||||
question: this.question.fields_dict["question"].value,
|
||||
type: $("#type").val(),
|
||||
},
|
||||
callback: (data) => {
|
||||
frappe.show_alert({
|
||||
message: __("Saved"),
|
||||
indicator: "green",
|
||||
});
|
||||
setTimeout(() => {
|
||||
window.location.href = `/assignments/${data.message}`;
|
||||
}, 2000);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from lms.lms.utils import has_course_moderator_role, has_course_instructor_role
|
||||
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
|
||||
if not has_course_moderator_role() or not has_course_instructor_role():
|
||||
message = "You do not have permission to access this page."
|
||||
if frappe.session.user == "Guest":
|
||||
message = "Please login to access this page."
|
||||
|
||||
raise frappe.PermissionError(_(message))
|
||||
|
||||
assignment = frappe.form_dict["assignment"]
|
||||
|
||||
if assignment == "new-assignment":
|
||||
context.assignment = frappe._dict()
|
||||
else:
|
||||
context.assignment = frappe.db.get_value(
|
||||
"LMS Assignment", assignment, ["title", "name", "type", "question"], as_dict=1
|
||||
)
|
||||
@@ -1,90 +0,0 @@
|
||||
{% extends "templates/base.html" %}
|
||||
{% block title %}
|
||||
{{ _("Assignment List") }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="common-page-style">
|
||||
<div class="container">
|
||||
{{ Header() }}
|
||||
{% if assignments | length %}
|
||||
{{ AssignmentList(assignments) }}
|
||||
{% else %}
|
||||
{{ EmptyState() }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
{% macro Header() %}
|
||||
<header class="sticky">
|
||||
<div class="edit-header">
|
||||
<div class="page-title">
|
||||
{{ _("Assignment List") }}
|
||||
</div>
|
||||
|
||||
<a class="btn btn-primary btn-sm align-self-center" href="/assignments/new-assignments">
|
||||
{{ _("Add Assignment") }}
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro AssignmentList(assignments) %}
|
||||
<div class="mt-5">
|
||||
<div class="form-grid">
|
||||
<div class="grid-heading-row">
|
||||
<div class="grid-row">
|
||||
<div class="data-row row">
|
||||
<div class="col grid-static-col">
|
||||
{{ _("Title") }}
|
||||
</div>
|
||||
<div class="col grid-static-col col-xs-3">
|
||||
{{ _("Type") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% for assignment in assignments %}
|
||||
<div class="grid-row">
|
||||
<div class="data-row row">
|
||||
<a class="col grid-static-col button-links clickable" href="/assignments/{{ assignment.name }}">
|
||||
{{ assignment.title }}
|
||||
</a>
|
||||
<div class="col grid-static-col col-xs-3">
|
||||
{{ assignment.type }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<!-- <ul class="list-unstyled">
|
||||
{% for assignment in assignments %}
|
||||
<li class="list-row">
|
||||
<a class="clickable" href="/assignments/{{ assignment.name }}">
|
||||
<span>
|
||||
{{ loop.index }}.
|
||||
</span>
|
||||
<span>
|
||||
{{ assignment.title }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul> -->
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro EmptyState() %}
|
||||
<div class="empty-state mt-5">
|
||||
<img class="icon icon-xl" src="/assets/lms/icons/comment.svg">
|
||||
<div class="empty-state-text">
|
||||
<div class="empty-state-heading">
|
||||
{{ _("You have not created any assignment yet.") }}
|
||||
</div>
|
||||
<div class="course-meta ">
|
||||
{{ _("Create an assignment and evaluate your students.") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
@@ -1,17 +0,0 @@
|
||||
import frappe
|
||||
from lms.lms.utils import has_course_moderator_role
|
||||
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
|
||||
filters = {"owner": frappe.session.user}
|
||||
|
||||
if has_course_moderator_role():
|
||||
filters = {}
|
||||
|
||||
context.assignments = frappe.get_all(
|
||||
"LMS Assignment",
|
||||
filters,
|
||||
["title", "name", "type", "question"],
|
||||
)
|
||||
@@ -1,134 +0,0 @@
|
||||
{% extends "lms/templates/lms_base.html" %}
|
||||
|
||||
{% block title %}
|
||||
{% if lesson.title %}
|
||||
{{ lesson.title }} - {{ course.title }}
|
||||
{% else %}
|
||||
{{ _("New Lesson") }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
<main class="common-page-style">
|
||||
{{ Header() }}
|
||||
<div class="container form-width" id="course-outline" {% if course.name %} data-course="{{ course.name }}" {% endif %}>
|
||||
{{ CreateLesson() }}
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% macro Header() %}
|
||||
<header class="sticky">
|
||||
<div class="container form-width">
|
||||
|
||||
<div class="edit-header">
|
||||
|
||||
<div>
|
||||
<div class="page-title">
|
||||
{{ course.title if course.name else _("Course Outline") }}
|
||||
</div>
|
||||
<div class="vertically-center small">
|
||||
<a class="dark-links" href="/courses/{{ course.name }}/edit">
|
||||
{{ _("Course Details") }}
|
||||
</a>
|
||||
<img class="icon icon-sm mr-0" src="/assets/lms/icons/chevron-right.svg">
|
||||
<a class="dark-links" href="/courses/{{ course.name }}/outline">
|
||||
{{ _("Course Outline") }}
|
||||
</a>
|
||||
<img class="icon icon-sm mr-0" src="/assets/lms/icons/chevron-right.svg">
|
||||
<span class="breadcrumb-destination">
|
||||
{{ _("New Lesson") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="align-self-center">
|
||||
{% if lesson.name %}
|
||||
<a class="btn btn-default btn-sm mr-2" href="{{ get_lesson_url(course.name, lesson_number) }}">
|
||||
<span>
|
||||
{{ _("Back to Lesson") }}
|
||||
</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<button class="btn btn-primary btn-sm" id="save-lesson">
|
||||
<span>
|
||||
{{ _("Save") }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro CreateLesson() %}
|
||||
<article class="field-parent">
|
||||
<div class="field-group">
|
||||
<div class="field-label">
|
||||
{{ _("Title") }}
|
||||
</div>
|
||||
<div class="">
|
||||
<input id="lesson-title" type="text" class="field-input" data-index="{{ lesson_index }}" data-chapter="{{ chapter | urlencode }}" data-course="{{ course.name }}" {% if lesson.name %} data-lesson="{{ lesson.name }}" value="{{ lesson.title }}" {% endif %}>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<label for="preview" class="vertically-center">
|
||||
<input type="checkbox" id="preview" {% if lesson.include_in_preview %} checked {% endif %}>
|
||||
<span>{{ _("Show preview of this lesson to Guest users.") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<div class="collapse-section collapsed" data-toggle="collapse" data-target="#instructor-notes-section">
|
||||
<svg class="icon icon-sm pull-right">
|
||||
<use href="#icon-up-line"></use>
|
||||
</svg>
|
||||
<div class="field-label">
|
||||
{{ _("Instructor Notes") }}
|
||||
</div>
|
||||
<div class="field-description mb-2">
|
||||
{{ _("These notes will only be visible to the Course Creator, Course Evaluaor and Moderator.") }}
|
||||
</div>
|
||||
</div>
|
||||
<div id="instructor-notes-section" class="collapse">
|
||||
<div id="instructor-notes" class="lesson-editor"></div>
|
||||
</div>
|
||||
{% if lesson.instructor_notes %}
|
||||
<div id="current-instructor-notes" class="hide">{{ lesson.instructor_notes }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<div>
|
||||
<div class="field-label">
|
||||
{{ _("Content") }}
|
||||
</div>
|
||||
<div class="field-description mb-2">
|
||||
{{ _("Add your lesson content here") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="lesson-content-section">
|
||||
<div id="lesson-content" class="lesson-editor"></div>
|
||||
</div>
|
||||
|
||||
{% if lesson.body %}
|
||||
<div id="current-lesson-content" class="hide">{{ lesson.body }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</article>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{%- block script %}
|
||||
{{ super() }}
|
||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@latest"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/paragraph@2.10.0"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/header@latest"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/embed@latest"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,532 +0,0 @@
|
||||
frappe.ready(() => {
|
||||
let self = this;
|
||||
frappe.require("controls.bundle.js");
|
||||
frappe.telemetry.capture("on_lesson_creation_page", "lms");
|
||||
|
||||
if ($("#current-lesson-content").length) {
|
||||
parse_string_to_lesson("lesson");
|
||||
}
|
||||
|
||||
if ($("#current-instructor-notes").length) {
|
||||
parse_string_to_lesson("notes");
|
||||
}
|
||||
|
||||
setup_editor_for_lesson_content();
|
||||
setup_editor_for_instructor_notes();
|
||||
|
||||
$("#save-lesson").click((e) => {
|
||||
save_lesson(e);
|
||||
});
|
||||
});
|
||||
|
||||
const setup_editor_for_lesson_content = () => {
|
||||
self.editor = new EditorJS({
|
||||
holder: "lesson-content",
|
||||
tools: get_tools(),
|
||||
data: {
|
||||
blocks: self.lesson_blocks || [],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const setup_editor_for_instructor_notes = () => {
|
||||
self.instructor_notes_editor = new EditorJS({
|
||||
holder: "instructor-notes",
|
||||
tools: get_tools(),
|
||||
data: {
|
||||
blocks: self.notes_blocks || [],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const get_tools = () => {
|
||||
return {
|
||||
embed: {
|
||||
class: Embed,
|
||||
config: {
|
||||
services: {
|
||||
youtube: true,
|
||||
vimeo: true,
|
||||
codepen: true,
|
||||
slides: {
|
||||
regex: /https:\/\/docs\.google\.com\/presentation\/d\/e\/([A-Za-z0-9_-]+)\/pub/,
|
||||
embedUrl:
|
||||
"https://docs.google.com/presentation/d/e/<%= remote_id %>/embed",
|
||||
html: "<iframe width='100%' height='300' frameborder='0' allowfullscreen='true'></iframe>",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
header: {
|
||||
class: Header,
|
||||
inlineToolbar: ["bold", "italic", "link"],
|
||||
config: {
|
||||
levels: [4, 5, 6],
|
||||
defaultLevel: 5,
|
||||
},
|
||||
icon: `<svg class="icon icon-sm" style="">
|
||||
<use class="" href="#icon-header"></use>
|
||||
</svg>`,
|
||||
},
|
||||
paragraph: {
|
||||
class: Paragraph,
|
||||
inlineToolbar: true,
|
||||
config: {
|
||||
preserveBlank: true,
|
||||
},
|
||||
},
|
||||
youtube: YouTubeVideo,
|
||||
quiz: Quiz,
|
||||
upload: Upload,
|
||||
};
|
||||
};
|
||||
|
||||
const parse_string_to_lesson = (type) => {
|
||||
let content;
|
||||
let blocks = [];
|
||||
|
||||
if (type == "lesson") {
|
||||
content = $("#current-lesson-content").html();
|
||||
} else if (type == "notes") {
|
||||
content = $("#current-instructor-notes").html();
|
||||
}
|
||||
|
||||
content.split("\n").forEach((block) => {
|
||||
if (block.includes("{{ YouTubeVideo")) {
|
||||
let youtube_id = block.match(/\(["']([^"']+?)["']\)/)[1];
|
||||
blocks.push({
|
||||
type: "youtube",
|
||||
data: {
|
||||
youtube: youtube_id,
|
||||
},
|
||||
});
|
||||
} else if (block.includes("{{ Quiz")) {
|
||||
let quiz = block.match(/\(["']([^"']+?)["']\)/)[1];
|
||||
blocks.push({
|
||||
type: "quiz",
|
||||
data: {
|
||||
quiz: quiz,
|
||||
},
|
||||
});
|
||||
} else if (block.includes("{{ Video")) {
|
||||
let video = block.match(/\(["']([^"']+?)["']\)/)[1];
|
||||
blocks.push({
|
||||
type: "upload",
|
||||
data: {
|
||||
file_url: video,
|
||||
file_type: "video",
|
||||
},
|
||||
});
|
||||
} else if (block.includes("{{ Audio")) {
|
||||
let audio = block.match(/\(["']([^"']+?)["']\)/)[1];
|
||||
blocks.push({
|
||||
type: "upload",
|
||||
data: {
|
||||
file_url: audio,
|
||||
file_type: "audio",
|
||||
},
|
||||
});
|
||||
} else if (block.includes("{{ PDF")) {
|
||||
let pdf = block.match(/\(["']([^"']+?)["']\)/)[1];
|
||||
blocks.push({
|
||||
type: "upload",
|
||||
data: {
|
||||
file_url: pdf,
|
||||
file_type: "pdf",
|
||||
},
|
||||
});
|
||||
} else if (block.includes("{{ Embed")) {
|
||||
let embed = block.match(/\(["']([^"']+?)["']\)/)[1];
|
||||
blocks.push({
|
||||
type: "embed",
|
||||
data: {
|
||||
service: embed.split("|||")[0],
|
||||
embed: embed.split("|||")[1],
|
||||
},
|
||||
});
|
||||
} else if (block.includes("![]")) {
|
||||
let image = block.match(/\((.*?)\)/)[1];
|
||||
blocks.push({
|
||||
type: "upload",
|
||||
data: {
|
||||
file_url: image,
|
||||
file_type: "image",
|
||||
},
|
||||
});
|
||||
} else if (block.includes("#")) {
|
||||
let level = (block.match(/#/g) || []).length;
|
||||
blocks.push({
|
||||
type: "header",
|
||||
data: {
|
||||
text: block.replace(/#/g, "").trim(),
|
||||
level: level,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
blocks.push({
|
||||
type: "paragraph",
|
||||
data: {
|
||||
text: block,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (type == "lesson") {
|
||||
this.lesson_blocks = blocks;
|
||||
} else if (type == "notes") {
|
||||
this.notes_blocks = blocks;
|
||||
}
|
||||
};
|
||||
|
||||
const save_lesson = (e) => {
|
||||
self.editor.save().then((outputData) => {
|
||||
parse_content_to_string(outputData, "lesson");
|
||||
|
||||
self.instructor_notes_editor.save().then((outputData) => {
|
||||
parse_content_to_string(outputData, "notes");
|
||||
save();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const parse_content_to_string = (data, type) => {
|
||||
let lesson_content = "";
|
||||
data.blocks.forEach((block) => {
|
||||
if (block.type == "youtube") {
|
||||
lesson_content += `{{ YouTubeVideo("${block.data.youtube}") }}\n`;
|
||||
} else if (block.type == "quiz") {
|
||||
lesson_content += `{{ Quiz("${block.data.quiz}") }}\n`;
|
||||
} else if (block.type == "upload") {
|
||||
let url = block.data.file_url;
|
||||
if (block.data.file_type == "video") {
|
||||
lesson_content += `{{ Video("${url}") }}\n`;
|
||||
} else if (block.data.file_type == "audio") {
|
||||
lesson_content += `{{ Audio("${url}") }}\n`;
|
||||
} else if (block.data.file_type == "pdf") {
|
||||
lesson_content += `{{ PDF("${url}") }}\n`;
|
||||
} else {
|
||||
lesson_content += ``;
|
||||
}
|
||||
} else if (block.type == "header") {
|
||||
lesson_content +=
|
||||
"#".repeat(block.data.level) + ` ${block.data.text}\n`;
|
||||
} else if (block.type == "paragraph") {
|
||||
lesson_content += `${block.data.text}\n`;
|
||||
} else if (block.type == "embed") {
|
||||
lesson_content += `{{ Embed("${
|
||||
block.data.service
|
||||
}|||${block.data.embed.replace(/&/g, "&")}") }}\n`;
|
||||
}
|
||||
});
|
||||
if (type == "lesson") {
|
||||
this.lesson_content_data = lesson_content;
|
||||
} else if (type == "notes") {
|
||||
this.instructor_notes_data = lesson_content;
|
||||
}
|
||||
};
|
||||
|
||||
const save = () => {
|
||||
validate_mandatory(this.lesson_content_data);
|
||||
let lesson = $("#lesson-title").data("lesson");
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_course.lms_course.save_lesson",
|
||||
args: {
|
||||
title: $("#lesson-title").val(),
|
||||
body: this.lesson_content_data,
|
||||
chapter: decodeURIComponent($("#lesson-title").data("chapter")),
|
||||
preview: $("#preview").prop("checked") ? 1 : 0,
|
||||
idx: $("#lesson-title").data("index"),
|
||||
lesson: lesson ? lesson : "",
|
||||
instructor_notes: this.instructor_notes_data,
|
||||
},
|
||||
callback: (data) => {
|
||||
frappe.show_alert({
|
||||
message: __("Saved"),
|
||||
indicator: "green",
|
||||
});
|
||||
setTimeout(() => {
|
||||
window.location.href = window.location.href.split("?")[0];
|
||||
}, 1000);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const validate_mandatory = (lesson_content) => {
|
||||
if (!$("#lesson-title").val()) {
|
||||
let error = $("p")
|
||||
.addClass("error-message")
|
||||
.text(__("Please enter a Lesson Title"));
|
||||
$(error).insertAfter("#lesson-title");
|
||||
$("#lesson-title").focus();
|
||||
throw "Title is mandatory";
|
||||
}
|
||||
|
||||
if (!lesson_content.trim()) {
|
||||
let error = $("p")
|
||||
.addClass("error-message")
|
||||
.text(__("Please enter some content for the lesson"));
|
||||
$(error).insertAfter("#lesson-content");
|
||||
document
|
||||
.getElementById("lesson-content")
|
||||
.scrollIntoView({ block: "start" });
|
||||
throw "Lesson Content is mandatory";
|
||||
}
|
||||
};
|
||||
|
||||
const get_file_type = (url) => {
|
||||
let video_types = ["mov", "mp4", "mkv"];
|
||||
let video_extension = url.split(".").pop();
|
||||
|
||||
if (video_types.indexOf(video_extension) >= 0) {
|
||||
return "video";
|
||||
}
|
||||
|
||||
let audio_types = ["mp3", "wav", "ogg"];
|
||||
let audio_extension = url.split(".").pop();
|
||||
|
||||
if (audio_types.indexOf(audio_extension) >= 0) {
|
||||
return "audio";
|
||||
}
|
||||
|
||||
if (url.split(".").pop() == "pdf") {
|
||||
return "pdf";
|
||||
}
|
||||
|
||||
return "image";
|
||||
};
|
||||
|
||||
class YouTubeVideo {
|
||||
constructor({ data }) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
static get toolbox() {
|
||||
return {
|
||||
title: "YouTube Video",
|
||||
icon: `<img src="/assets/lms/icons/video.svg" width="15" height="15">`,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
this.wrapper = document.createElement("div");
|
||||
if (this.data && this.data.youtube) {
|
||||
$(this.wrapper).html(this.render_youtube(this.data.youtube));
|
||||
} else {
|
||||
this.render_youtube_dialog();
|
||||
}
|
||||
return this.wrapper;
|
||||
}
|
||||
|
||||
render_youtube_dialog() {
|
||||
let me = this;
|
||||
let youtubedialog = new frappe.ui.Dialog({
|
||||
title: __("YouTube Video"),
|
||||
fields: [
|
||||
{
|
||||
fieldname: "youtube",
|
||||
fieldtype: "Data",
|
||||
label: __("YouTube Video ID"),
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "instructions_section_break",
|
||||
fieldtype: "Section Break",
|
||||
label: __("Instructions:"),
|
||||
},
|
||||
{
|
||||
fieldname: "instructions",
|
||||
fieldtype: "HTML",
|
||||
label: __("Instructions"),
|
||||
options: __(
|
||||
"Enter the YouTube Video ID. The ID is the part of the URL after <code>watch?v=</code>. For example, if the URL is <code>https://www.youtube.com/watch?v=QH2-TGUlwu4</code>, the ID is <code>QH2-TGUlwu4</code>"
|
||||
),
|
||||
},
|
||||
],
|
||||
primary_action_label: __("Insert"),
|
||||
primary_action(values) {
|
||||
youtubedialog.hide();
|
||||
me.youtube = values.youtube;
|
||||
$(me.wrapper).html(me.render_youtube(values.youtube));
|
||||
},
|
||||
});
|
||||
youtubedialog.show();
|
||||
}
|
||||
|
||||
render_youtube(youtube) {
|
||||
return `<iframe width="100%" height="400"
|
||||
src="https://www.youtube.com/embed/${youtube}"
|
||||
title="YouTube video player"
|
||||
frameborder="0"
|
||||
style="border-radius: var(--border-radius-lg); margin: 1rem 0;"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen>
|
||||
</iframe>`;
|
||||
}
|
||||
|
||||
validate(savedData) {
|
||||
return !savedData.youtube || !savedData.youtube.trim() ? false : true;
|
||||
}
|
||||
|
||||
save(block_content) {
|
||||
return {
|
||||
youtube: this.data.youtube || this.youtube,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class Quiz {
|
||||
static get toolbox() {
|
||||
return {
|
||||
title: "Quiz",
|
||||
icon: `<img src="/assets/lms/icons/quiz.svg" width="15" height="15">`,
|
||||
};
|
||||
}
|
||||
|
||||
constructor({ data }) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
render() {
|
||||
this.wrapper = document.createElement("div");
|
||||
if (this.data && this.data.quiz) {
|
||||
$(this.wrapper).html(this.render_quiz(this.data.quiz));
|
||||
} else {
|
||||
this.render_quiz_dialog();
|
||||
}
|
||||
return this.wrapper;
|
||||
}
|
||||
|
||||
render_quiz_dialog() {
|
||||
let me = this;
|
||||
let quizdialog = new frappe.ui.Dialog({
|
||||
title: __("Manage Quiz"),
|
||||
fields: [
|
||||
{
|
||||
fieldname: "quiz",
|
||||
fieldtype: "Link",
|
||||
label: __("Quiz"),
|
||||
options: "LMS Quiz",
|
||||
only_select: 1,
|
||||
},
|
||||
],
|
||||
primary_action_label: __("Insert"),
|
||||
primary_action(values) {
|
||||
me.quiz = values.quiz;
|
||||
quizdialog.hide();
|
||||
$(me.wrapper).html(me.render_quiz(me.quiz));
|
||||
},
|
||||
secondary_action_label: __("Create New"),
|
||||
secondary_action: () => {
|
||||
window.location.href = `/quizzes`;
|
||||
},
|
||||
});
|
||||
quizdialog.show();
|
||||
setTimeout(() => {
|
||||
$(".modal-body").css("min-height", "200px");
|
||||
$(".modal-body input").focus();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
render_quiz(quiz) {
|
||||
return `<a class="common-card-style p-20 my-2 justify-center bold-heading" target="_blank" href=/quizzes/${quiz}>
|
||||
Quiz: ${quiz}
|
||||
</a>`;
|
||||
}
|
||||
|
||||
validate(savedData) {
|
||||
return !savedData.quiz || !savedData.quiz.trim() ? false : true;
|
||||
}
|
||||
|
||||
save(block_content) {
|
||||
return {
|
||||
quiz: this.data.quiz || this.quiz,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class Upload {
|
||||
static get toolbox() {
|
||||
return {
|
||||
title: "Upload",
|
||||
icon: `<img src="/assets/lms/icons/upload.svg" width="15" height="15">`,
|
||||
};
|
||||
}
|
||||
|
||||
constructor({ data }) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
render() {
|
||||
this.wrapper = document.createElement("div");
|
||||
if (this.data && this.data.file_url) {
|
||||
$(this.wrapper).html(this.render_upload(this.data.file_url));
|
||||
} else {
|
||||
this.render_upload_dialog();
|
||||
}
|
||||
return this.wrapper;
|
||||
}
|
||||
|
||||
render_upload_dialog() {
|
||||
let self = this;
|
||||
new frappe.ui.FileUploader({
|
||||
disable_file_browser: true,
|
||||
folder: "Home/Attachments",
|
||||
make_attachments_public: true,
|
||||
restrictions: {
|
||||
allowed_file_types: ["image/*", "video/*", "audio/*", ".pdf"],
|
||||
},
|
||||
on_success: (file_doc) => {
|
||||
self.file_url = file_doc.file_url;
|
||||
$(self.wrapper).html(self.render_upload(self.file_url));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
render_upload(url) {
|
||||
this.file_type = get_file_type(url);
|
||||
if (this.file_type == "video") {
|
||||
return `<video controls width='100%' controls controlsList='nodownload'>
|
||||
<source src=${encodeURI(url)} type='video/mp4'>
|
||||
</video>`;
|
||||
} else if (this.file_type == "audio") {
|
||||
return `<audio controls width='100%' controls controlsList='nodownload'>
|
||||
<source src=${encodeURI(url)} type='audio/mp3'>
|
||||
</audio>`;
|
||||
} else if (this.file_type == "pdf") {
|
||||
return `<iframe src="${encodeURI(
|
||||
url
|
||||
)}#toolbar=0" width='100%' height='700px'></iframe>`;
|
||||
} else {
|
||||
return `<img src=${encodeURI(url)} width='100%'>`;
|
||||
}
|
||||
}
|
||||
|
||||
validate(savedData) {
|
||||
return !savedData.file_url || !savedData.file_url.trim() ? false : true;
|
||||
}
|
||||
|
||||
save(block_content) {
|
||||
return {
|
||||
file_url: this.data.file_url || this.file_url,
|
||||
file_type: this.file_type,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const make_instructor_notes_component = () => {
|
||||
this.instructor_notes = new frappe.ui.FieldGroup({
|
||||
fields: [
|
||||
{
|
||||
fieldname: "instructor_notes",
|
||||
fieldtype: "Text",
|
||||
default: $("#current-instructor-notes").html(),
|
||||
},
|
||||
],
|
||||
body: $("#instructor-notes").get(0),
|
||||
});
|
||||
this.instructor_notes.make();
|
||||
$("#instructor-notes .form-section:last").removeClass("empty-section");
|
||||
$("#instructor-notes .frappe-control").removeClass("hide-control");
|
||||
$("#instructor-notes .form-column").addClass("p-0");
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
import frappe
|
||||
from lms.www.utils import get_current_lesson_details, get_common_context
|
||||
from lms.lms.utils import is_instructor, has_course_moderator_role
|
||||
from frappe import _
|
||||
|
||||
|
||||
def get_context(context):
|
||||
get_common_context(context)
|
||||
chapter_index = frappe.form_dict.get("chapter")
|
||||
lesson_index = frappe.form_dict.get("lesson")
|
||||
lesson_number = f"{chapter_index}.{lesson_index}"
|
||||
context.lesson_index = lesson_index
|
||||
context.lesson_number = lesson_number
|
||||
context.chapter = frappe.db.get_value(
|
||||
"Chapter Reference", {"idx": chapter_index, "parent": context.course.name}, "chapter"
|
||||
)
|
||||
context.lesson = get_current_lesson_details(lesson_number, context, True)
|
||||
context.is_moderator = has_course_moderator_role()
|
||||
instructor = is_instructor(context.course.name)
|
||||
|
||||
if not instructor and not has_course_moderator_role():
|
||||
raise frappe.PermissionError(_("You do not have permission to access this page."))
|
||||
@@ -1,72 +0,0 @@
|
||||
% extends "templates/base.html" %}
|
||||
{% block title %}Join a Course{% endblock %}
|
||||
|
||||
{% block head_include %}
|
||||
<meta name="description" content="Join a Course"/>
|
||||
<meta name="keywords" content="" />
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if frappe.session.user == "Guest" %}
|
||||
|
||||
<div class="page-card">
|
||||
<div class='page-card-head'>
|
||||
<span class='indicator blue password-box'>{{ _("Login Required") }}</span>
|
||||
</div>
|
||||
<div class=''>{{ _("Please log in to confirm joining the course" )}} {{ batch.course_title }}.</div>
|
||||
<a type="submit" id="login" class="btn btn-primary w-100"
|
||||
href="/login?redirect-to=/courses/{{ batch.course }}/join?batch={{ batch.name }}">{{_("Login")}}</a>
|
||||
</div>
|
||||
|
||||
{% elif already_a_member %}
|
||||
|
||||
<div class="page-card">
|
||||
<div class='page-card-head'>
|
||||
<span class='indicator blue password-box'>{{ _("Already a member") }}</span>
|
||||
</div>
|
||||
<div class=''>{{ _("You are already a member of the batch") }} {{ batch.title }} {{ _("for the course") }} {{ batch.course_title }}.
|
||||
</div>
|
||||
<a type="submit" id="batch-home" class="btn btn-primary w-100" href="">{{_("Go to Batch Home")}}</a>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
|
||||
<div class="page-card">
|
||||
<div class='page-card-head'>
|
||||
<span class='indicator blue password-box'>{{ _("Confirm your membership") }}</span>
|
||||
</div>
|
||||
<div>{{ _("Please provide your confirmation to be a part of the batch") }} {{ batch.title }} {{ _("for the course") }}
|
||||
{{ batch.course_title }}.
|
||||
</div>
|
||||
<a type="submit" id="confirm" class="btn btn-primary w-100">{{_("Confirm")}}</a>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
<script>
|
||||
frappe.ready(() => {
|
||||
$("#confirm").click((e) => {
|
||||
frappe.call({
|
||||
"method": "lms.lms.doctype.lms_enrollment.lms_enrollment.create_membership",
|
||||
"args": {
|
||||
"batch_old": {{ batch.name }},
|
||||
"course": {{ batch.course }}
|
||||
},
|
||||
"callback": (data) => {
|
||||
if (data.message == "OK") {
|
||||
frappe.msgprint({
|
||||
message: __("You are now a member of this batch!"),
|
||||
clear: true
|
||||
});
|
||||
setTimeout(function () {
|
||||
window.location.href = "/courses/{{ batch.course }}/home";
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,11 +0,0 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
batch_name = frappe.form_dict["batch"]
|
||||
context.batch_old = frappe.get_doc("LMS Batch Old", batch_name)
|
||||
context.already_a_member = context.batch_old.is_member(frappe.session.user)
|
||||
context.batch_old.course_title = frappe.db.get_value(
|
||||
"LMS Course", context.batch_old.course, "title"
|
||||
)
|
||||
@@ -1,265 +0,0 @@
|
||||
{% extends "lms/templates/lms_base.html" %}
|
||||
{% from "www/macros/livecode.html" import LiveCodeEditorJS, LiveCodeEditor with context %}
|
||||
|
||||
|
||||
{% block title %}
|
||||
{{ lesson.title }} - {{ course.title }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block head_include %}
|
||||
<link rel="stylesheet" href="/assets/frappe/css/hljs-night-owl.css">
|
||||
|
||||
{% for ext in page_extensions %}
|
||||
{{ ext.render_header() }}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block page_content %}
|
||||
|
||||
<div class="common-page-style">
|
||||
<div class="container course-details-page">
|
||||
|
||||
<div class="course-content-parent">
|
||||
<div>
|
||||
<div class="bold-heading mb-4">
|
||||
{{ course.title }}
|
||||
</div>
|
||||
|
||||
{% if membership %}
|
||||
<div class="">
|
||||
<div class="progress-percent m-0">{{ progress }}% {{ _("Completed") }}</div>
|
||||
<div class="progress" title="{{ progress }}% Completed">
|
||||
<div class="progress-bar" role="progressbar" aria-valuenow="{{ progress }}"
|
||||
aria-valuemin="0" aria-valuemax="100" style="width:{{ progress }}%">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="course-details-outline">
|
||||
{% set classname = class_info.name if class_info else False %}
|
||||
{{ widgets.CourseOutline(course=course, membership=membership, lesson_page=True, classname=classname) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="lesson-parent">
|
||||
{{ BreadCrumb(course, lesson, class_info) }}
|
||||
{{ LessonContent(lesson, class_info) }}
|
||||
{% if course.status == "Approved" and not course.upcoming and not class_info %}
|
||||
{{ Discussions() }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
<!-- BreadCrumb -->
|
||||
{% macro BreadCrumb(course, lesson, class_info) %}
|
||||
<div class="breadcrumb">
|
||||
{% if class_info %}
|
||||
<a class="dark-links" href="/courses">
|
||||
{{ _("All Batches") }}
|
||||
</a>
|
||||
<img class="ml-1 mr-1" src="/assets/lms/icons/chevron-right.svg">
|
||||
<a class="dark-links" href="/batches/{{ class_info.name }}">
|
||||
{{ class_info.title }}
|
||||
</a>
|
||||
<img class="ml-1 mr-1" src="/assets/lms/icons/chevron-right.svg">
|
||||
<span class="breadcrumb-destination">
|
||||
{{ lesson.title }}
|
||||
</span>
|
||||
{% else %}
|
||||
<a class="dark-links" href="/courses">
|
||||
{{ _("All Courses") }}
|
||||
</a>
|
||||
<img class="ml-1 mr-1" src="/assets/lms/icons/chevron-right.svg">
|
||||
<a class="dark-links" href="/courses/{{ course.name }}">
|
||||
{{ course.title }}
|
||||
</a>
|
||||
<img class="ml-1 mr-1" src="/assets/lms/icons/chevron-right.svg">
|
||||
<span class="breadcrumb-destination">
|
||||
{{ lesson.title }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Lesson Details -->
|
||||
{% macro LessonContent(lesson, class_info) %}
|
||||
{% set instructors = get_instructors(course.name) %}
|
||||
{% set is_instructor = is_instructor(course.name) %}
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<div class="pull-right">
|
||||
{% if get_progress(course.name, lesson.name) == 'Complete' %}
|
||||
<span id="status-indicator" class="indicator-pill green">{{ _("COMPLETED") }}</span>
|
||||
{% endif %}
|
||||
|
||||
<!-- Edit Button -->
|
||||
{% if (is_instructor or has_course_moderator_role()) %}
|
||||
<a class="btn btn-secondary btn-sm ml-2" href="{{ get_lesson_url(course.name, lesson_number) }}/edit">
|
||||
{{ _("Edit") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="course-home-headings title {% if membership %} is-member {% endif %}" id="title"
|
||||
data-index="{{ lesson_index }}" data-course="{{ course.name }}" data-chapter="{{ chapter }}"
|
||||
{% if lesson.name %} data-lesson="{{ lesson.name }}" {% endif %}
|
||||
>{% if lesson.title %}{{ lesson.title }}{% endif %}</div>
|
||||
</div>
|
||||
|
||||
<!-- Instructors -->
|
||||
<div class="d-flex align-items-center">
|
||||
{% set ins_len = instructors | length %}
|
||||
{% for instructor in instructors %}
|
||||
{% if ins_len > 1 and loop.index == 1 %}
|
||||
<div class="avatar-group overlap">
|
||||
{% endif %}
|
||||
{{ widgets.Avatar(member=instructor, avatar_class="avatar-small") }}
|
||||
|
||||
{% if ins_len > 1 and loop.index == ins_len %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<a class="button-links ml-1" href="{{ get_profile_url(instructors[0].username) }}">
|
||||
<span class="course-meta">
|
||||
{% if ins_len == 1 %}
|
||||
{{ instructors[0].full_name }}
|
||||
{% elif ins_len == 2 %}
|
||||
{{ instructors[0].full_name.split(" ")[0] }} and {{ instructors[1].full_name.split(" ")[0] }}
|
||||
{% else %}
|
||||
{% set suffix = "other" if ins_len - 1 == 1 else "others" %}
|
||||
{{ instructors[0].full_name.split(" ")[0] }} and {{ ins_len - 1 }} {{ suffix }}
|
||||
{% endif %}
|
||||
</span>
|
||||
</a>
|
||||
<div class="ml-5 course-meta">
|
||||
{{ frappe.utils.format_date(lesson.creation, "medium") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lesson Content -->
|
||||
<div class="markdown-source lesson-content-card">
|
||||
{% if show_lesson %}
|
||||
|
||||
{% if is_instructor and not lesson.include_in_preview %}
|
||||
<div class="alert alert-info alert-dismissible mb-4">
|
||||
{{ _("This lesson is not available for preview. As you are the Instructor of the course only you can see it.") }}
|
||||
<a href="#" class="close" data-dismiss="alert" aria-label="close">×</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if instructor_notes and (is_moderator or instructor or is_evaluator) %}
|
||||
<div class="alert alert-secondary mb-4">
|
||||
<div class="bold-heading collapse-section collapsed" data-toggle="collapse" data-target="#instructor-notes">
|
||||
<svg class="icon icon-sm mt-1 pull-right">
|
||||
<use href="#icon-up-line"></use>
|
||||
</svg>
|
||||
<div>
|
||||
{{ _("Instructor Notes") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse" id="instructor-notes">
|
||||
{{ instructor_notes }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{{ render_html(lesson) }}
|
||||
|
||||
{% else %}
|
||||
{% set course_link = "<a class='enroll-in-course' data-course=" + course.name | urlencode + " href=''>" + _('here') + "</a>" %}
|
||||
<div class="alert alert-info mb-0">
|
||||
{{ _("There is no preview available for this lesson.
|
||||
Please join the course to access it.
|
||||
Click {0} to enroll.").format(course_link) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if not class_info %}
|
||||
{{ pagination(prev_url, next_url) }}
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Pagination -->
|
||||
{% macro pagination(prev_url, next_url) %}
|
||||
{% if prev_url or next_url %}
|
||||
<div class="lesson-pagination">
|
||||
{% if prev_url %}
|
||||
<a class="btn btn-secondary btn-sm prev" href="{{ prev_url }}">
|
||||
{{ _("Previous Lesson") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if next_url %}
|
||||
<a class="btn btn-primary btn-sm next pull-right" href="{{ next_url }}">
|
||||
{{ _("Next Lesson") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro UploadAttachments() %}
|
||||
<div class="attachments-parent">
|
||||
<div class="attachment-controls">
|
||||
<div class="show-attachments" data-toggle="collapse" data-target="#collapse-attachments" aria-expanded="false">
|
||||
<svg class="icon icon-sm">
|
||||
<use class="" href="#icon-attachment">
|
||||
</svg>
|
||||
<span class="attachment-count" data-count="0">0 {{ _("attachments") }}</span>
|
||||
</div>
|
||||
<div class="add-attachment">
|
||||
<span class="btn btn-sm btn-secondary">
|
||||
<svg class="icon icon-sm">
|
||||
<use class="" href="#icon-upload">
|
||||
</svg>
|
||||
{{ _("Upload Attachments") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<table class="attachments common-card-style collapse hide" id="collapse-attachments"></table>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
<!-- Discussions Component -->
|
||||
{% macro Discussions() %}
|
||||
{% set topics_count = frappe.db.count("Discussion Topic", {
|
||||
"reference_doctype": "Course Lesson",
|
||||
"reference_docname": lesson.name
|
||||
}) %}
|
||||
{% set condition = is_instructor(course.name) or membership or has_course_moderator_role() %}
|
||||
{% set doctype, docname = _("Course Lesson"), lesson.name %}
|
||||
{% set title = "Questions" if topics_count else "" %}
|
||||
{% set cta_title = "Ask a Question" %}
|
||||
{% set button_name = _("Start Learning") %}
|
||||
{% set redirect_to = "/courses/" + course.name %}
|
||||
{% set empty_state_title = _("Have a doubt?") %}
|
||||
{% set empty_state_subtitle = _("Post it here, our mentors will help you out.") %}
|
||||
<div class="pt-8">
|
||||
{% include "frappe/templates/discussions/discussions_section.html" %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{%- block script %}
|
||||
{{ super() }}
|
||||
{{ include_script('controls.bundle.js') }}
|
||||
<script type="text/javascript">
|
||||
var page_context = {{ page_context | tojson }};
|
||||
{% include "lms/templates/quiz/quiz.js" %}
|
||||
</script>
|
||||
{% for ext in page_extensions %}
|
||||
{{ ext.render_footer() }}
|
||||
{% endfor %}
|
||||
{%- endblock %}
|
||||
@@ -1,253 +0,0 @@
|
||||
frappe.ready(() => {
|
||||
this.marked_as_complete = false;
|
||||
let self = this;
|
||||
|
||||
frappe.telemetry.capture("on_lesson_page", "lms");
|
||||
|
||||
fetch_assignments();
|
||||
|
||||
save_current_lesson();
|
||||
|
||||
$(window).scroll(() => {
|
||||
let self = this;
|
||||
if (
|
||||
!$("#status-indicator").length &&
|
||||
!self.marked_as_complete &&
|
||||
$(".title").hasClass("is-member")
|
||||
) {
|
||||
self.marked_as_complete = true;
|
||||
mark_progress();
|
||||
}
|
||||
});
|
||||
|
||||
$("#certification").click((e) => {
|
||||
create_certificate(e);
|
||||
});
|
||||
|
||||
$(".submit-work").click((e) => {
|
||||
attach_work(e);
|
||||
});
|
||||
|
||||
$(".clear-work").click((e) => {
|
||||
clear_work(e);
|
||||
});
|
||||
|
||||
$(".btn-back").click((e) => {
|
||||
window.location.href = window.location.href.split("?")[0];
|
||||
});
|
||||
|
||||
$(document).on("click", ".copy-link", (e) => {
|
||||
frappe.utils.copy_to_clipboard($(e.currentTarget).data("link"));
|
||||
$(".attachments").collapse("hide");
|
||||
});
|
||||
});
|
||||
|
||||
const save_current_lesson = () => {
|
||||
if ($(".title").hasClass("is-member")) {
|
||||
frappe.call("lms.lms.api.save_current_lesson", {
|
||||
course_name: $(".title").attr("data-course"),
|
||||
lesson_name: $(".title").attr("data-lesson"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const mark_progress = () => {
|
||||
let status = "Complete";
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.course_lesson.course_lesson.save_progress",
|
||||
args: {
|
||||
lesson: $(".title").attr("data-lesson"),
|
||||
course: $(".title").attr("data-course"),
|
||||
status: status,
|
||||
},
|
||||
callback: (data) => {
|
||||
if (data.message) {
|
||||
change_progress_indicators();
|
||||
show_certificate_if_course_completed(data);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const change_progress_indicators = () => {
|
||||
$(".active-lesson .lesson-progress-tick").removeClass("hide");
|
||||
};
|
||||
|
||||
const show_certificate_if_course_completed = (data) => {
|
||||
if (
|
||||
data.message == 100 &&
|
||||
!$(".next").length &&
|
||||
$("#certification").hasClass("hide")
|
||||
) {
|
||||
$("#certification").removeClass("hide");
|
||||
}
|
||||
};
|
||||
|
||||
const create_certificate = (e) => {
|
||||
e.preventDefault();
|
||||
course = $(".title").attr("data-course");
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_certificate.lms_certificate.create_certificate",
|
||||
args: {
|
||||
course: course,
|
||||
},
|
||||
callback: (data) => {
|
||||
window.location.href = `/courses/${course}/${data.message.name}`;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const attach_work = (e) => {
|
||||
const target = $(e.currentTarget);
|
||||
let files = target.siblings(".attach-file").prop("files");
|
||||
if (files && files.length) {
|
||||
files = add_files(files);
|
||||
return_as_dataurl(files);
|
||||
files.map((file) => {
|
||||
upload_file(file, target);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const upload_file = (file, target) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState == XMLHttpRequest.DONE) {
|
||||
if (xhr.status === 200) {
|
||||
let response = JSON.parse(xhr.responseText);
|
||||
create_lesson_work(response.message, target);
|
||||
} else if (xhr.status === 403) {
|
||||
let response = JSON.parse(xhr.responseText);
|
||||
frappe.msgprint(
|
||||
`Not permitted. ${response._error_message || ""}`
|
||||
);
|
||||
} else if (xhr.status === 413) {
|
||||
frappe.msgprint(
|
||||
__("Size exceeds the maximum allowed file size.")
|
||||
);
|
||||
} else {
|
||||
frappe.msgprint(
|
||||
xhr.status === 0
|
||||
? "XMLHttpRequest Error"
|
||||
: `${xhr.status} : ${xhr.statusText}`
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.open("POST", "/api/method/upload_file", true);
|
||||
xhr.setRequestHeader("Accept", "application/json");
|
||||
xhr.setRequestHeader("X-Frappe-CSRF-Token", frappe.csrf_token);
|
||||
|
||||
let form_data = new FormData();
|
||||
if (file.file_obj) {
|
||||
form_data.append("file", file.file_obj, file.name);
|
||||
}
|
||||
|
||||
xhr.send(form_data);
|
||||
});
|
||||
};
|
||||
|
||||
const create_lesson_work = (file, target) => {
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_assignment_submission.lms_assignment_submission.upload_assignment",
|
||||
args: {
|
||||
assignment_attachment: file.file_url,
|
||||
lesson: $(".title").attr("data-lesson"),
|
||||
submission: $(".preview-work").data("submission") || "",
|
||||
},
|
||||
callback: (data) => {
|
||||
target.siblings(".attach-file").addClass("hide");
|
||||
target.siblings(".preview-work").removeClass("hide");
|
||||
target
|
||||
.siblings(".preview-work")
|
||||
.find("a")
|
||||
.attr("href", file.file_url)
|
||||
.text(file.file_name);
|
||||
target.addClass("hide");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const return_as_dataurl = (files) => {
|
||||
let promises = files.map((file) =>
|
||||
frappe.dom.file_to_base64(file.file_obj).then((dataurl) => {
|
||||
file.dataurl = dataurl;
|
||||
this.on_success && this.on_success(file);
|
||||
})
|
||||
);
|
||||
return Promise.all(promises);
|
||||
};
|
||||
|
||||
const add_files = (files) => {
|
||||
files = Array.from(files).map((file) => {
|
||||
let is_image = file.type.startsWith("image");
|
||||
return {
|
||||
file_obj: file,
|
||||
cropper_file: file,
|
||||
crop_box_data: null,
|
||||
optimize: this.attach_doc_image ? true : false,
|
||||
name: file.name,
|
||||
doc: null,
|
||||
progress: 0,
|
||||
total: 0,
|
||||
failed: false,
|
||||
request_succeeded: false,
|
||||
error_message: null,
|
||||
uploading: false,
|
||||
private: !is_image,
|
||||
};
|
||||
});
|
||||
return files;
|
||||
};
|
||||
|
||||
const clear_work = (e) => {
|
||||
const target = $(e.currentTarget);
|
||||
const parent = target.closest(".preview-work");
|
||||
parent.addClass("hide");
|
||||
parent.siblings(".attach-file").removeClass("hide").val(null);
|
||||
parent.siblings(".submit-work").removeClass("hide");
|
||||
};
|
||||
|
||||
const fetch_assignments = () => {
|
||||
if ($(".attach-file").length <= 0) return;
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_assignment_submission.lms_assignment_submission.get_assignment",
|
||||
args: {
|
||||
lesson: $(".title").attr("data-lesson"),
|
||||
},
|
||||
callback: (data) => {
|
||||
if (data.message) {
|
||||
const assignment = data.message;
|
||||
const status = assignment.status;
|
||||
let target = $(".attach-file");
|
||||
target.addClass("hide");
|
||||
target.siblings(".submit-work").addClass("hide");
|
||||
target.siblings(".preview-work").removeClass("hide");
|
||||
if (status != "Not Graded") {
|
||||
let color = status == "Pass" ? "green" : "red";
|
||||
$(".assignment-status")
|
||||
.removeClass("hide")
|
||||
.addClass(color)
|
||||
.text(data.message.status);
|
||||
target.siblings(".alert").addClass("hide");
|
||||
$(".clear-work").addClass("hide");
|
||||
if (assignment.comments) {
|
||||
$(".comments").removeClass("hide");
|
||||
$(".comment").text(assignment.comments);
|
||||
}
|
||||
}
|
||||
target
|
||||
.siblings(".preview-work")
|
||||
.find("a")
|
||||
.attr("href", assignment.assignment_attachment)
|
||||
.text(assignment.file_name);
|
||||
|
||||
target
|
||||
.siblings(".preview-work")
|
||||
.attr("data-submission", assignment.name);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,119 +0,0 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import cstr, flt
|
||||
from lms.lms.md import markdown_to_html
|
||||
|
||||
from lms.lms.utils import (
|
||||
get_lesson_url,
|
||||
has_course_moderator_role,
|
||||
is_instructor,
|
||||
has_course_evaluator_role,
|
||||
)
|
||||
from lms.www.utils import (
|
||||
get_common_context,
|
||||
redirect_to_lesson,
|
||||
get_current_lesson_details,
|
||||
)
|
||||
|
||||
|
||||
def get_context(context):
|
||||
get_common_context(context)
|
||||
|
||||
chapter_index = frappe.form_dict.get("chapter")
|
||||
lesson_index = frappe.form_dict.get("lesson")
|
||||
class_name = frappe.form_dict.get("class")
|
||||
|
||||
if class_name:
|
||||
context.class_info = frappe._dict(
|
||||
{
|
||||
"name": class_name,
|
||||
"title": frappe.db.get_value("LMS Batch", class_name, "title"),
|
||||
}
|
||||
)
|
||||
|
||||
lesson_number = f"{chapter_index}.{lesson_index}"
|
||||
context.lesson_number = lesson_number
|
||||
context.lesson_index = lesson_index
|
||||
context.chapter = frappe.db.get_value(
|
||||
"Chapter Reference", {"idx": chapter_index, "parent": context.course.name}, "chapter"
|
||||
)
|
||||
|
||||
if not chapter_index or not lesson_index:
|
||||
index_ = "1.1"
|
||||
redirect_to_lesson(context.course, index_)
|
||||
|
||||
context.lesson = get_current_lesson_details(lesson_number, context)
|
||||
context.instructor = is_instructor(context.course.name)
|
||||
context.is_moderator = has_course_moderator_role()
|
||||
context.is_evaluator = has_course_evaluator_role()
|
||||
|
||||
if context.lesson.instructor_notes:
|
||||
context.instructor_notes = markdown_to_html(context.lesson.instructor_notes)
|
||||
|
||||
context.show_lesson = (
|
||||
context.membership
|
||||
or (context.lesson and context.lesson.include_in_preview)
|
||||
or context.instructor
|
||||
or context.is_moderator
|
||||
or context.is_evaluator
|
||||
)
|
||||
|
||||
if not context.lesson:
|
||||
context.lesson = frappe._dict()
|
||||
|
||||
if frappe.form_dict.get("edit"):
|
||||
if not context.instructor and not context.is_moderator:
|
||||
raise frappe.PermissionError(_("You do not have permission to access this page."))
|
||||
context.lesson.edit_mode = True
|
||||
else:
|
||||
neighbours = get_neighbours(lesson_number, context.lessons)
|
||||
context.next_url = get_url(neighbours["next"], context.course)
|
||||
context.prev_url = get_url(neighbours["prev"], context.course)
|
||||
|
||||
meta_info = (
|
||||
context.lesson.title + " - " + context.course.title
|
||||
if context.lesson.title
|
||||
else "New Lesson"
|
||||
)
|
||||
context.metatags = {
|
||||
"title": meta_info,
|
||||
"keywords": meta_info,
|
||||
"description": meta_info,
|
||||
}
|
||||
|
||||
context.page_extensions = get_page_extensions(context)
|
||||
context.page_context = {
|
||||
"course": context.course.name,
|
||||
"batch_old": context.batch_old,
|
||||
"lesson": context.lesson.name if context.lesson.name else "New Lesson",
|
||||
"is_member": context.membership is not None,
|
||||
}
|
||||
|
||||
|
||||
def get_url(lesson_number, course):
|
||||
return (
|
||||
get_lesson_url(course.name, lesson_number)
|
||||
and get_lesson_url(course.name, lesson_number) + course.query_parameter
|
||||
)
|
||||
|
||||
|
||||
def get_page_extensions(context):
|
||||
default_value = ["lms.plugins.PageExtension"]
|
||||
classnames = frappe.get_hooks("lms_lesson_page_extensions") or default_value
|
||||
extensions = [frappe.get_attr(name)() for name in classnames]
|
||||
for e in extensions:
|
||||
e.set_context(context)
|
||||
return extensions
|
||||
|
||||
|
||||
def get_neighbours(current, lessons):
|
||||
numbers = [lesson.number for lesson in lessons]
|
||||
tuples_list = [tuple(int(x) for x in s.split(".")) for s in numbers]
|
||||
sorted_tuples = sorted(tuples_list)
|
||||
sorted_numbers = [".".join(str(num) for num in t) for t in sorted_tuples]
|
||||
index = sorted_numbers.index(current)
|
||||
|
||||
return {
|
||||
"prev": sorted_numbers[index - 1] if index - 1 >= 0 else None,
|
||||
"next": sorted_numbers[index + 1] if index + 1 < len(sorted_numbers) else None,
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
{% extends "templates/base.html" %}
|
||||
{% block title %}
|
||||
{{ quiz.title if quiz.name else _("Quiz Details") }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div class="common-page-style">
|
||||
{{ Header() }}
|
||||
<div class="container form-width">
|
||||
{{ QuizForm(quiz) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% macro QuizForm(quiz) %}
|
||||
<div id="quiz-form" {% if quiz.name %} data-name="{{ quiz.name }}" data-index="{{ quiz.questions | length }}" {% endif %}>
|
||||
{{ QuizDetails(quiz) }}
|
||||
<div class="field-group">
|
||||
<div class="questions-table"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro Header() %}
|
||||
<header class="sticky">
|
||||
<div class="container form-width">
|
||||
<div class="edit-header">
|
||||
<div>
|
||||
<div class="page-title">
|
||||
{{ _("Quiz Details") }}
|
||||
</div>
|
||||
<div class="vertically-center small">
|
||||
<a class="dark-links" href="/quizzes">
|
||||
{{ _("Quiz List") }}
|
||||
</a>
|
||||
<img class="icon icon-sm mr-0" src="/assets/lms/icons/chevron-right.svg">
|
||||
<span class="breadcrumb-destination">
|
||||
{{ quiz.title if quiz.title else _("New Quiz") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="align-self-center">
|
||||
<button class="btn btn-primary btn-sm btn-save-quiz">
|
||||
{{ _("Save") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro QuizDetails(quiz) %}
|
||||
<div class="field-parent">
|
||||
<div class="field-group">
|
||||
<div>
|
||||
<div class="field-label reqd">
|
||||
{{ _("Title") }}
|
||||
</div>
|
||||
<div class="field-description">
|
||||
{{ _("Add a title for the quiz") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="">
|
||||
<input type="text" class="field-input" id="quiz-title" {% if quiz.name %} value="{{ quiz.title }}" data-title="{{ quiz.title }}" {% endif %}>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<div class="field-label">
|
||||
{{ _("Max Attempts") }}
|
||||
</div>
|
||||
<div class="field-description">
|
||||
{{ _("Enter the maximum number of times a user can attempt this quiz") }}
|
||||
</div>
|
||||
<div>
|
||||
{% set max_attempts = quiz.max_attempts if quiz.name else 0 %}
|
||||
<input type="number" class="field-input" id="max-attempts" value="{{ max_attempts }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<div class="field-label reqd">
|
||||
{{ _("Passing Percentage") }}
|
||||
</div>
|
||||
<div class="field-description">
|
||||
{{ _("Minimum percentage required to pass this quiz.") }}
|
||||
</div>
|
||||
<div>
|
||||
<input type="number" class="field-input" id="passing-percentage" value="{{ quiz.passing_percentage }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-group vertically-center">
|
||||
{% set show_answers = quiz.show_answers or not quiz.name %}
|
||||
<label for="show-answers" class="vertically-center mb-0">
|
||||
<input type="checkbox" id="show-answers" {% if show_answers %} checked {% endif %}>
|
||||
{{ _("Show Answers") }}
|
||||
</label>
|
||||
<label for="show-submission-history" class="vertically-center mb-0 ml-20">
|
||||
<input type="checkbox" id="show-submission-history" {% if quiz.show_submission_history %} checked {% endif %}>
|
||||
{{ _("Show Submission History") }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro Question(question, index) %}
|
||||
{% set type = question.type if question.type else "Choices" %}
|
||||
<div class="list-row question-row" role="button" data-question="{{ question.name }}">
|
||||
<div class="flex clickable">
|
||||
<span class="mr-1">
|
||||
{{ index }}.
|
||||
</span>
|
||||
{{ question.question.split("\n")[0] }}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro EmptyState() %}
|
||||
<article class="empty-state my-5">
|
||||
<div class="text-center">
|
||||
<div class="bold-heading">
|
||||
{{ _("You have not added any question yet") }}
|
||||
</div>
|
||||
<div>
|
||||
{{ _("Create and manage questions from here.") }}
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<button class="btn btn-default btn-sm btn-add-question">
|
||||
<span>
|
||||
{{ _("Add Question") }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{% endmacro %}
|
||||
|
||||
{%- block script %}
|
||||
{{ super() }}
|
||||
{% if has_course_instructor_role() or has_course_moderator_role() %}
|
||||
<script>
|
||||
const quiz_questions = {{ quiz.questions or [] }}
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -1,307 +0,0 @@
|
||||
frappe.ready(() => {
|
||||
if ($(".questions-table").length) {
|
||||
frappe.require("controls.bundle.js", () => {
|
||||
create_questions_table();
|
||||
});
|
||||
}
|
||||
|
||||
$(".btn-save-quiz").click((e) => {
|
||||
save_quiz();
|
||||
});
|
||||
|
||||
$(".question-row").click((e) => {
|
||||
edit_question(e);
|
||||
});
|
||||
|
||||
$(document).on("click", ".questions-table .link-btn", (e) => {
|
||||
e.preventDefault();
|
||||
fetch_question_data(e);
|
||||
});
|
||||
});
|
||||
|
||||
const show_question_modal = (values = {}) => {
|
||||
let fields = get_question_fields(values);
|
||||
|
||||
this.question_dialog = new frappe.ui.Dialog({
|
||||
title: __("Add Question"),
|
||||
fields: fields,
|
||||
primary_action: (data) => {
|
||||
if (values) data.name = values.name;
|
||||
save_question(data);
|
||||
},
|
||||
});
|
||||
|
||||
question_dialog.show();
|
||||
};
|
||||
|
||||
const get_question_fields = (values = {}) => {
|
||||
if (!values.question) values = {};
|
||||
|
||||
let dialog_fields = [
|
||||
{
|
||||
fieldtype: "Text Editor",
|
||||
fieldname: "question",
|
||||
label: __("Question"),
|
||||
reqd: 1,
|
||||
default: values.question || "",
|
||||
},
|
||||
{
|
||||
fieldtype: "Select",
|
||||
fieldname: "type",
|
||||
label: __("Type"),
|
||||
options: ["Choices", "User Input"],
|
||||
default: values.type || "Choices",
|
||||
},
|
||||
];
|
||||
Array.from({ length: 4 }, (x, i) => {
|
||||
num = i + 1;
|
||||
|
||||
dialog_fields.push({
|
||||
fieldtype: "Section Break",
|
||||
fieldname: `section_break_${num}`,
|
||||
});
|
||||
|
||||
let option = {
|
||||
fieldtype: "Small Text",
|
||||
fieldname: `option_${num}`,
|
||||
label: __("Option") + ` ${num}`,
|
||||
depends_on: "eval:doc.type=='Choices'",
|
||||
default: values[`option_${num}`] || "",
|
||||
};
|
||||
|
||||
if (num <= 2) option.mandatory_depends_on = "eval:doc.type=='Choices'";
|
||||
|
||||
dialog_fields.push(option);
|
||||
console.log(dialog_fields);
|
||||
|
||||
dialog_fields.push({
|
||||
fieldtype: "Data",
|
||||
fieldname: `explanaion_${num}`,
|
||||
label: __("Explanation"),
|
||||
depends_on: "eval:doc.type=='Choices'",
|
||||
default: values[`explanaion_${num}`] || "",
|
||||
});
|
||||
|
||||
let is_correct = {
|
||||
fieldtype: "Check",
|
||||
fieldname: `is_correct_${num}`,
|
||||
label: __("Is Correct"),
|
||||
depends_on: "eval:doc.type=='Choices'",
|
||||
default: values[`is_correct_${num}`] || 0,
|
||||
};
|
||||
|
||||
if (num <= 2)
|
||||
is_correct.mandatory_depends_on = "eval:doc.type=='Choices'";
|
||||
|
||||
dialog_fields.push(is_correct);
|
||||
|
||||
possibility = {
|
||||
fieldtype: "Small Text",
|
||||
fieldname: `possibility_${num}`,
|
||||
label: __("Possible Answer") + ` ${num}`,
|
||||
depends_on: "eval:doc.type=='User Input'",
|
||||
default: values[`possibility_${num}`] || "",
|
||||
};
|
||||
|
||||
if (num == 1)
|
||||
possibility.mandatory_depends_on = "eval:doc.type=='User Input'";
|
||||
|
||||
dialog_fields.push(possibility);
|
||||
});
|
||||
|
||||
return dialog_fields;
|
||||
};
|
||||
|
||||
const edit_question = (e) => {
|
||||
let question = $(e.currentTarget).data("question");
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_quiz.lms_quiz.get_question_details",
|
||||
args: {
|
||||
question: question,
|
||||
},
|
||||
callback: (data) => {
|
||||
if (data.message) show_question_modal(data.message);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const save_quiz = (values) => {
|
||||
validate_mandatory();
|
||||
validate_questions();
|
||||
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_quiz.lms_quiz.save_quiz",
|
||||
args: {
|
||||
quiz_title: $("#quiz-title").val(),
|
||||
max_attempts: $("#max-attempts").val(),
|
||||
passing_percentage: $("#passing-percentage").val(),
|
||||
quiz: $("#quiz-form").data("name") || "",
|
||||
questions: this.table.get_value("questions"),
|
||||
show_answers: $("#show-answers").is(":checked") ? 1 : 0,
|
||||
show_submission_history: $("#show-submission-history").is(
|
||||
":checked"
|
||||
)
|
||||
? 1
|
||||
: 0,
|
||||
},
|
||||
callback: (data) => {
|
||||
frappe.show_alert({
|
||||
message: __("Saved"),
|
||||
indicator: "green",
|
||||
});
|
||||
setTimeout(() => {
|
||||
window.location.href = `/quizzes/${data.message}`;
|
||||
}, 2000);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const validate_mandatory = () => {
|
||||
let fields = ["#quiz-title", "#passing-percentage"];
|
||||
fields.forEach((field, idx) => {
|
||||
if (!$(field).val()) {
|
||||
let error = $("p")
|
||||
.addClass("error-message")
|
||||
.text(__("Please enter a value"));
|
||||
$(error).insertAfter(field);
|
||||
scroll_to_element($(field));
|
||||
throw "This field is mandatory";
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const validate_questions = () => {
|
||||
let questions = this.table.get_value("questions");
|
||||
|
||||
if (!questions.length) {
|
||||
frappe.throw(__("Please add a question."));
|
||||
}
|
||||
|
||||
questions.forEach((question, index) => {
|
||||
if (!question.question) {
|
||||
frappe.throw(__("Please add question in row") + " " + (index + 1));
|
||||
}
|
||||
|
||||
if (!question.marks) {
|
||||
frappe.throw(__("Please add marks in row") + " " + (index + 1));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const scroll_to_element = (element) => {
|
||||
if ($(element).length) {
|
||||
$([document.documentElement, document.body]).animate(
|
||||
{
|
||||
scrollTop: $(element).offset().top - 100,
|
||||
},
|
||||
1000
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const save_question = (values) => {
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_quiz.lms_quiz.save_question",
|
||||
args: {
|
||||
quiz: $("#quiz-form").data("name") || "",
|
||||
values: values,
|
||||
index: $("#quiz-form").data("index") + 1,
|
||||
},
|
||||
callback: (data) => {
|
||||
if (data.message) this.question_dialog.hide();
|
||||
|
||||
if (values.name) {
|
||||
frappe.show_alert({
|
||||
message: __("Saved"),
|
||||
indicator: "green",
|
||||
});
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
} else {
|
||||
let details = {
|
||||
question: data.message,
|
||||
};
|
||||
index = this.table.get_value("questions").length;
|
||||
add_question_row(details, index);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const create_questions_table = () => {
|
||||
this.table = new frappe.ui.FieldGroup({
|
||||
fields: [
|
||||
{
|
||||
fieldname: "questions",
|
||||
fieldtype: "Table",
|
||||
in_place_edit: 1,
|
||||
label: __("Questions"),
|
||||
fields: [
|
||||
{
|
||||
fieldname: "question",
|
||||
fieldtype: "Link",
|
||||
label: __("Question"),
|
||||
options: "LMS Question",
|
||||
in_list_view: 1,
|
||||
only_select: 1,
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "marks",
|
||||
fieldtype: "Int",
|
||||
label: __("Marks"),
|
||||
in_list_view: 1,
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "question_name",
|
||||
fieldname: "Link",
|
||||
options: "LMS Quiz Question",
|
||||
label: __("Question Name"),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
body: $(".questions-table").get(0),
|
||||
});
|
||||
this.table.make();
|
||||
$(".questions-table .form-section:last").removeClass("empty-section");
|
||||
$(".questions-table .frappe-control").removeClass("hide-control");
|
||||
$(".questions-table .form-column").addClass("p-0");
|
||||
|
||||
quiz_questions.forEach((question, idx) => {
|
||||
add_question_row(question, idx);
|
||||
});
|
||||
this.table.fields_dict["questions"].grid.add_custom_button(
|
||||
"New Question",
|
||||
show_question_modal,
|
||||
"bottom"
|
||||
);
|
||||
};
|
||||
|
||||
const add_question_row = (question, idx) => {
|
||||
this.table.fields_dict["questions"].grid.add_new_row();
|
||||
this.table.get_value("questions")[idx] = {
|
||||
question: question.question,
|
||||
marks: question.marks,
|
||||
};
|
||||
this.table.refresh();
|
||||
};
|
||||
|
||||
const fetch_question_data = (e) => {
|
||||
let question_name = $(e.currentTarget)
|
||||
.find(".btn-open")
|
||||
.attr("href")
|
||||
.split("/")[3];
|
||||
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_question.lms_question.get_question_details",
|
||||
args: {
|
||||
question: question_name,
|
||||
},
|
||||
callback: (data) => {
|
||||
show_question_modal(data.message);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
import frappe
|
||||
from frappe.utils import cstr
|
||||
from frappe import _
|
||||
from lms.lms.utils import has_course_instructor_role, has_course_moderator_role
|
||||
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
|
||||
if not has_course_moderator_role() or not has_course_instructor_role():
|
||||
message = "You do not have permission to access this page."
|
||||
if frappe.session.user == "Guest":
|
||||
message = "Please login to access this page."
|
||||
|
||||
raise frappe.PermissionError(_(message))
|
||||
|
||||
quizname = frappe.form_dict["quizname"]
|
||||
if quizname == "new-quiz":
|
||||
context.quiz = frappe._dict()
|
||||
else:
|
||||
|
||||
context.quiz = frappe.db.get_value(
|
||||
"LMS Quiz",
|
||||
quizname,
|
||||
[
|
||||
"title",
|
||||
"name",
|
||||
"max_attempts",
|
||||
"passing_percentage",
|
||||
"show_answers",
|
||||
"show_submission_history",
|
||||
],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
fields_arr = ["name", "question", "marks"]
|
||||
context.quiz.questions = frappe.get_all(
|
||||
"LMS Quiz Question", {"parent": quizname}, fields_arr, order_by="idx"
|
||||
)
|
||||
@@ -1,65 +0,0 @@
|
||||
{% extends "templates/base.html" %}
|
||||
{% block title %}
|
||||
{{ _("Quiz List") }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div class="common-page-style">
|
||||
<div class="container form-width">
|
||||
{{ Header() }}
|
||||
{% if quiz_list | length %}
|
||||
{{ QuizList(quiz_list) }}
|
||||
{% else %}
|
||||
{{ EmptyState() }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% macro Header() %}
|
||||
<header class="sticky">
|
||||
<div class="edit-header">
|
||||
<div class="page-title">
|
||||
{{ _("Quiz List") }}
|
||||
</div>
|
||||
|
||||
<a class="btn btn-primary btn-sm align-self-center" href="/quizzes/new-quiz">
|
||||
{{ _("Add Quiz") }}
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro QuizList(quiz_list) %}
|
||||
<div class="mt-5">
|
||||
<ul class="list-unstyled">
|
||||
{% for quiz in quiz_list %}
|
||||
<li class="outline-lesson">
|
||||
<a class="clickable" href="/quizzes/{{ quiz.name }}">
|
||||
<span>
|
||||
{{ loop.index }}.
|
||||
</span>
|
||||
<span>
|
||||
{{ quiz.title }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro EmptyState() %}
|
||||
<div class="empty-state mt-5">
|
||||
<img class="icon icon-xl" src="/assets/lms/icons/comment.svg">
|
||||
<div class="empty-state-text">
|
||||
<div class="empty-state-heading">
|
||||
{{ _("You have not created any quiz yet.") }}
|
||||
</div>
|
||||
<div class="course-meta ">
|
||||
{{ _("Create a quiz and add it to your course to engage your users.") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
@@ -1,17 +0,0 @@
|
||||
import frappe
|
||||
from lms.lms.utils import has_course_instructor_role, has_course_moderator_role
|
||||
from frappe import _
|
||||
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
|
||||
if not has_course_moderator_role() or not has_course_instructor_role():
|
||||
message = "You do not have permission to access this page."
|
||||
if frappe.session.user == "Guest":
|
||||
message = "Please login to access this page."
|
||||
|
||||
raise frappe.PermissionError(_(message))
|
||||
|
||||
filters = {} if has_course_moderator_role() else {"owner": frappe.session.user}
|
||||
context.quiz_list = frappe.get_all("LMS Quiz", filters, ["name", "title"])
|
||||
@@ -1,652 +0,0 @@
|
||||
{% extends "lms/templates/lms_base.html" %}
|
||||
{% block title %}
|
||||
{{ _(batch_info.title) }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block page_content %}
|
||||
<div class="common-page-style lms-page-style">
|
||||
<div class="container">
|
||||
{{ BreadCrumb(batch_info) }}
|
||||
<div class="">
|
||||
{{ BatchDetails(batch_info) }}
|
||||
{{ BatchSections(batch_info, batch_courses, batch_students, flow) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
<!-- BreadCrumb -->
|
||||
{% macro BreadCrumb(batch_info) %}
|
||||
<div class="breadcrumb">
|
||||
<a class="dark-links" href="/batches">{{ _("All Batches") }}</a>
|
||||
<img class="ml-1 mr-1" src="/assets/lms/icons/chevron-right.svg">
|
||||
<a class="dark-links" href="/batches/details/{{ batch_info.name }}">{{ _("Batch Details") }}</a>
|
||||
<img class="ml-1 mr-1" src="/assets/lms/icons/chevron-right.svg">
|
||||
<span class="breadcrumb-destination">{{ batch_info.title }}</span>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Batch Details -->
|
||||
{% macro BatchDetails(batch_info) %}
|
||||
<div class="class-details" data-batch="{{ batch_info.name }}">
|
||||
|
||||
<div class="page-title">
|
||||
{{ batch_info.title }}
|
||||
</div>
|
||||
|
||||
{% if batch_info.description %}
|
||||
<div class="mb-4">
|
||||
{{ batch_info.description }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="vertically-center">
|
||||
<div class="">
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-calendar"></use>
|
||||
</svg>
|
||||
<span>
|
||||
{{ frappe.utils.format_date(batch_info.start_date, "long") }}
|
||||
</span>
|
||||
|
||||
{% if batch_info.start_date != batch_info.end_date %}
|
||||
<span>
|
||||
- {{ frappe.utils.format_date(batch_info.end_date, "long") }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<span class="seperator"></span>
|
||||
|
||||
<div class="">
|
||||
<svg class="icon icon-md">
|
||||
<use href="#icon-education"></use>
|
||||
</svg>
|
||||
{{ batch_courses | length }} {{ _("Courses") }}
|
||||
</div>
|
||||
|
||||
<span class="seperator"></span>
|
||||
|
||||
<div class="">
|
||||
<svg class="icon icon-md">
|
||||
<use href="#icon-users"></use>
|
||||
</svg>
|
||||
{{ batch_students | length }} {{ _("Students") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if batch_info.custom_component %}
|
||||
<div class="mt-4">
|
||||
{{ batch_info.custom_component }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro BatchSections(batch_info, batch_courses, batch_students, flow) %}
|
||||
<div class="mt-4">
|
||||
|
||||
<ul class="nav lms-nav" id="batches-tab">
|
||||
{% if settings.show_dashboard and is_student %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if is_student %} active {% endif %}" data-toggle="tab" href="#dashboard">
|
||||
{{ _("Dashboard") }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if settings.show_courses %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if not is_student %} active {% endif %}" data-toggle="tab" href="#courses">
|
||||
{{ _("Courses") }}
|
||||
<span class="course-list-count">
|
||||
{{ batch_courses | length }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if show_timetable %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#timetable">
|
||||
{{ _("Timetable") }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if is_moderator %}
|
||||
{% if settings.show_students %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#students">
|
||||
{{ _("Students") }}
|
||||
<span class="course-list-count">
|
||||
{{ batch_students | length }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if settings.show_assessments %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#assessments">
|
||||
{{ _("Assessments") }}
|
||||
<span class="course-list-count">
|
||||
{{ assessments | length }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if settings.show_emails %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#emails">
|
||||
{{ _("Emails") }}
|
||||
<span class="course-list-count">
|
||||
{{ batch_emails | length }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if batch_students | length and (is_moderator or is_student) %}
|
||||
{% if settings.show_discussions %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#discussions">
|
||||
{{ _("Discussions") }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if settings.show_live_class %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#live-class">
|
||||
{{ _("Live Class") }}
|
||||
<span class="course-list-count">
|
||||
{{ live_classes | length }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if custom_tabs_header %}
|
||||
{% include custom_tabs_header %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
<div class="border-bottom mb-4"></div>
|
||||
|
||||
<div class="tab-content">
|
||||
|
||||
{% if settings.show_dashboard and is_student %}
|
||||
<div class="tab-pane {% if is_student %} active {% endif %}" id="dashboard" role="tabpanel" aria-labelledby="dashboard">
|
||||
{{ Dashboard(batch_info, batch_courses, current_student) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if settings.show_courses %}
|
||||
<div class="tab-pane {% if not is_student %} active {% endif %}" id="courses" role="tabpanel" aria-labelledby="courses">
|
||||
{{ CoursesSection(batch_info, batch_courses) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if show_timetable %}
|
||||
<div class="tab-pane" id="timetable" role="tabpanel" aria-labelledby="timetable">
|
||||
{{ Timetable() }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_moderator %}
|
||||
{% if settings.show_students %}
|
||||
<div class="tab-pane" id="students" role="tabpanel" aria-labelledby="students">
|
||||
{{ StudentsSection(batch_info, batch_students) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if settings.show_assessments %}
|
||||
<div class="tab-pane" id="assessments" role="tabpanel" aria-labelledby="assessments">
|
||||
{{ AssessmentsSection(batch_info) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if settings.show_emails %}
|
||||
<div class="tab-pane" id="emails" role="tabpanel" aria-labelledby="emails">
|
||||
{{ EmailsSection() }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if batch_students | length and (is_moderator or is_student or is_evaluator) %}
|
||||
{% if settings.show_discussions %}
|
||||
<div class="tab-pane" id="discussions" role="tabpanel" aria-labelledby="discussions">
|
||||
{{ Discussions(batch_info) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if settings.show_live_class %}
|
||||
<div class="tab-pane" id="live-class" role="tabpanel" aria-labelledby="live-class">
|
||||
{{ LiveClassSection(batch_info, live_classes) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if custom_tabs_content %}
|
||||
{% include custom_tabs_content %}
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro Dashboard(batch_info, batch_courses, current_student) %}
|
||||
|
||||
{% set upcoming_evals = current_student.upcoming_evals %}
|
||||
{% set assessments = current_student.assessments %}
|
||||
{% set student = current_student %}
|
||||
|
||||
<div>
|
||||
{% if student.name == frappe.session.user %}
|
||||
<button class="btn btn-default btn-sm btn-schedule-eval ml-2 pull-right">
|
||||
{{ _("Schedule Evaluation") }}
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
<div class="mb-8">
|
||||
{% include "lms/templates/upcoming_evals.html" %}
|
||||
</div>
|
||||
<div class="mb-8">
|
||||
{% include "lms/templates/assessments.html" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro Discussions(batch_info) %}
|
||||
<article class="class-discussion">
|
||||
{% set condition = is_moderator or is_student or is_evaluator %}
|
||||
{% set doctype, docname = _("LMS Batch"), batch_info.name %}
|
||||
{% set single_thread = True %}
|
||||
{% set title = "Discussions" %}
|
||||
{% set cta_title = "Post" %}
|
||||
{% set button_name = _("Start Learning") %}
|
||||
{% set redirect_to = "/batches/" + batch_info.name %}
|
||||
{% set empty_state_title = _("Have a doubt?") %}
|
||||
{% set empty_state_subtitle = _("Post it here, our mentors will help you out.") %}
|
||||
{% include "frappe/templates/discussions/discussions_section.html" %}
|
||||
</article>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro CoursesSection(batch_info, batch_courses) %}
|
||||
<article>
|
||||
<header class="mb-5">
|
||||
<div class="edit-header">
|
||||
<div class="bold-heading">
|
||||
{{ _("Courses") }}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{% if batch_courses | length %}
|
||||
<div class="cards-parent">
|
||||
{% for course in batch_courses %}
|
||||
<div class="h-100">
|
||||
{{ widgets.CourseCard(course=course, read_only=False) }}
|
||||
</div>
|
||||
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="">
|
||||
{{ _("No courses") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</article>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro StudentsSection(batch_info, batch_students) %}
|
||||
<article>
|
||||
<header>
|
||||
<div class="edit-header mb-5">
|
||||
<div class="bold-heading">
|
||||
{{ _("Students") }}
|
||||
</div>
|
||||
{% if is_moderator %}
|
||||
<button class="btn btn-default btn-sm btn-add-student">
|
||||
{{ _("Add Students") }}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{% if batch_students | length %}
|
||||
<div class="form-grid">
|
||||
<div class="grid-heading-row">
|
||||
<div class="grid-row">
|
||||
<div class="data-row row">
|
||||
<div class="col grid-static-col">
|
||||
{{ _("Full Name") }}
|
||||
</div>
|
||||
<div class="col grid-static-col col-xs-2 text-right">
|
||||
{{ _("Courses Completed") }}
|
||||
</div>
|
||||
<div class="col grid-static-col col-xs-2 text-right">
|
||||
{{ _("Assessments Completed") }}
|
||||
</div>
|
||||
<div class="col grid-static-col col-xs-2 text-right">
|
||||
{{ _("Assessments Graded") }}
|
||||
</div>
|
||||
<div class="col grid-static-col">
|
||||
{{ _("Last Active") }}
|
||||
</div>
|
||||
{% if is_moderator %}
|
||||
<div class="col grid-static-col col-xs-1">
|
||||
<svg class="icon icon-sm" style="filter: opacity(0.5)">
|
||||
<use class="" href="#icon-setting-gear"></use>
|
||||
</svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% for student in batch_students %}
|
||||
{% set allow_progress = is_moderator or is_evaluator %}
|
||||
<div class="grid-row">
|
||||
<div class="data-row row">
|
||||
<a class="col grid-static-col button-links {% if allow_progress %} clickable {% endif %}" {% if allow_progress %} href="/batches/{{ batch_info.name }}/students/{{ student.username }}" {% endif %}>
|
||||
{{ student.student_name }}
|
||||
</a>
|
||||
<div class="col grid-static-col col-xs-2 text-right">
|
||||
{{ student.courses_completed }}
|
||||
</div>
|
||||
<div class="col grid-static-col col-xs-2 text-right">
|
||||
{{ student.assessments_completed }}
|
||||
</div>
|
||||
<div class="col grid-static-col col-xs-2 text-right">
|
||||
{{ student.assessments_graded }}
|
||||
</div>
|
||||
<div class="col grid-static-col">
|
||||
{{ frappe.utils.pretty_date(student.last_active) }}
|
||||
</div>
|
||||
{% if is_moderator %}
|
||||
<div type="button" class="col grid-static-col col-xs-1 btn-remove-student" data-student="{{ student.student }}">
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-delete"></use>
|
||||
</svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted mt-3"> {{ _("No Students") }} </p>
|
||||
{% endif %}
|
||||
</article>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro AssessmentsSection(batch_info) %}
|
||||
<article>
|
||||
<header class="edit-header mb-5">
|
||||
<div class="bold-heading">
|
||||
{{ _("Assessments") }}
|
||||
</div>
|
||||
{% if is_moderator %}
|
||||
<button class="btn btn-default btn-sm" id="open-assessment-modal">
|
||||
{{ _("Manage Assessments") }}
|
||||
</button>
|
||||
{% endif %}
|
||||
</header>
|
||||
{{ AssessmentList(assessments) }}
|
||||
</article>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro EmailsSection() %}
|
||||
<div class="my-4">
|
||||
<button class="btn btn-secondary btn-sm btn-email">
|
||||
{{ _("Email to Students") }}
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
{% for email in batch_emails %}
|
||||
<div class="frappe-card mb-5">
|
||||
<div class="flex justify-between m-1">
|
||||
<span class="text-color flex">
|
||||
<span class="margin-right">
|
||||
{% set member = frappe.db.get_value("User", email.sender, ["full_name", "username", "name", "user_image"], as_dict=1) %}
|
||||
{{ widgets.Avatar(member=member, avatar_class="avatar-small") }}
|
||||
</span>
|
||||
<span>
|
||||
{{ member.full_name }}
|
||||
<div class="text-muted">
|
||||
<span class="frappe-timestamp" data-timestamp="{{ email.communication_date }}" title="{{ communication_date }}">
|
||||
{{ frappe.utils.pretty_date(email.communication_date) }}
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="ml-10">
|
||||
{{ email.content }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro AssessmentList(assessments) %}
|
||||
{% if assessments | length %}
|
||||
<div class="form-grid">
|
||||
<div class="grid-heading-row">
|
||||
<div class="grid-row">
|
||||
<div class="row data-row">
|
||||
<div class="col grid-static-col">
|
||||
{{ _("Title") }}
|
||||
</div>
|
||||
<div class="col grid-static-col">
|
||||
{{ _("Type") }}
|
||||
</div>
|
||||
<div class="col grid-static-col col-xs-1">
|
||||
<svg class="icon icon-sm" style="filter: opacity(0.5)">
|
||||
<use href="#icon-setting-gear"></use>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-body">
|
||||
<div class="rows">
|
||||
{% for assessment in assessments %}
|
||||
<div class="grid-row">
|
||||
<div class="row data-row">
|
||||
<a class="col grid-static-col clickable" href="{{ assessment.edit_url }}">
|
||||
{{ assessment.title }}
|
||||
</a>
|
||||
<div class="col grid-static-col">
|
||||
{{ assessment.assessment_type.split("LMS ")[1] }}
|
||||
</div>
|
||||
<div type="button" class="col grid-static-col col-xs-1 btn-remove-assessment" data-assessment="{{ assessment.assessment_name }}">
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-delete"></use>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted mt-3"> {{ _("No Assessments") }} </p>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro LiveClassSection(batch_info, live_classes) %}
|
||||
<article>
|
||||
<header class="edit-header">
|
||||
<div class="bold-heading">
|
||||
{{ _("Live Class") }}
|
||||
</div>
|
||||
{% if is_moderator %}
|
||||
<button class="btn btn-default btn-sm" id="open-class-modal">
|
||||
{{ _("Create a Live Class") }}
|
||||
</button>
|
||||
{% endif %}
|
||||
</header>
|
||||
{{ CreateLiveClass(batch_info) }}
|
||||
{{ LiveClassList(batch_info, live_classes) }}
|
||||
</article>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro CreateLiveClass(batch_info) %}
|
||||
{% if is_moderator %}
|
||||
<div class="modal fade live-class-modal" id="live-class-modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title">{{ _("Live Class Details") }}</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="modal-body">
|
||||
<form class="live-class-form" id="live-class-form"></form>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary btn-sm mr-2" data-dismiss="modal" aria-label="Close">
|
||||
{{ _("Discard") }}
|
||||
</button>
|
||||
|
||||
<button class="btn btn-primary btn-sm" id="create-live-class">
|
||||
{{ _("Submit") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro LiveClassList(batch_info, live_classes) %}
|
||||
<div class="lms-card-parent mt-5">
|
||||
{% if live_classes | length %}
|
||||
{% for class in live_classes %}
|
||||
<div class="common-card-style column-card">
|
||||
|
||||
<div class="mb-0">
|
||||
<div class="dropdown pull-right">
|
||||
<svg class="icon icon-sm dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<use href="#icon-dot-horizontal"></use>
|
||||
</svg>
|
||||
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton">
|
||||
{% if class.owner == frappe.session.user %}
|
||||
<li>
|
||||
<a class="dropdown-item small" href="{{ class.start_url }}"> {{ _("Start") }} </a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_student %}
|
||||
<li>
|
||||
<a class="dropdown-item small" href="{{ class.join_url }}"> {{ _("Join") }} </a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="bold-heading mb-4">
|
||||
{{ class.title }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-calendar"></use>
|
||||
</svg>
|
||||
{{ frappe.utils.format_date(class.date, "full") }}
|
||||
</div>
|
||||
<div>
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-clock"></use>
|
||||
</svg>
|
||||
{{ frappe.utils.format_time(class.time, "hh:mm a") }}
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
{{ class.description }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class=""> {{ _("No Live Classes") }} </p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro Timetable() %}
|
||||
<article>
|
||||
<header class="edit-header mb-5">
|
||||
<div class="bold-heading">
|
||||
{{ _("Timetable") }}
|
||||
</div>
|
||||
</header>
|
||||
<div class="calendar-navigation">
|
||||
<button class="btn icon-btn btn-default" id="prev-week">
|
||||
<svg class="icon icon-md">
|
||||
<use href="#icon-left"></use>
|
||||
</svg>
|
||||
</button>
|
||||
<span class="calendar-range"></span>
|
||||
<button class="btn icon-btn btn-default" id="next-week">
|
||||
<svg class="icon icon-md">
|
||||
<use href="#icon-right"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="calendar-legends">
|
||||
{% for legend in legends %}
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background-color: {{ legend.color }}"></div>
|
||||
<div class="legend-text">{{ legend.label }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div id="calendar" class="timetable-calendar" style="height: 700px"
|
||||
data-start="{{ batch_info.start_time }}" data-end="{{ batch_info.end_time }}">
|
||||
</div>
|
||||
|
||||
</article>
|
||||
{% endmacro %}
|
||||
|
||||
{%- block script %}
|
||||
{{ super() }}
|
||||
{% if batch_info.custom_script %}
|
||||
<script>
|
||||
{{ batch_info.custom_script }}
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
frappe.boot.single_types = []
|
||||
let courses = {{ course_list | json }};
|
||||
const legends = {{ legends | json }};
|
||||
const allow_future = {{ batch_info.allow_future }};
|
||||
const is_student = "{{ is_student or '' }}";
|
||||
const evaluation_end_date = "{{ batch_info.evaluation_end_date if batch_info.evaluation_end_date else '' }}"
|
||||
const show_day_view = {{ settings.show_day_view }};
|
||||
</script>
|
||||
|
||||
<link rel="stylesheet" href="https://uicdn.toast.com/calendar/latest/toastui-calendar.min.css" />
|
||||
<script src="https://uicdn.toast.com/calendar/latest/toastui-calendar.min.js"></script>
|
||||
{% endblock %}
|
||||
@@ -1,926 +0,0 @@
|
||||
frappe.ready(() => {
|
||||
let self = this;
|
||||
frappe.require("controls.bundle.js");
|
||||
|
||||
if ($("#calendar").length) {
|
||||
setup_timetable();
|
||||
}
|
||||
|
||||
if ($("#calendar").length) {
|
||||
$(document).on("click", "#prev-week", (e) => {
|
||||
this.calendar_ && this.calendar_.prev();
|
||||
set_calendar_range(this.calendar_, this.events);
|
||||
});
|
||||
}
|
||||
|
||||
if ($("#calendar").length) {
|
||||
$(document).on("click", "#next-week", (e) => {
|
||||
this.calendar_ && this.calendar_.next();
|
||||
set_calendar_range(this.calendar_, this.events);
|
||||
});
|
||||
}
|
||||
|
||||
if ($("#live-class-form").length) {
|
||||
setTimeout(() => {
|
||||
make_live_class_form();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
$(".btn-add-student").click((e) => {
|
||||
show_student_modal(e);
|
||||
});
|
||||
|
||||
$(".btn-remove-student").click((e) => {
|
||||
remove_student(e);
|
||||
});
|
||||
|
||||
$("#open-class-modal").click((e) => {
|
||||
e.preventDefault();
|
||||
$("#live-class-modal").modal("show");
|
||||
});
|
||||
|
||||
$("#create-live-class").click((e) => {
|
||||
create_live_class(e);
|
||||
});
|
||||
|
||||
$(".btn-remove-assessment").click((e) => {
|
||||
remove_assessment(e);
|
||||
});
|
||||
|
||||
$("#open-assessment-modal").click((e) => {
|
||||
e.preventDefault();
|
||||
show_assessment_modal();
|
||||
});
|
||||
|
||||
$(".btn-close").click((e) => {
|
||||
window.location.reload();
|
||||
});
|
||||
|
||||
$(".btn-schedule-eval").click((e) => {
|
||||
open_evaluation_form(e);
|
||||
});
|
||||
|
||||
$(document).on("click", ".slot", (e) => {
|
||||
mark_active_slot(e);
|
||||
});
|
||||
|
||||
$(".btn-email").click((e) => {
|
||||
email_to_students();
|
||||
});
|
||||
});
|
||||
|
||||
const create_live_class = (e) => {
|
||||
let batch_name = $(".class-details").data("batch");
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_batch.lms_batch.create_live_class",
|
||||
args: {
|
||||
batch_name: batch_name,
|
||||
title: $("input[data-fieldname='meeting_title']").val(),
|
||||
duration: $("input[data-fieldname='meeting_duration']").val(),
|
||||
date: $("input[data-fieldname='meeting_date']").val(),
|
||||
time: $("input[data-fieldname='meeting_time']").val(),
|
||||
timezone: $('select[data-fieldname="meeting_timezone"]').val(),
|
||||
auto_recording: $(
|
||||
'select[data-fieldname="meeting_recording"]'
|
||||
).val(),
|
||||
description: $(
|
||||
"textarea[data-fieldname='meeting_description']"
|
||||
).val(),
|
||||
},
|
||||
callback: (data) => {
|
||||
$("#live-class-modal").modal("hide");
|
||||
frappe.show_alert(
|
||||
{
|
||||
message: __("Live Class added successfully"),
|
||||
indicator: "green",
|
||||
},
|
||||
3
|
||||
);
|
||||
setTimeout(function () {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const make_live_class_form = (e) => {
|
||||
this.field_group = new frappe.ui.FieldGroup({
|
||||
fields: [
|
||||
{
|
||||
fieldname: "meeting_title",
|
||||
fieldtype: "Data",
|
||||
options: "",
|
||||
label: "Title",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "meeting_time",
|
||||
fieldtype: "Time",
|
||||
options: "",
|
||||
label: "Time",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "meeting_timezone",
|
||||
label: __("Time Zone"),
|
||||
fieldtype: "Select",
|
||||
options: get_timezones().join("\n"),
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "meeting_col",
|
||||
fieldtype: "Column Break",
|
||||
options: "",
|
||||
},
|
||||
{
|
||||
fieldname: "meeting_date",
|
||||
fieldtype: "Date",
|
||||
options: "",
|
||||
label: "Date",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "meeting_duration",
|
||||
fieldtype: "Int",
|
||||
options: "",
|
||||
label: "Duration (in Minutes)",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "meeting_recording",
|
||||
fieldtype: "Select",
|
||||
options: "No Recording\nLocal\nCloud",
|
||||
label: "Auto Recording",
|
||||
default: "No Recording",
|
||||
},
|
||||
{
|
||||
fieldname: "meeting_sec",
|
||||
fieldtype: "Section Break",
|
||||
options: "",
|
||||
},
|
||||
{
|
||||
fieldname: "meeting_description",
|
||||
fieldtype: "Small Text",
|
||||
options: "",
|
||||
max_height: 100,
|
||||
min_lines: 5,
|
||||
label: "Description",
|
||||
},
|
||||
],
|
||||
body: $("#live-class-form").get(0),
|
||||
});
|
||||
|
||||
this.field_group.make();
|
||||
$("#live-class-form .form-section:last").removeClass("empty-section");
|
||||
$("#live-class-form .frappe-control").removeClass("hide-control");
|
||||
};
|
||||
|
||||
const get_timezones = () => {
|
||||
return [
|
||||
"Pacific/Midway",
|
||||
"Pacific/Pago_Pago",
|
||||
"Pacific/Honolulu",
|
||||
"America/Anchorage",
|
||||
"America/Vancouver",
|
||||
"America/Los_Angeles",
|
||||
"America/Tijuana",
|
||||
"America/Edmonton",
|
||||
"America/Denver",
|
||||
"America/Phoenix",
|
||||
"America/Mazatlan",
|
||||
"America/Winnipeg",
|
||||
"America/Regina",
|
||||
"America/Chicago",
|
||||
"America/Mexico_City",
|
||||
"America/Guatemala",
|
||||
"America/El_Salvador",
|
||||
"America/Managua",
|
||||
"America/Costa_Rica",
|
||||
"America/Montreal",
|
||||
"America/New_York",
|
||||
"America/Indianapolis",
|
||||
"America/Panama",
|
||||
"America/Bogota",
|
||||
"America/Lima",
|
||||
"America/Halifax",
|
||||
"America/Puerto_Rico",
|
||||
"America/Caracas",
|
||||
"America/Santiago",
|
||||
"America/St_Johns",
|
||||
"America/Montevideo",
|
||||
"America/Araguaina",
|
||||
"America/Argentina/Buenos_Aires",
|
||||
"America/Godthab",
|
||||
"America/Sao_Paulo",
|
||||
"Atlantic/Azores",
|
||||
"Canada/Atlantic",
|
||||
"Atlantic/Cape_Verde",
|
||||
"UTC",
|
||||
"Etc/Greenwich",
|
||||
"Europe/Belgrade",
|
||||
"CET",
|
||||
"Atlantic/Reykjavik",
|
||||
"Europe/Dublin",
|
||||
"Europe/London",
|
||||
"Europe/Lisbon",
|
||||
"Africa/Casablanca",
|
||||
"Africa/Nouakchott",
|
||||
"Europe/Oslo",
|
||||
"Europe/Copenhagen",
|
||||
"Europe/Brussels",
|
||||
"Europe/Berlin",
|
||||
"Europe/Helsinki",
|
||||
"Europe/Amsterdam",
|
||||
"Europe/Rome",
|
||||
"Europe/Stockholm",
|
||||
"Europe/Vienna",
|
||||
"Europe/Luxembourg",
|
||||
"Europe/Paris",
|
||||
"Europe/Zurich",
|
||||
"Europe/Madrid",
|
||||
"Africa/Bangui",
|
||||
"Africa/Algiers",
|
||||
"Africa/Tunis",
|
||||
"Africa/Harare",
|
||||
"Africa/Nairobi",
|
||||
"Europe/Warsaw",
|
||||
"Europe/Prague",
|
||||
"Europe/Budapest",
|
||||
"Europe/Sofia",
|
||||
"Europe/Istanbul",
|
||||
"Europe/Athens",
|
||||
"Europe/Bucharest",
|
||||
"Asia/Nicosia",
|
||||
"Asia/Beirut",
|
||||
"Asia/Damascus",
|
||||
"Asia/Jerusalem",
|
||||
"Asia/Amman",
|
||||
"Africa/Tripoli",
|
||||
"Africa/Cairo",
|
||||
"Africa/Johannesburg",
|
||||
"Europe/Moscow",
|
||||
"Asia/Baghdad",
|
||||
"Asia/Kuwait",
|
||||
"Asia/Riyadh",
|
||||
"Asia/Bahrain",
|
||||
"Asia/Qatar",
|
||||
"Asia/Aden",
|
||||
"Asia/Tehran",
|
||||
"Africa/Khartoum",
|
||||
"Africa/Djibouti",
|
||||
"Africa/Mogadishu",
|
||||
"Asia/Dubai",
|
||||
"Asia/Muscat",
|
||||
"Asia/Baku",
|
||||
"Asia/Kabul",
|
||||
"Asia/Yekaterinburg",
|
||||
"Asia/Tashkent",
|
||||
"Asia/Calcutta",
|
||||
"Asia/Kathmandu",
|
||||
"Asia/Novosibirsk",
|
||||
"Asia/Almaty",
|
||||
"Asia/Dacca",
|
||||
"Asia/Krasnoyarsk",
|
||||
"Asia/Dhaka",
|
||||
"Asia/Bangkok",
|
||||
"Asia/Saigon",
|
||||
"Asia/Jakarta",
|
||||
"Asia/Irkutsk",
|
||||
"Asia/Shanghai",
|
||||
"Asia/Hong_Kong",
|
||||
"Asia/Taipei",
|
||||
"Asia/Kuala_Lumpur",
|
||||
"Asia/Singapore",
|
||||
"Australia/Perth",
|
||||
"Asia/Yakutsk",
|
||||
"Asia/Seoul",
|
||||
"Asia/Tokyo",
|
||||
"Australia/Darwin",
|
||||
"Australia/Adelaide",
|
||||
"Asia/Vladivostok",
|
||||
"Pacific/Port_Moresby",
|
||||
"Australia/Brisbane",
|
||||
"Australia/Sydney",
|
||||
"Australia/Hobart",
|
||||
"Asia/Magadan",
|
||||
"SST",
|
||||
"Pacific/Noumea",
|
||||
"Asia/Kamchatka",
|
||||
"Pacific/Fiji",
|
||||
"Pacific/Auckland",
|
||||
"Asia/Kolkata",
|
||||
"Europe/Kiev",
|
||||
"America/Tegucigalpa",
|
||||
"Pacific/Apia",
|
||||
];
|
||||
};
|
||||
|
||||
const show_student_modal = () => {
|
||||
let student_modal = new frappe.ui.Dialog({
|
||||
title: "Add Student",
|
||||
fields: [
|
||||
{
|
||||
fieldtype: "Link",
|
||||
options: "User",
|
||||
label: __("Student"),
|
||||
fieldname: "student",
|
||||
reqd: 1,
|
||||
only_select: 1,
|
||||
filters: {
|
||||
ignore_user_type: 1,
|
||||
},
|
||||
filter_description: " ",
|
||||
},
|
||||
],
|
||||
primary_action_label: __("Add"),
|
||||
primary_action(values) {
|
||||
add_student(values);
|
||||
student_modal.hide();
|
||||
},
|
||||
});
|
||||
student_modal.show();
|
||||
setTimeout(() => {
|
||||
$(".modal-body").css("min-height", "200px");
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const add_student = (values) => {
|
||||
frappe.call({
|
||||
method: "frappe.client.insert",
|
||||
args: {
|
||||
doc: {
|
||||
doctype: "Batch Student",
|
||||
student: values.student,
|
||||
parenttype: "LMS Batch",
|
||||
parentfield: "students",
|
||||
parent: $(".class-details").data("batch"),
|
||||
},
|
||||
},
|
||||
callback(r) {
|
||||
frappe.show_alert(
|
||||
{
|
||||
message: __("Student Added"),
|
||||
indicator: "green",
|
||||
},
|
||||
2000
|
||||
);
|
||||
window.location.reload();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const remove_student = (e) => {
|
||||
frappe.confirm(
|
||||
"Are you sure you want to remove this student from the batch?",
|
||||
() => {
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_batch.lms_batch.remove_student",
|
||||
args: {
|
||||
student: $(e.currentTarget).data("student"),
|
||||
batch_name: $(".class-details").data("batch"),
|
||||
},
|
||||
callback: (data) => {
|
||||
frappe.show_alert(
|
||||
{
|
||||
message: __("Student removed successfully"),
|
||||
indicator: "green",
|
||||
},
|
||||
2000
|
||||
);
|
||||
window.location.reload();
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const show_assessment_modal = (e) => {
|
||||
let assessment_modal = new frappe.ui.Dialog({
|
||||
title: "Manage Assessments",
|
||||
fields: [
|
||||
{
|
||||
fieldtype: "Link",
|
||||
options: "DocType",
|
||||
label: __("Assessment Type"),
|
||||
fieldname: "assessment_type",
|
||||
reqd: 1,
|
||||
only_select: 1,
|
||||
filters: {
|
||||
name: ["in", ["LMS Assignment", "LMS Quiz"]],
|
||||
},
|
||||
filter_description: " ",
|
||||
},
|
||||
{
|
||||
fieldtype: "Dynamic Link",
|
||||
options: "assessment_type",
|
||||
label: __("Assessment"),
|
||||
fieldname: "assessment_name",
|
||||
reqd: 1,
|
||||
only_select: 1,
|
||||
},
|
||||
{
|
||||
fieldtype: "Section Break",
|
||||
label: __("OR"),
|
||||
},
|
||||
{
|
||||
fieldtype: "Button",
|
||||
label: __("Create Assignment"),
|
||||
fieldname: "create_assignment",
|
||||
click: () => {
|
||||
window.location.href = "/assignments";
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldtype: "Column Break",
|
||||
},
|
||||
{
|
||||
fieldtype: "Button",
|
||||
label: __("Create Quiz"),
|
||||
fieldname: "create_quiz",
|
||||
click: () => {
|
||||
window.location.href = "/quizzes";
|
||||
},
|
||||
},
|
||||
],
|
||||
primary_action_label: __("Add"),
|
||||
primary_action(values) {
|
||||
add_addessment(values);
|
||||
assessment_modal.hide();
|
||||
},
|
||||
});
|
||||
assessment_modal.show();
|
||||
setTimeout(() => {
|
||||
$(".modal-body").css("min-height", "300px");
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const add_addessment = (values) => {
|
||||
frappe.call({
|
||||
method: "frappe.client.insert",
|
||||
args: {
|
||||
doc: {
|
||||
doctype: "LMS Assessment",
|
||||
assessment_type: values.assessment_type,
|
||||
assessment_name: values.assessment_name,
|
||||
parenttype: "LMS Batch",
|
||||
parentfield: "assessment",
|
||||
parent: $(".class-details").data("batch"),
|
||||
},
|
||||
},
|
||||
callback(r) {
|
||||
frappe.show_alert(
|
||||
{
|
||||
message: __("Assessment Added"),
|
||||
indicator: "green",
|
||||
},
|
||||
2000
|
||||
);
|
||||
window.location.reload();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const remove_assessment = (e) => {
|
||||
frappe.confirm("Are you sure you want to remove this assessment?", () => {
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_batch.lms_batch.remove_assessment",
|
||||
args: {
|
||||
assessment: $(e.currentTarget).data("assessment"),
|
||||
parent: $(".class-details").data("batch"),
|
||||
},
|
||||
callback(r) {
|
||||
frappe.show_alert(
|
||||
{
|
||||
message: __("Assessment Removed"),
|
||||
indicator: "green",
|
||||
},
|
||||
2000
|
||||
);
|
||||
window.location.reload();
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const open_evaluation_form = (e) => {
|
||||
this.eval_form = new frappe.ui.Dialog({
|
||||
title: __("Schedule Evaluation"),
|
||||
fields: [
|
||||
{
|
||||
fieldtype: "Link",
|
||||
fieldname: "course",
|
||||
label: __("Course"),
|
||||
options: "LMS Course",
|
||||
reqd: 1,
|
||||
filters: {
|
||||
name: ["in", courses],
|
||||
},
|
||||
filter_description: " ",
|
||||
only_select: 1,
|
||||
change: () => {
|
||||
this.eval_form.set_value("date", "");
|
||||
$("[data-fieldname='slots']").html("");
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldtype: "Date",
|
||||
fieldname: "date",
|
||||
label: __("Date"),
|
||||
reqd: 1,
|
||||
min_date: new Date(
|
||||
frappe.datetime.add_days(frappe.datetime.get_today(), 1)
|
||||
),
|
||||
max_date: evaluation_end_date
|
||||
? new Date(evaluation_end_date)
|
||||
: "",
|
||||
change: () => {
|
||||
if (this.eval_form.get_value("date")) get_slots();
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldtype: "HTML",
|
||||
fieldname: "slots",
|
||||
label: __("Slots"),
|
||||
},
|
||||
],
|
||||
primary_action: (values) => {
|
||||
submit_evaluation_form(values);
|
||||
},
|
||||
});
|
||||
this.eval_form.show();
|
||||
setTimeout(() => {
|
||||
$(".modal-body").css("min-height", "300px");
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const get_slots = () => {
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.course_evaluator.course_evaluator.get_schedule",
|
||||
args: {
|
||||
course: this.eval_form.get_value("course"),
|
||||
date: this.eval_form.get_value("date"),
|
||||
batch: $(".class-details").data("batch"),
|
||||
},
|
||||
callback: (r) => {
|
||||
if (r.message) {
|
||||
display_slots(r.message);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const display_slots = (slots) => {
|
||||
let slot_html = "";
|
||||
let slots_available = false;
|
||||
if (slots.length) {
|
||||
slot_html = `<div>
|
||||
<div class="mb-2"> ${__("Select a Slot")} </div>
|
||||
<div class="slots-parent">`;
|
||||
let day = moment(this.eval_form.get_value("date")).format("dddd");
|
||||
|
||||
slots.forEach((slot) => {
|
||||
if (slot.day == day) {
|
||||
slots_available = true;
|
||||
slot_html += `<div class="btn btn-sm btn-default slot" data-day="${
|
||||
slot.day
|
||||
}"
|
||||
data-start="${slot.start_time}" data-end="${slot.end_time}">
|
||||
${moment(slot.start_time, "hh:mm").format("hh:mm a")} -
|
||||
${moment(slot.end_time, "hh:mm").format("hh:mm a")}
|
||||
</div>`;
|
||||
}
|
||||
});
|
||||
slot_html += "</div> </div>";
|
||||
}
|
||||
|
||||
if (!slots_available) {
|
||||
slot_html = `<div class="alert alert-danger" role="alert">
|
||||
No slots available for this date.
|
||||
</div>`;
|
||||
}
|
||||
|
||||
$("[data-fieldname='slots']").html(slot_html);
|
||||
};
|
||||
|
||||
const mark_active_slot = (e) => {
|
||||
$(".slot").removeClass("btn-outline-primary");
|
||||
$(e.currentTarget).addClass("btn-outline-primary");
|
||||
this.current_slot = $(e.currentTarget);
|
||||
};
|
||||
|
||||
const submit_evaluation_form = (values) => {
|
||||
if (!this.current_slot) {
|
||||
frappe.throw(__("Please select a slot"));
|
||||
}
|
||||
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_certificate_request.lms_certificate_request.create_certificate_request",
|
||||
args: {
|
||||
course: values.course,
|
||||
date: values.date,
|
||||
start_time: this.current_slot.data("start"),
|
||||
end_time: this.current_slot.data("end"),
|
||||
day: this.current_slot.data("day"),
|
||||
batch_name: $(".class-details").data("batch"),
|
||||
},
|
||||
callback: (r) => {
|
||||
this.eval_form.hide();
|
||||
frappe.show_alert({
|
||||
message: __("Evaluation scheduled successfully"),
|
||||
indicator: "green",
|
||||
});
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const setup_timetable = () => {
|
||||
let self = this;
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_batch.lms_batch.get_batch_timetable",
|
||||
args: {
|
||||
batch: $(".class-details").data("batch"),
|
||||
},
|
||||
callback: (r) => {
|
||||
if (r.message.length) {
|
||||
setup_calendar(r.message);
|
||||
self.events = r.message;
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const setup_calendar = (events) => {
|
||||
const element = $("#calendar");
|
||||
const Calendar = tui.Calendar;
|
||||
const calendar_id = "calendar1";
|
||||
const container = element[0];
|
||||
const options = get_calendar_options(element, calendar_id);
|
||||
const calendar = new Calendar(container, options);
|
||||
this.calendar_ = calendar;
|
||||
|
||||
create_events(calendar, events, calendar_id);
|
||||
add_links_to_events(calendar, events);
|
||||
scroll_to_date(calendar, events);
|
||||
set_calendar_range(calendar, events);
|
||||
};
|
||||
|
||||
const get_calendar_options = (element, calendar_id) => {
|
||||
const start_time = element.data("start");
|
||||
const end_time = element.data("end");
|
||||
|
||||
return {
|
||||
defaultView: $(window).width() < 768 || show_day_view ? "day" : "week",
|
||||
usageStatistics: false,
|
||||
week: {
|
||||
narrowWeekend: true,
|
||||
hourStart: parseInt(start_time.split(":")[0]) - 1,
|
||||
/* hourEnd: parseInt(end_time.split(":")[0]) + 1, */
|
||||
},
|
||||
month: {
|
||||
narrowWeekend: true,
|
||||
},
|
||||
taskView: false,
|
||||
isReadOnly: true,
|
||||
calendars: [
|
||||
{
|
||||
id: calendar_id,
|
||||
name: "Timetable",
|
||||
backgroundColor: "var(--fg-color)",
|
||||
},
|
||||
],
|
||||
template: {
|
||||
allday: function (event) {
|
||||
let hide = event.raw.completed ? "" : "hide";
|
||||
return `<div class="calendar-event-time" title="${
|
||||
event.title
|
||||
} - ${frappe.datetime.get_time(
|
||||
event.start.d.d
|
||||
)} - ${frappe.datetime.get_time(event.end.d.d)}">
|
||||
<img class='icon icon-sm pull-right ${hide}' src="/assets/lms/icons/check.svg">
|
||||
<div class="calendar-event-title"> ${event.title} </div>
|
||||
</div>`;
|
||||
},
|
||||
time: function (event) {
|
||||
let hide = event.raw.completed ? "" : "hide";
|
||||
return `<div class="calendar-event-time" title="${
|
||||
event.title
|
||||
} - ${frappe.datetime.get_time(
|
||||
event.start.d.d
|
||||
)} - ${frappe.datetime.get_time(event.end.d.d)}">
|
||||
<img class='icon icon-sm pull-right ${hide}' src="/assets/lms/icons/check.svg">
|
||||
<div> ${frappe.datetime.get_time(event.start.d.d)} -
|
||||
${frappe.datetime.get_time(event.end.d.d)} </div>
|
||||
<div class="calendar-event-title"> ${event.title} </div>
|
||||
</div>`;
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const create_events = (calendar, events, calendar_id) => {
|
||||
let calendar_events = [];
|
||||
events.forEach((event, idx) => {
|
||||
let clr = get_background_color(event.reference_doctype);
|
||||
calendar_events.push({
|
||||
id: `event${idx}`,
|
||||
calendarId: calendar_id,
|
||||
title: event.title,
|
||||
start: `${event.date}T${format_time(event.start_time)}`,
|
||||
end: `${event.date}T${format_time(event.end_time)}`,
|
||||
isAllday: event.start_time ? false : true,
|
||||
category: event.start_time ? "time" : "allday",
|
||||
borderColor: clr,
|
||||
backgroundColor: "var(--fg-color)",
|
||||
customStyle: {
|
||||
borderRadius: "var(--border-radius-md)",
|
||||
boxShadow: "var(--shadow-base)",
|
||||
borderWidth: "8px",
|
||||
padding: "0.25rem 0.5rem 0.5rem",
|
||||
},
|
||||
raw: {
|
||||
url: event.url,
|
||||
milestone: event.milestone,
|
||||
name: event.name,
|
||||
idx: event.idx,
|
||||
parent: event.parent,
|
||||
completed: event.completed,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
calendar.createEvents(calendar_events);
|
||||
};
|
||||
|
||||
const format_time = (time) => {
|
||||
if (!time) return "00:00:00";
|
||||
let time_arr = time.split(":");
|
||||
if (time_arr[0] < 10) time_arr[0] = "0" + time_arr[0];
|
||||
return time_arr.join(":");
|
||||
};
|
||||
|
||||
const add_links_to_events = (calendar) => {
|
||||
calendar.on("clickEvent", ({ event }) => {
|
||||
let event_date = event.start.d.d;
|
||||
event_date = moment(event_date).format("YYYY-MM-DD");
|
||||
let current_date = moment().format("YYYY-MM-DD");
|
||||
|
||||
if (
|
||||
is_student &&
|
||||
!moment(event_date).isSameOrBefore(current_date) &&
|
||||
!allow_future
|
||||
)
|
||||
return;
|
||||
|
||||
if (is_student && event.raw.milestone) {
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_batch.lms_batch.is_milestone_complete",
|
||||
args: {
|
||||
idx: event.raw.idx,
|
||||
batch: event.raw.parent,
|
||||
},
|
||||
callback: (data) => {
|
||||
if (data.message) window.open(event.raw.url, "_blank");
|
||||
else
|
||||
frappe.show_alert({
|
||||
message:
|
||||
"Please complete all previous activities to proceed.",
|
||||
indicator: "red",
|
||||
});
|
||||
},
|
||||
});
|
||||
} else window.open(event.raw.url, "_blank");
|
||||
});
|
||||
};
|
||||
|
||||
const scroll_to_date = (calendar, events) => {
|
||||
if (
|
||||
new Date() < new Date(events[0].date) ||
|
||||
new Date() > new Date(events.slice(-1)[0].date)
|
||||
) {
|
||||
calendar.setDate(new Date(events[0].date));
|
||||
}
|
||||
};
|
||||
|
||||
const set_calendar_range = (calendar, events) => {
|
||||
let day_view = $(window).width() < 768 || show_day_view ? true : false;
|
||||
if (day_view) {
|
||||
let calendar_date = moment(calendar.getDate().d.d).format(
|
||||
"DD MMMM YYYY"
|
||||
);
|
||||
$(".calendar-range").text(`${calendar_date}`);
|
||||
|
||||
if (moment(calendar_date).isSameOrBefore(moment(events[0].date)))
|
||||
$("#prev-week").hide();
|
||||
else $("#prev-week").show();
|
||||
|
||||
if (
|
||||
moment(calendar_date).isSameOrAfter(
|
||||
moment(events.slice(-1)[0].date)
|
||||
)
|
||||
)
|
||||
$("#next-week").hide();
|
||||
else $("#next-week").show();
|
||||
} else {
|
||||
let week_start = moment(calendar.getDateRangeStart().d.d);
|
||||
let week_end = moment(calendar.getDateRangeEnd().d.d);
|
||||
|
||||
$(".calendar-range").text(
|
||||
`${moment(week_start).format("DD MMMM YYYY")} - ${moment(
|
||||
week_end
|
||||
).format("DD MMMM YYYY")}`
|
||||
);
|
||||
|
||||
if (week_start.diff(moment(events[0].date), "days") <= 0)
|
||||
$("#prev-week").hide();
|
||||
else $("#prev-week").show();
|
||||
|
||||
if (week_end.diff(moment(events.slice(-1)[0].date), "days") > 0)
|
||||
$("#next-week").hide();
|
||||
else $("#next-week").show();
|
||||
}
|
||||
};
|
||||
|
||||
const get_background_color = (doctype) => {
|
||||
const match = legends.filter((legend) => {
|
||||
return legend.reference_doctype == doctype;
|
||||
});
|
||||
if (match.length) return match[0].color;
|
||||
};
|
||||
|
||||
const email_to_students = () => {
|
||||
this.email_dialog = new frappe.ui.Dialog({
|
||||
title: __("Email to Students"),
|
||||
fields: [
|
||||
{
|
||||
fieldtype: "Data",
|
||||
fieldname: "subject",
|
||||
label: __("Subject"),
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldtype: "Data",
|
||||
fieldname: "reply_to",
|
||||
label: __("Reply To"),
|
||||
reqd: 0,
|
||||
},
|
||||
{
|
||||
fieldtype: "Text Editor",
|
||||
fieldname: "message",
|
||||
label: __("Message"),
|
||||
reqd: 1,
|
||||
max_height: 100,
|
||||
min_lines: 5,
|
||||
},
|
||||
],
|
||||
primary_action: (values) => {
|
||||
send_email(values);
|
||||
},
|
||||
});
|
||||
this.email_dialog.show();
|
||||
};
|
||||
|
||||
const send_email = (values) => {
|
||||
frappe.call({
|
||||
method: "frappe.client.get_list",
|
||||
args: {
|
||||
doctype: "Batch Student",
|
||||
parent: "LMS Batch",
|
||||
fields: ["student"],
|
||||
filters: {
|
||||
parent: $(".class-details").data("batch"),
|
||||
},
|
||||
},
|
||||
callback: (data) => {
|
||||
send_email_to_students(data.message, values);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const send_email_to_students = (students, values) => {
|
||||
students = students.map((row) => row.student);
|
||||
frappe.call({
|
||||
method: "frappe.core.doctype.communication.email.make",
|
||||
args: {
|
||||
recipients: students.join(", "),
|
||||
cc: values.reply_to,
|
||||
subject: values.subject,
|
||||
content: values.message,
|
||||
doctype: "LMS Batch",
|
||||
name: $(".class-details").data("batch"),
|
||||
send_email: 1,
|
||||
},
|
||||
callback: (r) => {
|
||||
this.email_dialog.hide();
|
||||
frappe.show_alert({
|
||||
message: __("Email sent successfully"),
|
||||
indicator: "green",
|
||||
});
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,279 +0,0 @@
|
||||
from frappe import _
|
||||
import frappe
|
||||
from frappe.utils import getdate
|
||||
from lms.www.utils import is_student
|
||||
from lms.lms.utils import (
|
||||
has_course_moderator_role,
|
||||
has_course_evaluator_role,
|
||||
get_upcoming_evals,
|
||||
has_submitted_assessment,
|
||||
has_graded_assessment,
|
||||
get_lesson_index,
|
||||
get_lesson_url,
|
||||
get_lesson_icon,
|
||||
get_membership,
|
||||
get_assessments,
|
||||
)
|
||||
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
batch_name = frappe.form_dict["batchname"]
|
||||
context.is_moderator = has_course_moderator_role()
|
||||
context.is_evaluator = has_course_evaluator_role()
|
||||
|
||||
context.batch_info = frappe.db.get_value(
|
||||
"LMS Batch",
|
||||
batch_name,
|
||||
[
|
||||
"name",
|
||||
"title",
|
||||
"start_date",
|
||||
"end_date",
|
||||
"description",
|
||||
"medium",
|
||||
"custom_component",
|
||||
"custom_script",
|
||||
"seat_count",
|
||||
"start_time",
|
||||
"end_time",
|
||||
"category",
|
||||
"paid_batch",
|
||||
"amount",
|
||||
"currency",
|
||||
"batch_details",
|
||||
"published",
|
||||
"allow_future",
|
||||
"evaluation_end_date",
|
||||
"meta_image",
|
||||
],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
context.reference_doctype = "LMS Batch"
|
||||
context.reference_name = batch_name
|
||||
|
||||
batch_courses = frappe.get_all(
|
||||
"Batch Course",
|
||||
{"parent": batch_name},
|
||||
["name", "course", "title"],
|
||||
order_by="idx",
|
||||
)
|
||||
|
||||
batch_students = frappe.get_all(
|
||||
"Batch Student",
|
||||
{"parent": batch_name},
|
||||
["name", "student", "student_name", "username"],
|
||||
order_by="idx",
|
||||
)
|
||||
|
||||
context.batch_courses = get_class_course_details(batch_courses)
|
||||
context.course_list = [course.course for course in context.batch_courses]
|
||||
context.all_courses = frappe.get_all(
|
||||
"LMS Course", fields=["name", "title"], limit_page_length=0
|
||||
)
|
||||
context.course_name_list = [course.course for course in context.batch_courses]
|
||||
context.assessments = get_assessments(batch_name)
|
||||
context.batch_emails = frappe.get_all(
|
||||
"Communication",
|
||||
filters={"reference_doctype": "LMS Batch", "reference_name": batch_name},
|
||||
fields=["subject", "content", "recipients", "cc", "communication_date", "sender"],
|
||||
order_by="communication_date desc",
|
||||
)
|
||||
|
||||
context.batch_students = get_class_student_details(
|
||||
batch_students, batch_courses, context.assessments
|
||||
)
|
||||
context.is_student = is_student(batch_name)
|
||||
|
||||
if not context.is_student and not context.is_moderator and not context.is_evaluator:
|
||||
raise frappe.PermissionError(_("You don't have permission to access this page."))
|
||||
|
||||
context.live_classes = frappe.get_all(
|
||||
"LMS Live Class",
|
||||
{"batch_name": batch_name, "date": [">=", getdate()]},
|
||||
["title", "description", "time", "date", "start_url", "join_url", "owner"],
|
||||
order_by="date",
|
||||
)
|
||||
|
||||
context.current_student = (
|
||||
get_current_student_details(batch_courses, batch_name) if context.is_student else None
|
||||
)
|
||||
context.all_assignments = get_all_assignments(batch_name)
|
||||
context.all_quizzes = get_all_quizzes(batch_name)
|
||||
context.show_timetable = frappe.db.count(
|
||||
"LMS Batch Timetable",
|
||||
{
|
||||
"parent": batch_name,
|
||||
},
|
||||
)
|
||||
context.legends = get_legends(batch_name)
|
||||
context.settings = frappe.get_single("LMS Settings")
|
||||
|
||||
custom_tabs = frappe.get_hooks("lms_batch_tabs")
|
||||
if custom_tabs:
|
||||
context.custom_tabs_header = custom_tabs.get("header_html")[0]
|
||||
context.custom_tabs_content = custom_tabs.get("content_html")[0]
|
||||
context.update(frappe.get_attr(custom_tabs.get("context")[0])())
|
||||
|
||||
|
||||
def get_all_quizzes(batch_name):
|
||||
filters = {} if has_course_moderator_role() else {"owner": frappe.session.user}
|
||||
all_quizzes = frappe.get_all("LMS Quiz", filters, ["name", "title"])
|
||||
for quiz in all_quizzes:
|
||||
quiz.checked = frappe.db.exists(
|
||||
{
|
||||
"doctype": "LMS Assessment",
|
||||
"assessment_type": "LMS Quiz",
|
||||
"assessment_name": quiz.name,
|
||||
"parent": batch_name,
|
||||
}
|
||||
)
|
||||
return all_quizzes
|
||||
|
||||
|
||||
def get_all_assignments(batch_name):
|
||||
filters = {} if has_course_moderator_role() else {"owner": frappe.session.user}
|
||||
all_assignments = frappe.get_all("LMS Assignment", filters, ["name", "title"])
|
||||
for assignment in all_assignments:
|
||||
assignment.checked = frappe.db.exists(
|
||||
{
|
||||
"doctype": "LMS Assessment",
|
||||
"assessment_type": "LMS Assignment",
|
||||
"assessment_name": assignment.name,
|
||||
"parent": batch_name,
|
||||
}
|
||||
)
|
||||
return all_assignments
|
||||
|
||||
|
||||
def get_class_course_details(batch_courses):
|
||||
for course in batch_courses:
|
||||
details = frappe.db.get_value(
|
||||
"LMS Course",
|
||||
course.course,
|
||||
[
|
||||
"name",
|
||||
"title",
|
||||
"image",
|
||||
"upcoming",
|
||||
"short_introduction",
|
||||
"paid_course",
|
||||
"course_price",
|
||||
"enable_certification",
|
||||
"currency",
|
||||
],
|
||||
as_dict=True,
|
||||
)
|
||||
course.update(details)
|
||||
return batch_courses
|
||||
|
||||
|
||||
def get_class_student_details(batch_students, batch_courses, assessments):
|
||||
for student in batch_students:
|
||||
student.update(
|
||||
frappe.db.get_value(
|
||||
"User", student.student, ["name", "full_name", "username", "headline"], as_dict=1
|
||||
)
|
||||
)
|
||||
student.update(frappe.db.get_value("User", student.student, "last_active", as_dict=1))
|
||||
get_progress_info(student, batch_courses)
|
||||
get_assessment_info(student, assessments)
|
||||
|
||||
return sort_students(batch_students)
|
||||
|
||||
|
||||
def get_progress_info(student, batch_courses):
|
||||
courses_completed = 0
|
||||
student["courses"] = frappe._dict()
|
||||
for course in batch_courses:
|
||||
membership = get_membership(course.course, student.student)
|
||||
if membership and membership.progress == 100:
|
||||
courses_completed += 1
|
||||
|
||||
student["courses_completed"] = courses_completed
|
||||
return student
|
||||
|
||||
|
||||
def get_assessment_info(student, assessments):
|
||||
assessments_completed = 0
|
||||
assessments_graded = 0
|
||||
for assessment in assessments:
|
||||
submission = has_submitted_assessment(
|
||||
assessment.assessment_name, assessment.assessment_type, student.student
|
||||
)
|
||||
if submission:
|
||||
assessments_completed += 1
|
||||
|
||||
if (
|
||||
assessment.assessment_type == "LMS Assignment" and has_graded_assessment(submission)
|
||||
):
|
||||
assessments_graded += 1
|
||||
elif assessment.assessment_type == "LMS Quiz":
|
||||
assessments_graded += 1
|
||||
|
||||
student["assessments_completed"] = assessments_completed
|
||||
student["assessments_graded"] = assessments_graded
|
||||
|
||||
return student
|
||||
|
||||
|
||||
def sort_students(batch_students):
|
||||
session_user = []
|
||||
remaining_students = []
|
||||
|
||||
for student in batch_students:
|
||||
if student.student == frappe.session.user:
|
||||
session_user.append(student)
|
||||
else:
|
||||
remaining_students.append(student)
|
||||
|
||||
if len(session_user):
|
||||
return session_user + remaining_students
|
||||
else:
|
||||
return batch_students
|
||||
|
||||
|
||||
def get_lesson_details(lesson, batch_name):
|
||||
lesson.update(
|
||||
frappe.db.get_value(
|
||||
"Course Lesson",
|
||||
lesson.lesson,
|
||||
["name", "title", "body", "course", "chapter"],
|
||||
as_dict=True,
|
||||
)
|
||||
)
|
||||
lesson.index = get_lesson_index(lesson.lesson)
|
||||
lesson.url = get_lesson_url(lesson.course, lesson.index) + "?class=" + batch_name
|
||||
lesson.icon = get_lesson_icon(lesson.body)
|
||||
return lesson
|
||||
|
||||
|
||||
def get_current_student_details(batch_courses, batch_name):
|
||||
student_details = frappe._dict()
|
||||
student_details.courses = frappe._dict()
|
||||
course_list = [course.course for course in batch_courses]
|
||||
|
||||
get_course_progress(batch_courses, student_details)
|
||||
student_details.name = frappe.session.user
|
||||
student_details.assessments = get_assessments(batch_name, frappe.session.user)
|
||||
student_details.upcoming_evals = get_upcoming_evals(frappe.session.user, course_list)
|
||||
|
||||
return student_details
|
||||
|
||||
|
||||
def get_course_progress(batch_courses, student_details):
|
||||
for course in batch_courses:
|
||||
membership = get_membership(course.course, frappe.session.user)
|
||||
if membership:
|
||||
student_details.courses[course.course] = membership.progress
|
||||
else:
|
||||
student_details.courses[course.course] = 0
|
||||
|
||||
|
||||
def get_legends(batch):
|
||||
return frappe.get_all(
|
||||
"LMS Timetable Legend",
|
||||
filters={"parenttype": "LMS Batch", "parent": batch},
|
||||
fields=["reference_doctype", "color", "label"],
|
||||
)
|
||||
@@ -1,246 +0,0 @@
|
||||
{% extends "lms/templates/lms_base.html" %}
|
||||
{% block title %}
|
||||
{{ _(batch_info.title) }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block page_content %}
|
||||
<div class="common-page-style lms-page-style">
|
||||
{{ BatchHeader(batch_info) }}
|
||||
<div class="container">
|
||||
{{ BatchOverlay(batch_info, courses, students) }}
|
||||
<div class="pt-10">
|
||||
{{ BatchDetails(batch_info) }}
|
||||
{{ CourseList(courses) }}
|
||||
</div>
|
||||
</div>
|
||||
{{ BatchDetailsRaw() }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% macro BatchHeader(batch_info) %}
|
||||
<div class="course-head-container">
|
||||
<div class="container">
|
||||
<div class="course-card-wide">
|
||||
{{ BreadCrumb(batch_info) }}
|
||||
{{ BatchHeaderDetails(batch_info, courses, students) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro BreadCrumb(batch_info) %}
|
||||
<article class="mb-8">
|
||||
<a class="dark-links" href="/batches">
|
||||
{{ _("All Batches") }}
|
||||
</a>
|
||||
<img class="" src="/assets/lms/icons/chevron-right.svg">
|
||||
<span class="breadcrumb-destination">
|
||||
{{ _("Batch Details") }}
|
||||
</span>
|
||||
</article>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro BatchHeaderDetails(batch_info, courses, students) %}
|
||||
<div class="class-details" data-batch="{{ batch_info.name }}">
|
||||
|
||||
<div class="page-title">
|
||||
{{ batch_info.title }}
|
||||
</div>
|
||||
|
||||
<div class="">
|
||||
{{ batch_info.description }}
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-calendar"></use>
|
||||
</svg>
|
||||
<span>
|
||||
{{ frappe.utils.format_date(batch_info.start_date, "long") }}
|
||||
</span>
|
||||
{% if batch_info.start_date != batch_info.end_date %}
|
||||
<span>
|
||||
- {{ frappe.utils.format_date(batch_info.end_date, "long") }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if batch_info.start_time and batch_info.end_time %}
|
||||
<div class="mt-1">
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-clock"></use>
|
||||
</svg>
|
||||
<span>
|
||||
{{ frappe.utils.format_time(batch_info.start_time, "hh:mm a") }} -
|
||||
</span>
|
||||
<span>
|
||||
{{ frappe.utils.format_time(batch_info.end_time, "hh:mm a") }}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro BatchOverlay(batch_info, courses, students) %}
|
||||
<div class="course-overlay-card class-overlay">
|
||||
|
||||
<div class="course-overlay-content">
|
||||
|
||||
{% if batch_info.seat_count %}
|
||||
{% if seats_left %}
|
||||
<div class="indicator-pill green pull-right">
|
||||
{{ _("Seats Available") }}: {{ seats_left }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="indicator-pill red pull-right">
|
||||
{{ _("No seats left") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if batch_info.paid_batch %}
|
||||
<div class="bold-heading">
|
||||
{{ frappe.utils.fmt_money(batch_info.amount, 0, batch_info.currency) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="vertically-center mt-2">
|
||||
<svg class="icon icon-md mr-1">
|
||||
<use href="#icon-education"></use>
|
||||
</svg>
|
||||
{{ courses | length }} {{ _("Courses") }}
|
||||
</div>
|
||||
|
||||
<div class="mt-2">
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-calendar"></use>
|
||||
</svg>
|
||||
<span>
|
||||
{{ frappe.utils.format_date(batch_info.start_date, "long") }}
|
||||
</span>
|
||||
{% if batch_info.start_date != batch_info.end_date %}
|
||||
<span>
|
||||
- {{ frappe.utils.format_date(batch_info.end_date, "long") }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if batch_info.start_time and batch_info.end_time %}
|
||||
<div class="mt-2">
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-clock"></use>
|
||||
</svg>
|
||||
<span>
|
||||
{{ frappe.utils.format_time(batch_info.start_time, "hh:mm a") }} -
|
||||
</span>
|
||||
<span>
|
||||
{{ frappe.utils.format_time(batch_info.end_time, "hh:mm a") }}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-2">
|
||||
{% if is_moderator or is_evaluator %}
|
||||
<a class="btn btn-primary wide-button" href="/batches/{{ batch_info.name }}">
|
||||
{{ _("Manage Batch") }}
|
||||
</a>
|
||||
{% elif batch_info.paid_batch %}
|
||||
<a class="btn btn-primary wide-button {% if batch_info.seat_count and not seats_left %} hide {% endif %}"
|
||||
href="/billing/batch/{{ batch_info.name }}">
|
||||
{{ _("Register Now") }}
|
||||
</a>
|
||||
{% elif batch_info.allow_self_enrollment and batch_info.seat_count and seats_left %}
|
||||
<button class="btn btn-primary wide-button enroll-batch">
|
||||
{{ _("Enroll Now") }}
|
||||
</button>
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
{{ _("To join this batch, please contact the Administrator.") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if is_moderator %}
|
||||
<div class="mt-2">
|
||||
<div class="btn btn-secondary wide-button" id="create-batch">
|
||||
{{ _("Edit") }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro BatchDetails(batch_info) %}
|
||||
<div class="batch-details">
|
||||
{{ batch_info.batch_details }}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro CourseList(courses) %}
|
||||
{% if courses | length or is_moderator %}
|
||||
<div class="batch-course-list">
|
||||
|
||||
<div class="flex align-center">
|
||||
<div class="page-title">
|
||||
{{ _("Courses") }}
|
||||
</div>
|
||||
{% if is_moderator %}
|
||||
<button class="btn btn-default btn-sm btn-add-course ml-4">
|
||||
{{ _("Add Course") }}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if courses | length %}
|
||||
<div class="cards-parent mt-2">
|
||||
{% for course in courses %}
|
||||
<div class="h-100">
|
||||
{% if is_moderator %}
|
||||
<div class="card-buttons">
|
||||
<button class="btn icon-btn btn-default btn-edit-course"
|
||||
data-name="{{ course.batch_course }}" data-course="{{ course.name }}"
|
||||
{% if course.evaluator %} data-evaluator="{{ course.evaluator }}" {% endif %}>
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-edit"></use>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn icon-btn btn-default btn-remove-course ml-2" data-course="{{ course.name }}">
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-delete"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{{ widgets.CourseCard(course=course, read_only=False) }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="">
|
||||
{{ _("No courses") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro BatchDetailsRaw() %}
|
||||
{% if batch_info.batch_details_raw %}
|
||||
<div class="mt-10 pt-10">
|
||||
{{ batch_info.batch_details_raw }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{%- block script %}
|
||||
{{ super() }}
|
||||
{% if is_moderator %}
|
||||
<script>
|
||||
let batch_info = {{ batch_info | json }};
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -1,129 +0,0 @@
|
||||
frappe.ready(() => {
|
||||
frappe.require("controls.bundle.js");
|
||||
|
||||
$(".btn-add-course").click((e) => {
|
||||
show_course_modal(e);
|
||||
});
|
||||
|
||||
$(".btn-edit-course").click((e) => {
|
||||
show_course_modal(e);
|
||||
});
|
||||
|
||||
$(".btn-remove-course").click((e) => {
|
||||
remove_course(e);
|
||||
});
|
||||
|
||||
$(".enroll-batch").click((e) => {
|
||||
enroll_batch(e);
|
||||
});
|
||||
});
|
||||
|
||||
const show_course_modal = (e) => {
|
||||
const target = $(e.currentTarget);
|
||||
const course = target.data("course");
|
||||
const evaluator = target.data("evaluator");
|
||||
const course_name = target.data("name");
|
||||
|
||||
let course_modal = new frappe.ui.Dialog({
|
||||
title: "Add Course",
|
||||
fields: [
|
||||
{
|
||||
fieldtype: "Link",
|
||||
options: "LMS Course",
|
||||
label: __("Course"),
|
||||
fieldname: "course",
|
||||
reqd: 1,
|
||||
only_select: 1,
|
||||
default: course || "",
|
||||
read_only: course ? 1 : 0,
|
||||
},
|
||||
{
|
||||
fieldtype: "Link",
|
||||
options: "Course Evaluator",
|
||||
label: __("Course Evaluator"),
|
||||
fieldname: "evaluator",
|
||||
only_select: 1,
|
||||
default: evaluator || "",
|
||||
},
|
||||
],
|
||||
primary_action_label: __("Add"),
|
||||
primary_action(values) {
|
||||
add_course(values, course_name);
|
||||
course_modal.hide();
|
||||
},
|
||||
});
|
||||
course_modal.show();
|
||||
setTimeout(() => {
|
||||
$(".modal-body").css("min-height", "300px");
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const enroll_batch = (e) => {
|
||||
let batch_name = $(".class-details").data("batch");
|
||||
if (frappe.session.user == "Guest") {
|
||||
window.location.href =
|
||||
"/login?redirect-to=/batches/details/" + batch_name;
|
||||
}
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.batch_student.batch_student.enroll_batch",
|
||||
args: {
|
||||
batch_name: batch_name,
|
||||
},
|
||||
callback(r) {
|
||||
frappe.show_alert(
|
||||
{
|
||||
message: __("Successfully Enrolled"),
|
||||
indicator: "green",
|
||||
},
|
||||
2000
|
||||
);
|
||||
window.location.href = `/batches/${batch_name}`;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const add_course = (values, course_name) => {
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_batch.lms_batch.add_course",
|
||||
args: {
|
||||
course: values.course,
|
||||
evaluator: values.evaluator,
|
||||
parent: $(".class-details").data("batch"),
|
||||
name: course_name || "",
|
||||
},
|
||||
callback(r) {
|
||||
frappe.show_alert(
|
||||
{
|
||||
message: course_name
|
||||
? __("Course Updated")
|
||||
: __("Course Added"),
|
||||
indicator: "green",
|
||||
},
|
||||
2000
|
||||
);
|
||||
window.location.reload();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const remove_course = (e) => {
|
||||
frappe.confirm("Are you sure you want to remove this course?", () => {
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_batch.lms_batch.remove_course",
|
||||
args: {
|
||||
course: $(e.currentTarget).data("course"),
|
||||
parent: $(".class-details").data("batch"),
|
||||
},
|
||||
callback(r) {
|
||||
frappe.show_alert(
|
||||
{
|
||||
message: __("Course Removed"),
|
||||
indicator: "green",
|
||||
},
|
||||
2000
|
||||
);
|
||||
window.location.reload();
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -1,87 +0,0 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from lms.lms.utils import (
|
||||
has_course_moderator_role,
|
||||
has_course_evaluator_role,
|
||||
check_multicurrency,
|
||||
)
|
||||
from lms.www.utils import is_student
|
||||
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
batch_name = frappe.form_dict["batchname"]
|
||||
|
||||
context.batch_info = frappe.db.get_value(
|
||||
"LMS Batch",
|
||||
batch_name,
|
||||
[
|
||||
"name",
|
||||
"title",
|
||||
"description",
|
||||
"batch_details",
|
||||
"start_date",
|
||||
"end_date",
|
||||
"paid_batch",
|
||||
"amount",
|
||||
"currency",
|
||||
"category",
|
||||
"medium",
|
||||
"start_time",
|
||||
"end_time",
|
||||
"seat_count",
|
||||
"published",
|
||||
"meta_image",
|
||||
"batch_details_raw",
|
||||
"evaluation_end_date",
|
||||
"amount_usd",
|
||||
"allow_self_enrollment",
|
||||
],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if context.batch_info.amount and context.batch_info.currency:
|
||||
amount, currency = check_multicurrency(
|
||||
context.batch_info.amount,
|
||||
context.batch_info.currency,
|
||||
None,
|
||||
context.batch_info.amount_usd,
|
||||
)
|
||||
context.batch_info.amount = amount
|
||||
context.batch_info.currency = currency
|
||||
|
||||
context.is_moderator = has_course_moderator_role()
|
||||
context.is_evaluator = has_course_evaluator_role()
|
||||
context.is_student = is_student(batch_name)
|
||||
|
||||
if not context.is_moderator and not context.batch_info.published:
|
||||
raise frappe.PermissionError(_("You do not have permission to access this page."))
|
||||
|
||||
if context.is_student:
|
||||
frappe.local.flags.redirect_location = f"/batches/{batch_name}"
|
||||
raise frappe.Redirect
|
||||
|
||||
context.courses = frappe.get_all(
|
||||
"Batch Course",
|
||||
{"parent": batch_name},
|
||||
["name as batch_course", "course", "title", "evaluator"],
|
||||
order_by="idx",
|
||||
)
|
||||
|
||||
for course in context.courses:
|
||||
course.update(
|
||||
frappe.db.get_value(
|
||||
"LMS Course", course.course, ["name", "short_introduction", "image"], as_dict=1
|
||||
)
|
||||
)
|
||||
|
||||
context.student_count = frappe.db.count("Batch Student", {"parent": batch_name})
|
||||
context.seats_left = context.batch_info.seat_count - context.student_count
|
||||
|
||||
context.metatags = {
|
||||
"title": context.batch_info.title,
|
||||
"image": context.batch_info.meta_image,
|
||||
"description": context.batch_info.description,
|
||||
"keywords": context.batch_info.title,
|
||||
"og:type": "website",
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
{% extends "lms/templates/lms_base.html" %}
|
||||
{% block title %}
|
||||
{{ _("All Batches") }}
|
||||
{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
<div class="common-page-style lms-page-style">
|
||||
<div class="container">
|
||||
{{ Header() }}
|
||||
{% if past_batches | length or upcoming_batches | length or private_batches | length %}
|
||||
{{ BatchTabs(past_batches, upcoming_batches, private_batches, my_batches) }}
|
||||
{% else %}
|
||||
{{ EmptyState() }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% macro Header() %}
|
||||
<header class="edit-header">
|
||||
<div class="page-title mb-6"> {{ _("All Batches") }} </div>
|
||||
{% if is_moderator %}
|
||||
<button class="btn btn-primary btn-sm pull-right" id="create-batch">
|
||||
{{ _("New Batch") }}
|
||||
</button>
|
||||
{% endif %}
|
||||
</header>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro BatchTabs(past_batches, upcoming_batches, private_batches, my_batches) %}
|
||||
<article>
|
||||
<ul class="nav lms-nav" id="courses-tab">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-toggle="tab" href="#upcoming">
|
||||
{{ _("Upcoming") }}
|
||||
<span class="course-list-count">
|
||||
{{ upcoming_batches | length }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% if is_moderator %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#past">
|
||||
{{ _("Archived") }}
|
||||
<span class="course-list-count">
|
||||
{{ past_batches | length }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#private">
|
||||
{{ _("Private") }}
|
||||
<span class="course-list-count">
|
||||
{{ private_batches | length }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if frappe.session.user != "Guest" %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#my-batch">
|
||||
{{ _("Enrolled") }}
|
||||
<span class="course-list-count">
|
||||
{{ my_batches | length }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
</ul>
|
||||
|
||||
<div class="border-bottom mb-4"></div>
|
||||
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="upcoming" role="tabpanel" aria-labelledby="upcoming">
|
||||
{{ BatchCard(upcoming_batches, show_price=True, label="Upcoming") }}
|
||||
</div>
|
||||
|
||||
{% if is_moderator %}
|
||||
<div class="tab-pane" id="past" role="tabpanel" aria-labelledby="past">
|
||||
{{ BatchCard(past_batches, show_price=False, label="Archived") }}
|
||||
</div>
|
||||
|
||||
<div class="tab-pane" id="private" role="tabpanel" aria-labelledby="private">
|
||||
{{ BatchCard(private_batches, show_price=False, label="Private") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if frappe.session.user != "Guest" %}
|
||||
<div class="tab-pane" id="my-batch" role="tabpanel" aria-labelledby="my-batches">
|
||||
{{ BatchCard(my_batches, show_price=False, label="Enrolled") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</article>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro BatchCard(batches, show_price=False, label="") %}
|
||||
{% if batches | length %}
|
||||
<div class="lms-card-parent">
|
||||
{% for batch in batches %}
|
||||
|
||||
<div class="common-card-style column-card" style="min-height: 150px;">
|
||||
|
||||
{% if batch.seat_count %}
|
||||
{% if batch.seats_left > 0 %}
|
||||
<div class="indicator-pill green align-self-start mb-2">
|
||||
{{ _("Seats Available") }}: {{ batch.seats_left }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="indicator-pill red align-self-start mb-2">
|
||||
{{ _("No Seats Left") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<div class="bold-heading">
|
||||
{{ batch.title }}
|
||||
</div>
|
||||
|
||||
{% if batch.description %}
|
||||
<div class="short-introduction">
|
||||
{{ batch.description }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if show_price and batch.paid_batch %}
|
||||
<div class="bold-heading mb-2">
|
||||
{{ frappe.utils.fmt_money(batch.amount, 0, batch.currency) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-auto mb-2">
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-calendar"></use>
|
||||
</svg>
|
||||
<span>
|
||||
{{ frappe.utils.format_date(batch.start_date, "medium") }}
|
||||
</span>
|
||||
{% if batch.start_date != batch.end_date %}
|
||||
<span>
|
||||
- {{ frappe.utils.format_date(batch.end_date, "long") }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-clock"></use>
|
||||
</svg>
|
||||
<span>
|
||||
{{ frappe.utils.format_time(batch.start_time, "HH:mm a") }} -
|
||||
</span>
|
||||
<span>
|
||||
{{ frappe.utils.format_time(batch.end_time, "HH:mm a") }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<svg class="icon icon-md">
|
||||
<use href="#icon-education"></use>
|
||||
</svg>
|
||||
{{ batch.course_count }} {{ _("Courses") }}
|
||||
</div>
|
||||
|
||||
{% if is_student(batch.name) %}
|
||||
<a class="stretched-link" href="/batches/{{ batch.name }}"></a>
|
||||
{% else %}
|
||||
<a class="stretched-link" href="/batches/details/{{ batch.name }}"></a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted mt-3">
|
||||
{{ _("No {0} batches").format(label|lower) }}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro EmptyState() %}
|
||||
<div class="empty-state">
|
||||
<img class="icon icon-xl" src="/assets/lms/icons/comment.svg">
|
||||
<div class="empty-state-text">
|
||||
<div class="empty-state-heading">{{ _("No Batches") }}</div>
|
||||
<div class="course-meta">{{ _("Please contact the Administrator for more information.") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{%- block script %}
|
||||
{{ super() }}
|
||||
{{ include_script('controls.bundle.js') }}
|
||||
{% if is_moderator %}
|
||||
<script>
|
||||
frappe.boot.user = {
|
||||
"can_create": [],
|
||||
"can_select": ["LMS Category"],
|
||||
"can_read": ["LMS Category"]
|
||||
};
|
||||
let batch_info = null;
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -1,97 +0,0 @@
|
||||
import frappe
|
||||
from frappe.utils import getdate, get_time_str, nowtime
|
||||
from lms.lms.utils import (
|
||||
has_course_moderator_role,
|
||||
has_course_evaluator_role,
|
||||
check_multicurrency,
|
||||
)
|
||||
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
context.is_moderator = has_course_moderator_role()
|
||||
context.is_evaluator = has_course_evaluator_role()
|
||||
batches = frappe.get_all(
|
||||
"LMS Batch",
|
||||
fields=[
|
||||
"name",
|
||||
"title",
|
||||
"description",
|
||||
"start_date",
|
||||
"end_date",
|
||||
"start_time",
|
||||
"end_time",
|
||||
"paid_batch",
|
||||
"amount",
|
||||
"currency",
|
||||
"seat_count",
|
||||
"published",
|
||||
"amount_usd",
|
||||
],
|
||||
order_by="start_date",
|
||||
)
|
||||
|
||||
past_batches, upcoming_batches, private_batches = [], [], []
|
||||
for batch in batches:
|
||||
batch.student_count = frappe.db.count("Batch Student", {"parent": batch.name})
|
||||
batch.course_count = frappe.db.count("Batch Course", {"parent": batch.name})
|
||||
|
||||
if batch.amount and batch.currency:
|
||||
amount, currency = check_multicurrency(
|
||||
batch.amount, batch.currency, None, batch.amount_usd
|
||||
)
|
||||
batch.amount = amount
|
||||
batch.currency = currency
|
||||
|
||||
batch.seats_left = (
|
||||
batch.seat_count - batch.student_count if batch.seat_count else None
|
||||
)
|
||||
if not batch.published:
|
||||
private_batches.append(batch)
|
||||
elif getdate(batch.start_date) < getdate():
|
||||
past_batches.append(batch)
|
||||
elif (
|
||||
getdate(batch.start_date) == getdate() and get_time_str(batch.start_time) < nowtime()
|
||||
):
|
||||
past_batches.append(batch)
|
||||
else:
|
||||
upcoming_batches.append(batch)
|
||||
|
||||
context.past_batches = sorted(past_batches, key=lambda d: d.start_date, reverse=True)
|
||||
context.upcoming_batches = sorted(upcoming_batches, key=lambda d: d.start_date)
|
||||
context.private_batches = sorted(private_batches, key=lambda d: d.start_date)
|
||||
|
||||
if frappe.session.user != "Guest":
|
||||
my_batches_info = []
|
||||
my_batches = frappe.get_all(
|
||||
"Batch Student", {"student": frappe.session.user}, pluck="parent"
|
||||
)
|
||||
|
||||
for batch in my_batches:
|
||||
batchinfo = frappe.db.get_value(
|
||||
"LMS Batch",
|
||||
batch,
|
||||
[
|
||||
"name",
|
||||
"title",
|
||||
"description",
|
||||
"start_date",
|
||||
"end_date",
|
||||
"paid_batch",
|
||||
"amount",
|
||||
"currency",
|
||||
"seat_count",
|
||||
],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
batchinfo.student_count = frappe.db.count(
|
||||
"Batch Student", {"parent": batchinfo.name}
|
||||
)
|
||||
batchinfo.course_count = frappe.db.count("Batch Course", {"parent": batchinfo.name})
|
||||
batchinfo.seats_left = batchinfo.seat_count - batchinfo.student_count
|
||||
|
||||
my_batches_info.append(batchinfo)
|
||||
my_batches_info = sorted(my_batches_info, key=lambda d: d.start_date, reverse=True)
|
||||
|
||||
context.my_batches = my_batches_info
|
||||
@@ -1,92 +0,0 @@
|
||||
{% extends "lms/templates/lms_base.html" %}
|
||||
{% block title %}
|
||||
{{ student.first_name }}'s {{ _("Progress") }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block page_content %}
|
||||
<div class="common-page-style">
|
||||
{{ Header() }}
|
||||
<div class="container">
|
||||
{{ Progress(batch, student) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% macro Header() %}
|
||||
<header class="sticky mb-5">
|
||||
<div class="container">
|
||||
<div class="edit-header">
|
||||
<div>
|
||||
<div class="page-title">
|
||||
{{ _("{0}").format(student.full_name) }}
|
||||
</div>
|
||||
<div class="vertically-center">
|
||||
<a class="dark-links" href="/batches">
|
||||
{{ _("All Batches") }}
|
||||
</a>
|
||||
<img class="icon icon-sm mr-0" src="/assets/lms/icons/chevron-right.svg">
|
||||
<a class="dark-links" href="/batches/{{ batch.name }}">
|
||||
{{ batch.title }}
|
||||
</a>
|
||||
<img class="icon icon-sm mr-0" src="/assets/lms/icons/chevron-right.svg">
|
||||
<span class="breadcrumb-destination">
|
||||
{{ _("Student Progress").format(student.full_name) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="align-self-center">
|
||||
<a class="btn btn-default btn-sm" href="/users/{{ student.username }}">
|
||||
{{ _("View Profile") }}
|
||||
</a>
|
||||
{% if student.name == frappe.session.user %}
|
||||
<button class="btn btn-default btn-sm btn-schedule-eval ml-2">
|
||||
{{ _("Schedule Evaluation") }}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if is_moderator %}
|
||||
<button class="btn btn-default btn-sm btn-certification ml-2">
|
||||
{{ _("Grant Certificate") }}
|
||||
</button>
|
||||
<a class="btn btn-primary btn-sm btn-evaluate ml-2" href="/evaluation/new?member={{student.name}}&date={{frappe.utils.getdate()}}&class_name={{batch.name}}">
|
||||
{{ _("Evaluate") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro Progress(batch, student) %}
|
||||
{{ UpcomingEvals(upcoming_evals) }}
|
||||
{{ Assessments(batch, student) }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro UpcomingEvals(upcoming_evals) %}
|
||||
<div class="mb-8">
|
||||
{% include "lms/templates/upcoming_evals.html" %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro Assessments(batch, student) %}
|
||||
<div class="mb-8">
|
||||
{% include "lms/templates/assessments.html" %}
|
||||
</div>
|
||||
|
||||
{% endmacro %}
|
||||
|
||||
{%- block script %}
|
||||
{{ super() }}
|
||||
<script>
|
||||
frappe.boot.user = {
|
||||
"can_create": [],
|
||||
"can_select": ["LMS Course"],
|
||||
"can_read": ["LMS Course"]
|
||||
};
|
||||
let courses = {{ courses | json }};
|
||||
let batch_name = "{{ batch.name }}";
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,45 +0,0 @@
|
||||
frappe.ready(() => {
|
||||
frappe.require("controls.bundle.js");
|
||||
|
||||
$(".clickable-row").click((e) => {
|
||||
window.location.href = $(e.currentTarget).data("href");
|
||||
});
|
||||
|
||||
$(".btn-certification").click((e) => {
|
||||
show_certificate_dialog(e);
|
||||
});
|
||||
});
|
||||
|
||||
const show_certificate_dialog = (e) => {
|
||||
this.certificate_dialog = new frappe.ui.Dialog({
|
||||
title: __("Grant Certificate"),
|
||||
fields: [
|
||||
{
|
||||
fieldtype: "Link",
|
||||
fieldname: "course",
|
||||
label: __("Course"),
|
||||
options: "LMS Course",
|
||||
reqd: 1,
|
||||
filters: {
|
||||
name: ["in", courses],
|
||||
},
|
||||
filter_description: " ",
|
||||
only_select: 1,
|
||||
},
|
||||
{
|
||||
fieldtype: "Date",
|
||||
fieldname: "issue_date",
|
||||
label: __("Issue Date"),
|
||||
reqd: 1,
|
||||
default: frappe.datetime.get_today(),
|
||||
},
|
||||
{
|
||||
fieldtype: "Date",
|
||||
fieldname: "expiry_date",
|
||||
label: __("Expiry Date"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
this.certificate_dialog.show();
|
||||
};
|
||||
@@ -1,41 +0,0 @@
|
||||
import frappe
|
||||
from lms.lms.utils import (
|
||||
has_course_moderator_role,
|
||||
has_course_evaluator_role,
|
||||
get_upcoming_evals,
|
||||
)
|
||||
from frappe import _
|
||||
from lms.www.utils import get_assessments
|
||||
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
|
||||
student = frappe.form_dict["username"]
|
||||
batch_name = frappe.form_dict["batchname"]
|
||||
context.is_moderator = has_course_moderator_role()
|
||||
context.is_evaluator = has_course_evaluator_role()
|
||||
|
||||
context.student = frappe.db.get_value(
|
||||
"User",
|
||||
{"username": student},
|
||||
["first_name", "full_name", "name", "last_active", "username"],
|
||||
as_dict=True,
|
||||
)
|
||||
if (
|
||||
not context.is_moderator
|
||||
and not context.is_evaluator
|
||||
and not context.student.name == frappe.session.user
|
||||
):
|
||||
raise frappe.PermissionError(_("You don't have permission to access this page."))
|
||||
|
||||
context.batch = frappe.db.get_value(
|
||||
"LMS Batch", batch_name, ["name", "title"], as_dict=True
|
||||
)
|
||||
|
||||
context.courses = frappe.get_all(
|
||||
"Batch Course", {"parent": batch_name}, pluck="course"
|
||||
)
|
||||
|
||||
context.assessments = get_assessments(batch_name, context.student.name)
|
||||
context.upcoming_evals = get_upcoming_evals(context.student.name, context.courses)
|
||||
@@ -1,75 +0,0 @@
|
||||
{% extends "lms/templates/lms_base.html" %}
|
||||
{% block title %}
|
||||
{{ title }} {{ _("Billing") }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block page_content %}
|
||||
<div class="common-page-style">
|
||||
<div class="container form-width common-card-style column-card px-0 h-0 mt-8">
|
||||
{{ Header() }}
|
||||
{{ Details() }}
|
||||
{{ BillingDetails() }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% macro Header() %}
|
||||
<div class="px-4 pb-2">
|
||||
<div class="page-title">
|
||||
{{ _("Order Details") }}
|
||||
</div>
|
||||
<div>
|
||||
{{ _("Enter the billing information to complete the payment.").format(module) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro Details() %}
|
||||
<div class="px-4 pt-5 border-top">
|
||||
<div class="">
|
||||
<div class="flex mb-2">
|
||||
<div class="field-label">
|
||||
{% set label = "Course" if module == "course" else "Batch" %}
|
||||
{{ _(label) }} : {{ title }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<div class="field-label">
|
||||
{{ _("Total Price: ") }}
|
||||
<span class="total-price">{{ frappe.utils.fmt_money(amount_with_gst, 2, currency) if gst_applied else frappe.utils.fmt_money(amount, 2, currency) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% if gst_applied %}
|
||||
<span id="gst-message" class="small mt-2">
|
||||
{{ _("18% GST included") }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro BillingDetails() %}
|
||||
<div class="mt-8 px-4">
|
||||
<div class="bold-heading mb-4">
|
||||
{{ _("Billing Details") }}
|
||||
</div>
|
||||
<div id="billing-form"></div>
|
||||
<button class="btn btn-primary btn-md btn-pay" data-doctype="{{ doctype }}" data-name="{{ docname | urlencode }}">
|
||||
{{ "Proceed to Payment" }}
|
||||
</button>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{%- block script %}
|
||||
{{ super() }}
|
||||
<script src="https://checkout.razorpay.com/v1/checkout.js"></script>
|
||||
<script>
|
||||
const address = {{ address if address else 0 }};
|
||||
const amount = {{ amount }};
|
||||
const currency = "{{ currency }}";
|
||||
const exception_country = {{ exception_country }};
|
||||
const original_price_formatted = "{{ frappe.utils.fmt_money(original_amount, 0, original_currency) }}"
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,246 +0,0 @@
|
||||
frappe.ready(() => {
|
||||
if ($("#billing-form").length) {
|
||||
frappe.require("controls.bundle.js", () => {
|
||||
setup_billing();
|
||||
});
|
||||
}
|
||||
|
||||
$(".btn-pay").click((e) => {
|
||||
generate_payment_link(e);
|
||||
});
|
||||
});
|
||||
|
||||
const setup_billing = () => {
|
||||
this.billing = new frappe.ui.FieldGroup({
|
||||
fields: [
|
||||
{
|
||||
fieldtype: "Data",
|
||||
label: __("Billing Name"),
|
||||
fieldname: "billing_name",
|
||||
reqd: 1,
|
||||
default: address && address.billing_name,
|
||||
},
|
||||
{
|
||||
fieldtype: "Data",
|
||||
label: __("Address Line 1"),
|
||||
fieldname: "address_line1",
|
||||
reqd: 1,
|
||||
default: address && address.address_line1,
|
||||
},
|
||||
{
|
||||
fieldtype: "Data",
|
||||
label: __("Address Line 2"),
|
||||
fieldname: "address_line2",
|
||||
default: address && address.address_line2,
|
||||
},
|
||||
{
|
||||
fieldtype: "Data",
|
||||
label: __("City/Town"),
|
||||
fieldname: "city",
|
||||
reqd: 1,
|
||||
default: address && address.city,
|
||||
},
|
||||
{
|
||||
fieldtype: "Data",
|
||||
label: __("State/Province"),
|
||||
fieldname: "state",
|
||||
default: address && address.state,
|
||||
},
|
||||
{
|
||||
fieldtype: "Column Break",
|
||||
},
|
||||
{
|
||||
fieldtype: "Link",
|
||||
label: __("Country"),
|
||||
fieldname: "country",
|
||||
options: "Country",
|
||||
reqd: 1,
|
||||
only_select: 1,
|
||||
default: address && address.country,
|
||||
change: () => {
|
||||
change_currency();
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldtype: "Data",
|
||||
label: __("Postal Code"),
|
||||
fieldname: "pincode",
|
||||
reqd: 1,
|
||||
default: address && address.pincode,
|
||||
},
|
||||
{
|
||||
fieldtype: "Data",
|
||||
label: __("Phone Number"),
|
||||
fieldname: "phone",
|
||||
reqd: 1,
|
||||
default: address && address.phone,
|
||||
},
|
||||
{
|
||||
fieldtype: "Link",
|
||||
label: __("Where did you hear about this?"),
|
||||
fieldname: "source",
|
||||
options: "LMS Source",
|
||||
only_select: 1,
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldtype: "Section Break",
|
||||
label: __("GST Details"),
|
||||
fieldname: "gst_details",
|
||||
depends_on: "eval:doc.country === 'India'",
|
||||
},
|
||||
{
|
||||
fieldtype: "Data",
|
||||
label: __("GSTIN"),
|
||||
fieldname: "gstin",
|
||||
},
|
||||
{
|
||||
fieldtype: "Column Break",
|
||||
fieldname: "gst_details_break",
|
||||
},
|
||||
{
|
||||
fieldtype: "Data",
|
||||
fieldname: "pan",
|
||||
label: __("PAN"),
|
||||
},
|
||||
],
|
||||
body: $("#billing-form").get(0),
|
||||
});
|
||||
this.billing.make();
|
||||
$("#billing-form .form-section:last").removeClass("empty-section");
|
||||
$("#billing-form .frappe-control").removeClass("hide-control");
|
||||
$("#billing-form .form-column").addClass("p-0");
|
||||
};
|
||||
|
||||
const generate_payment_link = (e) => {
|
||||
let new_address = this.billing.get_values();
|
||||
validate_address(new_address);
|
||||
let doctype = $(e.currentTarget).attr("data-doctype");
|
||||
let docname = decodeURIComponent($(e.currentTarget).attr("data-name"));
|
||||
|
||||
frappe.call({
|
||||
method: "lms.lms.utils.get_payment_options",
|
||||
args: {
|
||||
doctype: doctype,
|
||||
docname: docname,
|
||||
phone: new_address.phone,
|
||||
country: new_address.country,
|
||||
},
|
||||
callback: (data) => {
|
||||
data.message.handler = (response) => {
|
||||
handle_success(
|
||||
response,
|
||||
doctype,
|
||||
docname,
|
||||
new_address,
|
||||
data.message.order_id
|
||||
);
|
||||
};
|
||||
let rzp1 = new Razorpay(data.message);
|
||||
rzp1.open();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handle_success = (response, doctype, docname, address, order_id) => {
|
||||
frappe.call({
|
||||
method: "lms.lms.utils.verify_payment",
|
||||
args: {
|
||||
response: response,
|
||||
doctype: doctype,
|
||||
docname: docname,
|
||||
address: address,
|
||||
order_id: order_id,
|
||||
},
|
||||
callback: (data) => {
|
||||
frappe.show_alert({
|
||||
message: __("Payment Successful"),
|
||||
indicator: "green",
|
||||
});
|
||||
setTimeout(() => {
|
||||
window.location.href = data.message;
|
||||
}, 1000);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const change_currency = () => {
|
||||
$("#gst-message").removeClass("hide");
|
||||
let country = this.billing.get_value("country");
|
||||
if (exception_country.includes(country)) {
|
||||
update_price(original_price_formatted);
|
||||
return;
|
||||
}
|
||||
frappe.call({
|
||||
method: "lms.lms.utils.change_currency",
|
||||
args: {
|
||||
country: country,
|
||||
amount: amount,
|
||||
currency: currency,
|
||||
},
|
||||
callback: (data) => {
|
||||
let current_price = $(".total-price").text();
|
||||
if (current_price != data.message) {
|
||||
update_price(data.message);
|
||||
}
|
||||
if (data.message.includes("INR")) {
|
||||
$("#gst-message").removeClass("hide").addClass("show");
|
||||
} else {
|
||||
$("#gst-message").removeClass("show").addClass("hide");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const update_price = (price) => {
|
||||
$(".total-price").text(price);
|
||||
frappe.show_alert({
|
||||
message: "Total Price has been updated.",
|
||||
indicator: "yellow",
|
||||
});
|
||||
};
|
||||
|
||||
const validate_address = (billing_address) => {
|
||||
if (billing_address.country == "India" && !billing_address.state)
|
||||
frappe.throw(__("State is mandatory."));
|
||||
|
||||
const states = [
|
||||
"Andhra Pradesh",
|
||||
"Arunachal Pradesh",
|
||||
"Assam",
|
||||
"Bihar",
|
||||
"Chhattisgarh",
|
||||
"Goa",
|
||||
"Gujarat",
|
||||
"Haryana",
|
||||
"Himachal Pradesh",
|
||||
"Jharkhand",
|
||||
"Karnataka",
|
||||
"Kerala",
|
||||
"Madhya Pradesh",
|
||||
"Maharashtra",
|
||||
"Manipur",
|
||||
"Meghalaya",
|
||||
"Mizoram",
|
||||
"Nagaland",
|
||||
"Odisha",
|
||||
"Punjab",
|
||||
"Rajasthan",
|
||||
"Sikkim",
|
||||
"Tamil Nadu",
|
||||
"Telangana",
|
||||
"Tripura",
|
||||
"Uttar Pradesh",
|
||||
"Uttarakhand",
|
||||
"West Bengal",
|
||||
];
|
||||
if (
|
||||
billing_address.country == "India" &&
|
||||
!states.includes(billing_address.state)
|
||||
)
|
||||
frappe.throw(
|
||||
__(
|
||||
"Please enter a valid state with correct spelling and the first letter capitalized."
|
||||
)
|
||||
);
|
||||
};
|
||||
@@ -1,121 +0,0 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from lms.lms.utils import check_multicurrency, apply_gst
|
||||
|
||||
|
||||
def get_context(context):
|
||||
module = frappe.form_dict.module
|
||||
docname = frappe.form_dict.modulename
|
||||
doctype = "LMS Course" if module == "course" else "LMS Batch"
|
||||
|
||||
context.module = module
|
||||
context.docname = docname
|
||||
context.doctype = doctype
|
||||
|
||||
validate_access(doctype, docname, module)
|
||||
get_billing_details(context)
|
||||
|
||||
context.original_currency = context.currency
|
||||
context.original_amount = (
|
||||
(context.amount * 1.18) if context.original_currency == "INR" else context.amount
|
||||
)
|
||||
|
||||
context.exception_country = frappe.get_all(
|
||||
"Payment Country", filters={"parent": "LMS Settings"}, pluck="country"
|
||||
)
|
||||
|
||||
context.amount, context.currency = check_multicurrency(
|
||||
context.amount, context.currency, None, context.amount_usd
|
||||
)
|
||||
|
||||
context.address = get_address()
|
||||
if context.currency == "INR":
|
||||
context.amount_with_gst, context.gst_applied = apply_gst(context.amount, None)
|
||||
|
||||
|
||||
def validate_access(doctype, docname, module):
|
||||
if frappe.session.user == "Guest":
|
||||
raise frappe.PermissionError(_("Please login to continue with payment."))
|
||||
|
||||
if module not in ["course", "batch"]:
|
||||
raise ValueError(_("Module is incorrect."))
|
||||
|
||||
if not frappe.db.exists(doctype, docname):
|
||||
raise ValueError(_("Module Name is incorrect or does not exist."))
|
||||
|
||||
if doctype == "LMS Course":
|
||||
membership = frappe.db.exists(
|
||||
"LMS Enrollment", {"member": frappe.session.user, "course": docname}
|
||||
)
|
||||
if membership:
|
||||
raise frappe.PermissionError(_("You are already enrolled for this course"))
|
||||
|
||||
else:
|
||||
membership = frappe.db.exists(
|
||||
"Batch Student", {"student": frappe.session.user, "parent": docname}
|
||||
)
|
||||
if membership:
|
||||
raise frappe.PermissionError(_("You are already enrolled for this batch."))
|
||||
|
||||
|
||||
def get_billing_details(context):
|
||||
if context.doctype == "LMS Course":
|
||||
details = frappe.db.get_value(
|
||||
"LMS Course",
|
||||
context.docname,
|
||||
["title", "name", "paid_course", "course_price as amount", "currency", "amount_usd"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
if not details.paid_course:
|
||||
raise frappe.PermissionError(_("This course is free."))
|
||||
|
||||
else:
|
||||
details = frappe.db.get_value(
|
||||
"LMS Batch",
|
||||
context.docname,
|
||||
["title", "name", "paid_batch", "amount", "currency", "amount_usd"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
if not details.paid_batch:
|
||||
raise frappe.PermissionError(
|
||||
_("To join this batch, please contact the Administrator.")
|
||||
)
|
||||
|
||||
context.title = details.title
|
||||
context.amount = details.amount
|
||||
context.currency = details.currency
|
||||
context.amount_usd = details.amount_usd
|
||||
|
||||
|
||||
def get_address():
|
||||
address = frappe.get_all(
|
||||
"Address",
|
||||
{"email_id": frappe.session.user},
|
||||
[
|
||||
"address_title as billing_name",
|
||||
"address_line1",
|
||||
"address_line2",
|
||||
"city",
|
||||
"state",
|
||||
"country",
|
||||
"pincode",
|
||||
"phone",
|
||||
],
|
||||
order_by="creation desc",
|
||||
limit=1,
|
||||
)
|
||||
|
||||
if not len(address):
|
||||
return None
|
||||
else:
|
||||
address = address[0]
|
||||
|
||||
if not address.address_line2:
|
||||
address.address_line2 = ""
|
||||
|
||||
if not address.state:
|
||||
address.state = ""
|
||||
|
||||
return address
|
||||
@@ -1,63 +0,0 @@
|
||||
{% extends "lms/templates/lms_base.html" %}
|
||||
{% block title %}
|
||||
{{ _("Certified Participants") }}
|
||||
{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
<main class="common-page-style">
|
||||
<div class="container">
|
||||
<header>
|
||||
{% if course_filter | length %}
|
||||
<select class="lms-menu pull-right" id="certificate-filter">
|
||||
<option selected value="">
|
||||
{{ _("Filter by Certificate") }}
|
||||
</option>
|
||||
{% for course in course_filter %}
|
||||
<option value="{{ course }}">
|
||||
{{ course }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% endif %}
|
||||
<div class="page-title mb-5">
|
||||
{{ _("Certified Participants") }}
|
||||
</div>
|
||||
</header>
|
||||
{% if participants | length %}
|
||||
{{ ParticipantsList() }}
|
||||
{% else %}
|
||||
{{ EmptyState() }}
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
{% macro ParticipantsList() %}
|
||||
<article class="member-parent">
|
||||
{% for participant in participants %}
|
||||
<div class="common-card-style column-card align-center">
|
||||
{{ widgets.Avatar(member=participant, avatar_class="avatar-large") }}
|
||||
<div class="bold-heading text-center">
|
||||
{{ participant.full_name }}
|
||||
</div>
|
||||
{% for course in participant.courses %}
|
||||
<div class="course-name text-center mb-1" data-course="{{ course }}">
|
||||
{{ course }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<a class="stretched-link" href="/users/{{ participant.username }}"></a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</article>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro EmptyState() %}
|
||||
<div class="empty-state">
|
||||
<img class="icon icon-xl" src="/assets/lms/icons/comment.svg">
|
||||
<div class="empty-state-text">
|
||||
<div class="empty-state-heading">{{ _("No Certified Participants") }}</div>
|
||||
<div class="course-meta">{{ _("Enroll in a batch to get certified.") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
@@ -1,18 +0,0 @@
|
||||
frappe.ready(() => {
|
||||
$("#certificate-filter").change((e) => {
|
||||
filter_certified_participants();
|
||||
});
|
||||
});
|
||||
|
||||
const filter_certified_participants = () => {
|
||||
const certificate = $("#certificate-filter").val();
|
||||
$(".common-card-style").removeClass("hide");
|
||||
|
||||
if (certificate) {
|
||||
$(".common-card-style").addClass("hide");
|
||||
$(`[data-course='${certificate}']`)
|
||||
.closest(".common-card-style")
|
||||
.removeClass("hide");
|
||||
console.log(certificate);
|
||||
}
|
||||
};
|
||||
@@ -1,42 +0,0 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
members = frappe.get_all(
|
||||
"LMS Certificate",
|
||||
filters={"published": 1},
|
||||
pluck="member",
|
||||
order_by="issue_date desc",
|
||||
distinct=1,
|
||||
)
|
||||
|
||||
participants = []
|
||||
course_filter = []
|
||||
for member in members:
|
||||
details = frappe.db.get_value(
|
||||
"User", member, ["name", "full_name", "user_image", "username", "enabled"], as_dict=1
|
||||
)
|
||||
courses = frappe.get_all(
|
||||
"LMS Certificate",
|
||||
filters={"member": member, "published": 1},
|
||||
fields=["course", "issue_date"],
|
||||
)
|
||||
details.courses = []
|
||||
for course in courses:
|
||||
|
||||
if not details.issue_date:
|
||||
details.issue_date = course.issue_date
|
||||
|
||||
title = frappe.db.get_value("LMS Course", course.course, "title")
|
||||
details.courses.append(title)
|
||||
|
||||
if title not in course_filter:
|
||||
course_filter.append(title)
|
||||
|
||||
if details.enabled:
|
||||
participants.append(details)
|
||||
|
||||
participants = sorted(participants, key=lambda d: d.issue_date, reverse=True)
|
||||
context.participants = participants
|
||||
context.course_filter = course_filter
|
||||
@@ -1,34 +0,0 @@
|
||||
{% extends "templates/base.html" %}
|
||||
|
||||
{% macro render_nav(nav) %}
|
||||
|
||||
<div class="breadcrumb">
|
||||
{% for link in nav %}
|
||||
<a class="dark-links" href="{{ link.href }}">{{ link.title }}</a>
|
||||
{% if not loop.last %}
|
||||
<img class="ml-1 mr-1" src="/assets/lms/icons/chevron-right.svg">
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% block title %}Cohorts{% endblock %}
|
||||
{% block head_include %}
|
||||
<meta name="description" content="Cohorts" />
|
||||
<meta name="keywords" content="Cohorts" />
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div class="common-page-style">
|
||||
<div class='container'>
|
||||
{{ render_nav(nav | default([])) }}
|
||||
|
||||
{% block page_content %}
|
||||
Hello, world!
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
{% extends "www/cohorts/base.html" %} {% block title %} {{ _("Manage") }} {{
|
||||
course.title }} {% endblock %} {% block page_content %}
|
||||
<div class="course-home-headings">{{ cohort.title }}</div>
|
||||
{% if cohort.description %}
|
||||
<div>
|
||||
{{ frappe.utils.md_to_html(cohort.description) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<p>
|
||||
{{ frappe.db.count("Cohort Subgroup", {"cohort": cohort.name}) }} {{
|
||||
_("Subgroups") }} | {{ frappe.db.count("Cohort Mentor", {"cohort":
|
||||
cohort.name}) }} {{ _("Mentors") }} | {{ frappe.db.count("LMS Enrollment",
|
||||
{"cohort": cohort.name}) }} {{ _("Students") }}
|
||||
| {{ frappe.db.count("Cohort Join Request", {"cohort": cohort.name}) }} {{ _("Join Requests") }}
|
||||
</p>
|
||||
|
||||
{% if is_mentor %} {% set sg = mentor.get_subgroup() %}
|
||||
<div class="alert alert-info medium">
|
||||
<a href="{{sg.get_url()}}">
|
||||
{{ _("You are a mentor of {0} subgroup.").format(frappe.bold(sg.title))
|
||||
}}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<ul class="nav nav-tabs">
|
||||
{% set num_subgroups = cohort.get_subgroups() | length %} {{
|
||||
render_navitem("Subgroups", "", page=page, count=num_subgroups) }} {% for p
|
||||
in cohort.get_pages(scope="Cohort") %} {{ render_navitem(p.title, p.slug,
|
||||
page=page) }} {% endfor %}
|
||||
</ul>
|
||||
|
||||
<div class="my-5">
|
||||
{% if not page %} {{ render_subgroups() }} {% else %} {{ render_page(page)
|
||||
}} {% endif %}
|
||||
</div>
|
||||
|
||||
{% endblock %} {% macro render_subgroups() %}
|
||||
<ul class="list-group">
|
||||
{% for sg in cohort.get_subgroups(include_counts=True) %}
|
||||
<li class="list-group-item">
|
||||
<div>
|
||||
<a
|
||||
class="subgroup-title"
|
||||
style="font-weight: 700; color: inherit"
|
||||
href="/courses/{{course.name}}/subgroups/{{cohort.slug}}/{{sg.slug}}"
|
||||
>
|
||||
{{ sg.title }}
|
||||
</a>
|
||||
</div>
|
||||
<div style="font-size: 0.8em">
|
||||
{{ sg.num_mentors }} {{ _("Mentors") }} | {{sg.num_students}} {{
|
||||
_("Students") }} | {{sg.num_join_requests}} {{ _("Join Requests") }}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endmacro %} {% macro render_navitem(title, link, page, count=-1) %}
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link {{ 'active' if link==page }}"
|
||||
href="/courses/{{course.name}}/cohorts/{{cohort.slug}}/{{link}}"
|
||||
>
|
||||
{{ title }} {% if count != -1 %}
|
||||
<span
|
||||
class="badge {{'badge-primary' if link==page else 'badge-secondary'}}"
|
||||
>
|
||||
{{ count }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endmacro %}
|
||||
@@ -1,34 +0,0 @@
|
||||
import frappe
|
||||
|
||||
from . import utils
|
||||
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
course = utils.get_course()
|
||||
cohort = course and utils.get_cohort(course, frappe.form_dict["cohort"])
|
||||
if not cohort:
|
||||
context.template = "www/404.html"
|
||||
return
|
||||
|
||||
user = frappe.session.user
|
||||
mentor = cohort.get_mentor(user)
|
||||
is_mentor = mentor is not None
|
||||
is_admin = cohort.is_admin(user) or "System Manager" in frappe.get_roles()
|
||||
|
||||
utils.add_nav(context, "All Courses", "/courses")
|
||||
utils.add_nav(context, course.title, "/courses/" + course.name)
|
||||
utils.add_nav(context, "Cohorts", "/courses/" + course.name + "/manage")
|
||||
|
||||
context.course = course
|
||||
context.cohort = cohort
|
||||
context.mentor = mentor
|
||||
context.is_mentor = is_mentor
|
||||
context.is_admin = is_admin
|
||||
context.page = frappe.form_dict.get("page") or ""
|
||||
context.page_scope = "Cohort"
|
||||
|
||||
# Function to render to custom page given the slug
|
||||
context.render_page = lambda page: frappe.render_template(
|
||||
cohort.get_page_template(page, scope="Cohort"), context
|
||||
)
|
||||
@@ -1,40 +0,0 @@
|
||||
{% extends "www/cohorts/base.html" %} {% block title %} _("Manage") {{
|
||||
course.title }} {% endblock %} {% block page_content %} {% if cohorts %}
|
||||
<h2>{{ _("Cohorts") }}</h2>
|
||||
<div class="row">
|
||||
{% for cohort in cohorts %}
|
||||
<div class="col-md-6">{{ render_cohort(course, cohort) }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<h2>{{ _("Permission Denied") }}</h2>
|
||||
<p>{{ _("You don't have permission to manage this course.") }}</p>
|
||||
{% endif %} {% endblock %} {% macro render_cohort(course, cohort) %}
|
||||
<div class="cards-parent">
|
||||
<div class="common-card-style flex-column p-5">
|
||||
<h5 class="card-title">{{ cohort.title }}</h5>
|
||||
|
||||
{% if cohort.begin_date %}
|
||||
<h6 class="card-subtitle mb-2 text-muted">
|
||||
{{ frappe.utils.format_date(cohort.begin_date, "medium") }} - {{
|
||||
frappe.utils.format_date(cohort.end_date, "medium") }}
|
||||
</h6>
|
||||
{% endif %}
|
||||
|
||||
<p class="mb-0">
|
||||
{{ frappe.db.count("Cohort Subgroup", {"cohort": cohort.name}) }} {{
|
||||
_("Subgroups") }} | {{ frappe.db.count("Cohort Mentor", {"cohort":
|
||||
cohort.name}) }} {{ _("Mentors") }}
|
||||
| {{ frappe.db.count("LMS Enrollment", {"cohort": cohort.name}) }} {{ _("Students") }}
|
||||
| {{ frappe.db.count("Cohort Join Request", {"cohort": cohort.name}) }}
|
||||
{{ _("Join Requests") }}
|
||||
</p>
|
||||
|
||||
<a
|
||||
class="stretched-link"
|
||||
href="/courses/{{course.name}}/cohorts/{{cohort.slug}}"
|
||||
></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endmacro %}
|
||||
@@ -1,41 +0,0 @@
|
||||
import frappe
|
||||
from frappe.utils import get_url
|
||||
|
||||
from .utils import add_nav, get_course
|
||||
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
context.course = get_course()
|
||||
if frappe.session.user == "Guest":
|
||||
frappe.local.flags.redirect_location = "/login?redirect-to=" + frappe.request.path
|
||||
raise frappe.Redirect()
|
||||
|
||||
if not context.course:
|
||||
context.template = "www/404.html"
|
||||
return
|
||||
|
||||
context.cohorts = get_cohorts(context.course)
|
||||
if len(context.cohorts) == 1:
|
||||
frappe.local.flags.redirect_location = (
|
||||
f"{get_url()}/courses/{context.course.name}/cohorts/{context.cohorts[0].slug}"
|
||||
)
|
||||
raise frappe.Redirect
|
||||
|
||||
add_nav(context, "All Courses", "/courses")
|
||||
add_nav(context, context.course.title, "/courses/" + context.course.name)
|
||||
|
||||
|
||||
def get_cohorts(course):
|
||||
if "System Manager" in frappe.get_roles():
|
||||
return course.get_cohorts()
|
||||
|
||||
staff_roles = frappe.get_all(
|
||||
"Cohort Staff", filters={"course": course.name}, fields=["cohort"]
|
||||
)
|
||||
mentor_roles = frappe.get_all(
|
||||
"Cohort Mentor", filters={"course": course.name}, fields=["cohort"]
|
||||
)
|
||||
roles = staff_roles + mentor_roles
|
||||
names = {role.cohort for role in roles}
|
||||
return [frappe.get_doc("Cohort", name) for name in names]
|
||||
@@ -1,88 +0,0 @@
|
||||
{% extends "www/cohorts/base.html" %}
|
||||
|
||||
{% block title %}{{ _("Join Course") }}{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
|
||||
<h2>{{ _("Join Course") }}</h2>
|
||||
|
||||
<p>
|
||||
Course: {{course.title}}
|
||||
</p>
|
||||
<p>
|
||||
Cohort: {{cohort.title}}
|
||||
</p>
|
||||
<p>
|
||||
Subgroup: {{subgroup.title}}
|
||||
</p>
|
||||
|
||||
{% if frappe.session.user == "Guest" %}
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<p>
|
||||
{{ _("Please login to be able to join the course.") }}</p>
|
||||
|
||||
<p>
|
||||
{{ _("If you don't already have an account, you can") }} <a href="/login#signup">{{ _("sign up for a new account") }}</a>.
|
||||
</p>
|
||||
<a class="btn btn-primary" href="/login">{{ _("Login to continue") }}</a>
|
||||
</div>
|
||||
{% elif subgroup.has_student(frappe.session.user) %}
|
||||
<div class="alert alert-info">
|
||||
<p>{{ _("You are already a student of this course.") }}</p>
|
||||
<a class="btn btn-primary" href="/">{{ _("Start Learning") }} →</a>
|
||||
</div>
|
||||
{% elif subgroup.has_join_request(frappe.session.user) %}
|
||||
<div class="alert alert-info">
|
||||
<p>{{ _("We have received your request to join the course. You'll hear back from us soon.") }}</p>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
<a class="btn btn-primary" id="join">{{ _("Join the course") }}</a>
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
$(function() {
|
||||
console.log("ready!")
|
||||
$("#join").click(function() {
|
||||
var parts = window.location.pathname.split("/")
|
||||
var course = parts[2];
|
||||
var cohort = parts[4];
|
||||
var subgroup = parts[5];
|
||||
var invite_code = parts[6];
|
||||
|
||||
frappe.call('lms.lms.api.join_cohort', {
|
||||
course: course,
|
||||
cohort: cohort,
|
||||
subgroup: subgroup,
|
||||
invite_code: invite_code
|
||||
})
|
||||
.then(r => {
|
||||
if (r.message.ok) {
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: "Notification",
|
||||
primary_action_label: "Proceed",
|
||||
primary_action() {
|
||||
d.hide();
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
var message = "We've received your interest to join the course. We'll hear from us soon.";
|
||||
d.show();
|
||||
d.set_message(message);
|
||||
}
|
||||
else {
|
||||
frappe.msgprint(r.message.error);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,26 +0,0 @@
|
||||
import frappe
|
||||
|
||||
from . import utils
|
||||
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
|
||||
course = utils.get_course(frappe.form_dict["course"])
|
||||
cohort = course and utils.get_cohort(course, frappe.form_dict["cohort"])
|
||||
subgroup = cohort and utils.get_subgroup(cohort, frappe.form_dict["subgroup"])
|
||||
if not subgroup:
|
||||
context.template = "www/404.html"
|
||||
return
|
||||
|
||||
invite_code = frappe.form_dict["invite_code"]
|
||||
if subgroup.invite_code != invite_code:
|
||||
context.template = "www/404.html"
|
||||
return
|
||||
|
||||
utils.add_nav(context, "All Courses", "/courses")
|
||||
utils.add_nav(context, course.title, "/courses/" + course.name)
|
||||
|
||||
context.course = course
|
||||
context.cohort = cohort
|
||||
context.subgroup = subgroup
|
||||
@@ -1,262 +0,0 @@
|
||||
{% extends "www/cohorts/base.html" %}
|
||||
{% block title %} Subgroup {{subgroup.title}} - {{ course.title }} {% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
<div id="page-title" class="course-home-headings" data-subgroup="{{subgroup.name}}" data-title="{{subgroup.title}}">
|
||||
{{subgroup.title}}
|
||||
</div>
|
||||
|
||||
<ul class="nav nav-tabs">
|
||||
{{ render_navitem("Mentors", "/mentors", stats.mentors, page=="mentors")}}
|
||||
{{ render_navitem("Students", "/students", stats.students, page=="students")}}
|
||||
{% if is_mentor or is_admin %}
|
||||
{{ render_navitem("Join Requests", "/join-requests", stats.join_requests, page=="join-requests")}}
|
||||
|
||||
{% for p in cohort.get_pages(scope="Subgroup") %}
|
||||
{{ render_navitem(p.title, "/" + p.slug, -1, page==p.slug) }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if is_admin %}
|
||||
{{ render_navitem("Admin", "/admin", -1, page=="admin")}}
|
||||
{% endif %}
|
||||
</ul>
|
||||
<div class="my-5">
|
||||
{% if page == "info" %}
|
||||
{{ render_info() }}
|
||||
{% elif page == "mentors" %}
|
||||
{{ render_mentors() }}
|
||||
{% elif page == "students" %}
|
||||
{{ render_students() }}
|
||||
{% elif page == "join-requests" %}
|
||||
{{ render_join_requests() }}
|
||||
{% elif page == "admin" %}
|
||||
{{ render_admin() }}
|
||||
{% else %}
|
||||
{{ render_page(page) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% macro render_admin() %}
|
||||
<div style="background: white; padding: 20px;">
|
||||
<h5>Add a new mentor</h5>
|
||||
<form id="add-mentor-form">
|
||||
<div class="form-group">
|
||||
<input type="email" class="form-control" id="mentor-email" aria-describedby="emailHelp" placeholder="E-mail address">
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary" id="add-mentor">Add Mentor</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro render_mentors() %}
|
||||
{% set mentors = subgroup.get_mentors() %}
|
||||
{% if mentors %}
|
||||
<div class="member-parent">
|
||||
{% for m in mentors %}
|
||||
{{ widgets.MemberCard(member=m, avatar_class="avatar-medium", show_course_count=False) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div>None found.</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro render_students() %}
|
||||
{% set students = subgroup.get_students() %}
|
||||
{% if students %}
|
||||
<div class="member-parent">
|
||||
{% for student in students %}
|
||||
{{ widgets.MemberCard(member=student, avatar_class="avatar-medium", show_course_count=False) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div>None found.</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro render_join_requests() %}
|
||||
<h5>Invite Link</h5>
|
||||
{% set link = subgroup.get_invite_link() %}
|
||||
<p><a href="{{ link }}" id="invite-link">{{link}}</a>
|
||||
<br>
|
||||
<a class="btn btn-seconday btn-sm" id="copy-to-clipboard">Copy to Clipboard</a>
|
||||
</p>
|
||||
|
||||
{% set join_requests = subgroup.get_join_requests() %}
|
||||
<h5>Pending Requests</h5>
|
||||
{% if join_requests %}
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>When</th>
|
||||
<th>Email</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
{% for r in join_requests %}
|
||||
<tr>
|
||||
<td>{{loop.index}}</td>
|
||||
<td class="timestamp">{{r.creation}}</td>
|
||||
<td>{{r.email}}</td>
|
||||
<td class="actions"
|
||||
data-name="{{r.name}}"
|
||||
data-email="{{r.email}}">
|
||||
<a class="action-approve" href="#">Approve</a> | <a class="action-reject" href="#">Reject</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div> {{ _("There are no pending join requests.") }} </div>
|
||||
{% endif %}
|
||||
{% set rejected_requests = subgroup.get_join_requests(status="Rejected") %}
|
||||
|
||||
<h5>Rejected Requests</h5>
|
||||
{% if rejected_requests %}
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>When</th>
|
||||
<th>Email</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
{% for r in rejected_requests %}
|
||||
<tr>
|
||||
<td>{{loop.index}}</td>
|
||||
<td class="timestamp">{{r.creation}}</td>
|
||||
<td>{{r.email}}</td>
|
||||
<td class="actions"
|
||||
data-name="{{r.name}}"
|
||||
data-email="{{r.email}}">
|
||||
<a class="action-undo" href="#">Undo</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<p><em>There are no rejected requests.</em></p>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro render_navitem(title, link, count, active) %}
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link {{ 'active' if active }}"
|
||||
href="/courses/{{course.name}}/subgroups/{{cohort.slug}}/{{subgroup.slug}}{{link}}"
|
||||
>{{title}}
|
||||
{% if count != -1 %}
|
||||
<span
|
||||
class="badge {{'badge-primary' if active else 'badge-secondary'}}"
|
||||
>{{count}}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% block script %}
|
||||
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
$("#copy-to-clipboard").click(function() {
|
||||
var invite_link = $("#invite-link").text();
|
||||
navigator.clipboard.writeText(invite_link)
|
||||
.then(() => {
|
||||
$("#copy-to-clipboard").text("Copied!");
|
||||
setTimeout(
|
||||
() => $("#copy-to-clipboard").text("Copy to Clipboard"),
|
||||
500);
|
||||
});
|
||||
});
|
||||
|
||||
$(".timestamp"). each(function() {
|
||||
var t = moment($(this).text());
|
||||
var dt = t.from(moment.now());
|
||||
$(this).text(dt);
|
||||
});
|
||||
|
||||
$(".action-approve").click(function() {
|
||||
var el = $(this).parent().parent();
|
||||
var name = $(this).parent().data("name");
|
||||
var email = $(this).parent().data("email");
|
||||
|
||||
frappe.confirm(
|
||||
`Are you sure to accept ${email} to this subgroup?`,
|
||||
function() {
|
||||
run_action("lms.lms.api.approve_cohort_join_request", name, el, "approved", "Approved");
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$(".action-reject").click(function() {
|
||||
var el = $(this).parent().parent();
|
||||
var name = $(this).parent().data("name");
|
||||
var email = $(this).parent().data("email");
|
||||
frappe.confirm(`Are you sure to reject <strong>${email}</strong> from joining this subgroup?`, function() {
|
||||
run_action("lms.lms.api.reject_cohort_join_request", name, el, "rejected", "Rejected!");
|
||||
});
|
||||
});
|
||||
|
||||
$(".action-undo").click(function() {
|
||||
var el = $(this).parent().parent();
|
||||
var name = $(this).parent().data("name");
|
||||
var email = $(this).parent().data("email");
|
||||
frappe.confirm(`Are you sure to undo the rejection of <strong>${email}</strong>?`, function() {
|
||||
run_action("lms.lms.api.undo_reject_cohort_join_request", name, el, "undo-reject", "Reject Undone!");
|
||||
});
|
||||
});
|
||||
|
||||
function run_action(method, join_request, elem, classname, label) {
|
||||
frappe.call(method, {
|
||||
join_request: join_request,
|
||||
})
|
||||
.then(r => {
|
||||
if (r.message.ok) {
|
||||
$(elem)
|
||||
.addClass(classname)
|
||||
.find("td.actions").html(label);
|
||||
}
|
||||
else {
|
||||
frappe.msgprint(r.message.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$("#add-mentor").click(function() {
|
||||
var subgroup = $("#page-title").data("subgroup");
|
||||
var title = $("#page-title").data("title");
|
||||
var email = $("#mentor-email").val();
|
||||
frappe.call("lms.lms.api.add_mentor_to_subgroup", {
|
||||
subgroup: subgroup,
|
||||
email: email
|
||||
})
|
||||
.then(r => {
|
||||
if (r.message.ok) {
|
||||
frappe.msgprint(`Successfully added ${email} as mentor to ${title}`);
|
||||
}
|
||||
else {
|
||||
frappe.msgprint(r.message.error);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block style %}
|
||||
<style type="text/css">
|
||||
tr.approved {
|
||||
background:#c3e6cb;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
tr.rejected {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
tr.undo-reject {
|
||||
background:#d6d8d9;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -1,73 +0,0 @@
|
||||
import frappe
|
||||
|
||||
from . import utils
|
||||
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
course = utils.get_course()
|
||||
|
||||
cohort = utils.get_cohort(course, frappe.form_dict["cohort"])
|
||||
subgroup = utils.get_subgroup(cohort, frappe.form_dict["subgroup"])
|
||||
|
||||
if not subgroup:
|
||||
context.template = "www/404.html"
|
||||
return
|
||||
|
||||
page = frappe.form_dict.get("page")
|
||||
is_mentor = subgroup.is_mentor(frappe.session.user)
|
||||
is_admin = (
|
||||
cohort.is_admin(frappe.session.user) or "System Manager" in frappe.get_roles()
|
||||
)
|
||||
|
||||
if is_admin:
|
||||
role = "Admin"
|
||||
elif is_mentor:
|
||||
role = "Mentor"
|
||||
else:
|
||||
role = "Public"
|
||||
|
||||
pages = [
|
||||
("mentors", ["Admin", "Mentor", "Public"]),
|
||||
("students", ["Admin", "Mentor", "Public"]),
|
||||
("join-requests", ["Admin", "Mentor"]),
|
||||
("admin", ["Admin"]),
|
||||
]
|
||||
pages += [(p.slug, ["Admin", "Mentor"]) for p in cohort.get_pages(scope="Subgroup")]
|
||||
|
||||
page_names = [p for p, roles in pages if role in roles]
|
||||
|
||||
if page not in page_names:
|
||||
frappe.local.flags.redirect_location = subgroup.get_url() + "/mentors"
|
||||
raise frappe.Redirect
|
||||
|
||||
utils.add_nav(context, "All Courses", "/courses")
|
||||
utils.add_nav(context, course.title, f"/courses/{course.name}")
|
||||
utils.add_nav(context, "Cohorts", f"/courses/{course.name}/manage")
|
||||
utils.add_nav(context, cohort.title, f"/courses/{course.name}/cohorts/{cohort.slug}")
|
||||
|
||||
context.course = course
|
||||
context.cohort = cohort
|
||||
context.subgroup = subgroup
|
||||
context.stats = get_stats(subgroup)
|
||||
context.page = page
|
||||
context.is_admin = is_admin
|
||||
context.is_mentor = is_mentor
|
||||
context.page_scope = "Subgroup"
|
||||
|
||||
# Function to render to custom page given the slug
|
||||
context.render_page = lambda page: frappe.render_template(
|
||||
cohort.get_page_template(page, scope="Subgroup"), context
|
||||
)
|
||||
|
||||
|
||||
def get_stats(subgroup):
|
||||
return {
|
||||
"join_requests": len(subgroup.get_join_requests()),
|
||||
"students": len(subgroup.get_students()),
|
||||
"mentors": len(subgroup.get_mentors()),
|
||||
}
|
||||
|
||||
|
||||
def has_page(cohort, page):
|
||||
return cohort.get_page(page, scope="Subgroup")
|
||||
@@ -1,31 +0,0 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def get_course(course_name=None):
|
||||
course_name = course_name or frappe.form_dict["course"]
|
||||
return course_name and get_doc("LMS Course", course_name)
|
||||
|
||||
|
||||
def get_doc(doctype, name):
|
||||
try:
|
||||
return frappe.get_doc(doctype, name)
|
||||
except frappe.exceptions.DoesNotExistError:
|
||||
return
|
||||
|
||||
|
||||
def get_cohort(course, cohort_slug):
|
||||
name = frappe.get_value("Cohort", {"course": course.name, "slug": cohort_slug})
|
||||
return name and frappe.get_doc("Cohort", name)
|
||||
|
||||
|
||||
def get_subgroup(cohort, subgroup_slug):
|
||||
name = frappe.get_value(
|
||||
"Cohort Subgroup", {"cohort": cohort.name, "slug": subgroup_slug}
|
||||
)
|
||||
return name and frappe.get_doc("Cohort Subgroup", name)
|
||||
|
||||
|
||||
def add_nav(context, title, href):
|
||||
"""Adds a breadcrumb to the navigation."""
|
||||
nav = context.setdefault("nav", [])
|
||||
nav.append({"title": title, "href": href})
|
||||
@@ -1,71 +0,0 @@
|
||||
{% extends "templates/base.html" %}
|
||||
{% block title %} {{ member.full_name }} - {{ course.title }} {% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="common-page-style lms-page-style">
|
||||
<div class="container">
|
||||
|
||||
<div class="breadcrumb">
|
||||
<a class="dark-links" href="/courses">{{ _("All Courses") }}</a>
|
||||
<img class="ml-1 mr-1" src="/assets/lms/icons/chevron-right.svg">
|
||||
<a class="dark-links" href="/courses/{{ course.name }}">{{ course.title }}</a>
|
||||
</div>
|
||||
|
||||
<div class="certificate-parent">
|
||||
|
||||
<div class="certificate-content">
|
||||
{{ final_template }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{% if doc.member == frappe.session.user or is_moderator %}
|
||||
<div class="">
|
||||
<a class="btn btn-default btn-sm" target="_blank" href="/api/method/frappe.utils.print_format.download_pdf?doctype=LMS%20Certificate&name={{ doc.name }}&format={{ print_format }}&_lang=en">
|
||||
{{ _("Download") }}
|
||||
</a>
|
||||
|
||||
<!-- <a class="btn btn-default btn-sm ml-2" target="_blank" href="https://www.linkedin.com/sharing/share-offsite?url={{ url | urlencode }}">
|
||||
{{ _("Share") }}
|
||||
</a> -->
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card-heading mt-4">
|
||||
{{ _("Certificate Recipient") }}:
|
||||
</div>
|
||||
|
||||
<div class="certificate-recipient">
|
||||
{{ widgets.Avatar(member=member, avatar_class="avatar-small") }}
|
||||
<span class="ml-2">
|
||||
{{ member.full_name }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="card-heading mt-4">
|
||||
{{ _("Issued On") }}:
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ frappe.utils.format_date(doc.issue_date, "medium") }}
|
||||
</div>
|
||||
|
||||
<div class="card-heading mt-4">
|
||||
{{ _("About the Course") }}:
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ course.title }}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,79 +0,0 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils.jinja import render_template
|
||||
from frappe.utils import get_url
|
||||
from lms.lms.utils import has_course_moderator_role
|
||||
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
|
||||
try:
|
||||
course_name = frappe.form_dict["course"]
|
||||
certificate_name = frappe.form_dict["certificate"]
|
||||
except KeyError:
|
||||
redirect_to_course_list()
|
||||
|
||||
context.doc = frappe.db.get_value(
|
||||
"LMS Certificate",
|
||||
certificate_name,
|
||||
["name", "member", "issue_date", "expiry_date", "course", "template"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
if context.doc.course != course_name:
|
||||
redirect_to_course_list()
|
||||
|
||||
context.course = frappe.db.get_value(
|
||||
"LMS Course", course_name, ["title", "name", "image"], as_dict=True
|
||||
)
|
||||
context.member = frappe.db.get_value(
|
||||
"User", context.doc.member, ["full_name", "username"], as_dict=True
|
||||
)
|
||||
context.url = f"{get_url()}/courses/{context.course.name}/{context.doc.name}"
|
||||
context.is_moderator = has_course_moderator_role()
|
||||
|
||||
if context.doc.template:
|
||||
print_format = context.doc.template
|
||||
else:
|
||||
print_format = get_print_format()
|
||||
|
||||
context.print_format = print_format
|
||||
template = frappe.db.get_value(
|
||||
"Print Format", print_format, ["html", "css"], as_dict=True
|
||||
)
|
||||
merged_template = "<style> " + template.css + " </style>" + template.html
|
||||
final_template = render_template(merged_template, context)
|
||||
context.final_template = final_template
|
||||
|
||||
|
||||
def redirect_to_course_list():
|
||||
frappe.local.flags.redirect_location = "/courses"
|
||||
raise frappe.Redirect
|
||||
|
||||
|
||||
def get_print_format():
|
||||
print_format = None
|
||||
default = frappe.db.get_value(
|
||||
"Property Setter",
|
||||
{
|
||||
"doc_type": "LMS Certificate",
|
||||
"property": "default_print_format",
|
||||
},
|
||||
"value",
|
||||
)
|
||||
|
||||
if frappe.db.exists("Print Format", default):
|
||||
print_format = default
|
||||
|
||||
if not print_format and frappe.db.exists("Print Format", "Certificate"):
|
||||
print_format = "Certificate"
|
||||
|
||||
if not print_format:
|
||||
raise ValueError(
|
||||
_(
|
||||
"Default Print Format is not set for Certificate. Please contact the Administrator."
|
||||
)
|
||||
)
|
||||
|
||||
return print_format
|
||||
@@ -1,286 +0,0 @@
|
||||
{% extends "lms/templates/lms_base.html" %}
|
||||
{% block title %}
|
||||
{{ course.title if course.title else _("New Course") }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block page_content %}
|
||||
<div class="common-page-style">
|
||||
<div class="course-home-top-container">
|
||||
{{ CourseHomeHeader(course) }}
|
||||
<div class="course-home-page">
|
||||
<div class="container">
|
||||
{{ CourseHeaderOverlay(course) }}
|
||||
<div class="course-body-container">
|
||||
{{ Description(course) }}
|
||||
{{ widgets.CourseOutline(course=course, membership=membership, is_user_interested=is_user_interested) }}
|
||||
{% if course.status == "Approved" and not frappe.utils.cint(course.upcoming) %}
|
||||
{% include "lms/templates/reviews.html" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% macro CourseHomeHeader(course) %}
|
||||
<div class="course-head-container">
|
||||
<div class="container">
|
||||
<div class="course-card-wide">
|
||||
{{ BreadCrumb(course) }}
|
||||
{{ CourseCardWide(course) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- BreadCrumb -->
|
||||
{% macro BreadCrumb(course) %}
|
||||
<div class="breadcrumb">
|
||||
<a class="dark-links" href="/courses">{{ _("All Courses") }}</a>
|
||||
<img class="" src="/assets/lms/icons/chevron-right.svg">
|
||||
<span class="breadcrumb-destination">{{ course.title if course.title else _("New Course") }}</span>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Course Card -->
|
||||
{% macro CourseCardWide(course) %}
|
||||
<div class="d-flex align-items-center mt-8">
|
||||
{% for tag in get_tags(course.name) %}
|
||||
<div class="course-card-pills">
|
||||
{{ tag }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div id="title" class="page-title">
|
||||
{{ course.title }}
|
||||
</div>
|
||||
|
||||
<div id="intro">
|
||||
{% if course.short_introduction %}
|
||||
{{ course.short_introduction }}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if not course.upcoming %}
|
||||
<div class="avg-rating-stars">
|
||||
<div class="rating">
|
||||
{% for i in [1, 2, 3, 4, 5] %}
|
||||
<svg class="icon icon-lg {% if i <= frappe.utils.ceil(avg_rating) %} star-click {% endif %}" data-rating="{{ i }}">
|
||||
<use href="#icon-star"></use>
|
||||
</svg>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-2">
|
||||
<div class="bold-heading">{{ _("Instructors") }}:</div>
|
||||
{% for instructor in get_instructors(course.name) %}
|
||||
<div class="mt-1">
|
||||
{{ widgets.Avatar(member=instructor, avatar_class="avatar-small") }}
|
||||
<a class="button-links" href="{{ get_profile_url(instructor.username) }}">
|
||||
<span class="course-instructor"> {{ instructor.full_name }} </span>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if membership %}
|
||||
{% set progress = frappe.utils.cint(membership.progress) %}
|
||||
<div class="mt-8">
|
||||
<div class="progress-percent m-0">{{ progress }}% {{ _("Completed") }}</div>
|
||||
<div class="progress" title="{{ progress }}% Completed">
|
||||
<div class="progress-bar" role="progressbar" aria-valuenow="{{ progress }}"
|
||||
aria-valuemin="0" aria-valuemax="100" style="width:{{ progress }}%">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Overlay -->
|
||||
{% macro CourseHeaderOverlay(course) %}
|
||||
<div class="course-overlay-card">
|
||||
|
||||
{% if course.video_link %}
|
||||
<iframe class="preview-video" frameborder="0" allowfullscreen src="https://www.youtube.com/embed/{{ course.video_link }}"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"></iframe>
|
||||
{% endif %}
|
||||
|
||||
<div class="course-overlay-content">
|
||||
|
||||
<div class="cta-parent">
|
||||
{{ CTASection(course, membership) }}
|
||||
</div>
|
||||
|
||||
{{ Notes(course) }}
|
||||
|
||||
{% if course.paid_course %}
|
||||
<div class="vertically-center mb-3 bold-heading">
|
||||
{{ frappe.utils.fmt_money(course.course_price, 0, course.currency) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="vertically-center mb-3">
|
||||
<svg class="icon icon-md mr-1">
|
||||
<use class="" href="#icon-users">
|
||||
</svg>
|
||||
{{ format_number(get_students(course.name) | length) }} {{ _("Enrolled") }}
|
||||
</div>
|
||||
|
||||
<div class="vertically-center mb-3">
|
||||
<svg class="icon icon-md mr-1">
|
||||
<use href="#icon-education"></use>
|
||||
</svg>
|
||||
{{ get_lessons(course.name, None, False) }} {{ _("Lessons") }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Description -->
|
||||
{% macro Description(course) %}
|
||||
<div class="course-description-section">
|
||||
{{ course.description }}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
|
||||
<!-- Related Courses Section -->
|
||||
{% macro RelatedCourses(course) %}
|
||||
{% if course.related_courses | length %}
|
||||
<div class="related-courses">
|
||||
<div class="container">
|
||||
<div class="page-title"> {{ _("Other Courses") }} </div>
|
||||
<div class="carousel slide" id="carouselExampleControls" data-ride="carousel" data-interval="false">
|
||||
<div class="carousel-inner">
|
||||
{% for crs in course.related_courses %}
|
||||
{% if loop.index % 3 == 1 %}
|
||||
<div class="carousel-item {% if loop.index == 1 %} active {% endif %}"><div class="cards-parent">
|
||||
{% endif %}
|
||||
{{ widgets.CourseCard(course=crs, read_only=False) }}
|
||||
{% if loop.index % 3 == 0 or loop.index == course.related_courses | length %} </div> </div> {% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if course.related_courses | length > 3 %}
|
||||
<div class="slider-controls">
|
||||
<a class="carousel-control-prev" href="#carouselExampleControls" role="button" data-slide="prev">
|
||||
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
||||
</a>
|
||||
|
||||
<a class="carousel-control-next" href="#carouselExampleControls" role="button" data-slide="next">
|
||||
<span class="carousel-control-next-icon" aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- CTA's -->
|
||||
{% macro CTASection(course, membership) %}
|
||||
{% set lesson_index = get_lesson_index(membership.current_lesson) if membership and
|
||||
membership.current_lesson else "1.1" if first_lesson_exists(course.name) else None %}
|
||||
|
||||
<div class="all-cta">
|
||||
{% if is_instructor and not course.published and course.status != "Under Review" %}
|
||||
<div class="btn btn-primary wide-button" id="submit-for-review" data-course="{{ course.name | urlencode }}">
|
||||
{{ _("Submit for Review") }}
|
||||
</div>
|
||||
|
||||
{% elif is_instructor and lesson_index %}
|
||||
<a class="btn btn-primary wide-button" id="continue-learning"
|
||||
href="{{ get_lesson_url(course.name, lesson_index) }}{{ course.query_parameter }}">
|
||||
{{ _("Checkout Course") }}
|
||||
</a>
|
||||
|
||||
{% elif course.upcoming and not is_user_interested and not is_instructor %}
|
||||
<div class="btn btn-secondary wide-button notify-me" data-course="{{course.name | urlencode}}">
|
||||
{{ _("Notify me when available") }}
|
||||
</div>
|
||||
|
||||
{% elif is_cohort_staff(course.name, frappe.session.user) %}
|
||||
<a class="btn btn-secondary button-links wide-button" href="/courses/{{course.name}}/manage">
|
||||
{{ _("Manage Cohorts") }}
|
||||
</a>
|
||||
|
||||
{% elif membership %}
|
||||
<a class="btn btn-primary wide-button" id="continue-learning"
|
||||
href="{{ get_lesson_url(course.name, lesson_index) }}{{ course.query_parameter }}">
|
||||
{{ _("Continue Learning") }}
|
||||
</a>
|
||||
|
||||
{% elif course.paid_course and not is_instructor %}
|
||||
<a class="btn btn-primary wide-button" href="/billing/course/{{ course.name | urlencode }}">
|
||||
{{ _("Buy This Course") }}
|
||||
</a>
|
||||
|
||||
{% elif show_start_learing_cta(course, membership) %}
|
||||
<div class="btn btn-primary wide-button enroll-in-course" data-course="{{ course.name | urlencode }}">
|
||||
{{ _("Start Learning") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% set progress = frappe.utils.cint(membership.progress) %}
|
||||
|
||||
{% if membership and course.enable_certification %}
|
||||
{% if certificate %}
|
||||
<a class="btn btn-secondary wide-button mt-2" href="/courses/{{ course.name }}/{{ certificate }}">
|
||||
{{ _("Get Certificate") }}
|
||||
</a>
|
||||
|
||||
{% elif course.grant_certificate_after == "Completion" and progress == 100 %}
|
||||
<div class="btn btn-secondary wide-button mt-2" id="certification" data-course="{{ course.name }}">
|
||||
{{ _("Get Certificate") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if is_instructor or has_course_moderator_role() %}
|
||||
<a class="btn btn-secondary wide-button mt-2" title="Edit Course" href="/courses/{{ course.name }}/edit">
|
||||
<!-- <svg class="icon icon-md">
|
||||
<use href="#icon-edit"></use>
|
||||
</svg> -->
|
||||
{{ _("Edit") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Notes and Messages -->
|
||||
{% macro Notes(course) %}
|
||||
<div id="interest-alert" class="{% if not is_user_interested %} hide {% endif %}">
|
||||
{{ _("You have opted to be notified for this course. You will receive an email when the course becomes available.") }}
|
||||
</div>
|
||||
|
||||
{% if course.status == "Under Review" and is_instructor %}
|
||||
<div class="mb-4">
|
||||
{{ _("This course is currently under review. Once the review is complete, the System Admins will publish it on the website.") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if no_of_attempts and no_of_attempts >= course.max_attempts %}
|
||||
<p>
|
||||
{{ _("You have exceeded the maximum number of attempts allowed to appear for evaluations of this course.") }}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
@@ -1,144 +0,0 @@
|
||||
frappe.ready(() => {
|
||||
hide_wrapped_mentor_cards();
|
||||
|
||||
$(".review-link").click((e) => {
|
||||
show_review_dialog(e);
|
||||
});
|
||||
|
||||
$(".icon-rating").click((e) => {
|
||||
highlight_rating(e);
|
||||
});
|
||||
|
||||
$("#submit-review").click((e) => {
|
||||
submit_review(e);
|
||||
});
|
||||
|
||||
$("#certification").click((e) => {
|
||||
create_certificate(e);
|
||||
});
|
||||
|
||||
$("#submit-for-review").click((e) => {
|
||||
submit_for_review(e);
|
||||
});
|
||||
});
|
||||
|
||||
const hide_wrapped_mentor_cards = () => {
|
||||
let offset_top_prev;
|
||||
|
||||
$(".member-parent .member-card").each(function () {
|
||||
var offset_top = $(this).offset().top;
|
||||
if (offset_top > offset_top_prev) {
|
||||
$(this).addClass("wrapped").slideUp("fast");
|
||||
}
|
||||
if (!offset_top_prev) {
|
||||
offset_top_prev = offset_top;
|
||||
}
|
||||
});
|
||||
|
||||
if ($(".wrapped").length < 1) {
|
||||
$(".view-all-mentors").hide();
|
||||
}
|
||||
};
|
||||
|
||||
const show_review_dialog = (e) => {
|
||||
e.preventDefault();
|
||||
$("#review-modal").modal("show");
|
||||
};
|
||||
|
||||
const highlight_rating = (e) => {
|
||||
var rating = $(e.currentTarget).attr("data-rating");
|
||||
$(".icon-rating").removeClass("star-click");
|
||||
$(".icon-rating").each((i, elem) => {
|
||||
if (i <= rating - 1) {
|
||||
$(elem).addClass("star-click");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const submit_review = (e) => {
|
||||
e.preventDefault();
|
||||
let rating = $(".rating-field").children(".star-click").length;
|
||||
let review = $(".review-field").val();
|
||||
if (!rating) {
|
||||
$(".error-field").text("Please provide a rating.");
|
||||
return;
|
||||
}
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_course_review.lms_course_review.submit_review",
|
||||
args: {
|
||||
rating: rating,
|
||||
review: review,
|
||||
course: decodeURIComponent($(e.currentTarget).attr("data-course")),
|
||||
},
|
||||
callback: (data) => {
|
||||
if (data.message == "OK") {
|
||||
$(".review-modal").modal("hide");
|
||||
frappe.show_alert(
|
||||
{
|
||||
message: __("Review submitted."),
|
||||
indicator: "green",
|
||||
},
|
||||
3
|
||||
);
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const create_certificate = (e) => {
|
||||
e.preventDefault();
|
||||
course = $(e.currentTarget).attr("data-course");
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_certificate.lms_certificate.create_certificate",
|
||||
args: {
|
||||
course: course,
|
||||
},
|
||||
callback: (data) => {
|
||||
window.location.href = `/courses/${course}/${data.message.name}`;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const element_not_in_viewport = (el) => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
return (
|
||||
rect.bottom < 0 ||
|
||||
rect.right < 0 ||
|
||||
rect.left > window.innerWidth ||
|
||||
rect.top > window.innerHeight
|
||||
);
|
||||
};
|
||||
|
||||
const submit_for_review = (e) => {
|
||||
let course = $(e.currentTarget).data("course");
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_course.lms_course.submit_for_review",
|
||||
args: {
|
||||
course: course,
|
||||
},
|
||||
callback: (data) => {
|
||||
if (data.message == "No Chp") {
|
||||
frappe.msgprint(
|
||||
__(`There are no chapters in this course.
|
||||
Please add chapters and lessons to your course before you submit it for review.`)
|
||||
);
|
||||
} else if (data.message == "OK") {
|
||||
frappe.show_alert(
|
||||
{
|
||||
message: __(
|
||||
"Your course has been submitted for review."
|
||||
),
|
||||
indicator: "green",
|
||||
},
|
||||
3
|
||||
);
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,119 +0,0 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
from lms.lms.utils import (
|
||||
can_create_courses,
|
||||
get_evaluation_details,
|
||||
get_membership,
|
||||
has_course_moderator_role,
|
||||
is_certified,
|
||||
is_instructor,
|
||||
redirect_to_courses_list,
|
||||
get_average_rating,
|
||||
check_multicurrency,
|
||||
)
|
||||
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
|
||||
try:
|
||||
course_name = frappe.form_dict["course"]
|
||||
except KeyError:
|
||||
redirect_to_courses_list()
|
||||
|
||||
if course_name == "new-course":
|
||||
if not can_create_courses(course_name):
|
||||
message = "You do not have permission to access this page."
|
||||
if frappe.session.user == "Guest":
|
||||
message = "Please login to access this page."
|
||||
|
||||
raise frappe.PermissionError(_(message))
|
||||
|
||||
context.course = frappe._dict()
|
||||
context.course.edit_mode = True
|
||||
context.membership = None
|
||||
else:
|
||||
set_course_context(context, course_name)
|
||||
context.avg_rating = get_average_rating(context.course.name)
|
||||
|
||||
|
||||
def set_course_context(context, course_name):
|
||||
course = frappe.db.get_value(
|
||||
"LMS Course",
|
||||
course_name,
|
||||
[
|
||||
"name",
|
||||
"title",
|
||||
"image",
|
||||
"short_introduction",
|
||||
"description",
|
||||
"published",
|
||||
"upcoming",
|
||||
"disable_self_learning",
|
||||
"status",
|
||||
"video_link",
|
||||
"paid_course",
|
||||
"course_price",
|
||||
"currency",
|
||||
"amount_usd",
|
||||
"enable_certification",
|
||||
"grant_certificate_after",
|
||||
],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
if course.course_price:
|
||||
course.course_price, course.currency = check_multicurrency(
|
||||
course.course_price, course.currency, None, course.amount_usd
|
||||
)
|
||||
|
||||
if frappe.form_dict.get("edit"):
|
||||
if not is_instructor(course.name) and not has_course_moderator_role():
|
||||
raise frappe.PermissionError(_("You do not have permission to access this page."))
|
||||
course.edit_mode = True
|
||||
|
||||
if course is None:
|
||||
raise frappe.PermissionError(_("This is not a valid course URL."))
|
||||
|
||||
related_courses = frappe.get_all(
|
||||
"Related Courses", {"parent": course.name}, ["course"]
|
||||
)
|
||||
for csr in related_courses:
|
||||
csr.update(
|
||||
frappe.db.get_value(
|
||||
"LMS Course",
|
||||
csr.course,
|
||||
["name", "upcoming", "title", "image"],
|
||||
as_dict=True,
|
||||
)
|
||||
)
|
||||
course.related_courses = related_courses
|
||||
|
||||
context.course = course
|
||||
membership = get_membership(course.name, frappe.session.user)
|
||||
context.course.query_parameter = (
|
||||
"?batch=" + membership.batch_old if membership and membership.batch_old else ""
|
||||
)
|
||||
context.membership = membership
|
||||
context.is_instructor = is_instructor(course.name)
|
||||
context.certificate = is_certified(course.name)
|
||||
eval_details = get_evaluation_details(course.name)
|
||||
context.eligible_for_evaluation = eval_details.eligible
|
||||
context.no_of_attempts = eval_details.no_of_attempts
|
||||
if context.course.upcoming:
|
||||
context.is_user_interested = get_user_interest(context.course.name)
|
||||
|
||||
context.metatags = {
|
||||
"title": course.title,
|
||||
"image": course.image,
|
||||
"description": course.short_introduction,
|
||||
"keywords": course.title,
|
||||
"og:type": "website",
|
||||
}
|
||||
|
||||
|
||||
def get_user_interest(course):
|
||||
return frappe.db.count(
|
||||
"LMS Course Interest", {"course": course, "user": frappe.session.user}
|
||||
)
|
||||
@@ -1,216 +0,0 @@
|
||||
{% extends "lms/templates/lms_base.html" %}
|
||||
{% block title %}
|
||||
{{ course.title if course and course.title else _("New Course") }}
|
||||
{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
<main class="common-page-style">
|
||||
{{ Header() }}
|
||||
<div class="container form-width">
|
||||
{{ CreateCourse() }}
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% macro Header() %}
|
||||
<header class="sticky">
|
||||
<div class="container form-width">
|
||||
|
||||
<div class="edit-header">
|
||||
<div class="page-title"> {{ _("Course Details") }} </div>
|
||||
|
||||
<div class="align-self-center">
|
||||
{% if course.name %}
|
||||
<a class="btn btn-default btn-sm mr-2" href="/courses/{{ course.name }}">
|
||||
{{ _("Back to Course") }}
|
||||
</a>
|
||||
|
||||
<a class="btn btn-default btn-sm mr-2" href="/courses/{{ course.name }}/outline">
|
||||
{{ _("Course Outline") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<button class="btn btn-primary btn-sm btn-save-course">
|
||||
{{ _("Save") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</header>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro CreateCourse() %}
|
||||
<div class="field-parent">
|
||||
<div class="field-group">
|
||||
<div>
|
||||
<div class="field-label reqd">
|
||||
{{ _("Title") }}
|
||||
</div>
|
||||
<div class="field-description">
|
||||
{{ _("Something Short and Concise") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="">
|
||||
<input id="title" type="text" class="field-input" {% if course.title %} data-course="{{ course.name }}" value="{{ course.title }}" {% endif %}>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<div>
|
||||
<div class="field-label reqd">
|
||||
{{ _("Short Introduction") }}
|
||||
</div>
|
||||
<div class="field-description">
|
||||
{{ _("A one line brief description") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="">
|
||||
<input id="intro" type="text" class="field-input" {% if course.short_introduction %} value="{{ course.short_introduction }}" {% endif %}>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<div>
|
||||
<div class="field-label reqd">
|
||||
{{ _("Course Description") }}
|
||||
</div>
|
||||
<div class="field-description">
|
||||
{{ _("Add a detailed description to provide more information about your course.") }}
|
||||
</div>
|
||||
</div>
|
||||
<div id="description" class=""></div>
|
||||
{% if course.description %}
|
||||
<div id="description-data" class="hide">
|
||||
{{ course.description }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<div>
|
||||
<div class="field-label">
|
||||
{{ _("Preview Video ID") }}
|
||||
</div>
|
||||
<div class="field-description">
|
||||
{{ _("Enter the Preview Video ID. The ID is the part of the URL after <code>watch?v=</code>. For example, if the URL is <code>https://www.youtube.com/watch?v=QH2-TGUlwu4</code>, the ID is <code>QH2-TGUlwu4</code>") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="">
|
||||
<input id="video-link" type="text" class="field-input" {% if course.video_link %} value="{{ course.video_link }}" {% endif %}>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<div>
|
||||
<div class="field-label">
|
||||
{{ _("Tags") }}
|
||||
</div>
|
||||
<div class="field-description">
|
||||
{{ _("Tags act as search keywords. They also appear on the Course Card and Course Detail page") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="tags field-input">
|
||||
{% for tag in get_tags(course.name) %}
|
||||
<button class="btn btn-secondary btn-sm mr-2 text-uppercase">
|
||||
{{ tag }}
|
||||
<span class="btn-remove">
|
||||
<svg class="icon icon-sm">
|
||||
<use class="" href="#icon-close"></use>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
{% endfor %}
|
||||
<input type="text" class="invisible-input" id="tags-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if is_moderator %}
|
||||
<div class="field-group vertically-center">
|
||||
<label for="published" class="vertically-center mb-0">
|
||||
<input type="checkbox" id="published" {% if course.published %} checked {% endif %}>
|
||||
{{ _("Published") }}
|
||||
</label>
|
||||
<label for="upcoming" class="vertically-center mb-0 ml-20">
|
||||
<input type="checkbox" id="upcoming" {% if course.upcoming %} checked {% endif %}>
|
||||
{{ _("Upcoming") }}
|
||||
</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="field-group">
|
||||
<div>
|
||||
<div class="field-label">
|
||||
{{ _("Course Image") }}
|
||||
</div>
|
||||
<div class="field-description">
|
||||
{{ _("Image will appear on the Course Card") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="">
|
||||
<button class="btn btn-secondary btn-sm btn-upload mt-2">
|
||||
{{ _("Upload Image") }}
|
||||
</button>
|
||||
</div>
|
||||
<img {% if course.image %} class="image-preview" src="{{ course.image }}" {% endif %}>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<label for="paid_course" class="vertically-center mb-0">
|
||||
<input type="checkbox" id="paid-course" {% if course.paid_course %} checked {% endif %}>
|
||||
{{ _("Paid Course") }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="field-group price-field {% if not course.paid_course %} hide {% endif %}">
|
||||
<div class="field-label {% if course.paid_course %} reqd {% endif %}">
|
||||
{{ _("Course Price") }}
|
||||
</div>
|
||||
<div class="field-description">
|
||||
{{ _("The price of this course.") }}
|
||||
</div>
|
||||
<div class="">
|
||||
<input id="course-price" type="number" class="field-input" {% if course.course_price %} value="{{ course.course_price }}" {% endif %}>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-group price-field {% if not course.paid_course %} hide {% endif %}">
|
||||
<div class="field-label {% if course.paid_course %} reqd {% endif %}">
|
||||
{{ _("Currency") }}
|
||||
</div>
|
||||
<div class="field-description">
|
||||
{{ _("The currency in which users will pay for this course.") }}
|
||||
</div>
|
||||
|
||||
<select class="field-input" id="currency">
|
||||
<option></option>
|
||||
{% for currency in currencies %}
|
||||
<option value="{{ currency }}" {% if currency == course.currency %} selected {% endif %}>
|
||||
{{ currency}}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<div class="field-label">
|
||||
{{ _("Instructor") }}
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
{{ widgets.Avatar(member=member, avatar_class="avatar-medium") }}
|
||||
<span class="ml-2">
|
||||
{{ member.full_name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{%- block script %}
|
||||
{{ super() }}
|
||||
{{ include_script('controls.bundle.js') }}
|
||||
{% endblock %}
|
||||
@@ -1,191 +0,0 @@
|
||||
frappe.ready(() => {
|
||||
frappe.telemetry.capture("on_course_creation_page", "lms");
|
||||
$(".tags").click((e) => {
|
||||
e.preventDefault();
|
||||
$("#tags-input").focus();
|
||||
});
|
||||
|
||||
$("#tags-input").focusout((e) => {
|
||||
create_tag(e);
|
||||
});
|
||||
|
||||
$("#tags-input").focus((e) => {
|
||||
$(e.target).keypress((e) => {
|
||||
if (e.which == 13 || e.which == 44) {
|
||||
create_tag(e);
|
||||
setTimeout(() => {
|
||||
$("#tags-input").val("");
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$(document).on("click", ".btn-remove", (e) => {
|
||||
$(e.target).parent().parent().remove();
|
||||
});
|
||||
|
||||
$(".btn-save-course").click((e) => {
|
||||
save_course(e);
|
||||
});
|
||||
|
||||
if ($("#description").length) {
|
||||
make_editor();
|
||||
}
|
||||
|
||||
$(".field-input").focusout((e) => {
|
||||
if ($(e.currentTarget).siblings(".error-message")) {
|
||||
$(e.currentTarget).siblings(".error-message").remove();
|
||||
}
|
||||
});
|
||||
|
||||
$(".btn-upload").click((e) => {
|
||||
upload_file(e);
|
||||
});
|
||||
|
||||
$("#paid-course").click((e) => {
|
||||
setup_paid_course(e);
|
||||
});
|
||||
});
|
||||
|
||||
const create_tag = (e) => {
|
||||
if ($(e.target).val() == "") {
|
||||
return;
|
||||
}
|
||||
|
||||
let tag_value = $(e.target)
|
||||
.val()
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
let tag = `<button class="btn btn-secondary btn-sm mr-2 text-uppercase">
|
||||
${tag_value}
|
||||
<span class="btn-remove">
|
||||
<svg class="icon icon-sm">
|
||||
<use class="" href="#icon-close"></use>
|
||||
</svg>
|
||||
</span>
|
||||
</button>`;
|
||||
$(tag).insertBefore("#tags-input");
|
||||
$(e.target).val("");
|
||||
};
|
||||
|
||||
const save_course = (e) => {
|
||||
validate_mandatory();
|
||||
let tags = $(".tags button")
|
||||
.map((i, el) => $(el).text().trim())
|
||||
.get();
|
||||
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_course.lms_course.save_course",
|
||||
args: {
|
||||
tags: tags.join(", "),
|
||||
title: $("#title").val(),
|
||||
short_introduction: $("#intro").val(),
|
||||
video_link: $("#video-link").val(),
|
||||
image: $(".image-preview").attr("src"),
|
||||
description: this.description.fields_dict["description"].value,
|
||||
course: $("#title").data("course")
|
||||
? $("#title").data("course")
|
||||
: "",
|
||||
published: $("#published").prop("checked") ? 1 : 0,
|
||||
upcoming: $("#upcoming").prop("checked") ? 1 : 0,
|
||||
paid_course: $("#paid-course").prop("checked") ? 1 : 0,
|
||||
course_price: $("#course-price").val(),
|
||||
currency: $("#currency").val(),
|
||||
},
|
||||
callback: (data) => {
|
||||
frappe.show_alert({
|
||||
message: __("Saved"),
|
||||
indicator: "green",
|
||||
});
|
||||
setTimeout(() => {
|
||||
window.location.href = `/courses/${data.message}/edit`;
|
||||
}, 1000);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const validate_mandatory = () => {
|
||||
let fields = $(".field-label.reqd");
|
||||
fields.each((i, el) => {
|
||||
let input = $(el).closest(".field-group").find(".field-input");
|
||||
if (input.length && input.val().trim() == "") {
|
||||
if (input.siblings(".error-message").length == 0) {
|
||||
scroll_to_element(input);
|
||||
throw_error(el, input);
|
||||
}
|
||||
throw `${$(el).text().trim()} is mandatory`;
|
||||
}
|
||||
});
|
||||
|
||||
if (!strip_html(this.description.fields_dict["description"].value)) {
|
||||
scroll_to_element("#description");
|
||||
throw_error(
|
||||
"#description",
|
||||
this.description.fields_dict["description"].parent
|
||||
);
|
||||
throw "Description is mandatory";
|
||||
}
|
||||
};
|
||||
|
||||
const throw_error = (el, input) => {
|
||||
let error = document.createElement("p");
|
||||
error.classList.add("error-message");
|
||||
error.innerText = `Please enter a ${$(el).text().trim()}`;
|
||||
$(error).insertAfter($(input));
|
||||
};
|
||||
|
||||
const scroll_to_element = (element) => {
|
||||
if ($(element).length) {
|
||||
$([document.documentElement, document.body]).animate(
|
||||
{
|
||||
scrollTop: $(element).offset().top - 100,
|
||||
},
|
||||
1000
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const make_editor = () => {
|
||||
this.description = new frappe.ui.FieldGroup({
|
||||
fields: [
|
||||
{
|
||||
fieldname: "description",
|
||||
fieldtype: "Text Editor",
|
||||
default: $("#description-data").html(),
|
||||
},
|
||||
],
|
||||
body: $("#description").get(0),
|
||||
});
|
||||
this.description.make();
|
||||
$("#description .form-section:last").removeClass("empty-section");
|
||||
$("#description .frappe-control").removeClass("hide-control");
|
||||
$("#description .form-column").addClass("p-0");
|
||||
};
|
||||
|
||||
const upload_file = (e) => {
|
||||
new frappe.ui.FileUploader({
|
||||
disable_file_browser: true,
|
||||
folder: "Home/Attachments",
|
||||
make_attachments_public: true,
|
||||
restrictions: {
|
||||
allowed_file_types: ["image/*"],
|
||||
},
|
||||
on_success: (file_doc) => {
|
||||
$(e.target)
|
||||
.parent()
|
||||
.siblings("img")
|
||||
.addClass("image-preview")
|
||||
.attr("src", file_doc.file_url);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const setup_paid_course = (e) => {
|
||||
if ($(e.target).prop("checked")) {
|
||||
$(".price-field").removeClass("hide");
|
||||
$(".price-field").find(".field-label").addClass("reqd");
|
||||
} else {
|
||||
$(".price-field").addClass("hide");
|
||||
$(".price-field").find(".field-label").removeClass("reqd");
|
||||
}
|
||||
};
|
||||
@@ -1,61 +0,0 @@
|
||||
import frappe
|
||||
from lms.lms.utils import (
|
||||
redirect_to_courses_list,
|
||||
can_create_courses,
|
||||
has_course_moderator_role,
|
||||
has_course_instructor_role,
|
||||
)
|
||||
from frappe import _
|
||||
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
|
||||
try:
|
||||
course_name = frappe.form_dict["course"]
|
||||
except KeyError:
|
||||
redirect_to_courses_list()
|
||||
|
||||
if not can_create_courses(course_name) and course_name != "new-course":
|
||||
message = "You do not have permission to access this page."
|
||||
if frappe.session.user == "Guest":
|
||||
message = "Please login to access this page."
|
||||
|
||||
raise frappe.PermissionError(_(message))
|
||||
|
||||
if course_name == "new-course" and has_course_instructor_role():
|
||||
context.course = frappe._dict()
|
||||
context.course.edit_mode = True
|
||||
context.membership = None
|
||||
elif not frappe.db.exists("LMS Course", course_name):
|
||||
redirect_to_courses_list()
|
||||
else:
|
||||
set_course_context(context, course_name)
|
||||
|
||||
context.is_moderator = has_course_moderator_role()
|
||||
context.member = frappe.db.get_value(
|
||||
"User", frappe.session.user, ["full_name", "username"], as_dict=True
|
||||
)
|
||||
context.currencies = frappe.get_all("Currency", {"enabled": 1}, pluck="currency_name")
|
||||
|
||||
|
||||
def set_course_context(context, course_name):
|
||||
fields = [
|
||||
"name",
|
||||
"title",
|
||||
"short_introduction",
|
||||
"description",
|
||||
"image",
|
||||
"published",
|
||||
"upcoming",
|
||||
"disable_self_learning",
|
||||
"status",
|
||||
"video_link",
|
||||
"enable_certification",
|
||||
"grant_certificate_after",
|
||||
"paid_course",
|
||||
"course_price",
|
||||
"currency",
|
||||
"max_attempts",
|
||||
]
|
||||
context.course = frappe.db.get_value("LMS Course", course_name, fields, as_dict=True)
|
||||
@@ -1,171 +0,0 @@
|
||||
{% extends "lms/templates/lms_base.html" %}
|
||||
|
||||
|
||||
{% block title %}
|
||||
{{ 'Courses' }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block page_content %}
|
||||
<div class="common-page-style pt-8">
|
||||
<div class="container">
|
||||
{% if restriction %}
|
||||
{% set profile_link = "<a href='/edit-profile'> profile </a>" %}
|
||||
<div class="empty-state">
|
||||
<div class="course-home-headings text-center mb-0" style="color: inherit;">
|
||||
{{ _("You haven't completed your profile.") }}
|
||||
</div>
|
||||
<p class="small text-center">
|
||||
{{ _("Complete your {0} to access the courses.").format(profile_link) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
|
||||
{% include "lms/templates/search_course/search_course.html" %}
|
||||
|
||||
<div class="course-list-menu">
|
||||
|
||||
<select class="lms-menu" id="course-filter">
|
||||
<option disabled value="">
|
||||
{{ _("Sort By") }}
|
||||
</option>
|
||||
<option selected value="enrollment">
|
||||
{{ _("Most Popular") }}
|
||||
</option>
|
||||
<option value="rating">
|
||||
{{ _("Highest Rated") }}
|
||||
</option>
|
||||
<option value="creation">
|
||||
{{ _("Newest") }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<div class="course-list-buttons">
|
||||
{% if frappe.session.user != "Guest" %}
|
||||
<a class="btn btn-default btn-sm" href="/users">
|
||||
{{ _("My Profile") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<a class="btn btn-default btn-sm" id="open-search">
|
||||
{{ _("Search") }} (Ctrl + k)
|
||||
</a>
|
||||
|
||||
{% if show_creators_section %}
|
||||
<a class="btn btn-primary btn-sm" href="/courses/new-course/edit">
|
||||
{{ _("Create a Course") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="page-title mb-6">
|
||||
{{ _("All Courses") }}
|
||||
</div>
|
||||
|
||||
<ul class="nav lms-nav" id="courses-tab">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-toggle="tab" href="#live">
|
||||
{{ _("Live") }}
|
||||
<span class="course-list-count">
|
||||
{{ live_courses | length }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#upcoming">
|
||||
{{ _("Upcoming") }}
|
||||
<span class="course-list-count">
|
||||
{{ upcoming_courses | length }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% if frappe.session.user != "Guest" %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#courses-enrolled">
|
||||
{{ _("Enrolled") }}
|
||||
<span class="course-list-count">
|
||||
{{ enrolled_courses | length }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if show_creators_section %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#courses-created">
|
||||
{{ _("Created") }}
|
||||
<span class="course-list-count">
|
||||
{{ created_courses | length }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if show_review_section %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#courses-under-review">
|
||||
{{ _("Under Review") }}
|
||||
<span class="course-list-count">
|
||||
{{ review_courses | length }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
</ul>
|
||||
|
||||
<div class="border-bottom mb-4"></div>
|
||||
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="live" role="tabpanel" aria-labelledby="live">
|
||||
{% set courses = live_courses %}
|
||||
{% set title = _("Live Courses") %}
|
||||
{% set classes = "live-courses" %}
|
||||
{% include "lms/templates/course_list.html" %}
|
||||
</div>
|
||||
|
||||
<div class="tab-pane" id="upcoming" role="tabpanel" aria-labelledby="upcoming">
|
||||
{% set courses = upcoming_courses %}
|
||||
{% set title = _("Upcoming Courses") %}
|
||||
{% set classes = "upcoming-courses" %}
|
||||
{% include "lms/templates/course_list.html" %}
|
||||
</div>
|
||||
|
||||
{% if frappe.session.user != "Guest" %}
|
||||
<div class="tab-pane fade" id="courses-enrolled" role="tabpanel" aria-labelledby="courses-enrolled">
|
||||
{% set courses = enrolled_courses %}
|
||||
{% set title = _("Enrolled Courses") %}
|
||||
{% set classes = "enrolled-courses" %}
|
||||
{% include "lms/templates/course_list.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if show_creators_section %}
|
||||
<div class="tab-pane fade" id="courses-created" role="tabpanel" aria-labelledby="courses-created">
|
||||
{% set courses = created_courses %}
|
||||
{% set title = _("Created Courses") %}
|
||||
{% set classes = "created-courses" %}
|
||||
{% include "lms/templates/course_list.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if show_review_section %}
|
||||
<div class="tab-pane fade" id="courses-under-review" role="tabpanel" aria-labelledby="courses-under-review">
|
||||
{% set courses = review_courses %}
|
||||
{% set title = _("Review Courses") %}
|
||||
{% set classes = "review-courses" %}
|
||||
{% include "lms/templates/course_list.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,90 +0,0 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from lms.lms.utils import (
|
||||
check_profile_restriction,
|
||||
get_restriction_details,
|
||||
has_course_moderator_role,
|
||||
get_courses_under_review,
|
||||
get_average_rating,
|
||||
check_multicurrency,
|
||||
has_course_instructor_role,
|
||||
)
|
||||
from lms.overrides.user import get_enrolled_courses, get_authored_courses
|
||||
from frappe.utils.telemetry import capture
|
||||
|
||||
|
||||
def get_context(context):
|
||||
capture("active_site", "lms")
|
||||
context.no_cache = 1
|
||||
context.live_courses, context.upcoming_courses = get_courses()
|
||||
context.enrolled_courses = (
|
||||
get_enrolled_courses()["in_progress"] + get_enrolled_courses()["completed"]
|
||||
)
|
||||
context.created_courses = get_authored_courses(None, False)
|
||||
context.review_courses = get_courses_under_review()
|
||||
context.restriction = check_profile_restriction()
|
||||
|
||||
portal_course_creation = frappe.db.get_single_value(
|
||||
"LMS Settings", "portal_course_creation"
|
||||
)
|
||||
context.show_creators_section = (
|
||||
True
|
||||
if portal_course_creation == "Anyone"
|
||||
or has_course_moderator_role()
|
||||
or has_course_instructor_role()
|
||||
else False
|
||||
)
|
||||
context.show_review_section = (
|
||||
has_course_moderator_role() and frappe.session.user != "Guest"
|
||||
)
|
||||
|
||||
if context.restriction:
|
||||
context.restriction_details = get_restriction_details()
|
||||
|
||||
context.metatags = {
|
||||
"title": _("Course List"),
|
||||
"image": frappe.db.get_single_value("Website Settings", "banner_image"),
|
||||
"description": "This page lists all the courses published on our website",
|
||||
"keywords": "All Courses, Courses, Learn",
|
||||
}
|
||||
|
||||
|
||||
def get_courses():
|
||||
courses = frappe.get_all(
|
||||
"LMS Course",
|
||||
filters={"published": True},
|
||||
fields=[
|
||||
"name",
|
||||
"upcoming",
|
||||
"title",
|
||||
"short_introduction",
|
||||
"image",
|
||||
"paid_course",
|
||||
"course_price",
|
||||
"currency",
|
||||
"creation",
|
||||
"amount_usd",
|
||||
],
|
||||
)
|
||||
|
||||
live_courses, upcoming_courses = [], []
|
||||
for course in courses:
|
||||
course.enrollment_count = frappe.db.count(
|
||||
"LMS Enrollment", {"course": course.name, "member_type": "Student"}
|
||||
)
|
||||
|
||||
if course.course_price:
|
||||
course.course_price, course.currency = check_multicurrency(
|
||||
course.course_price, course.currency, None, course.amount_usd
|
||||
)
|
||||
|
||||
course.avg_rating = get_average_rating(course.name) or 0
|
||||
if course.upcoming:
|
||||
upcoming_courses.append(course)
|
||||
else:
|
||||
live_courses.append(course)
|
||||
|
||||
live_courses.sort(key=lambda x: x.enrollment_count, reverse=True)
|
||||
upcoming_courses.sort(key=lambda x: x.enrollment_count, reverse=True)
|
||||
|
||||
return live_courses, upcoming_courses
|
||||
@@ -1,194 +0,0 @@
|
||||
{% extends "lms/templates/lms_base.html" %}
|
||||
|
||||
|
||||
{% block title %}
|
||||
{{ _("Outline") }} - {{ course.title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
<main class="common-page-style">
|
||||
{{ Header() }}
|
||||
<div class="container form-width" id="course-outline" {% if course.name %} data-course="{{ course.name }}" {% endif %}>
|
||||
{% if chapters | length %}
|
||||
{{ Outline(chapters) }}
|
||||
{% else %}
|
||||
{{ EmptyState() }}
|
||||
{% endif %}
|
||||
{{ CreateChapter() }}
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% macro Header() %}
|
||||
<header class="sticky">
|
||||
<div class="container form-width">
|
||||
|
||||
<div class="edit-header">
|
||||
<div>
|
||||
<div class="page-title">
|
||||
{{ course.title if course.name else _("Course Outline") }}
|
||||
</div>
|
||||
<div class="vertically-center small">
|
||||
<a class="dark-links" href="/courses/{{ course.name }}/edit">{{ _("Course Details") }}</a>
|
||||
<img class="icon icon-sm mr-0" src="/assets/lms/icons/chevron-right.svg">
|
||||
<span class="breadcrumb-destination">{{ _("Course Outline") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary btn-sm btn-add-chapter align-self-center">
|
||||
<span>
|
||||
{{ _("Add Chapter") }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</header>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro Outline(chapters) %}
|
||||
{% if chapters %}
|
||||
<div class="chapter-dropzone">
|
||||
{% for chapter in chapters %}
|
||||
{% set chapter_index = loop.index %}
|
||||
{% set lessons = get_lessons(course.name, chapter) %}
|
||||
<div class="common-card-style column-card chapter-container p-4 my-5" data-chapter="{{ chapter.name }}" data-idx="{{ loop.index }}">
|
||||
<div class="level">
|
||||
<div class="drag-handle">
|
||||
<svg class="icon icon-xs level-item mr-2">
|
||||
<use class="" href="#icon-drag"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="bold-heading chapters-title">
|
||||
{{ chapter.title }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if chapter.description %}
|
||||
<div class="mb-2 ml-5 chapter-description">
|
||||
{{ chapter.description }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if lessons | length %}
|
||||
<div class="lesson-dropzone">
|
||||
{% for lesson in lessons %}
|
||||
<div class="outline-lesson level" data-lesson="{{ lesson.name }}">
|
||||
<div class="drag-handle">
|
||||
<svg class="icon icon-xs level-item mr-2">
|
||||
<use class="" href="#icon-drag"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<a class="clickable" href="/courses/{{ course.name }}/learn/{{ chapter_index }}.{{ loop.index }}/edit">
|
||||
{{ lesson.title }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<div class="align-self-start mt-4">
|
||||
<a class="btn btn-secondary btn-sm" href="/courses/{{ course.name }}/learn/{{ loop.index }}.{{ lessons | length + 1 }}/edit">
|
||||
<span>
|
||||
{{ _("Add Lesson") }}
|
||||
</span>
|
||||
</a>
|
||||
<button class="btn btn-secondary btn-sm ml-2 edit-chapter">
|
||||
<span>
|
||||
{{ _("Edit") }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro CreateChapter() %}
|
||||
<div class="modal fade chapter-modal" id="chapter-modal" tabindex="-1" role="dialog"
|
||||
aria-labelledby="exampleModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title">{{ _("New Chapter") }}</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="modal-body">
|
||||
<article id="create-chapter">
|
||||
<div class="chapter-container">
|
||||
|
||||
<div class="field-group">
|
||||
<div>
|
||||
<div class="field-label reqd">
|
||||
{{ _("Chapter Title") }}
|
||||
</div>
|
||||
<div class="field-description">
|
||||
{{ _("Something Short and Concise") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="">
|
||||
<input id="chapter-title" type="text" class="field-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<div>
|
||||
<div class="field-label">
|
||||
{{ _("Short Description") }}
|
||||
</div>
|
||||
<div class="field-description">
|
||||
{{ _("A brief description about this chapter.") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="">
|
||||
<input id="chapter-description" type="text" class="field-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary btn-sm mr-2" data-dismiss="modal" aria-label="Close">
|
||||
{{ _("Discard") }}
|
||||
</button>
|
||||
|
||||
<button class="btn btn-primary btn-sm align-self-start" id="save-chapter">
|
||||
{{ _("Save") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro EmptyState() %}
|
||||
<article class="empty-state my-5">
|
||||
<div class="text-center">
|
||||
<div class="bold-heading">
|
||||
{{ _("You have not added any chapter yet") }}
|
||||
</div>
|
||||
<div>
|
||||
{{ _("Create and manage your chapters from here.") }}
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<button class="btn btn-default btn-sm btn-add-chapter">
|
||||
<span>
|
||||
{{ _("Add Chapter") }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{% endmacro %}
|
||||
@@ -1,142 +0,0 @@
|
||||
frappe.ready(() => {
|
||||
frappe.telemetry.capture("on_course_outline_page", "lms");
|
||||
$(".btn-add-chapter").click((e) => {
|
||||
show_chapter_modal(e);
|
||||
});
|
||||
|
||||
$(".edit-chapter").click((e) => {
|
||||
show_chapter_modal(e);
|
||||
});
|
||||
|
||||
$("#save-chapter").click((e) => {
|
||||
save_chapter(e);
|
||||
});
|
||||
|
||||
$(".lesson-dropzone").each((i, el) => {
|
||||
setSortable(el);
|
||||
});
|
||||
|
||||
$(".chapter-dropzone").each((i, el) => {
|
||||
setSortable(el);
|
||||
});
|
||||
});
|
||||
|
||||
const show_chapter_modal = (e) => {
|
||||
e.preventDefault();
|
||||
$("#chapter-modal").modal("show");
|
||||
let parent = $(e.currentTarget).closest(".chapter-container");
|
||||
if (parent) {
|
||||
$("#chapter-title").val($.trim(parent.find(".chapters-title").text()));
|
||||
$("#chapter-description").val(
|
||||
$.trim(parent.find(".chapter-description").text())
|
||||
);
|
||||
$("#chapter-modal").data("chapter", parent.data("chapter"));
|
||||
$("#chapter-modal").data("idx", parent.data("idx"));
|
||||
}
|
||||
};
|
||||
|
||||
const save_chapter = (e) => {
|
||||
validate_mandatory();
|
||||
let parent = $("#chapter-modal");
|
||||
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_course.lms_course.save_chapter",
|
||||
args: {
|
||||
course: $("#course-outline").data("course"),
|
||||
title: $("#chapter-title").val(),
|
||||
chapter_description: $("#chapter-description").val(),
|
||||
idx: parent.data("idx") || $(".chapter-container").length,
|
||||
chapter: parent.data("chapter") || null,
|
||||
},
|
||||
callback: (data) => {
|
||||
$("#chapter-modal").modal("hide");
|
||||
frappe.show_alert({
|
||||
message: __("Saved"),
|
||||
indicator: "green",
|
||||
});
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const validate_mandatory = () => {
|
||||
if (!$("#chapter-title").val()) {
|
||||
let error = $("p")
|
||||
.addClass("error-message")
|
||||
.text("Chapter title is required");
|
||||
$(error).insertAfter("#chapter-title");
|
||||
throw __("Chapter title is required");
|
||||
}
|
||||
};
|
||||
|
||||
const setSortable = (el) => {
|
||||
new Sortable(el, {
|
||||
group: "drag",
|
||||
handle: ".drag-handle",
|
||||
animation: 150,
|
||||
fallbackOnBody: true,
|
||||
swapThreshold: 0.65,
|
||||
onEnd: (e) => {
|
||||
if ($(e.item).hasClass("outline-lesson")) reorder_lesson(e);
|
||||
else reorder_chapter(e);
|
||||
},
|
||||
onMove: (e) => {
|
||||
if (
|
||||
$(e.dragged).hasClass("outline-lesson") &&
|
||||
$(e.to).hasClass("chapter-dropzone")
|
||||
)
|
||||
return false;
|
||||
if (
|
||||
$(e.dragged).hasClass("chapter-edit") &&
|
||||
$(e.to).hasClass("lesson-dropzone")
|
||||
)
|
||||
return false;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const reorder_lesson = (e) => {
|
||||
let old_chapter = $(e.from).closest(".chapter-container").data("chapter");
|
||||
let new_chapter = $(e.to).closest(".chapter-container").data("chapter");
|
||||
|
||||
if (old_chapter == new_chapter && e.oldIndex == e.newIndex) return;
|
||||
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_course.lms_course.reorder_lesson",
|
||||
args: {
|
||||
old_chapter: old_chapter,
|
||||
old_lesson_array: $(e.from)
|
||||
.children()
|
||||
.map((i, e) => $(e).data("lesson"))
|
||||
.get(),
|
||||
new_chapter: new_chapter,
|
||||
new_lesson_array: $(e.to)
|
||||
.children()
|
||||
.map((i, e) => $(e).data("lesson"))
|
||||
.get(),
|
||||
},
|
||||
callback: (data) => {
|
||||
window.location.reload();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const reorder_chapter = (e) => {
|
||||
if (e.oldIndex == e.newIndex) return;
|
||||
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_course.lms_course.reorder_chapter",
|
||||
args: {
|
||||
new_index: e.newIndex + 1,
|
||||
chapter_array: $(e.to)
|
||||
.children()
|
||||
.map((i, e) => $(e).data("chapter"))
|
||||
.get(),
|
||||
},
|
||||
callback: (data) => {
|
||||
window.location.reload();
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from lms.lms.utils import get_chapters, can_create_courses, redirect_to_courses_list
|
||||
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
course_name = frappe.form_dict["course"]
|
||||
|
||||
if not frappe.db.exists("LMS Course", course_name):
|
||||
redirect_to_courses_list()
|
||||
|
||||
if not can_create_courses(course_name):
|
||||
message = "You do not have permission to access this page."
|
||||
if frappe.session.user == "Guest":
|
||||
message = "Please login to access this page."
|
||||
|
||||
raise frappe.PermissionError(_(message))
|
||||
|
||||
context.course = frappe.db.get_value(
|
||||
"LMS Course", course_name, ["name", "title"], as_dict=True
|
||||
)
|
||||
context.chapters = get_chapters(context.course.name)
|
||||
@@ -1,58 +0,0 @@
|
||||
{% extends "lms/templates/lms_base.html" %}
|
||||
{% block title %}{{ _('Job Openings') }}{% endblock %}
|
||||
|
||||
|
||||
{% block page_content %}
|
||||
<div class="common-page-style">
|
||||
|
||||
<div class="container">
|
||||
{% if allow_posting %}
|
||||
<a class="btn btn-primary btn-sm pull-right" href="/job-opportunity?new=1">{{ _("Post a Job") }}</a>
|
||||
{% endif %}
|
||||
|
||||
<div class="course-home-headings mb-2">{{ _("{0}").format(title) }}</div>
|
||||
<div class="job-subtitle">{{ _("{0}").format(subtitle) }}</div>
|
||||
|
||||
{% if jobs | length %}
|
||||
<div class="job-cards-parent">
|
||||
{% for job in jobs %}
|
||||
<div class="common-card-style job-card">
|
||||
{% set company_logo = job.company_logo.replace(' ', '%20') %}
|
||||
<span title="{{ job.company_name}}" style="background-image: url( {{ company_logo }} );"
|
||||
class="company-logo"></span>
|
||||
<div class="job-card-info">
|
||||
<div class="card-heading">{{ _(job.job_title) }}</div>
|
||||
|
||||
<div class="job-company course-meta">
|
||||
<div class="mr-5">{{ job.company_name }}</div>
|
||||
<div class="vertically-center">
|
||||
<svg class="icon icon-sm">
|
||||
<use class="" href="#icon-location">
|
||||
</svg>
|
||||
{{ job.location }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="job-card-logo-section course-meta">
|
||||
<div class="indicator-pill green mr-5"> {{ job.type }} </div>
|
||||
<div class="text-muted">{{ frappe.utils.format_date(job.creation, "medium") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<a class="stretched-link" href="/job-openings/{{ job.name }}"></a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
<div class="empty-state">
|
||||
<img class="icon icon-xl" src="/assets/lms/icons/comment.svg">
|
||||
<div class="empty-state-text">
|
||||
<div class="empty-state-heading">{{ _("No open jobs") }}</div>
|
||||
<div class="course-meta">{{ _("There are no job openings at present.") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,13 +0,0 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def get_context(context):
|
||||
context.jobs = frappe.get_all(
|
||||
"Job Opportunity",
|
||||
{"status": "Open", "disabled": False},
|
||||
["job_title", "location", "type", "company_name", "company_logo", "name", "creation"],
|
||||
order_by="creation desc",
|
||||
)
|
||||
context.title = frappe.db.get_single_value("Job Settings", "title")
|
||||
context.subtitle = frappe.db.get_single_value("Job Settings", "subtitle")
|
||||
context.allow_posting = frappe.db.get_single_value("Job Settings", "allow_posting")
|
||||
@@ -1,102 +0,0 @@
|
||||
{% extends "lms/templates/lms_base.html" %}
|
||||
{% block title %}{{ _(job.job_title) }}{% endblock %}
|
||||
|
||||
|
||||
{% block page_content %}
|
||||
|
||||
<div class="common-page-style">
|
||||
<div class="container">
|
||||
{{ BreadCrumb(job) }}
|
||||
|
||||
<div class="common-card-style job-detail-card">
|
||||
|
||||
<div class="job-detail-header">
|
||||
{% set company_logo = job.company_logo.replace(' ', '%20') %}
|
||||
<span title="{{ job.company_name}}" style="background-image: url( {{ company_logo }} );"
|
||||
class="company-logo"></span>
|
||||
|
||||
<div class="job-card-info">
|
||||
<div class="card-heading">{{ _(job.job_title) }}</div>
|
||||
<div class="job-company course-meta">
|
||||
<div class="mr-5">{{ job.company_name }}</div>
|
||||
|
||||
<div class="vertically-center">
|
||||
<svg class="icon icon-sm">
|
||||
<use class="" href="#icon-location">
|
||||
</svg>
|
||||
{{ job.location }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="job-card-logo-section course-meta">
|
||||
<div class="indicator-pill green mr-5"> {{ job.type }} </div>
|
||||
<div class="text-muted">{{ frappe.utils.format_date(job.creation, "medium") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% set application_link = job.application_link if frappe.session.user != 'Guest' else '/login?redirect-to=/job-openings/' + job.name %}
|
||||
<div class="job-actions">
|
||||
<a class="btn btn-primary btn-sm mr-2" href="{{ application_link }}"> {{ _("Apply") }} </a>
|
||||
<div class="btn btn-default btn-sm mr-2" id="report" data-job="{{ job.name }}"> {{ _("Report") }} </div>
|
||||
{% if job.owner == frappe.session.user %}
|
||||
<a class="btn btn-defaultb btn-sm button-links" href="/job-opportunity?name={{ job.name }}"> {{ _("Edit") }} </a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="course-meta mt-10">{{ _(job.description) }}</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade report-modal" id="report-modal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
|
||||
<div class="modal-header">
|
||||
<div class="modal-title">{{ _("Report this Post") }}</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<form class="review-form" id="review-form">
|
||||
<div class="form-group">
|
||||
<div class="clearfix">
|
||||
<label class="control-label reqd" style="padding-right: 0px;">
|
||||
{{ _("Reason for Reporting") }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="control-input-wrapper">
|
||||
<div class="control-input">
|
||||
<textarea type="text" autocomplete="off" class="input-with-feedback form-control report-field"
|
||||
data-fieldtype="Text" data-fieldname="feedback_comments" spellcheck="false"></textarea>
|
||||
</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="error-field muted-text"></p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary btn-sm mr-2 pull-right" data-dismiss="modal" aria-label="Close">
|
||||
{{ _("Discard") }}
|
||||
</button>
|
||||
|
||||
<div class="btn btn-primary btn-sm pull-right" data-job="{{ job.name }}" id="submit-report">
|
||||
{{ _("Report") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% macro BreadCrumb(job) %}
|
||||
<div class="breadcrumb">
|
||||
<a class="dark-links" href="/job-openings">{{ _("Job Openings") }}</a>
|
||||
<img class="ml-1 mr-1" src="/assets/lms/icons/chevron-right.svg">
|
||||
<span class="breadcrumb-destination">{{ job.job_title }}</span>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
@@ -1,42 +0,0 @@
|
||||
frappe.ready(() => {
|
||||
$("#report").click((e) => {
|
||||
open_report_dialog(e);
|
||||
});
|
||||
|
||||
$("#submit-report").click((e) => {
|
||||
report(e);
|
||||
});
|
||||
});
|
||||
|
||||
const open_report_dialog = (e) => {
|
||||
e.preventDefault();
|
||||
if (frappe.session.user == "Guest") {
|
||||
window.location.href = `/login?redirect-to=/job-openings/${$(
|
||||
e.currentTarget
|
||||
).data("job")}`;
|
||||
return;
|
||||
}
|
||||
$("#report-modal").modal("show");
|
||||
};
|
||||
|
||||
const report = (e) => {
|
||||
frappe.call({
|
||||
method: "lms.job.doctype.job_opportunity.job_opportunity.report",
|
||||
args: {
|
||||
job: $(e.currentTarget).data("job"),
|
||||
reason: $(".report-field").val(),
|
||||
},
|
||||
callback: (data) => {
|
||||
$(".report-modal").modal("hide");
|
||||
frappe.show_alert(
|
||||
{
|
||||
message: __(
|
||||
"Thanks for informing us about this post. The admin will look into it and take an appropriate action soon."
|
||||
),
|
||||
indicator: "green",
|
||||
},
|
||||
5
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def get_context(context):
|
||||
try:
|
||||
job = frappe.form_dict["job"]
|
||||
except KeyError:
|
||||
frappe.local.flags.redirect_location = "/job-openings"
|
||||
raise frappe.Redirect
|
||||
|
||||
context.job = frappe.get_doc("Job Opportunity", job)
|
||||
|
||||
context.metatags = {
|
||||
"title": context.job.job_title,
|
||||
"image": context.job.company_logo,
|
||||
"description": f"Job Posting for {context.job.job_title} by {context.job.company_name}",
|
||||
"keywords": "Job Opening, Job Posting, Job Opportunity, Job Vacancy, Job, Vacancy, Opening, Opportunity, Vacancy",
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import frappe
|
||||
from frappe.utils.telemetry import capture
|
||||
|
||||
no_cache = 1
|
||||
|
||||
|
||||
def get_context(context):
|
||||
csrf_token = frappe.sessions.get_csrf_token()
|
||||
frappe.db.commit()
|
||||
if frappe.session.user != "Guest":
|
||||
capture("active_site", "lms")
|
||||
context.csrf_token = csrf_token
|
||||
@@ -1,13 +1,35 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# GNU GPLv3 License. See license.txt
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe.utils.telemetry import capture
|
||||
from frappe import _
|
||||
|
||||
no_cache = 1
|
||||
|
||||
|
||||
def get_context(context):
|
||||
def get_context():
|
||||
app_path = frappe.form_dict.get("app_path")
|
||||
print(app_path)
|
||||
context = frappe._dict()
|
||||
context.meta = get_meta(app_path)
|
||||
csrf_token = frappe.sessions.get_csrf_token()
|
||||
frappe.db.commit()
|
||||
context.csrf_token = csrf_token
|
||||
if frappe.session.user != "Guest":
|
||||
capture("active_site", "lms")
|
||||
return context
|
||||
|
||||
|
||||
def get_meta(app_path):
|
||||
if app_path == "courses":
|
||||
return {
|
||||
"title": _("Course List"),
|
||||
"image": frappe.db.get_single_value("Website Settings", "banner_image"),
|
||||
"description": "This page lists all the courses published on our website",
|
||||
"keywords": "All Courses, Courses, Learn",
|
||||
}
|
||||
if app_path == "job-openings":
|
||||
return {
|
||||
"title": _("Job Openings"),
|
||||
"image": frappe.db.get_single_value("Website Settings", "banner_image"),
|
||||
"description": "This page lists all the job openings published on our website",
|
||||
"keywords": "Job Openings, Jobs, Vacancies",
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
{% macro MentorsSection(mentors, is_mentor, course_name) %}
|
||||
<h3>Mentors</h3>
|
||||
{% for m in mentors %}
|
||||
<div class="instructor">
|
||||
<div class="instructor-title">{{m.full_name}}</div>
|
||||
<div class="instructor-subtitle">Mentored {{m.get_batch_count()}} batches</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if not is_mentor %}
|
||||
<div id="mentor-request" class="notice">
|
||||
Interested to become a mentor?
|
||||
|
||||
<div><a id="apply-now" data-course="{{course_name | urlencode}}" href="">Apply Now!</a></div>
|
||||
</div>
|
||||
<div id="already-applied" class="notice hide">
|
||||
You've applied to become a mentor for this course. Your request is currently under review.
|
||||
|
||||
If you are not any more interested to mentor this course, you can <a id="cancel-request" data-course="{{course_name | urlencode}}" href="">cancel your application</a>.
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
@@ -1,127 +0,0 @@
|
||||
{% macro LiveCodeEditorLarge(name, code) %}
|
||||
<div class="livecode-editor livecode-editor-large" id="editor-{{name}}">
|
||||
<div class="row">
|
||||
<div class="col-lg-8 col-md-6">
|
||||
<div class="controls">
|
||||
<button class="run">Run</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="code-editor">
|
||||
<div class="row">
|
||||
<div class="col-lg-8 col-md-6">
|
||||
<div class="code-wrapper">
|
||||
<textarea class="code">{{code}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-6 canvas-wrapper">
|
||||
<canvas width="300" height="300"></canvas>
|
||||
<pre class="output"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro LiveCodeEditor(name, code, reset_code, is_exercise=False, last_submitted=None) %}
|
||||
<div class="livecode-editor livecode-editor-inline" id="editor-{{name}}">
|
||||
<div class="row">
|
||||
<div class="col-lg-8 col-md-6">
|
||||
<div class="controls">
|
||||
<button class="run">Run</button>
|
||||
<button class="reset">Reset</button>
|
||||
{% if is_exercise %}
|
||||
<button class="submit pull-right btn-primary">Submit</button>
|
||||
{% if last_submitted %}
|
||||
<span class="pull-right" style="padding-right: 10px;"><span class="human-time" data-timestamp="{{last_submitted}}"></span></span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div style="display: none">
|
||||
<pre class="reset-code">{{reset_code}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="code-editor">
|
||||
<div class="row">
|
||||
<div class="col-lg-8 col-md-6">
|
||||
<div class="code-wrapper">
|
||||
<textarea class="code">{{code}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-6 canvas-wrapper">
|
||||
<canvas width="300" height="300"></canvas>
|
||||
<pre class="output"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
|
||||
{% macro LiveCodeEditorJS(name, code) %}
|
||||
|
||||
<script type="text/javascript" src="/assets/frappe/node_modules/moment/min/moment-with-locales.min.js"></script>
|
||||
<script type="text/javascript" src="/assets/frappe/node_modules/moment-timezone/builds/moment-timezone-with-data.min.js"></script>
|
||||
<script type="text/javascript" src="/assets/frappe/js/frappe/utils/datetime.js"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
// comment_when is failing because of this
|
||||
if (!frappe.sys_defaults) {
|
||||
frappe.sys_defaults = {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script type="text/javascript" src="{{ livecode_url }}/static/livecode.js"></script>
|
||||
<script type="text/javascript" src="/assets/lms/js/livecode-canvas.js"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
var livecodeEditors = [];
|
||||
var livecodeEditorsMap = {};
|
||||
|
||||
$(function() {
|
||||
$(".livecode-editor").each((i, e) => {
|
||||
var name = e.id.replace("editor-", "");
|
||||
var editor = new LiveCodeEditor(e, {
|
||||
base_url: "{{ livecode_url }}",
|
||||
...getLiveCodeOptions()
|
||||
})
|
||||
livecodeEditors.push(editor);
|
||||
livecodeEditorsMap[e.id] = editor;
|
||||
|
||||
$(e).find(".reset").on('click', function() {
|
||||
let code = $(e).find(".reset-code").html();
|
||||
editor.codemirror.doc.setValue(code);
|
||||
});
|
||||
|
||||
$(e).find(".submit").on('click', function() {
|
||||
let code = editor.codemirror.doc.getValue();
|
||||
console.log("submit", name, code);
|
||||
frappe.call("lms.lms.api.submit_solution", {
|
||||
"exercise": name,
|
||||
"code": code
|
||||
}).then(r => {
|
||||
if (r.message.name) {
|
||||
frappe.msgprint("Submitted successfully!");
|
||||
|
||||
let d = r.message.creation;
|
||||
$(e).find(".human-time").html(__("Submitted {0}", [comment_when(d)]));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
function updateSubmitTimes() {
|
||||
$(".human-time").each(function(i, e) {
|
||||
var d = $(e).data().timestamp;
|
||||
$(e).html(__("Submitted {0}", [comment_when(d)]));
|
||||
});
|
||||
}
|
||||
|
||||
updateSubmitTimes();
|
||||
</script>
|
||||
|
||||
{% endmacro %}
|
||||
@@ -1,46 +0,0 @@
|
||||
{% extends "lms/templates/lms_base.html" %}
|
||||
{% block title %}
|
||||
{{ _('People') }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block page_content %}
|
||||
<div class="common-page-style">
|
||||
<div class="container">
|
||||
|
||||
{% if frappe.session.user != "Guest" %}
|
||||
<input class="search pull-right" id="search-user" placeholder="{{ _('Search') }}">
|
||||
{% endif %}
|
||||
<div class="course-home-headings">{{ _("People") }} </div>
|
||||
|
||||
<div class="empty-state alert alert-dismissible hide" id="search-empty-state">
|
||||
<a href="#" class="close-search-empty-state" aria-label="close">×</a>
|
||||
<div>
|
||||
<img class="icon icon-xl" src="/assets/frappe/images/ui-states/search-empty-state.svg">
|
||||
</div>
|
||||
<div class="empty-state-text">
|
||||
<div class="empty-state-heading">
|
||||
{{ _("No results found") }}
|
||||
</div>
|
||||
<div class="course-meta">
|
||||
{{ _("Try some other keyword or explore our community") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="member-parent">
|
||||
{% for user in users %}
|
||||
{{ widgets.MemberCard(member=user, show_course_count=False, avatar_class="avatar-large") }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if user_count > users | length %}
|
||||
<div class="mt-10 d-flex justify-content-center">
|
||||
<div class="btn btn-md btn-default" id="load-more" data-start="30" data-count="{{ user_count }}">
|
||||
{{ _("Load More") }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,61 +0,0 @@
|
||||
frappe.ready(() => {
|
||||
$("#load-more").click((e) => {
|
||||
search(e);
|
||||
});
|
||||
|
||||
$(".close-search-empty-state").click((e) => {
|
||||
close_search_empty_state(e);
|
||||
});
|
||||
|
||||
$("#search-user").keyup(function () {
|
||||
let timer;
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
search.apply(this, arguments);
|
||||
}, 300);
|
||||
});
|
||||
});
|
||||
|
||||
const search = (e) => {
|
||||
$("#search-empty-state").addClass("hide");
|
||||
let start = $(e.currentTarget).data("start");
|
||||
let input = $("#search-user").val();
|
||||
if ($(e.currentTarget).prop("nodeName") == "INPUT") start = 0;
|
||||
|
||||
frappe.call({
|
||||
method: "lms.overrides.user.search_users",
|
||||
args: {
|
||||
start: start,
|
||||
text: input,
|
||||
},
|
||||
callback: (data) => {
|
||||
if ($(e.currentTarget).prop("nodeName") == "INPUT")
|
||||
$(".member-parent").empty();
|
||||
|
||||
if (data.message.user_details.length)
|
||||
$("#load-more").removeClass("hide");
|
||||
else $("#search-empty-state").removeClass("hide");
|
||||
|
||||
let user_details = data.message.user_details;
|
||||
user_details
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/"/g, """);
|
||||
$(".member-parent").append(user_details);
|
||||
update_load_more_state(data);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const close_search_empty_state = (e) => {
|
||||
$("#search-empty-state").addClass("hide");
|
||||
$("#search-user").val("").keyup();
|
||||
};
|
||||
|
||||
const update_load_more_state = (data) => {
|
||||
$("#load-more").data("start", data.message.start);
|
||||
$("#load-more").data("count", data.message.count);
|
||||
if ($(".member-card").length == $("#load-more").data("count")) {
|
||||
$("#load-more").addClass("hide");
|
||||
}
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def get_context(context):
|
||||
context.user_count = frappe.db.count("User", {"enabled": True})
|
||||
context.users = frappe.get_all(
|
||||
"User",
|
||||
filters={"enabled": True},
|
||||
fields=["name", "username", "full_name", "user_image", "headline", "looking_for_job"],
|
||||
start=0,
|
||||
page_length=24,
|
||||
order_by="creation desc",
|
||||
)
|
||||
@@ -1,424 +0,0 @@
|
||||
{% extends "templates/base.html" %}
|
||||
{% block head_include %}
|
||||
<meta name="description" content="{{ member.full_name }}" />
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div class="common-page-style profile-page">
|
||||
{{ ProfileBanner(member) }}
|
||||
<div class="profile-page-body">
|
||||
<div class="container">
|
||||
|
||||
<ul class="nav lms-nav" id="courses-tab">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-toggle="tab" href="#profile">
|
||||
{{ _("Profile") }}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% if not read_only %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#courses-enrolled">
|
||||
{{ _("Courses Enrolled") }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if courses_created | length %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#courses-created">
|
||||
{{ _("Courses Created") }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if certificates | length %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#certificates">
|
||||
{{ _("Certificates") }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if not read_only %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#notifications">{{ _("Notifications") }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if has_course_moderator_role() %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#settings">
|
||||
{{ _("Settings") }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
</ul>
|
||||
|
||||
<div class="border-bottom mb-4"></div>
|
||||
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="profile" role="tabpanel" aria-labelledby="profile">
|
||||
<div class="">
|
||||
{{ About(member) }}
|
||||
{{ WorkDetails(member) }}
|
||||
{{ EducationDetails(member) }}
|
||||
{{ ExternalCertification(member) }}
|
||||
{{ Contact(member) }}
|
||||
{{ Skills(member) }}
|
||||
{{ CareerPreference(member) }}
|
||||
{{ ProfileTabs(profile_tabs) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if not read_only %}
|
||||
<div class="tab-pane fade" id="courses-enrolled" role="tabpanel" aria-labelledby="courses-enrolled">
|
||||
{% set courses = enrolled_courses %}
|
||||
{% set title = _("Enrolled Courses") %}
|
||||
{% set classes = "enrolled-courses" %}
|
||||
{% include "lms/templates/course_list.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if courses_created | length %}
|
||||
<div class="tab-pane fade" id="courses-created" role="tabpanel" aria-labelledby="courses-created">
|
||||
{% set courses = courses_created %}
|
||||
{% set title = _("Created Courses") %}
|
||||
{% set classes = "created-courses" %}
|
||||
{% include "lms/templates/course_list.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if certificates | length %}
|
||||
<div class="tab-pane fade" id="certificates" role="tabpanel" aria-labelledby="certificates">
|
||||
{% include "lms/templates/certificates_section.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if not read_only %}
|
||||
<div class="tab-pane" id="notifications" role="tabpanel" aria-labelledby="notifications">
|
||||
{% include "lms/templates/notifications.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="tab-pane fade" id="settings" role="tabpanel" aria-labelledby="settings">
|
||||
{{ RoleSettings(member) }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
<!-- Banner -->
|
||||
{% macro ProfileBanner(member) %}
|
||||
{% set cover_image = member.cover_image if member.cover_image else "/assets/lms/images/profile-banner.png" %}
|
||||
{% set enrollment = get_course_membership(member.name, member_type="Student") | length %}
|
||||
{% set enrollment_suffix = _("Courses") if enrollment > 1 else _("Course") %}
|
||||
|
||||
<div class="container">
|
||||
<div class="profile-banner" style="background-image: url({{ cover_image | urlencode }})">
|
||||
<div class="profile-avatar">
|
||||
{{ widgets.Avatar(member=member, avatar_class="avatar-xl") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="profile-info">
|
||||
<div class="profile-name-section">
|
||||
<div class="profile-name" data-name="{{ member.name }}"> {{ member.full_name }} </div>
|
||||
|
||||
{% if courses_created | length %}
|
||||
<div class="creator-badge"> {{ _("Creator") }} </div>
|
||||
{% endif %}
|
||||
|
||||
{% if member.looking_for_job %}
|
||||
<div class="creator-badge"> {{ _("Open Network") }} </div>
|
||||
{% endif %}
|
||||
|
||||
{% if frappe.session.user == member.email %}
|
||||
<div class="ml-auto mt-1">
|
||||
<a class="btn btn-secondary btn-sm" href="/courses"> {{ _("Course List") }} </a>
|
||||
<a class="btn btn-secondary btn-sm ml-2" href="/edit-profile/{{ member.email }}/edit"> {{ _("Edit Profile") }} </a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="profile-meta">
|
||||
{% if member.headline %}
|
||||
<div class="course-meta mr-3"> {{ member.headline }} </div>
|
||||
{% endif %}
|
||||
|
||||
{% if enrollment %}
|
||||
<div class="course-meta">
|
||||
<img src="/assets/lms/icons/book_plain.svg">
|
||||
{{ enrollment }} {{ enrollment_suffix }} {{ _("taken") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Courses Mentored -->
|
||||
{% macro CoursesMentored(member, read_only) %}
|
||||
{% if member.get_mentored_courses() | length %}
|
||||
<div class="profile-courses">
|
||||
<div class="page-title"> {{ _("Courses Mentored") }} </div>
|
||||
<div class="cards-parent">
|
||||
{% for course in member.get_mentored_courses() %}
|
||||
{{ widgets.CourseCard(course=course, read_only=read_only) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Profile Tabs Extension -->
|
||||
{% macro ProfileTabs(profile_tabs) %}
|
||||
<div>
|
||||
{% for tab in profile_tabs %}
|
||||
{% set slug = title.lower().replace(" ", "-") %}
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade py-4 show active" role="tabpanel" id="slug">
|
||||
{{ tab.render() }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Role Settings -->
|
||||
{% macro RoleSettings(member) %}
|
||||
{% if has_course_moderator_role() %}
|
||||
<div class="">
|
||||
<div class="">
|
||||
<div class="page-title mb-2"> {{ _("Role Settings") }} </div>
|
||||
<div class="d-flex">
|
||||
<label class="role">
|
||||
<input type="checkbox" id="course-creator" data-role="Course Creator"
|
||||
{% if has_course_instructor_role(member.name) %} checked {% endif %}>
|
||||
{{ _("Course Creator") }}
|
||||
</label>
|
||||
<label class="role">
|
||||
<input type="checkbox" id="moderator" data-role="Moderator"
|
||||
{% if has_course_moderator_role(member.name) %} checked {% endif %}>
|
||||
{{ _("Moderator") }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- About Section -->
|
||||
{% macro About(member) %}
|
||||
<div class="description">
|
||||
{% if member.bio %}
|
||||
{{ member.bio }}
|
||||
{% else %}
|
||||
{{ _("Hey, my name is ") }} {{ member.full_name }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Work Preference -->
|
||||
{% macro WorkPreference(member) %}
|
||||
<div class="page-title mt-10"> {{ _("Work Preference") }} </div>
|
||||
<div> {{ member.attire }} </div>
|
||||
<div> {{ member.collaboration }} </div>
|
||||
<div> {{ member.role }} </div>
|
||||
<div> {{ member.location_preference }} </div>
|
||||
<div> {{ member.time }} </div>
|
||||
<div> {{ member.company_type }} </div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Career Preference -->
|
||||
{% macro CareerPreference(member) %}
|
||||
{% if member.preferred_functions or member.preferred_industries or member.preferred_location or member.dream_companies %}
|
||||
<div class="page-title mt-10">
|
||||
{{ _("Career Preference") }}
|
||||
</div>
|
||||
<div class="profile-column-grid">
|
||||
|
||||
{% if member.preferred_functions | length %}
|
||||
<div>
|
||||
<b>{{ _("Preferred Functions:") }}</b>
|
||||
{% for function in member.preferred_functions %}
|
||||
<div class="description">{{ function.function }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if member.preferred_industries | length %}
|
||||
<div>
|
||||
<b>{{ _("Preferred Industries:") }}</b>
|
||||
{% for industry in member.preferred_industries %}
|
||||
<div class="description">{{ industry.industry }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if member.preferred_location %}
|
||||
<div>
|
||||
<b> {{ _("Preferred Locations:") }} </b>
|
||||
<div class="description"> {{ member.preferred_location }} </div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if member.dream_companies %}
|
||||
<div>
|
||||
<b> {{ _("Dream Companies:") }} </b>
|
||||
<div class="description"> {{ member.dream_companies }} </div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Contact Section -->
|
||||
{% macro Contact(member) %}
|
||||
{% if member.linkedin or member.medium or member.github %}
|
||||
<div class="page-title mt-10"> {{ _("Contact") }} </div>
|
||||
<div class="profile-column-grid">
|
||||
{% if member.linkedin %}
|
||||
{% set linkedin = member.linkedin[:-1] if member.linkedin[-1] == "/" else member.linkedin %}
|
||||
<a class="button-links description" href="{{ member.linkedin }}">
|
||||
<img src="/assets/lms/icons/linkedin.svg"> {{ linkedin.split("/")[-1] }}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if member.medium %}
|
||||
<a class="button-links description" href="{{ member.medium}}">
|
||||
<img src="/assets/lms/icons/medium.svg"> {{ member.medium.split("/")[-1] }}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if member.github %}
|
||||
<a class="button-links description" href="{{ member.github }}">
|
||||
<img src="/assets/lms/icons/github.svg"> {{ member.github.split("/")[-1] }}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Skills -->
|
||||
{% macro Skills(member) %}
|
||||
{% if member.skill | length %}
|
||||
<div class="page-title mt-10"> {{ _("Skills")}} </div>
|
||||
<div class="profile-column-grid">
|
||||
{% for skill in member.skill %}
|
||||
<div class="description"> {{ skill.skill_name }} </div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Education Details -->
|
||||
{% macro EducationDetails(member) %}
|
||||
{% if member.education %}
|
||||
<div class="page-title mt-10 mb-2"> {{ _("Education") }} </div>
|
||||
<div class="profile-grid-card">
|
||||
{% for edu in member.education %}
|
||||
<div class="column-card-row">
|
||||
<div class="bold-heading"> {{ edu.institution_name }} </div>
|
||||
<div class="profile-item"> {{ edu.degree_type }} <span></span> {{ edu.major }}
|
||||
{% if not member.hide_private %}
|
||||
<!-- {% if edu.grade_type %} {{ edu.grade_type }} {% endif %} -->
|
||||
{% if edu.grade %} <span></span> {{ edu.grade }} {% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="description">
|
||||
{% if edu.start_date %}
|
||||
{{ frappe.utils.format_date(edu.start_date, "MMM YYYY") }} -
|
||||
{% endif %}
|
||||
{{ frappe.utils.format_date(edu.end_date, "MMM YYYY") }}
|
||||
</div>
|
||||
<div class="description"> {{ edu.location }} </div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro WorkDetails(member) %}
|
||||
{% set work_details = member.work_experience + member.internship %}
|
||||
|
||||
{% if work_details | length %}
|
||||
<div class="page-title mt-10 mb-2"> {{ _("Work Experience") }} </div>
|
||||
<div class="profile-grid-card">
|
||||
|
||||
{% for work in work_details %}
|
||||
<div class="">
|
||||
<div class="bold-heading"> {{ work.title }} </div>
|
||||
<div class="profile-item"> {{ work.company }} </div>
|
||||
<div class="description">
|
||||
{{ frappe.utils.format_date(work.from_date, "MMM YYYY") }} -
|
||||
{% if work.to_date %} {{ frappe.utils.format_date(work.to_date, "MMM YYYY") }}
|
||||
{% else %} Present {% endif %}
|
||||
</div>
|
||||
|
||||
<div class="description"> {{ work.location }} </div>
|
||||
|
||||
{% if work.description %}
|
||||
<div class="profile-item">
|
||||
{{ work.description }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Certifications -->
|
||||
{% macro ExternalCertification(member) %}
|
||||
{% if member.certification %}
|
||||
<div class="page-title mt-10"> {{ _("External Certification") }} </div>
|
||||
<div class="profile-grid-card">
|
||||
{% for cert in member.certification %}
|
||||
<div class="">
|
||||
|
||||
<div class="bold-title"> {{ cert.certification_name }} </div>
|
||||
<div class="profile-item"> {{ cert.organization }} </div>
|
||||
|
||||
<div class="description">
|
||||
{{ frappe.utils.format_date(cert.issue_date, "MMM YYYY") }}
|
||||
{% if cert.expiration_date %}
|
||||
- {{ frappe.utils.format_date(cert.expiration_date, "MMM YYYY") }}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if cert.description %}
|
||||
<div class="profile-item">
|
||||
{{ cert.description }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
@@ -1,40 +0,0 @@
|
||||
frappe.ready(() => {
|
||||
make_profile_active_in_navbar();
|
||||
|
||||
$(".role").change((e) => {
|
||||
save_role(e);
|
||||
});
|
||||
});
|
||||
|
||||
const make_profile_active_in_navbar = () => {
|
||||
let member_name = $(".profile-name").data("name");
|
||||
if (member_name == frappe.session.user) {
|
||||
setTimeout(() => {
|
||||
let link_array = $(".nav-link").filter(
|
||||
(i, elem) => $(elem).text().trim() === "My Profile"
|
||||
);
|
||||
link_array.length && $(link_array[0]).addClass("active");
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
const save_role = (e) => {
|
||||
let member_name = $(".profile-name").data("name");
|
||||
let role = $(e.currentTarget).children("input");
|
||||
frappe.call({
|
||||
method: "lms.overrides.user.save_role",
|
||||
args: {
|
||||
user: member_name,
|
||||
role: role.data("role"),
|
||||
value: role.prop("checked") ? 1 : 0,
|
||||
},
|
||||
callback: (data) => {
|
||||
if (data.message) {
|
||||
frappe.show_alert({
|
||||
message: __("Saved"),
|
||||
indicator: "green",
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,58 +0,0 @@
|
||||
import frappe
|
||||
|
||||
from lms.lms.utils import get_lesson_index, get_certificates
|
||||
from lms.page_renderers import get_profile_url_prefix
|
||||
from lms.overrides.user import get_authored_courses, get_enrolled_courses
|
||||
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
|
||||
try:
|
||||
username = frappe.form_dict["username"]
|
||||
except KeyError:
|
||||
username = frappe.db.get_value("User", frappe.session.user, ["username"])
|
||||
if username:
|
||||
frappe.local.flags.redirect_location = get_profile_url_prefix() + username
|
||||
raise frappe.Redirect
|
||||
|
||||
try:
|
||||
context.member = frappe.get_doc("User", {"username": username})
|
||||
context.courses_created = get_authored_courses(context.member.name, True)
|
||||
context.enrolled_courses = (
|
||||
get_enrolled_courses()["in_progress"] + get_enrolled_courses()["completed"]
|
||||
)
|
||||
context.read_only = frappe.session.user != context.member.name
|
||||
context.certificates = get_certificates(context.member.name)
|
||||
except Exception:
|
||||
context.template = "www/404.html"
|
||||
return
|
||||
|
||||
context.profile_tabs = get_profile_tabs(context.member)
|
||||
context.notifications = get_notifications()
|
||||
|
||||
|
||||
def get_profile_tabs(user):
|
||||
"""Returns the enabled ProfileTab objects.
|
||||
|
||||
Each ProfileTab is rendered as a tab on the profile page and the
|
||||
they are specified as profile_tabs hook.
|
||||
"""
|
||||
tabs = frappe.get_hooks("profile_tabs") or []
|
||||
return [frappe.get_attr(tab)(user) for tab in tabs]
|
||||
|
||||
|
||||
def get_notifications():
|
||||
notifications = frappe.get_all(
|
||||
"Notification Log",
|
||||
{"document_type": "Course Lesson", "for_user": frappe.session.user},
|
||||
["subject", "creation", "from_user", "document_name"],
|
||||
)
|
||||
|
||||
for notification in notifications:
|
||||
course = frappe.db.get_value("Course Lesson", notification.document_name, "course")
|
||||
notification.url = (
|
||||
f"/courses/{course}/learn/{get_lesson_index(notification.document_name)}"
|
||||
)
|
||||
|
||||
return notifications
|
||||
@@ -1,33 +0,0 @@
|
||||
{% extends "templates/base.html" %}
|
||||
{% block title %}
|
||||
{{ _("Quiz Submission") }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div class="common-page-style">
|
||||
<div class="container">
|
||||
<div class="common-card-style column-card">
|
||||
<div class="course-home-headings">
|
||||
{{ _("Quiz Submission") }}
|
||||
</div>
|
||||
{% for question in questions %}
|
||||
<div>
|
||||
{{ question.question }}
|
||||
</div>
|
||||
{{ question.is_correct }}
|
||||
{{ question.answer }}
|
||||
{% for i in range(1,5) %}
|
||||
{% set num = frappe.utils.cstr(i) %}
|
||||
{% set option = question["option_" + num] %}
|
||||
{% if question["option_" + num] %}
|
||||
<div>
|
||||
{{ question["option_" + num] }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,26 +0,0 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
name = frappe.form_dict["subname"]
|
||||
|
||||
context.submission = frappe.db.get_value(
|
||||
"LMS Quiz Submission", name, ["name", "quiz"], as_dict=1
|
||||
)
|
||||
|
||||
questions = frappe.get_all(
|
||||
"LMS Quiz Result", {"parent": name}, ["question", "is_correct", "answer"]
|
||||
)
|
||||
|
||||
for question in questions:
|
||||
options = frappe.db.get_value(
|
||||
"LMS Quiz Question",
|
||||
{"question": question.question},
|
||||
["option_1", "option_2", "option_3", "option_4"],
|
||||
as_dict=1,
|
||||
)
|
||||
question.update(options)
|
||||
question.answer = question.answer.split(",")
|
||||
|
||||
context.questions = questions
|
||||
@@ -1,55 +0,0 @@
|
||||
{% extends "lms/templates/lms_base.html" %}
|
||||
{% block title %}
|
||||
{{ quiz.title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
<main class="common-page-style">
|
||||
{{ Header() }}
|
||||
<div class="container form-width">
|
||||
{{ SubmissionForm(quiz) }}
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
{% macro Header() %}
|
||||
<header class="sticky mb-5">
|
||||
<div class="container form-width">
|
||||
<div class="edit-header">
|
||||
<div>
|
||||
<div class="vertically-center">
|
||||
<div class="page-title">
|
||||
{{ quiz.title }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="vertically-center small">
|
||||
<a class="dark-links" href="/batches">
|
||||
{{ _("All Batches") }}
|
||||
</a>
|
||||
<img class="icon icon-sm mr-0" src="/assets/lms/icons/chevron-right.svg">
|
||||
<span class="breadcrumb-destination">{{ _("Quiz Submission") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="align-self-center">
|
||||
<!-- <button class="btn btn-primary btn-sm btn-save-assignment" {% if quiz.name %} data-quiz="{{ quiz.name }}" {% endif %}
|
||||
{% if submission.name %} data-submission="{{ submission.name }}" {% endif %}>
|
||||
{{ _("Save") }}
|
||||
</button> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro SubmissionForm(quiz) %}
|
||||
{% include("lms/templates/quiz/quiz.html") %}
|
||||
{% endmacro %}
|
||||
|
||||
{% block script %}
|
||||
{{ super() }}
|
||||
<script type="text/javascript">
|
||||
{% include "lms/templates/quiz/quiz.js" %}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user