chore: fixed merge conflicts

This commit is contained in:
Jannat Patel
2025-12-08 14:54:14 +05:30
110 changed files with 26439 additions and 11302 deletions

View File

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

View File

@@ -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",

View File

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

View File

@@ -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:

View File

@@ -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([

View File

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

View File

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

View File

@@ -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",

View File

@@ -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"
}
}

View File

@@ -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"])

View File

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

View File

@@ -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."))

View File

@@ -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()

View File

@@ -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

View File

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

View File

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

View File

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

View File

@@ -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

View File

@@ -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",

View File

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

View File

@@ -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"]

View File

@@ -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",

View File

@@ -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."))

View File

@@ -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"
}
}