Merge pull request #600 from pateljannat/paid-class

feat: Batches Revamp
This commit is contained in:
Jannat Patel
2023-09-13 15:18:55 +05:30
committed by GitHub
80 changed files with 2066 additions and 976 deletions

View File

@@ -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 👨‍👩‍👧‍👦

View File

@@ -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": [],
}

View File

@@ -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},

View File

@@ -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": [],

View File

@@ -5,5 +5,5 @@
from frappe.model.document import Document
class ClassCourse(Document):
class BatchCourse(Document):
pass

View File

@@ -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) {
// }
});

View File

@@ -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",

View File

@@ -5,5 +5,5 @@
from frappe.model.document import Document
class ClassStudent(Document):
class BatchStudent(Document):
pass

View File

@@ -5,5 +5,5 @@
from frappe.tests.utils import FrappeTestCase
class TestClassStudent(FrappeTestCase):
class TestBatchStudent(FrappeTestCase):
pass

View File

@@ -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",

View File

@@ -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,
},

View File

@@ -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,

View File

@@ -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

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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)

View File

@@ -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)
)

View File

@@ -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",

View File

@@ -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",

View File

@@ -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)

View 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) {
// },
// });

View 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"
}

View 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

View 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

View File

@@ -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",

View 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) {
// },
// });

View 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": []
}

View 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

View 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

View File

@@ -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]

View File

@@ -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);
}
};

View File

@@ -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

View File

@@ -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

View 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)

View 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)

View File

@@ -0,0 +1,12 @@
import frappe
def execute():
frappe.db.set_value(
"Top Bar Item",
{"url": "/classes"},
{
"label": "Batches",
"url": "/batches",
},
)

View 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)

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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}`;
}
},
});

View File

@@ -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>

View File

@@ -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 %}

View File

@@ -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>

View File

@@ -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,
};
}
}

View File

@@ -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">

View File

@@ -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"),
}
)

View File

View 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 %}

View File

@@ -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();

View File

@@ -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

View 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 %}

View 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();
},
});
});
};

View 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
View 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
View 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

View File

@@ -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 %}

View 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();
};

View File

@@ -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)

View File

@@ -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 %}

View File

@@ -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,
},

View File

@@ -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

View 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 %}

View 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);
}
};

View 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

View File

@@ -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 %}

View File

@@ -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

View File

@@ -1,5 +0,0 @@
frappe.ready(() => {
$(".clickable-row").click((e) => {
window.location.href = $(e.currentTarget).data("href");
});
});

View File

@@ -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>

View File

@@ -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>

View File

@@ -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="">

View File

@@ -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="">

View File

@@ -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>

View File

@@ -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,
},
)