diff --git a/school/hooks.py b/school/hooks.py index 7a6d7368..29caa59c 100644 --- a/school/hooks.py +++ b/school/hooks.py @@ -141,6 +141,12 @@ website_route_rules = [ {"from_route": "/courses//learn/.", "to_route": "batch/learn"}, {"from_route": "/courses//progress", "to_route": "batch/progress"}, {"from_route": "/courses//join", "to_route": "batch/join"}, + {"from_route": "/courses//manage", "to_route": "cohorts"}, + {"from_route": "/courses//cohorts/", "to_route": "cohorts/cohort"}, + {"from_route": "/courses//cohorts//", "to_route": "cohorts/cohort"}, + {"from_route": "/courses//subgroups//", "to_route": "cohorts/subgroup"}, + {"from_route": "/courses//subgroups///", "to_route": "cohorts/subgroup"}, + {"from_route": "/courses//join///", "to_route": "cohorts/join"}, {"from_route": "/users", "to_route": "profiles/profile"} ] @@ -171,6 +177,26 @@ jinja = { # "school.plugins.LiveCodeExtension" #] +profile_mandatory_fields = [ + "first_name", + "last_name", + "user_image", + "bio", + "linkedin", + "education", + "work_experience", + "skill", + "preferred_functions", + "preferred_industries", + "dream_companies", + "attire", + "collaboration", + "role", + "location_preference", + "time", + "company_type" +] + ## Markdown Macros for Lessons school_markdown_macro_renderers = { "Exercise": "school.plugins.exercise_renderer", diff --git a/school/lms/api.py b/school/lms/api.py index b60d2d13..f6aee823 100644 --- a/school/lms/api.py +++ b/school/lms/api.py @@ -45,3 +45,119 @@ def save_current_lesson(course_name, lesson_name): doc.current_lesson = lesson_name doc.save(ignore_permissions=True) return {"current_lesson": doc.current_lesson} + + +@frappe.whitelist() +def join_cohort(course, cohort, subgroup, invite_code): + """Creates a Cohort Join Request for given user. + """ + course_doc = frappe.get_doc("LMS Course", course) + cohort_doc = course_doc and course_doc.get_cohort(cohort) + subgroup_doc = cohort_doc and cohort_doc.get_subgroup(subgroup) + + if not subgroup_doc or subgroup_doc.invite_code != invite_code: + return { + "ok": False, + "error": "Invalid join link" + } + + data = { + "doctype": "Cohort Join Request", + "cohort": cohort_doc.name, + "subgroup": subgroup_doc.name, + "email": frappe.session.user, + "status": "Pending" + } + # Don't insert duplicate records + if frappe.db.exists(data): + return {"ok": True, "status": "record found"} + else: + doc = frappe.get_doc(data) + doc.insert(ignore_permissions=True) + return {"ok": True, "status": "record created"} + +@frappe.whitelist() +def approve_cohort_join_request(join_request): + r = frappe.get_doc("Cohort Join Request", join_request) + sg = r and frappe.get_doc("Cohort Subgroup", r.subgroup) + if not sg or r.status not in ["Pending", "Accepted"]: + return { + "ok": False, + "error": "Invalid Join Request" + } + if not sg.is_manager(frappe.session.user) and "System Manager" not in frappe.get_roles(): + return { + "ok": False, + "error": "Permission Deined" + } + + r.status = "Accepted" + r.save(ignore_permissions=True) + return {"ok": True} + +@frappe.whitelist() +def reject_cohort_join_request(join_request): + r = frappe.get_doc("Cohort Join Request", join_request) + sg = r and frappe.get_doc("Cohort Subgroup", r.subgroup) + if not sg or r.status not in ["Pending", "Rejected"]: + return { + "ok": False, + "error": "Invalid Join Request" + } + if not sg.is_manager(frappe.session.user) and "System Manager" not in frappe.get_roles(): + return { + "ok": False, + "error": "Permission Deined" + } + + r.status = "Rejected" + r.save(ignore_permissions=True) + return {"ok": True} + + +@frappe.whitelist() +def undo_reject_cohort_join_request(join_request): + r = frappe.get_doc("Cohort Join Request", join_request) + sg = r and frappe.get_doc("Cohort Subgroup", r.subgroup) + # keeping Pending as well to consider the case of duplicate requests + if not sg or r.status not in ["Pending", "Rejected"]: + return { + "ok": False, + "error": "Invalid Join Request" + } + if not sg.is_manager(frappe.session.user) and "System Manager" not in frappe.get_roles(): + return { + "ok": False, + "error": "Permission Deined" + } + + r.status = "Pending" + r.save(ignore_permissions=True) + return {"ok": True} + +@frappe.whitelist() +def add_mentor_to_subgroup(subgroup, email): + try: + sg = frappe.get_doc("Cohort Subgroup", subgroup) + except frappe.DoesNotExistError: + return { + "ok": False, + "error": f"Invalid subgroup: {subgroup}" + } + + if not sg.get_cohort().is_admin(frappe.session.user) and "System Manager" not in frappe.get_roles(): + return { + "ok": False, + "error": "Permission Deined" + } + + try: + user = frappe.get_doc("User", email) + except frappe.DoesNotExistError: + return { + "ok": False, + "error": f"Invalid user: {email}" + } + + sg.add_mentor(email) + return {"ok": True} diff --git a/school/lms/doctype/certification/certification.json b/school/lms/doctype/certification/certification.json index 8aef0e9b..cbd5be2e 100644 --- a/school/lms/doctype/certification/certification.json +++ b/school/lms/doctype/certification/certification.json @@ -1,6 +1,7 @@ { "actions": [], "allow_rename": 1, + "autoname": "hash", "creation": "2021-12-07 12:20:37.143096", "doctype": "DocType", "editable_grid": 1, @@ -61,10 +62,11 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-12-14 11:42:24.844113", + "modified": "2021-12-21 10:05:43.377876", "modified_by": "Administrator", "module": "LMS", "name": "Certification", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "sort_field": "modified", diff --git a/school/lms/doctype/cohort/__init__.py b/school/lms/doctype/cohort/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/school/lms/doctype/cohort/cohort.js b/school/lms/doctype/cohort/cohort.js new file mode 100644 index 00000000..002b8b3c --- /dev/null +++ b/school/lms/doctype/cohort/cohort.js @@ -0,0 +1,8 @@ +// Copyright (c) 2021, FOSS United and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Cohort', { + // refresh: function(frm) { + + // } +}); diff --git a/school/lms/doctype/cohort/cohort.json b/school/lms/doctype/cohort/cohort.json new file mode 100644 index 00000000..2efa1428 --- /dev/null +++ b/school/lms/doctype/cohort/cohort.json @@ -0,0 +1,131 @@ +{ + "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", + "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": "2021-12-16 14:44:25.406301", + "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", + "track_changes": 1 +} \ No newline at end of file diff --git a/school/lms/doctype/cohort/cohort.py b/school/lms/doctype/cohort/cohort.py new file mode 100644 index 00000000..429baac5 --- /dev/null +++ b/school/lms/doctype/cohort/cohort.py @@ -0,0 +1,85 @@ +# 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()}/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 Batch Membership") + 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 Batch Membership"), + "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) diff --git a/school/lms/doctype/cohort/test_cohort.py b/school/lms/doctype/cohort/test_cohort.py new file mode 100644 index 00000000..a7cf10ba --- /dev/null +++ b/school/lms/doctype/cohort/test_cohort.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, FOSS United and Contributors +# See license.txt + +# import frappe +import unittest + +class TestCohort(unittest.TestCase): + pass diff --git a/school/lms/doctype/cohort_join_request/__init__.py b/school/lms/doctype/cohort_join_request/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/school/lms/doctype/cohort_join_request/cohort_join_request.js b/school/lms/doctype/cohort_join_request/cohort_join_request.js new file mode 100644 index 00000000..8f6cec18 --- /dev/null +++ b/school/lms/doctype/cohort_join_request/cohort_join_request.js @@ -0,0 +1,8 @@ +// Copyright (c) 2021, FOSS United and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Cohort Join Request', { + // refresh: function(frm) { + + // } +}); diff --git a/school/lms/doctype/cohort_join_request/cohort_join_request.json b/school/lms/doctype/cohort_join_request/cohort_join_request.json new file mode 100644 index 00000000..8ecd4be3 --- /dev/null +++ b/school/lms/doctype/cohort_join_request/cohort_join_request.json @@ -0,0 +1,76 @@ +{ + "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": "2021-12-16 15:06:03.985221", + "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 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/school/lms/doctype/cohort_join_request/cohort_join_request.py b/school/lms/doctype/cohort_join_request/cohort_join_request.py new file mode 100644 index 00000000..95c975f8 --- /dev/null +++ b/school/lms/doctype/cohort_join_request/cohort_join_request.py @@ -0,0 +1,51 @@ +# 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 Batch Membership", + "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 Batch Membership", + "course": cohort.course, + "member": self.email, + "member_type": "Student" + } + name = frappe.db.exists(q) + if name: + doc = frappe.get_doc("LMS Batch Membership", 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 Batch Membership", + "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) diff --git a/school/lms/doctype/cohort_join_request/test_cohort_join_request.py b/school/lms/doctype/cohort_join_request/test_cohort_join_request.py new file mode 100644 index 00000000..dd386401 --- /dev/null +++ b/school/lms/doctype/cohort_join_request/test_cohort_join_request.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, FOSS United and Contributors +# See license.txt + +# import frappe +import unittest + +class TestCohortJoinRequest(unittest.TestCase): + pass diff --git a/school/lms/doctype/cohort_mentor/__init__.py b/school/lms/doctype/cohort_mentor/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/school/lms/doctype/cohort_mentor/cohort_mentor.js b/school/lms/doctype/cohort_mentor/cohort_mentor.js new file mode 100644 index 00000000..81f90950 --- /dev/null +++ b/school/lms/doctype/cohort_mentor/cohort_mentor.js @@ -0,0 +1,8 @@ +// Copyright (c) 2021, FOSS United and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Cohort Mentor', { + // refresh: function(frm) { + + // } +}); diff --git a/school/lms/doctype/cohort_mentor/cohort_mentor.json b/school/lms/doctype/cohort_mentor/cohort_mentor.json new file mode 100644 index 00000000..42aade7a --- /dev/null +++ b/school/lms/doctype/cohort_mentor/cohort_mentor.json @@ -0,0 +1,73 @@ +{ + "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 +} \ No newline at end of file diff --git a/school/lms/doctype/cohort_mentor/cohort_mentor.py b/school/lms/doctype/cohort_mentor/cohort_mentor.py new file mode 100644 index 00000000..71bf9c4b --- /dev/null +++ b/school/lms/doctype/cohort_mentor/cohort_mentor.py @@ -0,0 +1,12 @@ +# 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) diff --git a/school/lms/doctype/cohort_mentor/test_cohort_mentor.py b/school/lms/doctype/cohort_mentor/test_cohort_mentor.py new file mode 100644 index 00000000..06be484e --- /dev/null +++ b/school/lms/doctype/cohort_mentor/test_cohort_mentor.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, FOSS United and Contributors +# See license.txt + +# import frappe +import unittest + +class TestCohortMentor(unittest.TestCase): + pass diff --git a/school/lms/doctype/cohort_staff/__init__.py b/school/lms/doctype/cohort_staff/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/school/lms/doctype/cohort_staff/cohort_staff.js b/school/lms/doctype/cohort_staff/cohort_staff.js new file mode 100644 index 00000000..8deaab23 --- /dev/null +++ b/school/lms/doctype/cohort_staff/cohort_staff.js @@ -0,0 +1,8 @@ +// Copyright (c) 2021, FOSS United and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Cohort Staff', { + // refresh: function(frm) { + + // } +}); diff --git a/school/lms/doctype/cohort_staff/cohort_staff.json b/school/lms/doctype/cohort_staff/cohort_staff.json new file mode 100644 index 00000000..dca4e0f9 --- /dev/null +++ b/school/lms/doctype/cohort_staff/cohort_staff.json @@ -0,0 +1,77 @@ +{ + "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 +} \ No newline at end of file diff --git a/school/lms/doctype/cohort_staff/cohort_staff.py b/school/lms/doctype/cohort_staff/cohort_staff.py new file mode 100644 index 00000000..6febbcc4 --- /dev/null +++ b/school/lms/doctype/cohort_staff/cohort_staff.py @@ -0,0 +1,8 @@ +# 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 diff --git a/school/lms/doctype/cohort_staff/test_cohort_staff.py b/school/lms/doctype/cohort_staff/test_cohort_staff.py new file mode 100644 index 00000000..c27f423e --- /dev/null +++ b/school/lms/doctype/cohort_staff/test_cohort_staff.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, FOSS United and Contributors +# See license.txt + +# import frappe +import unittest + +class TestCohortStaff(unittest.TestCase): + pass diff --git a/school/lms/doctype/cohort_subgroup/__init__.py b/school/lms/doctype/cohort_subgroup/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/school/lms/doctype/cohort_subgroup/cohort_subgroup.js b/school/lms/doctype/cohort_subgroup/cohort_subgroup.js new file mode 100644 index 00000000..9c851cf0 --- /dev/null +++ b/school/lms/doctype/cohort_subgroup/cohort_subgroup.js @@ -0,0 +1,8 @@ +// Copyright (c) 2021, FOSS United and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Cohort Subgroup', { + // refresh: function(frm) { + + // } +}); diff --git a/school/lms/doctype/cohort_subgroup/cohort_subgroup.json b/school/lms/doctype/cohort_subgroup/cohort_subgroup.json new file mode 100644 index 00000000..06bc00ab --- /dev/null +++ b/school/lms/doctype/cohort_subgroup/cohort_subgroup.json @@ -0,0 +1,104 @@ +{ + "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 +} \ No newline at end of file diff --git a/school/lms/doctype/cohort_subgroup/cohort_subgroup.py b/school/lms/doctype/cohort_subgroup/cohort_subgroup.py new file mode 100644 index 00000000..dfb810b0 --- /dev/null +++ b/school/lms/doctype/cohort_subgroup/cohort_subgroup.py @@ -0,0 +1,95 @@ +# 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()}/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()}/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 Batch Membership", + "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 Batch Membership", + 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")) diff --git a/school/lms/doctype/cohort_subgroup/test_cohort_subgroup.py b/school/lms/doctype/cohort_subgroup/test_cohort_subgroup.py new file mode 100644 index 00000000..8715a234 --- /dev/null +++ b/school/lms/doctype/cohort_subgroup/test_cohort_subgroup.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, FOSS United and Contributors +# See license.txt + +# import frappe +import unittest + +class TestCohortSubgroup(unittest.TestCase): + pass diff --git a/school/lms/doctype/cohort_web_page/__init__.py b/school/lms/doctype/cohort_web_page/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/school/lms/doctype/cohort_web_page/cohort_web_page.json b/school/lms/doctype/cohort_web_page/cohort_web_page.json new file mode 100644 index 00000000..fac05f0e --- /dev/null +++ b/school/lms/doctype/cohort_web_page/cohort_web_page.json @@ -0,0 +1,64 @@ +{ + "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" +} \ No newline at end of file diff --git a/school/lms/doctype/cohort_web_page/cohort_web_page.py b/school/lms/doctype/cohort_web_page/cohort_web_page.py new file mode 100644 index 00000000..b290954e --- /dev/null +++ b/school/lms/doctype/cohort_web_page/cohort_web_page.py @@ -0,0 +1,9 @@ +# 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 diff --git a/school/lms/doctype/education_detail/education_detail.json b/school/lms/doctype/education_detail/education_detail.json index e448a4b4..70f085dc 100644 --- a/school/lms/doctype/education_detail/education_detail.json +++ b/school/lms/doctype/education_detail/education_detail.json @@ -1,6 +1,7 @@ { "actions": [], "allow_rename": 1, + "autoname": "hash", "creation": "2021-12-07 12:15:46.078717", "doctype": "DocType", "editable_grid": 1, @@ -74,10 +75,11 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-12-10 12:12:58.827429", + "modified": "2021-12-21 09:58:42.343823", "modified_by": "Administrator", "module": "LMS", "name": "Education Detail", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "sort_field": "modified", diff --git a/school/lms/doctype/exercise/exercise.py b/school/lms/doctype/exercise/exercise.py index a7e3308d..faccd1b7 100644 --- a/school/lms/doctype/exercise/exercise.py +++ b/school/lms/doctype/exercise/exercise.py @@ -36,7 +36,7 @@ class Exercise(Document): return old_submission course = frappe.get_doc("LMS Course", self.course) - batch = course.get_student_batch(user) + member = course.get_membership(frappe.session.user) doc = frappe.get_doc( doctype="Exercise Submission", @@ -44,8 +44,9 @@ class Exercise(Document): exercise_title=self.title, course=self.course, lesson=self.lesson, - batch=batch and batch.name, - solution=code) + batch=member.batch, + solution=code, + member=member.name) doc.insert(ignore_permissions=True) return doc diff --git a/school/lms/doctype/exercise/test_exercise.py b/school/lms/doctype/exercise/test_exercise.py index c68f3212..9a4a45fd 100644 --- a/school/lms/doctype/exercise/test_exercise.py +++ b/school/lms/doctype/exercise/test_exercise.py @@ -6,6 +6,7 @@ import unittest class TestExercise(unittest.TestCase): def setUp(self): + frappe.db.sql('delete from `tabLMS Batch Membership`') frappe.db.sql('delete from `tabExercise Submission`') frappe.db.sql('delete from `tabExercise`') frappe.db.sql('delete from `tabLMS Course`') @@ -19,6 +20,12 @@ class TestExercise(unittest.TestCase): "description": "Test Course" }) course.insert() + member = frappe.get_doc({ + "doctype": "LMS Batch Membership", + "course": course.name, + "member": frappe.session.user + }) + member.insert() e = frappe.get_doc({ "doctype": "Exercise", "name": "test-problem", diff --git a/school/lms/doctype/exercise_latest_submission/__init__.py b/school/lms/doctype/exercise_latest_submission/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/school/lms/doctype/exercise_latest_submission/exercise_latest_submission.js b/school/lms/doctype/exercise_latest_submission/exercise_latest_submission.js new file mode 100644 index 00000000..aea1f03d --- /dev/null +++ b/school/lms/doctype/exercise_latest_submission/exercise_latest_submission.js @@ -0,0 +1,8 @@ +// Copyright (c) 2021, Frappe and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Exercise Latest Submission', { + // refresh: function(frm) { + + // } +}); diff --git a/school/lms/doctype/exercise_latest_submission/exercise_latest_submission.json b/school/lms/doctype/exercise_latest_submission/exercise_latest_submission.json new file mode 100644 index 00000000..16ee0a6f --- /dev/null +++ b/school/lms/doctype/exercise_latest_submission/exercise_latest_submission.json @@ -0,0 +1,166 @@ +{ + "actions": [], + "creation": "2021-12-08 17:56:26.049675", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "exercise", + "status", + "batch", + "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": "Exercise", + "search_index": 1 + }, + { + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "options": "Correct\nIncorrect" + }, + { + "fieldname": "batch", + "fieldtype": "Link", + "label": "Batch", + "options": "LMS Batch" + }, + { + "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 Batch Membership" + }, + { + "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.312861", + "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 +} \ No newline at end of file diff --git a/school/lms/doctype/exercise_latest_submission/exercise_latest_submission.py b/school/lms/doctype/exercise_latest_submission/exercise_latest_submission.py new file mode 100644 index 00000000..43a7f5cd --- /dev/null +++ b/school/lms/doctype/exercise_latest_submission/exercise_latest_submission.py @@ -0,0 +1,8 @@ +# 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 diff --git a/school/lms/doctype/exercise_latest_submission/test_exercise_latest_submission.py b/school/lms/doctype/exercise_latest_submission/test_exercise_latest_submission.py new file mode 100644 index 00000000..76825ecd --- /dev/null +++ b/school/lms/doctype/exercise_latest_submission/test_exercise_latest_submission.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe and Contributors +# See license.txt + +# import frappe +import unittest + +class TestExerciseLatestSubmission(unittest.TestCase): + pass diff --git a/school/lms/doctype/exercise_submission/exercise_submission.json b/school/lms/doctype/exercise_submission/exercise_submission.json index 229e9de6..def5f1f7 100644 --- a/school/lms/doctype/exercise_submission/exercise_submission.json +++ b/school/lms/doctype/exercise_submission/exercise_submission.json @@ -16,7 +16,8 @@ "solution", "image", "test_results", - "comments" + "comments", + "member" ], "fields": [ { @@ -90,11 +91,17 @@ { "fieldname": "section_break_8", "fieldtype": "Section Break" + }, + { + "fieldname": "member", + "fieldtype": "Link", + "label": "Member", + "options": "LMS Batch Membership" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-09-29 15:27:57.273879", + "modified": "2021-12-08 22:25:05.809376", "modified_by": "Administrator", "module": "LMS", "name": "Exercise Submission", diff --git a/school/lms/doctype/exercise_submission/exercise_submission.py b/school/lms/doctype/exercise_submission/exercise_submission.py index d588281c..7209826f 100644 --- a/school/lms/doctype/exercise_submission/exercise_submission.py +++ b/school/lms/doctype/exercise_submission/exercise_submission.py @@ -5,4 +5,20 @@ import frappe from frappe.model.document import Document class ExerciseSubmission(Document): - pass + 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) diff --git a/school/lms/doctype/function/function.json b/school/lms/doctype/function/function.json index 269ef60e..918ceb3e 100644 --- a/school/lms/doctype/function/function.json +++ b/school/lms/doctype/function/function.json @@ -19,7 +19,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-12-14 14:02:46.474260", + "modified": "2021-12-21 09:34:35.018280", "modified_by": "Administrator", "module": "LMS", "name": "Function", @@ -37,6 +37,16 @@ "role": "System Manager", "share": 1, "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "select": 1, + "share": 1 } ], "sort_field": "modified", diff --git a/school/lms/doctype/industry/industry.json b/school/lms/doctype/industry/industry.json index c0c2ce2d..2ab23031 100644 --- a/school/lms/doctype/industry/industry.json +++ b/school/lms/doctype/industry/industry.json @@ -19,7 +19,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-12-14 14:38:10.405473", + "modified": "2021-12-21 09:35:20.443192", "modified_by": "Administrator", "module": "LMS", "name": "Industry", @@ -37,6 +37,16 @@ "role": "System Manager", "share": 1, "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "select": 1, + "share": 1 } ], "sort_field": "modified", diff --git a/school/lms/doctype/lms_batch_membership/lms_batch_membership.json b/school/lms/doctype/lms_batch_membership/lms_batch_membership.json index 69720d29..35f74e35 100644 --- a/school/lms/doctype/lms_batch_membership/lms_batch_membership.json +++ b/school/lms/doctype/lms_batch_membership/lms_batch_membership.json @@ -6,15 +6,19 @@ "engine": "InnoDB", "field_order": [ "course", - "member", - "member_name", - "member_username", + "cohort", + "subgroup", "column_break_3", "batch", + "current_lesson", + "role", + "member_section", + "member", "member_type", "progress", - "current_lesson", - "role" + "column_break_12", + "member_name", + "member_username" ], "fields": [ { @@ -88,12 +92,32 @@ "fieldtype": "Data", "label": "Progress", "read_only": 1 + }, + { + "fieldname": "cohort", + "fieldtype": "Link", + "label": "Cohort", + "options": "Cohort" + }, + { + "fieldname": "subgroup", + "fieldtype": "Link", + "label": "Subgroup", + "options": "Cohort Subgroup" + }, + { + "fieldname": "member_section", + "fieldtype": "Section Break", + "label": "Member" + }, + { + "fieldname": "column_break_12", + "fieldtype": "Column Break" } ], "index_web_pages_for_search": 1, "links": [], - "migration_hash": "fe10c462acf5e727d864305d7ce90e73", - "modified": "2021-10-20 15:10:33.767419", + "modified": "2021-12-16 14:49:25.964853", "modified_by": "Administrator", "module": "LMS", "name": "LMS Batch Membership", diff --git a/school/lms/doctype/lms_course/lms_course.py b/school/lms/doctype/lms_course/lms_course.py index b9993cbe..9562445c 100644 --- a/school/lms/doctype/lms_course/lms_course.py +++ b/school/lms/doctype/lms_course/lms_course.py @@ -210,6 +210,28 @@ class LMSCourse(Document): visibility="Public") return batches + def get_cohorts(self): + return find_all("Cohort", course=self.name, order_by="creation") + + def get_cohort(self, cohort_slug): + name = frappe.get_value("Cohort", {"course": self.name, "slug": cohort_slug}) + return name and frappe.get_doc("Cohort", name) + + def is_cohort_staff(self, user_email): + """Returns True if the user is either a mentor or a staff for one or more active cohorts of this course. + """ + q1 = { + "doctype": "Cohort Staff", + "course": self.name, + "email": user_email + } + q2 = { + "doctype": "Cohort Mentor", + "course": self.name, + "email": user_email + } + return frappe.db.exists(q1) or frappe.db.exists(q2) + def get_lesson_index(self, lesson_name): """Returns the {chapter_index}.{lesson_index} for the lesson. """ diff --git a/school/lms/doctype/lms_settings/lms_settings.json b/school/lms/doctype/lms_settings/lms_settings.json index 21b97e57..8f890332 100644 --- a/school/lms/doctype/lms_settings/lms_settings.json +++ b/school/lms/doctype/lms_settings/lms_settings.json @@ -6,15 +6,15 @@ "engine": "InnoDB", "field_order": [ "livecode_url", - "column_break_2", "email_sender", "verify_age", + "column_break_2", + "force_profile_completion", + "show_search", + "search_placeholder", "mentor_request_section", "mentor_request_creation", - "mentor_request_status_update", - "search_settings_section", - "show_search", - "search_placeholder" + "mentor_request_status_update" ], "fields": [ { @@ -50,11 +50,6 @@ "fieldname": "column_break_2", "fieldtype": "Column Break" }, - { - "fieldname": "search_settings_section", - "fieldtype": "Section Break", - "label": "Search Settings" - }, { "default": "0", "fieldname": "show_search", @@ -72,12 +67,18 @@ "fieldname": "verify_age", "fieldtype": "Check", "label": "Verify Age during Signup" + }, + { + "default": "0", + "fieldname": "force_profile_completion", + "fieldtype": "Check", + "label": "Force users to complete their Profile" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-12-16 17:13:42.423197", + "modified": "2021-12-21 11:32:28.196404", "modified_by": "Administrator", "module": "LMS", "name": "LMS Settings", @@ -97,4 +98,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/school/lms/doctype/lms_settings/lms_settings.py b/school/lms/doctype/lms_settings/lms_settings.py index 90ecdd6d..0681a991 100644 --- a/school/lms/doctype/lms_settings/lms_settings.py +++ b/school/lms/doctype/lms_settings/lms_settings.py @@ -3,8 +3,18 @@ # For license information, please see license.txt from __future__ import unicode_literals -# import frappe +import frappe from frappe.model.document import Document class LMSSettings(Document): pass + +@frappe.whitelist() +def check_profile_restriction(): + force_profile_completion = frappe.db.get_single_value("LMS Settings", "force_profile_completion") + user = frappe.db.get_value("User", frappe.session.user, ["profile_complete", "username"], as_dict=True) + return { + "restrict": force_profile_completion and not user.profile_complete, + "username": user.username, + "prefix": frappe.get_hooks("profile_url_prefix")[0] or "/users/" + } diff --git a/school/lms/doctype/preferred_function/preferred_function.json b/school/lms/doctype/preferred_function/preferred_function.json index c47b5d81..d4bafd05 100644 --- a/school/lms/doctype/preferred_function/preferred_function.json +++ b/school/lms/doctype/preferred_function/preferred_function.json @@ -1,6 +1,7 @@ { "actions": [], "allow_rename": 1, + "autoname": "hash", "creation": "2021-12-14 14:42:48.823215", "doctype": "DocType", "editable_grid": 1, @@ -12,19 +13,20 @@ { "fieldname": "function", "fieldtype": "Link", + "ignore_user_permissions": 1, "in_list_view": 1, "label": "Function", - "options": "Function", - "reqd": 1 + "options": "Function" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-12-14 14:42:48.823215", + "modified": "2021-12-21 10:07:01.448239", "modified_by": "Administrator", "module": "LMS", "name": "Preferred Function", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "sort_field": "modified", diff --git a/school/lms/doctype/preferred_industry/preferred_industry.json b/school/lms/doctype/preferred_industry/preferred_industry.json index d4be8c76..9e5c1402 100644 --- a/school/lms/doctype/preferred_industry/preferred_industry.json +++ b/school/lms/doctype/preferred_industry/preferred_industry.json @@ -1,6 +1,7 @@ { "actions": [], "allow_rename": 1, + "autoname": "hash", "creation": "2021-12-14 14:44:06.808797", "doctype": "DocType", "editable_grid": 1, @@ -21,10 +22,11 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-12-14 14:44:21.215262", + "modified": "2021-12-21 10:07:14.537564", "modified_by": "Administrator", "module": "LMS", "name": "Preferred Industry", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "sort_field": "modified", diff --git a/school/lms/doctype/skill/skill.json b/school/lms/doctype/skill/skill.json index 70fd28e3..c9d1820e 100644 --- a/school/lms/doctype/skill/skill.json +++ b/school/lms/doctype/skill/skill.json @@ -19,7 +19,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-12-12 10:02:39.584832", + "modified": "2021-12-21 09:35:44.265910", "modified_by": "Administrator", "module": "LMS", "name": "Skill", @@ -37,6 +37,16 @@ "role": "System Manager", "share": 1, "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "select": 1, + "share": 1 } ], "sort_field": "modified", diff --git a/school/lms/doctype/skills/skills.json b/school/lms/doctype/skills/skills.json index 21bdcade..856b0053 100644 --- a/school/lms/doctype/skills/skills.json +++ b/school/lms/doctype/skills/skills.json @@ -1,6 +1,7 @@ { "actions": [], "allow_rename": 1, + "autoname": "hash", "creation": "2021-12-07 12:22:44.139341", "doctype": "DocType", "editable_grid": 1, @@ -21,10 +22,11 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-12-13 14:42:43.962186", + "modified": "2021-12-21 09:59:31.631132", "modified_by": "Administrator", "module": "LMS", "name": "Skills", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "sort_field": "modified", diff --git a/school/lms/doctype/work_experience/work_experience.json b/school/lms/doctype/work_experience/work_experience.json index a5f0fa45..1aea3779 100644 --- a/school/lms/doctype/work_experience/work_experience.json +++ b/school/lms/doctype/work_experience/work_experience.json @@ -1,6 +1,7 @@ { "actions": [], "allow_rename": 1, + "autoname": "hash", "creation": "2021-12-07 12:17:49.571045", "doctype": "DocType", "editable_grid": 1, @@ -70,10 +71,11 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-12-09 16:25:48.104205", + "modified": "2021-12-21 09:58:56.254035", "modified_by": "Administrator", "module": "LMS", "name": "Work Experience", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "sort_field": "modified", diff --git a/school/lms/web_form/profile/profile.js b/school/lms/web_form/profile/profile.js index 0822212c..f76ec156 100644 --- a/school/lms/web_form/profile/profile.js +++ b/school/lms/web_form/profile/profile.js @@ -14,7 +14,7 @@ frappe.ready(function () { frappe.web_form.validate = () => { let information_missing; const data = frappe.web_form.get_values(); - data.work_experience.forEach(exp => { + data.work_experience && data.work_experience.length && data.work_experience.forEach(exp => { if (!exp.current && !exp.to_date) { information_missing = true frappe.msgprint('To Date is mandatory in Work Experience.'); diff --git a/school/lms/web_form/profile/profile.json b/school/lms/web_form/profile/profile.json index e5a77c0d..c581e8c6 100644 --- a/school/lms/web_form/profile/profile.json +++ b/school/lms/web_form/profile/profile.json @@ -21,7 +21,7 @@ "is_standard": 1, "login_required": 1, "max_attachment_size": 0, - "modified": "2021-12-14 16:35:24.751439", + "modified": "2021-12-21 11:05:56.505121", "modified_by": "Administrator", "module": "LMS", "name": "profile", @@ -330,12 +330,12 @@ "fieldname": "preferred_location", "fieldtype": "Data", "hidden": 0, - "label": "Preferred Location", + "label": "Preferred Locations", "max_length": 0, "max_value": 0, "options": "", "read_only": 0, - "reqd": 1, + "reqd": 0, "show_in_filter": 0 }, { @@ -411,7 +411,7 @@ "label": "Location Preference", "max_length": 0, "max_value": 0, - "options": "Travel to work\nOffice close to Home", + "options": "Travel\nOffice close to Home", "read_only": 0, "reqd": 0, "show_in_filter": 0 @@ -456,4 +456,4 @@ "show_in_filter": 0 } ] -} \ No newline at end of file +} diff --git a/school/overrides/user.py b/school/overrides/user.py index 81779ccd..7626d0ce 100644 --- a/school/overrides/user.py +++ b/school/overrides/user.py @@ -15,6 +15,7 @@ class CustomUser(User): super(CustomUser, self).validate() self.validate_username_characters() self.validate_skills() + self.validate_completion() def validate_username_characters(self): if len(self.username): @@ -53,6 +54,12 @@ class CustomUser(User): if len(self.username) < 4: frappe.throw(_("Username cannot be less than 4 characters")) + def get_username_from_first_name(self): + return frappe.scrub(self.first_name) + str(random.randint(0, 99)) + + def remove_illegal_characters(self): + return re.sub("[^\w]+", "", self.username).strip("_") + def validate_skills(self): unique_skills = [] for skill in self.skill: @@ -63,12 +70,18 @@ class CustomUser(User): else: frappe.throw(_("Skills must be unique")) + def validate_completion(self): + all_fields_have_value = True + if frappe.db.get_single_value("LMS Settings", "force_profile_completion"): + profile_mandatory_fields = frappe.get_hooks("profile_mandatory_fields") + docfields = frappe.get_meta(self.doctype).fields - def get_username_from_first_name(self): - return frappe.scrub(self.first_name) + str(random.randint(0, 99)) + for field in profile_mandatory_fields: + if not self.get(field): + all_fields_have_value = False + break - def remove_illegal_characters(self): - return re.sub("[^\w]+", "", self.username).strip("_") + self.profile_complete = all_fields_have_value def get_authored_courses(self) -> int: """Returns the number of courses authored by this user. diff --git a/school/plugins.py b/school/plugins.py index 490a07ca..ceb01c01 100644 --- a/school/plugins.py +++ b/school/plugins.py @@ -86,6 +86,17 @@ class LiveCodeExtension(PageExtension): "templates/livecode/extension_footer.html", context) +def set_mandatory_fields_for_profile(): + profile_form = frappe.get_doc("Web Form", "profile") + profile_mandatory_fields = frappe.get_hooks("profile_mandatory_fields") + for field in profile_form.web_form_fields: + field.reqd = 0 + if field.fieldname in profile_mandatory_fields: + print(field.fieldname) + field.reqd = 1 + + profile_form.save() + def quiz_renderer(quiz_name): quiz = frappe.get_doc("LMS Quiz", quiz_name) context = dict(quiz=quiz) diff --git a/school/public/js/profile.js b/school/public/js/profile.js index b3f4bc28..b0aa3ccc 100644 --- a/school/public/js/profile.js +++ b/school/public/js/profile.js @@ -1,6 +1,10 @@ frappe.ready(() => { + hide_profile_for_guest_users(); +}); + +const hide_profile_for_guest_users = () => { if (frappe.session.user == "Guest") { var link_array = $('.nav-link').filter((i, elem) => $(elem).text().trim() === "My Profile"); link_array.length && $(link_array[0]).addClass("hide"); } -}) +}; diff --git a/school/templates/exercise.html b/school/templates/exercise.html index fca923b7..4af77867 100644 --- a/school/templates/exercise.html +++ b/school/templates/exercise.html @@ -1,5 +1,5 @@
-

Exercise {{exercise.index_label}}: {{ exercise.title }}

+

Exercise {{exercise.index_label}}: {{ exercise.title }}

{{frappe.utils.md_to_html(exercise.description)}}
{% set submission = exercise.get_user_submission() %} diff --git a/school/www/cohorts/__init__.py b/school/www/cohorts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/school/www/cohorts/base.html b/school/www/cohorts/base.html new file mode 100644 index 00000000..f0177dc4 --- /dev/null +++ b/school/www/cohorts/base.html @@ -0,0 +1,34 @@ +{% extends "templates/base.html" %} + +{% macro render_nav(nav) %} + + +{% endmacro %} + +{% block title %}Cohorts{% endblock %} +{% block head_include %} + + +{% endblock %} + + +{% block content %} +
+
+ {{ render_nav(nav | default([])) }} + + {% block page_content %} + Hello, world! + {% endblock %} +
+
+{% endblock %} + + diff --git a/school/www/cohorts/cohort.html b/school/www/cohorts/cohort.html new file mode 100644 index 00000000..007e5d70 --- /dev/null +++ b/school/www/cohorts/cohort.html @@ -0,0 +1,77 @@ +{% extends "www/cohorts/base.html" %} +{% block title %}Manage {{ course.title }}{% endblock %} + +{% block page_content %} +

{{cohort.title}} Cohort

+ +

+ {% set stats = cohort.get_stats() %} + + {{ stats.subgroups }} Subgroups + | {{ stats.mentors }} Mentors + | {{ stats.students }} students + | {{ stats.join_requests }} join requests +

+ +{% if is_mentor %} +
+ {% set sg = mentor.get_subgroup() %} +

You are a mentor of {{sg.title}} subgroup.

+

Visit Your Subgroup →

+
+{% endif %} + + + +
+{% if not page %} + {{ render_subgroups() }} +{% else %} + {{ render_page(page) }} +{% endif %} +
+ +{% endblock %} + +{% macro render_subgroups() %} +
    + {% for sg in cohort.get_subgroups(include_counts=True) %} +
  • + +
    + {{sg.num_mentors}} Mentors + | + {{sg.num_students}} Students + | + {{sg.num_join_requests}} Join Requests +
    +
  • + {% endfor %} +
+{% endmacro %} + +{% macro render_navitem(title, link, page, count=-1) %} + +{% endmacro %} diff --git a/school/www/cohorts/cohort.py b/school/www/cohorts/cohort.py new file mode 100644 index 00000000..64c63757 --- /dev/null +++ b/school/www/cohorts/cohort.py @@ -0,0 +1,32 @@ +import frappe +from . import utils + +def get_context(context): + context.no_cache = 1 + course = utils.get_course() + cohort = course and utils.get_cohort(course, frappe.form_dict["cohort"]) + if not cohort: + context.template = "www/404.html" + return + + user = frappe.session.user + mentor = cohort.get_mentor(user) + is_mentor = mentor is not None + is_admin = cohort.is_admin(user) or "System Manager" in frappe.get_roles() + + utils.add_nav(context, "All Courses", "/courses") + utils.add_nav(context, course.title, "/courses/" + course.name) + utils.add_nav(context, "Cohorts", "/courses/" + course.name + "/manage") + + context.course = course + context.cohort = cohort + context.mentor = mentor + context.is_mentor = is_mentor + context.is_admin = is_admin + context.page = frappe.form_dict.get("page") or "" + context.page_scope = "Cohort" + + # Function to render to custom page given the slug + context.render_page = lambda page: frappe.render_template( + cohort.get_page_template(page, scope="Cohort"), + context) diff --git a/school/www/cohorts/index.html b/school/www/cohorts/index.html new file mode 100644 index 00000000..68009df8 --- /dev/null +++ b/school/www/cohorts/index.html @@ -0,0 +1,38 @@ +{% extends "www/cohorts/base.html" %} +{% block title %}Manage {{ course.title }}{% endblock %} + +{% block page_content %} + {% if cohorts %} +

Cohorts

+
+ {% for cohort in cohorts %} +
+ {{ render_cohort(course, cohort) }} +
+ {% endfor %} +
+ {% else %} +

Permission Denied

+

You don't have permission to manage this course.

+ {% endif %} +{% endblock %} + +{% macro render_cohort(course, cohort) %} +
+
+
{{cohort.title}}
+
{{cohort.begin_date}} - {{cohort.end_date}}
+

+ {% set stats = cohort.get_stats() %} + + {{ stats.subgroups }} Subgroups + | {{ stats.mentors }} Mentors + | {{ stats.students }} students + | {{ stats.join_requests }} join requests +

+ + Manage +
+
+ +{% endmacro %} diff --git a/school/www/cohorts/index.py b/school/www/cohorts/index.py new file mode 100644 index 00000000..2d1f648b --- /dev/null +++ b/school/www/cohorts/index.py @@ -0,0 +1,31 @@ +import frappe +from .utils import get_course, add_nav + +def get_context(context): + context.no_cache = 1 + context.course = get_course() + if frappe.session.user == "Guest": + frappe.local.flags.redirect_location = "/login?redirect-to=" + frappe.request.path + raise frappe.Redirect() + + if not context.course: + context.template = "www/404.html" + return + + context.cohorts = get_cohorts(context.course) + if len(context.cohorts) == 1: + frappe.local.flags.redirect_location = context.cohorts[0].get_url() + raise frappe.Redirect + + add_nav(context, "All Courses", "/courses") + add_nav(context, context.course.title, "/courses/" + context.course.name) + +def get_cohorts(course): + if "System Manager" in frappe.get_roles(): + return course.get_cohorts() + + staff_roles = frappe.get_all("Cohort Staff", filters={"course": course.name}, fields=["cohort"]) + mentor_roles = frappe.get_all("Cohort Mentor", filters={"course": course.name}, fields=["cohort"]) + roles = staff_roles + mentor_roles + names = {role.cohort for role in roles} + return [frappe.get_doc("Cohort", name) for name in names] diff --git a/school/www/cohorts/join.html b/school/www/cohorts/join.html new file mode 100644 index 00000000..34b55c2e --- /dev/null +++ b/school/www/cohorts/join.html @@ -0,0 +1,88 @@ +{% extends "www/cohorts/base.html" %} + +{% block title %}Join Course{% endblock %} + +{% block page_content %} + +

Join Course

+ +

+ Course: {{course.title}} +

+

+ Cohort: {{cohort.title}} +

+

+ Subgroup: {{subgroup.title}} +

+ +{% if frappe.session.user == "Guest" %} + +
+

+ Please login to be able to join the course.

+ +

+ If you don't already have an account, you can sign up for a new account. +

+ Login to continue +
+{% elif subgroup.has_student(frappe.session.user) %} +
+

You are already a student of this course.

+ Start Learning → +
+{% elif subgroup.has_join_request(frappe.session.user) %} +
+

We have received your request to join the course. You'll hear back from us soon.

+
+{% else %} + +Join the course + +{% endif %} + +{% endblock %} + +{% block script %} + + + +{% endblock %} diff --git a/school/www/cohorts/join.py b/school/www/cohorts/join.py new file mode 100644 index 00000000..4b6368a3 --- /dev/null +++ b/school/www/cohorts/join.py @@ -0,0 +1,24 @@ +import frappe +from . import utils + +def get_context(context): + context.no_cache = 1 + + course = utils.get_course(frappe.form_dict["course"]) + cohort = course and utils.get_cohort(course, frappe.form_dict["cohort"]) + subgroup = cohort and utils.get_subgroup(cohort, frappe.form_dict["subgroup"]) + if not subgroup: + context.template = "www/404.html" + return + + invite_code = frappe.form_dict["invite_code"] + if subgroup.invite_code != invite_code: + context.template = "www/404.html" + return + + utils.add_nav(context, "All Courses", "/courses") + utils.add_nav(context, course.title, "/courses/" + course.name) + + context.course = course + context.cohort = cohort + context.subgroup = subgroup diff --git a/school/www/cohorts/subgroup.html b/school/www/cohorts/subgroup.html new file mode 100644 index 00000000..a7d69895 --- /dev/null +++ b/school/www/cohorts/subgroup.html @@ -0,0 +1,264 @@ +{% extends "www/cohorts/base.html" %} +{% block title %} Subgroup {{subgroup.title}} - {{ course.title }} {% endblock %} + +{% block page_content %} +

{{subgroup.title}} Subgroup

+ + +
+ {% if page == "info" %} + {{ render_info() }} + {% elif page == "mentors" %} + {{ render_mentors() }} + {% elif page == "students" %} + {{ render_students() }} + {% elif page == "join-requests" %} + {{ render_join_requests() }} + {% elif page == "admin" %} + {{ render_admin() }} + {% else %} + {{ render_page(page) }} + {% endif %} +
+{% endblock %} + +{% macro render_admin() %} +
+
Add a new mentor
+
+
+ +
+ +
+
+{% endmacro %} + +{% macro render_mentors() %} +
Mentors
+ {% set mentors = subgroup.get_mentors() %} + {% if mentors %} +
+ {% for m in mentors %} + {{ widgets.MemberCard(member=m, show_course_count=False, dimension_class="") }} + {% endfor %} +
+ {% else %} + None found. + {% endif %} +{% endmacro %} + + +{% macro render_students() %} + {% set students = subgroup.get_students() %} + {% if students %} +
+ {% for student in students %} + {{ widgets.MemberCard(member=student, show_course_count=False, dimension_class="") }} + {% endfor %} +
+ {% else %} + None found. + {% endif %} +{% endmacro %} + + +{% macro render_join_requests() %} +
Invite Link
+ {% set link = subgroup.get_invite_link() %} +

{{link}} +
+ Copy to Clipboard +

+ +{% set join_requests = subgroup.get_join_requests() %} +
Pending Requests
+ {% if join_requests %} + + + + + + + + {% for r in join_requests %} + + + + + + + {% endfor %} +
#WhenEmailActions
{{loop.index}}{{r.creation}}{{r.email}} + Approve | Reject
+ {% else %} +

There are no pending join requests.

+ {% endif %} + {% set rejected_requests = subgroup.get_join_requests(status="Rejected") %} + +
Rejected Requests
+ {% if rejected_requests %} + + + + + + + + {% for r in rejected_requests %} + + + + + + + {% endfor %} +
#WhenEmailActions
{{loop.index}}{{r.creation}}{{r.email}} + Undo
+ {% else %} +

There are no rejected requests.

+ {% endif %} +{% endmacro %} + +{% macro render_navitem(title, link, count, active) %} + +{% endmacro %} + + +{% block script %} + + +{% endblock %} + +{% block style %} + +{% endblock %} diff --git a/school/www/cohorts/subgroup.py b/school/www/cohorts/subgroup.py new file mode 100644 index 00000000..4ab8c056 --- /dev/null +++ b/school/www/cohorts/subgroup.py @@ -0,0 +1,67 @@ +import frappe +from . import utils + +def get_context(context): + context.no_cache = 1 + course = utils.get_course() + + cohort = utils.get_cohort(course, frappe.form_dict['cohort']) + subgroup = utils.get_subgroup(cohort, frappe.form_dict['subgroup']) + + if not subgroup: + context.template = "www/404.html" + return + + page = frappe.form_dict.get("page") + is_mentor = subgroup.is_mentor(frappe.session.user) + is_admin = cohort.is_admin(frappe.session.user) or "System Manager" in frappe.get_roles() + + if is_admin: + role = "Admin" + elif is_mentor: + role = "Mentor" + else: + role = "Public" + + pages = [ + ("mentors", ["Admin", "Mentor", "Public"]), + ("students", ["Admin", "Mentor", "Public"]), + ("join-requests", ["Admin", "Mentor"]), + ("admin", ["Admin"]) + ] + pages += [(p.slug, ["Admin", "Mentor"]) for p in cohort.get_pages(scope="Subgroup")] + + page_names = [p for p, roles in pages if role in roles] + + if page not in page_names: + frappe.local.flags.redirect_location = subgroup.get_url() + "/mentors" + raise frappe.Redirect + + utils.add_nav(context, "All Courses", "/courses") + utils.add_nav(context, course.title, f"/courses/{course.name}") + utils.add_nav(context, "Cohorts", f"/courses/{course.name}/manage") + utils.add_nav(context, cohort.title, f"/courses/{course.name}/cohorts/{cohort.slug}") + + context.course = course + context.cohort = cohort + context.subgroup = subgroup + context.stats = get_stats(subgroup) + context.page = page + context.is_admin = is_admin + context.is_mentor = is_mentor + context.page_scope = "Subgroup" + + # Function to render to custom page given the slug + context.render_page = lambda page: frappe.render_template( + cohort.get_page_template(page, scope="Subgroup"), + context) + +def get_stats(subgroup): + return { + "join_requests": len(subgroup.get_join_requests()), + "students": len(subgroup.get_students()), + "mentors": len(subgroup.get_mentors()) + } + +def has_page(cohort, page): + return cohort.get_page(page, scope="Subgroup") diff --git a/school/www/cohorts/utils.py b/school/www/cohorts/utils.py new file mode 100644 index 00000000..a35c7e9f --- /dev/null +++ b/school/www/cohorts/utils.py @@ -0,0 +1,25 @@ +import frappe + +def get_course(course_name=None): + course_name = course_name or frappe.form_dict["course"] + return course_name and get_doc("LMS Course", course_name) + +def get_doc(doctype, name): + try: + return frappe.get_doc(doctype, name) + except frappe.exceptions.DoesNotExistError: + return + +def get_cohort(course, cohort_slug): + name = frappe.get_value("Cohort", {"course": course.name, "slug": cohort_slug}) + return name and frappe.get_doc("Cohort", name) + +def get_subgroup(cohort, subgroup_slug): + name = frappe.get_value("Cohort Subgroup", {"cohort": cohort.name, "slug": subgroup_slug}) + return name and frappe.get_doc("Cohort Subgroup", name) + +def add_nav(context, title, href): + """Adds a breadcrumb to the navigation. + """ + nav = context.setdefault("nav", []) + nav.append({"title": title, "href": href}) diff --git a/school/www/courses/course.html b/school/www/courses/course.html index 364fdeec..17159034 100644 --- a/school/www/courses/course.html +++ b/school/www/courses/course.html @@ -43,9 +43,9 @@
- {% if not course.disable_self_learning and not membership and not course.upcoming %} + {% if not course.disable_self_learning and not membership and not course.upcoming and not restriction.restrict %}
- Start Learning + {{ _("Start Learning") }}
{% endif %} @@ -70,6 +70,14 @@
{% endif %} + + {% if course.is_cohort_staff(frappe.session.user) %} + + Manage the course + + {% endif %} diff --git a/school/www/courses/course.py b/school/www/courses/course.py index 71e1708e..39a891bd 100644 --- a/school/www/courses/course.py +++ b/school/www/courses/course.py @@ -1,4 +1,5 @@ import frappe +from school.lms.doctype.lms_settings.lms_settings import check_profile_restriction def get_context(context): context.no_cache = 1 @@ -20,6 +21,7 @@ def get_context(context): context.membership = membership if context.course.upcoming: context.is_user_interested = get_user_interest(context.course.name) + context.restriction = check_profile_restriction() context.metatags = { "title": course.title, "image": course.image, diff --git a/school/www/courses/index.html b/school/www/courses/index.html index 78843b7e..237d6cea 100644 --- a/school/www/courses/index.html +++ b/school/www/courses/index.html @@ -9,9 +9,16 @@ {% block content %}
+ {% if restriction.restrict %} + {% set site_link = " profile " %} +
+
{{ _("You haven't completed your profile.") }}
+

{{ _("Complete your {0} to access the courses.").format(site_link) }}

+
+ + {% else %} {% include "school/templates/search_course/search_course.html" %} -
{% set title = _("Live Courses") %} {% set courses = live_courses %} @@ -23,6 +30,7 @@ {% set classes = "upcoming-courses mt-10" %} {% include "school/templates/course_list.html" %}
+ {% endif %}
{% endblock %} diff --git a/school/www/courses/index.py b/school/www/courses/index.py index f4981077..7c3a37a7 100644 --- a/school/www/courses/index.py +++ b/school/www/courses/index.py @@ -1,8 +1,10 @@ import frappe +from school.lms.doctype.lms_settings.lms_settings import check_profile_restriction def get_context(context): context.no_cache = 1 context.live_courses, context.upcoming_courses = get_courses() + context.restriction = check_profile_restriction() context.metatags = { "title": "All Courses", "image": frappe.db.get_single_value("Website Settings", "banner_image"), diff --git a/school/www/profiles/profile.html b/school/www/profiles/profile.html index 2f02ed04..a2751a8f 100644 --- a/school/www/profiles/profile.html +++ b/school/www/profiles/profile.html @@ -290,12 +290,16 @@
{{ edu.institution_name }}
{{ edu.degree_type }} {{ edu.major }} - {% if not member.hide_private %} + {% if not member.hide_private %} - {% if edu.grade %} {{ edu.grade }} {% endif %} + {% if edu.grade %} {{ edu.grade }} {% endif %} {% endif %}
-
{{ frappe.utils.format_date(edu.start_date, "MMM YYYY") }} - {{ frappe.utils.format_date(edu.end_date, "MMM YYYY") }}
+
+ {% if edu.start_date %} + {{ frappe.utils.format_date(edu.start_date, "MMM YYYY") }} - + {% endif %} + {{ frappe.utils.format_date(edu.end_date, "MMM YYYY") }}
{{ edu.location }}
{% endfor %}