chore: fixed merge conflicts
This commit is contained in:
@@ -1 +1 @@
|
||||
__version__ = "2.40.0"
|
||||
__version__ = "2.41.0"
|
||||
|
||||
@@ -104,7 +104,10 @@ doc_events = {
|
||||
"lms.lms.doctype.lms_badge.lms_badge.process_badges",
|
||||
]
|
||||
},
|
||||
"Discussion Reply": {"after_insert": "lms.lms.utils.handle_notifications"},
|
||||
"Discussion Reply": {
|
||||
"after_insert": "lms.lms.utils.handle_notifications",
|
||||
"validate": "lms.lms.utils.validate_discussion_reply",
|
||||
},
|
||||
"Notification Log": {"on_change": "lms.lms.utils.publish_notifications"},
|
||||
"User": {
|
||||
"validate": "lms.lms.user.validate_username_duplicates",
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
"type",
|
||||
"work_mode",
|
||||
"status",
|
||||
"disabled",
|
||||
"section_break_6",
|
||||
"company_name",
|
||||
"company_website",
|
||||
@@ -97,12 +96,6 @@
|
||||
"label": "Company Logo",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disabled"
|
||||
},
|
||||
{
|
||||
"fieldname": "company_email_address",
|
||||
"fieldtype": "Data",
|
||||
@@ -137,8 +130,8 @@
|
||||
}
|
||||
],
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2025-09-24 15:32:49.030004",
|
||||
"modified_by": "Administrator",
|
||||
"modified": "2025-12-02 16:58:49.903274",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "Job",
|
||||
"name": "Job Opportunity",
|
||||
"owner": "Administrator",
|
||||
|
||||
@@ -520,7 +520,7 @@ def get_sidebar_settings():
|
||||
web_pages = frappe.get_all(
|
||||
"LMS Sidebar Item",
|
||||
{"parenttype": "LMS Settings", "parentfield": "sidebar_items"},
|
||||
["web_page", "route", "title as label", "icon"],
|
||||
["web_page", "route", "title as label", "icon", "name"],
|
||||
)
|
||||
for page in web_pages:
|
||||
page.to = page.route
|
||||
@@ -1014,6 +1014,7 @@ def give_discussions_permission():
|
||||
"write": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"if_owner": 0 if role == "Moderator" else 1,
|
||||
}
|
||||
).save(ignore_permissions=True)
|
||||
|
||||
@@ -1303,7 +1304,24 @@ def get_notifications(filters):
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def get_lms_setting(field):
|
||||
def get_lms_setting(field=None):
|
||||
if not field:
|
||||
frappe.throw(_("Field name is required"))
|
||||
frappe.log_error("Field name is missing when accessing LMS Settings {0} {1} {2}").format(
|
||||
frappe.local.request_ip, frappe.get_request_header("Referer"), frappe.get_request_header("Origin")
|
||||
)
|
||||
|
||||
allowed_fields = [
|
||||
"allow_guest_access",
|
||||
"prevent_skipping_videos",
|
||||
"contact_us_email",
|
||||
"contact_us_url",
|
||||
"livecode_url",
|
||||
]
|
||||
|
||||
if field not in allowed_fields:
|
||||
frappe.throw(_("You are not allowed to access this field"))
|
||||
|
||||
return frappe.get_cached_value("LMS Settings", None, field)
|
||||
|
||||
|
||||
@@ -1451,11 +1469,11 @@ def get_meta_info(type, route):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_meta_info(type, route, meta_tags):
|
||||
parent_name = f"{type}/{route}"
|
||||
if not isinstance(meta_tags, list):
|
||||
frappe.throw(_("Meta tags should be a list."))
|
||||
def update_meta_info(meta_type, route, meta_tags):
|
||||
validate_meta_data_permissions(meta_type)
|
||||
validate_meta_tags(meta_tags)
|
||||
|
||||
parent_name = f"{meta_type}/{route}"
|
||||
for tag in meta_tags:
|
||||
existing_tag = frappe.db.exists(
|
||||
"Website Meta Tag",
|
||||
@@ -1482,18 +1500,43 @@ def update_meta_info(type, route, meta_tags):
|
||||
|
||||
parent_exists = frappe.db.exists("Website Route Meta", parent_name)
|
||||
if not parent_exists:
|
||||
route_meta = frappe.new_doc("Website Route Meta")
|
||||
route_meta.update(
|
||||
{
|
||||
"__newname": parent_name,
|
||||
}
|
||||
)
|
||||
route_meta.append("meta_tags", tag_properties)
|
||||
route_meta.insert()
|
||||
create_meta(parent_name, tag_properties)
|
||||
else:
|
||||
new_tag = frappe.new_doc("Website Meta Tag")
|
||||
new_tag.update(tag_properties)
|
||||
new_tag.insert()
|
||||
create_meta_tag(tag_properties)
|
||||
|
||||
|
||||
def validate_meta_tags(meta_tags):
|
||||
if not isinstance(meta_tags, list):
|
||||
frappe.throw(_("Meta tags should be a list."))
|
||||
|
||||
|
||||
def create_meta(parent_name, tag_properties):
|
||||
route_meta = frappe.new_doc("Website Route Meta")
|
||||
route_meta.update(
|
||||
{
|
||||
"__newname": parent_name,
|
||||
}
|
||||
)
|
||||
route_meta.append("meta_tags", tag_properties)
|
||||
route_meta.insert()
|
||||
|
||||
|
||||
def create_meta_tag(tag_properties):
|
||||
new_tag = frappe.new_doc("Website Meta Tag")
|
||||
new_tag.update(tag_properties)
|
||||
new_tag.insert()
|
||||
|
||||
|
||||
def validate_meta_data_permissions(meta_type):
|
||||
roles = frappe.get_roles()
|
||||
|
||||
if meta_type == "courses":
|
||||
if not ("Course Creator" in roles or "Moderator" in roles):
|
||||
frappe.throw(_("You do not have permission to update meta tags."))
|
||||
|
||||
elif meta_type == "batches":
|
||||
if not ("Batch Evaluator" in roles or "Moderator" in roles):
|
||||
frappe.throw(_("You do not have permission to update meta tags."))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -1678,7 +1721,18 @@ def get_profile_details(username):
|
||||
details = frappe.db.get_value(
|
||||
"User",
|
||||
{"username": username},
|
||||
["full_name", "name", "username", "user_image", "bio", "headline", "cover_image"],
|
||||
[
|
||||
"first_name",
|
||||
"last_name",
|
||||
"full_name",
|
||||
"name",
|
||||
"username",
|
||||
"user_image",
|
||||
"bio",
|
||||
"headline",
|
||||
"language",
|
||||
"cover_image",
|
||||
],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
"doctype": "Dashboard Chart",
|
||||
"document_type": "LMS Certificate",
|
||||
"dynamic_filters_json": "[]",
|
||||
"filters_json": "[[\"LMS Certificate\",\"published\",\"=\",1,false]]",
|
||||
"filters_json": "[[\"LMS Certificate\",\"published\",\"=\",1]]",
|
||||
"group_by_type": "Count",
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"modified": "2025-04-28 17:47:28.517149",
|
||||
"modified": "2025-12-07 17:47:28.517150",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "Certification",
|
||||
|
||||
@@ -9,19 +9,20 @@
|
||||
"doctype": "Dashboard Chart",
|
||||
"document_type": "User",
|
||||
"dynamic_filters_json": "[]",
|
||||
"filters_json": "[[\"User\",\"enabled\",\"=\",1,false]]",
|
||||
"filters_json": "[[\"User\",\"enabled\",\"=\",1]]",
|
||||
"group_by_type": "Count",
|
||||
"idx": 5,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"last_synced_on": "2025-04-28 15:09:52.161688",
|
||||
"modified": "2025-04-28 17:47:58.168293",
|
||||
"last_synced_on": "2025-12-08 13:05:16.186243",
|
||||
"modified": "2025-12-09 13:08:50.049053",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "New Signups",
|
||||
"number_of_groups": 0,
|
||||
"owner": "basawaraj@erpnext.com",
|
||||
"roles": [],
|
||||
"show_values_over_chart": 0,
|
||||
"source": "",
|
||||
"time_interval": "Daily",
|
||||
"timeseries": 1,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
from lms.lms.utils import has_course_instructor_role, has_course_moderator_role
|
||||
from lms.lms.utils import has_course_instructor_role, has_moderator_role
|
||||
|
||||
|
||||
class LMSAssignment(Document):
|
||||
@@ -13,7 +13,7 @@ class LMSAssignment(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def save_assignment(assignment, title, type, question):
|
||||
if not has_course_moderator_role() or not has_course_instructor_role():
|
||||
if not has_moderator_role() or not has_course_instructor_role():
|
||||
return
|
||||
|
||||
if assignment:
|
||||
|
||||
@@ -30,9 +30,7 @@ frappe.ui.form.on("LMS Badge", {
|
||||
|
||||
const user_fields = fields
|
||||
.filter(
|
||||
(df) =>
|
||||
(df.fieldtype === "Link" && df.options === "User") ||
|
||||
df.fieldtype === "Data"
|
||||
(df) => df.fieldtype === "Link" && df.options === "User"
|
||||
)
|
||||
.map(map_for_options)
|
||||
.concat([
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-11-10 11:39:42.233779",
|
||||
"modified": "2025-12-04 17:06:26.090276",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Badge Assignment",
|
||||
@@ -116,25 +116,13 @@
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "LMS Student",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
"role": "LMS Student"
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "LMS Student",
|
||||
"share": 1
|
||||
"role": "LMS Student"
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
|
||||
@@ -1,9 +1,64 @@
|
||||
# Copyright (c) 2024, Frappe and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
from lms.lms.doctype.lms_badge.lms_badge import eval_condition
|
||||
|
||||
|
||||
class LMSBadgeAssignment(Document):
|
||||
pass
|
||||
def validate(self):
|
||||
self.validate_owner()
|
||||
self.validate_duplicate_badge_assignment()
|
||||
self.validate_badge_criteria()
|
||||
|
||||
def validate_owner(self):
|
||||
if self.owner == self.member:
|
||||
return
|
||||
|
||||
roles = frappe.get_roles(self.owner)
|
||||
if "Moderator" not in roles:
|
||||
frappe.throw(_("You must be a Moderator to assign badges to users."))
|
||||
|
||||
def validate_duplicate_badge_assignment(self):
|
||||
grant_only_once = frappe.db.get_value("LMS Badge", self.badge, "grant_only_once")
|
||||
if not grant_only_once:
|
||||
return
|
||||
|
||||
if frappe.db.exists(
|
||||
"LMS Badge Assignment",
|
||||
{"badge": self.badge, "member": self.member, "name": ["!=", self.name]},
|
||||
):
|
||||
frappe.throw(
|
||||
_("Badge {0} has already been assigned to this {1}.").format(self.badge, self.member)
|
||||
)
|
||||
|
||||
def validate_badge_criteria(self):
|
||||
badge_details = frappe.db.get_value(
|
||||
"LMS Badge", self.badge, ["reference_doctype", "user_field", "condition", "enabled"], as_dict=True
|
||||
)
|
||||
|
||||
if badge_details:
|
||||
if badge_details.reference_doctype and badge_details.user_field and badge_details.condition:
|
||||
user_fieldname = frappe.db.get_value(
|
||||
"DocField",
|
||||
{"parent": badge_details.reference_doctype, "fieldname": badge_details.user_field},
|
||||
"fieldname",
|
||||
)
|
||||
|
||||
documents = frappe.get_all(
|
||||
badge_details.reference_doctype,
|
||||
{user_fieldname: self.member},
|
||||
)
|
||||
|
||||
for document in documents:
|
||||
reference_value = eval_condition(
|
||||
frappe.get_doc(badge_details.reference_doctype, document.name),
|
||||
badge_details.condition,
|
||||
)
|
||||
if reference_value:
|
||||
return
|
||||
|
||||
frappe.throw(_("Member does not meet the criteria for the badge {0}.").format(self.badge))
|
||||
|
||||
@@ -379,7 +379,7 @@
|
||||
"link_fieldname": "batch_name"
|
||||
}
|
||||
],
|
||||
"modified": "2025-05-26 15:30:55.083507",
|
||||
"modified": "2025-12-04 12:54:11.190967",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Batch",
|
||||
@@ -422,13 +422,8 @@
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "LMS Student",
|
||||
"share": 1
|
||||
"role": "LMS Student"
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
|
||||
@@ -73,8 +73,8 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-02-11 10:39:57.259526",
|
||||
"modified_by": "Administrator",
|
||||
"modified": "2025-12-04 12:53:38.246250",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Batch Enrollment",
|
||||
"owner": "Administrator",
|
||||
@@ -105,18 +105,14 @@
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "LMS Student",
|
||||
"share": 1
|
||||
"role": "LMS Student"
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "member_name"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,19 @@ class LMSBatchEnrollment(Document):
|
||||
self.add_member_to_live_class()
|
||||
|
||||
def validate(self):
|
||||
self.validate_owner()
|
||||
self.validate_duplicate_members()
|
||||
self.validate_seat_availability()
|
||||
self.validate_course_enrollment()
|
||||
|
||||
def validate_owner(self):
|
||||
if self.owner == self.member:
|
||||
return
|
||||
|
||||
roles = frappe.get_roles(self.owner)
|
||||
if not ("Moderator" in roles or "Batch Evaluator" in roles):
|
||||
frappe.throw(_("You must be a Moderator or Batch Evaluator to enroll users in a batch."))
|
||||
|
||||
def validate_duplicate_members(self):
|
||||
if frappe.db.exists(
|
||||
"LMS Batch Enrollment",
|
||||
@@ -25,6 +35,12 @@ class LMSBatchEnrollment(Document):
|
||||
):
|
||||
frappe.throw(_("Member already enrolled in this batch"))
|
||||
|
||||
def validate_seat_availability(self):
|
||||
seat_count = frappe.db.get_value("LMS Batch", self.batch, "seat_count")
|
||||
enrolled_count = frappe.db.count("LMS Batch Enrollment", {"batch": self.batch})
|
||||
if seat_count and enrolled_count >= seat_count:
|
||||
frappe.throw(_("There are no seats available in this batch."))
|
||||
|
||||
def validate_course_enrollment(self):
|
||||
courses = frappe.get_all("Batch Course", filters={"parent": self.batch}, fields=["course"])
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:category",
|
||||
"creation": "2023-06-15 12:40:36.484165",
|
||||
@@ -21,8 +22,8 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-03-19 12:12:23.723432",
|
||||
"modified_by": "Administrator",
|
||||
"modified": "2025-11-08 19:28:28.468137",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Category",
|
||||
"naming_rule": "By fieldname",
|
||||
@@ -73,9 +74,10 @@
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "category",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ class LMSCertificate(Document):
|
||||
self.name = make_autoname("hash", self.doctype)
|
||||
|
||||
def after_insert(self):
|
||||
if not frappe.flags.in_test:
|
||||
if not frappe.in_test:
|
||||
outgoing_email_account = frappe.get_cached_value(
|
||||
"Email Account", {"default_outgoing": 1, "enable_outgoing": 1}, "name"
|
||||
)
|
||||
@@ -115,29 +115,14 @@ def has_website_permission(doc, ptype, user, verbose=False):
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_certificate(course):
|
||||
certificate = is_certified(course)
|
||||
|
||||
if certificate:
|
||||
if is_certified(course):
|
||||
return frappe.db.get_value(
|
||||
"LMS Certificate", certificate, ["name", "course", "template"], as_dict=True
|
||||
)
|
||||
|
||||
else:
|
||||
default_certificate_template = frappe.db.get_value(
|
||||
"Property Setter",
|
||||
{
|
||||
"doc_type": "LMS Certificate",
|
||||
"property": "default_print_format",
|
||||
},
|
||||
"value",
|
||||
)
|
||||
if not default_certificate_template:
|
||||
default_certificate_template = frappe.db.get_value(
|
||||
"Print Format",
|
||||
{
|
||||
"doc_type": "LMS Certificate",
|
||||
},
|
||||
)
|
||||
validate_certification_eligibility(course)
|
||||
default_certificate_template = get_default_certificate_template()
|
||||
certificate = frappe.get_doc(
|
||||
{
|
||||
"doctype": "LMS Certificate",
|
||||
@@ -149,3 +134,37 @@ def create_certificate(course):
|
||||
)
|
||||
certificate.save(ignore_permissions=True)
|
||||
return certificate
|
||||
|
||||
|
||||
def get_default_certificate_template():
|
||||
default_certificate_template = frappe.db.get_value(
|
||||
"Property Setter",
|
||||
{
|
||||
"doc_type": "LMS Certificate",
|
||||
"property": "default_print_format",
|
||||
},
|
||||
"value",
|
||||
)
|
||||
if not default_certificate_template:
|
||||
default_certificate_template = frappe.db.get_value(
|
||||
"Print Format",
|
||||
{
|
||||
"doc_type": "LMS Certificate",
|
||||
},
|
||||
)
|
||||
|
||||
return default_certificate_template
|
||||
|
||||
|
||||
def validate_certification_eligibility(course):
|
||||
if not frappe.db.exists("LMS Enrollment", {"course": course, "member": frappe.session.user}):
|
||||
frappe.throw(_("You are not enrolled in this course."))
|
||||
|
||||
if not frappe.db.get_value("LMS Course", course, "enable_certification"):
|
||||
frappe.throw(_("Certification is not enabled for this course."))
|
||||
|
||||
progress = frappe.db.get_value(
|
||||
"LMS Enrollment", {"course": course, "member": frappe.session.user}, "progress"
|
||||
)
|
||||
if progress < 100:
|
||||
frappe.throw(_("You have not completed the course yet."))
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_years, cint, nowdate
|
||||
from frappe.utils import cint, nowdate
|
||||
|
||||
from lms.lms.doctype.lms_certificate.lms_certificate import create_certificate
|
||||
from lms.lms.doctype.lms_course.test_lms_course import new_course
|
||||
@@ -18,6 +18,7 @@ class TestLMSCertificate(unittest.TestCase):
|
||||
"enable_certification": 1,
|
||||
},
|
||||
)
|
||||
create_enrollment(course.name)
|
||||
certificate = create_certificate(course.name)
|
||||
|
||||
self.assertEqual(certificate.member, "Administrator")
|
||||
@@ -26,3 +27,11 @@ class TestLMSCertificate(unittest.TestCase):
|
||||
|
||||
frappe.db.delete("LMS Certificate", certificate.name)
|
||||
frappe.db.delete("LMS Course", course.name)
|
||||
|
||||
|
||||
def create_enrollment(course):
|
||||
enrollment = frappe.new_doc("LMS Enrollment")
|
||||
enrollment.course = course
|
||||
enrollment.member = frappe.session.user
|
||||
enrollment.progress = cint(100)
|
||||
enrollment.save()
|
||||
|
||||
@@ -6,7 +6,7 @@ from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
|
||||
from lms.lms.utils import has_course_moderator_role
|
||||
from lms.lms.utils import has_moderator_role
|
||||
|
||||
|
||||
class LMSCertificateEvaluation(Document):
|
||||
@@ -19,7 +19,7 @@ class LMSCertificateEvaluation(Document):
|
||||
|
||||
|
||||
def has_website_permission(doc, ptype, user, verbose=False):
|
||||
if has_course_moderator_role() or doc.member == frappe.session.user:
|
||||
if has_moderator_role() or doc.member == frappe.session.user:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ class TestLMSCourse(unittest.TestCase):
|
||||
frappe.delete_doc("User", "tester@example.com")
|
||||
|
||||
if frappe.db.exists("LMS Course", "test-course"):
|
||||
frappe.db.delete("Batch Course", {"course": "test-course"})
|
||||
frappe.db.delete("Exercise Submission", {"course": "test-course"})
|
||||
frappe.db.delete("Exercise Latest Submission", {"course": "test-course"})
|
||||
frappe.db.delete("LMS Exercise", {"course": "test-course"})
|
||||
|
||||
@@ -77,8 +77,7 @@ def update_program_progress(member):
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_membership(course, batch=None, member=None, member_type="Student", role="Member"):
|
||||
if frappe.db.get_value("LMS Course", course, "disable_self_learning"):
|
||||
return False
|
||||
validate_course_enrollment_eligibility(course, member)
|
||||
|
||||
enrollment = frappe.new_doc("LMS Enrollment")
|
||||
enrollment.update(
|
||||
@@ -95,6 +94,42 @@ def create_membership(course, batch=None, member=None, member_type="Student", ro
|
||||
return enrollment
|
||||
|
||||
|
||||
def validate_course_enrollment_eligibility(course, member):
|
||||
if not member:
|
||||
member = frappe.session.user
|
||||
|
||||
course_details = frappe.db.get_value(
|
||||
"LMS Course",
|
||||
course,
|
||||
["published", "disable_self_learning", "paid_course", "paid_certificate"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
if course_details.disable_self_learning:
|
||||
frappe.throw(
|
||||
_(
|
||||
"You cannot enroll in this course as self-learning is disabled. Please contact the Administrator."
|
||||
)
|
||||
)
|
||||
|
||||
if not course_details.published:
|
||||
frappe.throw(_("You cannot enroll in an unpublished course."))
|
||||
|
||||
if course_details.paid_course:
|
||||
payment = frappe.db.exists(
|
||||
"LMS Payment",
|
||||
{
|
||||
"reference_doctype": "LMS Course",
|
||||
"reference_docname": course,
|
||||
"member": member,
|
||||
"payment_receipt": True,
|
||||
},
|
||||
)
|
||||
|
||||
if not payment:
|
||||
frappe.throw(_("You need to complete the payment for this course before enrolling."))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_current_membership(batch, course, member):
|
||||
all_memberships = frappe.get_all("LMS Enrollment", {"member": member, "course": course})
|
||||
|
||||
@@ -9,60 +9,15 @@ from lms.lms.doctype.lms_course.test_lms_course import new_course, new_user
|
||||
|
||||
|
||||
class TestLMSEnrollment(unittest.TestCase):
|
||||
def setUp(self):
|
||||
frappe.db.delete("LMS Enrollment")
|
||||
frappe.db.delete("LMS Batch Old")
|
||||
frappe.db.delete("LMS Course Mentor Mapping")
|
||||
frappe.db.delete("User", {"email": ("like", "%@test.com")})
|
||||
|
||||
def new_course_batch(self):
|
||||
course = new_course("Test Course")
|
||||
|
||||
new_user("Test Mentor", "mentor@test.com")
|
||||
# without this, the creating batch will fail
|
||||
course.add_mentor("mentor@test.com")
|
||||
|
||||
frappe.session.user = "mentor@test.com"
|
||||
|
||||
batch = frappe.get_doc(
|
||||
{
|
||||
"doctype": "LMS Batch Old",
|
||||
"name": "test-batch",
|
||||
"title": "Test Batch",
|
||||
"course": course.name,
|
||||
}
|
||||
)
|
||||
batch.insert(ignore_permissions=True)
|
||||
|
||||
frappe.session.user = "Administrator"
|
||||
return course, batch
|
||||
|
||||
def add_membership(self, batch_name, member_name, course, member_type="Student"):
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "LMS Enrollment",
|
||||
"batch_old": batch_name,
|
||||
"member": member_name,
|
||||
"member_type": member_type,
|
||||
"course": course,
|
||||
}
|
||||
)
|
||||
doc.insert()
|
||||
return doc
|
||||
|
||||
def test_membership(self):
|
||||
course, batch = self.new_course_batch()
|
||||
member = new_user("Test", "test01@test.com")
|
||||
membership = self.add_membership(batch.name, member.name, course.name)
|
||||
course = new_course("Test Enrollment")
|
||||
enrollment = frappe.new_doc("LMS Enrollment")
|
||||
enrollment.course = course.name
|
||||
enrollment.member = frappe.session.user
|
||||
|
||||
assert membership.course == course.name
|
||||
assert membership.member_name == member.full_name
|
||||
enrollment.save()
|
||||
|
||||
def test_membership_change_role(self):
|
||||
course, batch = self.new_course_batch()
|
||||
member = new_user("Test", "test01@test.com")
|
||||
membership = self.add_membership(batch.name, member.name, course.name)
|
||||
|
||||
# it should be possible to change role
|
||||
membership.role = "Admin"
|
||||
membership.save()
|
||||
self.assertEqual(enrollment.course, course.name)
|
||||
self.assertEqual(enrollment.member, "Administrator")
|
||||
frappe.db.delete("LMS Enrollment", enrollment.name)
|
||||
frappe.db.delete("LMS Course", course.name)
|
||||
|
||||
@@ -3,52 +3,8 @@
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
from lms.lms.doctype.lms_course.test_lms_course import new_course
|
||||
# import frappe
|
||||
|
||||
|
||||
class TestLMSExercise(unittest.TestCase):
|
||||
def new_exercise(self):
|
||||
course = new_course("Test Course")
|
||||
member = frappe.get_doc(
|
||||
{
|
||||
"doctype": "LMS Enrollment",
|
||||
"course": course.name,
|
||||
"member": frappe.session.user,
|
||||
}
|
||||
)
|
||||
member.insert()
|
||||
e = frappe.get_doc(
|
||||
{
|
||||
"doctype": "LMS Exercise",
|
||||
"name": "test-problem",
|
||||
"course": course.name,
|
||||
"title": "Test Problem",
|
||||
"description": "draw a circle",
|
||||
"code": "# draw a single cicle",
|
||||
"answer": ("# draw a single circle\n" + "circle(100, 100, 50)"),
|
||||
}
|
||||
)
|
||||
e.insert()
|
||||
return e
|
||||
|
||||
def test_exercise(self):
|
||||
e = self.new_exercise()
|
||||
assert e.get_user_submission() is None
|
||||
|
||||
def test_exercise_submission(self):
|
||||
e = self.new_exercise()
|
||||
submission = e.submit("circle(100, 100, 50)")
|
||||
assert submission is not None
|
||||
assert submission.exercise == e.name
|
||||
assert submission.course == e.course
|
||||
|
||||
user_submission = e.get_user_submission()
|
||||
assert user_submission is not None
|
||||
assert user_submission.name == submission.name
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.delete("LMS Enrollment")
|
||||
frappe.db.delete("Exercise Submission")
|
||||
frappe.db.delete("LMS Exercise")
|
||||
pass
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-20 12:28:57.238902",
|
||||
"modified": "2025-12-04 12:56:14.249363",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Program",
|
||||
@@ -136,13 +136,8 @@
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "LMS Student",
|
||||
"share": 1
|
||||
"role": "LMS Student"
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
|
||||
@@ -17,7 +17,7 @@ class LMSProgram(Document):
|
||||
duplicates = {course for course in courses if courses.count(course) > 1}
|
||||
if len(duplicates):
|
||||
frappe.throw(
|
||||
_("Course {0} has already been added to this batch.").format(
|
||||
_("Course {0} has already been added to this program.").format(
|
||||
frappe.bold(next(iter(duplicates)))
|
||||
)
|
||||
)
|
||||
@@ -27,7 +27,7 @@ class LMSProgram(Document):
|
||||
duplicates = {member for member in members if members.count(member) > 1}
|
||||
if len(duplicates):
|
||||
frappe.throw(
|
||||
_("Member {0} has already been added to this batch.").format(
|
||||
_("Member {0} has already been added to this program.").format(
|
||||
frappe.bold(next(iter(duplicates)))
|
||||
)
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
from lms.lms.utils import has_course_instructor_role, has_course_moderator_role
|
||||
from lms.lms.utils import has_course_instructor_role, has_moderator_role
|
||||
|
||||
|
||||
class LMSQuestion(Document):
|
||||
@@ -95,7 +95,7 @@ def get_correct_options(question):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_question_details(question):
|
||||
if not has_course_instructor_role() or not has_course_moderator_role():
|
||||
if not has_course_instructor_role() or not has_moderator_role():
|
||||
return
|
||||
|
||||
fields = ["question", "type", "name"]
|
||||
|
||||
@@ -352,7 +352,7 @@
|
||||
"options": "Email Template"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"default": "1",
|
||||
"fieldname": "disable_signup",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disable Signup"
|
||||
@@ -444,7 +444,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-07 19:22:48.705933",
|
||||
"modified": "2025-12-02 12:21:15.832799",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Settings",
|
||||
|
||||
196
lms/lms/utils.py
196
lms/lms/utils.py
@@ -13,7 +13,6 @@ from frappe.desk.notifications import extract_mentions
|
||||
from frappe.rate_limiter import rate_limit
|
||||
from frappe.utils import (
|
||||
add_months,
|
||||
ceil,
|
||||
cint,
|
||||
cstr,
|
||||
flt,
|
||||
@@ -25,6 +24,7 @@ from frappe.utils import (
|
||||
getdate,
|
||||
nowtime,
|
||||
pretty_date,
|
||||
rounded,
|
||||
)
|
||||
|
||||
from lms.lms.md import find_macros, markdown_to_html
|
||||
@@ -201,7 +201,7 @@ def get_lesson_icon(body, content):
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@rate_limit(limit=50, seconds=60 * 60)
|
||||
@rate_limit(limit=500, seconds=60 * 60)
|
||||
def get_tags(course):
|
||||
tags = frappe.db.get_value("LMS Course", course, "tags")
|
||||
return tags.split(",") if tags else []
|
||||
@@ -246,7 +246,7 @@ def get_average_rating(course):
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@rate_limit(limit=50, seconds=60 * 60)
|
||||
@rate_limit(limit=500, seconds=60 * 60)
|
||||
def get_reviews(course):
|
||||
reviews = frappe.get_all(
|
||||
"LMS Course Review",
|
||||
@@ -492,7 +492,7 @@ def can_create_courses(course, member=None):
|
||||
if frappe.session.user == "Guest":
|
||||
return False
|
||||
|
||||
if has_course_moderator_role(member):
|
||||
if has_moderator_role(member):
|
||||
return True
|
||||
|
||||
if has_course_instructor_role(member) and member in instructors:
|
||||
@@ -504,7 +504,18 @@ def can_create_courses(course, member=None):
|
||||
return False
|
||||
|
||||
|
||||
def has_course_moderator_role(member=None):
|
||||
def can_create_batches(member=None):
|
||||
if not member:
|
||||
member = frappe.session.user
|
||||
|
||||
if has_moderator_role(member):
|
||||
return True
|
||||
if has_evaluator_role(member):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def has_moderator_role(member=None):
|
||||
return frappe.db.get_value(
|
||||
"Has Role",
|
||||
{"parent": member or frappe.session.user, "role": "Moderator"},
|
||||
@@ -512,7 +523,7 @@ def has_course_moderator_role(member=None):
|
||||
)
|
||||
|
||||
|
||||
def has_course_evaluator_role(member=None):
|
||||
def has_evaluator_role(member=None):
|
||||
return frappe.db.get_value(
|
||||
"Has Role",
|
||||
{"parent": member or frappe.session.user, "role": "Batch Evaluator"},
|
||||
@@ -737,7 +748,7 @@ def has_lessons(course):
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@rate_limit(limit=50, seconds=60 * 60)
|
||||
@rate_limit(limit=500, seconds=60 * 60)
|
||||
def get_chart_data(
|
||||
chart_name,
|
||||
timespan="Select Date Range",
|
||||
@@ -758,17 +769,18 @@ def get_chart_data(
|
||||
datefield = chart.based_on
|
||||
value_field = chart.value_based_on or "1"
|
||||
|
||||
filters = [([chart.document_type, "docstatus", "<", 2, False])]
|
||||
filters = [([chart.document_type, "docstatus", "<", 2])]
|
||||
print(chart.filters_json)
|
||||
filters = filters + json.loads(chart.filters_json)
|
||||
filters.append([doctype, datefield, ">=", from_date, False])
|
||||
filters.append([doctype, datefield, "<=", to_date, False])
|
||||
filters.append([doctype, datefield, ">=", from_date])
|
||||
filters.append([doctype, datefield, "<=", to_date])
|
||||
|
||||
data = frappe.db.get_all(
|
||||
doctype,
|
||||
fields=[f"{datefield} as _unit", f"SUM({value_field})", "COUNT(*)"],
|
||||
fields=[datefield, {"SUM": value_field}, {"COUNT": "*"}],
|
||||
filters=filters,
|
||||
group_by="_unit",
|
||||
order_by="_unit asc",
|
||||
group_by=datefield,
|
||||
order_by=datefield,
|
||||
as_list=True,
|
||||
)
|
||||
|
||||
@@ -785,7 +797,7 @@ def get_chart_data(
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@rate_limit(limit=50, seconds=60 * 60)
|
||||
@rate_limit(limit=500, seconds=60 * 60)
|
||||
def get_course_completion_data():
|
||||
all_membership = frappe.db.count("LMS Enrollment")
|
||||
completed = frappe.db.count("LMS Enrollment", {"progress": ["like", "%100%"]})
|
||||
@@ -812,7 +824,7 @@ def get_telemetry_boot_info():
|
||||
|
||||
@frappe.whitelist()
|
||||
def is_onboarding_complete():
|
||||
if not has_course_moderator_role():
|
||||
if not has_moderator_role():
|
||||
return {"is_onboarded": True}
|
||||
|
||||
course_created = frappe.db.a_row_exists("LMS Course")
|
||||
@@ -928,7 +940,7 @@ def check_multicurrency(amount, currency, country=None, amount_usd=None):
|
||||
if apply_rounding and amount % 100 != 0:
|
||||
amount = amount + 100 - amount % 100
|
||||
|
||||
return ceil(amount), currency
|
||||
return rounded(amount), currency
|
||||
|
||||
|
||||
def apply_gst(amount, country=None):
|
||||
@@ -961,7 +973,7 @@ def change_currency(amount, currency, country=None):
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@rate_limit(limit=50, seconds=60 * 60)
|
||||
@rate_limit(limit=500, seconds=60 * 60)
|
||||
def get_courses(filters=None, start=0):
|
||||
"""Returns the list of courses."""
|
||||
|
||||
@@ -1102,7 +1114,7 @@ def get_course_fields():
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@rate_limit(limit=50, seconds=60 * 60)
|
||||
@rate_limit(limit=500, seconds=60 * 60)
|
||||
def get_course_details(course):
|
||||
course_details = frappe.db.get_value(
|
||||
"LMS Course",
|
||||
@@ -1197,7 +1209,6 @@ def get_categorized_courses(courses):
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@rate_limit(limit=50, seconds=60 * 60)
|
||||
def get_course_outline(course, progress=False):
|
||||
"""Returns the course outline."""
|
||||
outline = []
|
||||
@@ -1225,7 +1236,7 @@ def get_course_outline(course, progress=False):
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@rate_limit(limit=50, seconds=60 * 60)
|
||||
@rate_limit(limit=500, seconds=60 * 60)
|
||||
def get_lesson(course, chapter, lesson):
|
||||
chapter_name = frappe.db.get_value("Chapter Reference", {"parent": course, "idx": chapter}, "chapter")
|
||||
lesson_name = frappe.db.get_value("Lesson Reference", {"parent": chapter_name, "idx": lesson}, "lesson")
|
||||
@@ -1256,7 +1267,7 @@ def get_lesson(course, chapter, lesson):
|
||||
if (
|
||||
not lesson_details.include_in_preview
|
||||
and not membership
|
||||
and not has_course_moderator_role()
|
||||
and not has_moderator_role()
|
||||
and not is_instructor(course)
|
||||
):
|
||||
return {
|
||||
@@ -1336,12 +1347,12 @@ def get_neighbour_lesson(course, chapter, lesson):
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@rate_limit(limit=50, seconds=60 * 60)
|
||||
@rate_limit(limit=500, seconds=60 * 60)
|
||||
def get_batch_details(batch):
|
||||
batch_students = frappe.get_all("LMS Batch Enrollment", {"batch": batch}, pluck="member")
|
||||
if (
|
||||
not frappe.db.get_value("LMS Batch", batch, "published")
|
||||
and has_student_role()
|
||||
and not can_create_batches()
|
||||
and frappe.session.user not in batch_students
|
||||
):
|
||||
return
|
||||
@@ -1457,7 +1468,7 @@ def get_question_details(question):
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@rate_limit(limit=50, seconds=60 * 60)
|
||||
@rate_limit(limit=500, seconds=60 * 60)
|
||||
def get_batch_courses(batch):
|
||||
courses = []
|
||||
course_list = frappe.get_all("Batch Course", {"parent": batch}, ["name", "course"])
|
||||
@@ -1681,6 +1692,11 @@ def has_submitted_assessment(assessment, assessment_type, member=None):
|
||||
docfield = "quiz"
|
||||
fields = ["percentage"]
|
||||
not_attempted = 0
|
||||
elif assessment_type == "LMS Programming Exercise":
|
||||
doctype = "LMS Programming Exercise Submission"
|
||||
docfield = "exercise"
|
||||
fields = ["status"]
|
||||
not_attempted = "Not Attempted"
|
||||
|
||||
filters = {}
|
||||
filters[docfield] = assessment
|
||||
@@ -1944,9 +1960,9 @@ def get_lesson_creation_details(course, chapter, lesson):
|
||||
def get_roles(name):
|
||||
frappe.only_for("Moderator")
|
||||
return {
|
||||
"moderator": has_course_moderator_role(name),
|
||||
"moderator": has_moderator_role(name),
|
||||
"course_creator": has_course_instructor_role(name),
|
||||
"batch_evaluator": has_course_evaluator_role(name),
|
||||
"batch_evaluator": has_evaluator_role(name),
|
||||
"lms_student": has_student_role(name),
|
||||
}
|
||||
|
||||
@@ -2048,29 +2064,59 @@ def enroll_in_course(course, payment_name):
|
||||
|
||||
@frappe.whitelist()
|
||||
def enroll_in_batch(batch, payment_name=None):
|
||||
if not frappe.db.exists("LMS Batch Enrollment", {"batch": batch, "member": frappe.session.user}):
|
||||
batch_doc = frappe.db.get_value("LMS Batch", batch, ["name", "seat_count"], as_dict=True)
|
||||
students = frappe.db.count("LMS Batch Enrollment", {"batch": batch})
|
||||
if batch_doc.seat_count and students >= batch_doc.seat_count:
|
||||
frappe.throw(_("The batch is full. Please contact the Administrator."))
|
||||
if not frappe.db.exists("LMS Batch", batch):
|
||||
frappe.throw(_("The specified batch does not exist."))
|
||||
|
||||
new_student = frappe.new_doc("LMS Batch Enrollment")
|
||||
batch_doc = frappe.db.get_value(
|
||||
"LMS Batch", batch, ["name", "seat_count", "allow_self_enrollment"], as_dict=True
|
||||
)
|
||||
payment_doc = get_payment_details(payment_name)
|
||||
validate_enrollment_eligibility(batch_doc, payment_doc)
|
||||
create_enrollment(batch, payment_doc)
|
||||
|
||||
|
||||
def get_payment_details(payment_name):
|
||||
payment_doc = None
|
||||
if payment_name:
|
||||
payment_doc = frappe.db.get_value(
|
||||
"LMS Payment", payment_name, ["name", "source", "payment_received"], as_dict=True
|
||||
)
|
||||
return payment_doc
|
||||
|
||||
|
||||
def validate_enrollment_eligibility(batch_doc, payment_doc=None):
|
||||
if frappe.db.exists("LMS Batch Enrollment", {"batch": batch_doc.name, "member": frappe.session.user}):
|
||||
frappe.throw(_("You are already enrolled in this batch."))
|
||||
|
||||
if batch_doc.paid_batch:
|
||||
if not payment_doc or not payment_doc.payment_received:
|
||||
frappe.throw(_("Payment is required to enroll in this batch."))
|
||||
|
||||
elif not batch_doc.allow_self_enrollment:
|
||||
frappe.throw(_("Enrollment in this batch is restricted. Please contact the Administrator."))
|
||||
|
||||
students = frappe.db.count("LMS Batch Enrollment", {"batch": batch_doc.name})
|
||||
if batch_doc.seat_count and students >= batch_doc.seat_count:
|
||||
frappe.throw(_("There are no seats available in this batch."))
|
||||
|
||||
|
||||
def create_enrollment(batch, payment_doc=None):
|
||||
new_student = frappe.new_doc("LMS Batch Enrollment")
|
||||
new_student.update(
|
||||
{
|
||||
"member": frappe.session.user,
|
||||
"batch": batch,
|
||||
}
|
||||
)
|
||||
|
||||
if payment_doc:
|
||||
new_student.update(
|
||||
{
|
||||
"member": frappe.session.user,
|
||||
"batch": batch,
|
||||
"payment": payment_doc.name,
|
||||
"source": payment_doc.source,
|
||||
}
|
||||
)
|
||||
|
||||
if payment_name:
|
||||
payment = frappe.db.get_value("LMS Payment", payment_name, ["name", "source"], as_dict=True)
|
||||
new_student.update(
|
||||
{
|
||||
"payment": payment.name,
|
||||
"source": payment.source,
|
||||
}
|
||||
)
|
||||
new_student.save()
|
||||
new_student.save()
|
||||
|
||||
|
||||
def update_certificate_purchase(course, payment_name):
|
||||
@@ -2159,8 +2205,8 @@ def get_program_details(program_name):
|
||||
|
||||
@frappe.whitelist()
|
||||
def enroll_in_program(program):
|
||||
if frappe.session.user == "Guest":
|
||||
frappe.throw(_("Please login to enroll in the program."))
|
||||
validate_program_enrollment(program)
|
||||
|
||||
if not frappe.db.exists("LMS Program Member", {"parent": program, "member": frappe.session.user}):
|
||||
program_member = frappe.new_doc("LMS Program Member")
|
||||
program_member.update(
|
||||
@@ -2174,8 +2220,17 @@ def enroll_in_program(program):
|
||||
program_member.save(ignore_permissions=True)
|
||||
|
||||
|
||||
def validate_program_enrollment(program):
|
||||
if frappe.session.user == "Guest":
|
||||
frappe.throw(_("Please login to enroll in the program."))
|
||||
|
||||
published = frappe.db.get_value("LMS Program", program, "published")
|
||||
if not published:
|
||||
frappe.throw(_("You cannot enroll in an unpublished program."))
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@rate_limit(limit=50, seconds=60 * 60)
|
||||
@rate_limit(limit=500, seconds=60 * 60)
|
||||
def get_batches(filters=None, start=0, order_by="start_date"):
|
||||
if not filters:
|
||||
filters = {}
|
||||
@@ -2289,7 +2344,7 @@ def get_palette(full_name):
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@rate_limit(limit=50, seconds=60 * 60)
|
||||
@rate_limit(limit=500, seconds=60 * 60)
|
||||
def get_related_courses(course):
|
||||
related_course_details = []
|
||||
related_courses = frappe.get_all("Related Courses", {"parent": course}, order_by="idx", pluck="course")
|
||||
@@ -2638,3 +2693,48 @@ def get_streak_info():
|
||||
"current_streak": current_streak,
|
||||
"longest_streak": longest_streak,
|
||||
}
|
||||
|
||||
|
||||
def validate_discussion_reply(doc, method):
|
||||
topic = frappe.db.get_value(
|
||||
"Discussion Topic", doc.topic, ["reference_doctype", "reference_docname"], as_dict=True
|
||||
)
|
||||
|
||||
if topic.reference_doctype == "Course Lesson":
|
||||
validate_course_access(topic.reference_docname)
|
||||
|
||||
elif topic.reference_doctype == "LMS Batch":
|
||||
validate_batch_access(topic.reference_docname)
|
||||
|
||||
|
||||
def validate_course_access(lesson):
|
||||
if not frappe.db.exists("Course Lesson", lesson):
|
||||
frappe.throw(_("The lesson does not exist."))
|
||||
|
||||
if has_moderator_role():
|
||||
return
|
||||
|
||||
if has_course_instructor_role():
|
||||
return
|
||||
|
||||
course = frappe.db.get_value("Course Lesson", lesson, "course")
|
||||
enrollment_exists = frappe.db.exists("LMS Enrollment", {"member": frappe.session.user, "course": course})
|
||||
if not enrollment_exists:
|
||||
frappe.throw(_("You do not have access to this course."))
|
||||
|
||||
|
||||
def validate_batch_access(batch):
|
||||
if not frappe.db.exists("LMS Batch", batch):
|
||||
frappe.throw(_("The batch does not exist."))
|
||||
|
||||
if has_moderator_role():
|
||||
return
|
||||
|
||||
if has_evaluator_role():
|
||||
return
|
||||
|
||||
enrollment_exists = frappe.db.exists(
|
||||
"LMS Batch Enrollment", {"member": frappe.session.user, "batch": batch}
|
||||
)
|
||||
if not enrollment_exists:
|
||||
frappe.throw(_("You do not have access to this batch."))
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
{
|
||||
"app": "lms",
|
||||
"charts": [
|
||||
{
|
||||
"chart_name": "Certification",
|
||||
"label": "Certification"
|
||||
},
|
||||
{
|
||||
"chart_name": "New Signups",
|
||||
"label": "Signups"
|
||||
@@ -10,7 +14,7 @@
|
||||
"label": "Enrollments"
|
||||
}
|
||||
],
|
||||
"content": "[{\"id\":\"jNO4sdKxHu\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Get Started</b></span>\",\"col\":12}},{\"id\":\"5s0qRBc4rY\",\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"/lms/courses\\\">Visit LMS Portal</a>\",\"col\":4}},{\"id\":\"lGMuNLpmv-\",\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"/lms/courses/new/edit\\\">Create a Course</a>\",\"col\":4}},{\"id\":\"3TVyc9AkPy\",\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"/app/web-page/new-web-page-1\\\">Setup a Home Page</a>\",\"col\":4}},{\"id\":\"9zcbqpu2gm\",\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"/app/lms-settings/LMS%20Settings\\\">LMS Settings</a>\",\"col\":4}},{\"id\":\"0ATmnKmXjc\",\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"https://docs.frappe.io/learning\\\">Documentation</a>\",\"col\":4}},{\"id\":\"C128a4abjX\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"5q4sPiv2ci\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Signups\",\"col\":6}},{\"id\":\"8NSaRaEV5u\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Enrollments\",\"col\":6}},{\"id\":\"kMuzko0uAU\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"iuvIOHmztI\",\"type\":\"header\",\"data\":{\"text\":\"<span style=\\\"font-size: 18px;\\\"><b>Statistics</b></span>\",\"col\":12}},{\"id\":\"l0VTd66Uy2\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Users\",\"col\":4}},{\"id\":\"wAWZin1KKk\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Course\",\"col\":4}},{\"id\":\"RLrIlFx0Hd\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Enrollments\",\"col\":4}},{\"id\":\"OuhWkhCQmq\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Course Completed\",\"col\":4}},{\"id\":\"3g8QmNqUXG\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Certificate\",\"col\":4}},{\"id\":\"EZsdsujs8N\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Evaluation\",\"col\":4}},{\"id\":\"s-nfsFQbGV\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"jeOBWBzHEa\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Master</b></span>\",\"col\":12}},{\"id\":\"sVhgfS5GIh\",\"type\":\"card\",\"data\":{\"card_name\":\"Course Data\",\"col\":4}},{\"id\":\"Iea0snm4Fg\",\"type\":\"card\",\"data\":{\"card_name\":\"Course Stats\",\"col\":4}},{\"id\":\"bZB7RqOl6a\",\"type\":\"card\",\"data\":{\"card_name\":\"Certification\",\"col\":4}}]",
|
||||
"content": "[{\"id\":\"jNO4sdKxHu\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Get Started</b></span>\",\"col\":12}},{\"id\":\"5s0qRBc4rY\",\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"/lms/courses\\\">Visit LMS Portal</a>\",\"col\":4}},{\"id\":\"lGMuNLpmv-\",\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"/lms/courses/new/edit\\\">Create a Course</a>\",\"col\":4}},{\"id\":\"3TVyc9AkPy\",\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"/app/web-page/new-web-page-1\\\">Setup a Home Page</a>\",\"col\":4}},{\"id\":\"9zcbqpu2gm\",\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"/app/lms-settings/LMS%20Settings\\\">LMS Settings</a>\",\"col\":4}},{\"id\":\"0ATmnKmXjc\",\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"https://docs.frappe.io/learning\\\">Documentation</a>\",\"col\":4}},{\"id\":\"C128a4abjX\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"5q4sPiv2ci\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Signups\",\"col\":6}},{\"id\":\"8NSaRaEV5u\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Enrollments\",\"col\":6}},{\"id\":\"_HkvT3xKVi\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Certification\",\"col\":12}},{\"id\":\"kMuzko0uAU\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"iuvIOHmztI\",\"type\":\"header\",\"data\":{\"text\":\"<span style=\\\"font-size: 18px;\\\"><b>Statistics</b></span>\",\"col\":12}},{\"id\":\"l0VTd66Uy2\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Users\",\"col\":4}},{\"id\":\"wAWZin1KKk\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Course\",\"col\":4}},{\"id\":\"RLrIlFx0Hd\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Enrollments\",\"col\":4}},{\"id\":\"OuhWkhCQmq\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Course Completed\",\"col\":4}},{\"id\":\"3g8QmNqUXG\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Certificate\",\"col\":4}},{\"id\":\"EZsdsujs8N\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Evaluation\",\"col\":4}},{\"id\":\"s-nfsFQbGV\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"jeOBWBzHEa\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Master</b></span>\",\"col\":12}},{\"id\":\"sVhgfS5GIh\",\"type\":\"card\",\"data\":{\"card_name\":\"Course Data\",\"col\":4}},{\"id\":\"Iea0snm4Fg\",\"type\":\"card\",\"data\":{\"card_name\":\"Course Stats\",\"col\":4}},{\"id\":\"bZB7RqOl6a\",\"type\":\"card\",\"data\":{\"card_name\":\"Certification\",\"col\":4}}]",
|
||||
"creation": "2021-10-21 17:20:01.358903",
|
||||
"custom_blocks": [],
|
||||
"docstatus": 0,
|
||||
@@ -146,8 +150,8 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2024-11-21 12:16:25.886431",
|
||||
"modified_by": "Administrator",
|
||||
"modified": "2025-12-08 13:23:09.718683",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS",
|
||||
"number_cards": [],
|
||||
@@ -215,4 +219,4 @@
|
||||
],
|
||||
"title": "LMS",
|
||||
"type": "Workspace"
|
||||
}
|
||||
}
|
||||
|
||||
788
lms/locale/ar.po
788
lms/locale/ar.po
File diff suppressed because it is too large
Load Diff
774
lms/locale/bs.po
774
lms/locale/bs.po
File diff suppressed because it is too large
Load Diff
746
lms/locale/cs.po
746
lms/locale/cs.po
File diff suppressed because it is too large
Load Diff
748
lms/locale/da.po
748
lms/locale/da.po
File diff suppressed because it is too large
Load Diff
750
lms/locale/de.po
750
lms/locale/de.po
File diff suppressed because it is too large
Load Diff
748
lms/locale/eo.po
748
lms/locale/eo.po
File diff suppressed because it is too large
Load Diff
796
lms/locale/es.po
796
lms/locale/es.po
File diff suppressed because it is too large
Load Diff
792
lms/locale/fa.po
792
lms/locale/fa.po
File diff suppressed because it is too large
Load Diff
784
lms/locale/fr.po
784
lms/locale/fr.po
File diff suppressed because it is too large
Load Diff
932
lms/locale/hr.po
932
lms/locale/hr.po
File diff suppressed because it is too large
Load Diff
778
lms/locale/hu.po
778
lms/locale/hu.po
File diff suppressed because it is too large
Load Diff
748
lms/locale/id.po
748
lms/locale/id.po
File diff suppressed because it is too large
Load Diff
752
lms/locale/it.po
752
lms/locale/it.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
746
lms/locale/my.po
746
lms/locale/my.po
File diff suppressed because it is too large
Load Diff
782
lms/locale/nb.po
782
lms/locale/nb.po
File diff suppressed because it is too large
Load Diff
746
lms/locale/nl.po
746
lms/locale/nl.po
File diff suppressed because it is too large
Load Diff
748
lms/locale/pl.po
748
lms/locale/pl.po
File diff suppressed because it is too large
Load Diff
746
lms/locale/pt.po
746
lms/locale/pt.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2266
lms/locale/ru.po
2266
lms/locale/ru.po
File diff suppressed because it is too large
Load Diff
8099
lms/locale/sl.po
Normal file
8099
lms/locale/sl.po
Normal file
File diff suppressed because it is too large
Load Diff
748
lms/locale/sr.po
748
lms/locale/sr.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
756
lms/locale/sv.po
756
lms/locale/sv.po
File diff suppressed because it is too large
Load Diff
746
lms/locale/ta.po
746
lms/locale/ta.po
File diff suppressed because it is too large
Load Diff
746
lms/locale/th.po
746
lms/locale/th.po
File diff suppressed because it is too large
Load Diff
784
lms/locale/tr.po
784
lms/locale/tr.po
File diff suppressed because it is too large
Load Diff
746
lms/locale/vi.po
746
lms/locale/vi.po
File diff suppressed because it is too large
Load Diff
748
lms/locale/zh.po
748
lms/locale/zh.po
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{% set onboarding_status = is_onboarding_complete() %}
|
||||
|
||||
{% if has_course_moderator_role() and not onboarding_status.is_onboarded %}
|
||||
{% if has_moderator_role() and not onboarding_status.is_onboarded %}
|
||||
<div class="onboarding-parent">
|
||||
<div class="container">
|
||||
<div class="onboarding-skip">{{ _("Skip") }}</div>
|
||||
|
||||
@@ -31,6 +31,7 @@ def get_boot():
|
||||
"frappe_version": frappe.__version__,
|
||||
"read_only_mode": frappe.flags.read_only,
|
||||
"csrf_token": frappe.sessions.get_csrf_token(),
|
||||
"site_name": frappe.local.site,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user