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. """