Merge pull request #600 from pateljannat/paid-class
feat: Batches Revamp
This commit is contained in:
@@ -48,7 +48,7 @@ You can create courses and lessons through simple forms. Lessons can be in the f
|
||||
- Add detailed descriptions and preview videos to the course. 🎬
|
||||
- Add videos, quizzes, and assignments to your lessons and make them interesting and interactive 📝
|
||||
- Discussions section below each lesson where instructors and students can interact with each other. 💬
|
||||
- Create classes to group your students based on courses and track their progress 🏛
|
||||
- Create batches to group your students based on courses and track their progress 🏛
|
||||
- Statistics dashboard that provides all important numbers at a glimpse. 📈
|
||||
- Job Board where users can post and look for jobs. 💼
|
||||
- People directory with each person's profile page 👨👩👧👦
|
||||
|
||||
17
lms/hooks.py
17
lms/hooks.py
@@ -152,7 +152,7 @@ website_route_rules = [
|
||||
},
|
||||
{"from_route": "/quizzes", "to_route": "batch/quiz_list"},
|
||||
{"from_route": "/quizzes/<quizname>", "to_route": "batch/quiz"},
|
||||
{"from_route": "/classes/<classname>", "to_route": "classes/class"},
|
||||
{"from_route": "/batches/<batchname>", "to_route": "batches/batch"},
|
||||
{"from_route": "/courses/<course>/progress", "to_route": "batch/progress"},
|
||||
{"from_route": "/courses/<course>/join", "to_route": "batch/join"},
|
||||
{"from_route": "/courses/<course>/manage", "to_route": "cohorts"},
|
||||
@@ -176,8 +176,8 @@ website_route_rules = [
|
||||
{"from_route": "/users", "to_route": "profiles/profile"},
|
||||
{"from_route": "/jobs/<job>", "to_route": "jobs/job"},
|
||||
{
|
||||
"from_route": "/classes/<classname>/students/<username>",
|
||||
"to_route": "/classes/progress",
|
||||
"from_route": "/batches/<batchname>/students/<username>",
|
||||
"to_route": "/batches/progress",
|
||||
},
|
||||
{"from_route": "/assignments/<assignment>", "to_route": "assignments/assignment"},
|
||||
{
|
||||
@@ -189,9 +189,17 @@ website_route_rules = [
|
||||
"to_route": "quiz_submission/quiz_submission",
|
||||
},
|
||||
{
|
||||
"from_route": "/billing/<course>",
|
||||
"from_route": "/billing/<module>/<modulename>",
|
||||
"to_route": "billing/billing",
|
||||
},
|
||||
{
|
||||
"from_route": "/batches/details/<batchname>",
|
||||
"to_route": "batches/batch_details",
|
||||
},
|
||||
{
|
||||
"from_route": "/certified-participants",
|
||||
"to_route": "certified_participants/certified_participants",
|
||||
},
|
||||
]
|
||||
|
||||
website_redirects = [
|
||||
@@ -249,6 +257,7 @@ jinja = {
|
||||
"lms.lms.utils.can_create_courses",
|
||||
"lms.lms.utils.get_telemetry_boot_info",
|
||||
"lms.lms.utils.is_onboarding_complete",
|
||||
"lms.www.utils.is_student",
|
||||
],
|
||||
"filters": [],
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ def add_pages_to_nav():
|
||||
pages = [
|
||||
{"label": "Explore", "idx": 1},
|
||||
{"label": "Courses", "url": "/courses", "parent": "Explore", "idx": 2},
|
||||
{"label": "Classes", "url": "/classes", "parent": "Explore", "idx": 3},
|
||||
{"label": "Batches", "url": "/batches", "parent": "Explore", "idx": 3},
|
||||
{"label": "Statistics", "url": "/statistics", "parent": "Explore", "idx": 4},
|
||||
{"label": "Jobs", "url": "/jobs", "parent": "Explore", "idx": 5},
|
||||
{"label": "People", "url": "/community", "parent": "Explore", "idx": 6},
|
||||
|
||||
@@ -38,10 +38,10 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-07-13 17:51:36.278393",
|
||||
"modified": "2023-08-28 10:03:02.960844",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Class Course",
|
||||
"name": "Batch Course",
|
||||
"naming_rule": "Autoincrement",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
@@ -5,5 +5,5 @@
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class ClassCourse(Document):
|
||||
class BatchCourse(Document):
|
||||
pass
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2022, Frappe and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Class Student", {
|
||||
frappe.ui.form.on("Batch Student", {
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
});
|
||||
@@ -7,7 +7,10 @@
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"student_details_section",
|
||||
"student",
|
||||
"payment",
|
||||
"column_break_oduu",
|
||||
"student_name",
|
||||
"username"
|
||||
],
|
||||
@@ -24,22 +27,40 @@
|
||||
"fetch_from": "student.full_name",
|
||||
"fieldname": "student_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Student Name"
|
||||
"in_list_view": 1,
|
||||
"label": "Student Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "student.username",
|
||||
"fieldname": "username",
|
||||
"fieldtype": "Data",
|
||||
"label": "Username"
|
||||
"label": "Username",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "student_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Student Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_oduu",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "payment",
|
||||
"fieldtype": "Link",
|
||||
"label": "Payment",
|
||||
"options": "LMS Payment"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-18 18:45:22.787839",
|
||||
"modified": "2023-09-12 16:46:41.042810",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Class Student",
|
||||
"name": "Batch Student",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
@@ -5,5 +5,5 @@
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class ClassStudent(Document):
|
||||
class BatchStudent(Document):
|
||||
pass
|
||||
@@ -5,5 +5,5 @@
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestClassStudent(FrappeTestCase):
|
||||
class TestBatchStudent(FrappeTestCase):
|
||||
pass
|
||||
@@ -37,8 +37,8 @@ class CourseEvaluator(Document):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_schedule(course, date, class_name=None):
|
||||
evaluator = get_evaluator(course, class_name)
|
||||
def get_schedule(course, date, batch=None):
|
||||
evaluator = get_evaluator(course, batch)
|
||||
|
||||
all_slots = frappe.get_all(
|
||||
"Evaluator Schedule",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// Copyright (c) 2022, Frappe and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("LMS Class", {
|
||||
frappe.ui.form.on("LMS Batch", {
|
||||
onload: function (frm) {
|
||||
frm.set_query("class_student", "students", function (doc) {
|
||||
frm.set_query("student", "students", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
class_name: doc.name,
|
||||
ignore_user_type: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -15,7 +15,7 @@ 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",
|
||||
method: "lms.lms.doctype.lms_batch.lms_batch.fetch_lessons",
|
||||
args: {
|
||||
courses: frm.doc.courses,
|
||||
},
|
||||
@@ -14,16 +14,22 @@
|
||||
"column_break_4",
|
||||
"start_time",
|
||||
"end_time",
|
||||
"published",
|
||||
"section_break_rgfj",
|
||||
"medium",
|
||||
"category",
|
||||
"column_break_flwy",
|
||||
"seat_count",
|
||||
"paid_class",
|
||||
"section_break_6",
|
||||
"description",
|
||||
"batch_details",
|
||||
"students",
|
||||
"courses",
|
||||
"section_break_gsac",
|
||||
"paid_batch",
|
||||
"column_break_iens",
|
||||
"amount",
|
||||
"currency",
|
||||
"section_break_ubxi",
|
||||
"custom_component",
|
||||
"assessment_tab",
|
||||
@@ -65,13 +71,13 @@
|
||||
"fieldname": "students",
|
||||
"fieldtype": "Table",
|
||||
"label": "Students",
|
||||
"options": "Class Student"
|
||||
"options": "Batch Student"
|
||||
},
|
||||
{
|
||||
"fieldname": "courses",
|
||||
"fieldtype": "Table",
|
||||
"label": "Courses",
|
||||
"options": "Class Course"
|
||||
"options": "Batch Course"
|
||||
},
|
||||
{
|
||||
"fieldname": "start_date",
|
||||
@@ -81,7 +87,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"description": "The HTML code entered here will be displayed on the class details page.",
|
||||
"description": "The HTML code entered here will be displayed on the batch details page.",
|
||||
"fieldname": "custom_component",
|
||||
"fieldtype": "Code",
|
||||
"label": "Custom Component",
|
||||
@@ -89,9 +95,10 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "paid_class",
|
||||
"description": "Students will be enrolled in a paid batch once they complete the payment",
|
||||
"fieldname": "paid_batch",
|
||||
"fieldtype": "Check",
|
||||
"label": "Paid Class"
|
||||
"label": "Paid Batch"
|
||||
},
|
||||
{
|
||||
"fieldname": "seat_count",
|
||||
@@ -158,14 +165,48 @@
|
||||
"fieldname": "schedule_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Schedule"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_gsac",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Pricing"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_iens",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "paid_batch",
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount"
|
||||
},
|
||||
{
|
||||
"depends_on": "paid_batch",
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Currency",
|
||||
"options": "Currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "batch_details",
|
||||
"fieldtype": "Text Editor",
|
||||
"label": "Batch Details",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "published",
|
||||
"fieldtype": "Check",
|
||||
"label": "Published"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-23 14:23:07.706539",
|
||||
"modified": "2023-09-12 12:30:06.565104",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Class",
|
||||
"name": "LMS Batch",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
@@ -192,18 +233,6 @@
|
||||
"role": "Moderator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Class Evaluator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"show_title_field_in_link": 1,
|
||||
@@ -11,7 +11,7 @@ from frappe.utils import cint, format_date, format_datetime
|
||||
from lms.lms.utils import get_lessons
|
||||
|
||||
|
||||
class LMSClass(Document):
|
||||
class LMSBatch(Document):
|
||||
def validate(self):
|
||||
if self.seat_count:
|
||||
self.validate_seats_left()
|
||||
@@ -26,7 +26,7 @@ class LMSClass(Document):
|
||||
duplicates = {student for student in students if students.count(student) > 1}
|
||||
if len(duplicates):
|
||||
frappe.throw(
|
||||
_("Student {0} has already been added to this class.").format(
|
||||
_("Student {0} has already been added to this batch.").format(
|
||||
frappe.bold(next(iter(duplicates)))
|
||||
)
|
||||
)
|
||||
@@ -37,7 +37,7 @@ class LMSClass(Document):
|
||||
if len(duplicates):
|
||||
title = frappe.db.get_value("LMS Course", next(iter(duplicates)), "title")
|
||||
frappe.throw(
|
||||
_("Course {0} has already been added to this class.").format(frappe.bold(title))
|
||||
_("Course {0} has already been added to this batch.").format(frappe.bold(title))
|
||||
)
|
||||
|
||||
def validate_duplicate_assessments(self):
|
||||
@@ -48,7 +48,7 @@ class LMSClass(Document):
|
||||
assessment.assessment_type, assessment.assessment_name, "title"
|
||||
)
|
||||
frappe.throw(
|
||||
_("Assessment {0} has already been added to this class.").format(
|
||||
_("Assessment {0} has already been added to this batch.").format(
|
||||
frappe.bold(title)
|
||||
)
|
||||
)
|
||||
@@ -66,7 +66,7 @@ class LMSClass(Document):
|
||||
|
||||
def validate_seats_left(self):
|
||||
if cint(self.seat_count) < len(self.students):
|
||||
frappe.throw(_("There are no seats available in this class."))
|
||||
frappe.throw(_("There are no seats available in this batch."))
|
||||
|
||||
def validate_schedule(self):
|
||||
for schedule in self.scheduled_flow:
|
||||
@@ -82,32 +82,32 @@ class LMSClass(Document):
|
||||
|
||||
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(
|
||||
_("Row #{0} Start time cannot be outside the batch 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)
|
||||
_("Row #{0} End time cannot be outside the batch 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)
|
||||
_("Row #{0} Date cannot be outside the batch duration.").format(schedule.idx)
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def remove_student(student, class_name):
|
||||
def remove_student(student, batch_name):
|
||||
frappe.only_for("Moderator")
|
||||
frappe.db.delete("Class Student", {"student": student, "parent": class_name})
|
||||
frappe.db.delete("Batch Student", {"student": student, "parent": batch_name})
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def remove_course(course, parent):
|
||||
frappe.only_for("Moderator")
|
||||
frappe.db.delete("Class Course", {"course": course, "parent": parent})
|
||||
frappe.db.delete("Batch Course", {"course": course, "parent": parent})
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -118,7 +118,7 @@ def remove_assessment(assessment, parent):
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_live_class(
|
||||
class_name, title, duration, date, time, timezone, auto_recording, description=None
|
||||
batch_name, title, duration, date, time, timezone, auto_recording, description=None
|
||||
):
|
||||
date = format_date(date, "yyyy-mm-dd", True)
|
||||
frappe.only_for("Moderator")
|
||||
@@ -152,7 +152,7 @@ def create_live_class(
|
||||
"host": frappe.session.user,
|
||||
"date": date,
|
||||
"time": time,
|
||||
"class_name": class_name,
|
||||
"batch_name": batch_name,
|
||||
"password": data.get("password"),
|
||||
"description": description,
|
||||
"auto_recording": auto_recording,
|
||||
@@ -186,39 +186,49 @@ def authenticate():
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_class(
|
||||
def create_batch(
|
||||
title,
|
||||
start_date,
|
||||
end_date,
|
||||
description=None,
|
||||
batch_details=None,
|
||||
seat_count=0,
|
||||
start_time=None,
|
||||
end_time=None,
|
||||
medium="Online",
|
||||
category=None,
|
||||
paid_batch=0,
|
||||
amount=0,
|
||||
currency=None,
|
||||
name=None,
|
||||
published=0,
|
||||
):
|
||||
frappe.only_for("Moderator")
|
||||
if name:
|
||||
class_details = frappe.get_doc("LMS Class", name)
|
||||
doc = frappe.get_doc("LMS Batch", name)
|
||||
else:
|
||||
class_details = frappe.get_doc({"doctype": "LMS Class"})
|
||||
doc = frappe.get_doc({"doctype": "LMS Batch"})
|
||||
|
||||
class_details.update(
|
||||
doc.update(
|
||||
{
|
||||
"title": title,
|
||||
"start_date": start_date,
|
||||
"end_date": end_date,
|
||||
"description": description,
|
||||
"batch_details": batch_details,
|
||||
"seat_count": seat_count,
|
||||
"start_time": start_time,
|
||||
"end_time": end_time,
|
||||
"medium": medium,
|
||||
"category": category,
|
||||
"paid_batch": paid_batch,
|
||||
"amount": amount,
|
||||
"currency": currency,
|
||||
"published": published,
|
||||
}
|
||||
)
|
||||
class_details.save()
|
||||
return class_details
|
||||
doc.save()
|
||||
return doc
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -230,3 +240,25 @@ def fetch_lessons(courses):
|
||||
lessons.extend(get_lessons(course.get("course")))
|
||||
|
||||
return lessons
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_course(course, parent, name=None, evaluator=None):
|
||||
frappe.only_for("Moderator")
|
||||
if name:
|
||||
doc = frappe.get_doc("Batch Course", name)
|
||||
else:
|
||||
doc = frappe.new_doc("Batch Course")
|
||||
|
||||
doc.update(
|
||||
{
|
||||
"course": course,
|
||||
"evaluator": evaluator,
|
||||
"parent": parent,
|
||||
"parentfield": "courses",
|
||||
"parenttype": "LMS Batch",
|
||||
}
|
||||
)
|
||||
doc.save()
|
||||
|
||||
return doc.name
|
||||
@@ -8,10 +8,11 @@
|
||||
"course",
|
||||
"member",
|
||||
"member_name",
|
||||
"published",
|
||||
"column_break_3",
|
||||
"issue_date",
|
||||
"expiry_date",
|
||||
"class_name"
|
||||
"batch_name"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -55,16 +56,22 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "class_name",
|
||||
"fieldname": "batch_name",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Class Name",
|
||||
"options": "LMS Class"
|
||||
"label": "Batch",
|
||||
"options": "LMS Batch"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "published",
|
||||
"fieldtype": "Check",
|
||||
"label": "Publish on Participant Page"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-23 14:48:49.351394",
|
||||
"modified": "2023-09-13 11:03:23.479255",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Certificate",
|
||||
@@ -81,6 +88,18 @@
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Moderator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"date",
|
||||
"start_time",
|
||||
"end_time",
|
||||
"class_name",
|
||||
"batch_name",
|
||||
"section_break_6",
|
||||
"rating",
|
||||
"status",
|
||||
@@ -97,16 +97,16 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "class_name",
|
||||
"fieldname": "batch_name",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Class Name",
|
||||
"options": "LMS Class"
|
||||
"label": "Batch Name",
|
||||
"options": "LMS Batch"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-23 14:51:21.947160",
|
||||
"modified": "2023-08-23 14:51:21.947169",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Certificate Evaluation",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"field_order": [
|
||||
"course",
|
||||
"evaluator",
|
||||
"class_name",
|
||||
"batch_name",
|
||||
"column_break_4",
|
||||
"member",
|
||||
"member_name",
|
||||
@@ -100,16 +100,16 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "class_name",
|
||||
"fieldname": "batch_name",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Class Name",
|
||||
"options": "LMS Class"
|
||||
"label": "Batch Name",
|
||||
"options": "LMS Batch"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-23 14:50:37.618350",
|
||||
"modified": "2023-08-23 14:50:37.618352",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Certificate Request",
|
||||
|
||||
@@ -104,9 +104,7 @@ def update_meeting_details(eval, event, calendar):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_certificate_request(
|
||||
course, date, day, start_time, end_time, class_name=None
|
||||
):
|
||||
def create_certificate_request(course, date, day, start_time, end_time, batch=None):
|
||||
is_member = frappe.db.exists(
|
||||
{"doctype": "LMS Enrollment", "course": course, "member": frappe.session.user}
|
||||
)
|
||||
@@ -117,13 +115,13 @@ def create_certificate_request(
|
||||
eval.update(
|
||||
{
|
||||
"course": course,
|
||||
"evaluator": get_evaluator(course, class_name),
|
||||
"evaluator": get_evaluator(course, batch),
|
||||
"member": frappe.session.user,
|
||||
"date": date,
|
||||
"day": day,
|
||||
"start_time": start_time,
|
||||
"end_time": end_time,
|
||||
"class_name": class_name,
|
||||
"batch": batch,
|
||||
}
|
||||
)
|
||||
eval.save(ignore_permissions=True)
|
||||
|
||||
@@ -5,12 +5,11 @@ import json
|
||||
import random
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, validate_phone_number
|
||||
from frappe.utils import cint
|
||||
from frappe.utils.telemetry import capture
|
||||
from lms.lms.utils import get_chapters, can_create_courses
|
||||
from ...utils import generate_slug, validate_image
|
||||
from frappe import _
|
||||
import razorpay
|
||||
|
||||
|
||||
class LMSCourse(Document):
|
||||
@@ -364,115 +363,3 @@ def reorder_chapter(chapter_array):
|
||||
"idx": chapter_array.index(chap) + 1,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_payment_options(course, phone):
|
||||
validate_phone_number(phone, True)
|
||||
course_details = frappe.db.get_value(
|
||||
"LMS Course", course, ["name", "title", "currency", "course_price"], as_dict=True
|
||||
)
|
||||
razorpay_key = frappe.db.get_single_value("LMS Settings", "razorpay_key")
|
||||
client = get_client()
|
||||
order = create_order(client, course_details)
|
||||
|
||||
options = {
|
||||
"key_id": razorpay_key,
|
||||
"name": frappe.db.get_single_value("Website Settings", "app_name"),
|
||||
"description": _("Payment for {0} course").format(course_details["title"]),
|
||||
"order_id": order["id"],
|
||||
"amount": order["amount"] * 100,
|
||||
"currency": order["currency"],
|
||||
"prefill": {
|
||||
"name": frappe.db.get_value("User", frappe.session.user, "full_name"),
|
||||
"email": frappe.session.user,
|
||||
"contact": phone,
|
||||
},
|
||||
}
|
||||
return options
|
||||
|
||||
|
||||
def save_address(address):
|
||||
address = json.loads(address)
|
||||
address.update(
|
||||
{
|
||||
"address_title": frappe.db.get_value("User", frappe.session.user, "full_name"),
|
||||
"address_type": "Billing",
|
||||
"is_primary_address": 1,
|
||||
"email_id": frappe.session.user,
|
||||
}
|
||||
)
|
||||
doc = frappe.new_doc("Address")
|
||||
doc.update(address)
|
||||
doc.save(ignore_permissions=True)
|
||||
return doc.name
|
||||
|
||||
|
||||
def get_client():
|
||||
razorpay_key = frappe.db.get_single_value("LMS Settings", "razorpay_key")
|
||||
razorpay_secret = frappe.db.get_single_value("LMS Settings", "razorpay_secret")
|
||||
|
||||
if not razorpay_key and not razorpay_secret:
|
||||
frappe.throw(
|
||||
_(
|
||||
"There is a problem with the payment gateway. Please contact the Administrator to proceed."
|
||||
)
|
||||
)
|
||||
|
||||
return razorpay.Client(auth=(razorpay_key, razorpay_secret))
|
||||
|
||||
|
||||
def create_order(client, course_details):
|
||||
try:
|
||||
return client.order.create(
|
||||
{
|
||||
"amount": course_details.course_price * 100,
|
||||
"currency": course_details.currency,
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
frappe.throw(
|
||||
_("Error during payment: {0}. Please contact the Administrator.").format(e)
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def verify_payment(response, course, address, order_id):
|
||||
response = json.loads(response)
|
||||
client = get_client()
|
||||
client.utility.verify_payment_signature(
|
||||
{
|
||||
"razorpay_order_id": order_id,
|
||||
"razorpay_payment_id": response["razorpay_payment_id"],
|
||||
"razorpay_signature": response["razorpay_signature"],
|
||||
}
|
||||
)
|
||||
|
||||
return create_membership(address, response, course, client)
|
||||
|
||||
|
||||
def create_membership(address, response, course, client):
|
||||
try:
|
||||
address_name = save_address(address)
|
||||
membership = frappe.new_doc("LMS Enrollment")
|
||||
payment = client.payment.fetch(response["razorpay_payment_id"])
|
||||
|
||||
membership.update(
|
||||
{
|
||||
"member": frappe.session.user,
|
||||
"course": course,
|
||||
"address": address_name,
|
||||
"payment_received": 1,
|
||||
"order_id": response["razorpay_order_id"],
|
||||
"payment_id": response["razorpay_payment_id"],
|
||||
"amount": payment["amount"] / 100,
|
||||
"currency": payment["currency"],
|
||||
}
|
||||
)
|
||||
membership.save(ignore_permissions=True)
|
||||
|
||||
return f"/courses/{course}/learn/1.1"
|
||||
except Exception as e:
|
||||
frappe.throw(
|
||||
_("Error during payment: {0}. Please contact the Administrator.").format(e)
|
||||
)
|
||||
|
||||
@@ -7,22 +7,15 @@
|
||||
"field_order": [
|
||||
"course",
|
||||
"member_type",
|
||||
"batch_old",
|
||||
"payment",
|
||||
"column_break_3",
|
||||
"member",
|
||||
"member_name",
|
||||
"member_username",
|
||||
"billing_information_section",
|
||||
"address",
|
||||
"amount",
|
||||
"currency",
|
||||
"column_break_rvzn",
|
||||
"order_id",
|
||||
"payment_id",
|
||||
"payment_received",
|
||||
"section_break_8",
|
||||
"cohort",
|
||||
"subgroup",
|
||||
"batch_old",
|
||||
"column_break_12",
|
||||
"current_lesson",
|
||||
"progress",
|
||||
@@ -122,52 +115,15 @@
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "billing_information_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Billing Information"
|
||||
},
|
||||
{
|
||||
"fieldname": "address",
|
||||
"fieldname": "payment",
|
||||
"fieldtype": "Link",
|
||||
"label": "Address",
|
||||
"options": "Address"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "payment_received",
|
||||
"fieldtype": "Check",
|
||||
"label": "Payment Received"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_rvzn",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "order_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "Order ID"
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "Payment ID"
|
||||
},
|
||||
{
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Data",
|
||||
"label": "Amount",
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Currency",
|
||||
"options": "Currency"
|
||||
"label": "Payment",
|
||||
"options": "LMS Payment"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-17 13:52:49.450491",
|
||||
"modified": "2023-08-24 17:52:35.487141",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Enrollment",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"field_order": [
|
||||
"title",
|
||||
"host",
|
||||
"class_name",
|
||||
"batch_name",
|
||||
"password",
|
||||
"auto_recording",
|
||||
"column_break_astv",
|
||||
@@ -111,10 +111,10 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "class_name",
|
||||
"fieldname": "batch_name",
|
||||
"fieldtype": "Link",
|
||||
"label": "Class",
|
||||
"options": "LMS Class"
|
||||
"label": "Batch",
|
||||
"options": "LMS Batch"
|
||||
},
|
||||
{
|
||||
"default": "No Recording",
|
||||
@@ -126,7 +126,7 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-03-14 18:44:48.813102",
|
||||
"modified": "2023-03-14 18:44:48.813103",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Live Class",
|
||||
|
||||
@@ -34,7 +34,7 @@ class LMSLiveClass(Document):
|
||||
|
||||
def add_event_participants(self, event, calendar):
|
||||
participants = frappe.get_all(
|
||||
"Class Student", {"parent": self.class_name}, pluck="student"
|
||||
"Batch Student", {"parent": self.class_name}, pluck="student"
|
||||
)
|
||||
|
||||
participants.append(frappe.session.user)
|
||||
|
||||
8
lms/lms/doctype/lms_payment/lms_payment.js
Normal file
8
lms/lms/doctype/lms_payment/lms_payment.js
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2023, Frappe and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("LMS Payment", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
147
lms/lms/doctype/lms_payment/lms_payment.json
Normal file
147
lms/lms/doctype/lms_payment/lms_payment.json
Normal file
@@ -0,0 +1,147 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "format:PAY-{#####}",
|
||||
"creation": "2023-08-24 17:46:52.065763",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"member",
|
||||
"column_break_rqkd",
|
||||
"billing_name",
|
||||
"payment_received",
|
||||
"payment_details_section",
|
||||
"currency",
|
||||
"amount",
|
||||
"amount_with_gst",
|
||||
"column_break_yxpl",
|
||||
"order_id",
|
||||
"payment_id",
|
||||
"billing_details_section",
|
||||
"address",
|
||||
"column_break_monu",
|
||||
"gstin",
|
||||
"pan"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "order_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "Order ID"
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "Payment ID"
|
||||
},
|
||||
{
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Amount",
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Currency",
|
||||
"options": "Currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_rqkd",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "gstin",
|
||||
"fieldtype": "Data",
|
||||
"label": "GSTIN"
|
||||
},
|
||||
{
|
||||
"fieldname": "pan",
|
||||
"fieldtype": "Data",
|
||||
"label": "PAN"
|
||||
},
|
||||
{
|
||||
"fieldname": "address",
|
||||
"fieldtype": "Link",
|
||||
"label": "Address",
|
||||
"options": "Address"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "payment_received",
|
||||
"fieldtype": "Check",
|
||||
"label": "Payment Received"
|
||||
},
|
||||
{
|
||||
"fetch_from": "user.full_name",
|
||||
"fieldname": "billing_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Billing Name",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Payment Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_yxpl",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "billing_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Billing Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_monu",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "member",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Member",
|
||||
"options": "User",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.currency == \"INR\";",
|
||||
"fieldname": "amount_with_gst",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount with GST"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-09-12 10:40:22.721371",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Payment",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"show_title_field_in_link": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "billing_name"
|
||||
}
|
||||
9
lms/lms/doctype/lms_payment/lms_payment.py
Normal file
9
lms/lms/doctype/lms_payment/lms_payment.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 LMSPayment(Document):
|
||||
pass
|
||||
9
lms/lms/doctype/lms_payment/test_lms_payment.py
Normal file
9
lms/lms/doctype/lms_payment/test_lms_payment.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2023, Frappe and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestLMSPayment(FrappeTestCase):
|
||||
pass
|
||||
@@ -20,9 +20,13 @@
|
||||
"allow_student_progress",
|
||||
"payment_section",
|
||||
"razorpay_key",
|
||||
"default_currency",
|
||||
"column_break_cfcv",
|
||||
"razorpay_secret",
|
||||
"apply_gst",
|
||||
"column_break_cfcv",
|
||||
"default_currency",
|
||||
"show_usd_equivalent",
|
||||
"apply_rounding",
|
||||
"exception_country",
|
||||
"signup_settings_tab",
|
||||
"signup_settings_section",
|
||||
"terms_of_use",
|
||||
@@ -188,6 +192,7 @@
|
||||
"default": "0",
|
||||
"fieldname": "allow_student_progress",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Allow students to see each others progress in class"
|
||||
},
|
||||
{
|
||||
@@ -223,12 +228,37 @@
|
||||
"fieldname": "razorpay_secret",
|
||||
"fieldtype": "Password",
|
||||
"label": "Razorpay Secret"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "apply_gst",
|
||||
"fieldtype": "Check",
|
||||
"label": "Apply GST for India"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "show_usd_equivalent",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show USD Equivalent"
|
||||
},
|
||||
{
|
||||
"depends_on": "show_usd_equivalent",
|
||||
"fieldname": "exception_country",
|
||||
"fieldtype": "Table MultiSelect",
|
||||
"label": "Maintain Original Currency",
|
||||
"options": "Payment Country"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "apply_rounding",
|
||||
"fieldtype": "Check",
|
||||
"label": "Apply Rounding on Equivalent"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-02 18:59:01.267732",
|
||||
"modified": "2023-09-11 21:56:39.996898",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Settings",
|
||||
|
||||
0
lms/lms/doctype/payment_country/__init__.py
Normal file
0
lms/lms/doctype/payment_country/__init__.py
Normal file
8
lms/lms/doctype/payment_country/payment_country.js
Normal file
8
lms/lms/doctype/payment_country/payment_country.js
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2023, Frappe and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Payment Country", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
33
lms/lms/doctype/payment_country/payment_country.json
Normal file
33
lms/lms/doctype/payment_country/payment_country.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2023-09-11 11:53:16.253740",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"country"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "country",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Country",
|
||||
"options": "Country"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-09-11 12:04:56.048632",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Payment Country",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
9
lms/lms/doctype/payment_country/payment_country.py
Normal file
9
lms/lms/doctype/payment_country/payment_country.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 PaymentCountry(Document):
|
||||
pass
|
||||
9
lms/lms/doctype/payment_country/test_payment_country.py
Normal file
9
lms/lms/doctype/payment_country/test_payment_country.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2023, Frappe and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestPaymentCountry(FrappeTestCase):
|
||||
pass
|
||||
244
lms/lms/utils.py
244
lms/lms/utils.py
@@ -1,6 +1,10 @@
|
||||
import re
|
||||
import string
|
||||
import frappe
|
||||
import json
|
||||
import razorpay
|
||||
import requests
|
||||
import base64
|
||||
from frappe import _
|
||||
from frappe.desk.doctype.dashboard_chart.dashboard_chart import get_result
|
||||
from frappe.desk.doctype.notification_log.notification_log import make_notification_logs
|
||||
@@ -13,9 +17,9 @@ from frappe.utils import (
|
||||
format_date,
|
||||
get_datetime,
|
||||
getdate,
|
||||
validate_phone_number,
|
||||
)
|
||||
from frappe.utils.dateutils import get_period
|
||||
|
||||
from lms.lms.md import find_macros, markdown_to_html
|
||||
|
||||
RE_SLUG_NOTALLOWED = re.compile("[^a-z0-9]+")
|
||||
@@ -796,13 +800,13 @@ def has_graded_assessment(submission):
|
||||
return False if status == "Not Graded" else True
|
||||
|
||||
|
||||
def get_evaluator(course, class_name=None):
|
||||
def get_evaluator(course, batch=None):
|
||||
evaluator = None
|
||||
|
||||
if class_name:
|
||||
if batch:
|
||||
evaluator = frappe.db.get_value(
|
||||
"Class Course",
|
||||
{"parent": class_name, "course": course},
|
||||
"Batch Course",
|
||||
{"parent": batch, "course": course},
|
||||
"evaluator",
|
||||
)
|
||||
|
||||
@@ -828,3 +832,233 @@ def get_upcoming_evals(student, courses):
|
||||
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
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_payment_options(doctype, docname, phone, country):
|
||||
if not frappe.db.exists(doctype, docname):
|
||||
frappe.throw(_("Invalid document provided."))
|
||||
|
||||
validate_phone_number(phone, True)
|
||||
details = get_details(doctype, docname)
|
||||
details.amount, details.currency = check_multicurrency(
|
||||
details.amount, details.currency
|
||||
)
|
||||
if details.currency == "INR":
|
||||
details.amount, details.gst_applied = apply_gst(details.amount, country)
|
||||
|
||||
client = get_client()
|
||||
order = create_order(client, details.amount, details.currency)
|
||||
|
||||
options = {
|
||||
"key_id": frappe.db.get_single_value("LMS Settings", "razorpay_key"),
|
||||
"name": frappe.db.get_single_value("Website Settings", "app_name"),
|
||||
"description": _("Payment for {0} course").format(details["title"]),
|
||||
"order_id": order["id"],
|
||||
"amount": order["amount"] * 100,
|
||||
"currency": order["currency"],
|
||||
"prefill": {
|
||||
"name": frappe.db.get_value("User", frappe.session.user, "full_name"),
|
||||
"email": frappe.session.user,
|
||||
"contact": phone,
|
||||
},
|
||||
}
|
||||
return options
|
||||
|
||||
|
||||
def check_multicurrency(amount, currency):
|
||||
show_usd_equivalent = frappe.db.get_single_value("LMS Settings", "show_usd_equivalent")
|
||||
exception_country = frappe.get_all(
|
||||
"Payment Country", filters={"parent": "LMS Settings"}, pluck="country"
|
||||
)
|
||||
apply_rounding = frappe.db.get_single_value("LMS Settings", "apply_rounding")
|
||||
country = frappe.db.get_value("User", frappe.session.user, "country")
|
||||
|
||||
if not show_usd_equivalent or currency == "USD":
|
||||
return amount, currency
|
||||
|
||||
if exception_country and country in exception_country:
|
||||
return amount, currency
|
||||
|
||||
exchange_rate = get_current_exchange_rate(currency, "USD")
|
||||
amount = amount * exchange_rate
|
||||
currency = "USD"
|
||||
|
||||
if apply_rounding and amount % 100 != 0:
|
||||
amount = amount + 100 - amount % 100
|
||||
|
||||
return amount, currency
|
||||
|
||||
|
||||
def apply_gst(amount, country=None):
|
||||
gst_applied = False
|
||||
apply_gst = frappe.db.get_single_value("LMS Settings", "apply_gst")
|
||||
|
||||
if not country:
|
||||
country = frappe.db.get_value("User", frappe.session.user, "country")
|
||||
|
||||
if apply_gst and country == "India":
|
||||
gst_applied = True
|
||||
amount = amount * 1.18
|
||||
|
||||
return amount, gst_applied
|
||||
|
||||
|
||||
def get_details(doctype, docname):
|
||||
if doctype == "LMS Course":
|
||||
details = frappe.db.get_value(
|
||||
"LMS Course",
|
||||
docname,
|
||||
["name", "title", "paid_course", "currency", "course_price as amount"],
|
||||
as_dict=True,
|
||||
)
|
||||
if not details.paid_course:
|
||||
frappe.throw(_("This course is free."))
|
||||
else:
|
||||
details = frappe.db.get_value(
|
||||
"LMS Batch",
|
||||
docname,
|
||||
["name", "title", "paid_batch", "currency", "amount"],
|
||||
as_dict=True,
|
||||
)
|
||||
if not details.paid_batch:
|
||||
frappe.throw(_("To join this batch, please contact the Administrator."))
|
||||
|
||||
return details
|
||||
|
||||
|
||||
def save_address(address):
|
||||
address.update(
|
||||
{
|
||||
"address_title": frappe.db.get_value("User", frappe.session.user, "full_name"),
|
||||
"address_type": "Billing",
|
||||
"is_primary_address": 1,
|
||||
"email_id": frappe.session.user,
|
||||
}
|
||||
)
|
||||
doc = frappe.new_doc("Address")
|
||||
doc.update(address)
|
||||
doc.save(ignore_permissions=True)
|
||||
return doc.name
|
||||
|
||||
|
||||
def get_client():
|
||||
settings = frappe.get_single("LMS Settings")
|
||||
razorpay_key = settings.razorpay_key
|
||||
razorpay_secret = settings.get_password("razorpay_secret", raise_exception=True)
|
||||
|
||||
if not razorpay_key and not razorpay_secret:
|
||||
frappe.throw(
|
||||
_(
|
||||
"There is a problem with the payment gateway. Please contact the Administrator to proceed."
|
||||
)
|
||||
)
|
||||
|
||||
return razorpay.Client(auth=(razorpay_key, razorpay_secret))
|
||||
|
||||
|
||||
def create_order(client, amount, currency):
|
||||
try:
|
||||
return client.order.create(
|
||||
{
|
||||
"amount": amount * 100,
|
||||
"currency": currency,
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
frappe.throw(
|
||||
_("Error during payment: {0}. Please contact the Administrator.").format(e)
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def verify_payment(response, doctype, docname, address, order_id):
|
||||
response = json.loads(response)
|
||||
client = get_client()
|
||||
client.utility.verify_payment_signature(
|
||||
{
|
||||
"razorpay_order_id": order_id,
|
||||
"razorpay_payment_id": response["razorpay_payment_id"],
|
||||
"razorpay_signature": response["razorpay_signature"],
|
||||
}
|
||||
)
|
||||
|
||||
payment = record_payment(address, response, client, doctype, docname)
|
||||
if doctype == "LMS Course":
|
||||
return create_membership(docname, payment)
|
||||
else:
|
||||
return add_student_to_batch(docname, payment)
|
||||
|
||||
|
||||
def record_payment(address, response, client, doctype, docname):
|
||||
address = frappe._dict(json.loads(address))
|
||||
address_name = save_address(address)
|
||||
|
||||
payment_details = get_payment_details(doctype, docname, address)
|
||||
payment_doc = frappe.new_doc("LMS Payment")
|
||||
payment_doc.update(
|
||||
{
|
||||
"member": frappe.session.user,
|
||||
"billing_name": address.billing_name,
|
||||
"address": address_name,
|
||||
"payment_received": 1,
|
||||
"order_id": response["razorpay_order_id"],
|
||||
"payment_id": response["razorpay_payment_id"],
|
||||
"amount": payment_details["amount"],
|
||||
"currency": payment_details["currency"],
|
||||
"amount_with_gst": payment_details["amount_with_gst"],
|
||||
"gstin": address.gstin,
|
||||
"pan": address.pan,
|
||||
}
|
||||
)
|
||||
payment_doc.save(ignore_permissions=True)
|
||||
return payment_doc.name
|
||||
|
||||
|
||||
def get_payment_details(doctype, docname, address):
|
||||
amount_field = "course_price" if doctype == "LMS Course" else "amount"
|
||||
amount = frappe.db.get_value(doctype, docname, amount_field)
|
||||
currency = frappe.db.get_value(doctype, docname, "currency")
|
||||
amount_with_gst = 0
|
||||
|
||||
amount, currency = check_multicurrency(amount, currency)
|
||||
if currency == "INR" and address.country == "India":
|
||||
amount_with_gst, gst_applied = apply_gst(amount, address.country)
|
||||
|
||||
return {
|
||||
"amount": amount,
|
||||
"currency": currency,
|
||||
"amount_with_gst": amount_with_gst,
|
||||
}
|
||||
|
||||
|
||||
def create_membership(course, payment):
|
||||
membership = frappe.new_doc("LMS Enrollment")
|
||||
membership.update(
|
||||
{"member": frappe.session.user, "course": course, "payment": payment}
|
||||
)
|
||||
membership.save(ignore_permissions=True)
|
||||
return f"/courses/{course}/learn/1.1"
|
||||
|
||||
|
||||
def add_student_to_batch(batchname, payment):
|
||||
student = frappe.new_doc("Batch Student")
|
||||
student.update(
|
||||
{
|
||||
"student": frappe.session.user,
|
||||
"payment": payment,
|
||||
"parent": batchname,
|
||||
"parenttype": "LMS Batch",
|
||||
"parentfield": "students",
|
||||
}
|
||||
)
|
||||
student.save(ignore_permissions=True)
|
||||
return f"/batches/{batchname}"
|
||||
|
||||
|
||||
def get_current_exchange_rate(source, target="USD"):
|
||||
url = f"https://api.frankfurter.app/latest?from={source}&to={target}"
|
||||
|
||||
response = requests.request("GET", url)
|
||||
details = response.json()
|
||||
return details["rates"][target]
|
||||
|
||||
@@ -3,7 +3,7 @@ frappe.ready(function () {
|
||||
let data = frappe.web_form.get_values();
|
||||
if (data.class) {
|
||||
setTimeout(() => {
|
||||
window.location.href = `/classes/${data.class}`;
|
||||
window.location.href = `/batches/${data.class}`;
|
||||
}, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"login_required": 1,
|
||||
"max_attachment_size": 0,
|
||||
"meta_image": "/files/og_image_web_form_evaluation_68ddf18e.png",
|
||||
"modified": "2023-08-23 14:37:03.086303",
|
||||
"modified": "2023-08-23 14:37:03.086304",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "evaluation",
|
||||
@@ -63,13 +63,13 @@
|
||||
},
|
||||
{
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"fieldname": "class_name",
|
||||
"fieldname": "batch_name",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"label": "Class Name",
|
||||
"label": "Batch Name",
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"options": "LMS Class",
|
||||
"options": "LMS Batch",
|
||||
"read_only": 0,
|
||||
"reqd": 0,
|
||||
"show_in_filter": 0
|
||||
|
||||
@@ -64,4 +64,8 @@ execute:frappe.permissions.reset_perms("LMS Certificate Evaluation")
|
||||
lms.patches.v1_0.paid_certificate_to_paid_course #18-08-2023
|
||||
lms.patches.v1_0.revert_class_registration #18-08-2023
|
||||
lms.patches.v1_0.rename_lms_batch_doctype
|
||||
lms.patches.v1_0.rename_lms_batch_membership_doctype
|
||||
lms.patches.v1_0.rename_lms_batch_membership_doctype
|
||||
lms.patches.v1_0.rename_lms_class_to_lms_batch
|
||||
lms.patches.v1_0.rename_classes_in_navbar
|
||||
lms.patches.v1_0.publish_batches
|
||||
lms.patches.v1_0.publish_certificates
|
||||
9
lms/patches/v1_0/publish_batches.py
Normal file
9
lms/patches/v1_0/publish_batches.py
Normal file
@@ -0,0 +1,9 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc("lms", "doctype", "lms_batch")
|
||||
batches = frappe.get_all("LMS Batch", pluck="name")
|
||||
|
||||
for batch in batches:
|
||||
frappe.db.set_value("LMS Batch", batch, "Published", 1)
|
||||
9
lms/patches/v1_0/publish_certificates.py
Normal file
9
lms/patches/v1_0/publish_certificates.py
Normal file
@@ -0,0 +1,9 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc("lms", "doctype", "lms_certificate")
|
||||
certificates = frappe.get_all("LMS Certificate", pluck="name")
|
||||
|
||||
for certificate in certificates:
|
||||
frappe.db.set_value("LMS Certificate", certificate, "published", 1)
|
||||
12
lms/patches/v1_0/rename_classes_in_navbar.py
Normal file
12
lms/patches/v1_0/rename_classes_in_navbar.py
Normal file
@@ -0,0 +1,12 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
frappe.db.set_value(
|
||||
"Top Bar Item",
|
||||
{"url": "/classes"},
|
||||
{
|
||||
"label": "Batches",
|
||||
"url": "/batches",
|
||||
},
|
||||
)
|
||||
32
lms/patches/v1_0/rename_lms_class_to_lms_batch.py
Normal file
32
lms/patches/v1_0/rename_lms_class_to_lms_batch.py
Normal file
@@ -0,0 +1,32 @@
|
||||
import frappe
|
||||
from frappe.model.rename_doc import rename_doc
|
||||
|
||||
|
||||
def execute():
|
||||
if frappe.db.exists("DocField", {"fieldname": "students", "parent": "LMS Batch"}):
|
||||
return
|
||||
|
||||
rename_lms_class()
|
||||
rename_class_student()
|
||||
rename_class_courses()
|
||||
|
||||
|
||||
def rename_lms_class():
|
||||
frappe.flags.ignore_route_conflict_validation = True
|
||||
rename_doc("DocType", "LMS Class", "LMS Batch")
|
||||
frappe.flags.ignore_route_conflict_validation = False
|
||||
frappe.reload_doctype("LMS Batch", force=True)
|
||||
|
||||
|
||||
def rename_class_student():
|
||||
frappe.flags.ignore_route_conflict_validation = True
|
||||
rename_doc("DocType", "Class Student", "Batch Student")
|
||||
frappe.flags.ignore_route_conflict_validation = False
|
||||
frappe.reload_doctype("Batch Student", force=True)
|
||||
|
||||
|
||||
def rename_class_courses():
|
||||
frappe.flags.ignore_route_conflict_validation = True
|
||||
rename_doc("DocType", "Class Course", "Batch Course")
|
||||
frappe.flags.ignore_route_conflict_validation = False
|
||||
frappe.reload_doctype("Batch Course", force=True)
|
||||
@@ -911,7 +911,7 @@ input[type=checkbox] {
|
||||
.profile-name-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 1rem 0 0.25rem;
|
||||
margin: 0.5rem 0 0.25rem;
|
||||
}
|
||||
|
||||
@media (max-width: 550px) {
|
||||
@@ -2188,25 +2188,12 @@ select {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.awesomplete ul li:last-child {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.students-parent {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, 150px);
|
||||
grid-gap: 1rem;
|
||||
}
|
||||
|
||||
.btn-remove-course {
|
||||
opacity: 0;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.btn-remove-course:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.rows .grid-row .data-row,
|
||||
.rows .grid-row .grid-footer-toolbar,
|
||||
.grid-form-heading {
|
||||
@@ -2302,6 +2289,10 @@ select {
|
||||
padding-right: 1rem !important;
|
||||
}
|
||||
|
||||
.class-overlay {
|
||||
top: 30%;
|
||||
}
|
||||
|
||||
.course-list-menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -2346,6 +2337,19 @@ select {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.batch-course-list .cards-parent {
|
||||
row-gap: 3rem
|
||||
}
|
||||
|
||||
.embed-tool__caption {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.card-buttons {
|
||||
display: flex;
|
||||
position: relative;
|
||||
top: 10%;
|
||||
left: 80%;
|
||||
z-index: 10;
|
||||
width: fit-content;
|
||||
}
|
||||
@@ -104,11 +104,18 @@
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<polyline points="12 6 12 12 16 14"></polyline>
|
||||
</svg>
|
||||
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" id="icon-success" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 18.75C14.8325 18.75 18.75 14.8325 18.75 10C18.75 5.16751 14.8325 1.25 10 1.25C5.16751 1.25 1.25 5.16751 1.25 10C1.25 14.8325 5.16751 18.75 10 18.75ZM13.966 7.48104C14.1856 7.21471 14.1477 6.8208 13.8813 6.60122C13.615 6.38164 13.2211 6.41954 13.0015 6.68587L8.68984 11.9155L7.01289 9.74823C6.80165 9.47524 6.40911 9.42517 6.13611 9.6364C5.86311 9.84764 5.81304 10.2402 6.02428 10.5132L8.18004 13.2993C8.29633 13.4495 8.47467 13.5388 8.66468 13.5417C8.85468 13.5447 9.0357 13.461 9.15658 13.3144L13.966 7.48104Z" fill="#171717"/>
|
||||
</svg>
|
||||
|
||||
<svg width="16" height="16" id="icon-drag" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 3C4 3.82843 4.67157 4.5 5.5 4.5C6.32843 4.5 7 3.82843 7 3C7 2.17157 6.32843 1.5 5.5 1.5C4.67157 1.5 4 2.17157 4 3ZM5.5 9.5C4.67157 9.5 4 8.82843 4 8C4 7.17157 4.67157 6.5 5.5 6.5C6.32843 6.5 7 7.17157 7 8C7 8.82843 6.32843 9.5 5.5 9.5ZM5.5 14.5C4.67157 14.5 4 13.8284 4 13C4 12.1716 4.67157 11.5 5.5 11.5C6.32843 11.5 7 12.1716 7 13C7 13.8284 6.32843 14.5 5.5 14.5ZM9 3C9 3.82843 9.67157 4.5 10.5 4.5C11.3284 4.5 12 3.82843 12 3C12 2.17157 11.3284 1.5 10.5 1.5C9.67157 1.5 9 2.17157 9 3ZM10.5 9.5C9.67157 9.5 9 8.82843 9 8C9 7.17157 9.67157 6.5 10.5 6.5C11.3284 6.5 12 7.17157 12 8C12 8.82843 11.3284 9.5 10.5 9.5ZM10.5 14.5C9.67157 14.5 9 13.8284 9 13C9 12.1716 9.67157 11.5 10.5 11.5C11.3284 11.5 12 12.1716 12 13C12 13.8284 11.3284 14.5 10.5 14.5Z" fill="#171717"/>
|
||||
</svg>
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="icon-clock" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="feather feather-clock">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<polyline points="12 6 12 12 16 14"></polyline>
|
||||
</svg>
|
||||
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
@@ -37,8 +37,8 @@ frappe.ready(() => {
|
||||
show_no_preview_dialog(e);
|
||||
});
|
||||
|
||||
$("#create-class").click((e) => {
|
||||
open_class_dialog(e);
|
||||
$("#create-batch").click((e) => {
|
||||
open_batch_dialog(e);
|
||||
});
|
||||
|
||||
$("#course-filter").change((e) => {
|
||||
@@ -250,37 +250,55 @@ const show_no_preview_dialog = (e) => {
|
||||
$("#no-preview-modal").modal("show");
|
||||
};
|
||||
|
||||
const open_class_dialog = () => {
|
||||
this.class_dialog = new frappe.ui.Dialog({
|
||||
title: __("New Class"),
|
||||
const open_batch_dialog = () => {
|
||||
this.batch_dialog = new frappe.ui.Dialog({
|
||||
title: __("New Batch"),
|
||||
fields: [
|
||||
{
|
||||
fieldtype: "Data",
|
||||
label: __("Title"),
|
||||
fieldname: "title",
|
||||
reqd: 1,
|
||||
default: class_info && class_info.title,
|
||||
default: batch_info && batch_info.title,
|
||||
},
|
||||
{
|
||||
fieldtype: "Check",
|
||||
label: __("Published"),
|
||||
fieldname: "published",
|
||||
default: batch_info && batch_info.published,
|
||||
},
|
||||
{
|
||||
fieldtype: "Column Break",
|
||||
},
|
||||
{
|
||||
fieldtype: "Int",
|
||||
label: __("Seat Count"),
|
||||
fieldname: "seat_count",
|
||||
default: batch_info && batch_info.seat_count,
|
||||
},
|
||||
{
|
||||
fieldtype: "Section Break",
|
||||
},
|
||||
{
|
||||
fieldtype: "Date",
|
||||
label: __("Start Date"),
|
||||
fieldname: "start_date",
|
||||
reqd: 1,
|
||||
default: class_info && class_info.start_date,
|
||||
default: batch_info && batch_info.start_date,
|
||||
},
|
||||
{
|
||||
fieldtype: "Date",
|
||||
label: __("End Date"),
|
||||
fieldname: "end_date",
|
||||
reqd: 1,
|
||||
default: class_info && class_info.end_date,
|
||||
default: batch_info && batch_info.end_date,
|
||||
},
|
||||
{
|
||||
fieldtype: "Select",
|
||||
label: __("Medium"),
|
||||
fieldname: "medium",
|
||||
options: ["Online", "Offline"],
|
||||
default: (class_info && class_info.medium) || "Online",
|
||||
default: (batch_info && batch_info.medium) || "Online",
|
||||
},
|
||||
{
|
||||
fieldtype: "Column Break",
|
||||
@@ -289,26 +307,20 @@ const open_class_dialog = () => {
|
||||
fieldtype: "Time",
|
||||
label: __("Start Time"),
|
||||
fieldname: "start_time",
|
||||
default: class_info && class_info.start_time,
|
||||
default: batch_info && batch_info.start_time,
|
||||
},
|
||||
{
|
||||
fieldtype: "Time",
|
||||
label: __("End Time"),
|
||||
fieldname: "end_time",
|
||||
default: class_info && class_info.end_time,
|
||||
},
|
||||
{
|
||||
fieldtype: "Int",
|
||||
label: __("Seat Count"),
|
||||
fieldname: "seat_count",
|
||||
default: class_info && class_info.seat_count,
|
||||
default: batch_info && batch_info.end_time,
|
||||
},
|
||||
{
|
||||
fieldtype: "Link",
|
||||
label: __("Category"),
|
||||
fieldname: "category",
|
||||
options: "LMS Category",
|
||||
default: class_info && class_info.category,
|
||||
default: batch_info && batch_info.category,
|
||||
},
|
||||
{
|
||||
fieldtype: "Section Break",
|
||||
@@ -317,43 +329,74 @@ const open_class_dialog = () => {
|
||||
fieldtype: "Small Text",
|
||||
label: __("Description"),
|
||||
fieldname: "description",
|
||||
default: class_info && class_info.description,
|
||||
default: batch_info && batch_info.description,
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldtype: "Text Editor",
|
||||
label: __("Batch Details"),
|
||||
fieldname: "batch_details",
|
||||
default: batch_info && batch_info.batch_details,
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldtype: "Section Break",
|
||||
label: __("Pricing"),
|
||||
fieldname: "pricing",
|
||||
},
|
||||
{
|
||||
fieldtype: "Check",
|
||||
label: __("Paid Batch"),
|
||||
fieldname: "paid_batch",
|
||||
default: batch_info && batch_info.paid_batch,
|
||||
},
|
||||
{
|
||||
fieldtype: "Currency",
|
||||
label: __("Amount"),
|
||||
fieldname: "amount",
|
||||
default: batch_info && batch_info.amount,
|
||||
mandatory_depends_on: "paid_batch",
|
||||
depends_on: "paid_batch",
|
||||
},
|
||||
{
|
||||
fieldtype: "Link",
|
||||
label: __("Currency"),
|
||||
fieldname: "currency",
|
||||
options: "Currency",
|
||||
default: batch_info && batch_info.currency,
|
||||
mandatory_depends_on: "paid_batch",
|
||||
depends_on: "paid_batch",
|
||||
only_select: 1,
|
||||
},
|
||||
],
|
||||
primary_action_label: __("Save"),
|
||||
primary_action: (values) => {
|
||||
save_class(values);
|
||||
save_batch(values);
|
||||
},
|
||||
});
|
||||
this.class_dialog.show();
|
||||
this.batch_dialog.show();
|
||||
};
|
||||
|
||||
const save_class = (values) => {
|
||||
const save_batch = (values) => {
|
||||
let args = {};
|
||||
if (batch_info) {
|
||||
args = Object.assign(batch_info, values);
|
||||
} else {
|
||||
args = values;
|
||||
}
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_class.lms_class.create_class",
|
||||
args: {
|
||||
title: values.title,
|
||||
start_date: values.start_date,
|
||||
end_date: values.end_date,
|
||||
description: values.description,
|
||||
seat_count: values.seat_count,
|
||||
start_time: values.start_time,
|
||||
end_time: values.end_time,
|
||||
medium: values.medium,
|
||||
category: values.category,
|
||||
name: class_info && class_info.name,
|
||||
},
|
||||
method: "lms.lms.doctype.lms_batch.lms_batch.create_batch",
|
||||
args: args,
|
||||
callback: (r) => {
|
||||
if (r.message) {
|
||||
frappe.show_alert({
|
||||
message: class_info
|
||||
? __("Class Updated")
|
||||
: __("Class Created"),
|
||||
message: batch_info
|
||||
? __("Batch Updated")
|
||||
: __("Batch Created"),
|
||||
indicator: "green",
|
||||
});
|
||||
this.class_dialog.hide();
|
||||
window.location.href = `/classes/${r.message.name}`;
|
||||
this.batch_dialog.hide();
|
||||
window.location.href = `/batches/details/${r.message.name}`;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
{% set course = frappe.db.get_value("LMS Course", certificate.course, ["title", "name", "image"], as_dict=True) %}
|
||||
|
||||
<div class="common-card-style column-card medium">
|
||||
<div class="font-weight-bold">
|
||||
<div class="bold-heading">
|
||||
{{ course.title }}
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -29,8 +29,8 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="vertically-center small">
|
||||
<a class="dark-links" href="/classes">
|
||||
{{ _("All Classes") }}
|
||||
<a class="dark-links" href="/batches">
|
||||
{{ _("All Batches") }}
|
||||
</a>
|
||||
<img class="icon icon-sm mr-0" src="/assets/lms/icons/chevron-right.svg">
|
||||
<span class="breadcrumb-destination">{{ _("Assignment Submission") }}</span>
|
||||
@@ -52,13 +52,18 @@
|
||||
|
||||
{% macro SubmissionForm(assignment) %}
|
||||
<article class="field-parent">
|
||||
{% if submission.name and is_moderator %}
|
||||
<div class="field-group">
|
||||
<div class="bold-heading">
|
||||
{{ _("Student Name") }}
|
||||
{% if submission.name %}
|
||||
<div class="alert alert-info">
|
||||
{{ _("You've successfully submitted the assignment. Once the moderator grades your submission, you'll find the details here. Feel free to make edits to your submission if needed.") }}
|
||||
</div>
|
||||
{{ submission.member_name }}
|
||||
</div>
|
||||
{% if is_moderator %}
|
||||
<div class="field-group">
|
||||
<div class="bold-heading">
|
||||
{{ _("Student Name") }}
|
||||
</div>
|
||||
{{ submission.member_name }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<div class="field-group">
|
||||
@@ -81,14 +86,15 @@
|
||||
<div class="btn btn-default btn-sm btn-upload mt-2 {% if submission.assignment_attachment %} hide {% endif %}" data-type="{{ assignment.type }}">
|
||||
{{ _("Browse").format(assignment.type) }}
|
||||
</div>
|
||||
<div class="field-input flex justify-between align-center {% if not submission.assignment_attachment %} hide {% endif %}" id="assignment-preview">
|
||||
<div class="field-input flex justify-between align-center overflow-auto
|
||||
{% if not submission.assignment_attachment %} hide {% endif %}" id="assignment-preview">
|
||||
<a class="clickable" {% if submission.assignment_attachment %} href="{{ submission.assignment_attachment }}" {% endif %}>
|
||||
{% if submission.assignment_attachment %} {{ submission.assignment_attachment }} {% endif %}
|
||||
</a>
|
||||
<span class="btn btn-default btn-sm btn-close {% if not submission %} hide {% endif %}">
|
||||
{{ _("Clear") }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="btn btn-default btn-sm btn-close {% if not submission %} hide {% endif %} mt-2">
|
||||
{{ _("Clear") }}
|
||||
</span>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
|
||||
@@ -116,15 +116,6 @@
|
||||
|
||||
{%- block script %}
|
||||
{{ super() }}
|
||||
{% if is_moderator %}
|
||||
<script>
|
||||
frappe.boot.user = {
|
||||
"can_create": [],
|
||||
"can_select": ["LMS Quiz"],
|
||||
"can_read": ["LMS Quiz"]
|
||||
};
|
||||
</script>
|
||||
{% endif %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@latest"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/paragraph@latest"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/header@latest"></script>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
frappe.ready(() => {
|
||||
let self = this;
|
||||
this.quiz_in_lesson = [];
|
||||
|
||||
frappe.telemetry.capture("on_lesson_creation_page", "lms");
|
||||
|
||||
@@ -15,7 +14,6 @@ frappe.ready(() => {
|
||||
}
|
||||
|
||||
setup_editor();
|
||||
fetch_quiz_list();
|
||||
|
||||
$("#save-lesson").click((e) => {
|
||||
save_lesson(e);
|
||||
@@ -90,11 +88,10 @@ const parse_string_to_lesson = () => {
|
||||
});
|
||||
} else if (block.includes("{{ Quiz")) {
|
||||
let quiz = block.match(/'([^']+)'/)[1];
|
||||
this.quiz_in_lesson.push(quiz);
|
||||
lesson_blocks.push({
|
||||
type: "quiz",
|
||||
data: {
|
||||
quiz: [quiz],
|
||||
quiz: quiz,
|
||||
},
|
||||
});
|
||||
} else if (block.includes("{{ Video")) {
|
||||
@@ -156,9 +153,7 @@ const parse_lesson_to_string = (data) => {
|
||||
if (block.type == "youtube") {
|
||||
lesson_content += `{{ YouTubeVideo("${block.data.youtube}") }}\n`;
|
||||
} else if (block.type == "quiz") {
|
||||
block.data.quiz.forEach((quiz) => {
|
||||
lesson_content += `{{ Quiz("${quiz}") }}\n`;
|
||||
});
|
||||
lesson_content += `{{ Quiz("${block.data.quiz}") }}\n`;
|
||||
} else if (block.type == "upload") {
|
||||
let url = block.data.file_url;
|
||||
lesson_content += block.data.is_video
|
||||
@@ -233,15 +228,6 @@ const validate_mandatory = (lesson_content) => {
|
||||
}
|
||||
};
|
||||
|
||||
const fetch_quiz_list = () => {
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_quiz.lms_quiz.get_user_quizzes",
|
||||
callback: (r) => {
|
||||
self.quiz_list = r.message;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const is_video = (url) => {
|
||||
let video_types = ["mov", "mp4", "mkv"];
|
||||
let video_extension = url.split(".").pop();
|
||||
@@ -339,57 +325,10 @@ class Quiz {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get_fields() {
|
||||
let fields = [
|
||||
{
|
||||
fieldname: "start_section",
|
||||
fieldtype: "Section Break",
|
||||
label: __(
|
||||
"To create a new quiz, click on the button below. Once you have created the new quiz you can come back to this lesson and add it from here."
|
||||
),
|
||||
},
|
||||
{
|
||||
fieldname: "create_quiz",
|
||||
fieldtype: "Button",
|
||||
label: __("Create Quiz"),
|
||||
click: () => {
|
||||
window.location.href = "/quizzes";
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "quiz_information",
|
||||
fieldtype: "HTML",
|
||||
options: __("OR"),
|
||||
},
|
||||
{
|
||||
fieldname: "quiz_list_section",
|
||||
fieldtype: "Section Break",
|
||||
label: __("Select a exisitng quiz to add to this lesson."),
|
||||
},
|
||||
];
|
||||
let break_index = Math.ceil(self.quiz_list.length / 2) + 4;
|
||||
|
||||
self.quiz_list.forEach((quiz) => {
|
||||
fields.push({
|
||||
fieldname: quiz.name,
|
||||
fieldtype: "Check",
|
||||
label: quiz.title,
|
||||
default: self.quiz_in_lesson.includes(quiz.name) ? 1 : 0,
|
||||
read_only: self.quiz_in_lesson.includes(quiz.name) ? 1 : 0,
|
||||
});
|
||||
});
|
||||
|
||||
fields.splice(break_index, 0, {
|
||||
fieldname: "column_break",
|
||||
fieldtype: "Column Break",
|
||||
});
|
||||
return fields;
|
||||
}
|
||||
|
||||
render() {
|
||||
this.wrapper = document.createElement("div");
|
||||
if (this.data && this.data.quiz) {
|
||||
$(this.wrapper).html(this.render_quiz());
|
||||
$(this.wrapper).html(this.render_quiz(this.data.quiz));
|
||||
} else {
|
||||
this.render_quiz_dialog();
|
||||
}
|
||||
@@ -398,16 +337,24 @@ class Quiz {
|
||||
|
||||
render_quiz_dialog() {
|
||||
let me = this;
|
||||
let fields = this.get_fields();
|
||||
let quizdialog = new frappe.ui.Dialog({
|
||||
title: __("Manage Quiz"),
|
||||
fields: fields,
|
||||
fields: [
|
||||
{
|
||||
fieldname: "quiz",
|
||||
fieldtype: "Link",
|
||||
label: __("Quiz"),
|
||||
options: "LMS Quiz",
|
||||
only_select: 1,
|
||||
},
|
||||
],
|
||||
primary_action_label: __("Insert"),
|
||||
primary_action(values) {
|
||||
me.analyze_quiz_list(values);
|
||||
me.quiz = values.quiz;
|
||||
quizdialog.hide();
|
||||
$(me.wrapper).html(me.render_quiz(me.quiz));
|
||||
},
|
||||
secondary_action_label: __("Create New Quiz"),
|
||||
secondary_action_label: __("Create New"),
|
||||
secondary_action: () => {
|
||||
window.location.href = `/quizzes`;
|
||||
},
|
||||
@@ -419,38 +366,19 @@ class Quiz {
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
analyze_quiz_list(values) {
|
||||
/* If quiz is selected and is not already in the lesson then render it.*/
|
||||
|
||||
this.quiz_to_render = [];
|
||||
Object.keys(values).forEach((key) => {
|
||||
if (values[key] === 1 && !self.quiz_in_lesson.includes(key)) {
|
||||
self.quiz_in_lesson.push(key);
|
||||
this.quiz_to_render.push(key);
|
||||
}
|
||||
});
|
||||
|
||||
$(this.wrapper).html(this.render_quiz());
|
||||
}
|
||||
|
||||
render_quiz() {
|
||||
let html = ``;
|
||||
let quiz_list = this.data.quiz || this.quiz_to_render;
|
||||
quiz_list.forEach((quiz) => {
|
||||
html += `<div class="common-card-style p-2 my-2 bold-heading">
|
||||
Quiz: ${quiz}
|
||||
</div>`;
|
||||
});
|
||||
return html;
|
||||
render_quiz(quiz) {
|
||||
return `<div class="common-card-style p-2 my-2 bold-heading">
|
||||
Quiz: ${quiz}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
validate(savedData) {
|
||||
return !savedData.quiz || !savedData.quiz.length ? false : true;
|
||||
return !savedData.quiz || !savedData.quiz.trim() ? false : true;
|
||||
}
|
||||
|
||||
save(block_content) {
|
||||
return {
|
||||
quiz: this.data.quiz || this.quiz_to_render,
|
||||
quiz: this.data.quiz || this.quiz,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,10 +61,10 @@
|
||||
<div class="breadcrumb">
|
||||
{% if class_info %}
|
||||
<a class="dark-links" href="/courses">
|
||||
{{ _("All Classes") }}
|
||||
{{ _("All Batches") }}
|
||||
</a>
|
||||
<img class="ml-1 mr-1" src="/assets/lms/icons/chevron-right.svg">
|
||||
<a class="dark-links" href="/classes/{{ class_info.name }}">
|
||||
<a class="dark-links" href="/batches/{{ class_info.name }}">
|
||||
{{ class_info.title }}
|
||||
</a>
|
||||
<img class="ml-1 mr-1" src="/assets/lms/icons/chevron-right.svg">
|
||||
|
||||
@@ -26,7 +26,7 @@ def get_context(context):
|
||||
context.class_info = frappe._dict(
|
||||
{
|
||||
"name": class_name,
|
||||
"title": frappe.db.get_value("LMS Class", class_name, "title"),
|
||||
"title": frappe.db.get_value("LMS Batch", class_name, "title"),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
0
lms/www/batches/__init__.py
Normal file
0
lms/www/batches/__init__.py
Normal file
@@ -1,16 +1,16 @@
|
||||
{% extends "lms/templates/lms_base.html" %}
|
||||
{% block title %}
|
||||
{{ _(class_info.title) }}
|
||||
{{ _(batch_info.title) }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block page_content %}
|
||||
<div class="common-page-style lms-page-style">
|
||||
<div class="container">
|
||||
{{ BreadCrumb(class_info) }}
|
||||
{{ BreadCrumb(batch_info) }}
|
||||
<div class="">
|
||||
{{ ClassDetails(class_info) }}
|
||||
{{ ClassSections(class_info, class_courses, class_students, flow) }}
|
||||
{{ BatchDetails(batch_info) }}
|
||||
{{ BatchSections(batch_info, batch_courses, batch_students, flow) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -18,26 +18,28 @@
|
||||
|
||||
|
||||
<!-- BreadCrumb -->
|
||||
{% macro BreadCrumb(class_info) %}
|
||||
{% macro BreadCrumb(batch_info) %}
|
||||
<div class="breadcrumb">
|
||||
<a class="dark-links" href="/classes">{{ _("All Classes") }}</a>
|
||||
<a class="dark-links" href="/batches">{{ _("All Batches") }}</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="/batches/details/{{ batch_info.name }}">{{ _("Batch Details") }}</a>
|
||||
<img class="ml-1 mr-1" src="/assets/lms/icons/chevron-right.svg">
|
||||
<span class="breadcrumb-destination">{{ batch_info.title }}</span>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Class Details -->
|
||||
{% macro ClassDetails(class_info) %}
|
||||
<div class="class-details" data-class="{{ class_info.name }}">
|
||||
<!-- Batch Details -->
|
||||
{% macro BatchDetails(batch_info) %}
|
||||
<div class="class-details" data-batch="{{ batch_info.name }}">
|
||||
|
||||
<div class="page-title">
|
||||
{{ class_info.title }}
|
||||
{{ batch_info.title }}
|
||||
</div>
|
||||
|
||||
{% if class_info.description %}
|
||||
{% if batch_info.description %}
|
||||
<div class="mb-4">
|
||||
{{ class_info.description }}
|
||||
{{ batch_info.description }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -47,10 +49,10 @@
|
||||
<use href="#icon-calendar"></use>
|
||||
</svg>
|
||||
<span>
|
||||
{{ frappe.utils.format_date(class_info.start_date, "long") }} -
|
||||
{{ frappe.utils.format_date(batch_info.start_date, "long") }} -
|
||||
</span>
|
||||
<span>
|
||||
{{ frappe.utils.format_date(class_info.end_date, "long") }}
|
||||
{{ frappe.utils.format_date(batch_info.end_date, "long") }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -60,7 +62,7 @@
|
||||
<svg class="icon icon-md">
|
||||
<use href="#icon-education"></use>
|
||||
</svg>
|
||||
{{ class_courses | length }} {{ _("Courses") }}
|
||||
{{ batch_courses | length }} {{ _("Courses") }}
|
||||
</div>
|
||||
|
||||
<span class="seperator"></span>
|
||||
@@ -69,29 +71,22 @@
|
||||
<svg class="icon icon-md">
|
||||
<use href="#icon-users"></use>
|
||||
</svg>
|
||||
{{ class_students | length }} {{ _("Students") }}
|
||||
{{ batch_students | length }} {{ _("Students") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% if class_info.custom_component %}
|
||||
{{ class_info.custom_component }}
|
||||
{% if batch_info.custom_component %}
|
||||
{{ batch_info.custom_component }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Class Sections -->
|
||||
{% macro ClassSections(class_info, class_courses, class_students, flow) %}
|
||||
{% macro BatchSections(batch_info, batch_courses, batch_students, flow) %}
|
||||
<div class="mt-4">
|
||||
|
||||
{% if is_moderator %}
|
||||
<button class="btn btn-default btn-sm pull-right" id="create-class">
|
||||
{{ _("Edit") }}
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
<ul class="nav lms-nav" id="classes-tab">
|
||||
<ul class="nav lms-nav" id="batches-tab">
|
||||
|
||||
{% if is_student %}
|
||||
<li class="nav-item">
|
||||
@@ -105,7 +100,7 @@
|
||||
<a class="nav-link {% if not is_student %} active {% endif %}" data-toggle="tab" href="#courses">
|
||||
{{ _("Courses") }}
|
||||
<span class="course-list-count">
|
||||
{{ class_courses | length }}
|
||||
{{ batch_courses | length }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
@@ -121,16 +116,16 @@
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if is_moderator %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#students">
|
||||
{{ _("Students") }}
|
||||
<span class="course-list-count">
|
||||
{{ class_students | length }}
|
||||
{{ batch_students | length }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% if is_moderator %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#assessments">
|
||||
{{ _("Assessments") }}
|
||||
@@ -141,7 +136,7 @@
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if class_students | length and (is_moderator or is_student) %}
|
||||
{% if batch_students | length and (is_moderator or is_student) %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#discussions">
|
||||
{{ _("Discussions") }}
|
||||
@@ -166,12 +161,12 @@
|
||||
|
||||
{% 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) }}
|
||||
{{ Dashboard(batch_info, batch_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) }}
|
||||
{{ CoursesSection(batch_info, batch_courses) }}
|
||||
</div>
|
||||
|
||||
{% if flow | length %}
|
||||
@@ -180,23 +175,23 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if is_moderator %}
|
||||
<div class="tab-pane" id="students" role="tabpanel" aria-labelledby="students">
|
||||
{{ StudentsSection(class_info, class_students) }}
|
||||
{{ StudentsSection(batch_info, batch_students) }}
|
||||
</div>
|
||||
|
||||
{% if is_moderator %}
|
||||
<div class="tab-pane" id="assessments" role="tabpanel" aria-labelledby="assessments">
|
||||
{{ AssessmentsSection(class_info) }}
|
||||
{{ AssessmentsSection(batch_info) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if class_students | length and (is_moderator or is_student or is_evaluator) %}
|
||||
{% if batch_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) }}
|
||||
{{ Discussions(batch_info) }}
|
||||
</div>
|
||||
|
||||
<div class="tab-pane" id="live-class" role="tabpanel" aria-labelledby="live-class">
|
||||
{{ LiveClassSection(class_info, live_classes) }}
|
||||
{{ LiveClassSection(batch_info, live_classes) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -204,7 +199,7 @@
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro Dashboard(class_info, class_courses, current_student) %}
|
||||
{% macro Dashboard(batch_info, batch_courses, current_student) %}
|
||||
|
||||
{% set upcoming_evals = current_student.upcoming_evals %}
|
||||
{% set assessments = current_student.assessments %}
|
||||
@@ -224,46 +219,36 @@
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro Discussions(class_info) %}
|
||||
{% macro Discussions(batch_info) %}
|
||||
<article class="class-discussion">
|
||||
{% set condition = is_moderator or is_student or is_evaluator %}
|
||||
{% set doctype, docname = _("LMS Class"), class_info.name %}
|
||||
{% set doctype, docname = _("LMS Batch"), batch_info.name %}
|
||||
{% set single_thread = True %}
|
||||
{% set title = "Discussions" %}
|
||||
{% set cta_title = "Post" %}
|
||||
{% set button_name = _("Start Learning") %}
|
||||
{% set redirect_to = "/classes/" + class_info.name %}
|
||||
{% set redirect_to = "/batches/" + batch_info.name %}
|
||||
{% set empty_state_title = _("Have a doubt?") %}
|
||||
{% set empty_state_subtitle = _("Post it here, our mentors will help you out.") %}
|
||||
{% include "frappe/templates/discussions/discussions_section.html" %}
|
||||
</article>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro CoursesSection(class_info, class_courses) %}
|
||||
{% macro CoursesSection(batch_info, batch_courses) %}
|
||||
<article>
|
||||
<header class="mb-5">
|
||||
<div class="edit-header">
|
||||
<div class="bold-heading">
|
||||
{{ _("Courses") }}
|
||||
</div>
|
||||
{% if is_moderator %}
|
||||
<button class="btn btn-default btn-sm btn-add-course">
|
||||
{{ _("Add Courses") }}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{% if class_courses | length %}
|
||||
{% if batch_courses | length %}
|
||||
<div class="cards-parent">
|
||||
{% for course in class_courses %}
|
||||
{% for course in batch_courses %}
|
||||
<div class="h-100">
|
||||
{{ widgets.CourseCard(course=course, read_only=False) }}
|
||||
<button class="btn icon-btn btn-default btn-block btn-remove-course" data-course="{{ course.name }}">
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-delete"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{% endfor %}
|
||||
@@ -278,7 +263,7 @@
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro StudentsSection(class_info, class_students) %}
|
||||
{% macro StudentsSection(batch_info, batch_students) %}
|
||||
<article>
|
||||
<header>
|
||||
<div class="edit-header mb-5">
|
||||
@@ -293,7 +278,7 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{% if class_students | length %}
|
||||
{% if batch_students | length %}
|
||||
<div class="form-grid">
|
||||
<div class="grid-heading-row">
|
||||
<div class="grid-row">
|
||||
@@ -323,11 +308,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% for student in class_students %}
|
||||
{% for student in batch_students %}
|
||||
{% set allow_progress = is_moderator or is_evaluator %}
|
||||
<div class="grid-row">
|
||||
<div class="data-row row">
|
||||
<a class="col grid-static-col button-links {% if allow_progress %} clickable {% endif %}" {% if allow_progress %} href="/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="/batches/{{ batch_info.name }}/students/{{ student.username }}" {% endif %}>
|
||||
{{ student.student_name }}
|
||||
</a>
|
||||
<div class="col grid-static-col col-xs-2 text-right">
|
||||
@@ -360,7 +345,7 @@
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro AssessmentsSection(class_info) %}
|
||||
{% macro AssessmentsSection(batch_info) %}
|
||||
<article>
|
||||
<header class="edit-header mb-5">
|
||||
<div class="bold-heading">
|
||||
@@ -425,7 +410,7 @@
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro LiveClassSection(class_info, live_classes) %}
|
||||
{% macro LiveClassSection(batch_info, live_classes) %}
|
||||
<article>
|
||||
<header class="edit-header">
|
||||
<div class="bold-heading">
|
||||
@@ -437,13 +422,13 @@
|
||||
</button>
|
||||
{% endif %}
|
||||
</header>
|
||||
{{ CreateLiveClass(class_info) }}
|
||||
{{ LiveClassList(class_info, live_classes) }}
|
||||
{{ CreateLiveClass(batch_info) }}
|
||||
{{ LiveClassList(batch_info, live_classes) }}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro CreateLiveClass(class_info) %}
|
||||
{% macro CreateLiveClass(batch_info) %}
|
||||
{% if is_moderator %}
|
||||
<div class="modal fade live-class-modal" id="live-class-modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
@@ -474,7 +459,7 @@
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro LiveClassList(class_info, live_classes) %}
|
||||
{% macro LiveClassList(batch_info, live_classes) %}
|
||||
<div class="lms-card-parent mt-5">
|
||||
{% if live_classes | length %}
|
||||
{% for class in live_classes %}
|
||||
@@ -606,27 +591,8 @@
|
||||
|
||||
{%- block script %}
|
||||
{{ super() }}
|
||||
{% if is_moderator %}
|
||||
<script>
|
||||
frappe.boot.user = {
|
||||
"can_create": [],
|
||||
"can_select": ["User", "LMS Category", "LMS Assignment", "LMS Quiz"],
|
||||
"can_read": ["User", "LMS Category", "LMS Assignment", "LMS Quiz"]
|
||||
};
|
||||
|
||||
frappe.boot.single_types = []
|
||||
|
||||
let class_info = {{ class_info | json }};
|
||||
</script>
|
||||
{% endif %}
|
||||
<script>
|
||||
frappe.boot.user = {
|
||||
"can_create": [],
|
||||
"can_select": ["LMS Course"],
|
||||
"can_read": ["LMS Course"]
|
||||
};
|
||||
let courses = {{ course_list | json }};
|
||||
</script>
|
||||
|
||||
{{ include_script('controls.bundle.js') }}
|
||||
{% endblock %}
|
||||
@@ -1,5 +1,12 @@
|
||||
frappe.ready(() => {
|
||||
let self = this;
|
||||
frappe.require("controls.bundle.js");
|
||||
|
||||
if ($("#live-class-form").length) {
|
||||
setTimeout(() => {
|
||||
make_live_class_form();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
$(".btn-add-student").click((e) => {
|
||||
show_student_modal(e);
|
||||
@@ -9,10 +16,6 @@ frappe.ready(() => {
|
||||
remove_student(e);
|
||||
});
|
||||
|
||||
if ($("#live-class-form").length) {
|
||||
make_live_class_form();
|
||||
}
|
||||
|
||||
$("#open-class-modal").click((e) => {
|
||||
e.preventDefault();
|
||||
$("#live-class-modal").modal("show");
|
||||
@@ -22,14 +25,6 @@ frappe.ready(() => {
|
||||
create_live_class(e);
|
||||
});
|
||||
|
||||
$(".btn-add-course").click((e) => {
|
||||
show_course_modal(e);
|
||||
});
|
||||
|
||||
$(".btn-remove-course").click((e) => {
|
||||
remove_course(e);
|
||||
});
|
||||
|
||||
$(".btn-remove-assessment").click((e) => {
|
||||
remove_assessment(e);
|
||||
});
|
||||
@@ -53,11 +48,11 @@ frappe.ready(() => {
|
||||
});
|
||||
|
||||
const create_live_class = (e) => {
|
||||
let class_name = $(".class-details").data("class");
|
||||
let batch_name = $(".class-details").data("batch");
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_class.lms_class.create_live_class",
|
||||
method: "lms.lms.doctype.lms_batch.lms_batch.create_live_class",
|
||||
args: {
|
||||
class_name: class_name,
|
||||
batch_name: batch_name,
|
||||
title: $("input[data-fieldname='meeting_title']").val(),
|
||||
duration: $("input[data-fieldname='meeting_duration']").val(),
|
||||
date: $("input[data-fieldname='meeting_date']").val(),
|
||||
@@ -298,77 +293,6 @@ const get_timezones = () => {
|
||||
];
|
||||
};
|
||||
|
||||
const show_course_modal = () => {
|
||||
let course_modal = new frappe.ui.Dialog({
|
||||
title: "Add Course",
|
||||
fields: [
|
||||
{
|
||||
fieldtype: "Link",
|
||||
options: "LMS Course",
|
||||
label: __("Course"),
|
||||
fieldname: "course",
|
||||
reqd: 1,
|
||||
},
|
||||
],
|
||||
primary_action_label: __("Add"),
|
||||
primary_action(values) {
|
||||
add_course(values);
|
||||
course_modal.hide();
|
||||
},
|
||||
});
|
||||
course_modal.show();
|
||||
setTimeout(() => {
|
||||
$(".modal-body").css("min-height", "200px");
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const add_course = (values) => {
|
||||
frappe.call({
|
||||
method: "frappe.client.insert",
|
||||
args: {
|
||||
doc: {
|
||||
doctype: "Class Course",
|
||||
course: values.course,
|
||||
parenttype: "LMS Class",
|
||||
parentfield: "courses",
|
||||
parent: $(".class-details").data("class"),
|
||||
},
|
||||
},
|
||||
callback(r) {
|
||||
frappe.show_alert(
|
||||
{
|
||||
message: __("Course Added"),
|
||||
indicator: "green",
|
||||
},
|
||||
2000
|
||||
);
|
||||
window.location.reload();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const remove_course = (e) => {
|
||||
frappe.confirm("Are you sure you want to remove this course?", () => {
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_class.lms_class.remove_course",
|
||||
args: {
|
||||
course: $(e.currentTarget).data("course"),
|
||||
parent: $(".class-details").data("class"),
|
||||
},
|
||||
callback(r) {
|
||||
frappe.show_alert(
|
||||
{
|
||||
message: __("Course Removed"),
|
||||
indicator: "green",
|
||||
},
|
||||
2000
|
||||
);
|
||||
window.location.reload();
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const show_student_modal = () => {
|
||||
let student_modal = new frappe.ui.Dialog({
|
||||
title: "Add Student",
|
||||
@@ -379,6 +303,7 @@ const show_student_modal = () => {
|
||||
label: __("Student"),
|
||||
fieldname: "student",
|
||||
reqd: 1,
|
||||
only_select: 1,
|
||||
filters: {
|
||||
ignore_user_type: 1,
|
||||
},
|
||||
@@ -402,11 +327,11 @@ const add_student = (values) => {
|
||||
method: "frappe.client.insert",
|
||||
args: {
|
||||
doc: {
|
||||
doctype: "Class Student",
|
||||
doctype: "Batch Student",
|
||||
student: values.student,
|
||||
parenttype: "LMS Class",
|
||||
parenttype: "LMS Batch",
|
||||
parentfield: "students",
|
||||
parent: $(".class-details").data("class"),
|
||||
parent: $(".class-details").data("batch"),
|
||||
},
|
||||
},
|
||||
callback(r) {
|
||||
@@ -424,13 +349,13 @@ const add_student = (values) => {
|
||||
|
||||
const remove_student = (e) => {
|
||||
frappe.confirm(
|
||||
"Are you sure you want to remove this student from the class?",
|
||||
"Are you sure you want to remove this student from the batch?",
|
||||
() => {
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_class.lms_class.remove_student",
|
||||
method: "lms.lms.doctype.lms_batch.lms_batch.remove_student",
|
||||
args: {
|
||||
student: $(e.currentTarget).data("student"),
|
||||
class_name: $(".class-details").data("class"),
|
||||
batch_name: $(".class-details").data("batch"),
|
||||
},
|
||||
callback: (data) => {
|
||||
frappe.show_alert(
|
||||
@@ -457,6 +382,7 @@ const show_assessment_modal = (e) => {
|
||||
label: __("Assessment Type"),
|
||||
fieldname: "assessment_type",
|
||||
reqd: 1,
|
||||
only_select: 1,
|
||||
filters: {
|
||||
name: ["in", ["LMS Assignment", "LMS Quiz"]],
|
||||
},
|
||||
@@ -468,6 +394,7 @@ const show_assessment_modal = (e) => {
|
||||
label: __("Assessment"),
|
||||
fieldname: "assessment_name",
|
||||
reqd: 1,
|
||||
only_select: 1,
|
||||
},
|
||||
{
|
||||
fieldtype: "Section Break",
|
||||
@@ -513,9 +440,9 @@ const add_addessment = (values) => {
|
||||
doctype: "LMS Assessment",
|
||||
assessment_type: values.assessment_type,
|
||||
assessment_name: values.assessment_name,
|
||||
parenttype: "LMS Class",
|
||||
parenttype: "LMS Batch",
|
||||
parentfield: "assessment",
|
||||
parent: $(".class-details").data("class"),
|
||||
parent: $(".class-details").data("batch"),
|
||||
},
|
||||
},
|
||||
callback(r) {
|
||||
@@ -534,10 +461,10 @@ const add_addessment = (values) => {
|
||||
const remove_assessment = (e) => {
|
||||
frappe.confirm("Are you sure you want to remove this assessment?", () => {
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_class.lms_class.remove_assessment",
|
||||
method: "lms.lms.doctype.lms_batch.lms_batch.remove_assessment",
|
||||
args: {
|
||||
assessment: $(e.currentTarget).data("assessment"),
|
||||
parent: $(".class-details").data("class"),
|
||||
parent: $(".class-details").data("batch"),
|
||||
},
|
||||
callback(r) {
|
||||
frappe.show_alert(
|
||||
@@ -567,6 +494,7 @@ const open_evaluation_form = (e) => {
|
||||
name: ["in", courses],
|
||||
},
|
||||
filter_description: " ",
|
||||
only_select: 1,
|
||||
},
|
||||
{
|
||||
fieldtype: "Date",
|
||||
@@ -602,7 +530,7 @@ const get_slots = () => {
|
||||
args: {
|
||||
course: this.eval_form.get_value("course"),
|
||||
date: this.eval_form.get_value("date"),
|
||||
class_name: $(".class-details").data("class"),
|
||||
batch_name: $(".class-details").data("batch"),
|
||||
},
|
||||
callback: (r) => {
|
||||
if (r.message) {
|
||||
@@ -664,7 +592,7 @@ const submit_evaluation_form = (values) => {
|
||||
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"),
|
||||
batch_name: $(".class-details").data("batch"),
|
||||
},
|
||||
callback: (r) => {
|
||||
this.eval_form.hide();
|
||||
@@ -1,7 +1,7 @@
|
||||
from frappe import _
|
||||
import frappe
|
||||
from frappe.utils import getdate, cint
|
||||
from lms.www.utils import get_assessments
|
||||
from lms.www.utils import get_assessments, is_student
|
||||
from lms.lms.utils import (
|
||||
has_course_moderator_role,
|
||||
has_course_evaluator_role,
|
||||
@@ -17,13 +17,13 @@ from lms.lms.utils import (
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
class_name = frappe.form_dict["classname"]
|
||||
batch_name = frappe.form_dict["batchname"]
|
||||
context.is_moderator = has_course_moderator_role()
|
||||
context.is_evaluator = has_course_evaluator_role()
|
||||
|
||||
context.class_info = frappe.db.get_value(
|
||||
"LMS Class",
|
||||
class_name,
|
||||
context.batch_info = frappe.db.get_value(
|
||||
"LMS Batch",
|
||||
batch_name,
|
||||
[
|
||||
"name",
|
||||
"title",
|
||||
@@ -36,58 +36,63 @@ def get_context(context):
|
||||
"start_time",
|
||||
"end_time",
|
||||
"category",
|
||||
"paid_batch",
|
||||
"amount",
|
||||
"currency",
|
||||
"batch_details",
|
||||
"published",
|
||||
],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
context.reference_doctype = "LMS Class"
|
||||
context.reference_name = class_name
|
||||
context.reference_doctype = "LMS Batch"
|
||||
context.reference_name = batch_name
|
||||
|
||||
class_courses = frappe.get_all(
|
||||
"Class Course",
|
||||
{"parent": class_name},
|
||||
batch_courses = frappe.get_all(
|
||||
"Batch Course",
|
||||
{"parent": batch_name},
|
||||
["name", "course", "title"],
|
||||
order_by="creation desc",
|
||||
)
|
||||
|
||||
class_students = frappe.get_all(
|
||||
"Class Student",
|
||||
{"parent": class_name},
|
||||
batch_students = frappe.get_all(
|
||||
"Batch Student",
|
||||
{"parent": batch_name},
|
||||
["name", "student", "student_name", "username"],
|
||||
order_by="creation desc",
|
||||
)
|
||||
|
||||
context.class_courses = get_class_course_details(class_courses)
|
||||
context.course_list = [course.course for course in context.class_courses]
|
||||
context.batch_courses = get_class_course_details(batch_courses)
|
||||
context.course_list = [course.course for course in context.batch_courses]
|
||||
context.all_courses = frappe.get_all(
|
||||
"LMS Course", fields=["name", "title"], limit_page_length=0
|
||||
)
|
||||
context.course_name_list = [course.course for course in context.class_courses]
|
||||
context.assessments = get_assessments(class_name)
|
||||
context.class_students = get_class_student_details(
|
||||
class_students, class_courses, context.assessments
|
||||
context.course_name_list = [course.course for course in context.batch_courses]
|
||||
context.assessments = get_assessments(batch_name)
|
||||
context.batch_students = get_class_student_details(
|
||||
batch_students, batch_courses, context.assessments
|
||||
)
|
||||
context.is_student = is_student(class_students)
|
||||
context.is_student = is_student(batch_name)
|
||||
|
||||
if not context.is_student and not context.is_moderator and not context.is_evaluator:
|
||||
raise frappe.PermissionError(_("You don't have permission to access this page."))
|
||||
|
||||
context.live_classes = frappe.get_all(
|
||||
"LMS Live Class",
|
||||
{"class_name": class_name, "date": [">=", getdate()]},
|
||||
{"batch_name": batch_name, "date": [">=", getdate()]},
|
||||
["title", "description", "time", "date", "start_url", "join_url", "owner"],
|
||||
order_by="date",
|
||||
)
|
||||
|
||||
context.current_student = (
|
||||
get_current_student_details(class_courses, class_name) if context.is_student else None
|
||||
get_current_student_details(batch_courses, batch_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)
|
||||
context.all_assignments = get_all_assignments(batch_name)
|
||||
context.all_quizzes = get_all_quizzes(batch_name)
|
||||
context.flow = get_scheduled_flow(batch_name)
|
||||
|
||||
|
||||
def get_all_quizzes(class_name):
|
||||
def get_all_quizzes(batch_name):
|
||||
filters = {} if has_course_moderator_role() else {"owner": frappe.session.user}
|
||||
all_quizzes = frappe.get_all("LMS Quiz", filters, ["name", "title"])
|
||||
for quiz in all_quizzes:
|
||||
@@ -96,13 +101,13 @@ def get_all_quizzes(class_name):
|
||||
"doctype": "LMS Assessment",
|
||||
"assessment_type": "LMS Quiz",
|
||||
"assessment_name": quiz.name,
|
||||
"parent": class_name,
|
||||
"parent": batch_name,
|
||||
}
|
||||
)
|
||||
return all_quizzes
|
||||
|
||||
|
||||
def get_all_assignments(class_name):
|
||||
def get_all_assignments(batch_name):
|
||||
filters = {} if has_course_moderator_role() else {"owner": frappe.session.user}
|
||||
all_assignments = frappe.get_all("LMS Assignment", filters, ["name", "title"])
|
||||
for assignment in all_assignments:
|
||||
@@ -111,14 +116,14 @@ def get_all_assignments(class_name):
|
||||
"doctype": "LMS Assessment",
|
||||
"assessment_type": "LMS Assignment",
|
||||
"assessment_name": assignment.name,
|
||||
"parent": class_name,
|
||||
"parent": batch_name,
|
||||
}
|
||||
)
|
||||
return all_assignments
|
||||
|
||||
|
||||
def get_class_course_details(class_courses):
|
||||
for course in class_courses:
|
||||
def get_class_course_details(batch_courses):
|
||||
for course in batch_courses:
|
||||
details = frappe.db.get_value(
|
||||
"LMS Course",
|
||||
course.course,
|
||||
@@ -137,27 +142,27 @@ def get_class_course_details(class_courses):
|
||||
as_dict=True,
|
||||
)
|
||||
course.update(details)
|
||||
return class_courses
|
||||
return batch_courses
|
||||
|
||||
|
||||
def get_class_student_details(class_students, class_courses, assessments):
|
||||
for student in class_students:
|
||||
def get_class_student_details(batch_students, batch_courses, assessments):
|
||||
for student in batch_students:
|
||||
student.update(
|
||||
frappe.db.get_value(
|
||||
"User", student.student, ["name", "full_name", "username", "headline"], as_dict=1
|
||||
)
|
||||
)
|
||||
student.update(frappe.db.get_value("User", student.student, "last_active", as_dict=1))
|
||||
get_progress_info(student, class_courses)
|
||||
get_progress_info(student, batch_courses)
|
||||
get_assessment_info(student, assessments)
|
||||
|
||||
return sort_students(class_students)
|
||||
return sort_students(batch_students)
|
||||
|
||||
|
||||
def get_progress_info(student, class_courses):
|
||||
def get_progress_info(student, batch_courses):
|
||||
courses_completed = 0
|
||||
student["courses"] = frappe._dict()
|
||||
for course in class_courses:
|
||||
for course in batch_courses:
|
||||
membership = get_membership(course.course, student.student)
|
||||
if membership and membership.progress == 100:
|
||||
courses_completed += 1
|
||||
@@ -189,11 +194,11 @@ def get_assessment_info(student, assessments):
|
||||
return student
|
||||
|
||||
|
||||
def sort_students(class_students):
|
||||
def sort_students(batch_students):
|
||||
session_user = []
|
||||
remaining_students = []
|
||||
|
||||
for student in class_students:
|
||||
for student in batch_students:
|
||||
if student.student == frappe.session.user:
|
||||
session_user.append(student)
|
||||
else:
|
||||
@@ -202,26 +207,21 @@ def sort_students(class_students):
|
||||
if len(session_user):
|
||||
return session_user + remaining_students
|
||||
else:
|
||||
return class_students
|
||||
return batch_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):
|
||||
def get_scheduled_flow(batch_name):
|
||||
chapters = []
|
||||
|
||||
lessons = frappe.get_all(
|
||||
"Scheduled Flow",
|
||||
{"parent": class_name},
|
||||
{"parent": batch_name},
|
||||
["name", "lesson", "date", "start_time", "end_time"],
|
||||
order_by="idx",
|
||||
)
|
||||
|
||||
for lesson in lessons:
|
||||
lesson = get_lesson_details(lesson, class_name)
|
||||
lesson = get_lesson_details(lesson, batch_name)
|
||||
chapter_exists = [
|
||||
chapter for chapter in chapters if chapter.chapter == lesson.chapter
|
||||
]
|
||||
@@ -242,7 +242,7 @@ def get_scheduled_flow(class_name):
|
||||
return chapters
|
||||
|
||||
|
||||
def get_lesson_details(lesson, class_name):
|
||||
def get_lesson_details(lesson, batch_name):
|
||||
lesson.update(
|
||||
frappe.db.get_value(
|
||||
"Course Lesson",
|
||||
@@ -252,26 +252,26 @@ def get_lesson_details(lesson, class_name):
|
||||
)
|
||||
)
|
||||
lesson.index = get_lesson_index(lesson.lesson)
|
||||
lesson.url = get_lesson_url(lesson.course, lesson.index) + "?class=" + class_name
|
||||
lesson.url = get_lesson_url(lesson.course, lesson.index) + "?class=" + batch_name
|
||||
lesson.icon = get_lesson_icon(lesson.body)
|
||||
return lesson
|
||||
|
||||
|
||||
def get_current_student_details(class_courses, class_name):
|
||||
def get_current_student_details(batch_courses, batch_name):
|
||||
student_details = frappe._dict()
|
||||
student_details.courses = frappe._dict()
|
||||
course_list = [course.course for course in class_courses]
|
||||
course_list = [course.course for course in batch_courses]
|
||||
|
||||
get_course_progress(class_courses, student_details)
|
||||
get_course_progress(batch_courses, student_details)
|
||||
student_details.name = frappe.session.user
|
||||
student_details.assessments = get_assessments(class_name, frappe.session.user)
|
||||
student_details.assessments = get_assessments(batch_name, frappe.session.user)
|
||||
student_details.upcoming_evals = get_upcoming_evals(frappe.session.user, course_list)
|
||||
|
||||
return student_details
|
||||
|
||||
|
||||
def get_course_progress(class_courses, student_details):
|
||||
for course in class_courses:
|
||||
def get_course_progress(batch_courses, student_details):
|
||||
for course in batch_courses:
|
||||
membership = get_membership(course.course, frappe.session.user)
|
||||
if membership:
|
||||
student_details.courses[course.course] = membership.progress
|
||||
224
lms/www/batches/batch_details.html
Normal file
224
lms/www/batches/batch_details.html
Normal file
@@ -0,0 +1,224 @@
|
||||
{% extends "lms/templates/lms_base.html" %}
|
||||
{% block title %}
|
||||
{{ _(batch_info.title) }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block page_content %}
|
||||
<div class="common-page-style lms-page-style">
|
||||
{{ BatchHeader(batch_info) }}
|
||||
<div class="container">
|
||||
{{ BatchOverlay(batch_info, courses, students) }}
|
||||
<div class="pt-10">
|
||||
{{ BatchDetails(batch_info) }}
|
||||
{{ CourseList(courses) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% macro BatchHeader(batch_info) %}
|
||||
<div class="course-head-container">
|
||||
<div class="container">
|
||||
<div class="course-card-wide">
|
||||
{{ BreadCrumb(batch_info) }}
|
||||
{{ BatchHeaderDetails(batch_info, courses, students) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro BreadCrumb(batch_info) %}
|
||||
<article class="mb-8">
|
||||
<a class="dark-links" href="/batches">
|
||||
{{ _("All Batches") }}
|
||||
</a>
|
||||
<img class="" src="/assets/lms/icons/chevron-right.svg">
|
||||
<span class="breadcrumb-destination">
|
||||
{{ _("Batch Details") }}
|
||||
</span>
|
||||
</article>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro BatchHeaderDetails(batch_info, courses, students) %}
|
||||
<div class="class-details" data-batch="{{ batch_info.name }}">
|
||||
|
||||
<div class="page-title">
|
||||
{{ batch_info.title }}
|
||||
</div>
|
||||
|
||||
<div class="">
|
||||
{{ batch_info.description }}
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-calendar"></use>
|
||||
</svg>
|
||||
<span>
|
||||
{{ frappe.utils.format_date(batch_info.start_date, "long") }} -
|
||||
</span>
|
||||
<span>
|
||||
{{ frappe.utils.format_date(batch_info.end_date, "long") }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{% if batch_info.start_time and batch_info.end_time %}
|
||||
<div class="mt-1">
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-clock"></use>
|
||||
</svg>
|
||||
<span>
|
||||
{{ frappe.utils.format_time(batch_info.start_time, "hh:mm a") }} -
|
||||
</span>
|
||||
<span>
|
||||
{{ frappe.utils.format_time(batch_info.end_time, "hh:mm a") }}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro BatchOverlay(batch_info, courses, students) %}
|
||||
<div class="course-overlay-card class-overlay">
|
||||
|
||||
<div class="course-overlay-content">
|
||||
|
||||
{% if batch_info.seat_count %}
|
||||
{% if seats_left %}
|
||||
<div class="indicator-pill green pull-right">
|
||||
{{ _("Seats Available") }}: {{ seats_left }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="indicator-pill red pull-right">
|
||||
{{ _("No seats left") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if batch_info.paid_batch %}
|
||||
<div class="bold-heading">
|
||||
{{ frappe.utils.fmt_money(batch_info.amount, 0, batch_info.currency) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="vertically-center mt-2">
|
||||
<svg class="icon icon-md mr-1">
|
||||
<use href="#icon-education"></use>
|
||||
</svg>
|
||||
{{ courses | length }} {{ _("Courses") }}
|
||||
</div>
|
||||
|
||||
<div class="mt-2">
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-calendar"></use>
|
||||
</svg>
|
||||
<span>
|
||||
{{ frappe.utils.format_date(batch_info.start_date, "long") }} -
|
||||
</span>
|
||||
<span>
|
||||
{{ frappe.utils.format_date(batch_info.end_date, "long") }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{% if batch_info.start_time and batch_info.end_time %}
|
||||
<div class="mt-2">
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-clock"></use>
|
||||
</svg>
|
||||
<span>
|
||||
{{ frappe.utils.format_time(batch_info.start_time, "hh:mm a") }} -
|
||||
</span>
|
||||
<span>
|
||||
{{ frappe.utils.format_time(batch_info.end_time, "hh:mm a") }}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-2">
|
||||
{% if is_moderator or is_evaluator or is_student %}
|
||||
<a class="btn btn-primary wide-button" href="/batches/{{ batch_info.name }}">
|
||||
{{ _("Checkout Batch") }}
|
||||
</a>
|
||||
{% elif batch_info.paid_batch %}
|
||||
<a class="btn btn-primary wide-button {% if batch_info.seat_count and not seats_left %} hide {% endif %}"
|
||||
href="/billing/batch/{{ batch_info.name }}">
|
||||
{{ _("Register Now") }}
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
{{ _("To join this batch, please contact the Administrator.") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if is_moderator %}
|
||||
<div class="mt-2">
|
||||
<div class="btn btn-secondary wide-button" id="create-batch">
|
||||
{{ _("Edit") }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro BatchDetails(batch_info) %}
|
||||
<div class="course-description-section w-50">
|
||||
<div class="mt-2">
|
||||
{{ batch_info.batch_details }}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro CourseList(courses) %}
|
||||
<div class="batch-course-list">
|
||||
{% if is_moderator %}
|
||||
<button class="btn btn-default btn-sm btn-add-course pull-right">
|
||||
{{ _("Add Courses") }}
|
||||
</button>
|
||||
{% endif %}
|
||||
<div class="page-title">
|
||||
{{ _("Courses") }}
|
||||
</div>
|
||||
{% if courses | length %}
|
||||
<div class="cards-parent mt-2">
|
||||
{% for course in courses %}
|
||||
<div class="h-100">
|
||||
{% if is_moderator %}
|
||||
<div class="card-buttons">
|
||||
<button class="btn icon-btn btn-default btn-edit-course"
|
||||
data-name="{{ course.batch_course }}" data-course="{{ course.name }}"
|
||||
{% if course.evaluator %} data-evaluator="{{ course.evaluator }}" {% endif %}>
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-edit"></use>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn icon-btn btn-default btn-remove-course ml-2" data-course="{{ course.name }}">
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-delete"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{{ widgets.CourseCard(course=course, read_only=False) }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="">
|
||||
{{ _("No courses") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{%- block script %}
|
||||
{{ super() }}
|
||||
{% if is_moderator %}
|
||||
<script>
|
||||
let batch_info = {{ batch_info | json }};
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
100
lms/www/batches/batch_details.js
Normal file
100
lms/www/batches/batch_details.js
Normal file
@@ -0,0 +1,100 @@
|
||||
frappe.ready(() => {
|
||||
frappe.require("controls.bundle.js");
|
||||
|
||||
$(".btn-add-course").click((e) => {
|
||||
show_course_modal(e);
|
||||
});
|
||||
|
||||
$(".btn-edit-course").click((e) => {
|
||||
show_course_modal(e);
|
||||
});
|
||||
|
||||
$(".btn-remove-course").click((e) => {
|
||||
remove_course(e);
|
||||
});
|
||||
});
|
||||
|
||||
const show_course_modal = (e) => {
|
||||
const target = $(e.currentTarget);
|
||||
const course = target.data("course");
|
||||
const evaluator = target.data("evaluator");
|
||||
const course_name = target.data("name");
|
||||
|
||||
let course_modal = new frappe.ui.Dialog({
|
||||
title: "Add Course",
|
||||
fields: [
|
||||
{
|
||||
fieldtype: "Link",
|
||||
options: "LMS Course",
|
||||
label: __("Course"),
|
||||
fieldname: "course",
|
||||
reqd: 1,
|
||||
only_select: 1,
|
||||
default: course || "",
|
||||
},
|
||||
{
|
||||
fieldtype: "Link",
|
||||
options: "Course Evaluator",
|
||||
label: __("Course Evaluator"),
|
||||
fieldname: "evaluator",
|
||||
only_select: 1,
|
||||
default: evaluator || "",
|
||||
},
|
||||
],
|
||||
primary_action_label: __("Add"),
|
||||
primary_action(values) {
|
||||
add_course(values, course_name);
|
||||
course_modal.hide();
|
||||
},
|
||||
});
|
||||
course_modal.show();
|
||||
setTimeout(() => {
|
||||
$(".modal-body").css("min-height", "300px");
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const add_course = (values, course_name) => {
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_batch.lms_batch.add_course",
|
||||
args: {
|
||||
course: values.course,
|
||||
evaluator: values.evaluator,
|
||||
parent: $(".class-details").data("batch"),
|
||||
name: course_name || "",
|
||||
},
|
||||
callback(r) {
|
||||
frappe.show_alert(
|
||||
{
|
||||
message: course_name
|
||||
? __("Course Updated")
|
||||
: __("Course Added"),
|
||||
indicator: "green",
|
||||
},
|
||||
2000
|
||||
);
|
||||
window.location.reload();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const remove_course = (e) => {
|
||||
frappe.confirm("Are you sure you want to remove this course?", () => {
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_batch.lms_batch.remove_course",
|
||||
args: {
|
||||
course: $(e.currentTarget).data("course"),
|
||||
parent: $(".class-details").data("batch"),
|
||||
},
|
||||
callback(r) {
|
||||
frappe.show_alert(
|
||||
{
|
||||
message: __("Course Removed"),
|
||||
indicator: "green",
|
||||
},
|
||||
2000
|
||||
);
|
||||
window.location.reload();
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
55
lms/www/batches/batch_details.py
Normal file
55
lms/www/batches/batch_details.py
Normal file
@@ -0,0 +1,55 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from lms.lms.utils import has_course_moderator_role, has_course_evaluator_role
|
||||
from lms.www.utils import is_student
|
||||
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
batch_name = frappe.form_dict["batchname"]
|
||||
|
||||
context.batch_info = frappe.db.get_value(
|
||||
"LMS Batch",
|
||||
batch_name,
|
||||
[
|
||||
"name",
|
||||
"title",
|
||||
"description",
|
||||
"batch_details",
|
||||
"start_date",
|
||||
"end_date",
|
||||
"paid_batch",
|
||||
"amount",
|
||||
"currency",
|
||||
"start_time",
|
||||
"end_time",
|
||||
"seat_count",
|
||||
"published",
|
||||
],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
context.is_moderator = has_course_moderator_role()
|
||||
context.is_evaluator = has_course_evaluator_role()
|
||||
|
||||
if not context.is_moderator and not context.batch_info.published:
|
||||
raise frappe.PermissionError(_("You do not have permission to access this page."))
|
||||
|
||||
context.courses = frappe.get_all(
|
||||
"Batch Course",
|
||||
{"parent": batch_name},
|
||||
["name as batch_course", "course", "title", "evaluator"],
|
||||
order_by="creation desc",
|
||||
)
|
||||
|
||||
for course in context.courses:
|
||||
course.update(
|
||||
frappe.db.get_value(
|
||||
"LMS Course", course.course, ["name", "short_introduction", "image"], as_dict=1
|
||||
)
|
||||
)
|
||||
|
||||
context.student_count = frappe.db.count("Batch Student", {"parent": batch_name})
|
||||
context.seats_left = context.batch_info.seat_count - context.student_count
|
||||
|
||||
context.is_student = is_student(batch_name)
|
||||
195
lms/www/batches/index.html
Normal file
195
lms/www/batches/index.html
Normal file
@@ -0,0 +1,195 @@
|
||||
{% extends "lms/templates/lms_base.html" %}
|
||||
{% block title %}
|
||||
{{ _("All Batches") }}
|
||||
{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
<div class="common-page-style lms-page-style">
|
||||
<div class="container">
|
||||
{{ Header() }}
|
||||
{% if past_batches | length or upcoming_batches | length or private_batches | length %}
|
||||
{{ BatchTabs(past_batches, upcoming_batches, private_batches, my_batches) }}
|
||||
{% else %}
|
||||
{{ EmptyState() }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% macro Header() %}
|
||||
<header class="edit-header">
|
||||
<div class="page-title mb-6"> {{ _("All Batches") }} </div>
|
||||
{% if is_moderator %}
|
||||
<button class="btn btn-primary btn-sm pull-right" id="create-batch">
|
||||
{{ _("New Batch") }}
|
||||
</button>
|
||||
{% endif %}
|
||||
</header>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro BatchTabs(past_batches, upcoming_batches, private_batches, my_batches) %}
|
||||
<article>
|
||||
<ul class="nav lms-nav" id="courses-tab">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-toggle="tab" href="#upcoming">
|
||||
{{ _("Upcoming") }}
|
||||
<span class="course-list-count">
|
||||
{{ upcoming_batches | length }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% if is_moderator %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#past">
|
||||
{{ _("Archived") }}
|
||||
<span class="course-list-count">
|
||||
{{ past_batches | length }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#private">
|
||||
{{ _("Private") }}
|
||||
<span class="course-list-count">
|
||||
{{ private_batches | length }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if frappe.session.user != "Guest" %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#my-batch">
|
||||
{{ _("Enrolled") }}
|
||||
<span class="course-list-count">
|
||||
{{ my_batches | length }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
</ul>
|
||||
|
||||
<div class="border-bottom mb-4"></div>
|
||||
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="upcoming" role="tabpanel" aria-labelledby="upcoming">
|
||||
{{ BatchCard(upcoming_batches, show_price=True, label="Upcoming") }}
|
||||
</div>
|
||||
|
||||
{% if is_moderator %}
|
||||
<div class="tab-pane" id="past" role="tabpanel" aria-labelledby="past">
|
||||
{{ BatchCard(past_batches, show_price=False, label="Archived") }}
|
||||
</div>
|
||||
|
||||
<div class="tab-pane" id="private" role="tabpanel" aria-labelledby="private">
|
||||
{{ BatchCard(private_batches, show_price=False, label="Private") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if frappe.session.user != "Guest" %}
|
||||
<div class="tab-pane" id="my-batch" role="tabpanel" aria-labelledby="my-batches">
|
||||
{{ BatchCard(my_batches, show_price=False, label="Enrolled") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</article>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro BatchCard(batches, show_price=False, label="") %}
|
||||
{% if batches | length %}
|
||||
<div class="lms-card-parent">
|
||||
{% for batch in batches %}
|
||||
|
||||
<div class="common-card-style column-card" style="min-height: 150px;">
|
||||
|
||||
{% if batch.seat_count %}
|
||||
{% if batch.seats_left > 0 %}
|
||||
<div class="indicator-pill green align-self-start mb-2">
|
||||
{{ _("Seats Available") }}: {{ batch.seats_left }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="indicator-pill red align-self-start mb-2">
|
||||
{{ _("No Seats Left") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<div class="bold-heading">
|
||||
{{ batch.title }}
|
||||
</div>
|
||||
|
||||
{% if batch.description %}
|
||||
<div class="short-introduction">
|
||||
{{ batch.description }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if show_price and batch.paid_batch %}
|
||||
<div class="bold-heading mb-2">
|
||||
{{ frappe.utils.fmt_money(batch.amount, 0, batch.currency) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-auto mb-2">
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-calendar"></use>
|
||||
</svg>
|
||||
<span>
|
||||
{{ frappe.utils.format_date(batch.start_date, "medium") }} -
|
||||
</span>
|
||||
<span>
|
||||
{{ frappe.utils.format_date(batch.end_date, "medium") }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<svg class="icon icon-md">
|
||||
<use href="#icon-education"></use>
|
||||
</svg>
|
||||
{{ batch.course_count }} {{ _("Courses") }}
|
||||
</div>
|
||||
|
||||
{% if is_student(batch.name) %}
|
||||
<a class="stretched-link" href="/batches/{{ batch.name }}"></a>
|
||||
{% else %}
|
||||
<a class="stretched-link" href="/batches/details/{{ batch.name }}"></a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted mt-3">
|
||||
{{ _("No {0} batches").format(label|lower) }}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro EmptyState() %}
|
||||
<div class="empty-state">
|
||||
<img class="icon icon-xl" src="/assets/lms/icons/comment.svg">
|
||||
<div class="empty-state-text">
|
||||
<div class="empty-state-heading">{{ _("No Batches") }}</div>
|
||||
<div class="course-meta">{{ _("Please contact the Administrator for more information.") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{%- block script %}
|
||||
{{ super() }}
|
||||
{{ include_script('controls.bundle.js') }}
|
||||
{% if is_moderator %}
|
||||
<script>
|
||||
frappe.boot.user = {
|
||||
"can_create": [],
|
||||
"can_select": ["LMS Category"],
|
||||
"can_read": ["LMS Category"]
|
||||
};
|
||||
let batch_info = null;
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
78
lms/www/batches/index.py
Normal file
78
lms/www/batches/index.py
Normal file
@@ -0,0 +1,78 @@
|
||||
import frappe
|
||||
from frappe.utils import getdate
|
||||
from lms.lms.utils import has_course_moderator_role, has_course_evaluator_role
|
||||
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
context.is_moderator = has_course_moderator_role()
|
||||
context.is_evaluator = has_course_evaluator_role()
|
||||
batches = frappe.get_all(
|
||||
"LMS Batch",
|
||||
fields=[
|
||||
"name",
|
||||
"title",
|
||||
"description",
|
||||
"start_date",
|
||||
"end_date",
|
||||
"paid_batch",
|
||||
"amount",
|
||||
"currency",
|
||||
"seat_count",
|
||||
"published",
|
||||
],
|
||||
order_by="start_date",
|
||||
)
|
||||
|
||||
past_batches, upcoming_batches, private_batches = [], [], []
|
||||
for batch in batches:
|
||||
batch.student_count = frappe.db.count("Batch Student", {"parent": batch.name})
|
||||
batch.course_count = frappe.db.count("Batch Course", {"parent": batch.name})
|
||||
batch.seats_left = (
|
||||
batch.seat_count - batch.student_count if batch.seat_count else None
|
||||
)
|
||||
print(batch.name, batch.published)
|
||||
if not batch.published:
|
||||
private_batches.append(batch)
|
||||
elif getdate(batch.start_date) < getdate():
|
||||
past_batches.append(batch)
|
||||
else:
|
||||
upcoming_batches.append(batch)
|
||||
|
||||
context.past_batches = sorted(past_batches, key=lambda d: d.start_date)
|
||||
context.upcoming_batches = sorted(upcoming_batches, key=lambda d: d.start_date)
|
||||
context.private_batches = sorted(private_batches, key=lambda d: d.start_date)
|
||||
|
||||
if frappe.session.user != "Guest":
|
||||
my_batches_info = []
|
||||
my_batches = frappe.get_all(
|
||||
"Batch Student", {"student": frappe.session.user}, pluck="parent"
|
||||
)
|
||||
|
||||
for batch in my_batches:
|
||||
batchinfo = frappe.db.get_value(
|
||||
"LMS Batch",
|
||||
batch,
|
||||
[
|
||||
"name",
|
||||
"title",
|
||||
"description",
|
||||
"start_date",
|
||||
"end_date",
|
||||
"paid_batch",
|
||||
"amount",
|
||||
"currency",
|
||||
"seat_count",
|
||||
],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
batchinfo.student_count = frappe.db.count(
|
||||
"Batch Student", {"parent": batchinfo.name}
|
||||
)
|
||||
batchinfo.course_count = frappe.db.count("Batch Course", {"parent": batchinfo.name})
|
||||
batchinfo.seats_left = batchinfo.seat_count - batchinfo.student_count
|
||||
|
||||
my_batches_info.append(batchinfo)
|
||||
|
||||
context.my_batches = my_batches_info
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="common-page-style">
|
||||
{{ Header() }}
|
||||
<div class="container">
|
||||
{{ Progress(class_info, student) }}
|
||||
{{ Progress(batch, student) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -22,12 +22,12 @@
|
||||
{{ _("{0}").format(student.full_name) }}
|
||||
</div>
|
||||
<div class="vertically-center">
|
||||
<a class="dark-links" href="/classes">
|
||||
{{ _("All Classes") }}
|
||||
<a class="dark-links" href="/batches">
|
||||
{{ _("All Batches") }}
|
||||
</a>
|
||||
<img class="icon icon-sm mr-0" src="/assets/lms/icons/chevron-right.svg">
|
||||
<a class="dark-links" href="/classes/{{ class_info.name }}">
|
||||
{{ class_info.name }}
|
||||
<a class="dark-links" href="/batches/{{ batch.name }}">
|
||||
{{ batch.title }}
|
||||
</a>
|
||||
<img class="icon icon-sm mr-0" src="/assets/lms/icons/chevron-right.svg">
|
||||
<span class="breadcrumb-destination">
|
||||
@@ -46,7 +46,10 @@
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if is_moderator %}
|
||||
<a class="btn btn-primary btn-sm btn-evaluate ml-2" href="/evaluation/new?member={{student.name}}&date={{frappe.utils.getdate()}}&class_name={{class_info.name}}">
|
||||
<button class="btn btn-default btn-sm btn-certification ml-2">
|
||||
{{ _("Grant Certificate") }}
|
||||
</button>
|
||||
<a class="btn btn-primary btn-sm btn-evaluate ml-2" href="/evaluation/new?member={{student.name}}&date={{frappe.utils.getdate()}}&class_name={{batch.name}}">
|
||||
{{ _("Evaluate") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
@@ -57,9 +60,9 @@
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro Progress(class_info, student) %}
|
||||
{% macro Progress(batch, student) %}
|
||||
{{ UpcomingEvals(upcoming_evals) }}
|
||||
{{ Assessments(class_info, student) }}
|
||||
{{ Assessments(batch, student) }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro UpcomingEvals(upcoming_evals) %}
|
||||
@@ -68,7 +71,7 @@
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro Assessments(class_info, student) %}
|
||||
{% macro Assessments(batch, student) %}
|
||||
<div class="mb-8">
|
||||
{% include "lms/templates/assessments.html" %}
|
||||
</div>
|
||||
@@ -84,7 +87,6 @@
|
||||
"can_read": ["LMS Course"]
|
||||
};
|
||||
let courses = {{ courses | json }};
|
||||
let class_name = "{{ class_info.name }}";
|
||||
let batch_name = "{{ batch.name }}";
|
||||
</script>
|
||||
{{ include_script('controls.bundle.js') }}
|
||||
{% endblock %}
|
||||
45
lms/www/batches/progress.js
Normal file
45
lms/www/batches/progress.js
Normal file
@@ -0,0 +1,45 @@
|
||||
frappe.ready(() => {
|
||||
frappe.require("controls.bundle.js");
|
||||
|
||||
$(".clickable-row").click((e) => {
|
||||
window.location.href = $(e.currentTarget).data("href");
|
||||
});
|
||||
|
||||
$(".btn-certification").click((e) => {
|
||||
show_certificate_dialog(e);
|
||||
});
|
||||
});
|
||||
|
||||
const show_certificate_dialog = (e) => {
|
||||
this.certificate_dialog = new frappe.ui.Dialog({
|
||||
title: __("Grant Certificate"),
|
||||
fields: [
|
||||
{
|
||||
fieldtype: "Link",
|
||||
fieldname: "course",
|
||||
label: __("Course"),
|
||||
options: "LMS Course",
|
||||
reqd: 1,
|
||||
filters: {
|
||||
name: ["in", courses],
|
||||
},
|
||||
filter_description: " ",
|
||||
only_select: 1,
|
||||
},
|
||||
{
|
||||
fieldtype: "Date",
|
||||
fieldname: "issue_date",
|
||||
label: __("Issue Date"),
|
||||
reqd: 1,
|
||||
default: frappe.datetime.get_today(),
|
||||
},
|
||||
{
|
||||
fieldtype: "Date",
|
||||
fieldname: "expiry_date",
|
||||
label: __("Expiry Date"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
this.certificate_dialog.show();
|
||||
};
|
||||
@@ -12,7 +12,7 @@ def get_context(context):
|
||||
context.no_cache = 1
|
||||
|
||||
student = frappe.form_dict["username"]
|
||||
class_name = frappe.form_dict["classname"]
|
||||
batch_name = frappe.form_dict["batchname"]
|
||||
context.is_moderator = has_course_moderator_role()
|
||||
context.is_evaluator = has_course_evaluator_role()
|
||||
|
||||
@@ -29,13 +29,13 @@ def get_context(context):
|
||||
):
|
||||
raise frappe.PermissionError(_("You don't have permission to access this page."))
|
||||
|
||||
context.class_info = frappe.db.get_value(
|
||||
"LMS Class", class_name, ["name"], as_dict=True
|
||||
context.batch = frappe.db.get_value(
|
||||
"LMS Batch", batch_name, ["name", "title"], as_dict=True
|
||||
)
|
||||
|
||||
context.courses = frappe.get_all(
|
||||
"Class Course", {"parent": class_name}, pluck="course"
|
||||
"Batch Course", {"parent": batch_name}, pluck="course"
|
||||
)
|
||||
|
||||
context.assessments = get_assessments(class_name, context.student.name)
|
||||
context.assessments = get_assessments(batch_name, context.student.name)
|
||||
context.upcoming_evals = get_upcoming_evals(context.student.name, context.courses)
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "lms/templates/lms_base.html" %}
|
||||
{% block title %}
|
||||
{{ course.title if course.title else _("New Course") }}
|
||||
{{ title }} {{ _("Billing") }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="common-page-style">
|
||||
<div class="container form-width common-card-style column-card px-0 h-0 mt-8">
|
||||
{{ Header() }}
|
||||
{{ CourseDetails() }}
|
||||
{{ Details() }}
|
||||
{{ BillingDetails() }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -20,25 +20,31 @@
|
||||
{{ _("Order Details") }}
|
||||
</div>
|
||||
<div>
|
||||
{{ _("Enter the billing information and complete the payment to purchase this course.") }}
|
||||
{{ _("Enter the billing information to complete the payment.").format(module) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro CourseDetails() %}
|
||||
{% macro Details() %}
|
||||
<div class="px-4 pt-5 border-top">
|
||||
<div class="">
|
||||
<div class="flex mb-2">
|
||||
<div class="field-label">
|
||||
{{ _("Course Name: ") }} {{ course.title }}
|
||||
{% set label = "Course" if module == "course" else "Batch" %}
|
||||
{{ _(label) }} : {{ title }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<div class="field-label">
|
||||
{{ _("Total Price: ") }} {{ frappe.utils.fmt_money(course.course_price, 2, course.currency) }}
|
||||
{{ _("Total Price: ") }} {{ frappe.utils.fmt_money(amount, 2, currency) }}
|
||||
</div>
|
||||
</div>
|
||||
{% if gst_applied %}
|
||||
<span class="small mt-2">
|
||||
{{ _("18% GST included") }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
@@ -49,7 +55,7 @@
|
||||
{{ _("Billing Details") }}
|
||||
</div>
|
||||
<div id="billing-form"></div>
|
||||
<button class="btn btn-primary btn-md btn-pay" data-course="{{ course.name | urlencode }}">
|
||||
<button class="btn btn-primary btn-md btn-pay" data-doctype="{{ doctype }}" data-name="{{ docname | urlencode }}">
|
||||
{{ "Proceed to Payment" }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -58,11 +64,4 @@
|
||||
{%- block script %}
|
||||
{{ super() }}
|
||||
<script src="https://checkout.razorpay.com/v1/checkout.js"></script>
|
||||
<script>
|
||||
frappe.boot.user = {
|
||||
"can_create": [],
|
||||
"can_select": ["Country"],
|
||||
"can_read": ["Country"]
|
||||
};
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
frappe.ready(() => {
|
||||
if ($("#billing-form").length) {
|
||||
setup_billing();
|
||||
frappe.require("controls.bundle.js", () => {
|
||||
setup_billing();
|
||||
});
|
||||
}
|
||||
|
||||
$(".btn-pay").click((e) => {
|
||||
@@ -11,6 +13,12 @@ frappe.ready(() => {
|
||||
const setup_billing = () => {
|
||||
this.billing = new frappe.ui.FieldGroup({
|
||||
fields: [
|
||||
{
|
||||
fieldtype: "Data",
|
||||
label: __("Billing Name"),
|
||||
fieldname: "billing_name",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldtype: "Data",
|
||||
label: __("Address Line 1"),
|
||||
@@ -28,20 +36,21 @@ const setup_billing = () => {
|
||||
fieldname: "city",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldtype: "Column Break",
|
||||
},
|
||||
{
|
||||
fieldtype: "Data",
|
||||
label: __("State/Province"),
|
||||
fieldname: "state",
|
||||
},
|
||||
{
|
||||
fieldtype: "Column Break",
|
||||
},
|
||||
{
|
||||
fieldtype: "Link",
|
||||
label: __("Country"),
|
||||
fieldname: "country",
|
||||
options: "Country",
|
||||
reqd: 1,
|
||||
only_select: 1,
|
||||
},
|
||||
{
|
||||
fieldtype: "Data",
|
||||
@@ -55,6 +64,26 @@ const setup_billing = () => {
|
||||
fieldname: "phone",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldtype: "Section Break",
|
||||
label: __("GST Details"),
|
||||
fieldname: "gst_details",
|
||||
depends_on: "eval:doc.country === 'India'",
|
||||
},
|
||||
{
|
||||
fieldtype: "Data",
|
||||
label: __("GSTIN"),
|
||||
fieldname: "gstin",
|
||||
},
|
||||
{
|
||||
fieldtype: "Column Break",
|
||||
fieldname: "gst_details_break",
|
||||
},
|
||||
{
|
||||
fieldtype: "Data",
|
||||
fieldname: "pan",
|
||||
label: __("PAN"),
|
||||
},
|
||||
],
|
||||
body: $("#billing-form").get(0),
|
||||
});
|
||||
@@ -66,19 +95,23 @@ const setup_billing = () => {
|
||||
|
||||
const generate_payment_link = (e) => {
|
||||
address = this.billing.get_values();
|
||||
let course = decodeURIComponent($(e.currentTarget).attr("data-course"));
|
||||
let doctype = $(e.currentTarget).attr("data-doctype");
|
||||
let docname = decodeURIComponent($(e.currentTarget).attr("data-name"));
|
||||
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_course.lms_course.get_payment_options",
|
||||
method: "lms.lms.utils.get_payment_options",
|
||||
args: {
|
||||
course: course,
|
||||
doctype: doctype,
|
||||
docname: docname,
|
||||
phone: address.phone,
|
||||
country: address.country,
|
||||
},
|
||||
callback: (data) => {
|
||||
data.message.handler = (response) => {
|
||||
handle_success(
|
||||
response,
|
||||
course,
|
||||
doctype,
|
||||
docname,
|
||||
address,
|
||||
data.message.order_id
|
||||
);
|
||||
@@ -89,12 +122,13 @@ const generate_payment_link = (e) => {
|
||||
});
|
||||
};
|
||||
|
||||
const handle_success = (response, course, address, order_id) => {
|
||||
const handle_success = (response, doctype, docname, address, order_id) => {
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_course.lms_course.verify_payment",
|
||||
method: "lms.lms.utils.verify_payment",
|
||||
args: {
|
||||
response: response,
|
||||
course: course,
|
||||
doctype: doctype,
|
||||
docname: docname,
|
||||
address: address,
|
||||
order_id: order_id,
|
||||
},
|
||||
|
||||
@@ -1,23 +1,77 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from lms.lms.utils import check_multicurrency, apply_gst
|
||||
|
||||
|
||||
def get_context(context):
|
||||
course_name = frappe.form_dict.course
|
||||
module = frappe.form_dict.module
|
||||
docname = frappe.form_dict.modulename
|
||||
doctype = "LMS Course" if module == "course" else "LMS Batch"
|
||||
|
||||
if not course_name:
|
||||
raise ValueError(_("Course is required."))
|
||||
context.module = module
|
||||
context.docname = docname
|
||||
context.doctype = doctype
|
||||
|
||||
validate_access(doctype, docname, module)
|
||||
get_billing_details(context)
|
||||
context.amount, context.currency = check_multicurrency(
|
||||
context.amount, context.currency
|
||||
)
|
||||
|
||||
if context.currency == "INR":
|
||||
context.amount, context.gst_applied = apply_gst(context.amount, None)
|
||||
|
||||
|
||||
def validate_access(doctype, docname, module):
|
||||
if frappe.session.user == "Guest":
|
||||
raise frappe.PermissionError(_("You are not allowed to access this page."))
|
||||
|
||||
membership = frappe.db.exists(
|
||||
"LMS Enrollment", {"member": frappe.session.user, "course": course_name}
|
||||
)
|
||||
if module not in ["course", "batch"]:
|
||||
raise ValueError(_("Module is incorrect."))
|
||||
|
||||
if membership:
|
||||
raise frappe.PermissionError(_("You are already enrolled for this course"))
|
||||
if not frappe.db.exists(doctype, docname):
|
||||
raise ValueError(_("Module Name is incorrect or does not exist."))
|
||||
|
||||
context.course = frappe.db.get_value(
|
||||
"LMS Course", course_name, ["title", "name", "course_price", "currency"], as_dict=True
|
||||
)
|
||||
if doctype == "LMS Course":
|
||||
membership = frappe.db.exists(
|
||||
"LMS Enrollment", {"member": frappe.session.user, "course": docname}
|
||||
)
|
||||
if membership:
|
||||
raise frappe.PermissionError(_("You are already enrolled for this course"))
|
||||
|
||||
else:
|
||||
membership = frappe.db.exists(
|
||||
"Batch Student", {"student": frappe.session.user, "parent": docname}
|
||||
)
|
||||
if membership:
|
||||
raise frappe.PermissionError(_("You are already enrolled for this batch."))
|
||||
|
||||
|
||||
def get_billing_details(context):
|
||||
if context.doctype == "LMS Course":
|
||||
details = frappe.db.get_value(
|
||||
"LMS Course",
|
||||
context.docname,
|
||||
["title", "name", "paid_course", "course_price as amount", "currency"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
if not details.paid_course:
|
||||
raise frappe.PermissionError(_("This course is free."))
|
||||
|
||||
else:
|
||||
details = frappe.db.get_value(
|
||||
"LMS Batch",
|
||||
context.docname,
|
||||
["title", "name", "paid_batch", "amount", "currency"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
if not details.paid_batch:
|
||||
raise frappe.PermissionError(
|
||||
_("To join this batch, please contact the Administrator.")
|
||||
)
|
||||
|
||||
context.title = details.title
|
||||
context.amount = details.amount
|
||||
context.currency = details.currency
|
||||
|
||||
63
lms/www/certified_participants/certified_participants.html
Normal file
63
lms/www/certified_participants/certified_participants.html
Normal file
@@ -0,0 +1,63 @@
|
||||
{% extends "lms/templates/lms_base.html" %}
|
||||
{% block title %}
|
||||
{{ _("Certified Participants") }}
|
||||
{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
<main class="common-page-style">
|
||||
<div class="container">
|
||||
<header>
|
||||
{% if course_filter | length %}
|
||||
<select class="lms-menu pull-right" id="certificate-filter">
|
||||
<option selected value="">
|
||||
{{ _("Filter by Certificate") }}
|
||||
</option>
|
||||
{% for course in course_filter %}
|
||||
<option value="{{ course }}">
|
||||
{{ course }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% endif %}
|
||||
<div class="page-title mb-5">
|
||||
{{ _("Certified Participants") }}
|
||||
</div>
|
||||
</header>
|
||||
{% if participants | length %}
|
||||
{{ ParticipantsList() }}
|
||||
{% else %}
|
||||
{{ EmptyState() }}
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
{% macro ParticipantsList() %}
|
||||
<article class="member-parent">
|
||||
{% for participant in participants %}
|
||||
<div class="common-card-style column-card align-center">
|
||||
{{ widgets.Avatar(member=participant, avatar_class="avatar-large") }}
|
||||
<div class="bold-heading text-center">
|
||||
{{ participant.full_name }}
|
||||
</div>
|
||||
{% for course in participant.courses %}
|
||||
<div class="course-name text-center mb-1" data-course="{{ course }}">
|
||||
{{ course }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<a class="stretched-link" href="/users/{{ participant.username }}"></a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</article>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro EmptyState() %}
|
||||
<div class="empty-state">
|
||||
<img class="icon icon-xl" src="/assets/lms/icons/comment.svg">
|
||||
<div class="empty-state-text">
|
||||
<div class="empty-state-heading">{{ _("No Certified Participants") }}</div>
|
||||
<div class="course-meta">{{ _("Enroll in a batch to get certified.") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
18
lms/www/certified_participants/certified_participants.js
Normal file
18
lms/www/certified_participants/certified_participants.js
Normal file
@@ -0,0 +1,18 @@
|
||||
frappe.ready(() => {
|
||||
$("#certificate-filter").change((e) => {
|
||||
filter_certified_participants();
|
||||
});
|
||||
});
|
||||
|
||||
const filter_certified_participants = () => {
|
||||
const certificate = $("#certificate-filter").val();
|
||||
$(".common-card-style").removeClass("hide");
|
||||
|
||||
if (certificate) {
|
||||
$(".common-card-style").addClass("hide");
|
||||
$(`[data-course='${certificate}']`)
|
||||
.closest(".common-card-style")
|
||||
.removeClass("hide");
|
||||
console.log(certificate);
|
||||
}
|
||||
};
|
||||
42
lms/www/certified_participants/certified_participants.py
Normal file
42
lms/www/certified_participants/certified_participants.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
members = frappe.get_all(
|
||||
"LMS Certificate",
|
||||
filters={"published": 1},
|
||||
pluck="member",
|
||||
order_by="issue_date desc",
|
||||
distinct=1,
|
||||
)
|
||||
|
||||
participants = []
|
||||
course_filter = []
|
||||
for member in members:
|
||||
details = frappe.db.get_value(
|
||||
"User", member, ["name", "full_name", "user_image", "username", "enabled"], as_dict=1
|
||||
)
|
||||
courses = frappe.get_all(
|
||||
"LMS Certificate",
|
||||
filters={"member": member, "published": 1},
|
||||
fields=["course", "issue_date"],
|
||||
)
|
||||
details.courses = []
|
||||
for course in courses:
|
||||
|
||||
if not details.issue_date:
|
||||
details.issue_date = course.issue_date
|
||||
|
||||
title = frappe.db.get_value("LMS Course", course.course, "title")
|
||||
details.courses.append(title)
|
||||
|
||||
if title not in course_filter:
|
||||
course_filter.append(title)
|
||||
|
||||
if details.enabled:
|
||||
participants.append(details)
|
||||
|
||||
participants = sorted(participants, key=lambda d: d.issue_date, reverse=True)
|
||||
context.participants = participants
|
||||
context.course_filter = course_filter
|
||||
@@ -1,163 +0,0 @@
|
||||
{% extends "lms/templates/lms_base.html" %}
|
||||
{% block title %}
|
||||
{{ _("All Classes") }}
|
||||
{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
<div class="common-page-style lms-page-style">
|
||||
<div class="container">
|
||||
{{ Header() }}
|
||||
{% if past_classes | length or upcoming_classes | length %}
|
||||
{{ ClassTabs(past_classes, upcoming_classes, my_classes) }}
|
||||
{% else %}
|
||||
{{ EmptyState() }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% macro Header() %}
|
||||
<header class="edit-header">
|
||||
<div class="page-title mb-6"> {{ _("All Classes") }} </div>
|
||||
{% if is_moderator %}
|
||||
<button class="btn btn-default btn-sm pull-right" id="create-class">
|
||||
{{ _("Create Class") }}
|
||||
</button>
|
||||
{% endif %}
|
||||
</header>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro ClassTabs(past_classes, upcoming_classes, my_classes) %}
|
||||
<article>
|
||||
<ul class="nav lms-nav" id="courses-tab">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-toggle="tab" href="#upcoming">
|
||||
{{ _("Upcoming") }}
|
||||
<span class="course-list-count">
|
||||
{{ upcoming_classes | length }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% if is_moderator %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#past">
|
||||
{{ _("Past Classes") }}
|
||||
<span class="course-list-count">
|
||||
{{ past_classes | length }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if frappe.session.user != "Guest" %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#my-class">
|
||||
{{ _("My Classes") }}
|
||||
<span class="course-list-count">
|
||||
{{ my_classes | length }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
</ul>
|
||||
|
||||
<div class="border-bottom mb-4"></div>
|
||||
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="upcoming" role="tabpanel" aria-labelledby="upcoming">
|
||||
{{ ClassCards(upcoming_classes) }}
|
||||
</div>
|
||||
|
||||
{% if is_moderator %}
|
||||
<div class="tab-pane" id="past" role="tabpanel" aria-labelledby="past">
|
||||
{{ ClassCards(past_classes) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if frappe.session.user != "Guest" %}
|
||||
<div class="tab-pane" id="my-class" role="tabpanel" aria-labelledby="my-classes">
|
||||
{{ ClassCards(my_classes) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</article>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro ClassCards(classes) %}
|
||||
<div class="lms-card-parent">
|
||||
{% for class in classes %}
|
||||
{% set course_count = frappe.db.count("Class Course", {"parent": class.name}) %}
|
||||
{% set student_count = frappe.db.count("Class Student", {"parent": class.name}) %}
|
||||
|
||||
<div class="common-card-style column-card" style="min-height: 150px;">
|
||||
|
||||
<div class="bold-heading">
|
||||
{{ class.title }}
|
||||
</div>
|
||||
|
||||
{% if class.description %}
|
||||
<div class="short-introduction">
|
||||
{{ class.description }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-auto mb-1">
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-calendar"></use>
|
||||
</svg>
|
||||
<span>
|
||||
{{ frappe.utils.format_date(class.start_date, "medium") }} -
|
||||
</span>
|
||||
<span>
|
||||
{{ frappe.utils.format_date(class.end_date, "medium") }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mb-1">
|
||||
<svg class="icon icon-md">
|
||||
<use href="#icon-education"></use>
|
||||
</svg>
|
||||
{{ course_count }} {{ _("Courses") }}
|
||||
</div>
|
||||
|
||||
<div class="mb-1">
|
||||
<svg class="icon icon-md">
|
||||
<use href="#icon-users"></use>
|
||||
</svg>
|
||||
{{ student_count }} {{ _("Students") }}
|
||||
</div>
|
||||
|
||||
<a class="stretched-link" href="/classes/{{ class.name }}"></a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro EmptyState() %}
|
||||
<div class="empty-state">
|
||||
<img class="icon icon-xl" src="/assets/lms/icons/comment.svg">
|
||||
<div class="empty-state-text">
|
||||
<div class="empty-state-heading">{{ _("No Classes") }}</div>
|
||||
<div class="course-meta">{{ _("Nothing to see here.") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{%- block script %}
|
||||
{{ super() }}
|
||||
{{ include_script('controls.bundle.js') }}
|
||||
{% if is_moderator %}
|
||||
<script>
|
||||
frappe.boot.user = {
|
||||
"can_create": [],
|
||||
"can_select": ["LMS Category"],
|
||||
"can_read": ["LMS Category"]
|
||||
};
|
||||
let class_info = null;
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -1,48 +0,0 @@
|
||||
import frappe
|
||||
from frappe.utils import getdate
|
||||
from lms.lms.utils import has_course_moderator_role
|
||||
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
context.is_moderator = has_course_moderator_role()
|
||||
classes = frappe.get_all(
|
||||
"LMS Class",
|
||||
fields=[
|
||||
"name",
|
||||
"title",
|
||||
"description",
|
||||
"start_date",
|
||||
"end_date",
|
||||
"paid_class",
|
||||
"seat_count",
|
||||
],
|
||||
)
|
||||
|
||||
past_classes, upcoming_classes = [], []
|
||||
for class_ in classes:
|
||||
if getdate(class_.start_date) < getdate():
|
||||
past_classes.append(class_)
|
||||
else:
|
||||
upcoming_classes.append(class_)
|
||||
|
||||
context.past_classes = sorted(past_classes, key=lambda d: d.start_date)
|
||||
context.upcoming_classes = sorted(upcoming_classes, key=lambda d: d.start_date)
|
||||
|
||||
if frappe.session.user != "Guest":
|
||||
my_classes_info = []
|
||||
my_classes = frappe.get_all(
|
||||
"Class Student", {"student": frappe.session.user}, pluck="parent"
|
||||
)
|
||||
|
||||
for class_ in my_classes:
|
||||
my_classes_info.append(
|
||||
frappe.db.get_value(
|
||||
"LMS Class",
|
||||
class_,
|
||||
["name", "title", "start_date", "end_date", "paid_class", "seat_count"],
|
||||
as_dict=True,
|
||||
)
|
||||
)
|
||||
|
||||
context.my_classes = my_classes_info
|
||||
@@ -1,5 +0,0 @@
|
||||
frappe.ready(() => {
|
||||
$(".clickable-row").click((e) => {
|
||||
window.location.href = $(e.currentTarget).data("href");
|
||||
});
|
||||
});
|
||||
@@ -10,8 +10,8 @@
|
||||
{{ CourseHomeHeader(course) }}
|
||||
<div class="course-home-page">
|
||||
<div class="container">
|
||||
{{ CourseHeaderOverlay(course) }}
|
||||
<div class="course-body-container">
|
||||
{{ CourseHeaderOverlay(course) }}
|
||||
{{ Description(course) }}
|
||||
{{ widgets.CourseOutline(course=course, membership=membership, is_user_interested=is_user_interested) }}
|
||||
{% if course.status == "Approved" and not frappe.utils.cint(course.upcoming) %}
|
||||
@@ -41,7 +41,7 @@
|
||||
{% macro BreadCrumb(course) %}
|
||||
<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">
|
||||
<img class="" src="/assets/lms/icons/chevron-right.svg">
|
||||
<span class="breadcrumb-destination">{{ course.title if course.title else _("New Course") }}</span>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
@@ -57,8 +57,8 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div id="title" {% if course.name %} data-course="{{ course.name | urlencode }}" {% endif %} class="page-title">
|
||||
{% if course.title %} {{ course.title }} {% endif %}
|
||||
<div id="title" class="page-title">
|
||||
{{ course.title }}
|
||||
</div>
|
||||
|
||||
<div id="intro">
|
||||
@@ -228,7 +228,7 @@
|
||||
</a>
|
||||
|
||||
{% elif course.paid_course and not is_instructor %}
|
||||
<a class="btn btn-primary wide-button" href="/billing/{{ course.name | urlencode }}">
|
||||
<a class="btn btn-primary wide-button" href="/billing/course/{{ course.name | urlencode }}">
|
||||
{{ _("Buy This Course") }}
|
||||
</a>
|
||||
|
||||
|
||||
@@ -48,15 +48,15 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if show_creators_section %}
|
||||
<a class="btn btn-default btn-sm" href="/courses/new-course/edit">
|
||||
{{ _("Create a Course") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<a class="btn btn-default btn-sm" id="open-search">
|
||||
{{ _("Search") }} (Ctrl + k)
|
||||
</a>
|
||||
|
||||
{% if show_creators_section %}
|
||||
<a class="btn btn-primary btn-sm" href="/courses/new-course/edit">
|
||||
{{ _("Create a Course") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -146,7 +146,7 @@
|
||||
{{ _("Short Description") }}
|
||||
</div>
|
||||
<div class="field-description">
|
||||
{{ _("A breif description about this chapter.") }}
|
||||
{{ _("A brief description about this chapter.") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="">
|
||||
|
||||
@@ -65,8 +65,8 @@
|
||||
<div class="tab-pane active" id="profile" role="tabpanel" aria-labelledby="profile">
|
||||
<div class="">
|
||||
{{ About(member) }}
|
||||
{{ EducationDetails(member) }}
|
||||
{{ WorkDetails(member) }}
|
||||
{{ EducationDetails(member) }}
|
||||
{{ ExternalCertification(member) }}
|
||||
{{ Contact(member) }}
|
||||
{{ Skills(member) }}
|
||||
@@ -171,7 +171,7 @@
|
||||
{% macro CoursesMentored(member, read_only) %}
|
||||
{% if member.get_mentored_courses() | length %}
|
||||
<div class="profile-courses">
|
||||
<div class="course-home-headings"> {{ _("Courses Mentored") }} </div>
|
||||
<div class="page-title"> {{ _("Courses Mentored") }} </div>
|
||||
<div class="cards-parent">
|
||||
{% for course in member.get_mentored_courses() %}
|
||||
{{ widgets.CourseCard(course=course, read_only=read_only) }}
|
||||
@@ -202,7 +202,7 @@
|
||||
{% if has_course_moderator_role() %}
|
||||
<div class="">
|
||||
<div class="">
|
||||
<div class="course-home-headings"> {{ _("Role Settings") }} </div>
|
||||
<div class="page-title mb-2"> {{ _("Role Settings") }} </div>
|
||||
<div class="d-flex">
|
||||
<label class="role">
|
||||
<input type="checkbox" id="course-creator" data-role="Course Creator"
|
||||
@@ -223,7 +223,6 @@
|
||||
|
||||
<!-- About Section -->
|
||||
{% macro About(member) %}
|
||||
<div class="course-home-headings"> {{ _("About") }} </div>
|
||||
<div class="description">
|
||||
{% if member.bio %}
|
||||
{{ member.bio }}
|
||||
@@ -236,7 +235,7 @@
|
||||
|
||||
<!-- Work Preference -->
|
||||
{% macro WorkPreference(member) %}
|
||||
<div class="course-home-headings mt-10"> {{ _("Work Preference") }} </div>
|
||||
<div class="page-title mt-10"> {{ _("Work Preference") }} </div>
|
||||
<div> {{ member.attire }} </div>
|
||||
<div> {{ member.collaboration }} </div>
|
||||
<div> {{ member.role }} </div>
|
||||
@@ -249,7 +248,7 @@
|
||||
<!-- Career Preference -->
|
||||
{% macro CareerPreference(member) %}
|
||||
{% if member.preferred_functions or member.preferred_industries or member.preferred_location or member.dream_companies %}
|
||||
<div class="course-home-headings mt-10">
|
||||
<div class="page-title mt-10">
|
||||
{{ _("Career Preference") }}
|
||||
</div>
|
||||
<div class="profile-column-grid">
|
||||
@@ -294,7 +293,7 @@
|
||||
<!-- Contact Section -->
|
||||
{% macro Contact(member) %}
|
||||
{% if member.linkedin or member.medium or member.github %}
|
||||
<div class="course-home-headings mt-10"> {{ _("Contact") }} </div>
|
||||
<div class="page-title mt-10"> {{ _("Contact") }} </div>
|
||||
<div class="profile-column-grid">
|
||||
{% if member.linkedin %}
|
||||
{% set linkedin = member.linkedin[:-1] if member.linkedin[-1] == "/" else member.linkedin %}
|
||||
@@ -323,7 +322,7 @@
|
||||
<!-- Skills -->
|
||||
{% macro Skills(member) %}
|
||||
{% if member.skill | length %}
|
||||
<div class="course-home-headings mt-10"> {{ _("Skills")}} </div>
|
||||
<div class="page-title mt-10"> {{ _("Skills")}} </div>
|
||||
<div class="profile-column-grid">
|
||||
{% for skill in member.skill %}
|
||||
<div class="description"> {{ skill.skill_name }} </div>
|
||||
@@ -336,11 +335,11 @@
|
||||
<!-- Education Details -->
|
||||
{% macro EducationDetails(member) %}
|
||||
{% if member.education %}
|
||||
<div class="course-home-headings mt-10"> {{ _("Education") }} </div>
|
||||
<div class="page-title mt-10 mb-2"> {{ _("Education") }} </div>
|
||||
<div class="profile-grid-card">
|
||||
{% for edu in member.education %}
|
||||
<div class="column-card-row">
|
||||
<div class="bold-title"> {{ edu.institution_name }} </div>
|
||||
<div class="bold-heading"> {{ edu.institution_name }} </div>
|
||||
<div class="profile-item"> {{ edu.degree_type }} <span></span> {{ edu.major }}
|
||||
{% if not member.hide_private %}
|
||||
<!-- {% if edu.grade_type %} {{ edu.grade_type }} {% endif %} -->
|
||||
@@ -362,17 +361,16 @@
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Work Details -->
|
||||
{% macro WorkDetails(member) %}
|
||||
{% set work_details = member.work_experience + member.internship %}
|
||||
|
||||
{% if work_details | length %}
|
||||
<div class="course-home-headings mt-10"> {{ _("Work Experience") }} </div>
|
||||
<div class="page-title mt-10 mb-2"> {{ _("Work Experience") }} </div>
|
||||
<div class="profile-grid-card">
|
||||
|
||||
{% for work in work_details %}
|
||||
<div class="">
|
||||
<div class="bold-title"> {{ work.title }} </div>
|
||||
<div class="bold-heading"> {{ work.title }} </div>
|
||||
<div class="profile-item"> {{ work.company }} </div>
|
||||
<div class="description">
|
||||
{{ frappe.utils.format_date(work.from_date, "MMM YYYY") }} -
|
||||
@@ -398,7 +396,7 @@
|
||||
<!-- Certifications -->
|
||||
{% macro ExternalCertification(member) %}
|
||||
{% if member.certification %}
|
||||
<div class="course-home-headings mt-10"> {{ _("External Certification") }} </div>
|
||||
<div class="page-title mt-10"> {{ _("External Certification") }} </div>
|
||||
<div class="profile-grid-card">
|
||||
{% for cert in member.certification %}
|
||||
<div class="">
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="vertically-center small">
|
||||
<a class="dark-links" href="/classes">
|
||||
{{ _("All Classes") }}
|
||||
<a class="dark-links" href="/batches">
|
||||
{{ _("All Batches") }}
|
||||
</a>
|
||||
<img class="icon icon-sm mr-0" src="/assets/lms/icons/chevron-right.svg">
|
||||
<span class="breadcrumb-destination">{{ _("Quiz Submission") }}</span>
|
||||
|
||||
@@ -61,13 +61,13 @@ def get_current_lesson_details(lesson_number, context, is_edit=False):
|
||||
return lesson_info
|
||||
|
||||
|
||||
def get_assessments(class_name, member=None):
|
||||
def get_assessments(batch, member=None):
|
||||
if not member:
|
||||
member = frappe.session.user
|
||||
|
||||
assessments = frappe.get_all(
|
||||
"LMS Assessment",
|
||||
{"parent": class_name},
|
||||
{"parent": batch},
|
||||
["name", "assessment_type", "assessment_name"],
|
||||
)
|
||||
|
||||
@@ -130,3 +130,16 @@ def get_quiz_details(assessment, member):
|
||||
existing_submission[0].name if len(existing_submission) else "new-submission"
|
||||
)
|
||||
assessment.url = f"/quiz-submission/{assessment.assessment_name}/{submission_name}"
|
||||
|
||||
|
||||
def is_student(batch, member=None):
|
||||
if not member:
|
||||
member = frappe.session.user
|
||||
|
||||
return frappe.db.exists(
|
||||
"Batch Student",
|
||||
{
|
||||
"student": member,
|
||||
"parent": batch,
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user