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

@@ -1 +1 @@
__version__ = "2.40.0"
__version__ = "2.41.0"

View File

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

View File

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

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

8099
lms/locale/sl.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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