Merge branch 'main' of https://github.com/frappe/lms into paid-courses
This commit is contained in:
@@ -86,26 +86,50 @@ describe("Course Creation", () => {
|
||||
// Add Discussion
|
||||
cy.get(".reply").click();
|
||||
cy.wait(500);
|
||||
cy.get(".topic-title").type("Question Title");
|
||||
cy.get(".comment-field").type(
|
||||
"Question Content. This is a very long question. It contains more than once sentence. Its meant to be this long as this is a UI test."
|
||||
);
|
||||
cy.get(".submit-discussion").click();
|
||||
cy.get(".discussion-modal").should("be.visible");
|
||||
|
||||
// View Discussion
|
||||
cy.wait(1000);
|
||||
cy.get(".discussion-topic-title:first").contains("Question Title");
|
||||
cy.get(".sidebar-parent:first").click();
|
||||
cy.get(".reply-text").contains(
|
||||
"Question Content. This is a very long question. It contains more than once sentence. Its meant to be this long as this is a UI test."
|
||||
// Enter title
|
||||
cy.get(".modal .topic-title")
|
||||
.type("Discussion from tests")
|
||||
.should("have.value", "Discussion from tests");
|
||||
|
||||
// Enter comment
|
||||
cy.get(".modal .discussions-comment").type(
|
||||
"This is a discussion from the cypress ui tests."
|
||||
);
|
||||
cy.get(".comment-field:visible").type(
|
||||
"This is a reply to the previous comment. Its not that long."
|
||||
|
||||
// Submit
|
||||
cy.get(".modal .submit-discussion").click();
|
||||
cy.wait(2000);
|
||||
|
||||
// Check if discussion is added to page and content is visible
|
||||
cy.get(".sidebar-parent:first .discussion-topic-title").should(
|
||||
"have.text",
|
||||
"Discussion from tests"
|
||||
);
|
||||
cy.get(".submit-discussion:visible").click();
|
||||
cy.wait(1000);
|
||||
cy.get(".reply-text:last p").contains(
|
||||
"This is a reply to the previous comment. Its not that long."
|
||||
cy.get(".sidebar-parent:first .discussion-topic-title").click();
|
||||
cy.get(".discussion-on-page:visible").should("have.class", "show");
|
||||
cy.get(
|
||||
".discussion-on-page:visible .reply-card .reply-text .ql-editor p"
|
||||
).should(
|
||||
"have.text",
|
||||
"This is a discussion from the cypress ui tests."
|
||||
);
|
||||
|
||||
cy.get(".discussion-form:visible .discussions-comment").type(
|
||||
"This is a discussion from the cypress ui tests. \n\nThis comment was entered through the commentbox on the page."
|
||||
);
|
||||
|
||||
cy.get(".discussion-form:visible .submit-discussion").click();
|
||||
cy.wait(3000);
|
||||
cy.get(".discussion-on-page:visible").should("have.class", "show");
|
||||
cy.get(".discussion-on-page:visible")
|
||||
.children(".reply-card")
|
||||
.eq(1)
|
||||
.find(".reply-text")
|
||||
.should(
|
||||
"have.text",
|
||||
"This is a discussion from the cypress ui tests. This comment was entered through the commentbox on the page.\n"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ def after_install():
|
||||
def after_sync():
|
||||
create_lms_roles()
|
||||
set_default_home()
|
||||
set_default_certificate_print_format()
|
||||
add_all_roles_to("Administrator")
|
||||
|
||||
|
||||
@@ -76,7 +77,7 @@ def create_course_creator_role():
|
||||
"desk_access": 0,
|
||||
}
|
||||
)
|
||||
role.save(ignore_permissions=True)
|
||||
role.save()
|
||||
|
||||
|
||||
def create_moderator_role():
|
||||
@@ -89,7 +90,7 @@ def create_moderator_role():
|
||||
"desk_access": 0,
|
||||
}
|
||||
)
|
||||
role.save(ignore_permissions=True)
|
||||
role.save()
|
||||
|
||||
|
||||
def create_evaluator_role():
|
||||
@@ -102,7 +103,26 @@ def create_evaluator_role():
|
||||
"desk_access": 0,
|
||||
}
|
||||
)
|
||||
role.save(ignore_permissions=True)
|
||||
role.save()
|
||||
|
||||
|
||||
def set_default_certificate_print_format():
|
||||
filters = {
|
||||
"doc_type": "LMS Certificate",
|
||||
"property": "default_print_format",
|
||||
}
|
||||
if not frappe.db.exists("Property Setter", filters):
|
||||
filters.update(
|
||||
{
|
||||
"doctype_or_field": "DocType",
|
||||
"property_type": "Data",
|
||||
"value": "Certificate",
|
||||
}
|
||||
)
|
||||
|
||||
doc = frappe.new_doc("Property Setter")
|
||||
doc.update(filters)
|
||||
doc.save()
|
||||
|
||||
|
||||
def delete_custom_fields():
|
||||
|
||||
@@ -92,7 +92,17 @@ def save_progress(lesson, course, status):
|
||||
"LMS Batch Membership", {"member": frappe.session.user, "course": course}
|
||||
)
|
||||
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}
|
||||
if frappe.db.exists("LMS Course Progress", filters):
|
||||
|
||||
@@ -24,17 +24,15 @@ class LMSCertificate(Document):
|
||||
_("{0} is already certified for the course {1}").format(full_name, course_name)
|
||||
)
|
||||
|
||||
def after_insert(self):
|
||||
share = frappe.get_doc(
|
||||
{
|
||||
"doctype": "DocShare",
|
||||
"read": 1,
|
||||
"share_doctype": "LMS Certificate",
|
||||
"share_name": self.name,
|
||||
"user": self.member,
|
||||
}
|
||||
def on_update(self):
|
||||
frappe.share.add_docshare(
|
||||
self.doctype,
|
||||
self.name,
|
||||
self.member,
|
||||
write=1,
|
||||
share=1,
|
||||
flags={"ignore_share_permission": True},
|
||||
)
|
||||
share.save(ignore_permissions=True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -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",
|
||||
"end_date",
|
||||
"column_break_4",
|
||||
"end_time",
|
||||
"start_time",
|
||||
"end_time",
|
||||
"section_break_rgfj",
|
||||
"medium",
|
||||
"category",
|
||||
@@ -24,9 +24,13 @@
|
||||
"description",
|
||||
"students",
|
||||
"courses",
|
||||
"section_break_ubxi",
|
||||
"custom_component",
|
||||
"assessment_tab",
|
||||
"assessment"
|
||||
"assessment",
|
||||
"schedule_tab",
|
||||
"fetch_lessons",
|
||||
"scheduled_flow"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -134,11 +138,31 @@
|
||||
"fieldname": "category",
|
||||
"fieldtype": "Autocomplete",
|
||||
"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,
|
||||
"links": [],
|
||||
"modified": "2023-07-13 11:30:09.097605",
|
||||
"modified": "2023-08-10 12:54:44.351907",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Class",
|
||||
|
||||
@@ -8,6 +8,7 @@ import json
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, format_date, format_datetime
|
||||
from lms.lms.utils import get_lessons
|
||||
|
||||
|
||||
class LMSClass(Document):
|
||||
@@ -18,6 +19,7 @@ class LMSClass(Document):
|
||||
self.validate_duplicate_students()
|
||||
self.validate_duplicate_assessments()
|
||||
self.validate_membership()
|
||||
self.validate_schedule()
|
||||
|
||||
def validate_duplicate_students(self):
|
||||
students = [row.student for row in self.students]
|
||||
@@ -66,6 +68,35 @@ class LMSClass(Document):
|
||||
if cint(self.seat_count) < len(self.students):
|
||||
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()
|
||||
def remove_student(student, class_name):
|
||||
@@ -188,3 +219,14 @@ def create_class(
|
||||
)
|
||||
class_details.save()
|
||||
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
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"absolute_value": 0,
|
||||
"align_labels_right": 0,
|
||||
"creation": "2023-02-22 21:36:54.560420",
|
||||
"creation": "2023-08-09 17:02:21.430320",
|
||||
"css": ".outer-border {\n font-family: \"Inter\" sans-serif;\n font-size: 16px;\n border-radius: 0.5rem;\n border: 1px solid #E2E6E9;\n padding: 1rem;\n}\n\n.inner-border {\n border: 10px solid #0089FF;\n border-radius: 8px;\n text-align: center;\n padding: 6rem 4rem;\n background-color: #FFFFFF;\n}\n\n.certificate-logo {\n height: 1.5rem;\n margin-bottom: 4rem;\n}\n\n.certificate-name {\n font-size: 2rem;\n font-weight: 500;\n color: #192734;\n margin-bottom: 0.5rem;\n}\n\n.certificate-footer {\n margin: 4rem auto 0;\n width: 70%;\n text-align: center;\n}\n\n.certificate-footer-item {\n color: #192734;\n}\n\n.cursive-font {\n font-family: cursive;\n font-weight: 600;\n}\n\n.certificate-divider {\n margin: 0.5rem 0;\n}\n\n.certificate-expiry {\n margin-left: 2rem;\n}",
|
||||
"custom_format": 1,
|
||||
"disabled": 0,
|
||||
@@ -10,14 +10,14 @@
|
||||
"doctype": "Print Format",
|
||||
"font_size": 14,
|
||||
"format_data": "{\"header\":\"<div class=\\\"document-header\\\">\\n\\t<h3>LMS Certificate</h3>\\n\\t<p>{{ doc.name }}</p>\\n</div>\",\"sections\":[{\"label\":\"\",\"columns\":[{\"label\":\"\",\"fields\":[{\"label\":\"Course\",\"fieldname\":\"course\",\"fieldtype\":\"Link\",\"options\":\"LMS Course\"},{\"label\":\"Member\",\"fieldname\":\"member\",\"fieldtype\":\"Link\",\"options\":\"User\"},{\"label\":\"Member Name\",\"fieldname\":\"member_name\",\"fieldtype\":\"Data\"},{\"label\":\"Evaluator\",\"fieldname\":\"evaluator\",\"fieldtype\":\"Data\",\"options\":\"\"}]},{\"label\":\"\",\"fields\":[{\"label\":\"Issue Date\",\"fieldname\":\"issue_date\",\"fieldtype\":\"Date\"},{\"label\":\"Expiry Date\",\"fieldname\":\"expiry_date\",\"fieldtype\":\"Date\"},{\"label\":\"Version\",\"fieldname\":\"version\",\"fieldtype\":\"Select\",\"options\":\"V13\\nV14\"},{\"label\":\"Module Names for Certificate\",\"fieldname\":\"module_names_for_certificate\",\"fieldtype\":\"Data\"}]}],\"has_fields\":true}]}",
|
||||
"html": "{% set certificate = frappe.db.get_value(\"LMS Certificate\", doc.name, [\"name\", \"member\", \"issue_date\", \"expiry_date\", \"course\"], as_dict=True) %}\n{% set member = frappe.db.get_value(\"User\", doc.member, [\"full_name\"], as_dict=True) %}\n{% set course = frappe.db.get_value(\"LMS Course\", doc.course, [\"title\", \"name\", \"image\"], as_dict=True) %}\n{% set logo = frappe.db.get_single_value(\"Website Settings\", \"banner_image\") %}\n{% set instructors = frappe.get_all(\"Course Instructor\", {\"parent\": doc.course}, pluck=\"instructor\", order_by=\"idx\") %}\n\n<meta name=\"pdfkit-orientation\" content=\"Landscape\">\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500&display=swap\" rel=\"stylesheet\">\n\n<div class=\"outer-border\">\n <div class=\"inner-border\">\n \n <img src=\"{{ logo }}\" class=\"certificate-logo\">\n <div>\n {{ _(\"This certifies that\") }}\n </div>\n \n <div class=\"certificate-name\" style=\"\">\n {{ member.full_name }}\n </div>\n <div>\n {{ _(\"has successfully completed the course on\") }}\n <b> {{ course.title }} </b>\n on {{ frappe.utils.format_date(certificate.issue_date, \"medium\") }}.\n </div>\n \n <table class=\"certificate-footer\">\n <tr>\n {% if instructors %}\n <td>\n <div class=\"certificate-footer-item cursive-font\">\n {% for i in instructors %}\n \t\t\t\t\t{{ frappe.db.get_value(\"User\", i, \"full_name\") }}\n \t\t\t\t\t{% if not loop.last %}\n \t\t\t\t\t,\n \t\t\t\t\t{% endif %}\n \t\t\t\t\t{% endfor %}\n </div>\n <hr class=\"certificate-divider\">\n <div class=\"text-center\"> {{ _(\"Course Instructor\") }} </div>\n </td>\n {% endif %}\n \n {% if certificate.expiry_date %}\n <td style=\"width: 30%\"></td>\n \n <td class=\"certificate-expiry\">\n <div class=\"certificate-footer-item\">\n {{ frappe.utils.format_date(certificate.expiry_date, \"medium\") }}\n </div>\n <hr class=\"certificate-divider\">\n <div class=\"text-center\"> {{ _(\"Expiry Date\") }} </div>\n </td>\n {% endif %}\n </tr>\n </table>\n </div>\n </div>",
|
||||
"html": "{% set certificate = frappe.db.get_value(\"LMS Certificate\", doc.name, [\"name\", \"member\", \"issue_date\", \"expiry_date\", \"course\"], as_dict=True) %}\n{% set member = frappe.db.get_value(\"User\", doc.member, [\"full_name\"], as_dict=True) %}\n{% set course = frappe.db.get_value(\"LMS Course\", doc.course, [\"title\", \"name\", \"image\"], as_dict=True) %}\n{% set logo = frappe.db.get_single_value(\"Website Settings\", \"banner_image\") %}\n{% set instructors = frappe.get_all(\"Course Instructor\", {\"parent\": doc.course}, pluck=\"instructor\", order_by=\"idx\") %}\n\n<meta name=\"pdfkit-orientation\" content=\"Landscape\">\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500&display=swap\" rel=\"stylesheet\">\n\n<div class=\"outer-border\">\n <div class=\"inner-border\">\n \n {% if logo %}\n <img src=\"{{ logo }}\" class=\"certificate-logo\">\n {% endif %}\n <div>\n {{ _(\"This certifies that\") }}\n </div>\n \n <div class=\"certificate-name\" style=\"\">\n {{ member.full_name }}\n </div>\n <div>\n {{ _(\"has successfully completed the course on\") }}\n <b> {{ course.title }} </b>\n on {{ frappe.utils.format_date(certificate.issue_date, \"medium\") }}.\n </div>\n \n <table class=\"certificate-footer\">\n <tr>\n {% if instructors %}\n <td>\n <div class=\"certificate-footer-item cursive-font\">\n {% for i in instructors %}\n \t\t\t\t\t{{ frappe.db.get_value(\"User\", i, \"full_name\") }}\n \t\t\t\t\t{% if not loop.last %}\n \t\t\t\t\t,\n \t\t\t\t\t{% endif %}\n \t\t\t\t\t{% endfor %}\n </div>\n <hr class=\"certificate-divider\">\n <div class=\"text-center\"> {{ _(\"Course Instructor\") }} </div>\n </td>\n {% endif %}\n \n {% if certificate.expiry_date %}\n <td style=\"width: 30%\"></td>\n \n <td class=\"certificate-expiry\">\n <div class=\"certificate-footer-item\">\n {{ frappe.utils.format_date(certificate.expiry_date, \"medium\") }}\n </div>\n <hr class=\"certificate-divider\">\n <div class=\"text-center\"> {{ _(\"Expiry Date\") }} </div>\n </td>\n {% endif %}\n </tr>\n </table>\n </div>\n </div>",
|
||||
"idx": 0,
|
||||
"line_breaks": 0,
|
||||
"margin_bottom": 0.0,
|
||||
"margin_left": 0.0,
|
||||
"margin_right": 0.0,
|
||||
"margin_top": 0.0,
|
||||
"modified": "2023-04-17 13:46:38.633751",
|
||||
"modified": "2023-08-09 17:02:21.430320",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Certificate",
|
||||
|
||||
@@ -57,21 +57,28 @@ def generate_slug(title, doctype):
|
||||
return slugify(title, used_slugs=slugs)
|
||||
|
||||
|
||||
def get_membership(course, member, batch=None):
|
||||
def get_membership(course, member=None, batch=None):
|
||||
if not member:
|
||||
member = frappe.session.user
|
||||
|
||||
filters = {"member": member, "course": course}
|
||||
if batch:
|
||||
filters["batch"] = batch
|
||||
|
||||
membership = frappe.db.get_value(
|
||||
"LMS Batch Membership",
|
||||
filters,
|
||||
["name", "batch", "current_lesson", "member_type", "progress"],
|
||||
as_dict=True,
|
||||
)
|
||||
is_member = frappe.db.exists("LMS Batch Membership", filters)
|
||||
if is_member:
|
||||
membership = frappe.db.get_value(
|
||||
"LMS Batch Membership",
|
||||
filters,
|
||||
["name", "batch", "current_lesson", "member_type", "progress"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
if membership and membership.batch:
|
||||
membership.batch_title = frappe.db.get_value("LMS Batch", membership.batch, "title")
|
||||
return membership
|
||||
if membership and membership.batch:
|
||||
membership.batch_title = frappe.db.get_value("LMS Batch", membership.batch, "title")
|
||||
return membership
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_chapters(course):
|
||||
@@ -136,18 +143,28 @@ def get_lesson_details(chapter):
|
||||
as_dict=True,
|
||||
)
|
||||
lesson_details.number = flt(f"{chapter.idx}.{row.idx}")
|
||||
lesson_details.icon = "icon-list"
|
||||
macros = find_macros(lesson_details.body)
|
||||
lesson_details.icon = get_lesson_icon(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)
|
||||
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):
|
||||
tags = frappe.db.get_value("LMS Course", course, "tags")
|
||||
return tags.split(",") if tags else []
|
||||
@@ -265,10 +282,13 @@ def get_slugified_chapter_title(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(
|
||||
"LMS Course Progress",
|
||||
{"course": course, "owner": frappe.session.user, "lesson": lesson},
|
||||
{"course": course, "owner": member, "lesson": lesson},
|
||||
["status"],
|
||||
)
|
||||
|
||||
@@ -336,7 +356,7 @@ def is_eligible_to_review(course, membership):
|
||||
|
||||
def get_course_progress(course, member=None):
|
||||
"""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:
|
||||
return 0
|
||||
completed_lessons = frappe.db.count(
|
||||
@@ -524,7 +544,7 @@ def has_course_moderator_role(member=None):
|
||||
def has_course_evaluator_role(member=None):
|
||||
return frappe.db.get_value(
|
||||
"Has Role",
|
||||
{"parent": member or frappe.session.user, "role": "Evaluator"},
|
||||
{"parent": member or frappe.session.user, "role": "Class Evaluator"},
|
||||
"name",
|
||||
)
|
||||
|
||||
@@ -700,7 +720,7 @@ def get_chart_data(chart_name, timespan, timegrain, from_date, to_date):
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@frappe.whitelist()
|
||||
def get_course_completion_data():
|
||||
all_membership = frappe.db.count("LMS Batch Membership")
|
||||
completed = frappe.db.count("LMS Batch Membership", {"progress": ["like", "%100%"]})
|
||||
@@ -785,3 +805,21 @@ def get_evaluator(course, class_name=None):
|
||||
evaluator = frappe.db.get_value("LMS Course", course, "evaluator")
|
||||
|
||||
return evaluator
|
||||
|
||||
|
||||
def get_upcoming_evals(student, courses):
|
||||
upcoming_evals = frappe.get_all(
|
||||
"LMS Certificate Request",
|
||||
{
|
||||
"member": student,
|
||||
"course": ["in", 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")
|
||||
return upcoming_evals
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
{% else %}
|
||||
{% 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 "" %}
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
<!-- <div class="small ml-auto">
|
||||
{{ lessons | length }} lessons
|
||||
</div> -->
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -57,7 +56,8 @@
|
||||
<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() %}
|
||||
<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 %}
|
||||
title="{{ _('This lesson is not available for preview. As you are the Instructor of the course only you can see it.') }}"
|
||||
{% endif %}>
|
||||
|
||||
@@ -43,7 +43,7 @@ body {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.frappe-control .ql-editor:not(.read-mode) {
|
||||
.field-group .frappe-control .ql-editor:not(.read-mode) {
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
@@ -58,10 +58,10 @@ body {
|
||||
}
|
||||
|
||||
.rating .star-click {
|
||||
--star-fill: var(--orange-500);
|
||||
background: var(--gray-200);
|
||||
border-radius: var(--border-radius-md);
|
||||
padding: var(--padding-xs);
|
||||
--star-fill: var(--orange-500);
|
||||
background: var(--gray-200);
|
||||
border-radius: var(--border-radius-md);
|
||||
padding: var(--padding-xs);
|
||||
}
|
||||
|
||||
.cta-parent {
|
||||
@@ -80,10 +80,10 @@ body {
|
||||
|
||||
.field-input {
|
||||
border: 1px solid var(--gray-300);
|
||||
border-radius: var(--border-radius-md);
|
||||
padding: 0.5rem;
|
||||
width: 100%;
|
||||
margin-top: 0.25rem;
|
||||
border-radius: var(--border-radius-md);
|
||||
padding: 0.5rem;
|
||||
width: 100%;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.field-input:focus-visible {
|
||||
@@ -151,9 +151,9 @@ textarea.field-input {
|
||||
}
|
||||
|
||||
.ce-block__content {
|
||||
max-width: 100%;
|
||||
padding: 0 0.5rem;
|
||||
margin: 0;
|
||||
max-width: 100%;
|
||||
padding: 0 0.5rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ce-toolbar__content {
|
||||
@@ -206,7 +206,7 @@ textarea.field-input {
|
||||
}
|
||||
|
||||
.codex-editor path {
|
||||
stroke: var(--gray-800);
|
||||
stroke: var(--gray-800);
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
@@ -321,11 +321,12 @@ input[type=checkbox] {
|
||||
}
|
||||
|
||||
.common-card-style {
|
||||
display: flex;
|
||||
background: #FFFFFF;
|
||||
border-radius: var(--border-radius-md);
|
||||
position: relative;
|
||||
border: 1px solid var(--gray-300)
|
||||
display: flex;
|
||||
background: #FFFFFF;
|
||||
border-radius: var(--border-radius-md);
|
||||
position: relative;
|
||||
border: 1px solid var(--gray-300);
|
||||
box-shadow: var(--shadow-inset);
|
||||
}
|
||||
|
||||
.course-card {
|
||||
@@ -624,19 +625,18 @@ input[type=checkbox] {
|
||||
}
|
||||
|
||||
.reviews-parent {
|
||||
color: var(--gray-900);
|
||||
color: var(--gray-900);
|
||||
}
|
||||
|
||||
.lesson-info {
|
||||
font-size: 16px;
|
||||
color: var(--gray-900);
|
||||
letter-spacing: -0.011em;
|
||||
padding: 0.5rem;
|
||||
color: var(--gray-900);
|
||||
letter-spacing: -0.011em;
|
||||
}
|
||||
|
||||
.lesson-links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
color: var(--gray-900);
|
||||
font-size: var(--text-base);
|
||||
}
|
||||
@@ -825,7 +825,7 @@ input[type=checkbox] {
|
||||
}
|
||||
|
||||
.lesson-pagination {
|
||||
margin: 2rem 0;
|
||||
margin: 2rem 0 5rem;
|
||||
}
|
||||
|
||||
.lesson-video {
|
||||
@@ -1052,42 +1052,42 @@ pre {
|
||||
|
||||
.certificate-parent {
|
||||
display: grid;
|
||||
grid-template-columns: 10fr 2fr;
|
||||
grid-gap: 3rem;
|
||||
grid-template-columns: 10fr 2fr;
|
||||
grid-gap: 3rem;
|
||||
}
|
||||
|
||||
.certificate-logo {
|
||||
height: 1.5rem;
|
||||
margin-bottom: 4rem;
|
||||
height: 1.5rem;
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
.certificate-name {
|
||||
font-size: 2rem;
|
||||
font-weight: 500;
|
||||
color: #192734;
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: 2rem;
|
||||
font-weight: 500;
|
||||
color: #192734;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.certificate-footer {
|
||||
margin: 4rem auto 0;
|
||||
width: fit-content;
|
||||
margin: 4rem auto 0;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.certificate-footer-item {
|
||||
color: #192734;
|
||||
color: #192734;
|
||||
}
|
||||
|
||||
.cursive-font {
|
||||
font-family: cursive;
|
||||
font-family: cursive;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.certificate-divider {
|
||||
margin: 0.5rem 0;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.certificate-expiry {
|
||||
margin-left: 2rem;
|
||||
margin-left: 2rem;
|
||||
}
|
||||
|
||||
.column-card {
|
||||
@@ -1656,10 +1656,6 @@ li {
|
||||
color: var(--gray-600);
|
||||
}
|
||||
|
||||
.discussions-parent .empty-state {
|
||||
background-color: var(--gray-200);
|
||||
}
|
||||
|
||||
.job-cards-parent {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(500px, 1fr));
|
||||
@@ -2128,10 +2124,10 @@ select {
|
||||
.lms-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid var(--gray-300);
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid var(--gray-300);
|
||||
/* box-shadow: var(--shadow-sm); */
|
||||
padding: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
@@ -2210,4 +2206,83 @@ select {
|
||||
.rows .grid-row .grid-footer-toolbar,
|
||||
.grid-form-heading {
|
||||
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 {
|
||||
font-size: var(--text-lg);
|
||||
}
|
||||
|
||||
.class-dashboard .progress {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
background: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.class-dashboard .progress::after {
|
||||
content: "";
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
border: 6px solid #eee;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.class-dashboard .progress>span {
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.class-dashboard .progress .progress-left {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.class-dashboard .progress .progress-bar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: none;
|
||||
border-width: 6px;
|
||||
border-style: solid;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.class-dashboard .progress .progress-left .progress-bar {
|
||||
left: 100%;
|
||||
border-top-right-radius: 80px;
|
||||
border-bottom-right-radius: 80px;
|
||||
border-left: 0;
|
||||
-webkit-transform-origin: center left;
|
||||
transform-origin: center left;
|
||||
}
|
||||
|
||||
.class-dashboard .progress .progress-right {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.class-dashboard .progress .progress-right .progress-bar {
|
||||
left: -100%;
|
||||
border-top-left-radius: 80px;
|
||||
border-bottom-left-radius: 80px;
|
||||
border-right: 0;
|
||||
-webkit-transform-origin: center right;
|
||||
transform-origin: center right;
|
||||
}
|
||||
|
||||
.class-dashboard .progress .progress-value {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
@@ -200,16 +200,8 @@ const expand_the_first_chapter = () => {
|
||||
};
|
||||
|
||||
const expand_the_active_chapter = () => {
|
||||
/* Find anchor matching the URL for course details page */
|
||||
let selector = $(
|
||||
`a[href="${decodeURIComponent(window.location.pathname)}"]`
|
||||
).parent();
|
||||
|
||||
if (!selector.length) {
|
||||
selector = $(
|
||||
`a[href^="${decodeURIComponent(window.location.pathname)}"]`
|
||||
).parent();
|
||||
}
|
||||
let selector = $(".course-home-headings.title");
|
||||
console.log(selector);
|
||||
if (selector.length && $(".course-details-page").length) {
|
||||
expand_for_course_details(selector);
|
||||
} else if ($(".active-lesson").length) {
|
||||
@@ -225,15 +217,11 @@ const expand_the_active_chapter = () => {
|
||||
const expand_for_course_details = (selector) => {
|
||||
$(".lesson-info").removeClass("active-lesson");
|
||||
$(".lesson-info").each((i, elem) => {
|
||||
let href = $(elem).find("use").attr("href");
|
||||
href.endsWith("blue") &&
|
||||
$(elem)
|
||||
.find("use")
|
||||
.attr("href", href.substring(0, href.length - 5));
|
||||
if ($(elem).data("lesson") == selector.data("lesson")) {
|
||||
$(elem).addClass("active-lesson");
|
||||
show_section($(elem).parent().parent());
|
||||
}
|
||||
});
|
||||
selector.addClass("active-lesson");
|
||||
|
||||
show_section(selector.parent().parent());
|
||||
};
|
||||
|
||||
const show_section = (element) => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import "./profile.js";
|
||||
import "./common_functions.js";
|
||||
import "../../../../frappe/frappe/public/js/frappe/ui/chart.js";
|
||||
import "../../../../frappe/frappe/public/js/frappe/ui/keyboard.js";
|
||||
import "../../../../frappe/frappe/public/js/frappe/event_emitter.js";
|
||||
|
||||
60
lms/templates/assessments.html
Normal file
60
lms/templates/assessments.html
Normal file
@@ -0,0 +1,60 @@
|
||||
<div>
|
||||
<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 %}
|
||||
</div>
|
||||
@@ -142,6 +142,9 @@ const quiz_summary = (e = undefined) => {
|
||||
$("#try-again").attr("data-submission", data.message.submission);
|
||||
$("#try-again").removeClass("hide");
|
||||
self.quiz_submitted = true;
|
||||
if (this.hasOwnProperty("marked_as_complete")) {
|
||||
mark_progress();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
45
lms/templates/upcoming_evals.html
Normal file
45
lms/templates/upcoming_evals.html
Normal file
@@ -0,0 +1,45 @@
|
||||
<div>
|
||||
<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 %}
|
||||
</div>
|
||||
@@ -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