From cb3af6fa63a2441b59063fc7eea4d240ddb19a36 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Thu, 19 Feb 2026 12:24:47 +0530 Subject: [PATCH 01/13] fix: sanitised badge assignment api --- .../src/components/Settings/BadgeForm.vue | 2 +- lms/lms/doctype/lms_badge/lms_badge.js | 10 ++++---- lms/lms/doctype/lms_badge/lms_badge.json | 6 ++--- lms/lms/doctype/lms_badge/lms_badge.py | 24 ++++++++++++++----- lms/patches.txt | 3 ++- .../v2_0/rename_badge_assignment_event.py | 7 ++++++ 6 files changed, 37 insertions(+), 15 deletions(-) create mode 100644 lms/patches/v2_0/rename_badge_assignment_event.py diff --git a/frontend/src/components/Settings/BadgeForm.vue b/frontend/src/components/Settings/BadgeForm.vue index 75d2cc8b..4a3a48bd 100644 --- a/frontend/src/components/Settings/BadgeForm.vue +++ b/frontend/src/components/Settings/BadgeForm.vue @@ -206,7 +206,7 @@ const referenceDoctypeOptions = computed(() => { }) const eventOptions = computed(() => { - let options = ['New', 'Value Change', 'Auto Assign'] + let options = ['New', 'Value Change', 'Manual Assignment'] return options.map((event) => ({ label: __(event), value: event })) }) diff --git a/lms/lms/doctype/lms_badge/lms_badge.js b/lms/lms/doctype/lms_badge/lms_badge.js index e157d38e..b59321f4 100644 --- a/lms/lms/doctype/lms_badge/lms_badge.js +++ b/lms/lms/doctype/lms_badge/lms_badge.js @@ -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); } }, @@ -49,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")); } }, }); diff --git a/lms/lms/doctype/lms_badge/lms_badge.json b/lms/lms/doctype/lms_badge/lms_badge.json index 633106a0..70e257d6 100644 --- a/lms/lms/doctype/lms_badge/lms_badge.json +++ b/lms/lms/doctype/lms_badge/lms_badge.json @@ -52,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\"" + "mandatory_depends_on": "eval:doc.event == \"Manual Assignment\"" }, { "depends_on": "eval:doc.event == 'Value Change'", @@ -100,7 +100,7 @@ "link_fieldname": "badge" } ], - "modified": "2026-02-03 10:52:37.122370", + "modified": "2026-02-19 12:04:56.263316", "modified_by": "sayali@frappe.io", "module": "LMS", "name": "LMS Badge", diff --git a/lms/lms/doctype/lms_badge/lms_badge.py b/lms/lms/doctype/lms_badge/lms_badge.py index 4ca727f7..4e4f981d 100644 --- a/lms/lms/doctype/lms_badge/lms_badge.py +++ b/lms/lms/doctype/lms_badge/lms_badge.py @@ -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,27 @@ def eval_condition(doc, condition): @frappe.whitelist() -def assign_badge(badge: str, user: str): - 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.event == "Manual Assignment": return fields = ["name"] fields.append(badge.user_field) - list = frappe.get_all(badge.reference_doctype, filters=badge.condition, fields=fields) + list = frappe.get_all(badge.reference_doctype, filters=json.loads(badge.condition), fields=fields) + for doc in list: - award(badge, doc.get(badge.user_field)) + 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): diff --git a/lms/patches.txt b/lms/patches.txt index e6a6e90d..9de69d3e 100644 --- a/lms/patches.txt +++ b/lms/patches.txt @@ -117,4 +117,5 @@ lms.patches.v2_0.fix_job_application_resume_urls lms.patches.v2_0.open_to_opportunities lms.patches.v2_0.open_to_work lms.patches.v2_0.share_enrollment -lms.patches.v2_0.give_user_list_permission #11-02-2026 \ No newline at end of file +lms.patches.v2_0.give_user_list_permission #11-02-2026 +lms.patches.v2_0.rename_badge_assignment_event \ No newline at end of file diff --git a/lms/patches/v2_0/rename_badge_assignment_event.py b/lms/patches/v2_0/rename_badge_assignment_event.py new file mode 100644 index 00000000..7612e789 --- /dev/null +++ b/lms/patches/v2_0/rename_badge_assignment_event.py @@ -0,0 +1,7 @@ +import frappe + + +def execute(): + badge_with_auto_assign = frappe.get_all("LMS Badge", filters={"event": "Auto Assign"}, fields=["name"]) + for badge in badge_with_auto_assign: + frappe.db.set_value("LMS Badge", badge.name, "event", "Manual Assignment") From 72cee7547437114d675567967ebdcbeab9df3627 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Thu, 19 Feb 2026 12:39:55 +0530 Subject: [PATCH 02/13] fix: only allow lms roles to be modified by moderator --- lms/lms/api.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lms/lms/api.py b/lms/lms/api.py index d9849999..36848d9e 100644 --- a/lms/lms/api.py +++ b/lms/lms/api.py @@ -1369,6 +1369,10 @@ def get_certification_details(course: str): @frappe.whitelist() def save_role(user: str, role: str, value: int): frappe.only_for("Moderator") + ALLOWED_ROLES = ["Moderator", "Course Creator", "Batch Evaluator", "LMS Student"] + if role not in ALLOWED_ROLES: + frappe.throw(_("You do not have permission to modify this role."), frappe.PermissionError) + if cint(value): doc = frappe.get_doc( { From c961923fa07c62d3ae67391b08ce4bd69df241d7 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Thu, 19 Feb 2026 12:43:50 +0530 Subject: [PATCH 03/13] fix: verify enrollment and admin access before returing batch assessment data --- lms/lms/utils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lms/lms/utils.py b/lms/lms/utils.py index 84e387cd..c336b19b 100644 --- a/lms/lms/utils.py +++ b/lms/lms/utils.py @@ -1240,6 +1240,10 @@ def get_batch_courses(batch: str) -> list: @frappe.whitelist() def get_assessments(batch: str) -> list: member = frappe.session.user + is_enrolled = frappe.db.exists("LMS Batch Enrollment", {"batch": batch, "member": member}) + if not is_enrolled and not can_modify_batch(batch): + frappe.throw(_("You are not authorized to view the assessments of this batch.")) + assessments = frappe.get_all( "LMS Assessment", {"parent": batch}, From 44ca59c64a062118da3385cfffea4f60f8fd7d24 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Thu, 19 Feb 2026 12:51:30 +0530 Subject: [PATCH 04/13] fix: return profile details only if the profile is of an LMS user --- lms/lms/api.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/lms/lms/api.py b/lms/lms/api.py index 36848d9e..21630743 100644 --- a/lms/lms/api.py +++ b/lms/lms/api.py @@ -44,6 +44,8 @@ from lms.lms.utils import ( has_moderator_role, ) +LMS_ROLES = ["Moderator", "Course Creator", "Batch Evaluator", "LMS Student"] + @frappe.whitelist() def get_user_info(): @@ -1369,8 +1371,7 @@ def get_certification_details(course: str): @frappe.whitelist() def save_role(user: str, role: str, value: int): frappe.only_for("Moderator") - ALLOWED_ROLES = ["Moderator", "Course Creator", "Batch Evaluator", "LMS Student"] - if role not in ALLOWED_ROLES: + if role not in LMS_ROLES: frappe.throw(_("You do not have permission to modify this role."), frappe.PermissionError) if cint(value): @@ -1720,11 +1721,21 @@ def get_profile_details(username: str): ], as_dict=True, ) - - details.roles = frappe.get_roles(details.name) + roles = frappe.get_roles(details.name) + if not has_lms_role(roles): + frappe.throw( + _("User does not have permission to access this users profile details."), frappe.PermissionError + ) + details.roles = roles return details +def has_lms_role(roles: list): + lms_roles = set(LMS_ROLES) + user_roles = set(roles) + return not lms_roles.isdisjoint(user_roles) + + @frappe.whitelist() def get_streak_info(): all_dates = fetch_activity_dates(frappe.session.user) From 08373dc2ab381d88f939e04e23adf10fedd561fa Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Thu, 19 Feb 2026 15:58:44 +0530 Subject: [PATCH 05/13] fix: refactored job form and permissions --- .../src/components/Controls/Autocomplete.vue | 4 +- frontend/src/components/Controls/Uploader.vue | 2 +- .../components/Settings/SettingDetails.vue | 8 +- frontend/src/components/Settings/Settings.vue | 19 ++ frontend/src/pages/JobForm.vue | 233 ++++++++++-------- frontend/src/pages/Jobs.vue | 10 +- .../job_opportunity/job_opportunity.json | 14 +- lms/job/doctype/job_settings/__init__.py | 0 lms/job/doctype/job_settings/job_settings.js | 7 - .../doctype/job_settings/job_settings.json | 54 ---- lms/job/doctype/job_settings/job_settings.py | 9 - .../doctype/job_settings/test_job_settings.py | 9 - lms/patches.txt | 3 +- lms/patches/v2_0/enable_allow_job_posting.py | 5 + 14 files changed, 170 insertions(+), 207 deletions(-) delete mode 100644 lms/job/doctype/job_settings/__init__.py delete mode 100644 lms/job/doctype/job_settings/job_settings.js delete mode 100644 lms/job/doctype/job_settings/job_settings.json delete mode 100644 lms/job/doctype/job_settings/job_settings.py delete mode 100644 lms/job/doctype/job_settings/test_job_settings.py create mode 100644 lms/patches/v2_0/enable_allow_job_posting.py diff --git a/frontend/src/components/Controls/Autocomplete.vue b/frontend/src/components/Controls/Autocomplete.vue index 9870bab0..71739c20 100644 --- a/frontend/src/components/Controls/Autocomplete.vue +++ b/frontend/src/components/Controls/Autocomplete.vue @@ -107,7 +107,7 @@
{{ - option.value == option.label + option.value == option.label && option.description ? option.description : option.label }} @@ -124,7 +124,7 @@ v-if="groups.length == 0" class="mt-1.5 rounded-md px-2.5 py-1.5 text-base text-ink-gray-5" > - No results found + {{ __('No results found') }}
diff --git a/frontend/src/components/Controls/Uploader.vue b/frontend/src/components/Controls/Uploader.vue index 5bcd81a2..1f6ec5ee 100644 --- a/frontend/src/components/Controls/Uploader.vue +++ b/frontend/src/components/Controls/Uploader.vue @@ -38,7 +38,7 @@ 'border object-cover', shape === 'circle' ? 'w-20 h-20 rounded-full' - : 'w-44 h-auto min-h-20 rounded-md', + : 'w-44 h-auto min-h-20 max-h-32 rounded-md', ]" />
+
+
-
{{ __(description) }} diff --git a/frontend/src/components/Settings/Settings.vue b/frontend/src/components/Settings/Settings.vue index 1a021f1e..0365ed02 100644 --- a/frontend/src/components/Settings/Settings.vue +++ b/frontend/src/components/Settings/Settings.vue @@ -219,6 +219,25 @@ const tabsStructure = computed(() => { }, ], }, + { + label: 'Jobs', + columns: [ + { + fields: [ + { + label: 'Allow Job Posting', + name: 'allow_job_posting', + type: 'checkbox', + description: + 'If enabled, users can post job openings on the job board. Else only admins can post jobs.', + }, + ], + }, + { + fields: [], + }, + ], + }, { label: '', columns: [ diff --git a/frontend/src/pages/JobForm.vue b/frontend/src/pages/JobForm.vue index 6337d5a0..1ee0d523 100644 --- a/frontend/src/pages/JobForm.vue +++ b/frontend/src/pages/JobForm.vue @@ -4,9 +4,14 @@ class="sticky top-0 z-10 flex items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5" > - +
+ + {{ __('Not Saved') }} + + +
@@ -109,15 +114,25 @@ diff --git a/frontend/src/pages/ProfileAbout.vue b/frontend/src/pages/ProfileAbout.vue index d2fb068c..5206e1a3 100644 --- a/frontend/src/pages/ProfileAbout.vue +++ b/frontend/src/pages/ProfileAbout.vue @@ -70,13 +70,16 @@
{{ badge.badge_description }}
-
+
{{ __('Issued on') }}: {{ dayjs(badge.issued_on).format('DD MMM YYYY') }}
-
+
{{ __('Share on') }}: @@ -125,6 +128,7 @@ import DOMPurify from 'dompurify' import { getLmsRoute } from '@/utils/basePath' const dayjs = inject('$dayjs') +const user = inject('$user') const { branding } = sessionStore() const props = defineProps({ @@ -135,13 +139,9 @@ const props = defineProps({ }) const badges = createResource({ - url: 'frappe.client.get_list', + url: 'lms.lms.api.get_badges', params: { - doctype: 'LMS Badge Assignment', - fields: ['name', 'badge', 'badge_image', 'badge_description', 'issued_on'], - filters: { - member: props.profile.data.name, - }, + member: props.profile.data.name, }, auto: true, transform(data) { @@ -160,14 +160,16 @@ const shareOnSocial = (badge, medium) => { let shareUrl const url = encodeURIComponent( `${window.location.origin}${getLmsRoute( - `badges/${badge.badge}/${props.profile.data?.email}` + `user/${props.profile.data?.username}` )}` ) - const summary = `I am happy to announce that I earned the ${ - badge.badge - } badge on ${dayjs(badge.issued_on).format('DD MMM YYYY')} at ${ + const summary = __( + 'I am happy to announce that I earned the {0} badge on {1} at {2}' + ).format( + badge.badge, + dayjs(badge.issued_on).format('DD MMM YYYY'), branding.data?.app_name - }.` + ) if (medium == 'LinkedIn') shareUrl = `https://www.linkedin.com/shareArticle?mini=true&url=${url}&text=${summary}` diff --git a/lms/lms/api.py b/lms/lms/api.py index 21630743..d60f6103 100644 --- a/lms/lms/api.py +++ b/lms/lms/api.py @@ -1298,6 +1298,7 @@ def get_lms_settings(): "contact_us_url", "livecode_url", "disable_pwa", + "allow_job_posting", ] settings = frappe._dict() @@ -1310,7 +1311,6 @@ def get_lms_settings(): @frappe.whitelist() def cancel_evaluation(evaluation: dict): evaluation = frappe._dict(evaluation) - print(evaluation.member, frappe.session.user) if evaluation.member != frappe.session.user: frappe.throw(_("You do not have permission to cancel this evaluation."), frappe.PermissionError) @@ -2219,3 +2219,17 @@ def get_assessment_from_lesson(course: str, assessmentType: str): assessments.append(quiz_name) return assessments + + +@frappe.whitelist() +def get_badges(member: str): + if not has_lms_role(frappe.get_roles()): + frappe.throw(_("You do not have permission to access badges."), frappe.PermissionError) + + badges = frappe.get_all( + "LMS Badge Assignment", + {"member": member}, + ["name", "member", "badge", "badge_image", "badge_description", "issued_on"], + ) + + return badges diff --git a/lms/lms/doctype/lms_badge/lms_badge.json b/lms/lms/doctype/lms_badge/lms_badge.json index 70e257d6..5f4cda8f 100644 --- a/lms/lms/doctype/lms_badge/lms_badge.json +++ b/lms/lms/doctype/lms_badge/lms_badge.json @@ -100,7 +100,7 @@ "link_fieldname": "badge" } ], - "modified": "2026-02-19 12:04:56.263316", + "modified": "2026-02-19 15:05:49.719925", "modified_by": "sayali@frappe.io", "module": "LMS", "name": "LMS Badge", @@ -131,15 +131,6 @@ "role": "Moderator", "share": 1, "write": 1 - }, - { - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "LMS Student", - "share": 1 } ], "row_format": "Dynamic", diff --git a/lms/lms/doctype/lms_badge_assignment/lms_badge_assignment.json b/lms/lms/doctype/lms_badge_assignment/lms_badge_assignment.json index 15935668..bada8b1c 100644 --- a/lms/lms/doctype/lms_badge_assignment/lms_badge_assignment.json +++ b/lms/lms/doctype/lms_badge_assignment/lms_badge_assignment.json @@ -84,7 +84,7 @@ "grid_page_length": 50, "index_web_pages_for_search": 1, "links": [], - "modified": "2025-12-04 17:06:26.090276", + "modified": "2026-02-19 15:06:08.389081", "modified_by": "sayali@frappe.io", "module": "LMS", "name": "LMS Badge Assignment", @@ -120,10 +120,6 @@ "read": 1, "role": "LMS Student" }, - { - "read": 1, - "role": "LMS Student" - }, { "create": 1, "delete": 1, diff --git a/lms/lms/doctype/lms_settings/lms_settings.json b/lms/lms/doctype/lms_settings/lms_settings.json index e9207e91..4a35a76a 100644 --- a/lms/lms/doctype/lms_settings/lms_settings.json +++ b/lms/lms/doctype/lms_settings/lms_settings.json @@ -76,7 +76,9 @@ "contact_us_tab", "contact_us_email", "column_break_gcgv", - "contact_us_url" + "contact_us_url", + "jobs_tab", + "allow_job_posting" ], "fields": [ { @@ -471,13 +473,24 @@ { "fieldname": "column_break_dtns", "fieldtype": "Column Break" + }, + { + "fieldname": "jobs_tab", + "fieldtype": "Tab Break", + "label": "Jobs" + }, + { + "default": "1", + "fieldname": "allow_job_posting", + "fieldtype": "Check", + "label": "Allow Job Posting" } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2026-01-01 19:36:54.443390", + "modified": "2026-02-19 12:57:28.499184", "modified_by": "sayali@frappe.io", "module": "LMS", "name": "LMS Settings", From 07c58251a133febd572125234f93fc8ec1cd2267 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Thu, 19 Feb 2026 16:27:35 +0530 Subject: [PATCH 07/13] fix: lms certificate request will allow students to read only if they are owner --- frontend/src/components/UpcomingEvaluations.vue | 8 +++++--- frontend/src/pages/Batches/components/LiveClass.vue | 2 +- frontend/src/pages/Home/StudentHome.vue | 8 ++++---- .../lms_certificate_request/lms_certificate_request.json | 3 ++- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/UpcomingEvaluations.vue b/frontend/src/components/UpcomingEvaluations.vue index 14fed3ba..567d560f 100644 --- a/frontend/src/components/UpcomingEvaluations.vue +++ b/frontend/src/components/UpcomingEvaluations.vue @@ -31,12 +31,14 @@
-
+
- + {{ evl.course_title }} -
+
{{ cls.title }}
diff --git a/frontend/src/pages/Home/StudentHome.vue b/frontend/src/pages/Home/StudentHome.vue index 1183c307..85491a12 100644 --- a/frontend/src/pages/Home/StudentHome.vue +++ b/frontend/src/pages/Home/StudentHome.vue @@ -1,17 +1,17 @@