feat: class billing

This commit is contained in:
Jannat Patel
2023-08-22 18:34:42 +05:30
parent ac64e59c43
commit 04ed7f412f
22 changed files with 663 additions and 244 deletions

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -2302,3 +2302,6 @@ select {
padding-right: 1rem !important;
}
.class-overlay {
top: 30%;
}

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

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

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

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

View File

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

View File

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

View File

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

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

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

View File

@@ -0,0 +1,3 @@
frappe.ready(() => {
frappe.require("controls.bundle.js");
});

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

View File

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

View File

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

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

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