diff --git a/cypress/e2e/course_creation.cy.js b/cypress/e2e/course_creation.cy.js
index 3aaf6155..48f3c5dd 100644
--- a/cypress/e2e/course_creation.cy.js
+++ b/cypress/e2e/course_creation.cy.js
@@ -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"
+ );
});
});
diff --git a/lms/install.py b/lms/install.py
index fca317b0..3bd0539b 100644
--- a/lms/install.py
+++ b/lms/install.py
@@ -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():
diff --git a/lms/lms/doctype/course_lesson/course_lesson.py b/lms/lms/doctype/course_lesson/course_lesson.py
index 96e593d0..9ff38e84 100644
--- a/lms/lms/doctype/course_lesson/course_lesson.py
+++ b/lms/lms/doctype/course_lesson/course_lesson.py
@@ -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):
diff --git a/lms/lms/doctype/lms_certificate/lms_certificate.py b/lms/lms/doctype/lms_certificate/lms_certificate.py
index bf3fc1c0..daa400e4 100644
--- a/lms/lms/doctype/lms_certificate/lms_certificate.py
+++ b/lms/lms/doctype/lms_certificate/lms_certificate.py
@@ -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()
diff --git a/lms/lms/doctype/lms_class/lms_class.js b/lms/lms/doctype/lms_class/lms_class.js
index d3d3e2a2..7f3c4299 100644
--- a/lms/lms/doctype/lms_class/lms_class.js
+++ b/lms/lms/doctype/lms_class/lms_class.js
@@ -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");
+ }
+ },
+ });
+ },
});
diff --git a/lms/lms/doctype/lms_class/lms_class.json b/lms/lms/doctype/lms_class/lms_class.json
index 7253100b..dd789b13 100644
--- a/lms/lms/doctype/lms_class/lms_class.json
+++ b/lms/lms/doctype/lms_class/lms_class.json
@@ -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",
diff --git a/lms/lms/doctype/lms_class/lms_class.py b/lms/lms/doctype/lms_class/lms_class.py
index 862bec75..b625d01b 100644
--- a/lms/lms/doctype/lms_class/lms_class.py
+++ b/lms/lms/doctype/lms_class/lms_class.py
@@ -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
diff --git a/lms/lms/doctype/scheduled_flow/__init__.py b/lms/lms/doctype/scheduled_flow/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/lms/lms/doctype/scheduled_flow/scheduled_flow.json b/lms/lms/doctype/scheduled_flow/scheduled_flow.json
new file mode 100644
index 00000000..18a602b5
--- /dev/null
+++ b/lms/lms/doctype/scheduled_flow/scheduled_flow.json
@@ -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": []
+}
\ No newline at end of file
diff --git a/lms/lms/doctype/scheduled_flow/scheduled_flow.py b/lms/lms/doctype/scheduled_flow/scheduled_flow.py
new file mode 100644
index 00000000..a712ec39
--- /dev/null
+++ b/lms/lms/doctype/scheduled_flow/scheduled_flow.py
@@ -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
diff --git a/lms/lms/print_format/certificate/certificate.json b/lms/lms/print_format/certificate/certificate.json
index cea5f505..47b27f51 100644
--- a/lms/lms/print_format/certificate/certificate.json
+++ b/lms/lms/print_format/certificate/certificate.json
@@ -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\":\"
\",\"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 \n \n \n \n\n\n
\n \n
\n
\n {{ _(\"This certifies that\") }}\n
\n \n
\n {{ member.full_name }}\n
\n
\n {{ _(\"has successfully completed the course on\") }}\n {{ course.title }} \n on {{ frappe.utils.format_date(certificate.issue_date, \"medium\") }}.\n
\n \n \n
\n
",
+ "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 \n \n \n \n\n\n
\n \n {% if logo %}\n
\n {% endif %}\n
\n {{ _(\"This certifies that\") }}\n
\n \n
\n {{ member.full_name }}\n
\n
\n {{ _(\"has successfully completed the course on\") }}\n {{ course.title }} \n on {{ frappe.utils.format_date(certificate.issue_date, \"medium\") }}.\n
\n \n \n
\n
",
"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",
diff --git a/lms/lms/utils.py b/lms/lms/utils.py
index db62cdcd..d472e6cf 100644
--- a/lms/lms/utils.py
+++ b/lms/lms/utils.py
@@ -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
diff --git a/lms/lms/widgets/CourseCard.html b/lms/lms/widgets/CourseCard.html
index e050ec0e..9c40bac4 100644
--- a/lms/lms/widgets/CourseCard.html
+++ b/lms/lms/widgets/CourseCard.html
@@ -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 "" %}
diff --git a/lms/lms/widgets/CourseOutline.html b/lms/lms/widgets/CourseOutline.html
index 1ef4059b..b95fbdba 100644
--- a/lms/lms/widgets/CourseOutline.html
+++ b/lms/lms/widgets/CourseOutline.html
@@ -36,7 +36,6 @@
-
@@ -57,7 +56,8 @@
{% if membership or lesson.include_in_preview or is_instructor or has_course_moderator_role() %}
-
diff --git a/lms/public/css/style.css b/lms/public/css/style.css
index 4c8bcf68..6b885abe 100644
--- a/lms/public/css/style.css
+++ b/lms/public/css/style.css
@@ -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;
}
\ No newline at end of file
diff --git a/lms/public/js/common_functions.js b/lms/public/js/common_functions.js
index b39acd21..9605629f 100644
--- a/lms/public/js/common_functions.js
+++ b/lms/public/js/common_functions.js
@@ -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) => {
diff --git a/lms/public/js/website.bundle.js b/lms/public/js/website.bundle.js
index f24ab539..1e86a794 100644
--- a/lms/public/js/website.bundle.js
+++ b/lms/public/js/website.bundle.js
@@ -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";
diff --git a/lms/templates/assessments.html b/lms/templates/assessments.html
new file mode 100644
index 00000000..e5f1f0b0
--- /dev/null
+++ b/lms/templates/assessments.html
@@ -0,0 +1,60 @@
+
+
+ {{ _("Assessments") }}
+
+ {% if assessments | length %}
+
+
+
+
+
+ {{ _("Assessment") }}
+
+
+ {{ _("Type") }}
+
+
+ {{ _("Status/Score") }}
+
+
+
+
+ {% for assessment in assessments %}
+ {% set has_access = is_moderator and assessment.submission or frappe.session.user == student.name %}
+
+
+
+ {{ assessment.title }}
+
+
+ {{ (assessment.assessment_type).split("LMS ")[1] }}
+
+
+
+ {% 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" %}
+
+ {{ status }}
+
+ {% else %}
+
+ {{ assessment.submission.score }}
+
+ {% endif %}
+ {% else %}
+
+ {{ _("Not Attempted") }}
+
+ {% endif %}
+
+
+
+
+ {% endfor %}
+
+ {% else %}
+
{{ _("No Assessments") }}
+ {% endif %}
+
\ No newline at end of file
diff --git a/lms/templates/quiz/quiz.js b/lms/templates/quiz/quiz.js
index 36c2ae08..1de0b05a 100644
--- a/lms/templates/quiz/quiz.js
+++ b/lms/templates/quiz/quiz.js
@@ -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();
+ }
},
});
};
diff --git a/lms/templates/upcoming_evals.html b/lms/templates/upcoming_evals.html
new file mode 100644
index 00000000..87dc783f
--- /dev/null
+++ b/lms/templates/upcoming_evals.html
@@ -0,0 +1,45 @@
+
+
+ {{ _("Upcoming Evaluations") }}
+
+ {% if upcoming_evals | length %}
+
+ {% for eval in upcoming_evals %}
+
+
+
+ {{ eval.course_title }}
+
+ {% if eval.google_meet_link %}
+
+ {{ _("Join") }}
+
+ {% endif %}
+
+
+
+
+
+
+
+ {{ frappe.utils.format_date(eval.date, "medium") }} -
+
+
+ {{ frappe.utils.format_time(eval.start_time, "hh:mm a") }}
+
+
+
+
+ {{ _("Evaluator") }}:
+
+
+ {{ eval.evaluator_name }}
+
+
+
+ {% endfor %}
+
+ {% else %}
+
{{ _("No Upcoming Evaluations") }}
+ {% endif %}
+
\ No newline at end of file
diff --git a/lms/www/batch/edit.js b/lms/www/batch/edit.js
index 285fc2d8..dd0ae722 100644
--- a/lms/www/batch/edit.js
+++ b/lms/www/batch/edit.js
@@ -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")) {
diff --git a/lms/www/batch/learn.html b/lms/www/batch/learn.html
index edc10f04..bf4a8d77 100644
--- a/lms/www/batch/learn.html
+++ b/lms/www/batch/learn.html
@@ -39,13 +39,14 @@
{% endif %}
- {{ 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) }}
- {{ 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 %}
@@ -56,19 +57,39 @@
-{% macro BreadCrumb(course, lesson) %}
+{% macro BreadCrumb(course, lesson, class_info) %}
{% endmacro %}
-{% 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 %}
- {{ frappe.utils.format_date(lesson.creation, "medium") }}
+
+ {{ frappe.utils.format_date(lesson.creation, "medium") }}
+
@@ -144,8 +167,9 @@
{% endif %}
+ {% if not class_info %}
{{ pagination(prev_url, next_url) }}
-
+ {% endif %}
{% 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" %}
+
+ {% include "frappe/templates/discussions/discussions_section.html" %}
+
{% endmacro %}
-
{%- block script %}
{{ super() }}
+ {{ include_script('controls.bundle.js') }}
+ {% else %}
+
{% endif %}
{{ include_script('controls.bundle.js') }}
diff --git a/lms/www/classes/class.js b/lms/www/classes/class.js
index a8a45a9e..7b43379e 100644
--- a/lms/www/classes/class.js
+++ b/lms/www/classes/class.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 += `
+ ${moment(slot.start_time, "hh:mm").format("hh:mm a")} -
+ ${moment(slot.end_time, "hh:mm").format("hh:mm a")}
+
`;
+ }
+ });
+
+ if (!slot_html) {
+ slot_html = `
+ No slots available for this date.
+
`;
+ }
+
+ $("[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);
+ },
+ });
+};
diff --git a/lms/www/classes/class.py b/lms/www/classes/class.py
index 74b4b5a3..589d6db0 100644
--- a/lms/www/classes/class.py
+++ b/lms/www/classes/class.py
@@ -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
diff --git a/lms/www/classes/progress.html b/lms/www/classes/progress.html
index 2639eda7..1c191be0 100644
--- a/lms/www/classes/progress.html
+++ b/lms/www/classes/progress.html
@@ -64,112 +64,13 @@
{% macro UpcomingEvals(upcoming_evals) %}
-
- {{ _("Upcoming Evaluations") }}
-
- {% if upcoming_evals | length %}
-
- {% for eval in upcoming_evals %}
-
-
-
- {{ eval.course_title }}
-
- {% if eval.google_meet_link %}
-
- {{ _("Join") }}
-
- {% endif %}
-
-
-
-
-
-
-
- {{ frappe.utils.format_date(eval.date, "medium") }} -
-
-
- {{ frappe.utils.format_time(eval.start_time, "hh:mm a") }}
-
-
-
-
- {{ _("Evaluator") }}:
-
-
- {{ eval.evaluator_name }}
-
-
-
- {% endfor %}
-
- {% else %}
-
{{ _("No Upcoming Evaluations") }}
- {% endif %}
+ {% include "lms/templates/upcoming_evals.html" %}
{% endmacro %}
{% macro Assessments(class_info, student) %}
-
- {{ _("Assessments") }}
-
- {% if assessments | length %}
-
-
-
-
-
- {{ _("Assessment") }}
-
-
- {{ _("Type") }}
-
-
- {{ _("Status/Score") }}
-
-
-
-
- {% for assessment in assessments %}
- {% set has_access = is_moderator and assessment.submission or frappe.session.user == student.name %}
-
-
-
- {{ assessment.title }}
-
-
- {{ (assessment.assessment_type).split("LMS ")[1] }}
-
-
-
- {% 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" %}
-
- {{ status }}
-
- {% else %}
-
- {{ assessment.submission.score }}
-
- {% endif %}
- {% else %}
-
- {{ _("Not Attempted") }}
-
- {% endif %}
-
-
-
-
- {% endfor %}
-
- {% else %}
-
{{ _("No Assessments") }}
- {% endif %}
+ {% include "lms/templates/assessments.html" %}
{% endmacro %}
diff --git a/lms/www/classes/progress.js b/lms/www/classes/progress.js
index d8c2b6e4..6c136c9a 100644
--- a/lms/www/classes/progress.js
+++ b/lms/www/classes/progress.js
@@ -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 += `
- ${moment(slot.start_time, "hh:mm").format("hh:mm a")} -
- ${moment(slot.end_time, "hh:mm").format("hh:mm a")}
-
`;
- }
- });
-
- if (!slot_html) {
- slot_html = `
- No slots available for this date.
-
`;
- }
-
- $("[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);
- },
- });
-};
diff --git a/lms/www/classes/progress.py b/lms/www/classes/progress.py
index d273162c..90b41282 100644
--- a/lms/www/classes/progress.py
+++ b/lms/www/classes/progress.py
@@ -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)
diff --git a/lms/www/courses/certificate.py b/lms/www/courses/certificate.py
index 0784db9d..91e4de53 100644
--- a/lms/www/courses/certificate.py
+++ b/lms/www/courses/certificate.py
@@ -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 = "" + 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