feat: meta tags

This commit is contained in:
Jannat Patel
2024-03-28 16:20:53 +05:30
parent e6d3819092
commit 3525e4c90b
264 changed files with 103 additions and 96669 deletions

View File

@@ -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 %}

View File

@@ -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");
};

View File

@@ -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"))

View File

@@ -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 %}

View File

@@ -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);
},
});
};

View File

@@ -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
)

View File

@@ -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 %}

View File

@@ -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"],
)

View File

@@ -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 %}

View File

@@ -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 += `![](${url})`;
}
} 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(/&amp;/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");
};

View File

@@ -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."))

View File

@@ -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 %}

View File

@@ -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"
)

View File

@@ -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">&times;</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 %}

View File

@@ -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);
}
},
});
};

View File

@@ -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,
}

View File

@@ -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 %}

View File

@@ -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);
},
});
};

View File

@@ -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"
)

View File

@@ -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 %}

View File

@@ -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"])

View File

@@ -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 %}

View File

@@ -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);
},
});
};

View File

@@ -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"],
)

View File

@@ -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 %}

View File

@@ -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();
},
});
});
};

View File

@@ -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",
}

View File

@@ -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 %}

View File

@@ -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

View File

@@ -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 %}

View File

@@ -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();
};

View File

@@ -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)

View File

@@ -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 %}

View File

@@ -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."
)
);
};

View File

@@ -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

View File

@@ -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 %}

View File

@@ -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);
}
};

View File

@@ -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

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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
)

View File

@@ -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 %}

View File

@@ -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]

View File

@@ -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") }} &rarr;</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 %}

View File

@@ -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

View File

@@ -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 %}

View File

@@ -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")

View File

@@ -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})

View File

@@ -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 %}

View File

@@ -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

View File

@@ -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 %}

View File

@@ -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);
}
},
});
};

View File

@@ -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}
)

View File

@@ -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 %}

View File

@@ -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, "&lt;")
.replace(/>/g, "&gt;");
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");
}
};

View File

@@ -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)

View File

@@ -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 %}

View File

@@ -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

View File

@@ -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 %}

View File

@@ -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();
},
});
};

View File

@@ -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)

View File

@@ -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 %}

View File

@@ -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")

View File

@@ -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 %}

View File

@@ -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
);
},
});
};

View File

@@ -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",
}

View File

@@ -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

View File

@@ -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",
}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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">&times;</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 %}

View File

@@ -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, "&amp;")
.replace(/</g, "&lt;")
.replace(/"/g, "&quot;");
$(".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");
}
};

View File

@@ -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",
)

View File

@@ -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 %}

View File

@@ -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",
});
}
},
});
};

View File

@@ -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

View File

@@ -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 %}

View File

@@ -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

View File

@@ -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