Merge v2.46.0 into develop — resolve all conflicts
- yarn.lock, components.d.ts, ru.po: accepted upstream v2.46.0 - AppSidebar.vue: kept custom sidebar links (MyPoints, LeaderBoard, ChatGPT, MyChild, Profile) + adopted async watch from v2.46.0 - MobileLayout.vue: merged onMounted with sidebarSettings.reload + custom addSideBar() for role-based mobile links - EditProfile.vue: adopted new Dialog options structure (size only) - Courses/Courses.vue: unified tab values to lowercase (enrolled, upcoming, new, created, unpublished) - CourseDetail.vue (old): removed — logic migrated to Courses/CourseDetail.vue; transferred custom tag flex-wrap styling to CourseOverview.vue - LessonForm.vue: kept Rutube video support - App.vue: clean (no conflict markers) - user.py: merged imports + kept custom sign_up params (phone, user_role) - utils.py: kept render_html (Rutube), is_mentor, is_eligible_to_review; added type hints for get_course_progress from v2.46.0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1 +1 @@
|
||||
__version__ = "2.39.2"
|
||||
__version__ = "2.46.0"
|
||||
|
||||
104
lms/auth.py
Normal file
104
lms/auth.py
Normal file
@@ -0,0 +1,104 @@
|
||||
import json
|
||||
|
||||
import frappe
|
||||
|
||||
ALLOWED_PATHS = [
|
||||
"/api/method/ping",
|
||||
"/api/method/login",
|
||||
"/api/method/logout",
|
||||
"/api/method/frappe.core.doctype.communication.email.mark_email_as_seen",
|
||||
"/api/method/frappe.realtime.get_user_info",
|
||||
"/api/method/frappe.realtime.can_subscribe_doc",
|
||||
"/api/method/frappe.realtime.can_subscribe_doctype",
|
||||
"/api/method/frappe.realtime.has_permission",
|
||||
"/api/method/frappe.integrations.oauth2.authorize",
|
||||
"/api/method/frappe.integrations.oauth2.approve",
|
||||
"/api/method/frappe.integrations.oauth2.get_token",
|
||||
"/api/method/frappe.www.login.login_via_google",
|
||||
"/api/method/frappe.www.login.login_via_github",
|
||||
"/api/method/frappe.www.login.login_via_facebook",
|
||||
"/api/method/frappe.www.login.login_via_frappe",
|
||||
"/api/method/frappe.www.login.login_via_office365",
|
||||
"/api/method/frappe.www.login.login_via_salesforce",
|
||||
"/api/method/frappe.www.login.login_via_fairlogin",
|
||||
"/api/method/frappe.www.login.login_via_keycloak",
|
||||
"/api/method/frappe.www.login.custom",
|
||||
"/api/method/frappe.integrations.oauth2.openid_profile",
|
||||
"/api/method/frappe.website.doctype.web_page_view.web_page_view.make_view_log",
|
||||
"/api/method/upload_file",
|
||||
"/api/method/frappe.search.web_search",
|
||||
"/api/method/frappe.email.queue.unsubscribe",
|
||||
"/api/method/frappe.website.doctype.web_form.web_form.accept",
|
||||
"/api/method/frappe.core.doctype.user.user.test_password_strength",
|
||||
"/api/method/frappe.core.doctype.user.user.update_password",
|
||||
"/api/method/frappe.utils.telemetry.pulse.client.is_enabled",
|
||||
"/api/method/frappe.client.get_value",
|
||||
"/api/method/frappe.client.get_count",
|
||||
"/api/method/frappe.client.get",
|
||||
"/api/method/frappe.client.insert",
|
||||
"/api/method/frappe.client.set_value",
|
||||
"/api/method/frappe.client.delete",
|
||||
"/api/method/frappe.client.get_list",
|
||||
"/api/method/frappe.client.rename_doc",
|
||||
"/api/method/frappe.onboarding.get_onboarding_status",
|
||||
"/api/method/frappe.utils.print_format.download_pdf",
|
||||
"/api/method/frappe.desk.search.search_link",
|
||||
"/api/method/frappe.core.doctype.communication.email.make",
|
||||
"/api/method/frappe.core.doctype.user.user.reset_password",
|
||||
"/api/method/frappe.desk.doctype.notification_log.notification_log.mark_as_read",
|
||||
"/api/method/frappe.desk.doctype.notification_log.notification_log.mark_all_as_read",
|
||||
"/api/method/frappe.sessions.clear",
|
||||
]
|
||||
|
||||
|
||||
def authenticate():
|
||||
if not frappe.conf.get("block_endpoints"):
|
||||
return
|
||||
|
||||
if frappe.form_dict.cmd:
|
||||
path = f"/api/method/{frappe.form_dict.cmd}"
|
||||
else:
|
||||
path = frappe.request.path
|
||||
|
||||
user_type = frappe.db.get_value("User", frappe.session.user, "user_type")
|
||||
if user_type == "System User":
|
||||
return
|
||||
|
||||
if not path.startswith("/api/"):
|
||||
return
|
||||
|
||||
if path.startswith("/lms") or path.startswith("/api/method/lms."):
|
||||
return
|
||||
|
||||
if is_server_script_path(path):
|
||||
return
|
||||
|
||||
if is_custom_app_endpoint(path):
|
||||
return
|
||||
|
||||
if path in ALLOWED_PATHS:
|
||||
return
|
||||
frappe.throw(f"Access not allowed for this URL: {path}", frappe.PermissionError)
|
||||
|
||||
|
||||
def is_server_script_path(path):
|
||||
endpoint = path.split("/api/method/")[-1]
|
||||
if frappe.db.exists("Server Script", {"script_type": "API", "api_method": endpoint, "disabled": 0}):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_custom_app_endpoint(path):
|
||||
allowed_custom_endpoints = frappe.conf.get("allowed_custom_endpoints", [])
|
||||
|
||||
if isinstance(allowed_custom_endpoints, str):
|
||||
try:
|
||||
parsed = json.loads(allowed_custom_endpoints)
|
||||
allowed_custom_endpoints = parsed if isinstance(parsed, list) else [allowed_custom_endpoints]
|
||||
except Exception:
|
||||
allowed_custom_endpoints = [allowed_custom_endpoints]
|
||||
|
||||
for endpoint in allowed_custom_endpoints:
|
||||
if endpoint in path:
|
||||
return True
|
||||
return False
|
||||
106
lms/command_palette.py
Normal file
106
lms/command_palette.py
Normal file
@@ -0,0 +1,106 @@
|
||||
import frappe
|
||||
from frappe.utils import nowdate
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def search_sqlite(query: str):
|
||||
from lms.sqlite import LearningSearch, LearningSearchIndexMissingError
|
||||
|
||||
search = LearningSearch()
|
||||
|
||||
try:
|
||||
result = search.search(query)
|
||||
except LearningSearchIndexMissingError:
|
||||
return []
|
||||
|
||||
return prepare_search_results(result)
|
||||
|
||||
|
||||
def prepare_search_results(result: dict):
|
||||
groups = get_grouped_results(result)
|
||||
|
||||
out = []
|
||||
for key in groups:
|
||||
groups[key] = remove_duplicates(groups[key])
|
||||
groups[key].sort(key=lambda x: x.get("modified"), reverse=True)
|
||||
out.append({"title": key, "items": groups[key]})
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def get_grouped_results(result):
|
||||
roles = frappe.get_roles()
|
||||
groups = {}
|
||||
for r in result["results"]:
|
||||
doctype = r["doctype"]
|
||||
if doctype == "LMS Course" and can_access_course(r, roles):
|
||||
r["author_info"] = get_instructor_info(doctype, r)
|
||||
groups.setdefault("Courses", []).append(r)
|
||||
elif doctype == "LMS Batch" and can_access_batch(r, roles):
|
||||
r["author_info"] = get_instructor_info(doctype, r)
|
||||
groups.setdefault("Batches", []).append(r)
|
||||
elif doctype == "Job Opportunity" and can_access_job(r, roles):
|
||||
r["author_info"] = get_instructor_info(doctype, r)
|
||||
groups.setdefault("Job Opportunities", []).append(r)
|
||||
return groups
|
||||
|
||||
|
||||
def remove_duplicates(items):
|
||||
seen = set()
|
||||
unique_items = []
|
||||
for item in items:
|
||||
if item["name"] not in seen:
|
||||
seen.add(item["name"])
|
||||
unique_items.append(item)
|
||||
return unique_items
|
||||
|
||||
|
||||
def can_access_course(course, roles):
|
||||
if can_create_course(roles):
|
||||
return True
|
||||
elif course.get("published"):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def can_access_batch(batch, roles):
|
||||
if can_create_batch(roles):
|
||||
return True
|
||||
elif batch.get("published") and batch.get("start_date") >= nowdate():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def can_access_job(job, roles):
|
||||
if "Moderator" in roles:
|
||||
return True
|
||||
return job.get("status") == "Open"
|
||||
|
||||
|
||||
def can_create_course(roles):
|
||||
return "Course Creator" in roles or "Moderator" in roles
|
||||
|
||||
|
||||
def can_create_batch(roles):
|
||||
return "Batch Evaluator" in roles or "Moderator" in roles
|
||||
|
||||
|
||||
def get_instructor_info(doctype, record):
|
||||
instructors = frappe.get_all(
|
||||
"Course Instructor", filters={"parenttype": doctype, "parent": record.get("name")}, pluck="instructor"
|
||||
)
|
||||
instructor = record.get("author")
|
||||
if len(instructors):
|
||||
for ins in instructors:
|
||||
if ins.split("@")[0] in record.get("content"):
|
||||
instructor = ins
|
||||
break
|
||||
if not instructor:
|
||||
instructor = instructors[0]
|
||||
|
||||
return frappe.db.get_value(
|
||||
"User",
|
||||
instructor,
|
||||
["full_name", "email", "user_image", "username"],
|
||||
as_dict=True,
|
||||
)
|
||||
20
lms/desktop_icon/frappe_learning.json
Normal file
20
lms/desktop_icon/frappe_learning.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"app": "lms",
|
||||
"creation": "2025-12-15 14:31:50.704854",
|
||||
"docstatus": 0,
|
||||
"doctype": "Desktop Icon",
|
||||
"hidden": 0,
|
||||
"icon_type": "App",
|
||||
"idx": 0,
|
||||
"label": "Frappe Learning",
|
||||
"link": "/lms",
|
||||
"link_type": "External",
|
||||
"logo_url": "/assets/lms/frontend/learning.svg",
|
||||
"modified": "2025-12-15 14:31:50.704854",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Frappe Learning",
|
||||
"owner": "Administrator",
|
||||
"restrict_removal": 0,
|
||||
"roles": [],
|
||||
"standard": 1
|
||||
}
|
||||
@@ -238,8 +238,8 @@
|
||||
"dt": "User",
|
||||
"fetch_from": null,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "looking_for_job",
|
||||
"fieldtype": "Check",
|
||||
"fieldname": "open_to",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 0,
|
||||
"hide_border": 0,
|
||||
"hide_days": 0,
|
||||
@@ -250,19 +250,19 @@
|
||||
"in_list_view": 0,
|
||||
"in_preview": 0,
|
||||
"in_standard_filter": 0,
|
||||
"insert_after": "interest",
|
||||
"insert_after": "verify_terms",
|
||||
"is_system_generated": 1,
|
||||
"is_virtual": 0,
|
||||
"label": "I am looking for a job",
|
||||
"label": "Open to",
|
||||
"length": 0,
|
||||
"link_filters": null,
|
||||
"mandatory_depends_on": null,
|
||||
"modified": "2021-12-31 12:56:32.110405",
|
||||
"modified": "2025-12-24 12:56:32.110406",
|
||||
"module": null,
|
||||
"name": "User-looking_for_job",
|
||||
"name": "User-open_to",
|
||||
"no_copy": 0,
|
||||
"non_negative": 0,
|
||||
"options": null,
|
||||
"options": "\nWork\nHiring",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
@@ -406,7 +406,7 @@
|
||||
"dt": "User",
|
||||
"fetch_from": null,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "medium",
|
||||
"fieldname": "twitter",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"hide_border": 0,
|
||||
@@ -421,6 +421,62 @@
|
||||
"insert_after": "github",
|
||||
"is_system_generated": 1,
|
||||
"is_virtual": 0,
|
||||
"label": "Twitter ID",
|
||||
"length": 0,
|
||||
"link_filters": null,
|
||||
"mandatory_depends_on": null,
|
||||
"modified": "2025-12-15 14:46:55.834145",
|
||||
"module": null,
|
||||
"name": "User-twitter",
|
||||
"no_copy": 0,
|
||||
"non_negative": 0,
|
||||
"options": null,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": null,
|
||||
"read_only": 0,
|
||||
"read_only_depends_on": null,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"show_dashboard": 0,
|
||||
"sort_options": 0,
|
||||
"translatable": 1,
|
||||
"unique": 0,
|
||||
"width": null
|
||||
},
|
||||
{
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"collapsible_depends_on": null,
|
||||
"columns": 0,
|
||||
"default": null,
|
||||
"depends_on": null,
|
||||
"description": null,
|
||||
"docstatus": 0,
|
||||
"doctype": "Custom Field",
|
||||
"dt": "User",
|
||||
"fetch_from": null,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "medium",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"hide_border": 0,
|
||||
"hide_days": 0,
|
||||
"hide_seconds": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_preview": 0,
|
||||
"in_standard_filter": 0,
|
||||
"insert_after": "twitter",
|
||||
"is_system_generated": 1,
|
||||
"is_virtual": 0,
|
||||
"label": "Medium ID",
|
||||
"length": 0,
|
||||
"link_filters": null,
|
||||
|
||||
74
lms/hooks.py
74
lms/hooks.py
@@ -1,16 +1,24 @@
|
||||
import frappe
|
||||
|
||||
from . import __version__ as app_version
|
||||
|
||||
app_name = "frappe_lms"
|
||||
app_title = "Frappe LMS"
|
||||
app_title = "Learning"
|
||||
app_publisher = "Frappe"
|
||||
app_description = "Frappe LMS App"
|
||||
app_description = "Open Source Learning Management System built with Frappe Framework"
|
||||
app_icon_url = "/assets/lms/images/lms-logo.png"
|
||||
app_icon_title = "Learning"
|
||||
app_icon_route = "/lms"
|
||||
app_color = "grey"
|
||||
app_email = "jannat@frappe.io"
|
||||
app_license = "AGPL"
|
||||
|
||||
|
||||
def get_lms_path():
|
||||
return (frappe.conf.get("lms_path") or "lms").strip("/")
|
||||
|
||||
|
||||
app_icon_route = f"/{get_lms_path()}"
|
||||
|
||||
# Includes in <head>
|
||||
# ------------------
|
||||
|
||||
@@ -64,6 +72,9 @@ after_install = "lms.install.after_install"
|
||||
after_sync = "lms.install.after_sync"
|
||||
before_uninstall = "lms.install.before_uninstall"
|
||||
setup_wizard_requires = "assets/lms/js/setup_wizard.js"
|
||||
after_migrate = [
|
||||
"lms.sqlite.build_index_in_background",
|
||||
]
|
||||
|
||||
# Desk Notifications
|
||||
# ------------------
|
||||
@@ -75,13 +86,16 @@ setup_wizard_requires = "assets/lms/js/setup_wizard.js"
|
||||
# -----------
|
||||
# Permissions evaluated in scripted ways
|
||||
|
||||
# permission_query_conditions = {
|
||||
# "Event": "frappe.desk.doctype.event.event.get_permission_query_conditions",
|
||||
# }
|
||||
#
|
||||
# has_permission = {
|
||||
# "Event": "frappe.desk.doctype.event.event.has_permission",
|
||||
# }
|
||||
permission_query_conditions = {
|
||||
"LMS Certificate": "lms.lms.doctype.lms_certificate.lms_certificate.get_permission_query_conditions",
|
||||
}
|
||||
|
||||
has_permission = {
|
||||
"LMS Live Class": "lms.lms.doctype.lms_live_class.lms_live_class.has_permission",
|
||||
"LMS Batch": "lms.lms.doctype.lms_batch.lms_batch.has_permission",
|
||||
"LMS Program": "lms.lms.doctype.lms_program.lms_program.has_permission",
|
||||
"LMS Certificate": "lms.lms.doctype.lms_certificate.lms_certificate.has_permission",
|
||||
}
|
||||
|
||||
# DocType Class
|
||||
# ---------------
|
||||
@@ -101,7 +115,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",
|
||||
@@ -112,6 +129,9 @@ doc_events = {
|
||||
# Scheduled Tasks
|
||||
# ---------------
|
||||
scheduler_events = {
|
||||
"all": [
|
||||
"lms.sqlite.build_index_in_background",
|
||||
],
|
||||
"hourly": [
|
||||
"lms.lms.doctype.lms_certificate_request.lms_certificate_request.schedule_evals",
|
||||
"lms.lms.api.update_course_statistics",
|
||||
@@ -123,6 +143,7 @@ scheduler_events = {
|
||||
"lms.lms.doctype.lms_payment.lms_payment.send_payment_reminder",
|
||||
"lms.lms.doctype.lms_batch.lms_batch.send_batch_start_reminder",
|
||||
"lms.lms.doctype.lms_live_class.lms_live_class.send_live_class_reminder",
|
||||
"lms.lms.doctype.lms_course.lms_course.send_notification_for_published_courses",
|
||||
],
|
||||
}
|
||||
|
||||
@@ -153,7 +174,8 @@ override_whitelisted_methods = {
|
||||
|
||||
# Add all simple route rules here
|
||||
website_route_rules = [
|
||||
{"from_route": "/lms/<path:app_path>", "to_route": "lms"},
|
||||
{"from_route": f"/{get_lms_path()}/<path:app_path>", "to_route": "_lms"},
|
||||
{"from_route": f"/{get_lms_path()}", "to_route": "_lms"},
|
||||
{
|
||||
"from_route": "/courses/<course_name>/<certificate_id>",
|
||||
"to_route": "certificate",
|
||||
@@ -162,24 +184,25 @@ website_route_rules = [
|
||||
|
||||
website_redirects = [
|
||||
{"source": "/update-profile", "target": "/edit-profile"},
|
||||
{"source": "/courses", "target": "/lms/courses"},
|
||||
{"source": "/courses", "target": f"/{get_lms_path()}/courses"},
|
||||
{
|
||||
"source": r"^/courses/.*$",
|
||||
"target": "/lms/courses",
|
||||
"target": f"/{get_lms_path()}/courses",
|
||||
},
|
||||
{"source": "/batches", "target": "/lms/batches"},
|
||||
{"source": "/batches", "target": f"/{get_lms_path()}/batches"},
|
||||
{
|
||||
"source": r"/batches/(.*)",
|
||||
"target": "/lms/batches",
|
||||
"target": f"/{get_lms_path()}/batches",
|
||||
"match_with_query_string": True,
|
||||
},
|
||||
{"source": "/job-openings", "target": "/lms/job-openings"},
|
||||
{"source": "/job-openings", "target": f"/{get_lms_path()}/job-openings"},
|
||||
{
|
||||
"source": r"/job-openings/(.*)",
|
||||
"target": "/lms/job-openings",
|
||||
"target": f"/{get_lms_path()}/job-openings",
|
||||
"match_with_query_string": True,
|
||||
},
|
||||
{"source": "/statistics", "target": "/lms/statistics"},
|
||||
{"source": "/statistics", "target": f"/{get_lms_path()}/statistics"},
|
||||
{"source": "_lms", "target": f"/{get_lms_path()}"},
|
||||
]
|
||||
|
||||
update_website_context = [
|
||||
@@ -188,17 +211,20 @@ update_website_context = [
|
||||
|
||||
jinja = {
|
||||
"methods": [
|
||||
"lms.lms.utils.get_signup_optin_checks",
|
||||
"lms.lms.utils.get_tags",
|
||||
"lms.lms.utils.get_lesson_count",
|
||||
"lms.lms.utils.get_instructors",
|
||||
"lms.lms.utils.get_lesson_index",
|
||||
"lms.lms.utils.get_lesson_url",
|
||||
"lms.lms.utils.get_lms_route",
|
||||
"lms.lms.utils.is_instructor",
|
||||
"lms.lms.utils.get_palette",
|
||||
],
|
||||
"filters": [],
|
||||
}
|
||||
|
||||
extend_bootinfo = [
|
||||
"lms.lms.utils.extend_bootinfo",
|
||||
]
|
||||
## Specify the additional tabs to be included in the user profile page.
|
||||
## Each entry must be a subclass of lms.lms.plugins.ProfileTab
|
||||
# profile_tabs = []
|
||||
@@ -248,7 +274,11 @@ add_to_apps_screen = [
|
||||
"name": "lms",
|
||||
"logo": "/assets/lms/frontend/learning.svg",
|
||||
"title": "Learning",
|
||||
"route": "/lms",
|
||||
"route": f"/{get_lms_path()}",
|
||||
"has_permission": "lms.lms.api.check_app_permission",
|
||||
}
|
||||
]
|
||||
|
||||
sqlite_search = ["lms.sqlite.LearningSearch"]
|
||||
auth_hooks = ["lms.auth.authenticate"]
|
||||
require_type_annotated_api_methods = True
|
||||
|
||||
@@ -7,6 +7,7 @@ from lms.lms.api import give_discussions_permission
|
||||
def after_install():
|
||||
create_batch_source()
|
||||
give_discussions_permission()
|
||||
give_user_list_permission()
|
||||
|
||||
|
||||
def after_sync():
|
||||
@@ -27,13 +28,6 @@ def create_lms_roles():
|
||||
create_lms_student_role()
|
||||
|
||||
|
||||
def delete_lms_roles():
|
||||
roles = ["Course Creator", "Moderator"]
|
||||
for role in roles:
|
||||
if frappe.db.exists("Role", role):
|
||||
frappe.db.delete("Role", role)
|
||||
|
||||
|
||||
def create_course_creator_role():
|
||||
if frappe.db.exists("Role", "Course Creator"):
|
||||
frappe.db.set_value("Role", "Course Creator", "desk_access", 0)
|
||||
@@ -136,7 +130,7 @@ def delete_custom_fields():
|
||||
"medium",
|
||||
"linkedin",
|
||||
"profession",
|
||||
"looking_for_job",
|
||||
"open_to",
|
||||
"cover_image" "work_environment",
|
||||
"dream_companies",
|
||||
"career_preference_column",
|
||||
@@ -185,3 +179,36 @@ def give_lms_roles_to_admin():
|
||||
doc.parentfield = "roles"
|
||||
doc.role = role
|
||||
doc.save()
|
||||
|
||||
|
||||
def give_user_list_permission():
|
||||
doctype = "User"
|
||||
roles = ["Course Creator", "Moderator", "Batch Evaluator"]
|
||||
for role in roles:
|
||||
permlevel = 0
|
||||
create_role(doctype, role, permlevel)
|
||||
create_role(doctype, "System Manager", 1)
|
||||
|
||||
|
||||
def create_role(doctype, role, permlevel):
|
||||
if not frappe.db.exists("Custom DocPerm", {"parent": doctype, "role": role, "permlevel": permlevel}):
|
||||
doc = frappe.new_doc("Custom DocPerm")
|
||||
doc.update(
|
||||
{
|
||||
"doctype": "Custom DocPerm",
|
||||
"parent": doctype,
|
||||
"role": role,
|
||||
"read": 1,
|
||||
"write": 1 if role in ["Moderator", "System Manager"] else 0,
|
||||
"create": 1 if role == "Moderator" else 0,
|
||||
"permlevel": permlevel,
|
||||
}
|
||||
)
|
||||
doc.save()
|
||||
|
||||
|
||||
def delete_lms_roles():
|
||||
roles = ["Course Creator", "Moderator", "Batch Evaluator", "LMS Student"]
|
||||
for role in roles:
|
||||
if frappe.db.exists("Role", role):
|
||||
frappe.db.delete("Role", role)
|
||||
|
||||
@@ -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": "2026-02-19 14:26:14.027340",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "Job",
|
||||
"name": "Job Opportunity",
|
||||
"owner": "Administrator",
|
||||
@@ -156,24 +149,16 @@
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "LMS Student",
|
||||
"select": 1,
|
||||
"share": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "LMS Student",
|
||||
"select": 1,
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ class JobOpportunity(Document):
|
||||
self.company_logo = validate_image(self.company_logo)
|
||||
|
||||
def validate_urls(self):
|
||||
validate_url(self.company_website, True)
|
||||
validate_url(self.company_website, True, ["http", "https"])
|
||||
|
||||
def autoname(self):
|
||||
if not self.name:
|
||||
@@ -35,7 +35,7 @@ def update_job_openings():
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def report(job, reason):
|
||||
def report(job: str, reason: str):
|
||||
system_managers = get_system_managers(only_name=True)
|
||||
user = frappe.db.get_value("User", frappe.session.user, "full_name")
|
||||
subject = _("User {0} has reported the job post {1}").format(user, job)
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
// Copyright (c) 2022, Frappe and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Job Settings", {
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
});
|
||||
@@ -1,54 +0,0 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2022-02-07 12:01:41.422955",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"allow_posting",
|
||||
"title",
|
||||
"subtitle"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "allow_posting",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Job Posting From Website"
|
||||
},
|
||||
{
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Job Board Title"
|
||||
},
|
||||
{
|
||||
"fieldname": "subtitle",
|
||||
"fieldtype": "Data",
|
||||
"label": "Job Board Subtitle"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2022-02-11 15:56:38.958317",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Job",
|
||||
"name": "Job Settings",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
# Copyright (c) 2022, Frappe and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
|
||||
class TestJobSettings(unittest.TestCase):
|
||||
pass
|
||||
@@ -60,7 +60,7 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-02-20 20:10:46.943871",
|
||||
"modified": "2025-11-01 14:03:02.903943",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Job",
|
||||
"name": "LMS Job Application",
|
||||
@@ -82,6 +82,7 @@
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
@@ -90,8 +91,9 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "user"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@ class LMSJobApplication(Document):
|
||||
self.validate_duplicate()
|
||||
|
||||
def after_insert(self):
|
||||
job_owner = frappe.get_value("Job Opportunity", self.job, "owner")
|
||||
if job_owner:
|
||||
frappe.share.add_docshare("LMS Job Application", self.name, job_owner, read=1)
|
||||
|
||||
outgoing_email_account = frappe.get_cached_value(
|
||||
"Email Account", {"default_outgoing": 1, "enable_outgoing": 1}, "name"
|
||||
)
|
||||
@@ -33,7 +37,7 @@ class LMSJobApplication(Document):
|
||||
resume = frappe.get_doc(
|
||||
"File",
|
||||
{
|
||||
"file_name": self.resume,
|
||||
"file_url": self.resume,
|
||||
},
|
||||
)
|
||||
frappe.sendmail(
|
||||
|
||||
1318
lms/lms/api.py
1318
lms/lms/api.py
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
// Copyright (c) 2021, FOSS United and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Cohort", {
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
});
|
||||
@@ -1,133 +0,0 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "format:{course}/{slug}",
|
||||
"creation": "2021-11-19 11:45:31.016097",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"course",
|
||||
"title",
|
||||
"slug",
|
||||
"section_break_2",
|
||||
"instructor",
|
||||
"status",
|
||||
"column_break_4",
|
||||
"begin_date",
|
||||
"end_date",
|
||||
"duration",
|
||||
"section_break_8",
|
||||
"description",
|
||||
"pages"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Markdown Editor",
|
||||
"label": "Description"
|
||||
},
|
||||
{
|
||||
"fieldname": "instructor",
|
||||
"fieldtype": "Link",
|
||||
"label": "Instructor",
|
||||
"options": "User",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"label": "Status",
|
||||
"options": "Upcoming\nLive\nCompleted\nCancelled",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "begin_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Begin Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "end_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "End Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "duration",
|
||||
"fieldtype": "Data",
|
||||
"label": "Duration"
|
||||
},
|
||||
{
|
||||
"fieldname": "slug",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Slug",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_8",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_2",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Title",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "course",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Course",
|
||||
"options": "LMS Course",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "pages",
|
||||
"fieldtype": "Table",
|
||||
"label": "Pages",
|
||||
"options": "Cohort Web Page"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [
|
||||
{
|
||||
"group": "Links",
|
||||
"link_doctype": "Cohort Subgroup",
|
||||
"link_fieldname": "cohort"
|
||||
}
|
||||
],
|
||||
"modified": "2022-10-13 15:46:32.322926",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Cohort",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
# Copyright (c) 2021, FOSS United and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class Cohort(Document):
|
||||
def get_url(self):
|
||||
return f"{frappe.utils.get_url()}/lms/courses/{self.course}/cohorts/{self.slug}"
|
||||
|
||||
def get_subgroups(self, include_counts=False, sort_by=None):
|
||||
names = frappe.get_all("Cohort Subgroup", filters={"cohort": self.name}, pluck="name")
|
||||
subgroups = [frappe.get_cached_doc("Cohort Subgroup", name) for name in names]
|
||||
subgroups = sorted(subgroups, key=lambda sg: sg.title)
|
||||
|
||||
if include_counts:
|
||||
mentors = self._get_subgroup_counts("Cohort Mentor")
|
||||
students = self._get_subgroup_counts("LMS Enrollment")
|
||||
join_requests = self._get_subgroup_counts("Cohort Join Request", status="Pending")
|
||||
for s in subgroups:
|
||||
s.num_mentors = mentors.get(s.name, 0)
|
||||
s.num_students = students.get(s.name, 0)
|
||||
s.num_join_requests = join_requests.get(s.name, 0)
|
||||
|
||||
if sort_by:
|
||||
subgroups.sort(key=lambda sg: getattr(sg, sort_by), reverse=True)
|
||||
return subgroups
|
||||
|
||||
def _get_subgroup_counts(self, doctype, **kw):
|
||||
rows = frappe.get_all(
|
||||
doctype,
|
||||
filters={"cohort": self.name, **kw},
|
||||
fields=["subgroup", "count(*) as count"],
|
||||
group_by="subgroup",
|
||||
)
|
||||
return {row["subgroup"]: row["count"] for row in rows}
|
||||
|
||||
def _get_count(self, doctype, **kw):
|
||||
filters = {"cohort": self.name, **kw}
|
||||
return frappe.db.count(doctype, filters=filters)
|
||||
|
||||
def get_page_template(self, slug, scope=None):
|
||||
p = self.get_page(slug, scope=scope)
|
||||
return p and p.get_template_html()
|
||||
|
||||
def get_page(self, slug, scope=None):
|
||||
for p in self.pages:
|
||||
if p.slug == slug and scope in [p.scope, None]:
|
||||
return p
|
||||
|
||||
def get_pages(self, scope=None):
|
||||
return [p for p in self.pages if scope in [p.scope, None]]
|
||||
|
||||
def get_stats(self):
|
||||
return {
|
||||
"subgroups": self._get_count("Cohort Subgroup"),
|
||||
"mentors": self._get_count("Cohort Mentor"),
|
||||
"students": self._get_count("LMS Enrollment"),
|
||||
"join_requests": self._get_count("Cohort Join Request", status="Pending"),
|
||||
}
|
||||
|
||||
def get_subgroup(self, slug):
|
||||
q = dict(cohort=self.name, slug=slug)
|
||||
name = frappe.db.get_value("Cohort Subgroup", q, "name")
|
||||
return name and frappe.get_doc("Cohort Subgroup", name)
|
||||
|
||||
def get_mentor(self, email):
|
||||
q = dict(cohort=self.name, email=email)
|
||||
name = frappe.db.get_value("Cohort Mentor", q, "name")
|
||||
return name and frappe.get_doc("Cohort Mentor", name)
|
||||
|
||||
def is_mentor(self, email):
|
||||
q = {"doctype": "Cohort Mentor", "cohort": self.name, "email": email}
|
||||
return frappe.db.exists(q)
|
||||
|
||||
def is_admin(self, email):
|
||||
q = {"doctype": "Cohort Staff", "cohort": self.name, "email": email, "role": "Admin"}
|
||||
return frappe.db.exists(q)
|
||||
@@ -1,9 +0,0 @@
|
||||
# Copyright (c) 2021, FOSS United and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
|
||||
class TestCohort(unittest.TestCase):
|
||||
pass
|
||||
@@ -1,7 +0,0 @@
|
||||
// Copyright (c) 2021, FOSS United and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Cohort Join Request", {
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
});
|
||||
@@ -1,88 +0,0 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2021-11-19 16:27:41.716509",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"cohort",
|
||||
"email",
|
||||
"column_break_3",
|
||||
"subgroup",
|
||||
"status"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "cohort",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cohort",
|
||||
"options": "Cohort",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "subgroup",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Subgroup",
|
||||
"options": "Cohort Subgroup",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "email",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "E-Mail",
|
||||
"options": "User",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "Pending",
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Status",
|
||||
"options": "Pending\nAccepted\nRejected"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-09-29 17:08:18.950560",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Cohort Join Request",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "LMS Student",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
# Copyright (c) 2021, FOSS United and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class CohortJoinRequest(Document):
|
||||
def on_update(self):
|
||||
if self.status == "Accepted":
|
||||
self.ensure_student()
|
||||
|
||||
def ensure_student(self):
|
||||
# case 1 - user is already a member
|
||||
q = {
|
||||
"doctype": "LMS Enrollment",
|
||||
"cohort": self.cohort,
|
||||
"subgroup": self.subgroup,
|
||||
"member": self.email,
|
||||
"member_type": "Student",
|
||||
}
|
||||
if frappe.db.exists(q):
|
||||
return
|
||||
|
||||
# case 2 - user has signed up for this course, possibly not this cohort
|
||||
cohort = frappe.get_doc("Cohort", self.cohort)
|
||||
|
||||
q = {
|
||||
"doctype": "LMS Enrollment",
|
||||
"course": cohort.course,
|
||||
"member": self.email,
|
||||
"member_type": "Student",
|
||||
}
|
||||
name = frappe.db.exists(q)
|
||||
if name:
|
||||
doc = frappe.get_doc("LMS Enrollment", name)
|
||||
doc.cohort = self.cohort
|
||||
doc.subgroup = self.subgroup
|
||||
doc.save(ignore_permissions=True)
|
||||
else:
|
||||
# case 3 - user has not signed up for this course yet
|
||||
data = {
|
||||
"doctype": "LMS Enrollment",
|
||||
"course": cohort.course,
|
||||
"cohort": self.cohort,
|
||||
"subgroup": self.subgroup,
|
||||
"member": self.email,
|
||||
"member_type": "Student",
|
||||
"role": "Member",
|
||||
}
|
||||
doc = frappe.get_doc(data)
|
||||
doc.insert(ignore_permissions=True)
|
||||
@@ -1,9 +0,0 @@
|
||||
# Copyright (c) 2021, FOSS United and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
|
||||
class TestCohortJoinRequest(unittest.TestCase):
|
||||
pass
|
||||
@@ -1,7 +0,0 @@
|
||||
// Copyright (c) 2021, FOSS United and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Cohort Mentor", {
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
});
|
||||
@@ -1,73 +0,0 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2021-11-19 15:31:47.129156",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"cohort",
|
||||
"email",
|
||||
"subgroup",
|
||||
"course"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "cohort",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Cohort",
|
||||
"options": "Cohort",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "email",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "E-mail",
|
||||
"options": "User",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "subgroup",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Primary Subgroup",
|
||||
"options": "Cohort Subgroup",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "cohort.course",
|
||||
"fieldname": "course",
|
||||
"fieldtype": "Link",
|
||||
"label": "Course",
|
||||
"options": "LMS Course",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-11-29 16:32:33.235281",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Cohort Mentor",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
# Copyright (c) 2021, FOSS United and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class CohortMentor(Document):
|
||||
def get_subgroup(self):
|
||||
return frappe.get_doc("Cohort Subgroup", self.subgroup)
|
||||
|
||||
def get_user(self):
|
||||
return frappe.get_doc("User", self.email)
|
||||
@@ -1,9 +0,0 @@
|
||||
# Copyright (c) 2021, FOSS United and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
|
||||
class TestCohortMentor(unittest.TestCase):
|
||||
pass
|
||||
@@ -1,7 +0,0 @@
|
||||
// Copyright (c) 2021, FOSS United and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Cohort Staff", {
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
});
|
||||
@@ -1,77 +0,0 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2021-11-19 15:35:00.551949",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"cohort",
|
||||
"course",
|
||||
"column_break_3",
|
||||
"email",
|
||||
"role"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "cohort",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Cohort",
|
||||
"options": "Cohort",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "email",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "User",
|
||||
"options": "User",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "role",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Role",
|
||||
"options": "Admin\nManager\nStaff",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "cohort.course",
|
||||
"fieldname": "course",
|
||||
"fieldtype": "Link",
|
||||
"label": "Course",
|
||||
"options": "LMS Course",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-12-16 15:16:04.042372",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Cohort Staff",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
# Copyright (c) 2021, FOSS United and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class CohortStaff(Document):
|
||||
pass
|
||||
@@ -1,9 +0,0 @@
|
||||
# Copyright (c) 2021, FOSS United and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
|
||||
class TestCohortStaff(unittest.TestCase):
|
||||
pass
|
||||
@@ -1,7 +0,0 @@
|
||||
// Copyright (c) 2021, FOSS United and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Cohort Subgroup", {
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
});
|
||||
@@ -1,104 +0,0 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "format:{title} ({cohort})",
|
||||
"creation": "2021-11-19 11:50:27.312434",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"cohort",
|
||||
"slug",
|
||||
"title",
|
||||
"column_break_4",
|
||||
"invite_code",
|
||||
"course",
|
||||
"section_break_7",
|
||||
"description"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "cohort",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_preview": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Cohort",
|
||||
"options": "Cohort",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"in_preview": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Title",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Markdown Editor",
|
||||
"label": "Description"
|
||||
},
|
||||
{
|
||||
"fieldname": "invite_code",
|
||||
"fieldtype": "Data",
|
||||
"label": "Invite Code",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "slug",
|
||||
"fieldtype": "Data",
|
||||
"label": "Slug",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "cohort.course",
|
||||
"fieldname": "course",
|
||||
"fieldtype": "Link",
|
||||
"label": "Course",
|
||||
"options": "LMS Course",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_7",
|
||||
"fieldtype": "Section Break"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [
|
||||
{
|
||||
"group": "Links",
|
||||
"link_doctype": "Cohort Join Request",
|
||||
"link_fieldname": "subgroup"
|
||||
}
|
||||
],
|
||||
"modified": "2021-12-16 15:12:42.504883",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Cohort Subgroup",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
# Copyright (c) 2021, FOSS United and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import random_string
|
||||
|
||||
|
||||
class CohortSubgroup(Document):
|
||||
def before_save(self):
|
||||
if not self.invite_code:
|
||||
self.invite_code = random_string(8)
|
||||
|
||||
def get_url(self):
|
||||
cohort = frappe.get_doc("Cohort", self.cohort)
|
||||
return f"{frappe.utils.get_url()}/lms/courses/{self.course}/subgroups/{cohort.slug}/{self.slug}"
|
||||
|
||||
def get_invite_link(self):
|
||||
cohort = frappe.get_doc("Cohort", self.cohort)
|
||||
return f"{frappe.utils.get_url()}/lms/courses/{self.course}/join/{cohort.slug}/{self.slug}/{self.invite_code}"
|
||||
|
||||
def has_student(self, email):
|
||||
"""Check if given user is a student of this subgroup."""
|
||||
q = {"doctype": "LMS Enrollment", "subgroup": self.name, "member": email}
|
||||
return frappe.db.exists(q)
|
||||
|
||||
def has_join_request(self, email):
|
||||
"""Check if given user is a student of this subgroup."""
|
||||
q = {"doctype": "Cohort Join Request", "subgroup": self.name, "email": email}
|
||||
return frappe.db.exists(q)
|
||||
|
||||
def get_join_requests(self, status="Pending"):
|
||||
q = {"subgroup": self.name, "status": status}
|
||||
return frappe.get_all("Cohort Join Request", filters=q, fields=["*"], order_by="creation desc")
|
||||
|
||||
def get_mentors(self):
|
||||
emails = frappe.get_all(
|
||||
"Cohort Mentor", filters={"subgroup": self.name}, fields=["email"], pluck="email"
|
||||
)
|
||||
return self._get_users(emails)
|
||||
|
||||
def get_students(self):
|
||||
emails = frappe.get_all(
|
||||
"LMS Enrollment",
|
||||
filters={"subgroup": self.name},
|
||||
fields=["member"],
|
||||
pluck="member",
|
||||
page_length=1000,
|
||||
)
|
||||
return self._get_users(emails)
|
||||
|
||||
def _get_users(self, emails):
|
||||
users = [frappe.get_cached_doc("User", email) for email in emails]
|
||||
return sorted(users, key=lambda user: user.full_name)
|
||||
|
||||
def is_mentor(self, email):
|
||||
q = {"doctype": "Cohort Mentor", "subgroup": self.name, "email": email}
|
||||
return frappe.db.exists(q)
|
||||
|
||||
def is_manager(self, email):
|
||||
"""Returns True if the given user is a manager of this subgroup.
|
||||
|
||||
Mentors of the subgroup, admins of the Cohort are considered as managers.
|
||||
"""
|
||||
return self.is_mentor(email) or self.get_cohort().is_admin(email)
|
||||
|
||||
def get_cohort(self):
|
||||
return frappe.get_doc("Cohort", self.cohort)
|
||||
|
||||
def add_mentor(self, email):
|
||||
d = {
|
||||
"doctype": "Cohort Mentor",
|
||||
"subgroup": self.name,
|
||||
"cohort": self.cohort,
|
||||
"email": email,
|
||||
}
|
||||
if frappe.db.exists(d):
|
||||
return
|
||||
doc = frappe.get_doc(d)
|
||||
doc.insert(ignore_permissions=True)
|
||||
|
||||
|
||||
# def after_doctype_insert():
|
||||
# frappe.db.add_unique("Cohort Subgroup", ("cohort", "slug"))
|
||||
@@ -1,9 +0,0 @@
|
||||
# Copyright (c) 2021, FOSS United and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
|
||||
class TestCohortSubgroup(unittest.TestCase):
|
||||
pass
|
||||
@@ -1,64 +0,0 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2021-12-04 23:28:40.429867",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"slug",
|
||||
"title",
|
||||
"template",
|
||||
"scope",
|
||||
"required_role"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Title",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "template",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Template",
|
||||
"options": "Web Template",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "Cohort",
|
||||
"fieldname": "scope",
|
||||
"fieldtype": "Select",
|
||||
"label": "Scope",
|
||||
"options": "Cohort\nSubgroup"
|
||||
},
|
||||
{
|
||||
"default": "Public",
|
||||
"fieldname": "required_role",
|
||||
"fieldtype": "Select",
|
||||
"label": "Required Role",
|
||||
"options": "Public\nStudent\nMentor\nAdmin"
|
||||
},
|
||||
{
|
||||
"fieldname": "slug",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Slug",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-12-04 23:33:03.954128",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Cohort Web Page",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
# Copyright (c) 2021, Frappe and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class CohortWebPage(Document):
|
||||
def get_template_html(self):
|
||||
return frappe.get_doc("Web Template", self.template).template
|
||||
@@ -4,25 +4,12 @@
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
from lms.lms.utils import get_course_progress, get_lesson_count
|
||||
from lms.lms.utils import get_lesson_count
|
||||
|
||||
|
||||
class CourseChapter(Document):
|
||||
def on_update(self):
|
||||
self.recalculate_course_progress()
|
||||
self.update_lesson_count()
|
||||
frappe.enqueue(method=self.recalculate_course_progress, queue="short", timeout=300, is_async=True)
|
||||
|
||||
def recalculate_course_progress(self):
|
||||
"""Recalculate course progress if a new lesson is added or removed"""
|
||||
previous_lessons = self.get_doc_before_save() and self.get_doc_before_save().as_dict().lessons
|
||||
current_lessons = self.lessons
|
||||
|
||||
if previous_lessons and previous_lessons != current_lessons:
|
||||
enrolled_members = frappe.get_all("LMS Enrollment", {"course": self.course}, ["member", "name"])
|
||||
for enrollment in enrolled_members:
|
||||
new_progress = get_course_progress(self.course, enrollment.member)
|
||||
frappe.db.set_value("LMS Enrollment", enrollment.name, "progress", new_progress)
|
||||
|
||||
def update_lesson_count(self):
|
||||
"""Update lesson count in the course"""
|
||||
|
||||
@@ -1,9 +1,39 @@
|
||||
# Copyright (c) 2021, FOSS United and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
import frappe
|
||||
|
||||
from lms.lms.api import delete_chapter
|
||||
from lms.lms.test_helpers import BaseTestUtils
|
||||
|
||||
|
||||
class TestCourseChapter(unittest.TestCase):
|
||||
pass
|
||||
class TestCourseChapter(BaseTestUtils):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.instructor = self._create_user(
|
||||
"frappe@example.com", "Frappe", "Admin", ["Moderator", "Course Creator"]
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
return super().tearDown()
|
||||
|
||||
def test_chapter_deletion_and_renumbering(self):
|
||||
course = self._create_course(f"Test Renumbering Course {frappe.generate_hash()[:8]}")
|
||||
chapters = []
|
||||
|
||||
for i in range(1, 4):
|
||||
chapter = self._create_chapter(f"Chapter {i}", course.name)
|
||||
chapters.append(chapter)
|
||||
self._create_chapter_reference(course.name, chapter.name, i)
|
||||
self.assertEqual(self._get_chapter_index(course.name, chapter.name), i)
|
||||
|
||||
delete_chapter(chapters[1].name)
|
||||
|
||||
idx_ch1 = self._get_chapter_index(course.name, chapters[0].name)
|
||||
idx_ch3 = self._get_chapter_index(course.name, chapters[2].name)
|
||||
|
||||
self.assertEqual(idx_ch1, 1, "Chapter 1 index should remain 1")
|
||||
self.assertEqual(idx_ch3, 2, "Chapter 3 index should be renumbered to 2 after deleting Chapter 2")
|
||||
|
||||
def _get_chapter_index(self, course, chapter):
|
||||
return frappe.db.get_value("Chapter Reference", {"parent": course, "chapter": chapter}, "idx")
|
||||
|
||||
@@ -83,8 +83,8 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-04 12:04:11.007945",
|
||||
"modified_by": "Administrator",
|
||||
"modified": "2026-02-23 14:50:11.733278",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "Course Evaluator",
|
||||
"naming_rule": "By fieldname",
|
||||
@@ -125,11 +125,24 @@
|
||||
"role": "Batch Evaluator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Course Creator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "full_name"
|
||||
"title_field": "full_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ from datetime import datetime
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import get_time, getdate
|
||||
from frappe.utils import add_days, get_time, getdate, nowdate
|
||||
|
||||
from lms.lms.utils import get_evaluator
|
||||
|
||||
@@ -58,29 +58,125 @@ class CourseEvaluator(Document):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_schedule(course, date, batch=None):
|
||||
def get_schedule(course: str, batch: str = None):
|
||||
evaluator = get_evaluator(course, batch)
|
||||
day = datetime.strptime(date, "%Y-%m-%d").strftime("%A")
|
||||
start_date = nowdate()
|
||||
end_date = get_schedule_range_end_date(start_date, batch)
|
||||
all_slots = get_all_slots(evaluator, start_date, end_date)
|
||||
booked_slots = get_booked_slots(evaluator, start_date, end_date)
|
||||
all_slots = remove_booked_slots(all_slots, booked_slots)
|
||||
return all_slots
|
||||
|
||||
all_slots = frappe.get_all(
|
||||
|
||||
def get_all_slots(evaluator, start_date, end_date):
|
||||
schedule = get_evaluator_schedule(evaluator)
|
||||
unavailable_dates = get_unavailable_dates(evaluator)
|
||||
all_slots = []
|
||||
current_date = getdate(start_date)
|
||||
end_date = getdate(end_date)
|
||||
|
||||
while current_date <= end_date:
|
||||
if current_date in unavailable_dates:
|
||||
current_date = add_days(current_date, 1)
|
||||
continue
|
||||
day_of_week = current_date.strftime("%A")
|
||||
slots_for_day = [x for x in schedule if x.day == day_of_week]
|
||||
for slot in slots_for_day:
|
||||
all_slots.append(
|
||||
frappe._dict(
|
||||
{
|
||||
"day": day_of_week,
|
||||
"date": current_date,
|
||||
"start_time": slot.start_time,
|
||||
"end_time": slot.end_time,
|
||||
}
|
||||
)
|
||||
)
|
||||
current_date = add_days(current_date, 1)
|
||||
return all_slots
|
||||
|
||||
|
||||
def get_evaluator_schedule(evaluator):
|
||||
return frappe.get_all(
|
||||
"Evaluator Schedule",
|
||||
filters={
|
||||
"parent": evaluator,
|
||||
"day": day,
|
||||
},
|
||||
fields=["day", "start_time", "end_time"],
|
||||
order_by="start_time",
|
||||
)
|
||||
|
||||
booked_slots = frappe.get_all(
|
||||
|
||||
def get_booked_slots(evaluator, start_date, end_date):
|
||||
date = ["between", [start_date, end_date]]
|
||||
return frappe.get_all(
|
||||
"LMS Certificate Request",
|
||||
filters={"evaluator": evaluator, "date": date},
|
||||
fields=["start_time", "day"],
|
||||
filters={
|
||||
"evaluator": evaluator,
|
||||
"date": date,
|
||||
"status": ["!=", "Cancelled"],
|
||||
},
|
||||
fields=["start_time", "day", "date"],
|
||||
)
|
||||
|
||||
for slot in booked_slots:
|
||||
same_slot = [x for x in all_slots if x.start_time == slot.start_time and x.day == slot.day]
|
||||
if len(same_slot):
|
||||
all_slots.remove(same_slot[0])
|
||||
|
||||
return all_slots
|
||||
def remove_booked_slots(all_slots, booked_slots):
|
||||
slots_to_remove = []
|
||||
for slot in all_slots:
|
||||
for booked in booked_slots:
|
||||
if slot.date == booked.date and slot.start_time == booked.start_time:
|
||||
slots_to_remove.append(slot)
|
||||
|
||||
for slot in slots_to_remove:
|
||||
all_slots.remove(slot)
|
||||
|
||||
return group_slots_by_date(all_slots)
|
||||
|
||||
|
||||
def group_slots_by_date(all_slots):
|
||||
slots_by_date = []
|
||||
dates_included = set()
|
||||
for slot in all_slots:
|
||||
date_str = slot.get("date").strftime("%Y-%m-%d")
|
||||
if date_str not in dates_included:
|
||||
slots_by_date.append({"date": date_str, "day": slot.day, "slots": []})
|
||||
dates_included.add(date_str)
|
||||
|
||||
for date_slot in slots_by_date:
|
||||
if date_slot.get("date") == date_str:
|
||||
date_slot.get("slots").append(
|
||||
{
|
||||
"start_time": slot.get("start_time"),
|
||||
"end_time": slot.get("end_time"),
|
||||
}
|
||||
)
|
||||
return slots_by_date
|
||||
|
||||
|
||||
def get_evaluator_availability(evaluator):
|
||||
return frappe.db.get_value(
|
||||
"Course Evaluator", evaluator, ["unavailable_from", "unavailable_to"], as_dict=1
|
||||
)
|
||||
|
||||
|
||||
def get_unavailable_dates(evaluator):
|
||||
availability = get_evaluator_availability(evaluator)
|
||||
unavailable_dates = []
|
||||
if availability.unavailable_from and availability.unavailable_to:
|
||||
current_date = getdate(availability.unavailable_from)
|
||||
end_date = getdate(availability.unavailable_to)
|
||||
|
||||
while current_date <= end_date:
|
||||
unavailable_dates.append(current_date)
|
||||
current_date = add_days(current_date, 1)
|
||||
return unavailable_dates
|
||||
|
||||
|
||||
def get_schedule_range_end_date(start_date, batch=None):
|
||||
end_date = add_days(start_date, 60)
|
||||
if batch:
|
||||
batch_end_date = frappe.db.get_value("LMS Batch", batch, "evaluation_end_date")
|
||||
if batch_end_date and batch_end_date < getdate(end_date):
|
||||
end_date = getdate(batch_end_date)
|
||||
|
||||
return end_date
|
||||
|
||||
@@ -1,9 +1,65 @@
|
||||
# Copyright (c) 2022, Frappe and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests import UnitTestCase
|
||||
from frappe.utils import add_days, format_time, getdate
|
||||
|
||||
from lms.lms.doctype.course_evaluator.course_evaluator import get_schedule, get_schedule_range_end_date
|
||||
from lms.lms.test_helpers import BaseTestUtils
|
||||
|
||||
|
||||
class TestCourseEvaluator(UnitTestCase):
|
||||
pass
|
||||
class TestCourseEvaluator(BaseTestUtils):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.admin = self._create_user(
|
||||
"frappe@example.com", "Frappe", "Admin", ["Moderator", "Course Creator", "Batch Evaluator"]
|
||||
)
|
||||
self.course = self._create_course()
|
||||
self.evaluator = self._create_evaluator()
|
||||
self.batch = self._create_batch(self.course.name)
|
||||
|
||||
def test_schedule_day_and_time(self):
|
||||
schedule = get_schedule(self.batch.courses[0].course, self.batch.name)
|
||||
days = ["Monday", "Wednesday"]
|
||||
self.assertGreaterEqual(len(schedule), 14)
|
||||
for row in schedule:
|
||||
self.assertIn(row.get("day"), days)
|
||||
if row.get("day") == "Monday":
|
||||
for slot in row.get("slots"):
|
||||
self.assertEqual(format_time(slot.get("start_time"), "HH:mm:ss"), "10:00:00")
|
||||
self.assertEqual(format_time(slot.get("end_time"), "HH:mm:ss"), "12:00:00")
|
||||
if row.get("day") == "Wednesday":
|
||||
for slot in row.get("slots"):
|
||||
self.assertEqual(format_time(slot.get("start_time"), "HH:mm:ss"), "14:00:00")
|
||||
self.assertEqual(format_time(slot.get("end_time"), "HH:mm:ss"), "16:00:00")
|
||||
|
||||
def test_schedule_dates(self):
|
||||
schedule = get_schedule(self.batch.courses[0].course, self.batch.name)
|
||||
first_date = self.calculated_first_date_of_schedule()
|
||||
last_date = self.calculated_last_date_of_schedule()
|
||||
self.assertEqual(getdate(schedule[0].get("date")), first_date)
|
||||
self.assertEqual(getdate(schedule[-1].get("date")), last_date)
|
||||
|
||||
def calculated_first_date_of_schedule(self):
|
||||
today = getdate()
|
||||
offset_monday = (0 - today.weekday() + 7) % 7 # 0 for Monday
|
||||
offset_wednesday = (2 - today.weekday() + 7) % 7 # 2 for Wednesday
|
||||
if offset_monday < offset_wednesday:
|
||||
first_date = add_days(today, offset_monday)
|
||||
else:
|
||||
first_date = add_days(today, offset_wednesday)
|
||||
return first_date
|
||||
|
||||
def calculated_last_date_of_schedule(self):
|
||||
last_day = getdate(get_schedule_range_end_date(getdate(), self.batch.name))
|
||||
while last_day.weekday() not in (0, 2):
|
||||
last_day = add_days(last_day, -1)
|
||||
|
||||
return last_day
|
||||
|
||||
def test_unavailability_dates(self):
|
||||
unavailable_from = getdate(self.evaluator.unavailable_from)
|
||||
unavailable_to = getdate(self.evaluator.unavailable_to)
|
||||
schedule = get_schedule(self.batch.courses[0].course, self.batch.name)
|
||||
for row in schedule:
|
||||
schedule_date = getdate(row.get("date"))
|
||||
self.assertFalse(unavailable_from < schedule_date < unavailable_to)
|
||||
|
||||
@@ -68,7 +68,6 @@
|
||||
{
|
||||
"fieldname": "body",
|
||||
"fieldtype": "Markdown Editor",
|
||||
"ignore_xss_filter": 1,
|
||||
"label": "Body"
|
||||
},
|
||||
{
|
||||
@@ -168,7 +167,7 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-04-10 15:19:22.400932",
|
||||
"modified": "2026-02-20 13:49:25.599827",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Course Lesson",
|
||||
|
||||
@@ -9,15 +9,39 @@ from frappe.model.document import Document
|
||||
from frappe.realtime import get_website_room
|
||||
from frappe.utils.telemetry import capture
|
||||
|
||||
from lms.lms.utils import get_course_progress
|
||||
from lms.lms.utils import get_course_progress, recalculate_course_progress
|
||||
|
||||
from ...md import find_macros
|
||||
|
||||
|
||||
class CourseLesson(Document):
|
||||
def after_insert(self):
|
||||
self.validate_progress_recalculation()
|
||||
|
||||
def after_delete(self):
|
||||
self.validate_progress_recalculation()
|
||||
|
||||
def on_update(self):
|
||||
self.validate_quiz_id()
|
||||
|
||||
def validate_progress_recalculation(self):
|
||||
if not self.course or not self.chapter:
|
||||
return
|
||||
|
||||
enrollments = frappe.db.get_all(
|
||||
"LMS Enrollment",
|
||||
filters={"course": self.course},
|
||||
fields=["name", "member"],
|
||||
)
|
||||
if not len(enrollments):
|
||||
return
|
||||
|
||||
frappe.enqueue(method=self.recalculate_progress, queue="long", is_async=True, enrollments=enrollments)
|
||||
|
||||
def recalculate_progress(self, enrollments):
|
||||
for enrollment in enrollments:
|
||||
recalculate_course_progress(self.course, enrollment.member)
|
||||
|
||||
def validate_quiz_id(self):
|
||||
if self.quiz_id and not frappe.db.exists("LMS Quiz", self.quiz_id):
|
||||
frappe.throw(_("Invalid Quiz ID"))
|
||||
@@ -46,7 +70,7 @@ class CourseLesson(Document):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def save_progress(lesson, course, scorm_details=None):
|
||||
def save_progress(lesson: str, course: str, scorm_details: dict = None):
|
||||
"""
|
||||
Note: Pass the argument scorm_details as a dict if it is SCORM related save_progress
|
||||
"""
|
||||
@@ -103,7 +127,7 @@ def save_progress(lesson, course, scorm_details=None):
|
||||
)
|
||||
|
||||
progress = get_course_progress(course)
|
||||
capture_progress_for_analytics(progress, course)
|
||||
capture_progress_for_analytics()
|
||||
|
||||
# Had to get doc, as on_change doesn't trigger when you use set_value. The trigger is necessary for badge to get assigned.
|
||||
enrollment = frappe.get_doc("LMS Enrollment", membership)
|
||||
@@ -121,9 +145,8 @@ def save_progress(lesson, course, scorm_details=None):
|
||||
return progress
|
||||
|
||||
|
||||
def capture_progress_for_analytics(progress, course):
|
||||
if progress in [25, 50, 75, 100]:
|
||||
capture("course_progress", "lms", properties={"course": course, "progress": progress})
|
||||
def capture_progress_for_analytics():
|
||||
capture("course_progress", "lms")
|
||||
|
||||
|
||||
def get_quiz_progress(lesson):
|
||||
@@ -182,8 +205,3 @@ def get_assignment_progress(lesson):
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_lesson_info(chapter):
|
||||
return frappe.db.get_value("Course Chapter", chapter, "course")
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
// Copyright (c) 2021, Frappe and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Exercise Latest Submission", {
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
});
|
||||
@@ -1,166 +0,0 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2021-12-08 17:56:26.049675",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"exercise",
|
||||
"status",
|
||||
"batch_old",
|
||||
"column_break_4",
|
||||
"exercise_title",
|
||||
"course",
|
||||
"lesson",
|
||||
"section_break_8",
|
||||
"solution",
|
||||
"image",
|
||||
"test_results",
|
||||
"comments",
|
||||
"latest_submission",
|
||||
"member",
|
||||
"member_email",
|
||||
"member_cohort",
|
||||
"member_subgroup"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "exercise",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Exercise",
|
||||
"options": "LMS Exercise",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"label": "Status",
|
||||
"options": "Correct\nIncorrect"
|
||||
},
|
||||
{
|
||||
"fieldname": "batch_old",
|
||||
"fieldtype": "Link",
|
||||
"label": "Batch Old",
|
||||
"options": "LMS Batch Old"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fetch_from": "exercise.title",
|
||||
"fieldname": "exercise_title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Exercise Title",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "exercise.course",
|
||||
"fieldname": "course",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Course",
|
||||
"options": "LMS Course",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "exercise.lesson",
|
||||
"fieldname": "lesson",
|
||||
"fieldtype": "Link",
|
||||
"label": "Lesson",
|
||||
"options": "Course Lesson"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_8",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fetch_from": "latest_submission.solution",
|
||||
"fieldname": "solution",
|
||||
"fieldtype": "Code",
|
||||
"label": "Solution"
|
||||
},
|
||||
{
|
||||
"fetch_from": "latest_submission.image",
|
||||
"fieldname": "image",
|
||||
"fieldtype": "Code",
|
||||
"label": "Image",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "latest_submission.test_results",
|
||||
"fieldname": "test_results",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Test Results"
|
||||
},
|
||||
{
|
||||
"fieldname": "comments",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Comments"
|
||||
},
|
||||
{
|
||||
"fieldname": "latest_submission",
|
||||
"fieldtype": "Link",
|
||||
"label": "Latest Submission",
|
||||
"options": "Exercise Submission"
|
||||
},
|
||||
{
|
||||
"fieldname": "member",
|
||||
"fieldtype": "Link",
|
||||
"label": "Member",
|
||||
"options": "LMS Enrollment"
|
||||
},
|
||||
{
|
||||
"fetch_from": "member.member",
|
||||
"fieldname": "member_email",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Member Email",
|
||||
"options": "User",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "member.cohort",
|
||||
"fieldname": "member_cohort",
|
||||
"fieldtype": "Link",
|
||||
"label": "Member Cohort",
|
||||
"options": "Cohort",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "member.subgroup",
|
||||
"fieldname": "member_subgroup",
|
||||
"fieldtype": "Link",
|
||||
"label": "Member Subgroup",
|
||||
"options": "Cohort Subgroup",
|
||||
"search_index": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-12-08 22:58:46.312863",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Exercise Latest Submission",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
# Copyright (c) 2021, Frappe and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class ExerciseLatestSubmission(Document):
|
||||
pass
|
||||
@@ -1,9 +0,0 @@
|
||||
# Copyright (c) 2021, Frappe and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
|
||||
class TestExerciseLatestSubmission(unittest.TestCase):
|
||||
pass
|
||||
@@ -1,7 +0,0 @@
|
||||
// Copyright (c) 2021, FOSS United and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Exercise Submission", {
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
});
|
||||
@@ -1,126 +0,0 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2021-05-19 11:41:18.108316",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"exercise",
|
||||
"status",
|
||||
"batch_old",
|
||||
"column_break_4",
|
||||
"exercise_title",
|
||||
"course",
|
||||
"lesson",
|
||||
"section_break_8",
|
||||
"solution",
|
||||
"image",
|
||||
"test_results",
|
||||
"comments",
|
||||
"member"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "exercise",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Exercise",
|
||||
"options": "LMS Exercise"
|
||||
},
|
||||
{
|
||||
"fetch_from": "exercise.title",
|
||||
"fieldname": "exercise_title",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Exercise Title",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "exercise.course",
|
||||
"fieldname": "course",
|
||||
"fieldtype": "Link",
|
||||
"label": "Course",
|
||||
"options": "LMS Course",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "batch_old",
|
||||
"fieldtype": "Link",
|
||||
"label": "Batch Old",
|
||||
"options": "LMS Batch Old"
|
||||
},
|
||||
{
|
||||
"fetch_from": "exercise.lesson",
|
||||
"fieldname": "lesson",
|
||||
"fieldtype": "Link",
|
||||
"label": "Lesson",
|
||||
"options": "Course Lesson"
|
||||
},
|
||||
{
|
||||
"fieldname": "image",
|
||||
"fieldtype": "Code",
|
||||
"label": "Image",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"label": "Status",
|
||||
"options": "Correct\nIncorrect"
|
||||
},
|
||||
{
|
||||
"fieldname": "test_results",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Test Results"
|
||||
},
|
||||
{
|
||||
"fieldname": "comments",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Comments"
|
||||
},
|
||||
{
|
||||
"fieldname": "solution",
|
||||
"fieldtype": "Code",
|
||||
"in_list_view": 1,
|
||||
"label": "Solution"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_8",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "member",
|
||||
"fieldtype": "Link",
|
||||
"label": "Member",
|
||||
"options": "LMS Enrollment"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-12-08 22:25:05.809377",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Exercise Submission",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
# Copyright (c) 2021, FOSS United and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class ExerciseSubmission(Document):
|
||||
def on_update(self):
|
||||
self.update_latest_submission()
|
||||
|
||||
def update_latest_submission(self):
|
||||
names = frappe.get_all(
|
||||
"Exercise Latest Submission", {"exercise": self.exercise, "member": self.member}
|
||||
)
|
||||
if names:
|
||||
doc = frappe.get_doc("Exercise Latest Submission", names[0])
|
||||
doc.latest_submission = self.name
|
||||
doc.save(ignore_permissions=True)
|
||||
else:
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Exercise Latest Submission",
|
||||
"exercise": self.exercise,
|
||||
"member": self.member,
|
||||
"latest_submission": self.name,
|
||||
}
|
||||
)
|
||||
doc.insert(ignore_permissions=True)
|
||||
@@ -1,9 +0,0 @@
|
||||
# Copyright (c) 2021, FOSS United and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
|
||||
class TestExerciseSubmission(unittest.TestCase):
|
||||
pass
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"autoname": "format: ASG-{#####}",
|
||||
"creation": "2023-05-26 19:41:26.025081",
|
||||
@@ -9,10 +10,11 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"title",
|
||||
"question",
|
||||
"column_break_hmwv",
|
||||
"type",
|
||||
"grade_assignment",
|
||||
"course",
|
||||
"column_break_hmwv",
|
||||
"question",
|
||||
"section_break_sjti",
|
||||
"show_answer",
|
||||
"answer"
|
||||
@@ -68,12 +70,24 @@
|
||||
{
|
||||
"fieldname": "section_break_sjti",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "course",
|
||||
"fieldtype": "Link",
|
||||
"label": "Course",
|
||||
"options": "LMS Course"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-12-24 09:36:31.464508",
|
||||
"modified_by": "Administrator",
|
||||
"links": [
|
||||
{
|
||||
"link_doctype": "LMS Assignment Submission",
|
||||
"link_fieldname": "assignment"
|
||||
}
|
||||
],
|
||||
"modified": "2026-02-05 11:37:36.492016",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Assignment",
|
||||
"naming_rule": "Expression (old style)",
|
||||
@@ -96,6 +110,7 @@
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"import": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
@@ -111,11 +126,37 @@
|
||||
"report": 1,
|
||||
"role": "LMS Student",
|
||||
"share": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"import": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Course Creator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"import": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Batch Evaluator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"show_title_field_in_link": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title"
|
||||
}
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
||||
@@ -4,23 +4,8 @@
|
||||
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):
|
||||
pass
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def save_assignment(assignment, title, type, question):
|
||||
if not has_course_moderator_role() or not has_course_instructor_role():
|
||||
return
|
||||
|
||||
if assignment:
|
||||
doc = frappe.get_doc("LMS Assignment", assignment)
|
||||
else:
|
||||
doc = frappe.get_doc({"doctype": "LMS Assignment"})
|
||||
|
||||
doc.update({"title": title, "type": type, "question": question})
|
||||
doc.save(ignore_permissions=True)
|
||||
return doc.name
|
||||
|
||||
@@ -42,7 +42,8 @@
|
||||
"fieldname": "assignment",
|
||||
"fieldtype": "Link",
|
||||
"label": "Assignment",
|
||||
"options": "LMS Assignment"
|
||||
"options": "LMS Assignment",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "member",
|
||||
@@ -150,7 +151,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2025-07-14 10:24:23.526176",
|
||||
"modified": "2026-02-05 11:38:03.792865",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Assignment Submission",
|
||||
@@ -195,7 +196,6 @@
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
@@ -207,7 +207,6 @@
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
|
||||
@@ -7,6 +7,8 @@ from frappe.desk.doctype.notification_log.notification_log import make_notificat
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import validate_url
|
||||
|
||||
from lms.lms.utils import get_lms_route
|
||||
|
||||
|
||||
class LMSAssignmentSubmission(Document):
|
||||
def validate(self):
|
||||
@@ -28,7 +30,7 @@ class LMSAssignmentSubmission(Document):
|
||||
)
|
||||
|
||||
def validate_url(self):
|
||||
if self.type == "URL" and not validate_url(self.answer):
|
||||
if self.type == "URL" and not validate_url(self.answer, True, ["http", "https"]):
|
||||
frappe.throw(_("Please enter a valid URL."))
|
||||
|
||||
def validate_status(self):
|
||||
@@ -64,92 +66,15 @@ class LMSAssignmentSubmission(Document):
|
||||
def trigger_update_notification(self):
|
||||
notification = frappe._dict(
|
||||
{
|
||||
"subject": _("There has been an update on your submission for assignment {0}").format(
|
||||
self.assignment_title
|
||||
"subject": _("The instructor has left a comment on your assignment {0}").format(
|
||||
frappe.bold(self.assignment_title)
|
||||
),
|
||||
"email_content": self.comments,
|
||||
"document_type": self.doctype,
|
||||
"document_name": self.name,
|
||||
"for_user": self.owner,
|
||||
"from_user": self.evaluator,
|
||||
"type": "Alert",
|
||||
"link": f"/assignment-submission/{self.assignment}/{self.name}",
|
||||
"link": get_lms_route(f"assignment-submission/{self.assignment}/{self.name}"),
|
||||
}
|
||||
)
|
||||
make_notification_logs(notification, [self.member])
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def upload_assignment(
|
||||
assignment_attachment=None,
|
||||
answer=None,
|
||||
assignment=None,
|
||||
lesson=None,
|
||||
status="Not Graded",
|
||||
comments=None,
|
||||
submission=None,
|
||||
):
|
||||
if frappe.session.user == "Guest":
|
||||
return
|
||||
|
||||
assignment_details = frappe.db.get_value(
|
||||
"LMS Assignment", assignment, ["type", "grade_assignment"], as_dict=1
|
||||
)
|
||||
assignment_type = assignment_details.type
|
||||
|
||||
if assignment_type in ["URL", "Text"] and not answer:
|
||||
frappe.throw(_("Please enter the URL for assignment submission."))
|
||||
|
||||
if assignment_type == "File" and not assignment_attachment:
|
||||
frappe.throw(_("Please upload the assignment file."))
|
||||
|
||||
if assignment_type == "URL" and not validate_url(answer):
|
||||
frappe.throw(_("Please enter a valid URL."))
|
||||
|
||||
if submission:
|
||||
doc = frappe.get_doc("LMS Assignment Submission", submission)
|
||||
else:
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "LMS Assignment Submission",
|
||||
"assignment": assignment,
|
||||
"lesson": lesson,
|
||||
"member": frappe.session.user,
|
||||
"type": assignment_type,
|
||||
}
|
||||
)
|
||||
|
||||
doc.update(
|
||||
{
|
||||
"assignment_attachment": assignment_attachment,
|
||||
"status": "Not Applicable"
|
||||
if assignment_type == "Text" and not assignment_details.grade_assignment
|
||||
else status,
|
||||
"comments": comments,
|
||||
"answer": answer,
|
||||
}
|
||||
)
|
||||
doc.save(ignore_permissions=True)
|
||||
return doc.name
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_assignment(lesson):
|
||||
assignment = frappe.db.get_value(
|
||||
"LMS Assignment Submission",
|
||||
{"lesson": lesson, "member": frappe.session.user},
|
||||
["name", "lesson", "member", "assignment_attachment", "comments", "status"],
|
||||
as_dict=True,
|
||||
)
|
||||
assignment.file_name = frappe.db.get_value(
|
||||
"File", {"file_url": assignment.assignment_attachment}, "file_name"
|
||||
)
|
||||
return assignment
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def grade_assignment(name, result, comments):
|
||||
doc = frappe.get_doc("LMS Assignment Submission", name)
|
||||
doc.status = result
|
||||
doc.comments = comments
|
||||
doc.save(ignore_permissions=True)
|
||||
|
||||
@@ -5,7 +5,7 @@ frappe.ui.form.on("LMS Badge", {
|
||||
refresh: (frm) => {
|
||||
frm.events.set_field_options(frm);
|
||||
|
||||
if (frm.doc.event == "Auto Assign") {
|
||||
if (frm.doc.event == "Manual Assignment" && frm.doc.enabled) {
|
||||
add_assign_button(frm);
|
||||
}
|
||||
},
|
||||
@@ -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([
|
||||
@@ -51,11 +49,13 @@ const add_assign_button = (frm) => {
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_badge.lms_badge.assign_badge",
|
||||
args: {
|
||||
badge: frm.doc,
|
||||
badge_name: frm.doc.name,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (r.message) {
|
||||
frappe.msgprint(r.message);
|
||||
if (r.message == "success") {
|
||||
frappe.toast(__("Badge assigned successfully"));
|
||||
} else {
|
||||
frappe.toast(__("Failed to assign badge"));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:title",
|
||||
"creation": "2024-04-30 11:29:53.548647",
|
||||
@@ -51,14 +52,14 @@
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Event",
|
||||
"options": "New\nValue Change\nAuto Assign",
|
||||
"options": "New\nValue Change\nManual Assignment",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "condition",
|
||||
"fieldtype": "Code",
|
||||
"label": "Condition",
|
||||
"mandatory_depends_on": "eval:doc.event == \"Auto Assign\""
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.event == 'Value Change'",
|
||||
@@ -99,8 +100,8 @@
|
||||
"link_fieldname": "badge"
|
||||
}
|
||||
],
|
||||
"modified": "2025-07-04 13:02:19.048994",
|
||||
"modified_by": "Administrator",
|
||||
"modified": "2026-02-20 17:58:25.924109",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Badge",
|
||||
"naming_rule": "By fieldname",
|
||||
@@ -119,13 +120,17 @@
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"import": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "All",
|
||||
"share": 1
|
||||
"role": "Moderator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
|
||||
@@ -10,7 +10,7 @@ from frappe.model.document import Document
|
||||
|
||||
class LMSBadge(Document):
|
||||
def on_update(self):
|
||||
if self.event == "Auto Assign" and self.condition:
|
||||
if self.event == "Manual Assignment" and self.condition:
|
||||
try:
|
||||
json.loads(self.condition)
|
||||
except ValueError:
|
||||
@@ -54,6 +54,7 @@ def award(doc, member):
|
||||
}
|
||||
)
|
||||
assignment.save()
|
||||
return assignment.name
|
||||
|
||||
|
||||
def eval_condition(doc, condition):
|
||||
@@ -61,16 +62,30 @@ def eval_condition(doc, condition):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def assign_badge(badge):
|
||||
badge = frappe._dict(json.loads(badge))
|
||||
if not badge.event == "Auto Assign":
|
||||
def assign_badge(badge_name: str):
|
||||
assignments = []
|
||||
badge = frappe.db.get_value(
|
||||
"LMS Badge",
|
||||
badge_name,
|
||||
["name", "event", "reference_doctype", "condition", "user_field"],
|
||||
as_dict=True,
|
||||
)
|
||||
if not badge:
|
||||
frappe.throw(_("Badge {0} not found").format(badge_name), frappe.DoesNotExistError)
|
||||
|
||||
if not badge.event == "Manual Assignment":
|
||||
return
|
||||
|
||||
fields = ["name"]
|
||||
fields.append(badge.user_field)
|
||||
list = frappe.get_all(badge.reference_doctype, filters=badge.condition, fields=fields)
|
||||
for doc in list:
|
||||
award(badge, doc.get(badge.user_field))
|
||||
docs = frappe.get_all(badge.reference_doctype, filters=json.loads(badge.condition), fields=fields)
|
||||
|
||||
for doc in docs:
|
||||
assignment_name = award(badge, doc.get(badge.user_field))
|
||||
if assignment_name:
|
||||
assignments.append(assignment_name)
|
||||
|
||||
return "success" if assignments else "failed"
|
||||
|
||||
|
||||
def process_badges(doc, state):
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-11-06 11:38:35.903520",
|
||||
"modified": "2026-02-19 15:06:08.389081",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Badge Assignment",
|
||||
@@ -116,25 +116,9 @@
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "LMS Student",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "LMS Student",
|
||||
"share": 1
|
||||
"role": "LMS Student"
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
@@ -166,5 +150,6 @@
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "member"
|
||||
"title_field": "member",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
||||
@@ -1,9 +1,66 @@
|
||||
# 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_duplicate_badge_assignment()
|
||||
self.validate_badge_criteria()
|
||||
self.validate_owner()
|
||||
|
||||
def validate_owner(self):
|
||||
event = frappe.db.get_value("LMS Badge", self.badge, "event")
|
||||
if event == "Manual Assignment":
|
||||
roles = frappe.get_roles(frappe.session.user)
|
||||
admins = ["Moderator", "Course Creator", "Batch Evaluator"]
|
||||
if not any(role in roles for role in admins):
|
||||
frappe.throw(_("You must be an Admin 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 not badge_details:
|
||||
return
|
||||
|
||||
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))
|
||||
|
||||
@@ -3,14 +3,6 @@
|
||||
|
||||
frappe.ui.form.on("LMS Batch", {
|
||||
onload: function (frm) {
|
||||
frm.set_query("student", "students", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
ignore_user_type: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("reference_doctype", "timetable", function () {
|
||||
let doctypes = ["Course Lesson", "LMS Quiz", "LMS Assignment"];
|
||||
return {
|
||||
@@ -20,6 +12,14 @@ frappe.ui.form.on("LMS Batch", {
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("course", "courses", function () {
|
||||
return {
|
||||
filters: {
|
||||
published: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("assessment_type", "assessment", function () {
|
||||
let doctypes = ["LMS Quiz", "LMS Assignment"];
|
||||
return {
|
||||
@@ -48,8 +48,9 @@ frappe.ui.form.on("LMS Batch", {
|
||||
},
|
||||
|
||||
refresh: (frm) => {
|
||||
const lmsPath = frappe.boot.lms_path || "lms";
|
||||
frm.add_web_link(
|
||||
`/lms/batches/details/${frm.doc.name}`,
|
||||
`/${lmsPath}/batches/${frm.doc.name}`,
|
||||
"See on website"
|
||||
);
|
||||
},
|
||||
|
||||
@@ -9,37 +9,43 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"section_break_earo",
|
||||
"published",
|
||||
"title",
|
||||
"start_date",
|
||||
"end_date",
|
||||
"column_break_4",
|
||||
"allow_self_enrollment",
|
||||
"start_time",
|
||||
"end_time",
|
||||
"timezone",
|
||||
"section_break_wuxt",
|
||||
"seat_count",
|
||||
"column_break_uamg",
|
||||
"category",
|
||||
"section_break_cssv",
|
||||
"published",
|
||||
"evaluation",
|
||||
"evaluation_end_date",
|
||||
"column_break_wfkz",
|
||||
"allow_self_enrollment",
|
||||
"column_break_vnrp",
|
||||
"certification",
|
||||
"section_break_6",
|
||||
"description",
|
||||
"column_break_hlqw",
|
||||
"instructors",
|
||||
"zoom_account",
|
||||
"column_break_hlqw",
|
||||
"batch_details",
|
||||
"section_break_rgfj",
|
||||
"medium",
|
||||
"category",
|
||||
"confirmation_email_template",
|
||||
"column_break_flwy",
|
||||
"seat_count",
|
||||
"evaluation_end_date",
|
||||
"zoom_account",
|
||||
"notification_sent",
|
||||
"section_break_jedp",
|
||||
"video_link",
|
||||
"column_break_kpct",
|
||||
"meta_image",
|
||||
"section_break_khcn",
|
||||
"batch_details",
|
||||
"batch_details_raw",
|
||||
"section_break_jgji",
|
||||
"courses",
|
||||
"section_break_khcn",
|
||||
"batch_details_raw",
|
||||
"assessment_tab",
|
||||
"assessment",
|
||||
"schedule_tab",
|
||||
@@ -293,6 +299,7 @@
|
||||
"label": "Allow accessing future dates"
|
||||
},
|
||||
{
|
||||
"depends_on": "evaluation",
|
||||
"fieldname": "evaluation_end_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Evaluation End Date"
|
||||
@@ -337,10 +344,6 @@
|
||||
"fieldname": "column_break_wfkz",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_vnrp",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "certification",
|
||||
@@ -354,13 +357,48 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_cssv",
|
||||
"fieldtype": "Section Break"
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Certification"
|
||||
},
|
||||
{
|
||||
"fieldname": "zoom_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Zoom Account",
|
||||
"options": "LMS Zoom Settings"
|
||||
},
|
||||
{
|
||||
"fieldname": "video_link",
|
||||
"fieldtype": "Attach",
|
||||
"label": "Preview Video"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "notification_sent",
|
||||
"fieldtype": "Check",
|
||||
"label": "Notification Sent",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_jedp",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_kpct",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "evaluation",
|
||||
"fieldtype": "Check",
|
||||
"label": "Evaluation"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_wuxt",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_uamg",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -377,9 +415,13 @@
|
||||
{
|
||||
"link_doctype": "LMS Certificate",
|
||||
"link_fieldname": "batch_name"
|
||||
},
|
||||
{
|
||||
"link_doctype": "LMS Payment",
|
||||
"link_fieldname": "payment_for_document"
|
||||
}
|
||||
],
|
||||
"modified": "2025-05-26 15:30:55.083507",
|
||||
"modified": "2026-02-13 14:23:51.913875",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Batch",
|
||||
@@ -422,13 +464,8 @@
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "LMS Student",
|
||||
"share": 1
|
||||
"role": "LMS Student"
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
|
||||
@@ -8,15 +8,19 @@ from datetime import timedelta
|
||||
import frappe
|
||||
import requests
|
||||
from frappe import _
|
||||
from frappe.desk.doctype.notification_log.notification_log import make_notification_logs
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import add_days, cint, format_datetime, get_time, nowdate
|
||||
|
||||
from lms.lms.utils import (
|
||||
generate_slug,
|
||||
get_assignment_details,
|
||||
get_instructors,
|
||||
get_lesson_index,
|
||||
get_lesson_url,
|
||||
get_lms_route,
|
||||
get_quiz_details,
|
||||
guest_access_allowed,
|
||||
update_payment_record,
|
||||
)
|
||||
|
||||
@@ -30,10 +34,13 @@ class LMSBatch(Document):
|
||||
self.validate_payments_app()
|
||||
self.validate_amount_and_currency()
|
||||
self.validate_duplicate_assessments()
|
||||
self.validate_membership()
|
||||
self.validate_timetable()
|
||||
self.validate_evaluation_end_date()
|
||||
|
||||
def on_update(self):
|
||||
if self.has_value_changed("published") and self.published:
|
||||
frappe.enqueue(send_notification_for_published_batch, batch=self)
|
||||
|
||||
def autoname(self):
|
||||
if not self.name:
|
||||
self.name = generate_slug(self.title, "LMS Batch")
|
||||
@@ -82,16 +89,6 @@ class LMSBatch(Document):
|
||||
if self.evaluation_end_date and self.evaluation_end_date < self.end_date:
|
||||
frappe.throw(_("Evaluation end date cannot be less than the batch end date."))
|
||||
|
||||
def validate_membership(self):
|
||||
members = frappe.get_all("LMS Batch Enrollment", {"batch": self.name}, pluck="member")
|
||||
for course in self.courses:
|
||||
for member in members:
|
||||
if not frappe.db.exists("LMS Enrollment", {"course": course.course, "member": member}):
|
||||
enrollment = frappe.new_doc("LMS Enrollment")
|
||||
enrollment.course = course.course
|
||||
enrollment.member = member
|
||||
enrollment.save()
|
||||
|
||||
def validate_seats_left(self):
|
||||
if cint(self.seat_count) < 0:
|
||||
frappe.throw(_("Seat count cannot be negative."))
|
||||
@@ -134,18 +131,93 @@ class LMSBatch(Document):
|
||||
update_payment_record("LMS Batch", self.name)
|
||||
|
||||
|
||||
def send_notification_for_published_batch(batch):
|
||||
send_notification = frappe.db.get_single_value("LMS Settings", "send_notification_for_published_batches")
|
||||
if not send_notification:
|
||||
return
|
||||
|
||||
if not batch.published:
|
||||
return
|
||||
if batch.notification_sent:
|
||||
return
|
||||
|
||||
if send_notification == "Email":
|
||||
send_email_notification_for_published_batch(batch)
|
||||
else:
|
||||
send_system_notification_for_published_batch(batch)
|
||||
|
||||
|
||||
def send_email_notification_for_published_batch(batch):
|
||||
brand_name = frappe.db.get_single_value("Website Settings", "app_name")
|
||||
brand_logo = frappe.db.get_single_value("Website Settings", "banner_image")
|
||||
subject = _("A new course has been published on {0}").format(brand_name)
|
||||
template = "published_batch_notification"
|
||||
students = frappe.get_all("User", {"enabled": 1}, pluck="name")
|
||||
instructors = get_instructors("LMS Batch", batch.name)
|
||||
|
||||
args = {
|
||||
"brand_logo": brand_logo,
|
||||
"brand_name": brand_name,
|
||||
"title": batch.title,
|
||||
"short_introduction": batch.description,
|
||||
"start_date": batch.start_date,
|
||||
"end_date": batch.end_date,
|
||||
"start_time": batch.start_time,
|
||||
"medium": batch.medium,
|
||||
"timezone": batch.timezone,
|
||||
"instructors": instructors,
|
||||
"batch_url": frappe.utils.get_url(get_lms_route(f"batches/{batch.name}")),
|
||||
}
|
||||
|
||||
frappe.sendmail(
|
||||
recipients=instructors,
|
||||
bcc=students,
|
||||
subject=subject,
|
||||
template=template,
|
||||
args=args,
|
||||
)
|
||||
frappe.db.set_value("LMS Batch", batch.name, "notification_sent", 1)
|
||||
|
||||
|
||||
def send_system_notification_for_published_batch(batch):
|
||||
students = frappe.get_all("User", {"enabled": 1}, pluck="name")
|
||||
instructors = frappe.get_all("Course Instructor", {"parent": batch.name}, pluck="instructor")
|
||||
instructor_name = frappe.db.get_value("User", instructors[0], "full_name")
|
||||
notification = frappe._dict(
|
||||
{
|
||||
"subject": _("{0} has published a new batch {1}").format(
|
||||
frappe.bold(instructor_name), frappe.bold(batch.title)
|
||||
),
|
||||
"email_content": _(
|
||||
"A new batch '{0}' has been published that might interest you. Check it out!"
|
||||
).format(batch.title),
|
||||
"document_type": "LMS Batch",
|
||||
"document_name": batch.name,
|
||||
"from_user": instructors[0] if instructors else None,
|
||||
"type": "Alert",
|
||||
"link": get_lms_route(f"batches/{batch.name}"),
|
||||
}
|
||||
)
|
||||
make_notification_logs(notification, students)
|
||||
frappe.db.set_value("LMS Batch", batch.name, "notification_sent", 1)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_live_class(
|
||||
batch_name,
|
||||
zoom_account,
|
||||
title,
|
||||
duration,
|
||||
date,
|
||||
time,
|
||||
timezone,
|
||||
auto_recording,
|
||||
description=None,
|
||||
batch_name: str,
|
||||
zoom_account: str,
|
||||
title: str,
|
||||
duration: int,
|
||||
date: str,
|
||||
time: str,
|
||||
timezone: str,
|
||||
auto_recording: str,
|
||||
description: str = None,
|
||||
):
|
||||
roles = frappe.get_roles()
|
||||
if not any(role in roles for role in ["Moderator", "Batch Evaluator"]):
|
||||
frappe.throw(_("You do not have permission to create a live class."))
|
||||
|
||||
payload = {
|
||||
"topic": title,
|
||||
"start_time": format_datetime(f"{date} {time}", "yyyy-MM-ddTHH:mm:ssZ"),
|
||||
@@ -213,7 +285,7 @@ def authenticate(zoom_account):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_batch_timetable(batch):
|
||||
def get_batch_timetable(batch: str):
|
||||
timetable = frappe.get_all(
|
||||
"LMS Batch Timetable",
|
||||
filters={"parent": batch},
|
||||
@@ -324,3 +396,26 @@ def send_mail(batch, student):
|
||||
args=args,
|
||||
header=[_(f"Batch Start Reminder: {batch.title}"), "orange"],
|
||||
)
|
||||
|
||||
|
||||
def has_permission(doc, ptype="read", user=None):
|
||||
user = user or frappe.session.user
|
||||
if user == "Guest" and not guest_access_allowed():
|
||||
return False
|
||||
|
||||
roles = frappe.get_roles(user)
|
||||
if "Moderator" in roles or "Batch Evaluator" in roles:
|
||||
return True
|
||||
|
||||
if ptype not in ("read", "select", "print"):
|
||||
return False
|
||||
|
||||
is_enrolled = frappe.db.exists("LMS Batch Enrollment", {"batch": doc.name, "member": user})
|
||||
if is_enrolled:
|
||||
return True
|
||||
|
||||
is_batch_published = frappe.db.get_value("LMS Batch", doc.name, "published")
|
||||
if is_batch_published:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-02-10 11:17:12.462368",
|
||||
"doctype": "DocType",
|
||||
@@ -8,6 +9,7 @@
|
||||
"member",
|
||||
"member_name",
|
||||
"member_username",
|
||||
"member_image",
|
||||
"column_break_sjzm",
|
||||
"batch",
|
||||
"payment",
|
||||
@@ -69,12 +71,18 @@
|
||||
"label": "Batch",
|
||||
"options": "LMS Batch",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "member.user_image",
|
||||
"fieldname": "member_image",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "Member Image"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-02-11 10:39:57.259526",
|
||||
"modified_by": "Administrator",
|
||||
"modified": "2026-02-10 16:07:28.315982",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Batch Enrollment",
|
||||
"owner": "Administrator",
|
||||
@@ -96,6 +104,7 @@
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"import": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
@@ -105,18 +114,27 @@
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"if_owner": 1,
|
||||
"read": 1,
|
||||
"role": "LMS Student"
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 1,
|
||||
"import": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "LMS Student",
|
||||
"share": 1
|
||||
"role": "Batch Evaluator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "member_name"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,51 @@ class LMSBatchEnrollment(Document):
|
||||
self.add_member_to_live_class()
|
||||
|
||||
def validate(self):
|
||||
self.validate_owner()
|
||||
self.validate_duplicate_members()
|
||||
self.validate_payment()
|
||||
self.validate_self_enrollment()
|
||||
self.validate_seat_availability()
|
||||
self.validate_course_enrollment()
|
||||
|
||||
def validate_owner(self):
|
||||
if self.owner == self.member:
|
||||
return
|
||||
|
||||
roles = frappe.get_roles()
|
||||
if "Moderator" not in roles and "Batch Evaluator" not in roles:
|
||||
frappe.throw(_("You must be a Moderator or Batch Evaluator to enroll users in a batch."))
|
||||
|
||||
def validate_payment(self):
|
||||
paid_batch = frappe.db.get_value("LMS Batch", self.batch, "paid_batch")
|
||||
if paid_batch:
|
||||
payment = frappe.db.exists(
|
||||
"LMS Payment",
|
||||
{
|
||||
"payment_for_document_type": "LMS Batch",
|
||||
"payment_for_document": self.batch,
|
||||
"member": self.member,
|
||||
"payment_received": True,
|
||||
},
|
||||
)
|
||||
if not payment:
|
||||
frappe.throw(_("Payment is required to enroll in this batch."))
|
||||
else:
|
||||
self.payment = payment
|
||||
|
||||
def validate_self_enrollment(self):
|
||||
batch_details = frappe.db.get_value(
|
||||
"LMS Batch", self.batch, ["allow_self_enrollment", "paid_batch"], as_dict=True
|
||||
)
|
||||
if batch_details.paid_batch:
|
||||
return
|
||||
if not batch_details.allow_self_enrollment and not self.is_admin():
|
||||
frappe.throw(_("Enrollment in this batch is restricted. Please contact the Administrator."))
|
||||
|
||||
def is_admin(self):
|
||||
roles = frappe.get_roles(frappe.session.user)
|
||||
return "Course Creator" in roles or "Moderator" in roles or "Batch Evaluator" in roles
|
||||
|
||||
def validate_duplicate_members(self):
|
||||
if frappe.db.exists(
|
||||
"LMS Batch Enrollment",
|
||||
@@ -25,6 +67,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"])
|
||||
|
||||
@@ -36,6 +84,7 @@ class LMSBatchEnrollment(Document):
|
||||
enrollment = frappe.new_doc("LMS Enrollment")
|
||||
enrollment.course = course.course
|
||||
enrollment.member = self.member
|
||||
enrollment.enrollment_from_batch = self.batch
|
||||
enrollment.save()
|
||||
|
||||
def add_member_to_live_class(self):
|
||||
@@ -57,7 +106,7 @@ class LMSBatchEnrollment(Document):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def send_confirmation_email(doc):
|
||||
def send_confirmation_email(doc: Document):
|
||||
if isinstance(doc, str):
|
||||
doc = frappe._dict(json.loads(doc))
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-05-21 15:58:51.667270",
|
||||
"modified": "2026-01-14 08:53:38.088168",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Batch Feedback",
|
||||
@@ -105,6 +105,30 @@
|
||||
"role": "LMS Student",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Moderator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Batch Evaluator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
// Copyright (c) 2021, FOSS United and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("LMS Batch Old", {
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
});
|
||||
@@ -1,150 +0,0 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "format: BATCH-{#####}",
|
||||
"creation": "2021-03-18 19:37:34.614796",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"course",
|
||||
"start_date",
|
||||
"start_time",
|
||||
"column_break_3",
|
||||
"title",
|
||||
"sessions_on",
|
||||
"end_time",
|
||||
"section_break_5",
|
||||
"description",
|
||||
"section_break_7",
|
||||
"visibility",
|
||||
"membership",
|
||||
"column_break_9",
|
||||
"status",
|
||||
"stage"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "course",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Course",
|
||||
"options": "LMS Course",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Title",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Markdown Editor",
|
||||
"label": "Description"
|
||||
},
|
||||
{
|
||||
"default": "Public",
|
||||
"fieldname": "visibility",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Visibility",
|
||||
"options": "Public\nUnlisted\nPrivate"
|
||||
},
|
||||
{
|
||||
"fieldname": "membership",
|
||||
"fieldtype": "Select",
|
||||
"label": "Membership",
|
||||
"options": "\nOpen\nRestricted\nInvite Only\nClosed"
|
||||
},
|
||||
{
|
||||
"default": "Active",
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Status",
|
||||
"options": "Active\nInactive"
|
||||
},
|
||||
{
|
||||
"default": "Ready",
|
||||
"fieldname": "stage",
|
||||
"fieldtype": "Select",
|
||||
"label": "Stage",
|
||||
"options": "Ready\nIn Progress\nCompleted\nCancelled"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_5",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Batch Description"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_9",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_7",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Batch Settings"
|
||||
},
|
||||
{
|
||||
"fieldname": "start_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Start Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "start_time",
|
||||
"fieldtype": "Time",
|
||||
"in_list_view": 1,
|
||||
"label": "Start Time"
|
||||
},
|
||||
{
|
||||
"fieldname": "sessions_on",
|
||||
"fieldtype": "Data",
|
||||
"label": "Sessions On Days"
|
||||
},
|
||||
{
|
||||
"fieldname": "end_time",
|
||||
"fieldtype": "Time",
|
||||
"in_list_view": 1,
|
||||
"label": "End Time"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [
|
||||
{
|
||||
"group": "Members",
|
||||
"link_doctype": "LMS Enrollment",
|
||||
"link_fieldname": "batch_old"
|
||||
}
|
||||
],
|
||||
"modified": "2022-09-28 18:43:22.955907",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Batch Old",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
# Copyright (c) 2021, FOSS United and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
from lms.lms.doctype.lms_enrollment.lms_enrollment import create_membership
|
||||
from lms.lms.utils import is_mentor
|
||||
|
||||
|
||||
class LMSBatchOld(Document):
|
||||
def validate(self):
|
||||
pass
|
||||
# self.validate_if_mentor()
|
||||
|
||||
def validate_if_mentor(self):
|
||||
if not is_mentor(self.course, frappe.session.user):
|
||||
course_title = frappe.db.get_value("LMS Course", self.course, "title")
|
||||
frappe.throw(_("You are not a mentor of the course {0}").format(course_title))
|
||||
|
||||
def after_insert(self):
|
||||
create_membership(batch=self.name, course=self.course, member_type="Mentor")
|
||||
|
||||
def is_member(self, email, member_type=None):
|
||||
"""Checks if a person is part of a batch.
|
||||
|
||||
If member_type is specified, checks if the person is a Student/Mentor.
|
||||
"""
|
||||
|
||||
filters = {"batch_old": self.name, "member": email}
|
||||
if member_type:
|
||||
filters["member_type"] = member_type
|
||||
return frappe.db.exists("LMS Enrollment", filters)
|
||||
|
||||
def get_membership(self, email):
|
||||
"""Returns the membership document of given user."""
|
||||
name = frappe.get_value(
|
||||
doctype="LMS Enrollment",
|
||||
filters={"batch_old": self.name, "member": email},
|
||||
fieldname="name",
|
||||
)
|
||||
return frappe.get_doc("LMS Enrollment", name)
|
||||
|
||||
def get_current_lesson(self, user):
|
||||
"""Returns the name of the current lesson for the given user."""
|
||||
membership = self.get_membership(user)
|
||||
return membership and membership.current_lesson
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def save_message(message, batch):
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "LMS Message",
|
||||
"batch_old": batch,
|
||||
"author": frappe.session.user,
|
||||
"message": message,
|
||||
}
|
||||
)
|
||||
doc.save(ignore_permissions=True)
|
||||
|
||||
|
||||
def switch_batch(course_name, email, batch_name):
|
||||
"""Switches the user from the current batch of the course to a new batch."""
|
||||
membership = frappe.get_last_doc("LMS Enrollment", filters={"course": course_name, "member": email})
|
||||
|
||||
batch = frappe.get_doc("LMS Batch Old", batch_name)
|
||||
if not batch:
|
||||
raise ValueError(f"Invalid Batch: {batch_name}")
|
||||
|
||||
if batch.course != course_name:
|
||||
raise ValueError("Can not switch batches across courses")
|
||||
|
||||
if batch.is_member(email):
|
||||
print(f"{email} is already a member of {batch.title}")
|
||||
return
|
||||
|
||||
old_batch = frappe.get_doc("LMS Batch Old", membership.batch_old)
|
||||
|
||||
membership.batch_old = batch_name
|
||||
membership.save()
|
||||
|
||||
# update exercise submissions
|
||||
filters = {"owner": email, "batch_old": old_batch.name}
|
||||
for name in frappe.db.get_all("Exercise Submission", filters=filters, pluck="name"):
|
||||
doc = frappe.get_doc("Exercise Submission", name)
|
||||
print("updating exercise submission", name)
|
||||
doc.batch_old = batch_name
|
||||
doc.save()
|
||||
@@ -1,9 +0,0 @@
|
||||
# Copyright (c) 2021, FOSS United and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
|
||||
class TestLMSBatchOld(unittest.TestCase):
|
||||
pass
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-07 19:24:12.272810",
|
||||
"modified": "2026-02-20 17:32:34.580862",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Certificate",
|
||||
@@ -155,15 +155,36 @@
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Batch Evaluator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Course Creator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "LMS Student",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
|
||||
@@ -6,25 +6,28 @@ from frappe import _
|
||||
from frappe.email.doctype.email_template.email_template import get_email_template
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.naming import make_autoname
|
||||
from frappe.utils import add_years, nowdate
|
||||
|
||||
from lms.lms.utils import is_certified
|
||||
from frappe.utils import nowdate
|
||||
from frappe.utils.telemetry import capture
|
||||
|
||||
|
||||
class LMSCertificate(Document):
|
||||
def validate(self):
|
||||
self.validate_criteria()
|
||||
self.validate_duplicate_certificate()
|
||||
|
||||
def autoname(self):
|
||||
self.name = make_autoname("hash", self.doctype)
|
||||
|
||||
def after_insert(self):
|
||||
if not frappe.flags.in_test:
|
||||
outgoing_email_account = frappe.get_cached_value(
|
||||
"Email Account", {"default_outgoing": 1, "enable_outgoing": 1}, "name"
|
||||
)
|
||||
if outgoing_email_account or frappe.conf.get("mail_login"):
|
||||
self.send_mail()
|
||||
capture("certificate_issued", "lms")
|
||||
self.send_certification_email()
|
||||
|
||||
def send_certification_email(self):
|
||||
outgoing_email_account = frappe.get_cached_value(
|
||||
"Email Account", {"default_outgoing": 1, "enable_outgoing": 1}, "name"
|
||||
)
|
||||
if outgoing_email_account or frappe.conf.get("mail_login"):
|
||||
self.send_mail()
|
||||
|
||||
def send_mail(self):
|
||||
subject = _("Congratulations on getting certified!")
|
||||
@@ -52,6 +55,43 @@ class LMSCertificate(Document):
|
||||
header=[subject, "green"],
|
||||
)
|
||||
|
||||
def validate_criteria(self):
|
||||
self.validate_role_of_owner()
|
||||
self.validate_batch_enrollment()
|
||||
self.validate_course_enrollment()
|
||||
|
||||
def validate_role_of_owner(self):
|
||||
roles = frappe.get_roles()
|
||||
is_admin = any(role in roles for role in ["Moderator", "Course Creator", "Batch Evaluator"])
|
||||
if not self.course and not self.batch_name and not is_admin:
|
||||
frappe.throw(_("Course or Batch is required to issue a certificate."))
|
||||
|
||||
def validate_batch_enrollment(self):
|
||||
if self.batch_name:
|
||||
is_enrolled = frappe.db.exists(
|
||||
"LMS Batch Enrollment", {"batch": self.batch_name, "member": self.member}
|
||||
)
|
||||
if not is_enrolled:
|
||||
frappe.throw(_("Certification cannot be issued as the member is not enrolled in this batch."))
|
||||
|
||||
def validate_course_enrollment(self):
|
||||
if self.course:
|
||||
is_enrolled = frappe.db.exists("LMS Enrollment", {"course": self.course, "member": self.member})
|
||||
if not is_enrolled:
|
||||
frappe.throw(
|
||||
_("Certification cannot be issued as the member is not enrolled in this course.")
|
||||
)
|
||||
|
||||
completion_certificate = frappe.db.get_value("LMS Course", self.course, "enable_certification")
|
||||
if completion_certificate:
|
||||
progress = frappe.db.get_value(
|
||||
"LMS Enrollment", {"course": self.course, "member": self.member}, "progress"
|
||||
)
|
||||
if progress < 100:
|
||||
frappe.throw(
|
||||
_("Certification cannot be issued as the member has not completed the course.")
|
||||
)
|
||||
|
||||
def validate_duplicate_certificate(self):
|
||||
self.validate_course_duplicates()
|
||||
self.validate_batch_duplicates()
|
||||
@@ -113,31 +153,23 @@ def has_website_permission(doc, ptype, user, verbose=False):
|
||||
return False
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_certificate(course):
|
||||
certificate = is_certified(course)
|
||||
def is_certified(course):
|
||||
certificate = frappe.get_all("LMS Certificate", {"member": frappe.session.user, "course": course})
|
||||
if len(certificate):
|
||||
return certificate[0].name
|
||||
return
|
||||
|
||||
if certificate:
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_certificate(course: str):
|
||||
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 +181,57 @@ 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."))
|
||||
|
||||
|
||||
def has_permission(doc, ptype="read", user=None):
|
||||
user = user or frappe.session.user
|
||||
roles = frappe.get_roles(user)
|
||||
if "Moderator" in roles or "Course Creator" in roles or "Batch Evaluator" in roles:
|
||||
return True
|
||||
if doc.owner == user:
|
||||
return True
|
||||
if ptype not in ("read", "select", "print"):
|
||||
return False
|
||||
return doc.published
|
||||
|
||||
|
||||
def get_permission_query_conditions(user):
|
||||
user = user or frappe.session.user
|
||||
roles = frappe.get_roles(user)
|
||||
if "Moderator" in roles or "Course Creator" in roles or "Batch Evaluator" in roles:
|
||||
return None
|
||||
return """(`tabLMS Certificate`.published = 1)"""
|
||||
|
||||
@@ -3,26 +3,6 @@
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_years, 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
|
||||
|
||||
|
||||
class TestLMSCertificate(unittest.TestCase):
|
||||
def test_certificate_creation(self):
|
||||
course = new_course(
|
||||
"Test Certificate",
|
||||
{
|
||||
"enable_certification": 1,
|
||||
},
|
||||
)
|
||||
certificate = create_certificate(course.name)
|
||||
|
||||
self.assertEqual(certificate.member, "Administrator")
|
||||
self.assertEqual(certificate.course, course.name)
|
||||
self.assertEqual(certificate.issue_date, nowdate())
|
||||
|
||||
frappe.db.delete("LMS Certificate", certificate.name)
|
||||
frappe.db.delete("LMS Course", course.name)
|
||||
pass
|
||||
|
||||
@@ -14,15 +14,6 @@ frappe.ui.form.on("LMS Certificate Evaluation", {
|
||||
},
|
||||
|
||||
onload: function (frm) {
|
||||
frm.set_query("course", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
enable_certification: true,
|
||||
grant_certificate_after: "Evaluation",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("member", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
|
||||
@@ -131,10 +131,11 @@
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-09-11 11:20:06.233491",
|
||||
"modified_by": "Administrator",
|
||||
"modified": "2025-11-10 11:41:38.999620",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Certificate Evaluation",
|
||||
"owner": "Administrator",
|
||||
@@ -164,6 +165,7 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [
|
||||
@@ -184,5 +186,6 @@
|
||||
"title": "In Progress"
|
||||
}
|
||||
],
|
||||
"title_field": "member_name"
|
||||
}
|
||||
"title_field": "member_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
||||
@@ -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,13 +19,13 @@ 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
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_lms_certificate(source_name, target_doc=None):
|
||||
def create_lms_certificate(source_name: str, target_doc: dict = None):
|
||||
doc = get_mapped_doc(
|
||||
"LMS Certificate Evaluation",
|
||||
source_name,
|
||||
|
||||
@@ -157,7 +157,7 @@
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-13 14:30:57.897102",
|
||||
"modified": "2026-02-23 14:45:44.994705",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Certificate Request",
|
||||
@@ -192,6 +192,7 @@
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
@@ -209,6 +210,18 @@
|
||||
"role": "Batch Evaluator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Course Creator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
@@ -228,5 +241,6 @@
|
||||
"title": "Cancelled"
|
||||
}
|
||||
],
|
||||
"title_field": "member_name"
|
||||
"title_field": "member_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ from frappe.utils import (
|
||||
format_time,
|
||||
get_datetime,
|
||||
get_fullname,
|
||||
get_system_timezone,
|
||||
get_time,
|
||||
getdate,
|
||||
nowtime,
|
||||
@@ -67,6 +68,7 @@ class LMSCertificateRequest(Document):
|
||||
{
|
||||
"evaluator": self.evaluator,
|
||||
"date": self.date,
|
||||
"status": ["!=", "Cancelled"],
|
||||
"start_time": self.start_time,
|
||||
"member": ["!=", self.member],
|
||||
},
|
||||
@@ -117,16 +119,7 @@ class LMSCertificateRequest(Document):
|
||||
def validate_timezone(self):
|
||||
if self.timezone:
|
||||
return
|
||||
if self.batch_name:
|
||||
timezone = frappe.db.get_value("LMS Batch", self.batch_name, "timezone")
|
||||
if timezone:
|
||||
self.timezone = timezone
|
||||
return
|
||||
if self.course:
|
||||
timezone = frappe.db.get_value("LMS Course", self.course, "timezone")
|
||||
if timezone:
|
||||
self.timezone = timezone
|
||||
return
|
||||
self.timezone = get_system_timezone()
|
||||
|
||||
def send_notification(self):
|
||||
outgoing_email_account = frappe.get_cached_value(
|
||||
@@ -173,7 +166,7 @@ def schedule_evals():
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def setup_calendar_event(eval):
|
||||
def setup_calendar_event(eval: str):
|
||||
if isinstance(eval, str):
|
||||
eval = frappe._dict(json.loads(eval))
|
||||
|
||||
@@ -185,7 +178,7 @@ def setup_calendar_event(eval):
|
||||
update_meeting_details(eval, event, calendar)
|
||||
|
||||
|
||||
def create_event(eval):
|
||||
def create_event(eval: dict):
|
||||
event = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Event",
|
||||
@@ -198,7 +191,7 @@ def create_event(eval):
|
||||
return event
|
||||
|
||||
|
||||
def add_participants(eval, event):
|
||||
def add_participants(eval: dict, event: Document):
|
||||
participants = [eval.member, eval.evaluator]
|
||||
for participant in participants:
|
||||
contact_name = frappe.db.get_value("Contact", {"email_id": participant}, "name")
|
||||
@@ -215,7 +208,7 @@ def add_participants(eval, event):
|
||||
).save()
|
||||
|
||||
|
||||
def update_meeting_details(eval, event, calendar):
|
||||
def update_meeting_details(eval: dict, event: Document, calendar: str):
|
||||
event.reload()
|
||||
event.update(
|
||||
{
|
||||
@@ -231,31 +224,8 @@ def update_meeting_details(eval, event, calendar):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_certificate_request(course, date, day, start_time, end_time, batch_name=None):
|
||||
is_member = frappe.db.exists(
|
||||
{"doctype": "LMS Enrollment", "course": course, "member": frappe.session.user}
|
||||
)
|
||||
|
||||
if not is_member:
|
||||
return
|
||||
eval = frappe.new_doc("LMS Certificate Request")
|
||||
eval.update(
|
||||
{
|
||||
"course": course,
|
||||
"evaluator": get_evaluator(course, batch_name),
|
||||
"member": frappe.session.user,
|
||||
"date": date,
|
||||
"day": day,
|
||||
"start_time": start_time,
|
||||
"end_time": end_time,
|
||||
"batch_name": batch_name,
|
||||
}
|
||||
)
|
||||
eval.save(ignore_permissions=True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_lms_certificate_evaluation(source_name, target_doc=None):
|
||||
def create_lms_certificate_evaluation(source_name: str, target_doc: dict = None):
|
||||
frappe.only_for(["Moderator", "Batch Evaluator", "System Manager"])
|
||||
doc = get_mapped_doc(
|
||||
"LMS Certificate Request",
|
||||
source_name,
|
||||
|
||||
8
lms/lms/doctype/lms_coupon/lms_coupon.js
Normal file
8
lms/lms/doctype/lms_coupon/lms_coupon.js
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2025, Frappe and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("LMS Coupon", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
194
lms/lms/doctype/lms_coupon/lms_coupon.json
Normal file
194
lms/lms/doctype/lms_coupon/lms_coupon.json
Normal file
@@ -0,0 +1,194 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"autoname": "hash",
|
||||
"creation": "2025-10-11 21:39:11.456420",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"enabled",
|
||||
"section_break_spfj",
|
||||
"code",
|
||||
"expires_on",
|
||||
"column_break_mptc",
|
||||
"discount_type",
|
||||
"percentage_discount",
|
||||
"fixed_amount_discount",
|
||||
"section_break_ixxu",
|
||||
"usage_limit",
|
||||
"column_break_dcvj",
|
||||
"redemption_count",
|
||||
"section_break_ophm",
|
||||
"applicable_items"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "code",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Code",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "discount_type",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Discount Type",
|
||||
"options": "Percentage\nFixed Amount",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "expires_on",
|
||||
"fieldtype": "Date",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Expires On"
|
||||
},
|
||||
{
|
||||
"fieldname": "usage_limit",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Usage Limit"
|
||||
},
|
||||
{
|
||||
"fieldname": "applicable_items",
|
||||
"fieldtype": "Table",
|
||||
"label": "Applicable Items",
|
||||
"options": "LMS Coupon Item",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "enabled",
|
||||
"fieldtype": "Check",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_mptc",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_ixxu",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_dcvj",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_ophm",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.discount_type=='Percentage'",
|
||||
"fieldname": "percentage_discount",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Percentage Discount",
|
||||
"mandatory_depends_on": "eval:doc.discount_type=='Percentage'"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.discount_type=='Fixed Amount'",
|
||||
"fieldname": "fixed_amount_discount",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Fixed Amount Discount",
|
||||
"mandatory_depends_on": "eval:doc.discount_type=='Fixed Amount'"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "redemption_count",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Redemption Count",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_spfj",
|
||||
"fieldtype": "Section Break"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-03 10:50:23.387175",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Coupon",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Administrator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"import": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Batch Evaluator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"import": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Course Creator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"import": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Moderator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"rows_threshold_for_grid_search": 20,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "code"
|
||||
}
|
||||
31
lms/lms/doctype/lms_coupon/lms_coupon.py
Normal file
31
lms/lms/doctype/lms_coupon/lms_coupon.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# Copyright (c) 2025, Frappe and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, nowdate
|
||||
|
||||
|
||||
class LMSCoupon(Document):
|
||||
def validate(self):
|
||||
self.convert_to_uppercase()
|
||||
self.validate_expiry_date()
|
||||
self.validate_applicable_items()
|
||||
self.validate_usage_limit()
|
||||
|
||||
def convert_to_uppercase(self):
|
||||
if self.code:
|
||||
self.code = self.code.strip().upper()
|
||||
|
||||
def validate_expiry_date(self):
|
||||
if self.expires_on and str(self.expires_on) < nowdate():
|
||||
frappe.throw(_("Expiry date cannot be in the past"))
|
||||
|
||||
def validate_applicable_items(self):
|
||||
if not self.get("applicable_items") or len(self.get("applicable_items")) == 0:
|
||||
frappe.throw(_("At least one applicable item is required"))
|
||||
|
||||
def validate_usage_limit(self):
|
||||
if self.usage_limit is not None and cint(self.usage_limit) < 0:
|
||||
frappe.throw(_("Usage limit cannot be negative"))
|
||||
20
lms/lms/doctype/lms_coupon/test_lms_coupon.py
Normal file
20
lms/lms/doctype/lms_coupon/test_lms_coupon.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# Copyright (c) 2025, Frappe and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
# On IntegrationTestCase, the doctype test records and all
|
||||
# link-field test record dependencies are recursively loaded
|
||||
# Use these module variables to add/remove to/from that list
|
||||
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
|
||||
|
||||
class IntegrationTestLMSCoupon(IntegrationTestCase):
|
||||
"""
|
||||
Integration tests for LMSCoupon.
|
||||
Use this class for testing interactions between multiple components.
|
||||
"""
|
||||
|
||||
pass
|
||||
43
lms/lms/doctype/lms_coupon_item/lms_coupon_item.json
Normal file
43
lms/lms/doctype/lms_coupon_item/lms_coupon_item.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-10-11 21:45:00",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"reference_doctype",
|
||||
"reference_name"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "reference_doctype",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Reference DocType",
|
||||
"options": "\nLMS Course\nLMS Batch",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Reference Name",
|
||||
"options": "reference_doctype",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-12 17:27:14.123811",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Coupon Item",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
# Copyright (c) 2022, Frappe and contributors
|
||||
# Copyright (c) 2025, Frappe and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class JobSettings(Document):
|
||||
class LMSCouponItem(Document):
|
||||
pass
|
||||
@@ -20,7 +20,11 @@ frappe.ui.form.on("LMS Course", {
|
||||
});
|
||||
},
|
||||
refresh: (frm) => {
|
||||
frm.add_web_link(`/lms/courses/${frm.doc.name}`, "See on Website");
|
||||
const lmsPath = frappe.boot.lms_path || "lms";
|
||||
frm.add_web_link(
|
||||
`/${lmsPath}/courses/${frm.doc.name}`,
|
||||
"See on Website"
|
||||
);
|
||||
|
||||
if (!frm.doc.currency)
|
||||
frappe.db
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
{
|
||||
"actions": [
|
||||
{
|
||||
"action": "lms.lms.doctype.lms_course.lms_course.reindex_exercises",
|
||||
"action_type": "Server Action",
|
||||
"group": "Reindex",
|
||||
"label": "Reindex Exercises"
|
||||
}
|
||||
],
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"creation": "2022-02-22 15:28:26.091549",
|
||||
@@ -41,9 +34,9 @@
|
||||
"pricing_tab",
|
||||
"pricing_section",
|
||||
"paid_course",
|
||||
"enable_certification",
|
||||
"paid_certificate",
|
||||
"column_break_acoj",
|
||||
"enable_certification",
|
||||
"section_break_vqbh",
|
||||
"course_price",
|
||||
"currency",
|
||||
@@ -55,7 +48,8 @@
|
||||
"statistics_section",
|
||||
"enrollments",
|
||||
"lessons",
|
||||
"rating"
|
||||
"rating",
|
||||
"notification_sent"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -76,6 +70,8 @@
|
||||
"default": "0",
|
||||
"fieldname": "published",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Published"
|
||||
},
|
||||
{
|
||||
@@ -152,8 +148,6 @@
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 1,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Status",
|
||||
"options": "In Progress\nUnder Review\nApproved",
|
||||
"read_only": 1
|
||||
@@ -174,7 +168,7 @@
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Currency",
|
||||
"mandatory_depends_on": "paid_course",
|
||||
"mandatory_depends_on": "eval: doc.paid_course || doc.paid_certificate",
|
||||
"options": "Currency"
|
||||
},
|
||||
{
|
||||
@@ -187,7 +181,7 @@
|
||||
"fieldname": "course_price",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount",
|
||||
"mandatory_depends_on": "paid_course"
|
||||
"mandatory_depends_on": "eval: doc.paid_course || doc.paid_certificate"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_acoj",
|
||||
@@ -295,6 +289,13 @@
|
||||
"fieldname": "timezone",
|
||||
"fieldtype": "Data",
|
||||
"label": "Timezone"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "notification_sent",
|
||||
"fieldtype": "Check",
|
||||
"label": "Notification Sent",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"is_published_field": "published",
|
||||
@@ -313,7 +314,7 @@
|
||||
}
|
||||
],
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2025-10-13 15:08:11.734204",
|
||||
"modified": "2026-02-19 11:41:57.038869",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Course",
|
||||
@@ -336,6 +337,7 @@
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"import": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
@@ -348,6 +350,7 @@
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"import": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user