@@ -92,7 +92,17 @@ def save_progress(lesson, course, status):
|
|||||||
"LMS Batch Membership", {"member": frappe.session.user, "course": course}
|
"LMS Batch Membership", {"member": frappe.session.user, "course": course}
|
||||||
)
|
)
|
||||||
if not membership:
|
if not membership:
|
||||||
return
|
return 0
|
||||||
|
|
||||||
|
body = frappe.db.get_value("Course Lesson", lesson, "body")
|
||||||
|
macros = find_macros(body)
|
||||||
|
quizzes = [value for name, value in macros if name == "Quiz"]
|
||||||
|
|
||||||
|
for quiz in quizzes:
|
||||||
|
if not frappe.db.exists(
|
||||||
|
"LMS Quiz Submission", {"quiz": quiz, "owner": frappe.session.user}
|
||||||
|
):
|
||||||
|
return 0
|
||||||
|
|
||||||
filters = {"lesson": lesson, "owner": frappe.session.user, "course": course}
|
filters = {"lesson": lesson, "owner": frappe.session.user, "course": course}
|
||||||
if frappe.db.exists("LMS Course Progress", filters):
|
if frappe.db.exists("LMS Course Progress", filters):
|
||||||
|
|||||||
@@ -11,4 +11,24 @@ frappe.ui.form.on("LMS Class", {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
fetch_lessons: (frm) => {
|
||||||
|
frm.clear_table("scheduled_flow");
|
||||||
|
frappe.call({
|
||||||
|
method: "lms.lms.doctype.lms_class.lms_class.fetch_lessons",
|
||||||
|
args: {
|
||||||
|
courses: frm.doc.courses,
|
||||||
|
},
|
||||||
|
callback: (r) => {
|
||||||
|
if (r.message) {
|
||||||
|
r.message.forEach((lesson) => {
|
||||||
|
let row = frm.add_child("scheduled_flow");
|
||||||
|
row.lesson = lesson.name;
|
||||||
|
row.lesson_title = lesson.title;
|
||||||
|
});
|
||||||
|
frm.refresh_field("scheduled_flow");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
"start_date",
|
"start_date",
|
||||||
"end_date",
|
"end_date",
|
||||||
"column_break_4",
|
"column_break_4",
|
||||||
"end_time",
|
|
||||||
"start_time",
|
"start_time",
|
||||||
|
"end_time",
|
||||||
"section_break_rgfj",
|
"section_break_rgfj",
|
||||||
"medium",
|
"medium",
|
||||||
"category",
|
"category",
|
||||||
@@ -24,9 +24,13 @@
|
|||||||
"description",
|
"description",
|
||||||
"students",
|
"students",
|
||||||
"courses",
|
"courses",
|
||||||
|
"section_break_ubxi",
|
||||||
"custom_component",
|
"custom_component",
|
||||||
"assessment_tab",
|
"assessment_tab",
|
||||||
"assessment"
|
"assessment",
|
||||||
|
"schedule_tab",
|
||||||
|
"fetch_lessons",
|
||||||
|
"scheduled_flow"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -134,11 +138,31 @@
|
|||||||
"fieldname": "category",
|
"fieldname": "category",
|
||||||
"fieldtype": "Autocomplete",
|
"fieldtype": "Autocomplete",
|
||||||
"label": "Category"
|
"label": "Category"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "scheduled_flow",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Scheduled Flow",
|
||||||
|
"options": "Scheduled Flow"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_ubxi",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "fetch_lessons",
|
||||||
|
"fieldtype": "Button",
|
||||||
|
"label": "Fetch Lessons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "schedule_tab",
|
||||||
|
"fieldtype": "Tab Break",
|
||||||
|
"label": "Schedule"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-07-13 11:30:09.097605",
|
"modified": "2023-08-10 12:54:44.351907",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Class",
|
"name": "LMS Class",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import json
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import cint, format_date, format_datetime
|
from frappe.utils import cint, format_date, format_datetime
|
||||||
|
from lms.lms.utils import get_lessons
|
||||||
|
|
||||||
|
|
||||||
class LMSClass(Document):
|
class LMSClass(Document):
|
||||||
@@ -18,6 +19,7 @@ class LMSClass(Document):
|
|||||||
self.validate_duplicate_students()
|
self.validate_duplicate_students()
|
||||||
self.validate_duplicate_assessments()
|
self.validate_duplicate_assessments()
|
||||||
self.validate_membership()
|
self.validate_membership()
|
||||||
|
self.validate_schedule()
|
||||||
|
|
||||||
def validate_duplicate_students(self):
|
def validate_duplicate_students(self):
|
||||||
students = [row.student for row in self.students]
|
students = [row.student for row in self.students]
|
||||||
@@ -66,6 +68,35 @@ class LMSClass(Document):
|
|||||||
if cint(self.seat_count) < len(self.students):
|
if cint(self.seat_count) < len(self.students):
|
||||||
frappe.throw(_("There are no seats available in this class."))
|
frappe.throw(_("There are no seats available in this class."))
|
||||||
|
|
||||||
|
def validate_schedule(self):
|
||||||
|
for schedule in self.scheduled_flow:
|
||||||
|
if schedule.start_time and schedule.end_time:
|
||||||
|
if (
|
||||||
|
schedule.start_time > schedule.end_time or schedule.start_time == schedule.end_time
|
||||||
|
):
|
||||||
|
frappe.throw(
|
||||||
|
_("Row #{0} Start time cannot be greater than or equal to end time.").format(
|
||||||
|
schedule.idx
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if schedule.start_time < self.start_time or schedule.start_time > self.end_time:
|
||||||
|
frappe.throw(
|
||||||
|
_("Row #{0} Start time cannot be outside the class duration.").format(
|
||||||
|
schedule.idx
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if schedule.end_time < self.start_time or schedule.end_time > self.end_time:
|
||||||
|
frappe.throw(
|
||||||
|
_("Row #{0} End time cannot be outside the class duration.").format(schedule.idx)
|
||||||
|
)
|
||||||
|
|
||||||
|
if schedule.date < self.start_date or schedule.date > self.end_date:
|
||||||
|
frappe.throw(
|
||||||
|
_("Row #{0} Date cannot be outside the class duration.").format(schedule.idx)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def remove_student(student, class_name):
|
def remove_student(student, class_name):
|
||||||
@@ -188,3 +219,14 @@ def create_class(
|
|||||||
)
|
)
|
||||||
class_details.save()
|
class_details.save()
|
||||||
return class_details
|
return class_details
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def fetch_lessons(courses):
|
||||||
|
lessons = []
|
||||||
|
courses = json.loads(courses)
|
||||||
|
|
||||||
|
for course in courses:
|
||||||
|
lessons.extend(get_lessons(course.get("course")))
|
||||||
|
|
||||||
|
return lessons
|
||||||
|
|||||||
0
lms/lms/doctype/scheduled_flow/__init__.py
Normal file
0
lms/lms/doctype/scheduled_flow/__init__.py
Normal file
69
lms/lms/doctype/scheduled_flow/scheduled_flow.json
Normal file
69
lms/lms/doctype/scheduled_flow/scheduled_flow.json
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"creation": "2023-07-31 15:10:29.287475",
|
||||||
|
"default_view": "List",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"lesson",
|
||||||
|
"lesson_title",
|
||||||
|
"column_break_yikh",
|
||||||
|
"date",
|
||||||
|
"start_time",
|
||||||
|
"end_time"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "lesson",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Lesson",
|
||||||
|
"options": "Course Lesson",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "start_time",
|
||||||
|
"fieldtype": "Time",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Start Time"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "end_time",
|
||||||
|
"fieldtype": "Time",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "End Time"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_yikh",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Date",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "lesson.title",
|
||||||
|
"fieldname": "lesson_title",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Lesson Title"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2023-08-07 12:10:28.095018",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "LMS",
|
||||||
|
"name": "Scheduled Flow",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
9
lms/lms/doctype/scheduled_flow/scheduled_flow.py
Normal file
9
lms/lms/doctype/scheduled_flow/scheduled_flow.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Copyright (c) 2023, Frappe and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class ScheduledFlow(Document):
|
||||||
|
pass
|
||||||
@@ -143,18 +143,28 @@ def get_lesson_details(chapter):
|
|||||||
as_dict=True,
|
as_dict=True,
|
||||||
)
|
)
|
||||||
lesson_details.number = flt(f"{chapter.idx}.{row.idx}")
|
lesson_details.number = flt(f"{chapter.idx}.{row.idx}")
|
||||||
lesson_details.icon = "icon-list"
|
lesson_details.icon = get_lesson_icon(lesson_details.body)
|
||||||
macros = find_macros(lesson_details.body)
|
|
||||||
|
|
||||||
for macro in macros:
|
|
||||||
if macro[0] == "YouTubeVideo" or macro[0] == "Video":
|
|
||||||
lesson_details.icon = "icon-youtube"
|
|
||||||
elif macro[0] == "Quiz":
|
|
||||||
lesson_details.icon = "icon-quiz"
|
|
||||||
lessons.append(lesson_details)
|
lessons.append(lesson_details)
|
||||||
return lessons
|
return lessons
|
||||||
|
|
||||||
|
|
||||||
|
def get_lesson_icon(content):
|
||||||
|
icon = None
|
||||||
|
macros = find_macros(content)
|
||||||
|
|
||||||
|
for macro in macros:
|
||||||
|
if macro[0] == "YouTubeVideo" or macro[0] == "Video":
|
||||||
|
icon = "icon-youtube"
|
||||||
|
elif macro[0] == "Quiz":
|
||||||
|
icon = "icon-quiz"
|
||||||
|
|
||||||
|
if not icon:
|
||||||
|
icon = "icon-list"
|
||||||
|
|
||||||
|
return icon
|
||||||
|
|
||||||
|
|
||||||
def get_tags(course):
|
def get_tags(course):
|
||||||
tags = frappe.db.get_value("LMS Course", course, "tags")
|
tags = frappe.db.get_value("LMS Course", course, "tags")
|
||||||
return tags.split(",") if tags else []
|
return tags.split(",") if tags else []
|
||||||
@@ -272,10 +282,13 @@ def get_slugified_chapter_title(chapter):
|
|||||||
return slugify(chapter)
|
return slugify(chapter)
|
||||||
|
|
||||||
|
|
||||||
def get_progress(course, lesson):
|
def get_progress(course, lesson, member=None):
|
||||||
|
if not member:
|
||||||
|
member = frappe.session.user
|
||||||
|
|
||||||
return frappe.db.get_value(
|
return frappe.db.get_value(
|
||||||
"LMS Course Progress",
|
"LMS Course Progress",
|
||||||
{"course": course, "owner": frappe.session.user, "lesson": lesson},
|
{"course": course, "owner": member, "lesson": lesson},
|
||||||
["status"],
|
["status"],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -343,7 +356,7 @@ def is_eligible_to_review(course, membership):
|
|||||||
|
|
||||||
def get_course_progress(course, member=None):
|
def get_course_progress(course, member=None):
|
||||||
"""Returns the course progress of the session user"""
|
"""Returns the course progress of the session user"""
|
||||||
lesson_count = len(get_lessons(course))
|
lesson_count = get_lessons(course, get_details=False)
|
||||||
if not lesson_count:
|
if not lesson_count:
|
||||||
return 0
|
return 0
|
||||||
completed_lessons = frappe.db.count(
|
completed_lessons = frappe.db.count(
|
||||||
|
|||||||
@@ -120,7 +120,7 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
{% if progress != 100 and membership and not course.upcoming %}
|
{% if progress != 100 and membership and not course.upcoming %}
|
||||||
|
|
||||||
{% set lesson_index = get_lesson_index(membership.current_lesson or "1.1") %}
|
{% set lesson_index = get_lesson_index(membership.current_lesson) or "1.1" %}
|
||||||
|
|
||||||
{% set query_parameter = "?batch=" + membership.batch if membership.batch else "" %}
|
{% set query_parameter = "?batch=" + membership.batch if membership.batch else "" %}
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,6 @@
|
|||||||
<!-- <div class="small ml-auto">
|
<!-- <div class="small ml-auto">
|
||||||
{{ lessons | length }} lessons
|
{{ lessons | length }} lessons
|
||||||
</div> -->
|
</div> -->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@@ -57,7 +56,8 @@
|
|||||||
<div data-lesson="{{ lesson.name }}" class="lesson-info {% if active %} active-lesson {% endif %}">
|
<div data-lesson="{{ lesson.name }}" class="lesson-info {% if active %} active-lesson {% endif %}">
|
||||||
|
|
||||||
{% if membership or lesson.include_in_preview or is_instructor or has_course_moderator_role() %}
|
{% if membership or lesson.include_in_preview or is_instructor or has_course_moderator_role() %}
|
||||||
<a class="lesson-links" href="{{ get_lesson_url(course.name, lesson.number) }}{{course.query_parameter}}"
|
<a class="lesson-links"
|
||||||
|
href="{{ get_lesson_url(course.name, lesson.number) }}{% if classname %}?class={{ classname }}{% endif %}{{course.query_parameter}}"
|
||||||
{% if is_instructor and not lesson.include_in_preview %}
|
{% if is_instructor and not lesson.include_in_preview %}
|
||||||
title="{{ _('This lesson is not available for preview. As you are the Instructor of the course only you can see it.') }}"
|
title="{{ _('This lesson is not available for preview. As you are the Instructor of the course only you can see it.') }}"
|
||||||
{% endif %}>
|
{% endif %}>
|
||||||
|
|||||||
@@ -58,10 +58,10 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rating .star-click {
|
.rating .star-click {
|
||||||
--star-fill: var(--orange-500);
|
--star-fill: var(--orange-500);
|
||||||
background: var(--gray-200);
|
background: var(--gray-200);
|
||||||
border-radius: var(--border-radius-md);
|
border-radius: var(--border-radius-md);
|
||||||
padding: var(--padding-xs);
|
padding: var(--padding-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cta-parent {
|
.cta-parent {
|
||||||
@@ -80,10 +80,10 @@ body {
|
|||||||
|
|
||||||
.field-input {
|
.field-input {
|
||||||
border: 1px solid var(--gray-300);
|
border: 1px solid var(--gray-300);
|
||||||
border-radius: var(--border-radius-md);
|
border-radius: var(--border-radius-md);
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 0.25rem;
|
margin-top: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field-input:focus-visible {
|
.field-input:focus-visible {
|
||||||
@@ -151,9 +151,9 @@ textarea.field-input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ce-block__content {
|
.ce-block__content {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
padding: 0 0.5rem;
|
padding: 0 0.5rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ce-toolbar__content {
|
.ce-toolbar__content {
|
||||||
@@ -206,7 +206,7 @@ textarea.field-input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.codex-editor path {
|
.codex-editor path {
|
||||||
stroke: var(--gray-800);
|
stroke: var(--gray-800);
|
||||||
}
|
}
|
||||||
|
|
||||||
.drag-handle {
|
.drag-handle {
|
||||||
@@ -618,19 +618,18 @@ input[type=checkbox] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.reviews-parent {
|
.reviews-parent {
|
||||||
color: var(--gray-900);
|
color: var(--gray-900);
|
||||||
}
|
}
|
||||||
|
|
||||||
.lesson-info {
|
.lesson-info {
|
||||||
font-size: 16px;
|
padding: 0.5rem;
|
||||||
color: var(--gray-900);
|
color: var(--gray-900);
|
||||||
letter-spacing: -0.011em;
|
letter-spacing: -0.011em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lesson-links {
|
.lesson-links {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0.5rem;
|
|
||||||
color: var(--gray-900);
|
color: var(--gray-900);
|
||||||
font-size: var(--text-base);
|
font-size: var(--text-base);
|
||||||
}
|
}
|
||||||
@@ -1046,42 +1045,42 @@ pre {
|
|||||||
|
|
||||||
.certificate-parent {
|
.certificate-parent {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 10fr 2fr;
|
grid-template-columns: 10fr 2fr;
|
||||||
grid-gap: 3rem;
|
grid-gap: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.certificate-logo {
|
.certificate-logo {
|
||||||
height: 1.5rem;
|
height: 1.5rem;
|
||||||
margin-bottom: 4rem;
|
margin-bottom: 4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.certificate-name {
|
.certificate-name {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #192734;
|
color: #192734;
|
||||||
margin-bottom: 0.25rem;
|
margin-bottom: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.certificate-footer {
|
.certificate-footer {
|
||||||
margin: 4rem auto 0;
|
margin: 4rem auto 0;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.certificate-footer-item {
|
.certificate-footer-item {
|
||||||
color: #192734;
|
color: #192734;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cursive-font {
|
.cursive-font {
|
||||||
font-family: cursive;
|
font-family: cursive;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.certificate-divider {
|
.certificate-divider {
|
||||||
margin: 0.5rem 0;
|
margin: 0.5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.certificate-expiry {
|
.certificate-expiry {
|
||||||
margin-left: 2rem;
|
margin-left: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.column-card {
|
.column-card {
|
||||||
@@ -2118,10 +2117,10 @@ select {
|
|||||||
.lms-card {
|
.lms-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
border: 1px solid var(--gray-300);
|
border: 1px solid var(--gray-300);
|
||||||
/* box-shadow: var(--shadow-sm); */
|
/* box-shadow: var(--shadow-sm); */
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
@@ -2202,6 +2201,12 @@ select {
|
|||||||
cursor: none;
|
cursor: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.schedule-header {
|
||||||
|
display: flex;
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
padding: 0.5rem 0.5rem 0 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.lms-page-style .discussions-section-title {
|
.lms-page-style .discussions-section-title {
|
||||||
font-size: var(--text-lg);
|
font-size: var(--text-lg);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,16 +200,8 @@ const expand_the_first_chapter = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const expand_the_active_chapter = () => {
|
const expand_the_active_chapter = () => {
|
||||||
/* Find anchor matching the URL for course details page */
|
let selector = $(".course-home-headings.title");
|
||||||
let selector = $(
|
console.log(selector);
|
||||||
`a[href="${decodeURIComponent(window.location.pathname)}"]`
|
|
||||||
).parent();
|
|
||||||
|
|
||||||
if (!selector.length) {
|
|
||||||
selector = $(
|
|
||||||
`a[href^="${decodeURIComponent(window.location.pathname)}"]`
|
|
||||||
).parent();
|
|
||||||
}
|
|
||||||
if (selector.length && $(".course-details-page").length) {
|
if (selector.length && $(".course-details-page").length) {
|
||||||
expand_for_course_details(selector);
|
expand_for_course_details(selector);
|
||||||
} else if ($(".active-lesson").length) {
|
} else if ($(".active-lesson").length) {
|
||||||
@@ -225,15 +217,11 @@ const expand_the_active_chapter = () => {
|
|||||||
const expand_for_course_details = (selector) => {
|
const expand_for_course_details = (selector) => {
|
||||||
$(".lesson-info").removeClass("active-lesson");
|
$(".lesson-info").removeClass("active-lesson");
|
||||||
$(".lesson-info").each((i, elem) => {
|
$(".lesson-info").each((i, elem) => {
|
||||||
let href = $(elem).find("use").attr("href");
|
if ($(elem).data("lesson") == selector.data("lesson")) {
|
||||||
href.endsWith("blue") &&
|
$(elem).addClass("active-lesson");
|
||||||
$(elem)
|
show_section($(elem).parent().parent());
|
||||||
.find("use")
|
}
|
||||||
.attr("href", href.substring(0, href.length - 5));
|
|
||||||
});
|
});
|
||||||
selector.addClass("active-lesson");
|
|
||||||
|
|
||||||
show_section(selector.parent().parent());
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const show_section = (element) => {
|
const show_section = (element) => {
|
||||||
|
|||||||
@@ -142,6 +142,9 @@ const quiz_summary = (e = undefined) => {
|
|||||||
$("#try-again").attr("data-submission", data.message.submission);
|
$("#try-again").attr("data-submission", data.message.submission);
|
||||||
$("#try-again").removeClass("hide");
|
$("#try-again").removeClass("hide");
|
||||||
self.quiz_submitted = true;
|
self.quiz_submitted = true;
|
||||||
|
if (this.hasOwnProperty("marked_as_complete")) {
|
||||||
|
mark_progress();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
frappe.ready(() => {
|
frappe.ready(() => {
|
||||||
frappe.telemetry.capture("on_lesson_creation_page", "lms");
|
frappe.telemetry.capture("on_lesson_creation_page", "lms");
|
||||||
let self = this;
|
let self = this;
|
||||||
|
this.quiz_in_lesson = [];
|
||||||
if ($("#current-lesson-content").length) {
|
if ($("#current-lesson-content").length) {
|
||||||
parse_string_to_lesson();
|
parse_string_to_lesson();
|
||||||
}
|
}
|
||||||
@@ -48,7 +49,6 @@ const setup_editor = () => {
|
|||||||
const parse_string_to_lesson = () => {
|
const parse_string_to_lesson = () => {
|
||||||
let lesson_content = $("#current-lesson-content").html();
|
let lesson_content = $("#current-lesson-content").html();
|
||||||
let lesson_blocks = [];
|
let lesson_blocks = [];
|
||||||
this.quiz_in_lesson = [];
|
|
||||||
|
|
||||||
lesson_content.split("\n").forEach((block) => {
|
lesson_content.split("\n").forEach((block) => {
|
||||||
if (block.includes("{{ YouTubeVideo")) {
|
if (block.includes("{{ YouTubeVideo")) {
|
||||||
|
|||||||
@@ -39,13 +39,14 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="course-details-outline">
|
<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>
|
</div>
|
||||||
<div class="lesson-parent">
|
<div class="lesson-parent">
|
||||||
{{ BreadCrumb(course, lesson) }}
|
{{ BreadCrumb(course, lesson, class_info) }}
|
||||||
{{ LessonContent(lesson) }}
|
{{ LessonContent(lesson, class_info) }}
|
||||||
{% if course.status == "Approved" and not course.upcoming %}
|
{% if course.status == "Approved" and not course.upcoming and not class_info %}
|
||||||
{{ Discussions() }}
|
{{ Discussions() }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@@ -56,19 +57,39 @@
|
|||||||
|
|
||||||
|
|
||||||
<!-- BreadCrumb -->
|
<!-- BreadCrumb -->
|
||||||
{% macro BreadCrumb(course, lesson) %}
|
{% macro BreadCrumb(course, lesson, class_info) %}
|
||||||
<div class="breadcrumb">
|
<div class="breadcrumb">
|
||||||
<a class="dark-links" href="/courses">{{ _("All Courses") }}</a>
|
{% if class_info %}
|
||||||
<img class="ml-1 mr-1" src="/assets/lms/icons/chevron-right.svg">
|
<a class="dark-links" href="/courses">
|
||||||
<a class="dark-links" href="/courses/{{ course.name }}">{{ course.title }}</a>
|
{{ _("All Classes") }}
|
||||||
<img class="ml-1 mr-1" src="/assets/lms/icons/chevron-right.svg">
|
</a>
|
||||||
<span class="breadcrumb-destination">{{ lesson.title if lesson.title else _("New Lesson") }}</span>
|
<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>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
<!-- Lesson Details -->
|
<!-- Lesson Details -->
|
||||||
{% macro LessonContent(lesson) %}
|
{% macro LessonContent(lesson, class_info) %}
|
||||||
{% set instructors = get_instructors(course.name) %}
|
{% set instructors = get_instructors(course.name) %}
|
||||||
{% set is_instructor = is_instructor(course.name) %}
|
{% set is_instructor = is_instructor(course.name) %}
|
||||||
|
|
||||||
@@ -146,8 +167,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if not class_info %}
|
||||||
{{ pagination(prev_url, next_url) }}
|
{{ pagination(prev_url, next_url) }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|||||||
@@ -61,8 +61,10 @@ const mark_progress = () => {
|
|||||||
status: status,
|
status: status,
|
||||||
},
|
},
|
||||||
callback: (data) => {
|
callback: (data) => {
|
||||||
change_progress_indicators();
|
if (data.message) {
|
||||||
show_certificate_if_course_completed(data);
|
change_progress_indicators();
|
||||||
|
show_certificate_if_course_completed(data);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,6 +15,16 @@ def get_context(context):
|
|||||||
|
|
||||||
chapter_index = frappe.form_dict.get("chapter")
|
chapter_index = frappe.form_dict.get("chapter")
|
||||||
lesson_index = frappe.form_dict.get("lesson")
|
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}"
|
lesson_number = f"{chapter_index}.{lesson_index}"
|
||||||
context.lesson_number = lesson_number
|
context.lesson_number = lesson_number
|
||||||
context.lesson_index = lesson_index
|
context.lesson_index = lesson_index
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
{{ BreadCrumb(class_info) }}
|
{{ BreadCrumb(class_info) }}
|
||||||
<div class="">
|
<div class="">
|
||||||
{{ ClassDetails(class_info) }}
|
{{ ClassDetails(class_info) }}
|
||||||
{{ ClassSections(class_info, class_courses, class_students) }}
|
{{ ClassSections(class_info, class_courses, class_students, flow) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
|
|
||||||
|
|
||||||
<!-- Class Sections -->
|
<!-- Class Sections -->
|
||||||
{% macro ClassSections(class_info, class_courses, class_students) %}
|
{% macro ClassSections(class_info, class_courses, class_students, flow) %}
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
|
|
||||||
{% if is_moderator %}
|
{% if is_moderator %}
|
||||||
@@ -110,6 +110,17 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</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">
|
<li class="nav-item">
|
||||||
<a class="nav-link" data-toggle="tab" href="#students">
|
<a class="nav-link" data-toggle="tab" href="#students">
|
||||||
{{ _("Students") }}
|
{{ _("Students") }}
|
||||||
@@ -163,6 +174,12 @@
|
|||||||
{{ CoursesSection(class_info, class_courses) }}
|
{{ CoursesSection(class_info, class_courses) }}
|
||||||
</div>
|
</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">
|
<div class="tab-pane" id="students" role="tabpanel" aria-labelledby="students">
|
||||||
{{ StudentsSection(class_info, class_students) }}
|
{{ StudentsSection(class_info, class_students) }}
|
||||||
</div>
|
</div>
|
||||||
@@ -510,6 +527,83 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% 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 %}
|
{%- block script %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
{% if is_moderator %}
|
{% if is_moderator %}
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ from lms.lms.utils import (
|
|||||||
get_upcoming_evals,
|
get_upcoming_evals,
|
||||||
has_submitted_assessment,
|
has_submitted_assessment,
|
||||||
has_graded_assessment,
|
has_graded_assessment,
|
||||||
|
get_lesson_index,
|
||||||
|
get_lesson_url,
|
||||||
|
get_lesson_icon,
|
||||||
get_membership,
|
get_membership,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,16 +57,9 @@ def get_context(context):
|
|||||||
order_by="creation desc",
|
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.class_courses = get_class_course_details(class_courses)
|
||||||
context.course_list = [course.course for course in context.class_courses]
|
context.course_list = [course.course for course in context.class_courses]
|
||||||
context.all_courses = frappe.get_list(
|
context.all_courses = frappe.get_all(
|
||||||
"LMS Course", fields=["name", "title"], limit_page_length=0
|
"LMS Course", fields=["name", "title"], limit_page_length=0
|
||||||
)
|
)
|
||||||
context.course_name_list = [course.course for course in context.class_courses]
|
context.course_name_list = [course.course for course in context.class_courses]
|
||||||
@@ -73,11 +69,22 @@ def get_context(context):
|
|||||||
)
|
)
|
||||||
context.is_student = is_student(class_students)
|
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 = (
|
context.current_student = (
|
||||||
get_current_student_details(class_courses, class_name) if context.is_student else None
|
get_current_student_details(class_courses, class_name) if context.is_student else None
|
||||||
)
|
)
|
||||||
context.all_assignments = get_all_assignments(class_name)
|
context.all_assignments = get_all_assignments(class_name)
|
||||||
context.all_quizzes = get_all_quizzes(class_name)
|
context.all_quizzes = get_all_quizzes(class_name)
|
||||||
|
context.flow = get_scheduled_flow(class_name)
|
||||||
|
|
||||||
|
|
||||||
def get_all_quizzes(class_name):
|
def get_all_quizzes(class_name):
|
||||||
@@ -203,6 +210,53 @@ def is_student(class_students):
|
|||||||
return frappe.session.user in 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):
|
def get_current_student_details(class_courses, class_name):
|
||||||
student_details = frappe._dict()
|
student_details = frappe._dict()
|
||||||
student_details.courses = frappe._dict()
|
student_details.courses = frappe._dict()
|
||||||
|
|||||||
Reference in New Issue
Block a user