feat: paid course from course creation form
This commit is contained in:
@@ -5,7 +5,7 @@ import json
|
|||||||
import random
|
import random
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import cint
|
from frappe.utils import cint, validate_phone_number
|
||||||
from frappe.utils.telemetry import capture
|
from frappe.utils.telemetry import capture
|
||||||
from lms.lms.utils import get_chapters, can_create_courses
|
from lms.lms.utils import get_chapters, can_create_courses
|
||||||
from ...utils import generate_slug, validate_image
|
from ...utils import generate_slug, validate_image
|
||||||
@@ -213,6 +213,9 @@ def save_course(
|
|||||||
published,
|
published,
|
||||||
upcoming,
|
upcoming,
|
||||||
image=None,
|
image=None,
|
||||||
|
paid_course=False,
|
||||||
|
course_price=None,
|
||||||
|
currency=None,
|
||||||
):
|
):
|
||||||
if not can_create_courses():
|
if not can_create_courses():
|
||||||
return
|
return
|
||||||
@@ -232,6 +235,9 @@ def save_course(
|
|||||||
"tags": tags,
|
"tags": tags,
|
||||||
"published": cint(published),
|
"published": cint(published),
|
||||||
"upcoming": cint(upcoming),
|
"upcoming": cint(upcoming),
|
||||||
|
"paid_course": cint(paid_course),
|
||||||
|
"course_price": course_price,
|
||||||
|
"currency": currency,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
doc.save(ignore_permissions=True)
|
doc.save(ignore_permissions=True)
|
||||||
@@ -360,6 +366,7 @@ def reorder_chapter(chapter_array):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_payment_options(course, phone):
|
def get_payment_options(course, phone):
|
||||||
|
validate_phone_number(phone, True)
|
||||||
course_details = frappe.db.get_value(
|
course_details = frappe.db.get_value(
|
||||||
"LMS Course", course, ["name", "title", "currency", "course_price"], as_dict=True
|
"LMS Course", course, ["name", "title", "currency", "course_price"], as_dict=True
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -475,7 +475,7 @@ def get_evaluation_details(course, member=None):
|
|||||||
def format_amount(amount, currency):
|
def format_amount(amount, currency):
|
||||||
amount_reduced = amount / 1000
|
amount_reduced = amount / 1000
|
||||||
if amount_reduced < 1:
|
if amount_reduced < 1:
|
||||||
return amount
|
return fmt_money(amount, 0, currency)
|
||||||
precision = 0 if amount % 1000 == 0 else 1
|
precision = 0 if amount % 1000 == 0 else 1
|
||||||
return _("{0}k").format(fmt_money(amount_reduced, precision, currency))
|
return _("{0}k").format(fmt_money(amount_reduced, precision, currency))
|
||||||
|
|
||||||
|
|||||||
@@ -421,7 +421,7 @@ input[type=checkbox] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.course-price {
|
.course-price {
|
||||||
font-weight: 700;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.view-course-link {
|
.view-course-link {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<span class="btn btn-secondary btn-sm review-link">
|
<span class="btn btn-secondary btn-sm review-link">
|
||||||
{{ _("Write a review") }}
|
{{ _("Write a review") }}
|
||||||
</span>
|
</span>
|
||||||
{% elif not is_instructor(course.name) and frappe.session.user == "Guest" %}
|
{% elif not is_instructor and frappe.session.user == "Guest" %}
|
||||||
<a class="btn btn-secondary btn-sm" href="/login?redirect-to=/courses/{{ course.name }}">
|
<a class="btn btn-secondary btn-sm" href="/login?redirect-to=/courses/{{ course.name }}">
|
||||||
{{ _("Write a review") }}
|
{{ _("Write a review") }}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
0
lms/www/billing/__init__.py
Normal file
0
lms/www/billing/__init__.py
Normal file
@@ -15,7 +15,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% macro Header() %}
|
{% macro Header() %}
|
||||||
<div class="px-4">
|
<div class="px-4 pb-2">
|
||||||
<div class="page-title">
|
<div class="page-title">
|
||||||
{{ _("Order Details") }}
|
{{ _("Order Details") }}
|
||||||
</div>
|
</div>
|
||||||
@@ -26,25 +26,18 @@
|
|||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro CourseDetails() %}
|
{% macro CourseDetails() %}
|
||||||
<div class="px-4 pt-10 pb-8 border-bottom">
|
<div class="px-4 pt-5 border-top">
|
||||||
<div class="flex justify-between">
|
<div class="">
|
||||||
<div class="">
|
<div class="flex mb-2">
|
||||||
{{ _("Course:") }}
|
<div class="field-label">
|
||||||
|
{{ _("Course Name: ") }} {{ course.title }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field-label">
|
|
||||||
{{ course.title }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-divider my-2"></div>
|
<div class="flex">
|
||||||
|
<div class="field-label">
|
||||||
<div class="flex justify-between">
|
{{ _("Total Price: ") }} {{ frappe.utils.fmt_money(course.course_price, 2, course.currency) }}
|
||||||
<div class="">
|
</div>
|
||||||
{{ _("Total Price:") }}
|
|
||||||
</div>
|
|
||||||
<div class="field-label">
|
|
||||||
<!-- $ 50k -->
|
|
||||||
{{ frappe.utils.fmt_money(course.course_price, 2, course.currency) }}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -99,7 +99,13 @@ const handle_success = (response, course, address, order_id) => {
|
|||||||
order_id: order_id,
|
order_id: order_id,
|
||||||
},
|
},
|
||||||
callback: (data) => {
|
callback: (data) => {
|
||||||
window.location.href = data.message;
|
frappe.show_alert({
|
||||||
|
message: __("Payment Successful"),
|
||||||
|
indicator: "green",
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = data.message;
|
||||||
|
}, 2000);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -122,6 +122,10 @@
|
|||||||
|
|
||||||
{{ Notes(course) }}
|
{{ Notes(course) }}
|
||||||
|
|
||||||
|
<div class="vertically-center mb-3 bold-heading">
|
||||||
|
{{ format_amount(course.course_price, course.currency) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="vertically-center mb-3">
|
<div class="vertically-center mb-3">
|
||||||
<svg class="icon icon-md mr-1">
|
<svg class="icon icon-md mr-1">
|
||||||
<use class="" href="#icon-users">
|
<use class="" href="#icon-users">
|
||||||
@@ -194,18 +198,18 @@
|
|||||||
membership.current_lesson else "1.1" if first_lesson_exists(course.name) else None %}
|
membership.current_lesson else "1.1" if first_lesson_exists(course.name) else None %}
|
||||||
|
|
||||||
<div class="all-cta">
|
<div class="all-cta">
|
||||||
{% if is_instructor(course.name) and not course.published and course.status != "Under Review" %}
|
{% if is_instructor and not course.published and course.status != "Under Review" %}
|
||||||
<div class="btn btn-primary wide-button" id="submit-for-review" data-course="{{ course.name | urlencode }}">
|
<div class="btn btn-primary wide-button" id="submit-for-review" data-course="{{ course.name | urlencode }}">
|
||||||
{{ _("Submit for Review") }}
|
{{ _("Submit for Review") }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% elif is_instructor(course.name) and lesson_index %}
|
{% elif is_instructor and lesson_index %}
|
||||||
<a class="btn btn-primary wide-button" id="continue-learning"
|
<a class="btn btn-primary wide-button" id="continue-learning"
|
||||||
href="{{ get_lesson_url(course.name, lesson_index) }}{{ course.query_parameter }}">
|
href="{{ get_lesson_url(course.name, lesson_index) }}{{ course.query_parameter }}">
|
||||||
{{ _("Checkout Course") }}
|
{{ _("Checkout Course") }}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{% elif course.upcoming and not is_user_interested and not is_instructor(course.name) %}
|
{% elif course.upcoming and not is_user_interested and not is_instructor %}
|
||||||
<div class="btn btn-secondary wide-button notify-me" data-course="{{course.name | urlencode}}">
|
<div class="btn btn-secondary wide-button notify-me" data-course="{{course.name | urlencode}}">
|
||||||
{{ _("Notify me when available") }}
|
{{ _("Notify me when available") }}
|
||||||
</div>
|
</div>
|
||||||
@@ -221,7 +225,7 @@
|
|||||||
{{ _("Continue Learning") }}
|
{{ _("Continue Learning") }}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{% elif course.paid_course %}
|
{% 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.name | urlencode }}">
|
||||||
{{ _("Buy This Course") }}
|
{{ _("Buy This Course") }}
|
||||||
</a>
|
</a>
|
||||||
@@ -247,7 +251,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if is_instructor(course.name) or has_course_moderator_role() %}
|
{% if is_instructor or has_course_moderator_role() %}
|
||||||
<a class="btn btn-secondary wide-button mt-2" title="Edit Course" href="/courses/{{ course.name }}/edit">
|
<a class="btn btn-secondary wide-button mt-2" title="Edit Course" href="/courses/{{ course.name }}/edit">
|
||||||
<!-- <svg class="icon icon-md">
|
<!-- <svg class="icon icon-md">
|
||||||
<use href="#icon-edit"></use>
|
<use href="#icon-edit"></use>
|
||||||
@@ -266,7 +270,7 @@
|
|||||||
{{ _("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 course.status == "Under Review" and is_instructor(course.name) %}
|
{% if course.status == "Under Review" and is_instructor %}
|
||||||
<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.") }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ def set_course_context(context, course_name):
|
|||||||
"?batch=" + membership.batch if membership and membership.batch else ""
|
"?batch=" + membership.batch if membership and membership.batch else ""
|
||||||
)
|
)
|
||||||
context.membership = membership
|
context.membership = membership
|
||||||
|
context.is_instructor = is_instructor(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
|
||||||
|
|||||||
@@ -157,6 +157,43 @@
|
|||||||
<img {% if course.image %} class="image-preview" src="{{ course.image }}" {% endif %}>
|
<img {% if course.image %} class="image-preview" src="{{ course.image }}" {% endif %}>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="field-group">
|
||||||
|
<label for="paid_course" class="vertically-center mb-0">
|
||||||
|
<input type="checkbox" id="paid-course" {% if course.paid_course %} checked {% endif %}>
|
||||||
|
{{ _("Paid Course") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field-group price-field {% if not course.paid_course %} hide {% endif %}">
|
||||||
|
<div class="field-label {% if course.paid_course %} reqd {% endif %}">
|
||||||
|
{{ _("Course Price") }}
|
||||||
|
</div>
|
||||||
|
<div class="field-description">
|
||||||
|
{{ _("The price of this course.") }}
|
||||||
|
</div>
|
||||||
|
<div class="">
|
||||||
|
<input id="course-price" type="number" class="field-input" {% if course.course_price %} value="{{ course.course_price }}" {% endif %}>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field-group price-field {% if not course.paid_course %} hide {% endif %}">
|
||||||
|
<div class="field-label {% if course.paid_course %} reqd {% endif %}">
|
||||||
|
{{ _("Currency") }}
|
||||||
|
</div>
|
||||||
|
<div class="field-description">
|
||||||
|
{{ _("The currency in which users will pay for this course.") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<select class="field-input" id="currency">
|
||||||
|
<option></option>
|
||||||
|
{% for currency in currencies %}
|
||||||
|
<option value="{{ currency }}" {% if currency == course.currency %} selected {% endif %}>
|
||||||
|
{{ currency}}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="field-group">
|
<div class="field-group">
|
||||||
<div class="field-label">
|
<div class="field-label">
|
||||||
{{ _("Instructor") }}
|
{{ _("Instructor") }}
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ frappe.ready(() => {
|
|||||||
$(".btn-upload").click((e) => {
|
$(".btn-upload").click((e) => {
|
||||||
upload_file(e);
|
upload_file(e);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#paid-course").click((e) => {
|
||||||
|
setup_paid_course(e);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const create_tag = (e) => {
|
const create_tag = (e) => {
|
||||||
@@ -79,6 +83,9 @@ const save_course = (e) => {
|
|||||||
: "",
|
: "",
|
||||||
published: $("#published").prop("checked") ? 1 : 0,
|
published: $("#published").prop("checked") ? 1 : 0,
|
||||||
upcoming: $("#upcoming").prop("checked") ? 1 : 0,
|
upcoming: $("#upcoming").prop("checked") ? 1 : 0,
|
||||||
|
paid_course: $("#paid-course").prop("checked") ? 1 : 0,
|
||||||
|
course_price: $("#course-price").val(),
|
||||||
|
currency: $("#currency").val(),
|
||||||
},
|
},
|
||||||
callback: (data) => {
|
callback: (data) => {
|
||||||
frappe.show_alert({
|
frappe.show_alert({
|
||||||
@@ -167,3 +174,13 @@ const upload_file = (e) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setup_paid_course = (e) => {
|
||||||
|
if ($(e.target).prop("checked")) {
|
||||||
|
$(".price-field").removeClass("hide");
|
||||||
|
$(".price-field").find(".field-label").addClass("reqd");
|
||||||
|
} else {
|
||||||
|
$(".price-field").addClass("hide");
|
||||||
|
$(".price-field").find(".field-label").removeClass("reqd");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ def get_context(context):
|
|||||||
context.member = frappe.db.get_value(
|
context.member = frappe.db.get_value(
|
||||||
"User", frappe.session.user, ["full_name", "username"], as_dict=True
|
"User", frappe.session.user, ["full_name", "username"], as_dict=True
|
||||||
)
|
)
|
||||||
|
context.currencies = frappe.get_all("Currency", {"enabled": 1}, pluck="currency_name")
|
||||||
|
|
||||||
|
|
||||||
def set_course_context(context, course_name):
|
def set_course_context(context, course_name):
|
||||||
@@ -51,8 +52,8 @@ def set_course_context(context, course_name):
|
|||||||
"video_link",
|
"video_link",
|
||||||
"enable_certification",
|
"enable_certification",
|
||||||
"grant_certificate_after",
|
"grant_certificate_after",
|
||||||
"paid_certificate",
|
"paid_course",
|
||||||
"price_certificate",
|
"course_price",
|
||||||
"currency",
|
"currency",
|
||||||
"max_attempts",
|
"max_attempts",
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user