feat: paid certifications on courses

This commit is contained in:
Jannat Patel
2025-02-21 19:12:20 +05:30
parent 4e6c1478f9
commit bacfaf4a71
17 changed files with 243 additions and 220 deletions
+20 -7
View File
@@ -190,24 +190,24 @@ def get_translations():
@frappe.whitelist()
def validate_billing_access(type, name):
def validate_billing_access(billing_type, name):
access = True
message = ""
doctype = "LMS Course" if type == "course" else "LMS Batch"
doctype = "LMS Batch" if billing_type == "batch" else "LMS Course"
if frappe.session.user == "Guest":
access = False
message = _("Please login to continue with payment.")
if type not in ["course", "batch"]:
if access and billing_type not in ["course", "batch", "certificate"]:
access = False
message = _("Module is incorrect.")
if not frappe.db.exists(doctype, name):
if access and not frappe.db.exists(doctype, name):
access = False
message = _("Module Name is incorrect or does not exist.")
if type == "course":
if access and billing_type == "course":
membership = frappe.db.exists(
"LMS Enrollment", {"member": frappe.session.user, "course": name}
)
@@ -215,7 +215,7 @@ def validate_billing_access(type, name):
access = False
message = _("You are already enrolled for this course.")
else:
elif access and billing_type == "batch":
membership = frappe.db.exists(
"LMS Batch Enrollment", {"member": frappe.session.user, "batch": name}
)
@@ -223,6 +223,19 @@ def validate_billing_access(type, name):
access = False
message = _("You are already enrolled for this batch.")
elif access and billing_type == "certificate":
purchased_certificate = frappe.db.exists(
"LMS Enrollment",
{
"course": name,
"member": frappe.session.user,
"purchased_certificate": 1,
},
)
if purchased_certificate:
access = False
message = _("You have already purchased the certificate for this course.")
address = frappe.db.get_value(
"Address",
{"email_id": frappe.session.user},
@@ -370,7 +383,7 @@ def get_evaluator_details(evaluator):
@frappe.whitelist(allow_guest=True)
def get_certified_participants(filters=None, start=0, page_length=30, search=None):
def get_certified_participants(filters=None, start=0, page_length=30):
or_filters = {}
if not filters:
filters = {}
-12
View File
@@ -10,7 +10,6 @@ from datetime import timedelta
from frappe.model.document import Document
from frappe.utils import cint, format_datetime, get_time, add_days, nowdate
from lms.lms.utils import (
get_lessons,
get_lesson_index,
get_lesson_url,
get_quiz_details,
@@ -258,17 +257,6 @@ def create_batch(
return doc
@frappe.whitelist()
def fetch_lessons(courses):
lessons = []
courses = json.loads(courses)
for course in courses:
lessons.extend(get_lessons(course.get("course")))
return lessons
@frappe.whitelist()
def add_course(course, parent, name=None, evaluator=None):
frappe.only_for("Moderator")
+12 -32
View File
@@ -40,15 +40,12 @@
"pricing_tab",
"pricing_section",
"paid_course",
"enable_certification",
"paid_certificate",
"column_break_acoj",
"course_price",
"currency",
"amount_usd",
"certification_tab",
"certification_section",
"enable_certification",
"column_break_rxww",
"expiry",
"tab_4_tab",
"statistics_section",
"enrollments",
@@ -134,22 +131,11 @@
"fieldtype": "Section Break",
"label": "Course Settings"
},
{
"fieldname": "certification_section",
"fieldtype": "Section Break"
},
{
"default": "0",
"fieldname": "enable_certification",
"fieldtype": "Check",
"label": "Enable Certification"
},
{
"default": "0",
"depends_on": "enable_certification",
"fieldname": "expiry",
"fieldtype": "Int",
"label": "Certification Expires After (Years)"
"label": "Completion Certificate"
},
{
"fieldname": "related_courses",
@@ -181,7 +167,6 @@
"fieldtype": "Section Break"
},
{
"depends_on": "paid_course",
"fieldname": "currency",
"fieldtype": "Link",
"label": "Currency",
@@ -195,22 +180,16 @@
"label": "Paid Course"
},
{
"depends_on": "paid_course",
"fieldname": "course_price",
"fieldtype": "Currency",
"label": "Course Price",
"label": "Amount",
"mandatory_depends_on": "paid_course"
},
{
"fieldname": "column_break_rxww",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_acoj",
"fieldtype": "Column Break"
},
{
"depends_on": "paid_course",
"description": "If you set an amount here, then the USD equivalent setting will not get applied.",
"fieldname": "amount_usd",
"fieldtype": "Currency",
@@ -238,12 +217,7 @@
{
"fieldname": "pricing_tab",
"fieldtype": "Tab Break",
"label": "Pricing"
},
{
"fieldname": "certification_tab",
"fieldtype": "Tab Break",
"label": "Certification"
"label": "Pricing and Certification"
},
{
"fieldname": "column_break_htgn",
@@ -284,6 +258,12 @@
"fieldtype": "Data",
"label": "Rating",
"read_only": 1
},
{
"default": "0",
"fieldname": "paid_certificate",
"fieldtype": "Check",
"label": "Paid Certificate"
}
],
"is_published_field": "published",
@@ -310,7 +290,7 @@
}
],
"make_attachments_public": 1,
"modified": "2024-10-30 23:08:31.842860",
"modified": "2025-02-20 16:44:38.891383",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Course",
+6 -4
View File
@@ -5,9 +5,8 @@ import json
import random
import frappe
from frappe.model.document import Document
from frappe.utils import cint, today
from frappe.utils.telemetry import capture
from lms.lms.utils import get_chapters, can_create_courses
from frappe.utils import today, cint
from lms.lms.utils import get_chapters
from ...utils import generate_slug, validate_image, update_payment_record
from frappe import _
@@ -53,9 +52,12 @@ class LMSCourse(Document):
frappe.throw(_("Please install the Payments app to create a paid courses."))
def validate_amount_and_currency(self):
if self.paid_course and (not self.course_price and not self.currency):
if self.paid_course and (cint(self.course_price) < 0 or not self.currency):
frappe.throw(_("Amount and currency are required for paid courses."))
if self.paid_certificate and (cint(self.course_price) <= 0 or not self.currency):
frappe.throw(_("Amount and currency are required for paid certificates."))
def on_update(self):
if not self.upcoming and self.has_value_changed("upcoming"):
self.send_email_to_interested_users()
@@ -14,6 +14,9 @@
"member",
"member_name",
"member_username",
"certification_section",
"purchased_certificate",
"certificate",
"section_break_8",
"cohort",
"subgroup",
@@ -123,11 +126,28 @@
"fieldtype": "Link",
"label": "Payment",
"options": "LMS Payment"
},
{
"fieldname": "certification_section",
"fieldtype": "Section Break",
"label": "Certification"
},
{
"default": "0",
"fieldname": "purchased_certificate",
"fieldtype": "Check",
"label": "Purchased Certificate"
},
{
"fieldname": "certificate",
"fieldtype": "Link",
"label": "Certificate",
"options": "LMS Certificate"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-10-30 12:44:16.103598",
"modified": "2025-02-21 17:11:37.986157",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Enrollment",
+8 -1
View File
@@ -14,6 +14,7 @@
"payment_for_document_type",
"payment_for_document",
"payment_received",
"payment_for_certificate",
"payment_details_section",
"currency",
"amount",
@@ -136,6 +137,12 @@
"fieldtype": "Link",
"label": "Source",
"options": "LMS Source"
},
{
"default": "0",
"fieldname": "payment_for_certificate",
"fieldtype": "Check",
"label": "Payment for Certificate"
}
],
"index_web_pages_for_search": 1,
@@ -149,7 +156,7 @@
"link_fieldname": "payment"
}
],
"modified": "2025-02-18 15:54:25.383353",
"modified": "2025-02-21 18:29:55.436611",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Payment",
+24 -8
View File
@@ -18,19 +18,26 @@ def validate_currency(payment_gateway, currency):
@frappe.whitelist()
def get_payment_link(doctype, docname, title, amount, total_amount, currency, address):
def get_payment_link(
doctype,
docname,
title,
amount,
total_amount,
currency,
address,
redirect_to,
payment_for_certificate,
):
payment_gateway = get_payment_gateway()
address = frappe._dict(address)
amount_with_gst = total_amount if total_amount != amount else 0
payment = record_payment(address, doctype, docname, amount, currency, amount_with_gst)
payment = record_payment(
address, doctype, docname, amount, currency, amount_with_gst, payment_for_certificate
)
controller = get_controller(payment_gateway)
if doctype == "LMS Course":
redirect_to = f"/lms/courses/{docname}/learn/1-1"
elif doctype == "LMS Batch":
redirect_to = f"/lms/batches/{docname}"
payment_details = {
"amount": total_amount,
"title": f"Payment for {doctype} {title} {docname}",
@@ -53,7 +60,15 @@ def get_payment_link(doctype, docname, title, amount, total_amount, currency, ad
return url
def record_payment(address, doctype, docname, amount, currency, amount_with_gst=0):
def record_payment(
address,
doctype,
docname,
amount,
currency,
amount_with_gst=0,
payment_for_certificate=0,
):
address = frappe._dict(address)
address_name = save_address(address)
@@ -71,6 +86,7 @@ def record_payment(address, doctype, docname, amount, currency, amount_with_gst=
"source": address.source,
"payment_for_document_type": doctype,
"payment_for_document": docname,
"payment_for_certificate": payment_for_certificate,
}
)
payment_doc.save(ignore_permissions=True)
+48 -18
View File
@@ -68,27 +68,26 @@ def generate_slug(title, doctype):
return slugify(title, used_slugs=slugs)
def get_membership(course, member=None, batch=None):
def get_membership(course, member=None):
if not member:
member = frappe.session.user
filters = {"member": member, "course": course}
if batch:
filters["batch_old"] = batch
is_member = frappe.db.exists("LMS Enrollment", filters)
if is_member:
if frappe.db.exists("LMS Enrollment", filters):
membership = frappe.db.get_value(
"LMS Enrollment",
filters,
["name", "batch_old", "current_lesson", "member_type", "progress", "member"],
[
"name",
"current_lesson",
"progress",
"member",
"purchased_certificate",
"certificate",
],
as_dict=True,
)
if membership and membership.batch_old:
membership.batch_title = frappe.db.get_value(
"LMS Batch Old", membership.batch_old, "title"
)
return membership
return False
@@ -1009,6 +1008,7 @@ def get_course_details(course):
"category",
"status",
"paid_course",
"paid_certificate",
"course_price",
"currency",
"amount_usd",
@@ -1023,7 +1023,7 @@ def get_course_details(course):
course_details.instructors = get_instructors(course_details.name)
# course_details.is_instructor = is_instructor(course_details.name)
if course_details.paid_course:
if course_details.paid_course or course_details.paid_certificate:
"""course_details.course_price, course_details.currency = check_multicurrency(
course_details.course_price, course_details.currency, None, course_details.amount_usd
)"""
@@ -1136,14 +1136,21 @@ def get_lesson(course, chapter, lesson):
return {}
membership = get_membership(course)
course_title = frappe.db.get_value("LMS Course", course, "title")
course_info = frappe.db.get_value(
"LMS Course", course, ["title", "paid_certificate"], as_dict=1
)
if (
not lesson_details.include_in_preview
and not membership
and not has_course_moderator_role()
and not is_instructor(course)
):
return {"no_preview": 1, "title": lesson_details.title, "course_title": course_title}
return {
"no_preview": 1,
"title": lesson_details.title,
"course_title": course_info.title,
}
lesson_details = frappe.db.get_value(
"Course Lesson",
@@ -1178,7 +1185,8 @@ def get_lesson(course, chapter, lesson):
lesson_details.prev = neighbours["prev"]
lesson_details.membership = membership
lesson_details.instructors = get_instructors(course)
lesson_details.course_title = course_title
lesson_details.course_title = course_info.title
lesson_details.paid_certificate = course_info.paid_certificate
return lesson_details
@@ -1612,11 +1620,19 @@ def get_order_summary(doctype, docname, country=None):
details = frappe.db.get_value(
"LMS Course",
docname,
["title", "name", "paid_course", "course_price as amount", "currency", "amount_usd"],
[
"title",
"name",
"paid_course",
"paid_certificate",
"course_price as amount",
"currency",
"amount_usd",
],
as_dict=True,
)
if not details.paid_course:
if not details.paid_course and not details.paid_certificate:
raise frappe.throw(_("This course is free."))
else:
@@ -1730,9 +1746,14 @@ def update_payment_record(doctype, docname):
"order_id": data.get("order_id"),
},
)
payment_for_certificate = frappe.db.get_value(
"LMS Payment", data.payment, "payment_for_certificate"
)
try:
if doctype == "LMS Course":
if payment_for_certificate:
update_certificate_purchase(docname)
elif doctype == "LMS Course":
enroll_in_course(data.payment, docname)
else:
enroll_in_batch(docname, data.payment)
@@ -1792,6 +1813,15 @@ def enroll_in_batch(batch, payment_name=None):
new_student.save()
def update_certificate_purchase(course):
frappe.db.set_value(
"LMS Enrollment",
{"member": frappe.session.user, "course": course},
"purchased_certificate",
1,
)
@frappe.whitelist()
def get_programs():
if (
-110
View File
@@ -1,110 +0,0 @@
{% set chapters = get_chapters(course.name) %}
{% set is_instructor = is_instructor(course.name) %}
{% if chapters | length %}
<div class="course-home-outline">
{% if not lesson_page %}
<div class="page-title mb-8" id="outline-heading" data-course="{{ course.name }}">
{{ _("Course Content") }}
</div>
<!-- <div class="mb-2">
<span>
{{ chapters | length }} chapters
</span>
<span>
. {{ get_lessons(course.name, None, False) }} lessons
</span>
</div> -->
{% endif %}
{% if chapters | length %}
<div>
{% for chapter in chapters %}
{% set lessons = get_lessons(course.name, chapter) %}
<div class="chapter-parent" data-chapter="{{ chapter.name }}">
<div class="chapter-title" data-toggle="collapse" aria-expanded="false"
data-target="#{{ get_slugified_chapter_title(chapter.title) }}">
<img class="chapter-icon" src="/assets/lms/icons/chevron-right.svg">
<div class="chapter-title-main">
{{ chapter.title }}
</div>
<!-- <div class="small ml-auto">
{{ lessons | length }} lessons
</div> -->
</div>
<div class="chapter-content collapse navbar-collapse" id="{{ get_slugified_chapter_title(chapter.title) }}">
{% if chapter.description %}
<div class="chapter-description">
{{ chapter.description }}
</div>
{% endif %}
<div class="lessons">
{% if lessons | length %}
{% for lesson in lessons %}
{% set active = membership.current_lesson == lesson.name %}
<div data-lesson="{{ lesson.name }}" class="lesson-info {% if active %} active-lesson {% endif %}">
{% if membership or lesson.include_in_preview or is_instructor or has_course_moderator_role() %}
<a class="lesson-links"
href="{{ get_lesson_url(course.name, lesson.number) }}{% if classname %}?class={{ classname }}{% endif %}{{course.query_parameter}}"
{% if is_instructor and not lesson.include_in_preview %}
title="{{ _('This lesson is not available for preview. As you are the Instructor of the course only you can see it.') }}"
{% endif %}>
<svg class="icon icon-sm mr-2">
<use class="" href="#{{ lesson.icon }}">
</svg>
<span>{{ lesson.title }}</span>
{% if membership %}
<svg class="icon icon-md lesson-progress-tick ml-auto {{ get_progress(course.name, lesson.name) != 'Complete' and 'hide' }}">
<use class="" href="#icon-success">
</svg>
{% endif %}
</a>
{% else %}
<div class="no-preview" title="This lesson is not available for preview">
<div class="lesson-links">
<svg class="icon icon-sm mr-2">
<use class="" href="#icon-lock-gray">
</svg>
<div>{{ lesson.title }}</div>
</div>
</div>
{% endif %}
</div>
{% endfor %}
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
</div>
{% endif %}
{% if chapters | length %}
<!-- No Preview Modal -->
{{ widgets.NoPreviewModal(course=course, membership=membership) }}
{% endif %}