feat: paid courses

This commit is contained in:
Jannat Patel
2023-08-02 18:08:42 +05:30
parent a5bc30f776
commit 79d9f31db7
14 changed files with 138 additions and 187 deletions

View File

@@ -11,14 +11,6 @@ frappe.ui.form.on("LMS Course", {
}; };
}); });
frm.set_query("instructor", "instructors", function () {
return {
filters: {
ignore_user_type: 1,
},
};
});
frm.set_query("course", "related_courses", function () { frm.set_query("course", "related_courses", function () {
return { return {
filters: { filters: {
@@ -29,5 +21,12 @@ frappe.ui.form.on("LMS Course", {
}, },
refresh: (frm) => { refresh: (frm) => {
frm.add_web_link(`/courses/${frm.doc.name}`, "See on Website"); frm.add_web_link(`/courses/${frm.doc.name}`, "See on Website");
if (!frm.doc.currency)
frappe.db
.get_single_value("LMS Settings", "default_currency")
.then((value) => {
frm.set_value("currency", value);
});
}, },
}); });

View File

@@ -32,19 +32,18 @@
"description", "description",
"chapters", "chapters",
"related_courses", "related_courses",
"pricing_section",
"paid_course",
"currency",
"course_price",
"certification_section", "certification_section",
"enable_certification", "enable_certification",
"expiry", "expiry",
"section_break_23", "max_attempts",
"column_break_rxww",
"grant_certificate_after", "grant_certificate_after",
"evaluator", "evaluator",
"column_break_26", "duration"
"max_attempts",
"duration",
"pricing_section",
"paid_certificate",
"currency",
"price_certificate"
], ],
"fields": [ "fields": [
{ {
@@ -170,13 +169,6 @@
"fieldname": "column_break_12", "fieldname": "column_break_12",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{
"default": "0",
"depends_on": "enable_certification",
"fieldname": "paid_certificate",
"fieldtype": "Check",
"label": "Paid Certificate"
},
{ {
"depends_on": "enable_certification", "depends_on": "enable_certification",
"fieldname": "grant_certificate_after", "fieldname": "grant_certificate_after",
@@ -193,20 +185,12 @@
"options": "Course Evaluator" "options": "Course Evaluator"
}, },
{ {
"depends_on": "eval: doc.grant_certificate_after == \"Evaluation\"",
"fieldname": "pricing_section", "fieldname": "pricing_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Pricing" "label": "Pricing"
}, },
{ {
"depends_on": "paid_certificate", "depends_on": "paid_course",
"fieldname": "price_certificate",
"fieldtype": "Currency",
"label": "Certificate Price",
"mandatory_depends_on": "paid_certificate"
},
{
"depends_on": "paid_certificate",
"fieldname": "currency", "fieldname": "currency",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Currency", "label": "Currency",
@@ -228,11 +212,21 @@
"options": "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12" "options": "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12"
}, },
{ {
"fieldname": "section_break_23", "default": "0",
"fieldtype": "Section Break" "fieldname": "paid_course",
"fieldtype": "Check",
"label": "Paid Course"
}, },
{ {
"fieldname": "column_break_26", "depends_on": "paid_course",
"fieldname": "course_price",
"fieldtype": "Currency",
"label": "Course Price",
"option": "currency",
"mandatory_depends_on": "paid_certificate"
},
{
"fieldname": "column_break_rxww",
"fieldtype": "Column Break" "fieldtype": "Column Break"
} }
], ],
@@ -260,39 +254,12 @@
} }
], ],
"make_attachments_public": 1, "make_attachments_public": 1,
"modified": "2023-05-11 17:08:19.763405", "modified": "2023-08-02 12:07:26.354119",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Course", "name": "LMS Course",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [],
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"select": 1,
"share": 1,
"write": 1
},
{
"create": 1,
"email": 1,
"export": 1,
"if_owner": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"select": 1,
"share": 1,
"write": 1
}
],
"search_fields": "title", "search_fields": "title",
"show_title_field_in_link": 1, "show_title_field_in_link": 1,
"sort_field": "creation", "sort_field": "creation",

View File

@@ -6,16 +6,21 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"default_home", "default_home",
"send_calendar_invite_for_evaluations",
"allow_student_progress",
"column_break_zdel",
"is_onboarding_complete",
"force_profile_completion", "force_profile_completion",
"section_break_szgq", "is_onboarding_complete",
"search_placeholder", "column_break_zdel",
"portal_course_creation",
"column_break_2",
"livecode_url", "livecode_url",
"course_settings_section",
"search_placeholder",
"column_break_iqxy",
"portal_course_creation",
"section_break_szgq",
"send_calendar_invite_for_evaluations",
"column_break_2",
"allow_student_progress",
"payment_section",
"default_currency",
"column_break_cfcv",
"signup_settings_tab", "signup_settings_tab",
"signup_settings_section", "signup_settings_section",
"terms_of_use", "terms_of_use",
@@ -37,6 +42,7 @@
"default": "https://livecode.dev.fossunited.org", "default": "https://livecode.dev.fossunited.org",
"fieldname": "livecode_url", "fieldname": "livecode_url",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 1,
"label": "LiveCode URL" "label": "LiveCode URL"
}, },
{ {
@@ -163,7 +169,8 @@
}, },
{ {
"fieldname": "section_break_szgq", "fieldname": "section_break_szgq",
"fieldtype": "Section Break" "fieldtype": "Section Break",
"label": "Class Settings"
}, },
{ {
"fieldname": "signup_settings_tab", "fieldname": "signup_settings_tab",
@@ -180,12 +187,36 @@
"fieldname": "allow_student_progress", "fieldname": "allow_student_progress",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Allow students to see each others progress in class" "label": "Allow students to see each others progress in class"
},
{
"fieldname": "payment_section",
"fieldtype": "Section Break",
"label": "Payment"
},
{
"fieldname": "default_currency",
"fieldtype": "Link",
"label": "Default Currency",
"options": "Currency"
},
{
"fieldname": "column_break_cfcv",
"fieldtype": "Column Break"
},
{
"fieldname": "course_settings_section",
"fieldtype": "Section Break",
"label": "Course Settings"
},
{
"fieldname": "column_break_iqxy",
"fieldtype": "Column Break"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2023-04-17 12:54:44.706101", "modified": "2023-08-02 12:30:50.897156",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Settings", "name": "LMS Settings",

View File

@@ -59,19 +59,12 @@
{{ frappe.utils.flt(avg_rating, frappe.get_system_settings("float_precision") or 3) }} {{ frappe.utils.flt(avg_rating, frappe.get_system_settings("float_precision") or 3) }}
</div> </div>
{% endif %} {% endif %}
</div>
{% if course.paid_certificate %} <div class="course-card-title">
<div class="vertically-center"> {{ course.title }}
<svg class="icon icon-md">
<use href="#icon-badge"></use>
</svg>
<span class="certificate-price" data-price="{{ course.price_certificate }}">
{{ format_amount(course.price_certificate, course.currency) }}
</span>
</div> </div>
{% endif %}
</div>
<div class="course-card-title">{{ course.title }}</div>
<div class="short-introduction"> <div class="short-introduction">
{{ course.short_introduction }} {{ course.short_introduction }}
</div> </div>
@@ -87,7 +80,8 @@
{% endif %} {% endif %}
<div class="course-card-footer"> <div class="course-card-footer">
<span class="">
<div class="course-card-instructors">
{% set instructors = get_instructors(course.name) %} {% set instructors = get_instructors(course.name) %}
{% set ins_len = instructors | length %} {% set ins_len = instructors | length %}
{% for instructor in instructors %} {% for instructor in instructors %}
@@ -112,7 +106,15 @@
{% endif %} {% endif %}
</span> </span>
</a> </a>
</span> </div>
<div class="course-price">
{% if course.paid_course %}
{{ format_amount(course.course_price, course.currency) }}
{% else %}
{{ _("Free") }}
{% endif %}
</div>
</div> </div>
{% if read_only %} {% if read_only %}

View File

@@ -30,7 +30,7 @@
{{ _("Notify me when available") }} {{ _("Notify me when available") }}
</button> </button>
{% elif show_start_learing_cta(course, membership) %} {% elif show_start_learing_cta(course, membership) %}
<button class="btn btn-primary btn-sm join-batch pull-right" data-course="{{ course.name | urlencode}}"> <button class="btn btn-primary btn-sm enroll-in-course pull-right" data-course="{{ course.name | urlencode}}">
{{ _("Start Learning") }} {{ _("Start Learning") }}
</button> </button>
{% endif %} {% endif %}

View File

@@ -61,3 +61,4 @@ execute:frappe.permissions.reset_perms("LMS Class")
execute:frappe.permissions.reset_perms("Course Evaluator") execute:frappe.permissions.reset_perms("Course Evaluator")
execute:frappe.permissions.reset_perms("LMS Certificate Request") execute:frappe.permissions.reset_perms("LMS Certificate Request")
execute:frappe.permissions.reset_perms("LMS Certificate Evaluation") execute:frappe.permissions.reset_perms("LMS Certificate Evaluation")
lms.patches.v1_0.paid_certificate_to_paid_course

View File

@@ -0,0 +1,20 @@
import frappe
def execute():
courses = frappe.get_all(
"LMS Course",
{"paid_certificate": ["is", "set"]},
["name", "price_certificate", "currency"],
)
for course in courses:
frappe.db.set_value(
"LMS Course",
course.name,
{
"paid_course": 1,
"course_price": course.price_certificate,
"currency": course.currency,
},
)

View File

@@ -414,11 +414,13 @@ input[type=checkbox] {
} }
.course-card-footer { .course-card-footer {
display: flex;
justify-content: space-between;
margin-top: auto; margin-top: auto;
} }
.course-card-footer .avatar-group { .course-price {
display: inherit; font-weight: 700;
} }
.view-course-link { .view-course-link {
@@ -551,6 +553,11 @@ input[type=checkbox] {
} }
} }
.course-card-instructors {
display: flex;
align-items: center;
}
.course-card-wide-content { .course-card-wide-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -2,8 +2,8 @@ frappe.ready(() => {
setup_file_size(); setup_file_size();
pin_header(); pin_header();
$(".join-batch").click((e) => { $(".enroll-in-course").click((e) => {
join_course(e); enroll_in_course(e);
}); });
$(".notify-me").click((e) => { $(".notify-me").click((e) => {
@@ -68,7 +68,7 @@ const file_size = (value) => {
return value; return value;
}; };
const join_course = (e) => { const enroll_in_course = (e) => {
e.preventDefault(); e.preventDefault();
let course = $(e.currentTarget).attr("data-course"); let course = $(e.currentTarget).attr("data-course");
if (frappe.session.user == "Guest") { if (frappe.session.user == "Guest") {

View File

@@ -7,7 +7,7 @@
{{ _("Write a review") }} {{ _("Write a review") }}
</a> </a>
{% elif show_start_learing_cta(course, membership) %} {% elif show_start_learing_cta(course, membership) %}
<div class="btn btn-secondary btn-sm join-batch" data-course="{{ course.name | urlencode }}"> <div class="btn btn-secondary btn-sm enroll-in-course" data-course="{{ course.name | urlencode }}">
{{ _("Start Learning") }} {{ _("Start Learning") }}
</div> </div>
{% endif %} {% endif %}

View File

@@ -135,7 +135,7 @@
{{ render_html(lesson) }} {{ render_html(lesson) }}
{% else %} {% else %}
{% set course_link = "<a class='join-batch' data-course=" + course.name | urlencode + " href=''>" + _('here') + "</a>" %} {% set course_link = "<a class='enroll-in-course' data-course=" + course.name | urlencode + " href=''>" + _('here') + "</a>" %}
<div class="alert alert-info medium mb-0"> <div class="alert alert-info medium mb-0">
{{ _("There is no preview available for this lesson. {{ _("There is no preview available for this lesson.
Please join the course to access it. Please join the course to access it.

View File

@@ -136,18 +136,8 @@
{{ get_lessons(course.name, None, False) }} {{ _("Lessons") }} {{ get_lessons(course.name, None, False) }} {{ _("Lessons") }}
</div> </div>
{% if course.enable_certification %}
<div class="vertically-center mb-3">
<svg class="icon icon-md mr-1">
<use href="#icon-badge"></use>
</svg>
{{ _("Get Certified") }}
</div>
{% endif %}
</div> </div>
</div> </div>
{{ SlotModal(course) }}
{% endmacro %} {% endmacro %}
@@ -231,8 +221,13 @@
{{ _("Continue Learning") }} {{ _("Continue Learning") }}
</a> </a>
{% elif course.paid_course %}
<a class="btn btn-primary wide-button" id="buy-course">
{{ _("Buy This Course") }}
</a>
{% elif show_start_learing_cta(course, membership) %} {% elif show_start_learing_cta(course, membership) %}
<div class="btn btn-primary wide-button join-batch" data-course="{{ course.name | urlencode }}"> <div class="btn btn-primary wide-button enroll-in-course" data-course="{{ course.name | urlencode }}">
{{ _("Start Learning") }} {{ _("Start Learning") }}
</div> </div>
{% endif %} {% endif %}
@@ -245,11 +240,6 @@
{{ _("Get Certificate") }} {{ _("Get Certificate") }}
</a> </a>
{% elif eligible_for_evaluation %}
<a class="btn btn-secondary wide-button mt-2" id="apply-certificate" data-course="{{ course.name }}">
{{ _("Apply for Certificate") }}
</a>
{% elif course.grant_certificate_after == "Completion" and progress == 100 %} {% elif course.grant_certificate_after == "Completion" and progress == 100 %}
<div class="btn btn-secondary wide-button mt-2" id="certification" data-course="{{ course.name }}"> <div class="btn btn-secondary wide-button mt-2" id="certification" data-course="{{ course.name }}">
{{ _("Get Certificate") }} {{ _("Get Certificate") }}
@@ -279,16 +269,6 @@
{{ _("You have opted to be notified for this course. You will receive an email when the course becomes available.") }} {{ _("You have opted to be notified for this course. You will receive an email when the course becomes available.") }}
</div> </div>
{% if certificate_request and not certificate %}
<p class="mb-2">
<b>
{{ _("Evaluation On: ") }}
</b>
{{ _("{0} at {1}").format(frappe.utils.format_date(certificate_request.date, "medium"),
frappe.utils.format_time(certificate_request.start_time, "short")) }}
</p>
{% endif %}
{% if course.status == "Under Review" and is_instructor(course.name) %} {% if course.status == "Under Review" and is_instructor(course.name) %}
<div class="mb-4"> <div class="mb-4">
{{ _("This course is currently under review. Once the review is complete, the System Admins will publish it on the website.") }} {{ _("This course is currently under review. Once the review is complete, the System Admins will publish it on the website.") }}
@@ -301,54 +281,3 @@
</p> </p>
{% endif %} {% endif %}
{% endmacro %} {% endmacro %}
<!-- Modal for Slots -->
{% macro SlotModal(course) %}
<div class="modal fade" id="slot-modal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<div class="modal-title">{{ _("Pick a Slot") }}</div>
</div>
<div class="modal-body">
<form id="slot-form">
<p class="">{{ _("This course requires you to complete an evaluation to get certified. Please pick a slot based on your convenience for the evaluations. ") }}</p>
<div class="form-group">
<div class="clearfix">
<label class="control-label reqd" style="padding-right: 0px;">{{ _("Date") }}</label>
</div>
<div class="control-input-wrapper">
<div class="control-input">
<input type="date" class="input-with-feedback form-control bold" data-fieldtype="Date" data-course="{{ course.name | urlencode }}"
id="slot-date" min="{{ frappe.utils.format_date(frappe.utils.add_days(frappe.utils.getdate(), 1), 'yyyy-mm-dd') }}">
</div>
</div>
</div>
<div class="form-group">
<div class="clearfix">
<label class="control-label reqd slot-label hide" style="padding-right: 0px;">{{ _("Slots") }}</label>
</div>
<div class="control-input-wrapper">
<div class="control-input">
<div class="slots"></div>
</div>
</div>
</div>
<p id="no-slots-message" class="small text-danger hide"> {{ _("There are no slots available on this day.") }} </p>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-secondary btn-sm pull-right mr-2 close-slot-modal" data-dismiss="modal" aria-label="Close">
{{ _("Discard") }}
</button>
<button class="btn btn-primary btn-sm pull-right" data-course="{{ course.name | urlencode}}" id="submit-slot">
{{ _("Submit") }}
</button>
</div>
</div>
</div>
</div>
{% endmacro %}

View File

@@ -52,13 +52,10 @@ def set_course_context(context, course_name):
"disable_self_learning", "disable_self_learning",
"status", "status",
"video_link", "video_link",
"enable_certification", "paid_course",
"grant_certificate_after", "course_price",
"paid_certificate",
"price_certificate",
"currency", "currency",
"max_attempts", "grant_certificate_after",
"duration",
], ],
as_dict=True, as_dict=True,
) )
@@ -79,7 +76,7 @@ def set_course_context(context, course_name):
frappe.db.get_value( frappe.db.get_value(
"LMS Course", "LMS Course",
csr.course, csr.course,
["name", "upcoming", "title", "image", "enable_certification"], ["name", "upcoming", "title", "image"],
as_dict=True, as_dict=True,
) )
) )
@@ -94,7 +91,6 @@ def set_course_context(context, course_name):
context.certificate = is_certified(course.name) context.certificate = is_certified(course.name)
eval_details = get_evaluation_details(course.name) eval_details = get_evaluation_details(course.name)
context.eligible_for_evaluation = eval_details.eligible context.eligible_for_evaluation = eval_details.eligible
context.certificate_request = eval_details.request
context.no_of_attempts = eval_details.no_of_attempts context.no_of_attempts = eval_details.no_of_attempts
if context.course.upcoming: if context.course.upcoming:
context.is_user_interested = get_user_interest(context.course.name) context.is_user_interested = get_user_interest(context.course.name)

View File

@@ -45,9 +45,8 @@ def get_courses():
"title", "title",
"short_introduction", "short_introduction",
"image", "image",
"enable_certification", "paid_course",
"paid_certificate", "course_price",
"price_certificate",
"currency", "currency",
], ],
) )