Merge pull request #272 from anandology/cohorts-v1
This commit is contained in:
@@ -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"}
|
||||
]
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
0
school/lms/doctype/cohort/__init__.py
Normal file
0
school/lms/doctype/cohort/__init__.py
Normal file
8
school/lms/doctype/cohort/cohort.js
Normal file
8
school/lms/doctype/cohort/cohort.js
Normal 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) {
|
||||
|
||||
// }
|
||||
});
|
||||
131
school/lms/doctype/cohort/cohort.json
Normal file
131
school/lms/doctype/cohort/cohort.json
Normal 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
|
||||
}
|
||||
85
school/lms/doctype/cohort/cohort.py
Normal file
85
school/lms/doctype/cohort/cohort.py
Normal 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)
|
||||
8
school/lms/doctype/cohort/test_cohort.py
Normal file
8
school/lms/doctype/cohort/test_cohort.py
Normal file
@@ -0,0 +1,8 @@
|
||||
# Copyright (c) 2021, FOSS United and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestCohort(unittest.TestCase):
|
||||
pass
|
||||
0
school/lms/doctype/cohort_join_request/__init__.py
Normal file
0
school/lms/doctype/cohort_join_request/__init__.py
Normal 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) {
|
||||
|
||||
// }
|
||||
});
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
@@ -0,0 +1,8 @@
|
||||
# Copyright (c) 2021, FOSS United and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestCohortJoinRequest(unittest.TestCase):
|
||||
pass
|
||||
0
school/lms/doctype/cohort_mentor/__init__.py
Normal file
0
school/lms/doctype/cohort_mentor/__init__.py
Normal file
8
school/lms/doctype/cohort_mentor/cohort_mentor.js
Normal file
8
school/lms/doctype/cohort_mentor/cohort_mentor.js
Normal 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) {
|
||||
|
||||
// }
|
||||
});
|
||||
73
school/lms/doctype/cohort_mentor/cohort_mentor.json
Normal file
73
school/lms/doctype/cohort_mentor/cohort_mentor.json
Normal 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
|
||||
}
|
||||
12
school/lms/doctype/cohort_mentor/cohort_mentor.py
Normal file
12
school/lms/doctype/cohort_mentor/cohort_mentor.py
Normal 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)
|
||||
8
school/lms/doctype/cohort_mentor/test_cohort_mentor.py
Normal file
8
school/lms/doctype/cohort_mentor/test_cohort_mentor.py
Normal file
@@ -0,0 +1,8 @@
|
||||
# Copyright (c) 2021, FOSS United and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestCohortMentor(unittest.TestCase):
|
||||
pass
|
||||
0
school/lms/doctype/cohort_staff/__init__.py
Normal file
0
school/lms/doctype/cohort_staff/__init__.py
Normal file
8
school/lms/doctype/cohort_staff/cohort_staff.js
Normal file
8
school/lms/doctype/cohort_staff/cohort_staff.js
Normal 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) {
|
||||
|
||||
// }
|
||||
});
|
||||
77
school/lms/doctype/cohort_staff/cohort_staff.json
Normal file
77
school/lms/doctype/cohort_staff/cohort_staff.json
Normal 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
|
||||
}
|
||||
8
school/lms/doctype/cohort_staff/cohort_staff.py
Normal file
8
school/lms/doctype/cohort_staff/cohort_staff.py
Normal 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
|
||||
8
school/lms/doctype/cohort_staff/test_cohort_staff.py
Normal file
8
school/lms/doctype/cohort_staff/test_cohort_staff.py
Normal file
@@ -0,0 +1,8 @@
|
||||
# Copyright (c) 2021, FOSS United and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestCohortStaff(unittest.TestCase):
|
||||
pass
|
||||
0
school/lms/doctype/cohort_subgroup/__init__.py
Normal file
0
school/lms/doctype/cohort_subgroup/__init__.py
Normal file
8
school/lms/doctype/cohort_subgroup/cohort_subgroup.js
Normal file
8
school/lms/doctype/cohort_subgroup/cohort_subgroup.js
Normal 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) {
|
||||
|
||||
// }
|
||||
});
|
||||
104
school/lms/doctype/cohort_subgroup/cohort_subgroup.json
Normal file
104
school/lms/doctype/cohort_subgroup/cohort_subgroup.json
Normal 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
|
||||
}
|
||||
95
school/lms/doctype/cohort_subgroup/cohort_subgroup.py
Normal file
95
school/lms/doctype/cohort_subgroup/cohort_subgroup.py
Normal 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"))
|
||||
@@ -0,0 +1,8 @@
|
||||
# Copyright (c) 2021, FOSS United and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestCohortSubgroup(unittest.TestCase):
|
||||
pass
|
||||
0
school/lms/doctype/cohort_web_page/__init__.py
Normal file
0
school/lms/doctype/cohort_web_page/__init__.py
Normal file
64
school/lms/doctype/cohort_web_page/cohort_web_page.json
Normal file
64
school/lms/doctype/cohort_web_page/cohort_web_page.json
Normal 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"
|
||||
}
|
||||
9
school/lms/doctype/cohort_web_page/cohort_web_page.py
Normal file
9
school/lms/doctype/cohort_web_page/cohort_web_page.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
// }
|
||||
});
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,8 @@
|
||||
# Copyright (c) 2021, Frappe and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestExerciseLatestSubmission(unittest.TestCase):
|
||||
pass
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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() %}
|
||||
|
||||
0
school/www/cohorts/__init__.py
Normal file
0
school/www/cohorts/__init__.py
Normal file
34
school/www/cohorts/base.html
Normal file
34
school/www/cohorts/base.html
Normal 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 %}
|
||||
|
||||
|
||||
77
school/www/cohorts/cohort.html
Normal file
77
school/www/cohorts/cohort.html
Normal 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 →</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 %}
|
||||
32
school/www/cohorts/cohort.py
Normal file
32
school/www/cohorts/cohort.py
Normal 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)
|
||||
38
school/www/cohorts/index.html
Normal file
38
school/www/cohorts/index.html
Normal 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 %}
|
||||
31
school/www/cohorts/index.py
Normal file
31
school/www/cohorts/index.py
Normal 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]
|
||||
88
school/www/cohorts/join.html
Normal file
88
school/www/cohorts/join.html
Normal 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 →</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 %}
|
||||
24
school/www/cohorts/join.py
Normal file
24
school/www/cohorts/join.py
Normal 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
|
||||
264
school/www/cohorts/subgroup.html
Normal file
264
school/www/cohorts/subgroup.html
Normal 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 %}
|
||||
67
school/www/cohorts/subgroup.py
Normal file
67
school/www/cohorts/subgroup.py
Normal 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")
|
||||
25
school/www/cohorts/utils.py
Normal file
25
school/www/cohorts/utils.py
Normal 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})
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user