Merge branch 'main' of https://github.com/frappe/lms into paid-courses
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
frappe.ready(() => {
|
||||
frappe.telemetry.capture("on_lesson_creation_page", "lms");
|
||||
let self = this;
|
||||
this.quiz_in_lesson = [];
|
||||
if ($("#current-lesson-content").length) {
|
||||
parse_string_to_lesson();
|
||||
}
|
||||
@@ -48,7 +49,6 @@ const setup_editor = () => {
|
||||
const parse_string_to_lesson = () => {
|
||||
let lesson_content = $("#current-lesson-content").html();
|
||||
let lesson_blocks = [];
|
||||
this.quiz_in_lesson = [];
|
||||
|
||||
lesson_content.split("\n").forEach((block) => {
|
||||
if (block.includes("{{ YouTubeVideo")) {
|
||||
|
||||
@@ -39,13 +39,14 @@
|
||||
{% endif %}
|
||||
|
||||
<div class="course-details-outline">
|
||||
{{ widgets.CourseOutline(course=course, membership=membership, lesson_page=True) }}
|
||||
{% 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) }}
|
||||
{{ LessonContent(lesson) }}
|
||||
{% if course.status == "Approved" and not course.upcoming %}
|
||||
{{ BreadCrumb(course, lesson, class_info) }}
|
||||
{{ LessonContent(lesson, class_info) }}
|
||||
{% if course.status == "Approved" and not course.upcoming and not class_info %}
|
||||
{{ Discussions() }}
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -56,19 +57,39 @@
|
||||
|
||||
|
||||
<!-- BreadCrumb -->
|
||||
{% macro BreadCrumb(course, lesson) %}
|
||||
{% macro BreadCrumb(course, lesson, class_info) %}
|
||||
<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>
|
||||
<img class="ml-1 mr-1" src="/assets/lms/icons/chevron-right.svg">
|
||||
<span class="breadcrumb-destination">{{ lesson.title if lesson.title else _("New Lesson") }}</span>
|
||||
{% if class_info %}
|
||||
<a class="dark-links" href="/courses">
|
||||
{{ _("All Classes") }}
|
||||
</a>
|
||||
<img class="ml-1 mr-1" src="/assets/lms/icons/chevron-right.svg">
|
||||
<a class="dark-links" href="/classes/{{ 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) %}
|
||||
{% macro LessonContent(lesson, class_info) %}
|
||||
{% set instructors = get_instructors(course.name) %}
|
||||
{% set is_instructor = is_instructor(course.name) %}
|
||||
|
||||
@@ -118,7 +139,9 @@
|
||||
{% endif %}
|
||||
</span>
|
||||
</a>
|
||||
<div class="ml-5 course-meta"> {{ frappe.utils.format_date(lesson.creation, "medium") }} </div>
|
||||
<div class="ml-5 course-meta">
|
||||
{{ frappe.utils.format_date(lesson.creation, "medium") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lesson Content -->
|
||||
@@ -144,8 +167,9 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if not class_info %}
|
||||
{{ pagination(prev_url, next_url) }}
|
||||
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endmacro %}
|
||||
@@ -236,13 +260,15 @@
|
||||
{% 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.") %}
|
||||
{% include "frappe/templates/discussions/discussions_section.html" %}
|
||||
<div class="pt-8">
|
||||
{% include "frappe/templates/discussions/discussions_section.html" %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Scripts -->
|
||||
{%- block script %}
|
||||
{{ super() }}
|
||||
{{ include_script('controls.bundle.js') }}
|
||||
<script type="text/javascript">
|
||||
var page_context = {{ page_context | tojson }};
|
||||
{% include "lms/templates/quiz/quiz.js" %}
|
||||
|
||||
@@ -61,8 +61,10 @@ const mark_progress = () => {
|
||||
status: status,
|
||||
},
|
||||
callback: (data) => {
|
||||
change_progress_indicators();
|
||||
show_certificate_if_course_completed(data);
|
||||
if (data.message) {
|
||||
change_progress_indicators();
|
||||
show_certificate_if_course_completed(data);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -15,6 +15,16 @@ def get_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 Class", class_name, "title"),
|
||||
}
|
||||
)
|
||||
|
||||
lesson_number = f"{chapter_index}.{lesson_index}"
|
||||
context.lesson_number = lesson_number
|
||||
context.lesson_index = lesson_index
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "lms/templates/lms_base.html" %}
|
||||
{% block title %}
|
||||
{{ _(class_info.title) }}
|
||||
{{ _(class_info.title) }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
{{ BreadCrumb(class_info) }}
|
||||
<div class="">
|
||||
{{ ClassDetails(class_info) }}
|
||||
{{ ClassSections(class_info, class_courses, class_students, published_courses) }}
|
||||
{{ ClassSections(class_info, class_courses, class_students, flow) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -20,9 +20,9 @@
|
||||
<!-- BreadCrumb -->
|
||||
{% macro BreadCrumb(class_info) %}
|
||||
<div class="breadcrumb">
|
||||
<a class="dark-links" href="/classes">{{ _("All Classes") }}</a>
|
||||
<img class="ml-1 mr-1" src="/assets/lms/icons/chevron-right.svg">
|
||||
<span class="breadcrumb-destination">{{ class_info.title }}</span>
|
||||
<a class="dark-links" href="/classes">{{ _("All Classes") }}</a>
|
||||
<img class="ml-1 mr-1" src="/assets/lms/icons/chevron-right.svg">
|
||||
<span class="breadcrumb-destination">{{ class_info.title }}</span>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
|
||||
|
||||
<!-- Class Sections -->
|
||||
{% macro ClassSections(class_info, class_courses, class_students, published_courses) %}
|
||||
{% macro ClassSections(class_info, class_courses, class_students, flow) %}
|
||||
<div class="mt-4">
|
||||
|
||||
{% if is_moderator %}
|
||||
@@ -92,8 +92,17 @@
|
||||
{% endif %}
|
||||
|
||||
<ul class="nav lms-nav" id="classes-tab">
|
||||
|
||||
{% if is_student %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-toggle="tab" href="#courses">
|
||||
<a class="nav-link {% if is_student %} active {% endif %}" data-toggle="tab" href="#dashboard">
|
||||
{{ _("Dashboard") }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<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">
|
||||
{{ class_courses | length }}
|
||||
@@ -101,6 +110,17 @@
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% if flow | length %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#schedule">
|
||||
{{ _("Schedule") }}
|
||||
<span class="course-list-count">
|
||||
{{ flow | length }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#students">
|
||||
{{ _("Students") }}
|
||||
@@ -122,6 +142,12 @@
|
||||
{% endif %}
|
||||
|
||||
{% if class_students | length and (is_moderator or is_student) %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#discussions">
|
||||
{{ _("Discussions") }}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#live-class">
|
||||
{{ _("Live Class") }}
|
||||
@@ -137,9 +163,22 @@
|
||||
<div class="border-bottom mb-4"></div>
|
||||
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="courses" role="tabpanel" aria-labelledby="courses">
|
||||
{{ CoursesSection(class_info, class_courses, published_courses) }}
|
||||
|
||||
{% if is_student %}
|
||||
<div class="tab-pane {% if is_student %} active {% endif %}" id="dashboard" role="tabpanel" aria-labelledby="dashboard">
|
||||
{{ Dashboard(class_info, class_courses, current_student) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="tab-pane {% if not is_student %} active {% endif %}" id="courses" role="tabpanel" aria-labelledby="courses">
|
||||
{{ CoursesSection(class_info, class_courses) }}
|
||||
</div>
|
||||
|
||||
{% if flow | length %}
|
||||
<div class="tab-pane" id="schedule" role="tabpanel" aria-labelledby="schedule">
|
||||
{{ ScheduleSection(flow) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="tab-pane" id="students" role="tabpanel" aria-labelledby="students">
|
||||
{{ StudentsSection(class_info, class_students) }}
|
||||
@@ -151,7 +190,11 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if class_students | length and (is_moderator or is_student) %}
|
||||
{% if class_students | length and (is_moderator or is_student or is_evaluator) %}
|
||||
<div class="tab-pane" id="discussions" role="tabpanel" aria-labelledby="discussions">
|
||||
{{ Discussions(class_info) }}
|
||||
</div>
|
||||
|
||||
<div class="tab-pane" id="live-class" role="tabpanel" aria-labelledby="live-class">
|
||||
{{ LiveClassSection(class_info, live_classes) }}
|
||||
</div>
|
||||
@@ -161,8 +204,42 @@
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro Dashboard(class_info, class_courses, current_student) %}
|
||||
|
||||
{% macro CoursesSection(class_info, class_courses, published_courses) %}
|
||||
{% set upcoming_evals = current_student.upcoming_evals %}
|
||||
{% set assessments = current_student.assessments %}
|
||||
{% set student = current_student %}
|
||||
|
||||
{% 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>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro Discussions(class_info) %}
|
||||
<article class="class-discussion">
|
||||
{% set condition = is_moderator or is_student or is_evaluator %}
|
||||
{% set doctype, docname = _("LMS Class"), class_info.name %}
|
||||
{% set single_thread = True %}
|
||||
{% set title = "Discussions" %}
|
||||
{% set cta_title = "Post" %}
|
||||
{% set button_name = _("Start Learning") %}
|
||||
{% set redirect_to = "/classes/" + class_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(class_info, class_courses) %}
|
||||
<article>
|
||||
<header class="mb-5">
|
||||
<div class="edit-header">
|
||||
@@ -247,10 +324,10 @@
|
||||
</div>
|
||||
</div>
|
||||
{% for student in class_students %}
|
||||
{% set allow_progress = is_moderator or student.student == frappe.session.user or is_evaluator %}
|
||||
{% set allow_progress = is_moderator or is_evaluator %}
|
||||
<div class="grid-row">
|
||||
<div class="data-row row">
|
||||
<a class="col grid-static-col {% if allow_progress %} clickable {% endif %}" {% if allow_progress %} href="/classes/{{ class_info.name }}/students/{{ student.username }}" {% endif %}>
|
||||
<a class="col grid-static-col button-links {% if allow_progress %} clickable {% endif %}" {% if allow_progress %} href="/classes/{{ class_info.name }}/students/{{ student.username }}" {% endif %}>
|
||||
{{ student.student_name }}
|
||||
</a>
|
||||
<div class="col grid-static-col col-xs-2 text-right">
|
||||
@@ -450,6 +527,83 @@
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro ScheduleSection(flow) %}
|
||||
<article>
|
||||
<header class="edit-header mb-5">
|
||||
<div class="bold-heading">
|
||||
{{ _("Schedule") }}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div>
|
||||
{% for chapter in flow %}
|
||||
<div class="chapter-parent">
|
||||
<div class="chapter-title" data-toggle="collapse" data-target="#{{ get_slugified_chapter_title(chapter.chapter_title) }}">
|
||||
<img class="chapter-icon" src="/assets/lms/icons/chevron-right.svg">
|
||||
<div class="chapter-title-main">
|
||||
{{ chapter.chapter_title }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="chapter-content lessons collapse navbar-collapse" id="{{ get_slugified_chapter_title(chapter.chapter_title) }}">
|
||||
|
||||
<div class="schedule-header">
|
||||
<div class="w-50">
|
||||
{{ _("Lesson") }}
|
||||
</div>
|
||||
<div class="w-25">
|
||||
{{ _("Date") }}
|
||||
</div>
|
||||
<div class="w-25 text-center">
|
||||
{{ _("Start Time") }}
|
||||
</div>
|
||||
<div class="w-25 text-center">
|
||||
{{ _("End Time") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% for lesson in chapter.lessons %}
|
||||
<div class="lesson-info flex align-center">
|
||||
<a class="lesson-links w-50" href="{{ lesson.url }}">
|
||||
<svg class="icon icon-sm mr-2">
|
||||
<use class="" href="#{{ lesson.icon }}">
|
||||
</svg>
|
||||
|
||||
{{ lesson.title }}
|
||||
|
||||
{% if current_student.name and get_membership(lesson.course, current_student.name) %}
|
||||
{% set lesson_progress = get_progress(lesson.course, lesson.name, current_student.name) %}
|
||||
<svg class="icon icon-md lesson-progress-tick ml-3 {% if lesson_progress != 'Complete' %} hide {% endif %}">
|
||||
<use class="" href="#icon-success">
|
||||
</svg>
|
||||
{% endif %}
|
||||
</a>
|
||||
<div class="w-25">
|
||||
{{ frappe.utils.format_date(lesson.date, "medium") }}
|
||||
</div>
|
||||
<div class="w-25 text-center">
|
||||
{% if lesson.start_time %}
|
||||
{{ frappe.utils.format_time(lesson.start_time, "HH:mm a") }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="w-25 text-center">
|
||||
{% if lesson.end_time %}
|
||||
{{ frappe.utils.format_time(lesson.end_time, "HH:mm a") }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</article>
|
||||
{% endmacro %}
|
||||
|
||||
{%- block script %}
|
||||
{{ super() }}
|
||||
{% if is_moderator %}
|
||||
@@ -464,6 +618,15 @@
|
||||
|
||||
let class_info = {{ class_info | json }};
|
||||
</script>
|
||||
{% else %}
|
||||
<script>
|
||||
frappe.boot.user = {
|
||||
"can_create": [],
|
||||
"can_select": ["LMS Course"],
|
||||
"can_read": ["LMS Course"]
|
||||
};
|
||||
let courses = {{ course_list | json }};
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
{{ include_script('controls.bundle.js') }}
|
||||
|
||||
@@ -42,6 +42,14 @@ frappe.ready(() => {
|
||||
$(".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);
|
||||
});
|
||||
});
|
||||
|
||||
const create_live_class = (e) => {
|
||||
@@ -544,3 +552,121 @@ const remove_assessment = (e) => {
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
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: " ",
|
||||
},
|
||||
{
|
||||
fieldtype: "Date",
|
||||
fieldname: "date",
|
||||
label: __("Date"),
|
||||
reqd: 1,
|
||||
min_date: new Date(
|
||||
frappe.datetime.add_days(frappe.datetime.get_today(), 1)
|
||||
),
|
||||
change: () => {
|
||||
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"),
|
||||
class_name: $(".class-details").data("class"),
|
||||
},
|
||||
callback: (r) => {
|
||||
if (r.message) {
|
||||
display_slots(r.message);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const display_slots = (slots) => {
|
||||
let slot_html = "";
|
||||
let day = moment(this.eval_form.get_value("date")).format("dddd");
|
||||
|
||||
slots.forEach((slot) => {
|
||||
if (slot.day == day) {
|
||||
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>`;
|
||||
}
|
||||
});
|
||||
|
||||
if (!slot_html) {
|
||||
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"),
|
||||
class_name: $(".class-details").data("class"),
|
||||
},
|
||||
callback: (r) => {
|
||||
this.eval_form.hide();
|
||||
frappe.show_alert({
|
||||
message: __("Evaluation scheduled successfully"),
|
||||
indicator: "green",
|
||||
});
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
from frappe import _
|
||||
import frappe
|
||||
from frappe.utils import getdate
|
||||
from frappe.utils import getdate, cint
|
||||
from lms.www.utils import get_assessments
|
||||
from lms.lms.utils import (
|
||||
has_course_moderator_role,
|
||||
has_course_evaluator_role,
|
||||
get_course_progress,
|
||||
get_upcoming_evals,
|
||||
has_submitted_assessment,
|
||||
has_graded_assessment,
|
||||
get_lesson_index,
|
||||
get_lesson_url,
|
||||
get_lesson_icon,
|
||||
get_membership,
|
||||
)
|
||||
|
||||
|
||||
@@ -36,14 +40,13 @@ def get_context(context):
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
context.published_courses = frappe.get_all(
|
||||
"LMS Course", {"published": 1}, ["name", "title"]
|
||||
)
|
||||
context.reference_doctype = "LMS Class"
|
||||
context.reference_name = class_name
|
||||
|
||||
class_courses = frappe.get_all(
|
||||
"Class Course",
|
||||
{"parent": class_name},
|
||||
["name", "course"],
|
||||
["name", "course", "title"],
|
||||
order_by="creation desc",
|
||||
)
|
||||
|
||||
@@ -54,15 +57,9 @@ def get_context(context):
|
||||
order_by="creation desc",
|
||||
)
|
||||
|
||||
context.live_classes = frappe.get_all(
|
||||
"LMS Live Class",
|
||||
{"class_name": class_name, "date": [">=", getdate()]},
|
||||
["title", "description", "time", "date", "start_url", "join_url", "owner"],
|
||||
order_by="date",
|
||||
)
|
||||
|
||||
context.class_courses = get_class_course_details(class_courses)
|
||||
context.all_courses = frappe.get_list(
|
||||
context.course_list = [course.course for course in context.class_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.class_courses]
|
||||
@@ -71,8 +68,23 @@ def get_context(context):
|
||||
class_students, class_courses, context.assessments
|
||||
)
|
||||
context.is_student = is_student(class_students)
|
||||
|
||||
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",
|
||||
{"class_name": class_name, "date": [">=", getdate()]},
|
||||
["title", "description", "time", "date", "start_url", "join_url", "owner"],
|
||||
order_by="date",
|
||||
)
|
||||
|
||||
context.current_student = (
|
||||
get_current_student_details(class_courses, class_name) if context.is_student else None
|
||||
)
|
||||
context.all_assignments = get_all_assignments(class_name)
|
||||
context.all_quizzes = get_all_quizzes(class_name)
|
||||
context.flow = get_scheduled_flow(class_name)
|
||||
|
||||
|
||||
def get_all_quizzes(class_name):
|
||||
@@ -136,36 +148,47 @@ def get_class_student_details(class_students, class_courses, assessments):
|
||||
)
|
||||
)
|
||||
student.update(frappe.db.get_value("User", student.student, "last_active", as_dict=1))
|
||||
|
||||
courses_completed = 0
|
||||
for course in class_courses:
|
||||
if get_course_progress(course.course, student.student) == 100:
|
||||
courses_completed += 1
|
||||
student["courses_completed"] = courses_completed
|
||||
|
||||
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
|
||||
get_progress_info(student, class_courses)
|
||||
get_assessment_info(student, assessments)
|
||||
|
||||
return sort_students(class_students)
|
||||
|
||||
|
||||
def get_progress_info(student, class_courses):
|
||||
courses_completed = 0
|
||||
student["courses"] = frappe._dict()
|
||||
for course in class_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(class_students):
|
||||
session_user = []
|
||||
remaining_students = []
|
||||
@@ -185,3 +208,72 @@ def sort_students(class_students):
|
||||
def is_student(class_students):
|
||||
students = [student.student for student in class_students]
|
||||
return frappe.session.user in students
|
||||
|
||||
|
||||
def get_scheduled_flow(class_name):
|
||||
chapters = []
|
||||
|
||||
lessons = frappe.get_all(
|
||||
"Scheduled Flow",
|
||||
{"parent": class_name},
|
||||
["name", "lesson", "date", "start_time", "end_time"],
|
||||
order_by="idx",
|
||||
)
|
||||
|
||||
for lesson in lessons:
|
||||
lesson = get_lesson_details(lesson, class_name)
|
||||
chapter_exists = [
|
||||
chapter for chapter in chapters if chapter.chapter == lesson.chapter
|
||||
]
|
||||
|
||||
if len(chapter_exists) == 0:
|
||||
chapters.append(
|
||||
frappe._dict(
|
||||
{
|
||||
"chapter": lesson.chapter,
|
||||
"chapter_title": frappe.db.get_value("Course Chapter", lesson.chapter, "title"),
|
||||
"lessons": [lesson],
|
||||
}
|
||||
)
|
||||
)
|
||||
else:
|
||||
chapter_exists[0]["lessons"].append(lesson)
|
||||
|
||||
return chapters
|
||||
|
||||
|
||||
def get_lesson_details(lesson, class_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=" + class_name
|
||||
lesson.icon = get_lesson_icon(lesson.body)
|
||||
return lesson
|
||||
|
||||
|
||||
def get_current_student_details(class_courses, class_name):
|
||||
student_details = frappe._dict()
|
||||
student_details.courses = frappe._dict()
|
||||
course_list = [course.course for course in class_courses]
|
||||
|
||||
get_course_progress(class_courses, student_details)
|
||||
student_details.name = frappe.session.user
|
||||
student_details.assessments = get_assessments(class_name, frappe.session.user)
|
||||
student_details.upcoming_evals = get_upcoming_evals(frappe.session.user, course_list)
|
||||
|
||||
return student_details
|
||||
|
||||
|
||||
def get_course_progress(class_courses, student_details):
|
||||
for course in class_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
|
||||
|
||||
@@ -64,112 +64,13 @@
|
||||
|
||||
{% macro UpcomingEvals(upcoming_evals) %}
|
||||
<div class="mb-8">
|
||||
<div class="bold-heading mb-2">
|
||||
{{ _("Upcoming Evaluations") }}
|
||||
</div>
|
||||
{% if upcoming_evals | length %}
|
||||
<article class="cards-parent">
|
||||
{% for eval in upcoming_evals %}
|
||||
<div class="common-card-style column-card">
|
||||
<div class="flex align-center justify-between">
|
||||
<div class="bold-heading">
|
||||
{{ eval.course_title }}
|
||||
</div>
|
||||
{% if eval.google_meet_link %}
|
||||
<a class="btn btn-default btn-sm pull-right" href="{{ eval.google_meet_link }}">
|
||||
{{ _("Join") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="vertically-center">
|
||||
<svg class="icon icon-sm mr-1">
|
||||
<use href="#icon-calendar"></use>
|
||||
</svg>
|
||||
<span>
|
||||
{{ frappe.utils.format_date(eval.date, "medium") }} -
|
||||
</span>
|
||||
<span>
|
||||
{{ frappe.utils.format_time(eval.start_time, "hh:mm a") }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="field-label">
|
||||
{{ _("Evaluator") }}:
|
||||
</span>
|
||||
<span>
|
||||
{{ eval.evaluator_name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</article>
|
||||
{% else %}
|
||||
<p class="text-muted"> {{ _("No Upcoming Evaluations") }} </p>
|
||||
{% endif %}
|
||||
{% include "lms/templates/upcoming_evals.html" %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro Assessments(class_info, student) %}
|
||||
<div class="mb-8">
|
||||
<div class="bold-heading mb-2">
|
||||
{{ _("Assessments") }}
|
||||
</div>
|
||||
{% if assessments | length %}
|
||||
<article class="form-grid">
|
||||
<div class="grid-heading-row">
|
||||
<div class="grid-row">
|
||||
<div class="data-row row">
|
||||
<div class="col grid-static-col">
|
||||
{{ _("Assessment") }}
|
||||
</div>
|
||||
<div class="col grid-static-col col-xs-2">
|
||||
{{ _("Type") }}
|
||||
</div>
|
||||
<div class="col grid-static-col col-xs-2">
|
||||
{{ _("Status/Score") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% for assessment in assessments %}
|
||||
{% set has_access = is_moderator and assessment.submission or frappe.session.user == student.name %}
|
||||
<div class="grid-row">
|
||||
<div class="data-row row">
|
||||
<a class="col grid-static-col {% if has_access %} clickable {% endif %}" {% if has_access %} href="{{ assessment.url }}" {% endif %}>
|
||||
{{ assessment.title }}
|
||||
</a>
|
||||
<div class="col grid-static-col col-xs-2">
|
||||
{{ (assessment.assessment_type).split("LMS ")[1] }}
|
||||
</div>
|
||||
|
||||
<div class="col grid-static-col col-xs-2 mb-2">
|
||||
{% if assessment.submission %}
|
||||
{% if assessment.assessment_type == "LMS Assignment" %}
|
||||
{% set status = assessment.submission.status %}
|
||||
{% set color = "green" if status == "Pass" else "red" if status == "Fail" else "orange" %}
|
||||
<div class="indicator-pill {{ color }}">
|
||||
{{ status }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div>
|
||||
{{ assessment.submission.score }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="indicator-pill red">
|
||||
{{ _("Not Attempted") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</article>
|
||||
{% else %}
|
||||
<p class="text-muted"> {{ _("No Assessments") }} </p>
|
||||
{% endif %}
|
||||
{% include "lms/templates/assessments.html" %}
|
||||
</div>
|
||||
|
||||
{% endmacro %}
|
||||
|
||||
@@ -2,130 +2,4 @@ frappe.ready(() => {
|
||||
$(".clickable-row").click((e) => {
|
||||
window.location.href = $(e.currentTarget).data("href");
|
||||
});
|
||||
|
||||
$(".btn-schedule-eval").click((e) => {
|
||||
open_evaluation_form(e);
|
||||
});
|
||||
|
||||
$(document).on("click", ".slot", (e) => {
|
||||
mark_active_slot(e);
|
||||
});
|
||||
});
|
||||
|
||||
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: " ",
|
||||
},
|
||||
{
|
||||
fieldtype: "Date",
|
||||
fieldname: "date",
|
||||
label: __("Date"),
|
||||
reqd: 1,
|
||||
min_date: new Date(
|
||||
frappe.datetime.add_days(frappe.datetime.get_today(), 1)
|
||||
),
|
||||
change: () => {
|
||||
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"),
|
||||
class_name: class_name,
|
||||
},
|
||||
callback: (r) => {
|
||||
if (r.message) {
|
||||
display_slots(r.message);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const display_slots = (slots) => {
|
||||
let slot_html = "";
|
||||
let day = moment(this.eval_form.get_value("date")).format("dddd");
|
||||
|
||||
slots.forEach((slot) => {
|
||||
if (slot.day == day) {
|
||||
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>`;
|
||||
}
|
||||
});
|
||||
|
||||
if (!slot_html) {
|
||||
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"),
|
||||
class_name: class_name,
|
||||
},
|
||||
callback: (r) => {
|
||||
this.eval_form.hide();
|
||||
frappe.show_alert({
|
||||
message: __("Evaluation scheduled successfully"),
|
||||
indicator: "green",
|
||||
});
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import frappe
|
||||
from lms.lms.utils import has_course_moderator_role, has_course_evaluator_role
|
||||
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
|
||||
|
||||
@@ -34,20 +38,4 @@ def get_context(context):
|
||||
)
|
||||
|
||||
context.assessments = get_assessments(class_name, context.student.name)
|
||||
|
||||
upcoming_evals = frappe.get_all(
|
||||
"LMS Certificate Request",
|
||||
{
|
||||
"member": context.student.name,
|
||||
"course": ["in", context.courses],
|
||||
"date": [">=", frappe.utils.nowdate()],
|
||||
},
|
||||
["date", "start_time", "course", "evaluator", "google_meet_link"],
|
||||
order_by="date",
|
||||
)
|
||||
|
||||
for evals in upcoming_evals:
|
||||
evals.course_title = frappe.db.get_value("LMS Course", evals.course, "title")
|
||||
evals.evaluator_name = frappe.db.get_value("User", evals.evaluator, "full_name")
|
||||
|
||||
context.upcoming_evals = upcoming_evals
|
||||
context.upcoming_evals = get_upcoming_evals(context.student.name, context.courses)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils.jinja import render_template
|
||||
from frappe.utils import get_url
|
||||
|
||||
@@ -30,18 +31,10 @@ def get_context(context):
|
||||
)
|
||||
context.url = f"{get_url()}/courses/{context.course.name}/{context.doc.name}"
|
||||
|
||||
default_print_format = frappe.db.get_value(
|
||||
"Property Setter",
|
||||
{
|
||||
"doc_type": "LMS Certificate",
|
||||
"property": "default_print_format",
|
||||
},
|
||||
["value"],
|
||||
as_dict=True,
|
||||
)
|
||||
print_format = get_print_format()
|
||||
|
||||
template = frappe.db.get_value(
|
||||
"Print Format", default_print_format.value, ["html", "css"], as_dict=True
|
||||
"Print Format", print_format, ["html", "css"], as_dict=True
|
||||
)
|
||||
merged_template = "<style> " + template.css + " </style>" + template.html
|
||||
final_template = render_template(merged_template, context)
|
||||
@@ -51,3 +44,30 @@ def get_context(context):
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user