feat: class billing
This commit is contained in:
@@ -189,9 +189,13 @@ website_route_rules = [
|
||||
"to_route": "quiz_submission/quiz_submission",
|
||||
},
|
||||
{
|
||||
"from_route": "/billing/<course>",
|
||||
"from_route": "/billing/<module>/<modulename>",
|
||||
"to_route": "billing/billing",
|
||||
},
|
||||
{
|
||||
"from_route": "/classes/details/<classname>",
|
||||
"to_route": "classes/class_details",
|
||||
},
|
||||
]
|
||||
|
||||
website_redirects = [
|
||||
@@ -249,6 +253,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": [],
|
||||
}
|
||||
|
||||
@@ -10,10 +10,13 @@
|
||||
"student",
|
||||
"student_name",
|
||||
"username",
|
||||
"column_break_zvlp",
|
||||
"address",
|
||||
"column_break_zvlp",
|
||||
"amount",
|
||||
"currency"
|
||||
"currency",
|
||||
"order_id",
|
||||
"payment_id",
|
||||
"payment_received"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -59,12 +62,28 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Currency",
|
||||
"options": "Currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "order_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "Order ID"
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "Payment ID"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "payment_received",
|
||||
"fieldtype": "Check",
|
||||
"label": "Payment Received"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-18 19:19:34.701473",
|
||||
"modified": "2023-08-22 10:41:40.577437",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Class Student",
|
||||
|
||||
@@ -19,11 +19,16 @@
|
||||
"category",
|
||||
"column_break_flwy",
|
||||
"seat_count",
|
||||
"paid_class",
|
||||
"section_break_6",
|
||||
"description",
|
||||
"prerequisite",
|
||||
"students",
|
||||
"courses",
|
||||
"section_break_gsac",
|
||||
"paid_class",
|
||||
"column_break_iens",
|
||||
"amount",
|
||||
"currency",
|
||||
"section_break_ubxi",
|
||||
"custom_component",
|
||||
"assessment_tab",
|
||||
@@ -89,6 +94,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Students will be enrolled in a paid class once they complete the payment",
|
||||
"fieldname": "paid_class",
|
||||
"fieldtype": "Check",
|
||||
"label": "Paid Class"
|
||||
@@ -158,11 +164,39 @@
|
||||
"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_class",
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount"
|
||||
},
|
||||
{
|
||||
"depends_on": "paid_class",
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Currency",
|
||||
"options": "Currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "prerequisite",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Prerequisite",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-10 12:54:44.351907",
|
||||
"modified": "2023-08-22 11:53:22.248596",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Class",
|
||||
@@ -192,18 +226,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
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
|
||||
@@ -185,42 +185,6 @@ def authenticate():
|
||||
return response.json()["access_token"]
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_class(
|
||||
title,
|
||||
start_date,
|
||||
end_date,
|
||||
description=None,
|
||||
seat_count=0,
|
||||
start_time=None,
|
||||
end_time=None,
|
||||
medium="Online",
|
||||
category=None,
|
||||
name=None,
|
||||
):
|
||||
frappe.only_for("Moderator")
|
||||
if name:
|
||||
class_details = frappe.get_doc("LMS Class", name)
|
||||
else:
|
||||
class_details = frappe.get_doc({"doctype": "LMS Class"})
|
||||
|
||||
class_details.update(
|
||||
{
|
||||
"title": title,
|
||||
"start_date": start_date,
|
||||
"end_date": end_date,
|
||||
"description": description,
|
||||
"seat_count": seat_count,
|
||||
"start_time": start_time,
|
||||
"end_time": end_time,
|
||||
"medium": medium,
|
||||
"category": category,
|
||||
}
|
||||
)
|
||||
class_details.save()
|
||||
return class_details
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def fetch_lessons(courses):
|
||||
lessons = []
|
||||
|
||||
@@ -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):
|
||||
@@ -362,115 +361,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 Batch Membership")
|
||||
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)
|
||||
)
|
||||
|
||||
166
lms/lms/utils.py
166
lms/lms/utils.py
@@ -1,6 +1,8 @@
|
||||
import re
|
||||
import string
|
||||
import frappe
|
||||
import json
|
||||
import razorpay
|
||||
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 +15,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]+")
|
||||
@@ -825,3 +827,165 @@ 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):
|
||||
if not frappe.db.exists(doctype, docname):
|
||||
frappe.throw(_("Invalid document provided."))
|
||||
|
||||
validate_phone_number(phone, True)
|
||||
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 Class",
|
||||
docname,
|
||||
["name", "title", "paid_class", "currency", "amount"],
|
||||
as_dict=True,
|
||||
)
|
||||
if not details.paid_class:
|
||||
frappe.throw(_("To join this class, please contact the Administrator."))
|
||||
|
||||
razorpay_key = frappe.db.get_single_value("LMS Settings", "razorpay_key")
|
||||
client = get_client()
|
||||
order = create_order(client, details.amount, details.currency)
|
||||
|
||||
options = {
|
||||
"key_id": 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 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, 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"],
|
||||
}
|
||||
)
|
||||
|
||||
if doctype == "LMS Course":
|
||||
return create_membership(address, response, docname, client)
|
||||
else:
|
||||
return add_student_to_class(address, response, docname, client)
|
||||
|
||||
|
||||
def create_membership(address, response, course, client):
|
||||
try:
|
||||
address_name = save_address(address)
|
||||
payment = client.payment.fetch(response["razorpay_payment_id"])
|
||||
membership = frappe.new_doc("LMS Batch Membership")
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
|
||||
def add_student_to_class(address, response, classname, client):
|
||||
try:
|
||||
address_name = save_address(address)
|
||||
payment = client.payment.fetch(response["razorpay_payment_id"])
|
||||
student = frappe.new_doc("Class Student")
|
||||
|
||||
student.update(
|
||||
{
|
||||
"student": frappe.session.user,
|
||||
"parent": classname,
|
||||
"parenttype": "LMS Class",
|
||||
"parentfield": "students",
|
||||
"address": address_name,
|
||||
"amount": payment["amount"] / 100,
|
||||
"currency": payment["currency"],
|
||||
"payment_received": 1,
|
||||
"order_id": response["razorpay_order_id"],
|
||||
"payment_id": response["razorpay_payment_id"],
|
||||
}
|
||||
)
|
||||
student.save(ignore_permissions=True)
|
||||
return f"/classes/{classname}"
|
||||
except Exception as e:
|
||||
frappe.throw(
|
||||
_("Error during payment: {0}. Please contact the Administrator.").format(e)
|
||||
)
|
||||
|
||||
@@ -2302,3 +2302,6 @@ select {
|
||||
padding-right: 1rem !important;
|
||||
}
|
||||
|
||||
.class-overlay {
|
||||
top: 30%;
|
||||
}
|
||||
@@ -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 |
@@ -320,6 +320,41 @@ const open_class_dialog = () => {
|
||||
default: class_info && class_info.description,
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldtype: "Small Text",
|
||||
label: __("Prerequisite"),
|
||||
fieldname: "prerequisite",
|
||||
default: class_info && class_info.prerequisite,
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldtype: "Section Break",
|
||||
label: __("Pricing"),
|
||||
fieldname: "pricing",
|
||||
},
|
||||
{
|
||||
fieldtype: "Check",
|
||||
label: __("Paid Class"),
|
||||
fieldname: "paid_class",
|
||||
default: class_info && class_info.paid_class,
|
||||
},
|
||||
{
|
||||
fieldtype: "Currency",
|
||||
label: __("Amount"),
|
||||
fieldname: "amount",
|
||||
default: class_info && class_info.amount,
|
||||
mandatory_depends_on: "paid_class",
|
||||
depends_on: "paid_class",
|
||||
},
|
||||
{
|
||||
fieldtype: "Link",
|
||||
label: __("Currency"),
|
||||
fieldname: "currency",
|
||||
options: "Currency",
|
||||
default: class_info && class_info.currency,
|
||||
mandatory_depends_on: "paid_class",
|
||||
depends_on: "paid_class",
|
||||
},
|
||||
],
|
||||
primary_action_label: __("Save"),
|
||||
primary_action: (values) => {
|
||||
@@ -330,19 +365,19 @@ const open_class_dialog = () => {
|
||||
};
|
||||
|
||||
const save_class = (values) => {
|
||||
let method, args;
|
||||
if (class_info) {
|
||||
method = "frappe.client.save";
|
||||
args = Object.assign(class_info, values);
|
||||
} else {
|
||||
method = "frappe.client.insert";
|
||||
args = values;
|
||||
args.doctype = "LMS Class";
|
||||
}
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_class.lms_class.create_class",
|
||||
method: method,
|
||||
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,
|
||||
doc: args,
|
||||
},
|
||||
callback: (r) => {
|
||||
if (r.message) {
|
||||
@@ -353,7 +388,7 @@ const save_class = (values) => {
|
||||
indicator: "green",
|
||||
});
|
||||
this.class_dialog.hide();
|
||||
window.location.href = `/classes/${r.message.name}`;
|
||||
window.location.href = `/classes/details/${r.message.name}`;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,23 +20,24 @@
|
||||
{{ _("Order Details") }}
|
||||
</div>
|
||||
<div>
|
||||
{{ _("Enter the billing information and complete the payment to purchase this course.") }}
|
||||
{{ _("Enter the billing information and complete the payment to purchase this {0}.").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 Name" if module == "course" else "Class Name" %}
|
||||
{{ _(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>
|
||||
</div>
|
||||
@@ -49,7 +50,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>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
frappe.ready(() => {
|
||||
if ($("#billing-form").length) {
|
||||
setup_billing();
|
||||
frappe.require("controls.bundle.js", () => {
|
||||
setup_billing();
|
||||
});
|
||||
}
|
||||
|
||||
$(".btn-pay").click((e) => {
|
||||
@@ -66,19 +68,22 @@ 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,
|
||||
},
|
||||
callback: (data) => {
|
||||
data.message.handler = (response) => {
|
||||
handle_success(
|
||||
response,
|
||||
course,
|
||||
doctype,
|
||||
docname,
|
||||
address,
|
||||
data.message.order_id
|
||||
);
|
||||
@@ -89,12 +94,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,
|
||||
},
|
||||
|
||||
@@ -3,21 +3,66 @@ from frappe import _
|
||||
|
||||
|
||||
def get_context(context):
|
||||
course_name = frappe.form_dict.course
|
||||
|
||||
if not course_name:
|
||||
raise ValueError(_("Course is required."))
|
||||
module = frappe.form_dict.module
|
||||
docname = frappe.form_dict.modulename
|
||||
|
||||
if frappe.session.user == "Guest":
|
||||
raise frappe.PermissionError(_("You are not allowed to access this page."))
|
||||
|
||||
membership = frappe.db.exists(
|
||||
"LMS Batch Membership", {"member": frappe.session.user, "course": course_name}
|
||||
)
|
||||
if module not in ["course", "class"]:
|
||||
raise ValueError(_("Module is incorrect."))
|
||||
|
||||
if membership:
|
||||
raise frappe.PermissionError(_("You are already enrolled for this course"))
|
||||
doctype = "LMS Course" if module == "course" else "LMS Class"
|
||||
context.module = module
|
||||
context.docname = docname
|
||||
context.doctype = doctype
|
||||
|
||||
context.course = frappe.db.get_value(
|
||||
"LMS Course", course_name, ["title", "name", "course_price", "currency"], as_dict=True
|
||||
)
|
||||
if not frappe.db.exists(doctype, docname):
|
||||
print(doctype, docname)
|
||||
raise ValueError(_("Module Name is incorrect or does not exist."))
|
||||
|
||||
if doctype == "LMS Course":
|
||||
membership = frappe.db.exists(
|
||||
"LMS Batch Membership", {"member": frappe.session.user, "course": docname}
|
||||
)
|
||||
if membership:
|
||||
raise frappe.PermissionError(_("You are already enrolled for this course"))
|
||||
|
||||
else:
|
||||
membership = frappe.db.exists(
|
||||
"Class Student", {"student": frappe.session.user, "parent": docname}
|
||||
)
|
||||
if membership:
|
||||
raise frappe.PermissionError(_("You are already enrolled for this class"))
|
||||
|
||||
if doctype == "LMS Course":
|
||||
course = frappe.db.get_value(
|
||||
"LMS Course",
|
||||
docname,
|
||||
["title", "name", "paid_course", "course_price", "currency"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
if not course.paid_course:
|
||||
raise frappe.PermissionError(_("This course is free."))
|
||||
|
||||
context.title = course.title
|
||||
context.amount = course.course_price
|
||||
context.currency = course.currency
|
||||
|
||||
else:
|
||||
class_info = frappe.db.get_value(
|
||||
"LMS Class",
|
||||
docname,
|
||||
["title", "name", "paid_class", "amount", "currency"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
if not class_info.paid_class:
|
||||
raise frappe.PermissionError(
|
||||
_("To join this class, please contact the Administrator.")
|
||||
)
|
||||
|
||||
context.title = class_info.title
|
||||
context.amount = class_info.amount
|
||||
context.currency = class_info.currency
|
||||
|
||||
@@ -85,12 +85,6 @@
|
||||
{% macro ClassSections(class_info, class_courses, class_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">
|
||||
|
||||
{% if is_student %}
|
||||
@@ -610,17 +604,16 @@
|
||||
<script>
|
||||
frappe.boot.user = {
|
||||
"can_create": [],
|
||||
"can_select": ["User", "LMS Category", "LMS Assignment", "LMS Quiz"],
|
||||
"can_read": ["User", "LMS Category", "LMS Assignment", "LMS Quiz"]
|
||||
"can_select": ["User", "LMS Assignment", "LMS Quiz"],
|
||||
"can_read": ["User", "LMS Assignment", "LMS Quiz"]
|
||||
};
|
||||
|
||||
frappe.boot.single_types = []
|
||||
|
||||
let class_info = {{ class_info | json }};
|
||||
</script>
|
||||
{% else %}
|
||||
<script>
|
||||
frappe.boot.user = {
|
||||
frappe.boot.user= {
|
||||
"can_create": [],
|
||||
"can_select": ["LMS Course"],
|
||||
"can_read": ["LMS Course"]
|
||||
@@ -629,5 +622,4 @@
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
{{ include_script('controls.bundle.js') }}
|
||||
{% endblock %}
|
||||
@@ -1,6 +1,11 @@
|
||||
frappe.ready(() => {
|
||||
let self = this;
|
||||
|
||||
if ($("#live-class-form").length) {
|
||||
frappe.require("controls.bundle.js", () => {
|
||||
make_live_class_form();
|
||||
});
|
||||
}
|
||||
$(".btn-add-student").click((e) => {
|
||||
show_student_modal(e);
|
||||
});
|
||||
@@ -9,10 +14,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");
|
||||
@@ -25,7 +26,6 @@ frappe.ready(() => {
|
||||
$(".btn-add-course").click((e) => {
|
||||
show_course_modal(e);
|
||||
});
|
||||
|
||||
$(".btn-remove-course").click((e) => {
|
||||
remove_course(e);
|
||||
});
|
||||
@@ -309,6 +309,12 @@ const show_course_modal = () => {
|
||||
fieldname: "course",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldtype: "Link",
|
||||
options: "Course Evaluator",
|
||||
label: __("Course Evaluator"),
|
||||
fieldname: "evaluator",
|
||||
},
|
||||
],
|
||||
primary_action_label: __("Add"),
|
||||
primary_action(values) {
|
||||
|
||||
@@ -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,
|
||||
@@ -36,6 +36,10 @@ def get_context(context):
|
||||
"start_time",
|
||||
"end_time",
|
||||
"category",
|
||||
"paid_class",
|
||||
"amount",
|
||||
"currency",
|
||||
"prerequisite",
|
||||
],
|
||||
as_dict=True,
|
||||
)
|
||||
@@ -67,7 +71,7 @@ def get_context(context):
|
||||
context.class_students = get_class_student_details(
|
||||
class_students, class_courses, context.assessments
|
||||
)
|
||||
context.is_student = is_student(class_students)
|
||||
context.is_student = is_student(class_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."))
|
||||
@@ -205,11 +209,6 @@ def sort_students(class_students):
|
||||
return class_students
|
||||
|
||||
|
||||
def is_student(class_students):
|
||||
students = [student.student for student in class_students]
|
||||
return frappe.session.user in students
|
||||
|
||||
|
||||
def get_scheduled_flow(class_name):
|
||||
chapters = []
|
||||
|
||||
|
||||
214
lms/www/classes/class_details.html
Normal file
214
lms/www/classes/class_details.html
Normal file
@@ -0,0 +1,214 @@
|
||||
{% extends "lms/templates/lms_base.html" %}
|
||||
{% block title %}
|
||||
{{ _(class_info.title) }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block page_content %}
|
||||
<div class="common-page-style lms-page-style">
|
||||
{{ ClassHeader(class_info) }}
|
||||
<div class="container">
|
||||
{{ CourseHeaderOverlay(class_info) }}
|
||||
<div class="pt-10">
|
||||
{{ Prerequisites(class_info) }}
|
||||
{{ CourseList(class_info) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% macro ClassHeader(class_info) %}
|
||||
<div class="course-head-container">
|
||||
<div class="container">
|
||||
<div class="course-card-wide">
|
||||
{{ BreadCrumb(class_info) }}
|
||||
{{ ClassHeaderDetails(class_info) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro BreadCrumb(class_info) %}
|
||||
<article class="mb-8">
|
||||
<a class="dark-links" href="/classes">
|
||||
{{ _("All Classes") }}
|
||||
</a>
|
||||
<img class="" src="/assets/lms/icons/chevron-right.svg">
|
||||
<span class="breadcrumb-destination">
|
||||
{{ _("Class Details") }}
|
||||
</span>
|
||||
</article>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro ClassHeaderDetails(class_info) %}
|
||||
<div class="class-details" data-class="{{ class_info.name }}">
|
||||
<div class="flex align-center">
|
||||
<span>
|
||||
{{ class_info.courses | length }} {{ _("Courses") }}
|
||||
</span>
|
||||
<span class="px-2"> · </span>
|
||||
<span>
|
||||
{{ class_info.students | length }} {{ _("Students") }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="page-title">
|
||||
{{ class_info.title }}
|
||||
</div>
|
||||
|
||||
<div class="">
|
||||
{{ class_info.description }}
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-calendar"></use>
|
||||
</svg>
|
||||
<span>
|
||||
{{ frappe.utils.format_date(class_info.start_date, "long") }} -
|
||||
</span>
|
||||
<span>
|
||||
{{ frappe.utils.format_date(class_info.end_date, "long") }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{% if class_info.start_time and class_info.end_time %}
|
||||
<div class="mt-1">
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-clock"></use>
|
||||
</svg>
|
||||
<span>
|
||||
{{ frappe.utils.format_time(class_info.start_time, "hh:mm a") }} -
|
||||
</span>
|
||||
<span>
|
||||
{{ frappe.utils.format_time(class_info.end_time, "hh:mm a") }}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro CourseHeaderOverlay(class_info) %}
|
||||
<div class="course-overlay-card class-overlay">
|
||||
|
||||
<div class="course-overlay-content">
|
||||
{% if class_info.paid_class %}
|
||||
<div class="bold-heading">
|
||||
{{ frappe.utils.fmt_money(class_info.amount, 0, class_info.currency) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="vertically-center mt-2">
|
||||
<svg class="icon icon-md mr-1">
|
||||
<use href="#icon-education"></use>
|
||||
</svg>
|
||||
{{ class_info.courses | length }} {{ _("Courses") }}
|
||||
</div>
|
||||
|
||||
<div class="vertically-center mt-2">
|
||||
<svg class="icon icon-md mr-1">
|
||||
<use class="" href="#icon-users">
|
||||
</svg>
|
||||
{{ class_info.students | length }} {{ _("Students") }}
|
||||
</div>
|
||||
|
||||
<div class="mt-2">
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-calendar"></use>
|
||||
</svg>
|
||||
<span>
|
||||
{{ frappe.utils.format_date(class_info.start_date, "long") }} -
|
||||
</span>
|
||||
<span>
|
||||
{{ frappe.utils.format_date(class_info.end_date, "long") }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{% if class_info.start_time and class_info.end_time %}
|
||||
<div class="mt-2">
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-clock"></use>
|
||||
</svg>
|
||||
<span>
|
||||
{{ frappe.utils.format_time(class_info.start_time, "hh:mm a") }} -
|
||||
</span>
|
||||
<span>
|
||||
{{ frappe.utils.format_time(class_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="/classes/{{ class_info.name }}">
|
||||
{{ _("Checkout Class") }}
|
||||
</a>
|
||||
{% elif class_info.paid_class %}
|
||||
<a class="btn btn-primary wide-button" href="/billing/class/{{ class_info.name }}">
|
||||
{{ _("Register Now") }}
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
{{ _("To join this class, please contact the Administrator.") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if is_moderator %}
|
||||
<div class="mt-2">
|
||||
<div class="btn btn-secondary wide-button" id="create-class">
|
||||
{{ _("Edit Class") }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro Prerequisites(class_info) %}
|
||||
<div class="course-description-section w-50">
|
||||
<div class="page-title">
|
||||
{{ _("Prerequisite Knowledge") }}
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
{{ class_info.prerequisite }}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro CourseList(class_info) %}
|
||||
<div>
|
||||
<div class="page-title">
|
||||
{{ _("Courses") }}
|
||||
</div>
|
||||
{% if class_info.courses | length %}
|
||||
<div class="cards-parent mt-2">
|
||||
{% for course in class_info.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 %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="">
|
||||
{{ _("No courses") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{%- block script %}
|
||||
{{ super() }}
|
||||
{% if is_moderator %}
|
||||
<script>
|
||||
let class_info = {{ class_info | json }};
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
3
lms/www/classes/class_details.js
Normal file
3
lms/www/classes/class_details.js
Normal file
@@ -0,0 +1,3 @@
|
||||
frappe.ready(() => {
|
||||
frappe.require("controls.bundle.js");
|
||||
});
|
||||
21
lms/www/classes/class_details.py
Normal file
21
lms/www/classes/class_details.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import frappe
|
||||
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
|
||||
class_name = frappe.form_dict["classname"]
|
||||
|
||||
context.class_info = frappe.get_doc("LMS Class", class_name)
|
||||
|
||||
for course in context.class_info.courses:
|
||||
course.update(
|
||||
frappe.db.get_value(
|
||||
"LMS Course", course.course, ["name", "short_introduction", "image"], as_dict=1
|
||||
)
|
||||
)
|
||||
|
||||
context.is_moderator = has_course_moderator_role()
|
||||
context.is_evaluator = has_course_evaluator_role()
|
||||
context.is_student = is_student(class_name)
|
||||
@@ -43,7 +43,7 @@
|
||||
{% if is_moderator %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#past">
|
||||
{{ _("Past Classes") }}
|
||||
{{ _("Archived") }}
|
||||
<span class="course-list-count">
|
||||
{{ past_classes | length }}
|
||||
</span>
|
||||
@@ -54,7 +54,7 @@
|
||||
{% if frappe.session.user != "Guest" %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#my-class">
|
||||
{{ _("My Classes") }}
|
||||
{{ _("Enrolled") }}
|
||||
<span class="course-list-count">
|
||||
{{ my_classes | length }}
|
||||
</span>
|
||||
@@ -68,18 +68,18 @@
|
||||
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="upcoming" role="tabpanel" aria-labelledby="upcoming">
|
||||
{{ ClassCards(upcoming_classes) }}
|
||||
{{ ClassCards(upcoming_classes, show_price=True) }}
|
||||
</div>
|
||||
|
||||
{% if is_moderator %}
|
||||
<div class="tab-pane" id="past" role="tabpanel" aria-labelledby="past">
|
||||
{{ ClassCards(past_classes) }}
|
||||
{{ ClassCards(past_classes, show_price=False) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if frappe.session.user != "Guest" %}
|
||||
<div class="tab-pane" id="my-class" role="tabpanel" aria-labelledby="my-classes">
|
||||
{{ ClassCards(my_classes) }}
|
||||
{{ ClassCards(my_classes, show_price=False) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
</article>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro ClassCards(classes) %}
|
||||
{% macro ClassCards(classes, show_price=False) %}
|
||||
<div class="lms-card-parent">
|
||||
{% for class in classes %}
|
||||
{% set course_count = frappe.db.count("Class Course", {"parent": class.name}) %}
|
||||
@@ -105,6 +105,12 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if show_price and class.paid_class %}
|
||||
<div class="bold-heading">
|
||||
{{ frappe.utils.fmt_money(class.amount, 0, class.currency) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-auto mb-1">
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-calendar"></use>
|
||||
@@ -131,7 +137,11 @@
|
||||
{{ student_count }} {{ _("Students") }}
|
||||
</div>
|
||||
|
||||
<a class="stretched-link" href="/classes/{{ class.name }}"></a>
|
||||
{% if is_student(class.name) %}
|
||||
<a class="stretched-link" href="/classes/{{ class.name }}"></a>
|
||||
{% else %}
|
||||
<a class="stretched-link" href="/classes/details/{{ class.name }}"></a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import frappe
|
||||
from frappe.utils import getdate
|
||||
from lms.lms.utils import has_course_moderator_role
|
||||
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()
|
||||
classes = frappe.get_all(
|
||||
"LMS Class",
|
||||
fields=[
|
||||
@@ -15,6 +16,8 @@ def get_context(context):
|
||||
"start_date",
|
||||
"end_date",
|
||||
"paid_class",
|
||||
"amount",
|
||||
"currency",
|
||||
"seat_count",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -128,3 +128,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(class_name, member=None):
|
||||
if not member:
|
||||
member = frappe.session.user
|
||||
|
||||
return frappe.db.exists(
|
||||
"Class Student",
|
||||
{
|
||||
"student": member,
|
||||
"parent": class_name,
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user