From 7cd57cadb2c641678c8fd3809d136f908c0ef0a8 Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Mon, 29 Nov 2021 17:26:38 +0530 Subject: [PATCH 01/37] feat: added doctypes for cohorts Issue #271 --- school/lms/doctype/cohort/__init__.py | 0 school/lms/doctype/cohort/cohort.js | 8 ++ school/lms/doctype/cohort/cohort.json | 135 ++++++++++++++++++ school/lms/doctype/cohort/cohort.py | 15 ++ school/lms/doctype/cohort/test_cohort.py | 8 ++ .../doctype/cohort_join_request/__init__.py | 0 .../cohort_join_request.js | 8 ++ .../cohort_join_request.json | 71 +++++++++ .../cohort_join_request.py | 13 ++ .../test_cohort_join_request.py | 8 ++ school/lms/doctype/cohort_mentor/__init__.py | 0 .../doctype/cohort_mentor/cohort_mentor.js | 8 ++ .../doctype/cohort_mentor/cohort_mentor.json | 73 ++++++++++ .../doctype/cohort_mentor/cohort_mentor.py | 8 ++ .../cohort_mentor/test_cohort_mentor.py | 8 ++ school/lms/doctype/cohort_staff/__init__.py | 0 .../lms/doctype/cohort_staff/cohort_staff.js | 8 ++ .../doctype/cohort_staff/cohort_staff.json | 72 ++++++++++ .../lms/doctype/cohort_staff/cohort_staff.py | 8 ++ .../doctype/cohort_staff/test_cohort_staff.py | 8 ++ .../lms/doctype/cohort_subgroup/__init__.py | 0 .../cohort_subgroup/cohort_subgroup.js | 8 ++ .../cohort_subgroup/cohort_subgroup.json | 76 ++++++++++ .../cohort_subgroup/cohort_subgroup.py | 45 ++++++ .../cohort_subgroup/test_cohort_subgroup.py | 8 ++ .../lms_batch_membership.json | 19 ++- 26 files changed, 612 insertions(+), 3 deletions(-) create mode 100644 school/lms/doctype/cohort/__init__.py create mode 100644 school/lms/doctype/cohort/cohort.js create mode 100644 school/lms/doctype/cohort/cohort.json create mode 100644 school/lms/doctype/cohort/cohort.py create mode 100644 school/lms/doctype/cohort/test_cohort.py create mode 100644 school/lms/doctype/cohort_join_request/__init__.py create mode 100644 school/lms/doctype/cohort_join_request/cohort_join_request.js create mode 100644 school/lms/doctype/cohort_join_request/cohort_join_request.json create mode 100644 school/lms/doctype/cohort_join_request/cohort_join_request.py create mode 100644 school/lms/doctype/cohort_join_request/test_cohort_join_request.py create mode 100644 school/lms/doctype/cohort_mentor/__init__.py create mode 100644 school/lms/doctype/cohort_mentor/cohort_mentor.js create mode 100644 school/lms/doctype/cohort_mentor/cohort_mentor.json create mode 100644 school/lms/doctype/cohort_mentor/cohort_mentor.py create mode 100644 school/lms/doctype/cohort_mentor/test_cohort_mentor.py create mode 100644 school/lms/doctype/cohort_staff/__init__.py create mode 100644 school/lms/doctype/cohort_staff/cohort_staff.js create mode 100644 school/lms/doctype/cohort_staff/cohort_staff.json create mode 100644 school/lms/doctype/cohort_staff/cohort_staff.py create mode 100644 school/lms/doctype/cohort_staff/test_cohort_staff.py create mode 100644 school/lms/doctype/cohort_subgroup/__init__.py create mode 100644 school/lms/doctype/cohort_subgroup/cohort_subgroup.js create mode 100644 school/lms/doctype/cohort_subgroup/cohort_subgroup.json create mode 100644 school/lms/doctype/cohort_subgroup/cohort_subgroup.py create mode 100644 school/lms/doctype/cohort_subgroup/test_cohort_subgroup.py 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..6c98c101 --- /dev/null +++ b/school/lms/doctype/cohort/cohort.json @@ -0,0 +1,135 @@ +{ + "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", + "slug", + "title", + "section_break_2", + "instructor", + "status", + "column_break_4", + "begin_date", + "end_date", + "duration", + "section_break_8", + "description" + ], + "fields": [ + { + "fieldname": "description", + "fieldtype": "Markdown Editor", + "label": "Description" + }, + { + "fieldname": "instructor", + "fieldtype": "Link", + "label": "Instructor", + "options": "User", + "reqd": 1 + }, + { + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "options": "Upcoming\nLive\nCompleted\nCancelled", + "reqd": 1 + }, + { + "fieldname": "begin_date", + "fieldtype": "Date", + "label": "Begin Date" + }, + { + "fieldname": "end_date", + "fieldtype": "Date", + "label": "End Date" + }, + { + "fieldname": "duration", + "fieldtype": "Data", + "label": "Duration" + }, + { + "fieldname": "slug", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Slug", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_8", + "fieldtype": "Section Break" + }, + { + "fieldname": "section_break_2", + "fieldtype": "Section Break" + }, + { + "fieldname": "title", + "fieldtype": "Data", + "in_list_view": 1, + "label": "title", + "reqd": 1 + }, + { + "fieldname": "course", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Course", + "options": "LMS Course", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [ + { + "group": "Links", + "link_doctype": "Cohort Subgroup", + "link_fieldname": "cohort" + }, + { + "group": "Links", + "link_doctype": "Cohort Student", + "link_fieldname": "cohort" + }, + { + "group": "Links", + "link_doctype": "Cohort Page", + "link_fieldname": "cohort" + } + ], + "modified": "2021-11-29 15:13:41.296384", + "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..bad383e3 --- /dev/null +++ b/school/lms/doctype/cohort/cohort.py @@ -0,0 +1,15 @@ +# 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_subgroups(self): + names = frappe.get_all("Cohort Subgroup", filters={"cohort": self.name}, pluck="name") + return [frappe.get_doc("Cohort Subgroup", name) for name in names] + + 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) 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..476d05ed --- /dev/null +++ b/school/lms/doctype/cohort_join_request/cohort_join_request.json @@ -0,0 +1,71 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2021-11-19 16:27:41.716509", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "cohort", + "subgroup", + "email", + "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" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-11-29 12:18:15.947544", + "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..984096ab --- /dev/null +++ b/school/lms/doctype/cohort_join_request/cohort_join_request.py @@ -0,0 +1,13 @@ +# 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): + pass 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..a385819c --- /dev/null +++ b/school/lms/doctype/cohort_mentor/cohort_mentor.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 CohortMentor(Document): + pass 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..1fd207d7 --- /dev/null +++ b/school/lms/doctype/cohort_staff/cohort_staff.json @@ -0,0 +1,72 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2021-11-19 15:35:00.551949", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "cohort", + "course", + "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": "E-mail", + "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 + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-11-29 16:31:57.214875", + "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..2b31c98e --- /dev/null +++ b/school/lms/doctype/cohort_subgroup/cohort_subgroup.json @@ -0,0 +1,76 @@ +{ + "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", + "title", + "description", + "invite_code" + ], + "fields": [ + { + "fieldname": "cohort", + "fieldtype": "Link", + "in_list_view": 1, + "in_preview": 1, + "in_standard_filter": 1, + "label": "Cohort", + "options": "Cohort" + }, + { + "fieldname": "title", + "fieldtype": "Data", + "in_list_view": 1, + "in_preview": 1, + "in_standard_filter": 1, + "label": "title" + }, + { + "fieldname": "description", + "fieldtype": "Markdown Editor", + "label": "description" + }, + { + "fieldname": "invite_code", + "fieldtype": "Data", + "label": "invite_code", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [ + { + "group": "Links", + "link_doctype": "Cohort Student", + "link_fieldname": "subgroup" + } + ], + "modified": "2021-11-29 16:57:51.660847", + "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..2f11e079 --- /dev/null +++ b/school/lms/doctype/cohort_subgroup/cohort_subgroup.py @@ -0,0 +1,45 @@ +# 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_invite_link(self): + return f"{frappe.utils.get_url()}/cohorts/{self.cohort}/join/{self.slug}/{self.invite_code}" + + def has_student(self, email): + """Check if given user is a student of this subgroup. + """ + q = { + "doctype": "Cohort Student", + "subgroup": self.name, + "email": 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") + + +#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/lms_batch_membership/lms_batch_membership.json b/school/lms/doctype/lms_batch_membership/lms_batch_membership.json index 69720d29..8514100f 100644 --- a/school/lms/doctype/lms_batch_membership/lms_batch_membership.json +++ b/school/lms/doctype/lms_batch_membership/lms_batch_membership.json @@ -14,7 +14,9 @@ "member_type", "progress", "current_lesson", - "role" + "role", + "cohort", + "subgroup" ], "fields": [ { @@ -88,12 +90,23 @@ "fieldtype": "Data", "label": "Progress", "read_only": 1 + }, + { + "fieldname": "cohort", + "fieldtype": "Link", + "label": "Cohort", + "options": "Cohort" + }, + { + "fieldname": "subgroup", + "fieldtype": "Link", + "label": "Subgroup", + "options": "Cohort Subgroup" } ], "index_web_pages_for_search": 1, "links": [], - "migration_hash": "fe10c462acf5e727d864305d7ce90e73", - "modified": "2021-10-20 15:10:33.767419", + "modified": "2021-11-29 15:22:31.609216", "modified_by": "Administrator", "module": "LMS", "name": "LMS Batch Membership", From 102fa9c0a853b5b650b784cfbdf712e057b49094 Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Mon, 29 Nov 2021 17:33:45 +0530 Subject: [PATCH 02/37] feat: added a page to list cohorts of a course - added a page /courses//manage that lists the active cohorts - only accessible to mentors and staff - also added a "manage the course" button on course page for mentors/staff Issue #271 --- school/hooks.py | 1 + school/lms/doctype/lms_course/lms_course.py | 18 +++++++++++ school/www/cohorts/base.html | 34 +++++++++++++++++++++ school/www/cohorts/index.html | 34 +++++++++++++++++++++ school/www/cohorts/index.py | 30 ++++++++++++++++++ school/www/cohorts/utils.py | 17 +++++++++++ school/www/courses/course.html | 7 +++++ 7 files changed, 141 insertions(+) create mode 100644 school/www/cohorts/base.html create mode 100644 school/www/cohorts/index.html create mode 100644 school/www/cohorts/index.py create mode 100644 school/www/cohorts/utils.py diff --git a/school/hooks.py b/school/hooks.py index 01d6d866..b04c9da9 100644 --- a/school/hooks.py +++ b/school/hooks.py @@ -141,6 +141,7 @@ 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": "/users", "to_route": "profiles/profile"} ] diff --git a/school/lms/doctype/lms_course/lms_course.py b/school/lms/doctype/lms_course/lms_course.py index b9993cbe..6815dd6e 100644 --- a/school/lms/doctype/lms_course/lms_course.py +++ b/school/lms/doctype/lms_course/lms_course.py @@ -210,6 +210,24 @@ class LMSCourse(Document): visibility="Public") return batches + def get_cohorts(self): + return find_all("Cohort", course=self.name, order_by="creation") + + 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/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/index.html b/school/www/cohorts/index.html new file mode 100644 index 00000000..dcb4c5d8 --- /dev/null +++ b/school/www/cohorts/index.html @@ -0,0 +1,34 @@ +{% 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}}
+ + Manage +
+
+ +{% endmacro %} diff --git a/school/www/cohorts/index.py b/school/www/cohorts/index.py new file mode 100644 index 00000000..5f27ef2f --- /dev/null +++ b/school/www/cohorts/index.py @@ -0,0 +1,30 @@ +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) + + 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 + print(roles) + names = {role.cohort for role in roles} + print(names) + return [frappe.get_doc("Cohort", name) for name in names] diff --git a/school/www/cohorts/utils.py b/school/www/cohorts/utils.py new file mode 100644 index 00000000..001d1e4d --- /dev/null +++ b/school/www/cohorts/utils.py @@ -0,0 +1,17 @@ +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 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..e457ee58 100644 --- a/school/www/courses/course.html +++ b/school/www/courses/course.html @@ -70,6 +70,13 @@ {% endif %} + + {% if course.is_cohort_staff(frappe.session.user) %} + + Manage the course + + {% endif %} From 1277cfed64f113311b6a919253d3abbdbaf73075 Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Tue, 30 Nov 2021 07:46:48 +0530 Subject: [PATCH 03/37] feat: added cohort and subgroup pages Issue #271 --- school/hooks.py | 4 + school/lms/doctype/cohort/cohort.py | 45 +++++- .../cohort_subgroup/cohort_subgroup.json | 31 +++- .../cohort_subgroup/cohort_subgroup.py | 18 ++- school/www/cohorts/cohort.html | 25 ++++ school/www/cohorts/cohort.py | 34 +++++ school/www/cohorts/subgroup.html | 136 ++++++++++++++++++ school/www/cohorts/subgroup.py | 33 +++++ school/www/cohorts/utils.py | 8 ++ 9 files changed, 327 insertions(+), 7 deletions(-) create mode 100644 school/www/cohorts/cohort.html create mode 100644 school/www/cohorts/cohort.py create mode 100644 school/www/cohorts/subgroup.html create mode 100644 school/www/cohorts/subgroup.py diff --git a/school/hooks.py b/school/hooks.py index b04c9da9..7eaed275 100644 --- a/school/hooks.py +++ b/school/hooks.py @@ -142,6 +142,10 @@ website_route_rules = [ {"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//subgroups//", "to_route": "cohorts/subgroup", "defaults": {"page": "info"}}, + {"from_route": "/courses//subgroups///students", "to_route": "cohorts/subgroup", "defaults": {"page": "students"}}, + {"from_route": "/courses//subgroups///join-requests", "to_route": "cohorts/subgroup", "defaults": {"page": "join-requests"}}, {"from_route": "/users", "to_route": "profiles/profile"} ] diff --git a/school/lms/doctype/cohort/cohort.py b/school/lms/doctype/cohort/cohort.py index bad383e3..91ba2004 100644 --- a/school/lms/doctype/cohort/cohort.py +++ b/school/lms/doctype/cohort/cohort.py @@ -5,11 +5,52 @@ import frappe from frappe.model.document import Document class Cohort(Document): - def get_subgroups(self): + def get_subgroups(self, include_counts=False): names = frappe.get_all("Cohort Subgroup", filters={"cohort": self.name}, pluck="name") - return [frappe.get_doc("Cohort Subgroup", name) for name in names] + subgroups = [frappe.get_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") + 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) + return subgroups + + def _get_subgroup_counts(self, doctype): + q = f""" + SELECT subgroup, count(*) as count + FROM `tab{doctype}` + WHERE cohort = %(cohort)s""" + rows = frappe.db.sql(q, values={"cohort": self.name}) + return {subgroup: count for subgroup, count in rows} 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_subgroup/cohort_subgroup.json b/school/lms/doctype/cohort_subgroup/cohort_subgroup.json index 2b31c98e..71732e24 100644 --- a/school/lms/doctype/cohort_subgroup/cohort_subgroup.json +++ b/school/lms/doctype/cohort_subgroup/cohort_subgroup.json @@ -8,9 +8,11 @@ "engine": "InnoDB", "field_order": [ "cohort", + "slug", "title", "description", - "invite_code" + "invite_code", + "course" ], "fields": [ { @@ -20,7 +22,8 @@ "in_preview": 1, "in_standard_filter": 1, "label": "Cohort", - "options": "Cohort" + "options": "Cohort", + "reqd": 1 }, { "fieldname": "title", @@ -28,7 +31,8 @@ "in_list_view": 1, "in_preview": 1, "in_standard_filter": 1, - "label": "title" + "label": "title", + "reqd": 1 }, { "fieldname": "description", @@ -40,6 +44,20 @@ "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 } ], "index_web_pages_for_search": 1, @@ -48,9 +66,14 @@ "group": "Links", "link_doctype": "Cohort Student", "link_fieldname": "subgroup" + }, + { + "group": "Links", + "link_doctype": "Cohort Join Request", + "link_fieldname": "subgroup" } ], - "modified": "2021-11-29 16:57:51.660847", + "modified": "2021-11-30 07:41:54.893270", "modified_by": "Administrator", "module": "LMS", "name": "Cohort Subgroup", diff --git a/school/lms/doctype/cohort_subgroup/cohort_subgroup.py b/school/lms/doctype/cohort_subgroup/cohort_subgroup.py index 2f11e079..42d28ad2 100644 --- a/school/lms/doctype/cohort_subgroup/cohort_subgroup.py +++ b/school/lms/doctype/cohort_subgroup/cohort_subgroup.py @@ -11,7 +11,8 @@ class CohortSubgroup(Document): self.invite_code = random_string(8) def get_invite_link(self): - return f"{frappe.utils.get_url()}/cohorts/{self.cohort}/join/{self.slug}/{self.invite_code}" + 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. @@ -40,6 +41,21 @@ class CohortSubgroup(Document): } return frappe.get_all("Cohort Join Request", filters=q, fields=["*"], order_by="creation") + def get_mentors(self): + emails = frappe.get_all("Cohort Mentor", filters={"subgroup": self.name}, fields=["email"], pluck='email') + return [frappe.get_doc("User", email) for email in emails] + + def get_students(self): + emails = frappe.get_all("LMS Batch Membership", filters={"subgroup": self.name}, fields=["member"], pluck='member') + return [frappe.get_doc("User", email) for email in emails] + + def is_mentor(self, email): + q = { + "doctype": "Cohort Mentor", + "subgroup": self.name, + "email": email + } + return frappe.db.exists(q) #def after_doctype_insert(): # frappe.db.add_unique("Cohort Subgroup", ("cohort", "slug")) diff --git a/school/www/cohorts/cohort.html b/school/www/cohorts/cohort.html new file mode 100644 index 00000000..761a8406 --- /dev/null +++ b/school/www/cohorts/cohort.html @@ -0,0 +1,25 @@ +{% extends "www/cohorts/base.html" %} +{% block title %}Manage {{ course.title }}{% endblock %} + +{% block page_content %} +

{{cohort.title}} Cohort

+ +
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 %} +
+{% endblock %} + diff --git a/school/www/cohorts/cohort.py b/school/www/cohorts/cohort.py new file mode 100644 index 00000000..da120492 --- /dev/null +++ b/school/www/cohorts/cohort.py @@ -0,0 +1,34 @@ +import frappe +from . import utils + +def get_context(context): + context.no_cache = 1 + if frappe.session.user == "Guest": + frappe.local.flags.redirect_location = "/login?redirect-to=" + frappe.request.path + raise frappe.Redirect() + + course = utils.get_course() + cohort = course and get_cohort(course, frappe.form_dict["cohort"]) + + if not cohort: + context.template = "www/404.html" + return + + utils.add_nav(context, "All Courses", "/courses") + utils.add_nav(context, course.title, "/courses/" + course.name) + utils.add_nav(context, "Cohorts", "/courses/" + course.name + "/cohorts") + + context.course = course + context.cohort = cohort + +def get_cohort(course, cohort_slug): + cohort = utils.get_cohort(course, cohort_slug) + + if cohort.is_mentor(frappe.session.user): + mentor = cohort.get_mentor(frappe.session.user) + sg = frappe.get_doc("Cohort Subgroup", mentor.subgroup) + frappe.local.flags.redirect_location = f"/courses/{course.name}/subgroups/{cohort.slug}/{sg.slug}" + raise frappe.Redirect + elif cohort.is_admin(frappe.session.user) or "System Manager" in frappe.get_roles(): + return cohort + diff --git a/school/www/cohorts/subgroup.html b/school/www/cohorts/subgroup.html new file mode 100644 index 00000000..c1b6f164 --- /dev/null +++ b/school/www/cohorts/subgroup.html @@ -0,0 +1,136 @@ +{% 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 == "students" %} + {{ render_students() }} + {% elif page == "join-requests" %} + {{ render_join_requests() }} + {% endif %} +
+{% endblock %} + +{% macro render_info() %} +
Invite Link
+ {% set link = subgroup.get_invite_link() %} +

{{link}} +
+ Copy to Clipboard +

+ +
Mentors
+ {% set mentors = subgroup.get_mentors() %} + {% if mentors %} + {% for m in mentors %} +
+ {{ widgets.Avatar(member=m, avatar_class="avatar-small") }} + {{ m.full_name }} +
+ {% endfor %} + {% else %} + None found. + {% endif %} +{% endmacro %} + + +{% macro render_students() %} + {% set students = subgroup.get_students() %} + {% if students %} + {% for student in students %} +
+ {{ widgets.Avatar(member=student, avatar_class="avatar-small") }} + {{ student.full_name }} +
+ {% endfor %} + {% else %} + None found. + {% endif %} +{% endmacro %} + + +{% macro render_join_requests() %} + + + + + + + + {% for r in subgroup.get_join_requests() %} + + + + + + + {% endfor %} +
#WhenEmailActions
{{loop.index}}{{r.creation}}{{r.email}} + Approve | Reject
+{% endmacro %} + +{% macro render_navitem(title, link, count, active) %} + +{% endmacro %} + + +{% block script %} + + +{% endblock %} diff --git a/school/www/cohorts/subgroup.py b/school/www/cohorts/subgroup.py new file mode 100644 index 00000000..b8b1c894 --- /dev/null +++ b/school/www/cohorts/subgroup.py @@ -0,0 +1,33 @@ +import frappe +from . import utils + +def get_context(context): + context.no_cache = 1 + course = utils.get_course() + if frappe.session.user == "Guest": + frappe.local.flags.redirect_location = "/login?redirect-to=" + frappe.request.path + raise frappe.Redirect() + + 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 + + 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}/cohorts") + 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 = frappe.form_dict["page"] + +def get_stats(subgroup): + return { + "join_requests": len(subgroup.get_join_requests()), + "students": len(subgroup.get_students()) + } diff --git a/school/www/cohorts/utils.py b/school/www/cohorts/utils.py index 001d1e4d..a35c7e9f 100644 --- a/school/www/cohorts/utils.py +++ b/school/www/cohorts/utils.py @@ -10,6 +10,14 @@ def 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. """ From f1157895dbec3f21a1237bc52cd9dac897b7a43b Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Tue, 30 Nov 2021 08:29:24 +0530 Subject: [PATCH 04/37] feat: added portal page to join a cohort Issue #271 --- school/hooks.py | 1 + school/lms/api.py | 25 ++++++ school/lms/doctype/lms_course/lms_course.py | 4 + school/www/cohorts/join.html | 88 +++++++++++++++++++++ school/www/cohorts/join.py | 24 ++++++ 5 files changed, 142 insertions(+) create mode 100644 school/www/cohorts/join.html create mode 100644 school/www/cohorts/join.py diff --git a/school/hooks.py b/school/hooks.py index 7eaed275..7c217970 100644 --- a/school/hooks.py +++ b/school/hooks.py @@ -146,6 +146,7 @@ website_route_rules = [ {"from_route": "/courses//subgroups//", "to_route": "cohorts/subgroup", "defaults": {"page": "info"}}, {"from_route": "/courses//subgroups///students", "to_route": "cohorts/subgroup", "defaults": {"page": "students"}}, {"from_route": "/courses//subgroups///join-requests", "to_route": "cohorts/subgroup", "defaults": {"page": "join-requests"}}, + {"from_route": "/courses//join///", "to_route": "cohorts/join"}, {"from_route": "/users", "to_route": "profiles/profile"} ] diff --git a/school/lms/api.py b/school/lms/api.py index b60d2d13..fac39d52 100644 --- a/school/lms/api.py +++ b/school/lms/api.py @@ -45,3 +45,28 @@ 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 + } + doc = frappe.get_doc(data) + doc.insert(ignore_permissions=True) + return {"ok": True} diff --git a/school/lms/doctype/lms_course/lms_course.py b/school/lms/doctype/lms_course/lms_course.py index 6815dd6e..9562445c 100644 --- a/school/lms/doctype/lms_course/lms_course.py +++ b/school/lms/doctype/lms_course/lms_course.py @@ -213,6 +213,10 @@ class LMSCourse(Document): 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. """ diff --git a/school/www/cohorts/join.html b/school/www/cohorts/join.html new file mode 100644 index 00000000..a31ecc63 --- /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.

+ Go to the course +
+{% 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 From 3328359ba4679a087069515a189c748f650c4205 Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Tue, 30 Nov 2021 13:44:58 +0530 Subject: [PATCH 05/37] style: "manage the course" button Issue #271 --- school/www/courses/course.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/school/www/courses/course.html b/school/www/courses/course.html index e457ee58..9b169569 100644 --- a/school/www/courses/course.html +++ b/school/www/courses/course.html @@ -72,8 +72,9 @@ {% endif %} {% if course.is_cohort_staff(frappe.session.user) %} - + Manage the course {% endif %} From ffd9e9d48ede15ac532bb57f2116ea9dce9a52ea Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Tue, 30 Nov 2021 18:05:01 +0530 Subject: [PATCH 06/37] feat: Added support for approve/reject join requests to a cohort subgroup Issue #271 --- school/lms/api.py | 38 +++++++++++++++++++ .../cohort_join_request.py | 25 +++++++++++- .../cohort_subgroup/cohort_subgroup.py | 10 +++++ school/www/cohorts/subgroup.html | 22 ++++++++++- 4 files changed, 91 insertions(+), 4 deletions(-) diff --git a/school/lms/api.py b/school/lms/api.py index fac39d52..46518a76 100644 --- a/school/lms/api.py +++ b/school/lms/api.py @@ -70,3 +70,41 @@ def join_cohort(course, cohort, subgroup, invite_code): doc = frappe.get_doc(data) doc.insert(ignore_permissions=True) return {"ok": True} + +@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): + 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): + return { + "ok": False, + "error": "Permission Deined" + } + + r.status = "Rejected" + r.save(ignore_permissions=True) + return {"ok": True} diff --git a/school/lms/doctype/cohort_join_request/cohort_join_request.py b/school/lms/doctype/cohort_join_request/cohort_join_request.py index 984096ab..dde1f60f 100644 --- a/school/lms/doctype/cohort_join_request/cohort_join_request.py +++ b/school/lms/doctype/cohort_join_request/cohort_join_request.py @@ -1,7 +1,7 @@ # Copyright (c) 2021, FOSS United and contributors # For license information, please see license.txt -# import frappe +import frappe from frappe.model.document import Document class CohortJoinRequest(Document): @@ -10,4 +10,25 @@ class CohortJoinRequest(Document): self.ensure_student() def ensure_student(self): - pass + q = { + "doctype": "LMS Batch Membership", + "cohort": self.cohort, + "subgroup": self.subgroup, + "email": self.email + } + if frappe.db.exists(q): + return + + cohort = frappe.get_doc("Cohort", self.cohort) + + 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_subgroup/cohort_subgroup.py b/school/lms/doctype/cohort_subgroup/cohort_subgroup.py index 42d28ad2..aa675206 100644 --- a/school/lms/doctype/cohort_subgroup/cohort_subgroup.py +++ b/school/lms/doctype/cohort_subgroup/cohort_subgroup.py @@ -57,5 +57,15 @@ class CohortSubgroup(Document): } 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 after_doctype_insert(): # frappe.db.add_unique("Cohort Subgroup", ("cohort", "slug")) diff --git a/school/www/cohorts/subgroup.html b/school/www/cohorts/subgroup.html index c1b6f164..ea9be393 100644 --- a/school/www/cohorts/subgroup.html +++ b/school/www/cohorts/subgroup.html @@ -59,6 +59,8 @@ {% macro render_join_requests() %} + {% set join_requests = subgroup.get_join_requests() %} + {% if join_requests %} @@ -78,6 +80,9 @@ {% endfor %}
#
+ {% else %} + There are no pending join requests. + {% endif %} {% endmacro %} {% macro render_navitem(title, link, count, active) %} @@ -118,7 +123,7 @@ $(function() { frappe.confirm( `Are you sure to accept ${email} to this subgroup?`, function() { - console.log("approve", name); + run_action("school.lms.api.approve_cohort_join_request", name); } ); }); @@ -127,10 +132,23 @@ $(function() { var name = $(this).parent().data("name"); var email = $(this).parent().data("email"); frappe.confirm(`Are you sure to reject ${email} from joining this subgroup?`, function() { - console.log("reject", name); + run_action("school.lms.api.reject_cohort_join_request", name); }); }); + function run_action(method, join_request) { + frappe.call(method, { + join_request: join_request, + }) + .then(r => { + if (r.message.ok) { + window.location.reload(); + } + else { + frappe.msgprint(r.message.error); + } + }); + } }); {% endblock %} From 6c747ff8b4875e03f3b9a9c9cd538b162b83c552 Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Wed, 1 Dec 2021 08:43:10 +0530 Subject: [PATCH 07/37] feat: added get_url method to cohort doctypes This is required to make it easier to include links in the email notification to mentors. Issue #271 --- school/lms/doctype/cohort/cohort.py | 3 +++ school/lms/doctype/cohort_mentor/cohort_mentor.py | 8 ++++++-- school/lms/doctype/cohort_subgroup/cohort_subgroup.py | 4 ++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/school/lms/doctype/cohort/cohort.py b/school/lms/doctype/cohort/cohort.py index 91ba2004..1a7a3025 100644 --- a/school/lms/doctype/cohort/cohort.py +++ b/school/lms/doctype/cohort/cohort.py @@ -5,6 +5,9 @@ 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): names = frappe.get_all("Cohort Subgroup", filters={"cohort": self.name}, pluck="name") subgroups = [frappe.get_doc("Cohort Subgroup", name) for name in names] diff --git a/school/lms/doctype/cohort_mentor/cohort_mentor.py b/school/lms/doctype/cohort_mentor/cohort_mentor.py index a385819c..71bf9c4b 100644 --- a/school/lms/doctype/cohort_mentor/cohort_mentor.py +++ b/school/lms/doctype/cohort_mentor/cohort_mentor.py @@ -1,8 +1,12 @@ # Copyright (c) 2021, FOSS United and contributors # For license information, please see license.txt -# import frappe +import frappe from frappe.model.document import Document class CohortMentor(Document): - pass + 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_subgroup/cohort_subgroup.py b/school/lms/doctype/cohort_subgroup/cohort_subgroup.py index aa675206..68550bd0 100644 --- a/school/lms/doctype/cohort_subgroup/cohort_subgroup.py +++ b/school/lms/doctype/cohort_subgroup/cohort_subgroup.py @@ -10,6 +10,10 @@ class CohortSubgroup(Document): 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}" From e014c94446c6bd94039e967c10e73190e17d3237 Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Wed, 1 Dec 2021 12:19:03 +0530 Subject: [PATCH 08/37] fix: mentor counts on cohort page The group by clause was missing in the query. Issue #271 --- school/lms/doctype/cohort/cohort.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/school/lms/doctype/cohort/cohort.py b/school/lms/doctype/cohort/cohort.py index 1a7a3025..6dbec93d 100644 --- a/school/lms/doctype/cohort/cohort.py +++ b/school/lms/doctype/cohort/cohort.py @@ -27,7 +27,8 @@ class Cohort(Document): q = f""" SELECT subgroup, count(*) as count FROM `tab{doctype}` - WHERE cohort = %(cohort)s""" + WHERE cohort = %(cohort)s + GROUP BY subgroup""" rows = frappe.db.sql(q, values={"cohort": self.name}) return {subgroup: count for subgroup, count in rows} From c96e3ee2f96d9b3568da85f9f2b5fa155834a849 Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Thu, 2 Dec 2021 10:36:17 +0530 Subject: [PATCH 09/37] feat: show counts on cohort listing and view pages Issue #271 --- school/lms/doctype/cohort/cohort.py | 28 ++++++++++++++------- school/www/cohorts/cohort.html | 9 +++++++ school/www/cohorts/cohort.py | 2 +- school/www/cohorts/index.html | 38 ++++++++++++++++------------- school/www/cohorts/subgroup.py | 2 +- 5 files changed, 51 insertions(+), 28 deletions(-) diff --git a/school/lms/doctype/cohort/cohort.py b/school/lms/doctype/cohort/cohort.py index 6dbec93d..c2ace214 100644 --- a/school/lms/doctype/cohort/cohort.py +++ b/school/lms/doctype/cohort/cohort.py @@ -16,21 +16,31 @@ class Cohort(Document): 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") + 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) return subgroups - def _get_subgroup_counts(self, doctype): - q = f""" - SELECT subgroup, count(*) as count - FROM `tab{doctype}` - WHERE cohort = %(cohort)s - GROUP BY subgroup""" - rows = frappe.db.sql(q, values={"cohort": self.name}) - return {subgroup: count for subgroup, count in rows} + def _get_subgroup_counts(self, doctype, **kw): + rows = frappe.get_list(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_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) diff --git a/school/www/cohorts/cohort.html b/school/www/cohorts/cohort.html index 761a8406..e8c17abd 100644 --- a/school/www/cohorts/cohort.html +++ b/school/www/cohorts/cohort.html @@ -4,6 +4,15 @@ {% 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 +

+
Subgroups
    {% for sg in cohort.get_subgroups(include_counts=True) %} diff --git a/school/www/cohorts/cohort.py b/school/www/cohorts/cohort.py index da120492..54fb17af 100644 --- a/school/www/cohorts/cohort.py +++ b/school/www/cohorts/cohort.py @@ -16,7 +16,7 @@ def get_context(context): utils.add_nav(context, "All Courses", "/courses") utils.add_nav(context, course.title, "/courses/" + course.name) - utils.add_nav(context, "Cohorts", "/courses/" + course.name + "/cohorts") + utils.add_nav(context, "Cohorts", "/courses/" + course.name + "/manage") context.course = course context.cohort = cohort diff --git a/school/www/cohorts/index.html b/school/www/cohorts/index.html index dcb4c5d8..68009df8 100644 --- a/school/www/cohorts/index.html +++ b/school/www/cohorts/index.html @@ -2,23 +2,19 @@ {% 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 %} -
    -
    -
    + {% 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) %} @@ -26,6 +22,14 @@
    {{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
    diff --git a/school/www/cohorts/subgroup.py b/school/www/cohorts/subgroup.py index b8b1c894..7a4794b1 100644 --- a/school/www/cohorts/subgroup.py +++ b/school/www/cohorts/subgroup.py @@ -17,7 +17,7 @@ def get_context(context): 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}/cohorts") + 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 From c963e93b52cd9d417e79c56119084725942dd14b Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Fri, 3 Dec 2021 00:28:13 +0530 Subject: [PATCH 10/37] feat: allow students who have already joined the course to join the cohort Issue #271 --- .../cohort_join_request.py | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/school/lms/doctype/cohort_join_request/cohort_join_request.py b/school/lms/doctype/cohort_join_request/cohort_join_request.py index dde1f60f..95c975f8 100644 --- a/school/lms/doctype/cohort_join_request/cohort_join_request.py +++ b/school/lms/doctype/cohort_join_request/cohort_join_request.py @@ -10,25 +10,42 @@ class CohortJoinRequest(Document): 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, - "email": self.email + "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) - data = { + q = { "doctype": "LMS Batch Membership", "course": cohort.course, - "cohort": self.cohort, - "subgroup": self.subgroup, "member": self.email, - "member_type": "Student", - "role": "Member" + "member_type": "Student" } - doc = frappe.get_doc(data) - doc.insert(ignore_permissions=True) + 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) From d84302682e4bd51d760887c9f3d24f5b97c45eff Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Fri, 3 Dec 2021 16:03:14 +0530 Subject: [PATCH 11/37] fix: permission issue on cohort page Using get_all instead of get_list to disable permission check. Issue #271 --- school/lms/doctype/cohort/cohort.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/school/lms/doctype/cohort/cohort.py b/school/lms/doctype/cohort/cohort.py index c2ace214..a2b2df08 100644 --- a/school/lms/doctype/cohort/cohort.py +++ b/school/lms/doctype/cohort/cohort.py @@ -24,7 +24,7 @@ class Cohort(Document): return subgroups def _get_subgroup_counts(self, doctype, **kw): - rows = frappe.get_list(doctype, + rows = frappe.get_all(doctype, filters={"cohort": self.name, **kw}, fields=['subgroup', 'count(*) as count'], group_by='subgroup') From d5da5bd8aa89f015a67f1f772b139165075a83e3 Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Sat, 4 Dec 2021 07:25:15 +0530 Subject: [PATCH 12/37] feat: allow all mentors to see the cohort status Issue #271 --- school/www/cohorts/cohort.html | 17 +++++++++++++++-- school/www/cohorts/cohort.py | 26 ++++++++++++-------------- school/www/cohorts/index.py | 5 +++-- 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/school/www/cohorts/cohort.html b/school/www/cohorts/cohort.html index e8c17abd..07fc9193 100644 --- a/school/www/cohorts/cohort.html +++ b/school/www/cohorts/cohort.html @@ -13,13 +13,26 @@ | {{ stats.join_requests }} join requests

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

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

    +

    Visit Your Subgroup →

    +
    + +{% endif %} + +
    All Subgroups
      {% for sg in cohort.get_subgroups(include_counts=True) %}
    • + {% if is_admin %} {{sg.title}} -
      + {% else %} + {{sg.title}} + {% endif %} +
      {{sg.num_mentors}} Mentors | diff --git a/school/www/cohorts/cohort.py b/school/www/cohorts/cohort.py index 54fb17af..c6d5cf57 100644 --- a/school/www/cohorts/cohort.py +++ b/school/www/cohorts/cohort.py @@ -8,27 +8,25 @@ def get_context(context): raise frappe.Redirect() course = utils.get_course() - cohort = course and get_cohort(course, frappe.form_dict["cohort"]) - + 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() + + if not is_admin and not is_mentor : + frappe.throw("Permission Deined", frappe.PermissionError) + 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 - -def get_cohort(course, cohort_slug): - cohort = utils.get_cohort(course, cohort_slug) - - if cohort.is_mentor(frappe.session.user): - mentor = cohort.get_mentor(frappe.session.user) - sg = frappe.get_doc("Cohort Subgroup", mentor.subgroup) - frappe.local.flags.redirect_location = f"/courses/{course.name}/subgroups/{cohort.slug}/{sg.slug}" - raise frappe.Redirect - elif cohort.is_admin(frappe.session.user) or "System Manager" in frappe.get_roles(): - return cohort - + context.mentor = mentor + context.is_mentor = is_mentor + context.is_admin = is_admin diff --git a/school/www/cohorts/index.py b/school/www/cohorts/index.py index 5f27ef2f..2d1f648b 100644 --- a/school/www/cohorts/index.py +++ b/school/www/cohorts/index.py @@ -13,6 +13,9 @@ def get_context(context): 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) @@ -24,7 +27,5 @@ def get_cohorts(course): 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 - print(roles) names = {role.cohort for role in roles} - print(names) return [frappe.get_doc("Cohort", name) for name in names] From e0c73e26eeb847f59c258a3a9ed6e933a432665b Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Sat, 4 Dec 2021 17:19:49 +0530 Subject: [PATCH 13/37] fix: increased the page_length when querying for students of a subgroup. --- school/lms/doctype/cohort_subgroup/cohort_subgroup.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/school/lms/doctype/cohort_subgroup/cohort_subgroup.py b/school/lms/doctype/cohort_subgroup/cohort_subgroup.py index 68550bd0..3b15837f 100644 --- a/school/lms/doctype/cohort_subgroup/cohort_subgroup.py +++ b/school/lms/doctype/cohort_subgroup/cohort_subgroup.py @@ -50,7 +50,12 @@ class CohortSubgroup(Document): return [frappe.get_doc("User", email) for email in emails] def get_students(self): - emails = frappe.get_all("LMS Batch Membership", filters={"subgroup": self.name}, fields=["member"], pluck='member') + emails = frappe.get_all("LMS Batch Membership", + filters={"subgroup": self.name}, + fields=["member"], + pluck='member', + page_length=1000) + return [frappe.get_doc("User", email) for email in emails] def is_mentor(self, email): From fb447a30e428f7ffaab6155c355ae62491eb2649 Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Sat, 4 Dec 2021 22:47:23 +0530 Subject: [PATCH 14/37] feat: added undo of rejected join requests Also improved the dispaly of timestamp, showing the diff now. Issue #271 --- school/lms/api.py | 21 ++++++++++++++ school/www/cohorts/subgroup.html | 47 ++++++++++++++++++++++++++++++-- 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/school/lms/api.py b/school/lms/api.py index 46518a76..a25f35ea 100644 --- a/school/lms/api.py +++ b/school/lms/api.py @@ -108,3 +108,24 @@ def reject_cohort_join_request(join_request): 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): + return { + "ok": False, + "error": "Permission Deined" + } + + r.status = "Pending" + r.save(ignore_permissions=True) + return {"ok": True} diff --git a/school/www/cohorts/subgroup.html b/school/www/cohorts/subgroup.html index ea9be393..08c511fc 100644 --- a/school/www/cohorts/subgroup.html +++ b/school/www/cohorts/subgroup.html @@ -60,6 +60,7 @@ {% macro render_join_requests() %} {% set join_requests = subgroup.get_join_requests() %} +
      Pending Requests
      {% if join_requests %} @@ -68,10 +69,10 @@ - {% for r in subgroup.get_join_requests() %} + {% for r in join_requests %} - +
      Email Actions
      {{loop.index}}{{r.creation}}{{r.creation}} {{r.email}} {% else %} - There are no pending join requests. +

      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 %} @@ -116,6 +143,12 @@ $(function() { }); }); + $(".timestamp"). each(function() { + var t = moment($(this).text()); + var dt = t.from(moment.now()); + $(this).text(dt); + }); + $(".action-approve").click(function() { var name = $(this).parent().data("name"); var email = $(this).parent().data("email"); @@ -136,6 +169,14 @@ $(function() { }); }); + $(".action-undo").click(function() { + var name = $(this).parent().data("name"); + var email = $(this).parent().data("email"); + frappe.confirm(`Are you sure to undo the rejection of ${email}?`, function() { + run_action("school.lms.api.undo_reject_cohort_join_request", name); + }); + }); + function run_action(method, join_request) { frappe.call(method, { join_request: join_request, From 52fd8913702c53eee2e98d89a70b157f94834714 Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Sat, 4 Dec 2021 22:52:56 +0530 Subject: [PATCH 15/37] fix: show the recent join requests on the top Issue #271 --- school/lms/doctype/cohort_subgroup/cohort_subgroup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/school/lms/doctype/cohort_subgroup/cohort_subgroup.py b/school/lms/doctype/cohort_subgroup/cohort_subgroup.py index 3b15837f..6d2d1d08 100644 --- a/school/lms/doctype/cohort_subgroup/cohort_subgroup.py +++ b/school/lms/doctype/cohort_subgroup/cohort_subgroup.py @@ -43,7 +43,7 @@ class CohortSubgroup(Document): "subgroup": self.name, "status": status } - return frappe.get_all("Cohort Join Request", filters=q, fields=["*"], order_by="creation") + 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') From 63c4f708c3b3e40b43ea37aec5d6f804bffff082 Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Sat, 4 Dec 2021 23:03:58 +0530 Subject: [PATCH 16/37] feat: improved the subgroup page load time Using `get_cached_doc` instead of `get_doc` for loading students and mentors. Issue #271 --- school/lms/doctype/cohort_subgroup/cohort_subgroup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/school/lms/doctype/cohort_subgroup/cohort_subgroup.py b/school/lms/doctype/cohort_subgroup/cohort_subgroup.py index 6d2d1d08..814a1c70 100644 --- a/school/lms/doctype/cohort_subgroup/cohort_subgroup.py +++ b/school/lms/doctype/cohort_subgroup/cohort_subgroup.py @@ -47,7 +47,7 @@ class CohortSubgroup(Document): def get_mentors(self): emails = frappe.get_all("Cohort Mentor", filters={"subgroup": self.name}, fields=["email"], pluck='email') - return [frappe.get_doc("User", email) for email in emails] + return [frappe.get_cached_doc("User", email) for email in emails] def get_students(self): emails = frappe.get_all("LMS Batch Membership", @@ -56,7 +56,7 @@ class CohortSubgroup(Document): pluck='member', page_length=1000) - return [frappe.get_doc("User", email) for email in emails] + return [frappe.get_cached_doc("User", email) for email in emails] def is_mentor(self, email): q = { From 22f5508beaa7fd620819512bfaa8e8b67128624f Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Sat, 4 Dec 2021 23:13:44 +0530 Subject: [PATCH 17/37] style: improved the display of users and mentors in a subgroup Using MemberCard insted of Avatar macro. Issue #271 --- school/www/cohorts/subgroup.html | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/school/www/cohorts/subgroup.html b/school/www/cohorts/subgroup.html index 08c511fc..2699296f 100644 --- a/school/www/cohorts/subgroup.html +++ b/school/www/cohorts/subgroup.html @@ -31,12 +31,11 @@
      Mentors
      {% set mentors = subgroup.get_mentors() %} {% if mentors %} +
      {% for m in mentors %} -
      - {{ widgets.Avatar(member=m, avatar_class="avatar-small") }} - {{ m.full_name }} -
      + {{ widgets.MemberCard(member=m, show_course_count=False, dimension_class="") }} {% endfor %} +
      {% else %} None found. {% endif %} @@ -46,12 +45,11 @@ {% macro render_students() %} {% set students = subgroup.get_students() %} {% if students %} +
      {% for student in students %} -
      - {{ widgets.Avatar(member=student, avatar_class="avatar-small") }} - {{ student.full_name }} -
      + {{ widgets.MemberCard(member=student, show_course_count=False, dimension_class="") }} {% endfor %} +
      {% else %} None found. {% endif %} From a1d0f3948a001d6981ceccaeadcd4808e9141490 Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Sat, 4 Dec 2021 23:47:29 +0530 Subject: [PATCH 18/37] feat: added support for custom web pages for cohorts This allows adding custom web pages to each cohort by defining a web template and attaching it to the cohort. Issue #271 --- school/hooks.py | 1 + school/lms/doctype/cohort/cohort.json | 11 +++- school/lms/doctype/cohort/cohort.py | 5 ++ .../lms/doctype/cohort_web_page/__init__.py | 0 .../cohort_web_page/cohort_web_page.json | 64 +++++++++++++++++++ .../cohort_web_page/cohort_web_page.py | 9 +++ school/www/cohorts/cohort.html | 36 ++++++++++- school/www/cohorts/cohort.py | 1 + 8 files changed, 122 insertions(+), 5 deletions(-) create mode 100644 school/lms/doctype/cohort_web_page/__init__.py create mode 100644 school/lms/doctype/cohort_web_page/cohort_web_page.json create mode 100644 school/lms/doctype/cohort_web_page/cohort_web_page.py diff --git a/school/hooks.py b/school/hooks.py index 7c217970..d73c18cf 100644 --- a/school/hooks.py +++ b/school/hooks.py @@ -143,6 +143,7 @@ website_route_rules = [ {"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", "defaults": {"page": "info"}}, {"from_route": "/courses//subgroups///students", "to_route": "cohorts/subgroup", "defaults": {"page": "students"}}, {"from_route": "/courses//subgroups///join-requests", "to_route": "cohorts/subgroup", "defaults": {"page": "join-requests"}}, diff --git a/school/lms/doctype/cohort/cohort.json b/school/lms/doctype/cohort/cohort.json index 6c98c101..f9cb7527 100644 --- a/school/lms/doctype/cohort/cohort.json +++ b/school/lms/doctype/cohort/cohort.json @@ -18,7 +18,8 @@ "end_date", "duration", "section_break_8", - "description" + "description", + "pages" ], "fields": [ { @@ -89,6 +90,12 @@ "label": "Course", "options": "LMS Course", "reqd": 1 + }, + { + "fieldname": "pages", + "fieldtype": "Table", + "label": "Pages", + "options": "Cohort Web Page" } ], "index_web_pages_for_search": 1, @@ -109,7 +116,7 @@ "link_fieldname": "cohort" } ], - "modified": "2021-11-29 15:13:41.296384", + "modified": "2021-12-04 23:22:10.248781", "modified_by": "Administrator", "module": "LMS", "name": "Cohort", diff --git a/school/lms/doctype/cohort/cohort.py b/school/lms/doctype/cohort/cohort.py index a2b2df08..30c0983f 100644 --- a/school/lms/doctype/cohort/cohort.py +++ b/school/lms/doctype/cohort/cohort.py @@ -34,6 +34,11 @@ class Cohort(Document): filters = {"cohort": self.name, **kw} return frappe.db.count(doctype, filters=filters) + def get_page_template(self, slug): + for p in self.pages: + if p.slug == slug: + return p.get_template_html() + def get_stats(self): return { "subgroups": self._get_count("Cohort Subgroup"), 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/www/cohorts/cohort.html b/school/www/cohorts/cohort.html index 07fc9193..edfa2434 100644 --- a/school/www/cohorts/cohort.html +++ b/school/www/cohorts/cohort.html @@ -19,10 +19,26 @@

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

      Visit Your Subgroup →

      - {% endif %} -
      All Subgroups
      + + +
      +{% if not page %} + {{ render_subgroups() }} +{% else %} + {{ cohort.get_page_template(page) }} +{% endif %} +
      + +{% endblock %} + +{% macro render_subgroups() %}
        {% for sg in cohort.get_subgroups(include_counts=True) %}
      • @@ -43,5 +59,19 @@
      • {% endfor %}
      -{% endblock %} +{% endmacro %} +{% macro render_navitem(title, link, page, count=-1) %} + +{% endmacro %} diff --git a/school/www/cohorts/cohort.py b/school/www/cohorts/cohort.py index c6d5cf57..dae645f3 100644 --- a/school/www/cohorts/cohort.py +++ b/school/www/cohorts/cohort.py @@ -30,3 +30,4 @@ def get_context(context): context.mentor = mentor context.is_mentor = is_mentor context.is_admin = is_admin + context.page = frappe.form_dict.get("page") or "" From ebcb3c5466bf1693ece9858078f2bbe6566b72bf Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Sun, 5 Dec 2021 00:01:42 +0530 Subject: [PATCH 19/37] fix: fixed the issue with rendering custom cohort page Issue #271 --- school/www/cohorts/cohort.html | 2 +- school/www/cohorts/cohort.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/school/www/cohorts/cohort.html b/school/www/cohorts/cohort.html index edfa2434..78d90ba1 100644 --- a/school/www/cohorts/cohort.html +++ b/school/www/cohorts/cohort.html @@ -32,7 +32,7 @@ {% if not page %} {{ render_subgroups() }} {% else %} - {{ cohort.get_page_template(page) }} + {{ render_page(page) }} {% endif %} diff --git a/school/www/cohorts/cohort.py b/school/www/cohorts/cohort.py index dae645f3..e8db20fa 100644 --- a/school/www/cohorts/cohort.py +++ b/school/www/cohorts/cohort.py @@ -31,3 +31,8 @@ def get_context(context): context.is_mentor = is_mentor context.is_admin = is_admin context.page = frappe.form_dict.get("page") or "" + + # Function to render to custom page given the slug + context.render_page = lambda page: frappe.render_template( + cohort.get_page_template(page), + context) From cd0cc2b5011d0f955437c6e3a532241370401945 Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Sun, 5 Dec 2021 00:13:05 +0530 Subject: [PATCH 20/37] feat: added support to get subgroups in sorted order Issue #271 --- school/lms/doctype/cohort/cohort.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/school/lms/doctype/cohort/cohort.py b/school/lms/doctype/cohort/cohort.py index 30c0983f..c0d9a191 100644 --- a/school/lms/doctype/cohort/cohort.py +++ b/school/lms/doctype/cohort/cohort.py @@ -8,7 +8,7 @@ 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): + 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_doc("Cohort Subgroup", name) for name in names] subgroups = sorted(subgroups, key=lambda sg: sg.title) @@ -21,6 +21,9 @@ class Cohort(Document): 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): From fe31a641756e675c20e0d540fbefbf0a32f04765 Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Sun, 5 Dec 2021 00:22:27 +0530 Subject: [PATCH 21/37] style: updated the styles of subgroup links on cohort page --- school/www/cohorts/cohort.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/school/www/cohorts/cohort.html b/school/www/cohorts/cohort.html index 78d90ba1..f785c4fb 100644 --- a/school/www/cohorts/cohort.html +++ b/school/www/cohorts/cohort.html @@ -44,7 +44,10 @@
    • {% if is_admin %} - {{sg.title}} + {{sg.title}} {% else %} {{sg.title}} {% endif %} From 7001ddc96f85379e927dee2810cdef2811238ea8 Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Sun, 5 Dec 2021 01:04:46 +0530 Subject: [PATCH 22/37] feat: made all the cohort pages public There is some info on the page that is only accessible to mentors and admins and not shown to other users. --- school/hooks.py | 5 ++--- school/lms/doctype/cohort/cohort.py | 2 +- school/www/cohorts/cohort.html | 17 +++++++--------- school/www/cohorts/cohort.py | 7 ------- school/www/cohorts/subgroup.html | 31 +++++++++++++++++++---------- school/www/cohorts/subgroup.py | 16 ++++++++++----- 6 files changed, 41 insertions(+), 37 deletions(-) diff --git a/school/hooks.py b/school/hooks.py index d73c18cf..21f83c9c 100644 --- a/school/hooks.py +++ b/school/hooks.py @@ -144,9 +144,8 @@ website_route_rules = [ {"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", "defaults": {"page": "info"}}, - {"from_route": "/courses//subgroups///students", "to_route": "cohorts/subgroup", "defaults": {"page": "students"}}, - {"from_route": "/courses//subgroups///join-requests", "to_route": "cohorts/subgroup", "defaults": {"page": "join-requests"}}, + {"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"} ] diff --git a/school/lms/doctype/cohort/cohort.py b/school/lms/doctype/cohort/cohort.py index c0d9a191..04dfe508 100644 --- a/school/lms/doctype/cohort/cohort.py +++ b/school/lms/doctype/cohort/cohort.py @@ -10,7 +10,7 @@ class Cohort(Document): 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_doc("Cohort Subgroup", name) for name in names] + subgroups = [frappe.get_cached_doc("Cohort Subgroup", name) for name in names] subgroups = sorted(subgroups, key=lambda sg: sg.title) if include_counts: diff --git a/school/www/cohorts/cohort.html b/school/www/cohorts/cohort.html index f785c4fb..0fe3ae26 100644 --- a/school/www/cohorts/cohort.html +++ b/school/www/cohorts/cohort.html @@ -22,7 +22,8 @@ {% endif %}
      {% if page == "info" %} {{ render_info() }} + {% elif page == "mentors" %} + {{ render_mentors() }} {% elif page == "students" %} {{ render_students() }} - {% elif page == "join-requests" %} + {% elif page == "admin" %} {{ render_join_requests() }} {% endif %}
      {% endblock %} {% macro render_info() %} -
      Invite Link
      - {% set link = subgroup.get_invite_link() %} -

      {{link}} -
      - Copy to Clipboard -

      + {% if is_admin %} + {% endif %} +{% endmacro %} +{% macro render_mentors() %}
      Mentors
      {% set mentors = subgroup.get_mentors() %} {% if mentors %} @@ -57,7 +59,14 @@ {% macro render_join_requests() %} - {% set join_requests = subgroup.get_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 %} diff --git a/school/www/cohorts/subgroup.py b/school/www/cohorts/subgroup.py index 7a4794b1..602fc218 100644 --- a/school/www/cohorts/subgroup.py +++ b/school/www/cohorts/subgroup.py @@ -4,9 +4,6 @@ from . import utils def get_context(context): context.no_cache = 1 course = utils.get_course() - if frappe.session.user == "Guest": - frappe.local.flags.redirect_location = "/login?redirect-to=" + frappe.request.path - raise frappe.Redirect() cohort = utils.get_cohort(course, frappe.form_dict['cohort']) subgroup = utils.get_subgroup(cohort, frappe.form_dict['subgroup']) @@ -15,6 +12,13 @@ def get_context(context): context.template = "www/404.html" return + page = frappe.form_dict.get("page") + is_admin = subgroup.is_manager(frappe.session.user) or "System Manager" in frappe.get_roles() + + if page not in ["mentors", "students", "admin"] or (page == "admin" and not is_admin): + 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") @@ -24,10 +28,12 @@ def get_context(context): context.cohort = cohort context.subgroup = subgroup context.stats = get_stats(subgroup) - context.page = frappe.form_dict["page"] + context.page = page + context.is_admin = is_admin def get_stats(subgroup): return { "join_requests": len(subgroup.get_join_requests()), - "students": len(subgroup.get_students()) + "students": len(subgroup.get_students()), + "mentors": len(subgroup.get_mentors()) } From 0637b9c8f88d78ef2ed16be3edb0bc14975ee488 Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Sun, 5 Dec 2021 01:29:24 +0530 Subject: [PATCH 23/37] fix: allow admins to manage join requests Issue #271 --- school/lms/api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/school/lms/api.py b/school/lms/api.py index a25f35ea..8621a89c 100644 --- a/school/lms/api.py +++ b/school/lms/api.py @@ -80,7 +80,7 @@ def approve_cohort_join_request(join_request): "ok": False, "error": "Invalid Join Request" } - if not sg.is_manager(frappe.session.user): + if not sg.is_manager(frappe.session.user) and "System Manager" not in frappe.get_roles(): return { "ok": False, "error": "Permission Deined" @@ -99,7 +99,7 @@ def reject_cohort_join_request(join_request): "ok": False, "error": "Invalid Join Request" } - if not sg.is_manager(frappe.session.user): + if not sg.is_manager(frappe.session.user) and "System Manager" not in frappe.get_roles(): return { "ok": False, "error": "Permission Deined" @@ -120,7 +120,7 @@ def undo_reject_cohort_join_request(join_request): "ok": False, "error": "Invalid Join Request" } - if not sg.is_manager(frappe.session.user): + if not sg.is_manager(frappe.session.user) and "System Manager" not in frappe.get_roles(): return { "ok": False, "error": "Permission Deined" From f68fc02e570a3ec9494159810d514325b53525a5 Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Sun, 5 Dec 2021 01:29:48 +0530 Subject: [PATCH 24/37] style: improved the ux of approve/reject flow Issue #271 --- school/www/cohorts/subgroup.html | 37 ++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/school/www/cohorts/subgroup.html b/school/www/cohorts/subgroup.html index eca37463..ae8d3fba 100644 --- a/school/www/cohorts/subgroup.html +++ b/school/www/cohorts/subgroup.html @@ -81,7 +81,7 @@ - @@ -107,7 +107,7 @@ - @@ -157,40 +157,45 @@ $(function() { }); $(".action-approve").click(function() { + var el = $(this).parent().parent(); var name = $(this).parent().data("name"); var email = $(this).parent().data("email"); frappe.confirm( `Are you sure to accept ${email} to this subgroup?`, function() { - run_action("school.lms.api.approve_cohort_join_request", name); + run_action("school.lms.api.approve_cohort_join_request", name, el, "approved", "Approved"); } ); }); $(".action-reject").click(function() { + var el = $(this).parent().parent(); var name = $(this).parent().data("name"); var email = $(this).parent().data("email"); frappe.confirm(`Are you sure to reject ${email} from joining this subgroup?`, function() { - run_action("school.lms.api.reject_cohort_join_request", name); + run_action("school.lms.api.reject_cohort_join_request", name, el, "rejected", "Rejected!"); }); }); $(".action-undo").click(function() { + var el = $(this).parent().parent(); var name = $(this).parent().data("name"); var email = $(this).parent().data("email"); frappe.confirm(`Are you sure to undo the rejection of ${email}?`, function() { - run_action("school.lms.api.undo_reject_cohort_join_request", name); + run_action("school.lms.api.undo_reject_cohort_join_request", name, el, "undo-reject", "Reject Undone!"); }); }); - function run_action(method, join_request) { + function run_action(method, join_request, elem, classname, label) { frappe.call(method, { join_request: join_request, }) .then(r => { if (r.message.ok) { - window.location.reload(); + $(elem) + .addClass(classname) + .find("td.actions").html(label); } else { frappe.msgprint(r.message.error); @@ -200,3 +205,21 @@ $(function() { }); {% endblock %} + +{% block style %} + +{% endblock %} From 2f994628c3e39dffe722ad0178f16c9fdeeaaabd Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Sun, 5 Dec 2021 01:33:33 +0530 Subject: [PATCH 25/37] fix: show students/mentors on subgroup page alphabetically Issue #271 --- school/lms/doctype/cohort_subgroup/cohort_subgroup.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/school/lms/doctype/cohort_subgroup/cohort_subgroup.py b/school/lms/doctype/cohort_subgroup/cohort_subgroup.py index 814a1c70..064a8e92 100644 --- a/school/lms/doctype/cohort_subgroup/cohort_subgroup.py +++ b/school/lms/doctype/cohort_subgroup/cohort_subgroup.py @@ -47,7 +47,7 @@ class CohortSubgroup(Document): def get_mentors(self): emails = frappe.get_all("Cohort Mentor", filters={"subgroup": self.name}, fields=["email"], pluck='email') - return [frappe.get_cached_doc("User", email) for email in emails] + return self._get_users(emails) def get_students(self): emails = frappe.get_all("LMS Batch Membership", @@ -55,8 +55,11 @@ class CohortSubgroup(Document): fields=["member"], pluck='member', page_length=1000) + return self._get_users(emails) - return [frappe.get_cached_doc("User", email) for email in 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 = { From 3cd4e649574226a830600dd5de49f4365136cc47 Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Sun, 5 Dec 2021 02:10:58 +0530 Subject: [PATCH 26/37] feat: allow cohort admins to add mentors Issue #271 --- school/lms/api.py | 27 +++++++++++ .../cohort_subgroup/cohort_subgroup.py | 12 +++++ school/www/cohorts/subgroup.html | 45 ++++++++++++++++--- school/www/cohorts/subgroup.py | 8 +++- 4 files changed, 84 insertions(+), 8 deletions(-) diff --git a/school/lms/api.py b/school/lms/api.py index 8621a89c..7e60cbb7 100644 --- a/school/lms/api.py +++ b/school/lms/api.py @@ -129,3 +129,30 @@ def undo_reject_cohort_join_request(join_request): 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/cohort_subgroup/cohort_subgroup.py b/school/lms/doctype/cohort_subgroup/cohort_subgroup.py index 064a8e92..f5b6dc01 100644 --- a/school/lms/doctype/cohort_subgroup/cohort_subgroup.py +++ b/school/lms/doctype/cohort_subgroup/cohort_subgroup.py @@ -79,5 +79,17 @@ class CohortSubgroup(Document): 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/www/cohorts/subgroup.html b/school/www/cohorts/subgroup.html index ae8d3fba..983d2d62 100644 --- a/school/www/cohorts/subgroup.html +++ b/school/www/cohorts/subgroup.html @@ -2,13 +2,19 @@ {% block title %} Subgroup {{subgroup.title}} - {{ course.title }} {% endblock %} {% block page_content %} -

      {{subgroup.title}} Subgroup

      +

      {{subgroup.title}} Subgroup

      @@ -18,15 +24,24 @@ {{ render_mentors() }} {% elif page == "students" %} {{ render_students() }} - {% elif page == "admin" %} + {% elif page == "join-requests" %} {{ render_join_requests() }} + {% elif page == "admin" %} + {{ render_admin() }} {% endif %}
      {% endblock %} -{% macro render_info() %} - {% if is_admin %} - {% endif %} +{% macro render_admin() %} +
      +
      Add a new mentor
      +
      +
      + +
      + + +
      {% endmacro %} {% macro render_mentors() %} @@ -202,6 +217,24 @@ $(function() { } }); } + + $("#add-mentor").click(function() { + var subgroup = $("#page-title").data("subgroup"); + var title = $("#page-title").data("title"); + var email = $("#mentor-email").val(); + frappe.call("school.lms.api.add_mentor_to_subgroup", { + subgroup: subgroup, + email: email + }) + .then(r => { + if (r.message.ok) { + frappe.msgprint(`Successfully added ${email} as mentor to ${title}`); + } + else { + frappe.msgprint(r.message.error); + } + }); + }); }); {% endblock %} diff --git a/school/www/cohorts/subgroup.py b/school/www/cohorts/subgroup.py index 602fc218..e213396d 100644 --- a/school/www/cohorts/subgroup.py +++ b/school/www/cohorts/subgroup.py @@ -13,9 +13,12 @@ def get_context(context): return page = frappe.form_dict.get("page") - is_admin = subgroup.is_manager(frappe.session.user) or "System Manager" in frappe.get_roles() + is_mentor = subgroup.is_mentor(frappe.session.user) + is_admin = cohort.is_admin(frappe.session.user) or "System Manager" in frappe.get_roles() - if page not in ["mentors", "students", "admin"] or (page == "admin" and not is_admin): + if (page not in ["mentors", "students", "join-requests", "admin"] + or (page == "join-requests" and not (is_mentor or is_admin)) + or (page == "admin" and not is_admin)): frappe.local.flags.redirect_location = subgroup.get_url() + "/mentors" raise frappe.Redirect @@ -30,6 +33,7 @@ def get_context(context): context.stats = get_stats(subgroup) context.page = page context.is_admin = is_admin + context.is_mentor = is_mentor def get_stats(subgroup): return { From c8d7ac48ea127155abfb148622388db37c62bdcd Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Tue, 7 Dec 2021 15:37:46 +0530 Subject: [PATCH 27/37] fix: CohortSubgroup.has_student Fixed the wrong query. Issue #271 --- school/lms/doctype/cohort_subgroup/cohort_subgroup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/school/lms/doctype/cohort_subgroup/cohort_subgroup.py b/school/lms/doctype/cohort_subgroup/cohort_subgroup.py index f5b6dc01..dfb810b0 100644 --- a/school/lms/doctype/cohort_subgroup/cohort_subgroup.py +++ b/school/lms/doctype/cohort_subgroup/cohort_subgroup.py @@ -22,9 +22,9 @@ class CohortSubgroup(Document): """Check if given user is a student of this subgroup. """ q = { - "doctype": "Cohort Student", + "doctype": "LMS Batch Membership", "subgroup": self.name, - "email": email + "member": email } return frappe.db.exists(q) From b83918c2aae0c6d7118f3d5d04abe6fba7e3cbc7 Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Wed, 8 Dec 2021 23:01:29 +0530 Subject: [PATCH 28/37] feat: added Exercise Latest Submission doctype Issue #274 --- school/lms/doctype/exercise/exercise.py | 7 +- .../exercise_latest_submission/__init__.py | 0 .../exercise_latest_submission.js | 8 + .../exercise_latest_submission.json | 166 ++++++++++++++++++ .../exercise_latest_submission.py | 8 + .../test_exercise_latest_submission.py | 8 + .../exercise_submission.json | 11 +- .../exercise_submission.py | 18 +- 8 files changed, 220 insertions(+), 6 deletions(-) create mode 100644 school/lms/doctype/exercise_latest_submission/__init__.py create mode 100644 school/lms/doctype/exercise_latest_submission/exercise_latest_submission.js create mode 100644 school/lms/doctype/exercise_latest_submission/exercise_latest_submission.json create mode 100644 school/lms/doctype/exercise_latest_submission/exercise_latest_submission.py create mode 100644 school/lms/doctype/exercise_latest_submission/test_exercise_latest_submission.py 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_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..27290c79 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.save(ignore_permissions=True) From 943c8eabbfe24fd71f0c6c245cd9911ecdb36249 Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Thu, 9 Dec 2021 01:17:03 +0530 Subject: [PATCH 29/37] feat: added custom pages at subgroup level --- school/lms/doctype/cohort/cohort.py | 13 ++++++++++--- school/www/cohorts/cohort.html | 2 +- school/www/cohorts/cohort.py | 2 +- school/www/cohorts/subgroup.html | 6 ++++++ school/www/cohorts/subgroup.py | 29 ++++++++++++++++++++++++++--- 5 files changed, 44 insertions(+), 8 deletions(-) diff --git a/school/lms/doctype/cohort/cohort.py b/school/lms/doctype/cohort/cohort.py index 04dfe508..429baac5 100644 --- a/school/lms/doctype/cohort/cohort.py +++ b/school/lms/doctype/cohort/cohort.py @@ -37,10 +37,17 @@ class Cohort(Document): filters = {"cohort": self.name, **kw} return frappe.db.count(doctype, filters=filters) - def get_page_template(self, slug): + 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: - return p.get_template_html() + 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 { diff --git a/school/www/cohorts/cohort.html b/school/www/cohorts/cohort.html index 0fe3ae26..007e5d70 100644 --- a/school/www/cohorts/cohort.html +++ b/school/www/cohorts/cohort.html @@ -24,7 +24,7 @@ diff --git a/school/www/cohorts/cohort.py b/school/www/cohorts/cohort.py index 358e268f..7f890367 100644 --- a/school/www/cohorts/cohort.py +++ b/school/www/cohorts/cohort.py @@ -27,5 +27,5 @@ def get_context(context): # Function to render to custom page given the slug context.render_page = lambda page: frappe.render_template( - cohort.get_page_template(page), + cohort.get_page_template(page, scope="Cohort"), context) diff --git a/school/www/cohorts/subgroup.html b/school/www/cohorts/subgroup.html index 983d2d62..a7d69895 100644 --- a/school/www/cohorts/subgroup.html +++ b/school/www/cohorts/subgroup.html @@ -12,6 +12,10 @@ {{ render_navitem("Students", "/students", stats.students, page=="students")}} {% if is_mentor or is_admin %} {{ render_navitem("Join Requests", "/join-requests", stats.join_requests, page=="join-requests")}} + + {% for p in cohort.get_pages(scope="Subgroup") %} + {{ render_navitem(p.title, "/" + p.slug, -1, page==p.slug) }} + {% endfor %} {% endif %} {% if is_admin %} {{ render_navitem("Admin", "/admin", -1, page=="admin")}} @@ -28,6 +32,8 @@ {{ render_join_requests() }} {% elif page == "admin" %} {{ render_admin() }} + {% else %} + {{ render_page(page) }} {% endif %} {% endblock %} diff --git a/school/www/cohorts/subgroup.py b/school/www/cohorts/subgroup.py index e213396d..94c83764 100644 --- a/school/www/cohorts/subgroup.py +++ b/school/www/cohorts/subgroup.py @@ -16,9 +16,24 @@ def get_context(context): is_mentor = subgroup.is_mentor(frappe.session.user) is_admin = cohort.is_admin(frappe.session.user) or "System Manager" in frappe.get_roles() - if (page not in ["mentors", "students", "join-requests", "admin"] - or (page == "join-requests" and not (is_mentor or is_admin)) - or (page == "admin" and not is_admin)): + 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 @@ -35,9 +50,17 @@ def get_context(context): context.is_admin = is_admin context.is_mentor = is_mentor + # Function to render to custom page given the slug + context.render_page = lambda page: frappe.render_template( + cohort.get_page_template(page), + 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") From 59b3b68bde21da836538de824fe531c1bd8cd479 Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Thu, 9 Dec 2021 18:21:29 +0530 Subject: [PATCH 30/37] fix: fixed the issue of duplicate join requests Someone was spamming mon.school by generating thousands of join requests. Added to fix to avoid creating duplicate requests. --- school/lms/api.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/school/lms/api.py b/school/lms/api.py index 7e60cbb7..f6aee823 100644 --- a/school/lms/api.py +++ b/school/lms/api.py @@ -65,11 +65,16 @@ def join_cohort(course, cohort, subgroup, invite_code): "doctype": "Cohort Join Request", "cohort": cohort_doc.name, "subgroup": subgroup_doc.name, - "email": frappe.session.user + "email": frappe.session.user, + "status": "Pending" } - doc = frappe.get_doc(data) - doc.insert(ignore_permissions=True) - return {"ok": True} + # 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): From bc80c2200d8564f2a381fee1eac3a82a1ae938ef Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Mon, 13 Dec 2021 11:16:56 +0530 Subject: [PATCH 31/37] feat: add anchor to exercise to allow linking to an exercise --- school/templates/exercise.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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() %} From 8f740d70e0aba654c1e4a75fb0a48bf4bb325dde Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Mon, 13 Dec 2021 21:30:19 +0530 Subject: [PATCH 32/37] fix: broken link to the course on join page --- school/www/cohorts/join.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/school/www/cohorts/join.html b/school/www/cohorts/join.html index a31ecc63..34b55c2e 100644 --- a/school/www/cohorts/join.html +++ b/school/www/cohorts/join.html @@ -30,7 +30,7 @@ {% elif subgroup.has_student(frappe.session.user) %}

      You are already a student of this course.

      - Go to the course + Start Learning →
      {% elif subgroup.has_join_request(frappe.session.user) %}
      From 465bc3b24a920e91a84c48dde1bd54b3f17a6345 Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Tue, 14 Dec 2021 22:09:51 +0530 Subject: [PATCH 33/37] feat: allow the same custom page to be avaiable both for cohort and subgroup --- school/www/cohorts/cohort.py | 1 + school/www/cohorts/subgroup.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/school/www/cohorts/cohort.py b/school/www/cohorts/cohort.py index 7f890367..64c63757 100644 --- a/school/www/cohorts/cohort.py +++ b/school/www/cohorts/cohort.py @@ -24,6 +24,7 @@ def get_context(context): 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( diff --git a/school/www/cohorts/subgroup.py b/school/www/cohorts/subgroup.py index 94c83764..4ab8c056 100644 --- a/school/www/cohorts/subgroup.py +++ b/school/www/cohorts/subgroup.py @@ -49,10 +49,11 @@ def get_context(context): 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), + cohort.get_page_template(page, scope="Subgroup"), context) def get_stats(subgroup): From 051196179dd69610a2612921a243f702c6b19232 Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Wed, 15 Dec 2021 11:42:52 +0530 Subject: [PATCH 34/37] fix: failing test in doctype Exercise --- school/lms/doctype/exercise/test_exercise.py | 7 +++++++ 1 file changed, 7 insertions(+) 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", From 274ecaa222bcd43e98c129318c0a9f67faa6a7a7 Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Wed, 15 Dec 2021 22:27:57 +0530 Subject: [PATCH 35/37] fix: typo in exercise_submission.py Corrected the use of save instead of insert. --- school/lms/doctype/exercise_submission/exercise_submission.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/school/lms/doctype/exercise_submission/exercise_submission.py b/school/lms/doctype/exercise_submission/exercise_submission.py index 27290c79..7209826f 100644 --- a/school/lms/doctype/exercise_submission/exercise_submission.py +++ b/school/lms/doctype/exercise_submission/exercise_submission.py @@ -21,4 +21,4 @@ class ExerciseSubmission(Document): "member": self.member, "latest_submission": self.name }) - doc.save(ignore_permissions=True) + doc.insert(ignore_permissions=True) From eb59713b656fc33106dd792406bdb0d42f6575d6 Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Thu, 16 Dec 2021 11:45:29 +0530 Subject: [PATCH 36/37] fix: delete invalid links from Cohort doctype --- school/lms/doctype/cohort/cohort.json | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/school/lms/doctype/cohort/cohort.json b/school/lms/doctype/cohort/cohort.json index f9cb7527..62caae8a 100644 --- a/school/lms/doctype/cohort/cohort.json +++ b/school/lms/doctype/cohort/cohort.json @@ -104,19 +104,9 @@ "group": "Links", "link_doctype": "Cohort Subgroup", "link_fieldname": "cohort" - }, - { - "group": "Links", - "link_doctype": "Cohort Student", - "link_fieldname": "cohort" - }, - { - "group": "Links", - "link_doctype": "Cohort Page", - "link_fieldname": "cohort" } ], - "modified": "2021-12-04 23:22:10.248781", + "modified": "2021-12-16 11:45:12.451226", "modified_by": "Administrator", "module": "LMS", "name": "Cohort", From 668a5d6334270a772c21eaa79a41234a514ed863 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Thu, 16 Dec 2021 15:20:08 +0530 Subject: [PATCH 37/37] fix: labels and order --- school/lms/doctype/cohort/cohort.json | 7 +++-- .../cohort_join_request.json | 9 +++++-- .../doctype/cohort_staff/cohort_staff.json | 9 +++++-- .../cohort_subgroup/cohort_subgroup.json | 27 +++++++++++-------- .../lms_batch_membership.json | 27 +++++++++++++------ school/www/cohorts/__init__.py | 0 6 files changed, 52 insertions(+), 27 deletions(-) create mode 100644 school/www/cohorts/__init__.py diff --git a/school/lms/doctype/cohort/cohort.json b/school/lms/doctype/cohort/cohort.json index 62caae8a..2efa1428 100644 --- a/school/lms/doctype/cohort/cohort.json +++ b/school/lms/doctype/cohort/cohort.json @@ -8,8 +8,8 @@ "engine": "InnoDB", "field_order": [ "course", - "slug", "title", + "slug", "section_break_2", "instructor", "status", @@ -61,7 +61,6 @@ "fieldtype": "Data", "in_list_view": 1, "label": "Slug", - "reqd": 1, "unique": 1 }, { @@ -80,7 +79,7 @@ "fieldname": "title", "fieldtype": "Data", "in_list_view": 1, - "label": "title", + "label": "Title", "reqd": 1 }, { @@ -106,7 +105,7 @@ "link_fieldname": "cohort" } ], - "modified": "2021-12-16 11:45:12.451226", + "modified": "2021-12-16 14:44:25.406301", "modified_by": "Administrator", "module": "LMS", "name": "Cohort", diff --git a/school/lms/doctype/cohort_join_request/cohort_join_request.json b/school/lms/doctype/cohort_join_request/cohort_join_request.json index 476d05ed..8ecd4be3 100644 --- a/school/lms/doctype/cohort_join_request/cohort_join_request.json +++ b/school/lms/doctype/cohort_join_request/cohort_join_request.json @@ -7,8 +7,9 @@ "engine": "InnoDB", "field_order": [ "cohort", - "subgroup", "email", + "column_break_3", + "subgroup", "status" ], "fields": [ @@ -42,11 +43,15 @@ "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-11-29 12:18:15.947544", + "modified": "2021-12-16 15:06:03.985221", "modified_by": "Administrator", "module": "LMS", "name": "Cohort Join Request", diff --git a/school/lms/doctype/cohort_staff/cohort_staff.json b/school/lms/doctype/cohort_staff/cohort_staff.json index 1fd207d7..dca4e0f9 100644 --- a/school/lms/doctype/cohort_staff/cohort_staff.json +++ b/school/lms/doctype/cohort_staff/cohort_staff.json @@ -8,6 +8,7 @@ "field_order": [ "cohort", "course", + "column_break_3", "email", "role" ], @@ -24,7 +25,7 @@ "fieldname": "email", "fieldtype": "Link", "in_list_view": 1, - "label": "E-mail", + "label": "User", "options": "User", "reqd": 1 }, @@ -43,11 +44,15 @@ "label": "Course", "options": "LMS Course", "read_only": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-11-29 16:31:57.214875", + "modified": "2021-12-16 15:16:04.042372", "modified_by": "Administrator", "module": "LMS", "name": "Cohort Staff", diff --git a/school/lms/doctype/cohort_subgroup/cohort_subgroup.json b/school/lms/doctype/cohort_subgroup/cohort_subgroup.json index 71732e24..06bc00ab 100644 --- a/school/lms/doctype/cohort_subgroup/cohort_subgroup.json +++ b/school/lms/doctype/cohort_subgroup/cohort_subgroup.json @@ -10,9 +10,11 @@ "cohort", "slug", "title", - "description", + "column_break_4", "invite_code", - "course" + "course", + "section_break_7", + "description" ], "fields": [ { @@ -31,18 +33,18 @@ "in_list_view": 1, "in_preview": 1, "in_standard_filter": 1, - "label": "title", + "label": "Title", "reqd": 1 }, { "fieldname": "description", "fieldtype": "Markdown Editor", - "label": "description" + "label": "Description" }, { "fieldname": "invite_code", "fieldtype": "Data", - "label": "invite_code", + "label": "Invite Code", "read_only": 1 }, { @@ -58,22 +60,25 @@ "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 Student", - "link_fieldname": "subgroup" - }, { "group": "Links", "link_doctype": "Cohort Join Request", "link_fieldname": "subgroup" } ], - "modified": "2021-11-30 07:41:54.893270", + "modified": "2021-12-16 15:12:42.504883", "modified_by": "Administrator", "module": "LMS", "name": "Cohort Subgroup", 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 8514100f..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,17 +6,19 @@ "engine": "InnoDB", "field_order": [ "course", - "member", - "member_name", - "member_username", + "cohort", + "subgroup", "column_break_3", "batch", - "member_type", - "progress", "current_lesson", "role", - "cohort", - "subgroup" + "member_section", + "member", + "member_type", + "progress", + "column_break_12", + "member_name", + "member_username" ], "fields": [ { @@ -102,11 +104,20 @@ "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": [], - "modified": "2021-11-29 15:22:31.609216", + "modified": "2021-12-16 14:49:25.964853", "modified_by": "Administrator", "module": "LMS", "name": "LMS Batch Membership", diff --git a/school/www/cohorts/__init__.py b/school/www/cohorts/__init__.py new file mode 100644 index 00000000..e69de29b
      {{loop.index}} {{r.creation}} {{r.email}} Approve | Reject{{loop.index}} {{r.creation}} {{r.email}} Undo