@@ -142,6 +142,10 @@ website_route_rules = [
|
||||
{"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>/subgroups/<cohort>/<subgroup>", "to_route": "cohorts/subgroup", "defaults": {"page": "info"}},
|
||||
{"from_route": "/courses/<course>/subgroups/<cohort>/<subgroup>/students", "to_route": "cohorts/subgroup", "defaults": {"page": "students"}},
|
||||
{"from_route": "/courses/<course>/subgroups/<cohort>/<subgroup>/join-requests", "to_route": "cohorts/subgroup", "defaults": {"page": "join-requests"}},
|
||||
{"from_route": "/users", "to_route": "profiles/profile"}
|
||||
]
|
||||
|
||||
|
||||
@@ -5,11 +5,52 @@ import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class Cohort(Document):
|
||||
def get_subgroups(self):
|
||||
def get_subgroups(self, include_counts=False):
|
||||
names = frappe.get_all("Cohort Subgroup", filters={"cohort": self.name}, pluck="name")
|
||||
return [frappe.get_doc("Cohort Subgroup", name) for name in names]
|
||||
subgroups = [frappe.get_doc("Cohort Subgroup", name) for name in names]
|
||||
subgroups = sorted(subgroups, key=lambda sg: sg.title)
|
||||
|
||||
if include_counts:
|
||||
mentors = self._get_subgroup_counts("Cohort Mentor")
|
||||
students = self._get_subgroup_counts("LMS Batch Membership")
|
||||
join_requests = self._get_subgroup_counts("Cohort Join Request")
|
||||
for s in subgroups:
|
||||
s.num_mentors = mentors.get(s.name, 0)
|
||||
s.num_students = students.get(s.name, 0)
|
||||
s.num_join_requests = join_requests.get(s.name, 0)
|
||||
return subgroups
|
||||
|
||||
def _get_subgroup_counts(self, doctype):
|
||||
q = f"""
|
||||
SELECT subgroup, count(*) as count
|
||||
FROM `tab{doctype}`
|
||||
WHERE cohort = %(cohort)s"""
|
||||
rows = frappe.db.sql(q, values={"cohort": self.name})
|
||||
return {subgroup: count for subgroup, count in rows}
|
||||
|
||||
def get_subgroup(self, slug):
|
||||
q = dict(cohort=self.name, slug=slug)
|
||||
name = frappe.db.get_value("Cohort Subgroup", q, "name")
|
||||
return name and frappe.get_doc("Cohort Subgroup", name)
|
||||
|
||||
def get_mentor(self, email):
|
||||
q = dict(cohort=self.name, email=email)
|
||||
name = frappe.db.get_value("Cohort Mentor", q, "name")
|
||||
return name and frappe.get_doc("Cohort Mentor", name)
|
||||
|
||||
def is_mentor(self, email):
|
||||
q = {
|
||||
"doctype": "Cohort Mentor",
|
||||
"cohort": self.name,
|
||||
"email": email
|
||||
}
|
||||
return frappe.db.exists(q)
|
||||
|
||||
def is_admin(self, email):
|
||||
q = {
|
||||
"doctype": "Cohort Staff",
|
||||
"cohort": self.name,
|
||||
"email": email,
|
||||
"role": "Admin"
|
||||
}
|
||||
return frappe.db.exists(q)
|
||||
|
||||
@@ -8,9 +8,11 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"cohort",
|
||||
"slug",
|
||||
"title",
|
||||
"description",
|
||||
"invite_code"
|
||||
"invite_code",
|
||||
"course"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -20,7 +22,8 @@
|
||||
"in_preview": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Cohort",
|
||||
"options": "Cohort"
|
||||
"options": "Cohort",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "title",
|
||||
@@ -28,7 +31,8 @@
|
||||
"in_list_view": 1,
|
||||
"in_preview": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "title"
|
||||
"label": "title",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
@@ -40,6 +44,20 @@
|
||||
"fieldtype": "Data",
|
||||
"label": "invite_code",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "slug",
|
||||
"fieldtype": "Data",
|
||||
"label": "Slug",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "cohort.course",
|
||||
"fieldname": "course",
|
||||
"fieldtype": "Link",
|
||||
"label": "Course",
|
||||
"options": "LMS Course",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
@@ -48,9 +66,14 @@
|
||||
"group": "Links",
|
||||
"link_doctype": "Cohort Student",
|
||||
"link_fieldname": "subgroup"
|
||||
},
|
||||
{
|
||||
"group": "Links",
|
||||
"link_doctype": "Cohort Join Request",
|
||||
"link_fieldname": "subgroup"
|
||||
}
|
||||
],
|
||||
"modified": "2021-11-29 16:57:51.660847",
|
||||
"modified": "2021-11-30 07:41:54.893270",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Cohort Subgroup",
|
||||
|
||||
@@ -11,7 +11,8 @@ class CohortSubgroup(Document):
|
||||
self.invite_code = random_string(8)
|
||||
|
||||
def get_invite_link(self):
|
||||
return f"{frappe.utils.get_url()}/cohorts/{self.cohort}/join/{self.slug}/{self.invite_code}"
|
||||
cohort = frappe.get_doc("Cohort", self.cohort)
|
||||
return f"{frappe.utils.get_url()}/courses/{self.course}/join/{cohort.slug}/{self.slug}/{self.invite_code}"
|
||||
|
||||
def has_student(self, email):
|
||||
"""Check if given user is a student of this subgroup.
|
||||
@@ -40,6 +41,21 @@ class CohortSubgroup(Document):
|
||||
}
|
||||
return frappe.get_all("Cohort Join Request", filters=q, fields=["*"], order_by="creation")
|
||||
|
||||
def get_mentors(self):
|
||||
emails = frappe.get_all("Cohort Mentor", filters={"subgroup": self.name}, fields=["email"], pluck='email')
|
||||
return [frappe.get_doc("User", email) for email in emails]
|
||||
|
||||
def get_students(self):
|
||||
emails = frappe.get_all("LMS Batch Membership", filters={"subgroup": self.name}, fields=["member"], pluck='member')
|
||||
return [frappe.get_doc("User", email) for email in emails]
|
||||
|
||||
def is_mentor(self, email):
|
||||
q = {
|
||||
"doctype": "Cohort Mentor",
|
||||
"subgroup": self.name,
|
||||
"email": email
|
||||
}
|
||||
return frappe.db.exists(q)
|
||||
|
||||
#def after_doctype_insert():
|
||||
# frappe.db.add_unique("Cohort Subgroup", ("cohort", "slug"))
|
||||
|
||||
25
school/www/cohorts/cohort.html
Normal file
25
school/www/cohorts/cohort.html
Normal file
@@ -0,0 +1,25 @@
|
||||
{% 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>
|
||||
|
||||
<h5>Subgroups</h5>
|
||||
<ul class="list-group">
|
||||
{% for sg in cohort.get_subgroups(include_counts=True) %}
|
||||
<li class="list-group-item">
|
||||
<div>
|
||||
<a class="subgroup-title" 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>
|
||||
{% endblock %}
|
||||
|
||||
34
school/www/cohorts/cohort.py
Normal file
34
school/www/cohorts/cohort.py
Normal file
@@ -0,0 +1,34 @@
|
||||
import frappe
|
||||
from . import utils
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
if frappe.session.user == "Guest":
|
||||
frappe.local.flags.redirect_location = "/login?redirect-to=" + frappe.request.path
|
||||
raise frappe.Redirect()
|
||||
|
||||
course = utils.get_course()
|
||||
cohort = course and get_cohort(course, frappe.form_dict["cohort"])
|
||||
|
||||
if not cohort:
|
||||
context.template = "www/404.html"
|
||||
return
|
||||
|
||||
utils.add_nav(context, "All Courses", "/courses")
|
||||
utils.add_nav(context, course.title, "/courses/" + course.name)
|
||||
utils.add_nav(context, "Cohorts", "/courses/" + course.name + "/cohorts")
|
||||
|
||||
context.course = course
|
||||
context.cohort = cohort
|
||||
|
||||
def get_cohort(course, cohort_slug):
|
||||
cohort = utils.get_cohort(course, cohort_slug)
|
||||
|
||||
if cohort.is_mentor(frappe.session.user):
|
||||
mentor = cohort.get_mentor(frappe.session.user)
|
||||
sg = frappe.get_doc("Cohort Subgroup", mentor.subgroup)
|
||||
frappe.local.flags.redirect_location = f"/courses/{course.name}/subgroups/{cohort.slug}/{sg.slug}"
|
||||
raise frappe.Redirect
|
||||
elif cohort.is_admin(frappe.session.user) or "System Manager" in frappe.get_roles():
|
||||
return cohort
|
||||
|
||||
136
school/www/cohorts/subgroup.html
Normal file
136
school/www/cohorts/subgroup.html
Normal file
@@ -0,0 +1,136 @@
|
||||
{% extends "www/cohorts/base.html" %}
|
||||
{% block title %} Subgroup {{subgroup.title}} - {{ course.title }} {% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
<h2>{{subgroup.title}} <span class="badge badge-secondary">Subgroup</span></h2>
|
||||
|
||||
<ul class="nav nav-tabs">
|
||||
{{ render_navitem("Info", "", -1, page=="info")}}
|
||||
{{ render_navitem("Students", "/students", stats.students, page=="students")}}
|
||||
{{ render_navitem("Requests to join", "/join-requests", stats.join_requests, page=="join-requests")}}
|
||||
</ul>
|
||||
<div class="my-5">
|
||||
{% if page == "info" %}
|
||||
{{ render_info() }}
|
||||
{% elif page == "students" %}
|
||||
{{ render_students() }}
|
||||
{% elif page == "join-requests" %}
|
||||
{{ render_join_requests() }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% macro render_info() %}
|
||||
<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>
|
||||
|
||||
<h5>Mentors</h5>
|
||||
{% set mentors = subgroup.get_mentors() %}
|
||||
{% if mentors %}
|
||||
{% for m in mentors %}
|
||||
<div class="my-5">
|
||||
{{ widgets.Avatar(member=m, avatar_class="avatar-small") }}
|
||||
<a href="/{{m.username}}" title="{{m.full_name}}">{{ m.full_name }}</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<em>None found.</em>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro render_students() %}
|
||||
{% set students = subgroup.get_students() %}
|
||||
{% if students %}
|
||||
{% for student in students %}
|
||||
<div class="my-5">
|
||||
{{ widgets.Avatar(member=student, avatar_class="avatar-small") }}
|
||||
<a href="/{{student.username}}" title="{{student.full_name}}">{{ student.full_name }}</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<em>None found.</em>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro render_join_requests() %}
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>When</th>
|
||||
<th>Email</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
{% for r in subgroup.get_join_requests() %}
|
||||
<tr>
|
||||
<td>{{loop.index}}</td>
|
||||
<td>{{r.creation}}</td>
|
||||
<td>{{r.email}}</td>
|
||||
<td
|
||||
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>
|
||||
{% 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);
|
||||
});
|
||||
});
|
||||
|
||||
$(".action-approve").click(function() {
|
||||
var name = $(this).parent().data("name");
|
||||
var email = $(this).parent().data("email");
|
||||
|
||||
frappe.confirm(
|
||||
`Are you sure to accept ${email} to this subgroup?`,
|
||||
function() {
|
||||
console.log("approve", name);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$(".action-reject").click(function() {
|
||||
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() {
|
||||
console.log("reject", name);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
33
school/www/cohorts/subgroup.py
Normal file
33
school/www/cohorts/subgroup.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import frappe
|
||||
from . import utils
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
course = utils.get_course()
|
||||
if frappe.session.user == "Guest":
|
||||
frappe.local.flags.redirect_location = "/login?redirect-to=" + frappe.request.path
|
||||
raise frappe.Redirect()
|
||||
|
||||
cohort = utils.get_cohort(course, frappe.form_dict['cohort'])
|
||||
subgroup = utils.get_subgroup(cohort, frappe.form_dict['subgroup'])
|
||||
|
||||
if not subgroup:
|
||||
context.template = "www/404.html"
|
||||
return
|
||||
|
||||
utils.add_nav(context, "All Courses", "/courses")
|
||||
utils.add_nav(context, course.title, f"/courses/{course.name}")
|
||||
utils.add_nav(context, "Cohorts", f"/courses/{course.name}/cohorts")
|
||||
utils.add_nav(context, cohort.title, f"/courses/{course.name}/cohorts/{cohort.slug}")
|
||||
|
||||
context.course = course
|
||||
context.cohort = cohort
|
||||
context.subgroup = subgroup
|
||||
context.stats = get_stats(subgroup)
|
||||
context.page = frappe.form_dict["page"]
|
||||
|
||||
def get_stats(subgroup):
|
||||
return {
|
||||
"join_requests": len(subgroup.get_join_requests()),
|
||||
"students": len(subgroup.get_students())
|
||||
}
|
||||
@@ -10,6 +10,14 @@ def get_doc(doctype, name):
|
||||
except frappe.exceptions.DoesNotExistError:
|
||||
return
|
||||
|
||||
def get_cohort(course, cohort_slug):
|
||||
name = frappe.get_value("Cohort", {"course": course.name, "slug": cohort_slug})
|
||||
return name and frappe.get_doc("Cohort", name)
|
||||
|
||||
def get_subgroup(cohort, subgroup_slug):
|
||||
name = frappe.get_value("Cohort Subgroup", {"cohort": cohort.name, "slug": subgroup_slug})
|
||||
return name and frappe.get_doc("Cohort Subgroup", name)
|
||||
|
||||
def add_nav(context, title, href):
|
||||
"""Adds a breadcrumb to the navigation.
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user