Merge pull request #272 from anandology/cohorts-v1

This commit is contained in:
Jannat Patel
2021-12-17 10:45:56 +05:30
committed by GitHub
54 changed files with 1956 additions and 14 deletions

View File

@@ -141,6 +141,12 @@ website_route_rules = [
{"from_route": "/courses/<course>/learn/<int:chapter>.<int:lesson>", "to_route": "batch/learn"},
{"from_route": "/courses/<course>/progress", "to_route": "batch/progress"},
{"from_route": "/courses/<course>/join", "to_route": "batch/join"},
{"from_route": "/courses/<course>/manage", "to_route": "cohorts"},
{"from_route": "/courses/<course>/cohorts/<cohort>", "to_route": "cohorts/cohort"},
{"from_route": "/courses/<course>/cohorts/<cohort>/<page>", "to_route": "cohorts/cohort"},
{"from_route": "/courses/<course>/subgroups/<cohort>/<subgroup>", "to_route": "cohorts/subgroup"},
{"from_route": "/courses/<course>/subgroups/<cohort>/<subgroup>/<page>", "to_route": "cohorts/subgroup"},
{"from_route": "/courses/<course>/join/<cohort>/<subgroup>/<invite_code>", "to_route": "cohorts/join"},
{"from_route": "/users", "to_route": "profiles/profile"}
]

View File

@@ -45,3 +45,119 @@ def save_current_lesson(course_name, lesson_name):
doc.current_lesson = lesson_name
doc.save(ignore_permissions=True)
return {"current_lesson": doc.current_lesson}
@frappe.whitelist()
def join_cohort(course, cohort, subgroup, invite_code):
"""Creates a Cohort Join Request for given user.
"""
course_doc = frappe.get_doc("LMS Course", course)
cohort_doc = course_doc and course_doc.get_cohort(cohort)
subgroup_doc = cohort_doc and cohort_doc.get_subgroup(subgroup)
if not subgroup_doc or subgroup_doc.invite_code != invite_code:
return {
"ok": False,
"error": "Invalid join link"
}
data = {
"doctype": "Cohort Join Request",
"cohort": cohort_doc.name,
"subgroup": subgroup_doc.name,
"email": frappe.session.user,
"status": "Pending"
}
# Don't insert duplicate records
if frappe.db.exists(data):
return {"ok": True, "status": "record found"}
else:
doc = frappe.get_doc(data)
doc.insert(ignore_permissions=True)
return {"ok": True, "status": "record created"}
@frappe.whitelist()
def approve_cohort_join_request(join_request):
r = frappe.get_doc("Cohort Join Request", join_request)
sg = r and frappe.get_doc("Cohort Subgroup", r.subgroup)
if not sg or r.status not in ["Pending", "Accepted"]:
return {
"ok": False,
"error": "Invalid Join Request"
}
if not sg.is_manager(frappe.session.user) and "System Manager" not in frappe.get_roles():
return {
"ok": False,
"error": "Permission Deined"
}
r.status = "Accepted"
r.save(ignore_permissions=True)
return {"ok": True}
@frappe.whitelist()
def reject_cohort_join_request(join_request):
r = frappe.get_doc("Cohort Join Request", join_request)
sg = r and frappe.get_doc("Cohort Subgroup", r.subgroup)
if not sg or r.status not in ["Pending", "Rejected"]:
return {
"ok": False,
"error": "Invalid Join Request"
}
if not sg.is_manager(frappe.session.user) and "System Manager" not in frappe.get_roles():
return {
"ok": False,
"error": "Permission Deined"
}
r.status = "Rejected"
r.save(ignore_permissions=True)
return {"ok": True}
@frappe.whitelist()
def undo_reject_cohort_join_request(join_request):
r = frappe.get_doc("Cohort Join Request", join_request)
sg = r and frappe.get_doc("Cohort Subgroup", r.subgroup)
# keeping Pending as well to consider the case of duplicate requests
if not sg or r.status not in ["Pending", "Rejected"]:
return {
"ok": False,
"error": "Invalid Join Request"
}
if not sg.is_manager(frappe.session.user) and "System Manager" not in frappe.get_roles():
return {
"ok": False,
"error": "Permission Deined"
}
r.status = "Pending"
r.save(ignore_permissions=True)
return {"ok": True}
@frappe.whitelist()
def add_mentor_to_subgroup(subgroup, email):
try:
sg = frappe.get_doc("Cohort Subgroup", subgroup)
except frappe.DoesNotExistError:
return {
"ok": False,
"error": f"Invalid subgroup: {subgroup}"
}
if not sg.get_cohort().is_admin(frappe.session.user) and "System Manager" not in frappe.get_roles():
return {
"ok": False,
"error": "Permission Deined"
}
try:
user = frappe.get_doc("User", email)
except frappe.DoesNotExistError:
return {
"ok": False,
"error": f"Invalid user: {email}"
}
sg.add_mentor(email)
return {"ok": True}

View File

View File

@@ -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) {
// }
});

View File

@@ -0,0 +1,131 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "format:{course}/{slug}",
"creation": "2021-11-19 11:45:31.016097",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"course",
"title",
"slug",
"section_break_2",
"instructor",
"status",
"column_break_4",
"begin_date",
"end_date",
"duration",
"section_break_8",
"description",
"pages"
],
"fields": [
{
"fieldname": "description",
"fieldtype": "Markdown Editor",
"label": "Description"
},
{
"fieldname": "instructor",
"fieldtype": "Link",
"label": "Instructor",
"options": "User",
"reqd": 1
},
{
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "Upcoming\nLive\nCompleted\nCancelled",
"reqd": 1
},
{
"fieldname": "begin_date",
"fieldtype": "Date",
"label": "Begin Date"
},
{
"fieldname": "end_date",
"fieldtype": "Date",
"label": "End Date"
},
{
"fieldname": "duration",
"fieldtype": "Data",
"label": "Duration"
},
{
"fieldname": "slug",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Slug",
"unique": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_8",
"fieldtype": "Section Break"
},
{
"fieldname": "section_break_2",
"fieldtype": "Section Break"
},
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title",
"reqd": 1
},
{
"fieldname": "course",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Course",
"options": "LMS Course",
"reqd": 1
},
{
"fieldname": "pages",
"fieldtype": "Table",
"label": "Pages",
"options": "Cohort Web Page"
}
],
"index_web_pages_for_search": 1,
"links": [
{
"group": "Links",
"link_doctype": "Cohort Subgroup",
"link_fieldname": "cohort"
}
],
"modified": "2021-12-16 14:44:25.406301",
"modified_by": "Administrator",
"module": "LMS",
"name": "Cohort",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -0,0 +1,85 @@
# Copyright (c) 2021, FOSS United and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
class Cohort(Document):
def get_url(self):
return f"{frappe.utils.get_url()}/courses/{self.course}/cohorts/{self.slug}"
def get_subgroups(self, include_counts=False, sort_by=None):
names = frappe.get_all("Cohort Subgroup", filters={"cohort": self.name}, pluck="name")
subgroups = [frappe.get_cached_doc("Cohort Subgroup", name) for name in names]
subgroups = sorted(subgroups, key=lambda sg: sg.title)
if include_counts:
mentors = self._get_subgroup_counts("Cohort Mentor")
students = self._get_subgroup_counts("LMS Batch Membership")
join_requests = self._get_subgroup_counts("Cohort Join Request", status="Pending")
for s in subgroups:
s.num_mentors = mentors.get(s.name, 0)
s.num_students = students.get(s.name, 0)
s.num_join_requests = join_requests.get(s.name, 0)
if sort_by:
subgroups.sort(key=lambda sg: getattr(sg, sort_by), reverse=True)
return subgroups
def _get_subgroup_counts(self, doctype, **kw):
rows = frappe.get_all(doctype,
filters={"cohort": self.name, **kw},
fields=['subgroup', 'count(*) as count'],
group_by='subgroup')
return {row['subgroup']: row['count'] for row in rows}
def _get_count(self, doctype, **kw):
filters = {"cohort": self.name, **kw}
return frappe.db.count(doctype, filters=filters)
def get_page_template(self, slug, scope=None):
p = self.get_page(slug, scope=scope)
return p and p.get_template_html()
def get_page(self, slug, scope=None):
for p in self.pages:
if p.slug == slug and scope in [p.scope, None]:
return p
def get_pages(self, scope=None):
return [p for p in self.pages if scope in [p.scope, None]]
def get_stats(self):
return {
"subgroups": self._get_count("Cohort Subgroup"),
"mentors": self._get_count("Cohort Mentor"),
"students": self._get_count("LMS Batch Membership"),
"join_requests": self._get_count("Cohort Join Request", status="Pending"),
}
def get_subgroup(self, slug):
q = dict(cohort=self.name, slug=slug)
name = frappe.db.get_value("Cohort Subgroup", q, "name")
return name and frappe.get_doc("Cohort Subgroup", name)
def get_mentor(self, email):
q = dict(cohort=self.name, email=email)
name = frappe.db.get_value("Cohort Mentor", q, "name")
return name and frappe.get_doc("Cohort Mentor", name)
def is_mentor(self, email):
q = {
"doctype": "Cohort Mentor",
"cohort": self.name,
"email": email
}
return frappe.db.exists(q)
def is_admin(self, email):
q = {
"doctype": "Cohort Staff",
"cohort": self.name,
"email": email,
"role": "Admin"
}
return frappe.db.exists(q)

View File

@@ -0,0 +1,8 @@
# Copyright (c) 2021, FOSS United and Contributors
# See license.txt
# import frappe
import unittest
class TestCohort(unittest.TestCase):
pass

View File

@@ -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) {
// }
});

View File

@@ -0,0 +1,76 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2021-11-19 16:27:41.716509",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"cohort",
"email",
"column_break_3",
"subgroup",
"status"
],
"fields": [
{
"fieldname": "cohort",
"fieldtype": "Link",
"label": "Cohort",
"options": "Cohort",
"reqd": 1
},
{
"fieldname": "subgroup",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Subgroup",
"options": "Cohort Subgroup",
"reqd": 1
},
{
"fieldname": "email",
"fieldtype": "Link",
"in_list_view": 1,
"label": "E-Mail",
"options": "User",
"reqd": 1
},
{
"default": "Pending",
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Pending\nAccepted\nRejected"
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-12-16 15:06:03.985221",
"modified_by": "Administrator",
"module": "LMS",
"name": "Cohort Join Request",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -0,0 +1,51 @@
# Copyright (c) 2021, FOSS United and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
class CohortJoinRequest(Document):
def on_update(self):
if self.status == "Accepted":
self.ensure_student()
def ensure_student(self):
# case 1 - user is already a member
q = {
"doctype": "LMS Batch Membership",
"cohort": self.cohort,
"subgroup": self.subgroup,
"member": self.email,
"member_type": "Student"
}
if frappe.db.exists(q):
return
# case 2 - user has signed up for this course, possibly not this cohort
cohort = frappe.get_doc("Cohort", self.cohort)
q = {
"doctype": "LMS Batch Membership",
"course": cohort.course,
"member": self.email,
"member_type": "Student"
}
name = frappe.db.exists(q)
if name:
doc = frappe.get_doc("LMS Batch Membership", name)
doc.cohort = self.cohort
doc.subgroup = self.subgroup
doc.save(ignore_permissions=True)
else:
# case 3 - user has not signed up for this course yet
data = {
"doctype": "LMS Batch Membership",
"course": cohort.course,
"cohort": self.cohort,
"subgroup": self.subgroup,
"member": self.email,
"member_type": "Student",
"role": "Member"
}
doc = frappe.get_doc(data)
doc.insert(ignore_permissions=True)

View File

@@ -0,0 +1,8 @@
# Copyright (c) 2021, FOSS United and Contributors
# See license.txt
# import frappe
import unittest
class TestCohortJoinRequest(unittest.TestCase):
pass

View File

@@ -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) {
// }
});

View File

@@ -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
}

View File

@@ -0,0 +1,12 @@
# Copyright (c) 2021, FOSS United and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
class CohortMentor(Document):
def get_subgroup(self):
return frappe.get_doc("Cohort Subgroup", self.subgroup)
def get_user(self):
return frappe.get_doc("User", self.email)

View File

@@ -0,0 +1,8 @@
# Copyright (c) 2021, FOSS United and Contributors
# See license.txt
# import frappe
import unittest
class TestCohortMentor(unittest.TestCase):
pass

View File

@@ -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) {
// }
});

View File

@@ -0,0 +1,77 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2021-11-19 15:35:00.551949",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"cohort",
"course",
"column_break_3",
"email",
"role"
],
"fields": [
{
"fieldname": "cohort",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Cohort",
"options": "Cohort",
"reqd": 1
},
{
"fieldname": "email",
"fieldtype": "Link",
"in_list_view": 1,
"label": "User",
"options": "User",
"reqd": 1
},
{
"fieldname": "role",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Role",
"options": "Admin\nManager\nStaff",
"reqd": 1
},
{
"fetch_from": "cohort.course",
"fieldname": "course",
"fieldtype": "Link",
"label": "Course",
"options": "LMS Course",
"read_only": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-12-16 15:16:04.042372",
"modified_by": "Administrator",
"module": "LMS",
"name": "Cohort Staff",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -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

View File

@@ -0,0 +1,8 @@
# Copyright (c) 2021, FOSS United and Contributors
# See license.txt
# import frappe
import unittest
class TestCohortStaff(unittest.TestCase):
pass

View File

@@ -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) {
// }
});

View File

@@ -0,0 +1,104 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "format:{title} ({cohort})",
"creation": "2021-11-19 11:50:27.312434",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"cohort",
"slug",
"title",
"column_break_4",
"invite_code",
"course",
"section_break_7",
"description"
],
"fields": [
{
"fieldname": "cohort",
"fieldtype": "Link",
"in_list_view": 1,
"in_preview": 1,
"in_standard_filter": 1,
"label": "Cohort",
"options": "Cohort",
"reqd": 1
},
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"in_preview": 1,
"in_standard_filter": 1,
"label": "Title",
"reqd": 1
},
{
"fieldname": "description",
"fieldtype": "Markdown Editor",
"label": "Description"
},
{
"fieldname": "invite_code",
"fieldtype": "Data",
"label": "Invite Code",
"read_only": 1
},
{
"fieldname": "slug",
"fieldtype": "Data",
"label": "Slug",
"reqd": 1
},
{
"fetch_from": "cohort.course",
"fieldname": "course",
"fieldtype": "Link",
"label": "Course",
"options": "LMS Course",
"read_only": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_7",
"fieldtype": "Section Break"
}
],
"index_web_pages_for_search": 1,
"links": [
{
"group": "Links",
"link_doctype": "Cohort Join Request",
"link_fieldname": "subgroup"
}
],
"modified": "2021-12-16 15:12:42.504883",
"modified_by": "Administrator",
"module": "LMS",
"name": "Cohort Subgroup",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -0,0 +1,95 @@
# Copyright (c) 2021, FOSS United and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
from frappe.utils import random_string
class CohortSubgroup(Document):
def before_save(self):
if not self.invite_code:
self.invite_code = random_string(8)
def get_url(self):
cohort = frappe.get_doc("Cohort", self.cohort)
return f"{frappe.utils.get_url()}/courses/{self.course}/subgroups/{cohort.slug}/{self.slug}"
def get_invite_link(self):
cohort = frappe.get_doc("Cohort", self.cohort)
return f"{frappe.utils.get_url()}/courses/{self.course}/join/{cohort.slug}/{self.slug}/{self.invite_code}"
def has_student(self, email):
"""Check if given user is a student of this subgroup.
"""
q = {
"doctype": "LMS Batch Membership",
"subgroup": self.name,
"member": email
}
return frappe.db.exists(q)
def has_join_request(self, email):
"""Check if given user is a student of this subgroup.
"""
q = {
"doctype": "Cohort Join Request",
"subgroup": self.name,
"email": email
}
return frappe.db.exists(q)
def get_join_requests(self, status="Pending"):
q = {
"subgroup": self.name,
"status": status
}
return frappe.get_all("Cohort Join Request", filters=q, fields=["*"], order_by="creation desc")
def get_mentors(self):
emails = frappe.get_all("Cohort Mentor", filters={"subgroup": self.name}, fields=["email"], pluck='email')
return self._get_users(emails)
def get_students(self):
emails = frappe.get_all("LMS Batch Membership",
filters={"subgroup": self.name},
fields=["member"],
pluck='member',
page_length=1000)
return self._get_users(emails)
def _get_users(self, emails):
users = [frappe.get_cached_doc("User", email) for email in emails]
return sorted(users, key=lambda user: user.full_name)
def is_mentor(self, email):
q = {
"doctype": "Cohort Mentor",
"subgroup": self.name,
"email": email
}
return frappe.db.exists(q)
def is_manager(self, email):
"""Returns True if the given user is a manager of this subgroup.
Mentors of the subgroup, admins of the Cohort are considered as managers.
"""
return self.is_mentor(email) or self.get_cohort().is_admin(email)
def get_cohort(self):
return frappe.get_doc("Cohort", self.cohort)
def add_mentor(self, email):
d = {
"doctype": "Cohort Mentor",
"subgroup": self.name,
"cohort": self.cohort,
"email": email
}
if frappe.db.exists(d):
return
doc = frappe.get_doc(d)
doc.insert(ignore_permissions=True)
#def after_doctype_insert():
# frappe.db.add_unique("Cohort Subgroup", ("cohort", "slug"))

View File

@@ -0,0 +1,8 @@
# Copyright (c) 2021, FOSS United and Contributors
# See license.txt
# import frappe
import unittest
class TestCohortSubgroup(unittest.TestCase):
pass

View File

@@ -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"
}

View File

@@ -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

View File

@@ -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

View File

@@ -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",

View File

@@ -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) {
// }
});

View File

@@ -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
}

View File

@@ -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

View File

@@ -0,0 +1,8 @@
# Copyright (c) 2021, Frappe and Contributors
# See license.txt
# import frappe
import unittest
class TestExerciseLatestSubmission(unittest.TestCase):
pass

View File

@@ -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",

View File

@@ -5,4 +5,20 @@ import frappe
from frappe.model.document import Document
class ExerciseSubmission(Document):
pass
def on_update(self):
self.update_latest_submission()
def update_latest_submission(self):
names = frappe.get_all("Exercise Latest Submission", {"exercise": self.exercise, "member": self.member})
if names:
doc = frappe.get_doc("Exercise Latest Submission", names[0])
doc.latest_submission = self.name
doc.save(ignore_permissions=True)
else:
doc = frappe.get_doc({
"doctype": "Exercise Latest Submission",
"exercise": self.exercise,
"member": self.member,
"latest_submission": self.name
})
doc.insert(ignore_permissions=True)

View File

@@ -6,15 +6,19 @@
"engine": "InnoDB",
"field_order": [
"course",
"member",
"member_name",
"member_username",
"cohort",
"subgroup",
"column_break_3",
"batch",
"current_lesson",
"role",
"member_section",
"member",
"member_type",
"progress",
"current_lesson",
"role"
"column_break_12",
"member_name",
"member_username"
],
"fields": [
{
@@ -88,12 +92,32 @@
"fieldtype": "Data",
"label": "Progress",
"read_only": 1
},
{
"fieldname": "cohort",
"fieldtype": "Link",
"label": "Cohort",
"options": "Cohort"
},
{
"fieldname": "subgroup",
"fieldtype": "Link",
"label": "Subgroup",
"options": "Cohort Subgroup"
},
{
"fieldname": "member_section",
"fieldtype": "Section Break",
"label": "Member"
},
{
"fieldname": "column_break_12",
"fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"links": [],
"migration_hash": "fe10c462acf5e727d864305d7ce90e73",
"modified": "2021-10-20 15:10:33.767419",
"modified": "2021-12-16 14:49:25.964853",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Batch Membership",

View File

@@ -210,6 +210,28 @@ class LMSCourse(Document):
visibility="Public")
return batches
def get_cohorts(self):
return find_all("Cohort", course=self.name, order_by="creation")
def get_cohort(self, cohort_slug):
name = frappe.get_value("Cohort", {"course": self.name, "slug": cohort_slug})
return name and frappe.get_doc("Cohort", name)
def is_cohort_staff(self, user_email):
"""Returns True if the user is either a mentor or a staff for one or more active cohorts of this course.
"""
q1 = {
"doctype": "Cohort Staff",
"course": self.name,
"email": user_email
}
q2 = {
"doctype": "Cohort Mentor",
"course": self.name,
"email": user_email
}
return frappe.db.exists(q1) or frappe.db.exists(q2)
def get_lesson_index(self, lesson_name):
"""Returns the {chapter_index}.{lesson_index} for the lesson.
"""

View File

@@ -1,5 +1,5 @@
<div class="exercise">
<h3>Exercise {{exercise.index_label}}: {{ exercise.title }}</h3>
<h3><a name="E{{exercise.index_label}}">Exercise {{exercise.index_label}}: {{ exercise.title }}</a></h3>
<div class="exercise-description">{{frappe.utils.md_to_html(exercise.description)}}</div>
{% set submission = exercise.get_user_submission() %}

View File

View File

@@ -0,0 +1,34 @@
{% extends "templates/base.html" %}
{% macro render_nav(nav) %}
<div class="breadcrumb">
{% for link in nav %}
<a class="dark-links" href="{{ link.href }}">{{ link.title }}</a>
{% if not loop.last %}
<img class="ml-1 mr-1" src="/assets/school/icons/chevron-right.svg">
{% endif %}
{% endfor %}
</div>
{% endmacro %}
{% block title %}Cohorts{% endblock %}
{% block head_include %}
<meta name="description" content="Cohorts" />
<meta name="keywords" content="Cohorts" />
{% endblock %}
{% block content %}
<div class="common-page-style">
<div class='container'>
{{ render_nav(nav | default([])) }}
{% block page_content %}
Hello, world!
{% endblock %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,77 @@
{% extends "www/cohorts/base.html" %}
{% block title %}Manage {{ course.title }}{% endblock %}
{% block page_content %}
<h2>{{cohort.title}} <span class="badge badge-secondary">Cohort</span></h2>
<p>
{% set stats = cohort.get_stats() %}
{{ stats.subgroups }} Subgroups
| {{ stats.mentors }} Mentors
| {{ stats.students }} students
| {{ stats.join_requests }} join requests
</p>
{% if is_mentor %}
<div class="alert alert-info">
{% set sg = mentor.get_subgroup() %}
<p>You are a mentor of <b>{{sg.title}}</b> subgroup.</p>
<p><a href="{{sg.get_url()}}" class="btn btn-primary">Visit Your Subgroup &rarr;</a></p>
</div>
{% endif %}
<ul class="nav nav-tabs">
{% set num_subgroups = cohort.get_subgroups() | length %}
{{ render_navitem("Subgroups", "", page=page, count=num_subgroups) }}
{% for p in cohort.get_pages(scope="Cohort") %}
{{ render_navitem(p.title, p.slug, page=page) }}
{% endfor %}
</ul>
<div class="my-5">
{% if not page %}
{{ render_subgroups() }}
{% else %}
{{ render_page(page) }}
{% endif %}
</div>
{% endblock %}
{% macro render_subgroups() %}
<ul class="list-group">
{% for sg in cohort.get_subgroups(include_counts=True) %}
<li class="list-group-item">
<div>
<a class="subgroup-title"
style="font-weight: 700; color: inherit;"
href="/courses/{{course.name}}/subgroups/{{cohort.slug}}/{{sg.slug}}"
>{{sg.title}}</a>
</div>
<div style="font-size: 0.8em;">
{{sg.num_mentors}} Mentors
|
{{sg.num_students}} Students
|
{{sg.num_join_requests}} Join Requests
</div>
</li>
{% endfor %}
</ul>
{% endmacro %}
{% macro render_navitem(title, link, page, count=-1) %}
<li class="nav-item">
<a
class="nav-link {{ 'active' if link==page }}"
href="/courses/{{course.name}}/cohorts/{{cohort.slug}}/{{link}}"
>{{title}}
{% if count != -1 %}
<span
class="badge {{'badge-primary' if link==page else 'badge-secondary'}}"
>{{count}}</span>
{% endif %}
</a>
</li>
{% endmacro %}

View File

@@ -0,0 +1,32 @@
import frappe
from . import utils
def get_context(context):
context.no_cache = 1
course = utils.get_course()
cohort = course and utils.get_cohort(course, frappe.form_dict["cohort"])
if not cohort:
context.template = "www/404.html"
return
user = frappe.session.user
mentor = cohort.get_mentor(user)
is_mentor = mentor is not None
is_admin = cohort.is_admin(user) or "System Manager" in frappe.get_roles()
utils.add_nav(context, "All Courses", "/courses")
utils.add_nav(context, course.title, "/courses/" + course.name)
utils.add_nav(context, "Cohorts", "/courses/" + course.name + "/manage")
context.course = course
context.cohort = cohort
context.mentor = mentor
context.is_mentor = is_mentor
context.is_admin = is_admin
context.page = frappe.form_dict.get("page") or ""
context.page_scope = "Cohort"
# Function to render to custom page given the slug
context.render_page = lambda page: frappe.render_template(
cohort.get_page_template(page, scope="Cohort"),
context)

View File

@@ -0,0 +1,38 @@
{% extends "www/cohorts/base.html" %}
{% block title %}Manage {{ course.title }}{% endblock %}
{% block page_content %}
{% if cohorts %}
<h2>Cohorts</h2>
<div class="row">
{% for cohort in cohorts %}
<div class="col-md-6">
{{ render_cohort(course, cohort) }}
</div>
{% endfor %}
</div>
{% else %}
<h2>Permission Denied</h2>
<p>You don't have permission to manage this course.</p>
{% endif %}
{% endblock %}
{% macro render_cohort(course, cohort) %}
<div class="card">
<div class="card-body">
<h5 class="card-title">{{cohort.title}}</h5>
<h6 class="card-subtitle mb-2 text-muted">{{cohort.begin_date}} - {{cohort.end_date}}</h6>
<p>
{% set stats = cohort.get_stats() %}
{{ stats.subgroups }} Subgroups
| {{ stats.mentors }} Mentors
| {{ stats.students }} students
| {{ stats.join_requests }} join requests
</p>
<a href="/courses/{{course.name}}/cohorts/{{cohort.slug}}" class="card-link">Manage</a>
</div>
</div>
{% endmacro %}

View File

@@ -0,0 +1,31 @@
import frappe
from .utils import get_course, add_nav
def get_context(context):
context.no_cache = 1
context.course = get_course()
if frappe.session.user == "Guest":
frappe.local.flags.redirect_location = "/login?redirect-to=" + frappe.request.path
raise frappe.Redirect()
if not context.course:
context.template = "www/404.html"
return
context.cohorts = get_cohorts(context.course)
if len(context.cohorts) == 1:
frappe.local.flags.redirect_location = context.cohorts[0].get_url()
raise frappe.Redirect
add_nav(context, "All Courses", "/courses")
add_nav(context, context.course.title, "/courses/" + context.course.name)
def get_cohorts(course):
if "System Manager" in frappe.get_roles():
return course.get_cohorts()
staff_roles = frappe.get_all("Cohort Staff", filters={"course": course.name}, fields=["cohort"])
mentor_roles = frappe.get_all("Cohort Mentor", filters={"course": course.name}, fields=["cohort"])
roles = staff_roles + mentor_roles
names = {role.cohort for role in roles}
return [frappe.get_doc("Cohort", name) for name in names]

View File

@@ -0,0 +1,88 @@
{% extends "www/cohorts/base.html" %}
{% block title %}Join Course{% endblock %}
{% block page_content %}
<h2>Join Course</h2>
<p>
Course: {{course.title}}
</p>
<p>
Cohort: {{cohort.title}}
</p>
<p>
Subgroup: {{subgroup.title}}
</p>
{% if frappe.session.user == "Guest" %}
<div class="alert alert-warning">
<p>
Please login to be able to join the course.</p>
<p>
If you don't already have an account, you can <a href="/login#signup">sign up for a new account</a>.
</p>
<a class="btn btn-primary" href="/login">Login to continue</a>
</div>
{% elif subgroup.has_student(frappe.session.user) %}
<div class="alert alert-info">
<p>You are already a student of this course.</p>
<a class="btn btn-primary" href="/">Start Learning &rarr;</a>
</div>
{% elif subgroup.has_join_request(frappe.session.user) %}
<div class="alert alert-info">
<p>We have received your request to join the course. You'll hear back from us soon.</p>
</div>
{% else %}
<a class="btn btn-primary" id="join">Join the course</a>
{% endif %}
{% endblock %}
{% block script %}
<script type="text/javascript">
$(function() {
console.log("ready!")
$("#join").click(function() {
var parts = window.location.pathname.split("/")
var course = parts[2];
var cohort = parts[4];
var subgroup = parts[5];
var invite_code = parts[6];
frappe.call('school.lms.api.join_cohort', {
course: course,
cohort: cohort,
subgroup: subgroup,
invite_code: invite_code
})
.then(r => {
if (r.message.ok) {
let d = new frappe.ui.Dialog({
title: "Notification",
primary_action_label: "Proceed",
primary_action() {
d.hide();
window.location.reload();
}
});
var message = "We've received your interest to join the course. We'll hear from us soon.";
d.show();
d.set_message(message);
}
else {
frappe.msgprint(r.message.error);
}
});
});
});
</script>
{% endblock %}

View File

@@ -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

View File

@@ -0,0 +1,264 @@
{% extends "www/cohorts/base.html" %}
{% block title %} Subgroup {{subgroup.title}} - {{ course.title }} {% endblock %}
{% block page_content %}
<h2 id="page-title"
data-subgroup="{{subgroup.name}}"
data-title="{{subgroup.title}}"
>{{subgroup.title}} <span class="badge badge-secondary">Subgroup</span></h2>
<ul class="nav nav-tabs">
{{ render_navitem("Mentors", "/mentors", stats.mentors, page=="mentors")}}
{{ 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")}}
{% endif %}
</ul>
<div class="my-5">
{% if page == "info" %}
{{ render_info() }}
{% elif page == "mentors" %}
{{ render_mentors() }}
{% elif page == "students" %}
{{ render_students() }}
{% elif page == "join-requests" %}
{{ render_join_requests() }}
{% elif page == "admin" %}
{{ render_admin() }}
{% else %}
{{ render_page(page) }}
{% endif %}
</div>
{% endblock %}
{% macro render_admin() %}
<div style="background: white; padding: 20px;">
<h5>Add a new mentor</h5>
<form id="add-mentor-form">
<div class="form-group">
<input type="email" class="form-control" id="mentor-email" aria-describedby="emailHelp" placeholder="E-mail address">
</div>
<button type="button" class="btn btn-primary" id="add-mentor">Add Mentor</button>
</form>
</div>
{% endmacro %}
{% macro render_mentors() %}
<h5>Mentors</h5>
{% set mentors = subgroup.get_mentors() %}
{% if mentors %}
<div class="mentors-section">
{% for m in mentors %}
{{ widgets.MemberCard(member=m, show_course_count=False, dimension_class="") }}
{% endfor %}
</div>
{% else %}
<em>None found.</em>
{% endif %}
{% endmacro %}
{% macro render_students() %}
{% set students = subgroup.get_students() %}
{% if students %}
<div class="mentors-section">
{% for student in students %}
{{ widgets.MemberCard(member=student, show_course_count=False, dimension_class="") }}
{% endfor %}
</div>
{% else %}
<em>None found.</em>
{% endif %}
{% endmacro %}
{% macro render_join_requests() %}
<h5>Invite Link</h5>
{% set link = subgroup.get_invite_link() %}
<p><a href="{{ link }}" id="invite-link">{{link}}</a>
<br>
<a class="btn btn-seconday btn-sm" id="copy-to-clipboard">Copy to Clipboard</a>
</p>
{% set join_requests = subgroup.get_join_requests() %}
<h5>Pending Requests</h5>
{% if join_requests %}
<table class="table">
<tr>
<th>#</th>
<th>When</th>
<th>Email</th>
<th>Actions</th>
</tr>
{% for r in join_requests %}
<tr>
<td>{{loop.index}}</td>
<td class="timestamp">{{r.creation}}</td>
<td>{{r.email}}</td>
<td class="actions"
data-name="{{r.name}}"
data-email="{{r.email}}">
<a class="action-approve" href="#">Approve</a> | <a class="action-reject" href="#">Reject</a></td>
</tr>
{% endfor %}
</table>
{% else %}
<p><em>There are no pending join requests.</em></p>
{% endif %}
{% set rejected_requests = subgroup.get_join_requests(status="Rejected") %}
<h5>Rejected Requests</h5>
{% if rejected_requests %}
<table class="table">
<tr>
<th>#</th>
<th>When</th>
<th>Email</th>
<th>Actions</th>
</tr>
{% for r in rejected_requests %}
<tr>
<td>{{loop.index}}</td>
<td class="timestamp">{{r.creation}}</td>
<td>{{r.email}}</td>
<td class="actions"
data-name="{{r.name}}"
data-email="{{r.email}}">
<a class="action-undo" href="#">Undo</a></td>
</tr>
{% endfor %}
</table>
{% else %}
<p><em>There are no rejected requests.</em></p>
{% endif %}
{% endmacro %}
{% macro render_navitem(title, link, count, active) %}
<li class="nav-item">
<a
class="nav-link {{ 'active' if active }}"
href="/courses/{{course.name}}/subgroups/{{cohort.slug}}/{{subgroup.slug}}{{link}}"
>{{title}}
{% if count != -1 %}
<span
class="badge {{'badge-primary' if active else 'badge-secondary'}}"
>{{count}}</span>
{% endif %}
</a>
</li>
{% endmacro %}
{% block script %}
<script type="text/javascript">
$(function() {
$("#copy-to-clipboard").click(function() {
var invite_link = $("#invite-link").text();
navigator.clipboard.writeText(invite_link)
.then(() => {
$("#copy-to-clipboard").text("Copied!");
setTimeout(
() => $("#copy-to-clipboard").text("Copy to Clipboard"),
500);
});
});
$(".timestamp"). each(function() {
var t = moment($(this).text());
var dt = t.from(moment.now());
$(this).text(dt);
});
$(".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, 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 <strong>${email}</strong> from joining this subgroup?`, function() {
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 <strong>${email}</strong>?`, function() {
run_action("school.lms.api.undo_reject_cohort_join_request", name, el, "undo-reject", "Reject Undone!");
});
});
function run_action(method, join_request, elem, classname, label) {
frappe.call(method, {
join_request: join_request,
})
.then(r => {
if (r.message.ok) {
$(elem)
.addClass(classname)
.find("td.actions").html(label);
}
else {
frappe.msgprint(r.message.error);
}
});
}
$("#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);
}
});
});
});
</script>
{% endblock %}
{% block style %}
<style type="text/css">
tr.approved {
background:#c3e6cb;
color: #155724;
}
tr.rejected {
background: #f8d7da;
color: #721c24;
}
tr.undo-reject {
background:#d6d8d9;
}
</style>
{% endblock %}

View File

@@ -0,0 +1,67 @@
import frappe
from . import utils
def get_context(context):
context.no_cache = 1
course = utils.get_course()
cohort = utils.get_cohort(course, frappe.form_dict['cohort'])
subgroup = utils.get_subgroup(cohort, frappe.form_dict['subgroup'])
if not subgroup:
context.template = "www/404.html"
return
page = frappe.form_dict.get("page")
is_mentor = subgroup.is_mentor(frappe.session.user)
is_admin = cohort.is_admin(frappe.session.user) or "System Manager" in frappe.get_roles()
if is_admin:
role = "Admin"
elif is_mentor:
role = "Mentor"
else:
role = "Public"
pages = [
("mentors", ["Admin", "Mentor", "Public"]),
("students", ["Admin", "Mentor", "Public"]),
("join-requests", ["Admin", "Mentor"]),
("admin", ["Admin"])
]
pages += [(p.slug, ["Admin", "Mentor"]) for p in cohort.get_pages(scope="Subgroup")]
page_names = [p for p, roles in pages if role in roles]
if page not in page_names:
frappe.local.flags.redirect_location = subgroup.get_url() + "/mentors"
raise frappe.Redirect
utils.add_nav(context, "All Courses", "/courses")
utils.add_nav(context, course.title, f"/courses/{course.name}")
utils.add_nav(context, "Cohorts", f"/courses/{course.name}/manage")
utils.add_nav(context, cohort.title, f"/courses/{course.name}/cohorts/{cohort.slug}")
context.course = course
context.cohort = cohort
context.subgroup = subgroup
context.stats = get_stats(subgroup)
context.page = page
context.is_admin = is_admin
context.is_mentor = is_mentor
context.page_scope = "Subgroup"
# Function to render to custom page given the slug
context.render_page = lambda page: frappe.render_template(
cohort.get_page_template(page, scope="Subgroup"),
context)
def get_stats(subgroup):
return {
"join_requests": len(subgroup.get_join_requests()),
"students": len(subgroup.get_students()),
"mentors": len(subgroup.get_mentors())
}
def has_page(cohort, page):
return cohort.get_page(page, scope="Subgroup")

View File

@@ -0,0 +1,25 @@
import frappe
def get_course(course_name=None):
course_name = course_name or frappe.form_dict["course"]
return course_name and get_doc("LMS Course", course_name)
def get_doc(doctype, name):
try:
return frappe.get_doc(doctype, name)
except frappe.exceptions.DoesNotExistError:
return
def get_cohort(course, cohort_slug):
name = frappe.get_value("Cohort", {"course": course.name, "slug": cohort_slug})
return name and frappe.get_doc("Cohort", name)
def get_subgroup(cohort, subgroup_slug):
name = frappe.get_value("Cohort Subgroup", {"cohort": cohort.name, "slug": subgroup_slug})
return name and frappe.get_doc("Cohort Subgroup", name)
def add_nav(context, title, href):
"""Adds a breadcrumb to the navigation.
"""
nav = context.setdefault("nav", [])
nav.append({"title": title, "href": href})

View File

@@ -70,6 +70,14 @@
<img class="ml-2" src="/assets/school/images/play.png" />
</div>
{% endif %}
{% if course.is_cohort_staff(frappe.session.user) %}
<a class="button wide-button is-secondary"
href="/courses/{{course.name}}/manage"
style="color: inherit;">
Manage the course
</a>
{% endif %}
</div>
</div>
</div>