diff --git a/frontend/src/components/BadgePopover.vue b/frontend/src/components/BadgePopover.vue new file mode 100644 index 00000000..0b0c23da --- /dev/null +++ b/frontend/src/components/BadgePopover.vue @@ -0,0 +1,9 @@ + + diff --git a/frontend/src/pages/Profile.vue b/frontend/src/pages/Profile.vue index 44ba89fc..f18f30c6 100644 --- a/frontend/src/pages/Profile.vue +++ b/frontend/src/pages/Profile.vue @@ -140,7 +140,7 @@ const coverImage = createResource({ const setActiveTab = () => { let fragments = route.path.split('/') - let sections = ['certificates', 'roles', 'evaluations'] + let sections = ['certificates', 'achievements', 'roles', 'evaluations'] sections.forEach((section) => { if (fragments.includes(section)) { activeTab.value = convertToTitleCase(section) @@ -154,6 +154,7 @@ watchEffect(() => { let route = { About: { name: 'ProfileAbout' }, Certificates: { name: 'ProfileCertificates' }, + Achievements: { name: 'ProfileAchievements' }, Roles: { name: 'ProfileRoles' }, Evaluations: { name: 'ProfileEvaluator' }, }[activeTab.value] @@ -170,7 +171,11 @@ const isSessionUser = () => { } const getTabButtons = () => { - let buttons = [{ label: 'About' }, { label: 'Certificates' }] + let buttons = [ + { label: 'About' }, + { label: 'Certificates' }, + { label: 'Achievements' }, + ] if ($user.data?.is_moderator) buttons.push({ label: 'Roles' }) if (isSessionUser() && $user.data?.is_evaluator) buttons.push({ label: 'Evaluations' }) diff --git a/frontend/src/pages/ProfileAchievements.vue b/frontend/src/pages/ProfileAchievements.vue new file mode 100644 index 00000000..8cd49d9a --- /dev/null +++ b/frontend/src/pages/ProfileAchievements.vue @@ -0,0 +1,65 @@ + + diff --git a/frontend/src/router.js b/frontend/src/router.js index fab065d7..ff090428 100644 --- a/frontend/src/router.js +++ b/frontend/src/router.js @@ -72,6 +72,11 @@ const routes = [ path: 'certificates', component: () => import('@/pages/ProfileCertificates.vue'), }, + { + name: 'ProfileAchievements', + path: 'achievements', + component: () => import('@/pages/ProfileAchievements.vue'), + }, { name: 'ProfileRoles', path: 'roles', diff --git a/lms/hooks.py b/lms/hooks.py index 0bb32c40..36472024 100644 --- a/lms/hooks.py +++ b/lms/hooks.py @@ -97,6 +97,11 @@ override_doctype_class = { # Hook on document methods and events doc_events = { + "*": { + "on_change": [ + "lms.lms.doctype.lms_badge.lms_badge.process_badges", + ] + }, "Discussion Reply": {"after_insert": "lms.lms.utils.handle_notifications"}, } diff --git a/lms/lms/api.py b/lms/lms/api.py index 3f174d6f..288a2a1b 100644 --- a/lms/lms/api.py +++ b/lms/lms/api.py @@ -359,3 +359,18 @@ def get_certified_participants(): participant_details.append(details) return participant_details + + +@frappe.whitelist() +def get_assigned_badges(member): + assigned_badges = frappe.get_all( + "LMS Badge Assignment", + {"member": member}, + ["badge"], + as_dict=1, + ) + + for badge in assigned_badges: + badge.update( + frappe.db.get_value("LMS Badge", badge.badge, ["name", "title", "image"]) + ) diff --git a/lms/lms/doctype/lms_badge/__init__.py b/lms/lms/doctype/lms_badge/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lms/lms/doctype/lms_badge/lms_badge.js b/lms/lms/doctype/lms_badge/lms_badge.js new file mode 100644 index 00000000..7c031086 --- /dev/null +++ b/lms/lms/doctype/lms_badge/lms_badge.js @@ -0,0 +1,8 @@ +// Copyright (c) 2024, Frappe and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("LMS Badge", { +// refresh(frm) { + +// }, +// }); diff --git a/lms/lms/doctype/lms_badge/lms_badge.json b/lms/lms/doctype/lms_badge/lms_badge.json new file mode 100644 index 00000000..ba54e869 --- /dev/null +++ b/lms/lms/doctype/lms_badge/lms_badge.json @@ -0,0 +1,113 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:title", + "creation": "2024-04-30 11:29:53.548647", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "enabled", + "title", + "description", + "image", + "reference_doctype", + "column_break_wgum", + "grant_only_once", + "event", + "field_to_check", + "condition" + ], + "fields": [ + { + "fieldname": "title", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Title", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "image", + "fieldtype": "Attach Image", + "label": "Image", + "reqd": 1 + }, + { + "fieldname": "column_break_wgum", + "fieldtype": "Column Break" + }, + { + "fieldname": "reference_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Reference Document Type", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "event", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Event", + "options": "New\nValue Change", + "reqd": 1 + }, + { + "fieldname": "condition", + "fieldtype": "Code", + "label": "Condition" + }, + { + "depends_on": "eval:doc.event == 'Value Change'", + "fieldname": "field_to_check", + "fieldtype": "Select", + "label": "Field To Check" + }, + { + "default": "0", + "fieldname": "grant_only_once", + "fieldtype": "Check", + "label": "Grant only once" + }, + { + "default": "0", + "fieldname": "enabled", + "fieldtype": "Check", + "label": "Enabled", + "options": "1" + }, + { + "fieldname": "description", + "fieldtype": "Small Text", + "label": "Description", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2024-04-30 17:16:17.725459", + "modified_by": "Administrator", + "module": "LMS", + "name": "LMS Badge", + "naming_rule": "By fieldname", + "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": "creation", + "sort_order": "DESC", + "states": [], + "title_field": "title" +} \ No newline at end of file diff --git a/lms/lms/doctype/lms_badge/lms_badge.py b/lms/lms/doctype/lms_badge/lms_badge.py new file mode 100644 index 00000000..317a5641 --- /dev/null +++ b/lms/lms/doctype/lms_badge/lms_badge.py @@ -0,0 +1,72 @@ +# Copyright (c) 2024, Frappe and contributors +# For license information, please see license.txt + +import frappe +from frappe.model.document import Document + + +class LMSBadge(Document): + def apply(self, doc): + if self.rule_condition_satisfied(doc): + print("rule satisfied") + self.award(doc) + + def rule_condition_satisfied(self, doc): + doc_before_save = doc.get_doc_before_save() + if self.event == "New" and doc_before_save != None: + return False + print("its new") + if self.event == "Value Change": + field_to_check = self.field_to_check + if not self.field_to_check: + return False + if doc_before_save and doc_before_save.get(field_to_check) == doc.get( + field_to_check + ): + return False + + if self.condition: + print("found condition") + print(self.eval_condition(doc)) + return self.eval_condition(doc) + + return False + + def award(self, doc): + if self.grant_only_once: + if frappe.db.exists( + "LMS Badge Assignment", + {"badge": self.name, "user": frappe.session.user}, + ): + return + + assignment = frappe.new_doc("LMS Badge Assignment") + assignment.update( + { + "badge": self.name, + "user": frappe.session.user, + "issued_on": frappe.utils.now(), + } + ) + assignment.save() + + def eval_condition(self, doc): + return self.condition and frappe.safe_eval( + self.condition, None, {"doc": doc.as_dict()} + ) + + +def process_badges(doc, state): + if ( + frappe.flags.in_patch + or frappe.flags.in_install + or frappe.flags.in_migrate + or frappe.flags.in_import + or frappe.flags.in_setup_wizard + ): + return + + for d in frappe.cache_manager.get_doctype_map( + "LMS Badge", doc.doctype, dict(reference_doctype=doc.doctype, enabled=1) + ): + frappe.get_doc("LMS Badge", d.get("name")).apply(doc) diff --git a/lms/lms/doctype/lms_badge/test_lms_badge.py b/lms/lms/doctype/lms_badge/test_lms_badge.py new file mode 100644 index 00000000..fc84ce8d --- /dev/null +++ b/lms/lms/doctype/lms_badge/test_lms_badge.py @@ -0,0 +1,9 @@ +# Copyright (c) 2024, Frappe and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestLMSBadge(FrappeTestCase): + pass diff --git a/lms/lms/doctype/lms_badge_assignment/__init__.py b/lms/lms/doctype/lms_badge_assignment/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lms/lms/doctype/lms_badge_assignment/lms_badge_assignment.js b/lms/lms/doctype/lms_badge_assignment/lms_badge_assignment.js new file mode 100644 index 00000000..89f1e58e --- /dev/null +++ b/lms/lms/doctype/lms_badge_assignment/lms_badge_assignment.js @@ -0,0 +1,14 @@ +// Copyright (c) 2024, Frappe and contributors +// For license information, please see license.txt + +frappe.ui.form.on("LMS Badge Assignment", { + refresh(frm) { + frm.set_query("member", function (doc) { + return { + filters: { + ignore_user_type: 1, + }, + }; + }); + }, +}); diff --git a/lms/lms/doctype/lms_badge_assignment/lms_badge_assignment.json b/lms/lms/doctype/lms_badge_assignment/lms_badge_assignment.json new file mode 100644 index 00000000..c9d0284e --- /dev/null +++ b/lms/lms/doctype/lms_badge_assignment/lms_badge_assignment.json @@ -0,0 +1,113 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2024-04-30 11:58:44.096879", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "member", + "issued_on", + "column_break_ugix", + "badge", + "badge_image", + "badge_description" + ], + "fields": [ + { + "fieldname": "member", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Member", + "options": "User", + "reqd": 1 + }, + { + "fieldname": "badge", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Badge", + "options": "LMS Badge", + "reqd": 1 + }, + { + "fieldname": "issued_on", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Issued On", + "options": "Today", + "reqd": 1 + }, + { + "fetch_from": "badge.image", + "fieldname": "badge_image", + "fieldtype": "Attach", + "label": "Badge Image", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "column_break_ugix", + "fieldtype": "Column Break" + }, + { + "fetch_from": "badge.description", + "fieldname": "badge_description", + "fieldtype": "Small Text", + "label": "Badge Description", + "read_only": 1, + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2024-04-30 17:19:39.554248", + "modified_by": "Administrator", + "module": "LMS", + "name": "LMS Badge Assignment", + "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": "Moderator", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "LMS Student", + "share": 1, + "write": 1 + } + ], + "show_title_field_in_link": 1, + "sort_field": "creation", + "sort_order": "DESC", + "states": [], + "title_field": "member" +} \ No newline at end of file diff --git a/lms/lms/doctype/lms_badge_assignment/lms_badge_assignment.py b/lms/lms/doctype/lms_badge_assignment/lms_badge_assignment.py new file mode 100644 index 00000000..3416edad --- /dev/null +++ b/lms/lms/doctype/lms_badge_assignment/lms_badge_assignment.py @@ -0,0 +1,9 @@ +# Copyright (c) 2024, Frappe and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class LMSBadgeAssignment(Document): + pass diff --git a/lms/lms/doctype/lms_badge_assignment/test_lms_badge_assignment.py b/lms/lms/doctype/lms_badge_assignment/test_lms_badge_assignment.py new file mode 100644 index 00000000..9a037eda --- /dev/null +++ b/lms/lms/doctype/lms_badge_assignment/test_lms_badge_assignment.py @@ -0,0 +1,9 @@ +# Copyright (c) 2024, Frappe and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestLMSBadgeAssignment(FrappeTestCase): + pass