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

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