fix: conflicts

This commit is contained in:
pateljannat
2021-05-07 12:08:51 +05:30
34 changed files with 514 additions and 319 deletions

View File

@@ -8,6 +8,8 @@ import re
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
import random import random
from frappe.utils import cint
import hashlib
class CommunityMember(Document): class CommunityMember(Document):
@@ -45,6 +47,24 @@ class CommunityMember(Document):
'member_type': 'Mentor' 'member_type': 'Mentor'
}) })
def get_palette(self):
palette = [
['--orange-avatar-bg', '--orange-avatar-color'],
['--pink-avatar-bg', '--pink-avatar-color'],
['--blue-avatar-bg', '--blue-avatar-color'],
['--green-avatar-bg', '--green-avatar-color'],
['--dark-green-avatar-bg', '--dark-green-avatar-color'],
['--red-avatar-bg', '--red-avatar-color'],
['--yellow-avatar-bg', '--yellow-avatar-color'],
['--purple-avatar-bg', '--purple-avatar-color'],
['--gray-avatar-bg', '--gray-avatar-color0']
]
encoded_name = str(self.full_name).encode("utf-8")
hash_name = hashlib.md5(encoded_name).hexdigest()
idx = cint((int(hash_name[4:6], 16) + 1) / 5.33)
return palette[idx % 8]
def __repr__(self): def __repr__(self):
return f"<CommunityMember: {self.email}>" return f"<CommunityMember: {self.email}>"

View File

@@ -0,0 +1,14 @@
{% set color = member.get_palette() %}
<a href="/{{member.username}}">
<span class="avatar {{ avatar_class }}" title="{{ member.full_name }}">
{% if member.photo %}
<img class="avatar-frame standard-image" src="{{ member.photo }}" title="{{ member.full_name }}">
</img>
{% else %}
<span class="avatar-frame standard-image" title="{{ member.full_name }}"
style="background-color: var({{color[0]}}); color: var({{color[1]}});">
{{ member.abbr }}
</span>
{% endif %}
</a>
</span>

View File

@@ -1,6 +1,5 @@
{ {
"actions": [], "actions": [],
"autoname": "field:title",
"creation": "2021-03-18 19:37:34.614796", "creation": "2021-03-18 19:37:34.614796",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
@@ -51,10 +50,12 @@
"label": "Description" "label": "Description"
}, },
{ {
"default": "Public",
"fieldname": "visibility", "fieldname": "visibility",
"fieldtype": "Select", "fieldtype": "Select",
"in_list_view": 1,
"label": "Visibility", "label": "Visibility",
"options": "\nPublic\nUnlisted\nPrivate" "options": "Public\nUnlisted\nPrivate"
}, },
{ {
"fieldname": "membership", "fieldname": "membership",
@@ -63,16 +64,19 @@
"options": "\nOpen\nRestricted\nInvite Only\nClosed" "options": "\nOpen\nRestricted\nInvite Only\nClosed"
}, },
{ {
"default": "Active",
"fieldname": "status", "fieldname": "status",
"fieldtype": "Select", "fieldtype": "Select",
"in_list_view": 1,
"label": "Status", "label": "Status",
"options": "\nActive\nInactive" "options": "Active\nInactive"
}, },
{ {
"default": "Ready",
"fieldname": "stage", "fieldname": "stage",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Stage", "label": "Stage",
"options": "\nReady\nIn Progress\nCompleted\nCancelled" "options": "Ready\nIn Progress\nCompleted\nCancelled"
}, },
{ {
"fieldname": "column_break_3", "fieldname": "column_break_3",
@@ -122,7 +126,7 @@
"link_fieldname": "batch" "link_fieldname": "batch"
} }
], ],
"modified": "2021-04-30 09:52:18.941276", "modified": "2021-05-06 05:46:38.469120",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Batch", "name": "LMS Batch",

View File

@@ -8,6 +8,7 @@ from frappe.model.document import Document
from community.www.courses.utils import get_member_with_email from community.www.courses.utils import get_member_with_email
from frappe import _ from frappe import _
from community.lms.doctype.lms_batch_membership.lms_batch_membership import create_membership from community.lms.doctype.lms_batch_membership.lms_batch_membership import create_membership
from community.query import find, find_all
class LMSBatch(Document): class LMSBatch(Document):
def validate(self): def validate(self):
@@ -29,15 +30,20 @@ class LMSBatch(Document):
self.code = short_code + str(len(course_batches) + 1) self.code = short_code + str(len(course_batches) + 1)
def get_mentors(self): def get_mentors(self):
mentors = []
memberships = frappe.get_all( memberships = frappe.get_all(
"LMS Batch Membership", "LMS Batch Membership",
{"batch": self.name, "member_type": "Mentor"}, {"batch": self.name, "member_type": "Mentor"},
["member"]) ["member"])
for membership in memberships: member_names = [m['member'] for m in memberships]
member = frappe.db.get_value("Community Member", membership.member, ["full_name", "photo", "abbr"], as_dict=1) return find_all("Community Member", name=["IN", member_names])
mentors.append(member)
return mentors def is_member(self, email):
"""Checks if a person is part of a batch.
"""
member = find("Community Member", email=email)
return member and frappe.db.exists(
"LMS Batch Membership",
{"batch": self.name, "member": member.name})
@frappe.whitelist() @frappe.whitelist()
def get_messages(batch): def get_messages(batch):

View File

@@ -2,14 +2,12 @@
"actions": [], "actions": [],
"allow_guest_to_view": 1, "allow_guest_to_view": 1,
"allow_rename": 1, "allow_rename": 1,
"autoname": "field:title",
"creation": "2021-03-01 16:49:33.622422", "creation": "2021-03-01 16:49:33.622422",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"title", "title",
"slug",
"is_published", "is_published",
"column_break_3", "column_break_3",
"short_code", "short_code",
@@ -43,14 +41,6 @@
"fieldtype": "Data", "fieldtype": "Data",
"label": "Short Code" "label": "Short Code"
}, },
{
"description": "The slug of the course. Autogenerated from the title if not specified.",
"fieldname": "slug",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Slug",
"unique": 1
},
{ {
"fieldname": "column_break_3", "fieldname": "column_break_3",
"fieldtype": "Column Break" "fieldtype": "Column Break"
@@ -87,9 +77,14 @@
"group": "Mentors", "group": "Mentors",
"link_doctype": "LMS Course Mentor Mapping", "link_doctype": "LMS Course Mentor Mapping",
"link_fieldname": "course" "link_fieldname": "course"
},
{
"group": "Mentors",
"link_doctype": "LMS Mentor Request",
"link_fieldname": "course"
} }
], ],
"modified": "2021-05-03 05:52:30.396824", "modified": "2021-05-06 13:37:03.318829",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Course", "name": "LMS Course",
@@ -108,7 +103,7 @@
"write": 1 "write": 1
} }
], ],
"search_fields": "slug", "search_fields": "title",
"sort_field": "creation", "sort_field": "creation",
"sort_order": "DESC", "sort_order": "DESC",
"title_field": "title", "title_field": "title",

View File

@@ -6,13 +6,18 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
from ...utils import slugify from ...utils import slugify
from community.query import find, find_all
class LMSCourse(Document): class LMSCourse(Document):
@staticmethod @staticmethod
def find(slug): def find(name):
"""Returns the course with specified slug. """Returns the course with specified name.
""" """
return find("LMS Course", is_published=True, slug=slug) return find("LMS Course", is_published=True, name=name)
def autoname(self):
if not self.name:
self.name = self.generate_slug(title=self.title)
@staticmethod @staticmethod
def find_all(): def find_all():
@@ -20,19 +25,15 @@ class LMSCourse(Document):
""" """
return find_all("LMS Course", is_published=True) return find_all("LMS Course", is_published=True)
def before_save(self):
if not self.slug:
self.slug = self.generate_slug(title=self.title)
def generate_slug(self, title): def generate_slug(self, title):
result = frappe.get_all( result = frappe.get_all(
'LMS Course', 'LMS Course',
fields=['slug']) fields=['name'])
slugs = set([row['slug'] for row in result]) slugs = set([row['name'] for row in result])
return slugify(title, used_slugs=slugs) return slugify(title, used_slugs=slugs)
def __repr__(self): def __repr__(self):
return f"<Course#{self.name} {self.slug}>" return f"<Course#{self.name}>"
def get_topic(self, slug): def get_topic(self, slug):
"""Returns the topic with given slug in this course as a Document. """Returns the topic with given slug in this course as a Document.
@@ -123,6 +124,9 @@ class LMSCourse(Document):
# TODO: chapters should have a way to specify the order # TODO: chapters should have a way to specify the order
return find_all("Chapter", course=self.name, order_by="creation") return find_all("Chapter", course=self.name, order_by="creation")
def get_batch(self, batch_name):
return find("LMS Batch", name=batch_name, course=self.name)
def get_batches(self, mentor=None): def get_batches(self, mentor=None):
batches = find_all("LMS Batch", course=self.name) batches = find_all("LMS Batch", course=self.name)
if mentor: if mentor:
@@ -139,24 +143,8 @@ class LMSCourse(Document):
now = frappe.utils.nowdate() now = frappe.utils.nowdate()
batches = find_all("LMS Batch", batches = find_all("LMS Batch",
course=self.name, course=self.name,
start_date=[">", now]) start_date=[">", now],
status="Active",
visibility="Public")
return batches return batches
def find_all(doctype, order_by=None, **filters):
"""Queries the database for documents of a doctype matching given filters.
"""
rows = frappe.db.get_all(doctype,
filters=filters,
fields='*',
order_by=order_by)
return [frappe.get_doc(dict(row, doctype=doctype)) for row in rows]
def find(doctype, **filters):
"""Queries the database for a document of given doctype matching given filters.
"""
rows = frappe.db.get_all(doctype,
filters=filters,
fields='*')
if rows:
row = rows[0]
return frappe.get_doc(dict(row, doctype=doctype))

View File

@@ -24,7 +24,7 @@ class TestLMSCourse(unittest.TestCase):
def test_new_course(self): def test_new_course(self):
course = self.new_course("Test Course") course = self.new_course("Test Course")
assert course.title == "Test Course" assert course.title == "Test Course"
assert course.slug == "test-course" assert course.name == "test-course"
assert course.get_mentors() == [] assert course.get_mentors() == []
def test_find_all(self): def test_find_all(self):
@@ -41,7 +41,7 @@ class TestLMSCourse(unittest.TestCase):
# now we should find one course # now we should find one course
courses = LMSCourse.find_all() courses = LMSCourse.find_all()
assert [c.slug for c in courses] == [course.slug] assert [c.name for c in courses] == [course.name]
# disabled this test as it is failing # disabled this test as it is failing
def _test_add_mentors(self): def _test_add_mentors(self):

View File

@@ -8,86 +8,119 @@ from frappe.model.document import Document
from frappe import _ from frappe import _
class LMSMentorRequest(Document): class LMSMentorRequest(Document):
def on_update(self): def on_update(self):
if self.has_value_changed('status'): if self.has_value_changed('status'):
template = frappe.db.get_single_value('LMS Settings', 'mentor_request_status_update')
if not template:
return
email_template = frappe.get_doc('Email Template', template) if self.status == "Approved":
message = frappe.render_template(email_template.response, {'member_name': self.member_name, 'status': self.status}) self.create_course_mentor_mapping()
subject = _('The status of your application has changed.')
member_email = frappe.db.get_value("Community Member", self.member, "email") if self.status != "Pending":
self.send_status_change_email()
if self.status == 'Approved' or self.status == 'Rejected':
reviewed_by = frappe.db.get_value('Community Member', self.reviewed_by, 'email') def create_course_mentor_mapping(self):
send_email(member_email, [get_course_author(self.course), reviewed_by], subject, message) mapping = frappe.get_doc({
"doctype": "LMS Course Mentor Mapping",
elif self.status == 'Withdrawn': "mentor": self.member,
send_email([member_email, get_course_author(self.course)], None, subject, message) "course": self.course
})
mapping.save()
def send_creation_email(self, member):
email_template = self.get_email_template('mentor_request_creation')
if not email_template:
return
course_details = frappe.db.get_value("LMS Course", self.course, ["owner", "slug", "title"], as_dict=True)
message = frappe.render_template(email_template.response,
{
'member_name': member.full_name,
'course_url': '/courses/' + course_details.slug,
'course': course_details.title
})
email_args = {
"recipients": [frappe.session.user, course_details.owner],
"subject": email_template.subject,
"header": email_template.subject,
"message": message
}
frappe.enqueue(method=frappe.sendmail, queue="short", timeout=300, is_async=True, **email_args)
def send_status_change_email(self):
email_template = self.get_email_template('mentor_request_status_update')
if not email_template:
return
course_details = frappe.db.get_value("LMS Course", self.course, ["owner", "title"], as_dict=True)
message = frappe.render_template(email_template.response,
{
'member_name': self.member_name,
'status': self.status,
'course': course_details.title
})
member_email = frappe.db.get_value("Community Member", self.member, "email")
if self.status == 'Approved' or self.status == 'Rejected':
reviewed_by = frappe.db.get_value('Community Member', self.reviewed_by, 'email')
email_args = {
"recipients": member_email,
"cc": [course_details.owner, reviewed_by],
"subject": email_template.subject,
"header": email_template.subject,
"message": message
}
frappe.enqueue(method=frappe.sendmail, queue="short", timeout=300, is_async=True, **email_args)
elif self.status == 'Withdrawn':
email_args = {
"recipients": [member_email, course_details.owner],
"subject": email_template.subject,
"header": email_template.subject,
"message": message
}
frappe.enqueue(method=frappe.sendmail, queue="short", timeout=300, is_async=True, **email_args)
def get_email_template(self, template_name):
template = frappe.db.get_single_value('LMS Settings', template_name)
if template:
return frappe.get_doc('Email Template', template)
@frappe.whitelist() @frappe.whitelist()
def has_requested(course): def has_requested(course):
return len(frappe.get_all('LMS Mentor Request', return frappe.db.count('LMS Mentor Request',
filters = { filters = {
'member': get_member().name, 'member': get_member().name,
'course': course, 'course': course,
'status': ['in', ('Pending', 'Approved')] 'status': ['in', ('Pending', 'Approved')]
} }
)
) )
@frappe.whitelist() @frappe.whitelist()
def create_request(course): def create_request(course):
if not has_requested(course): if not has_requested(course):
member = get_member() member = get_member()
frappe.get_doc({ request = frappe.get_doc({
'doctype': 'LMS Mentor Request', 'doctype': 'LMS Mentor Request',
'member': member.name, 'member': member.name,
'course': course, 'course': course,
'status': 'Pending' 'status': 'Pending'
}).save(ignore_permissions=True) })
send_creation_email(course, member) request.save(ignore_permissions=True)
return 'OK' request.send_creation_email(member)
else: return 'OK'
return 'Already Applied'
else:
return 'Already Applied'
@frappe.whitelist() @frappe.whitelist()
def cancel_request(course): def cancel_request(course):
request = frappe.get_doc('LMS Mentor Request', {'member': get_member().name, 'course': course, 'status': ['in', ('Pending', 'Approved')]}) request = frappe.get_doc('LMS Mentor Request', {'member': get_member().name, 'course': course, 'status': ['in', ('Pending', 'Approved')]})
request.status = 'Withdrawn' request.status = 'Withdrawn'
request.save(ignore_permissions=True) request.save(ignore_permissions=True)
return 'OK' return 'OK'
def get_member(): def get_member():
try: try:
return frappe.get_doc('Community Member', {'email': frappe.session.user}) return frappe.get_doc('Community Member', {'email': frappe.session.user})
except frappe.DoesNotExistError: except frappe.DoesNotExistError:
return return
def get_course_author(course):
return frappe.db.get_value('LMS Course', course, 'owner')
def send_creation_email(course, member):
template = frappe.db.get_single_value('LMS Settings', 'mentor_request_creation')
if not template:
return
email_template = frappe.get_doc('Email Template', template)
member_name = member.full_name
message = frappe.render_template(email_template.response, {'member_name': member_name})
subject = _('Request for Mentorship')
send_email([frappe.session.user, get_course_author(course)], None, subject, message)
def send_email(recipients, cc=None, subject=None, message=None, template=None, args=None):
frappe.sendmail(
recipients = recipients,
cc = cc,
sender = frappe.db.get_single_value('LMS Settings', 'email_sender'),
subject = subject,
send_priority = 0,
queue_separately = True,
message = message,
template=template,
args=args
)

View File

@@ -1,6 +1,6 @@
<div class="chapter-teaser"> <div class="chapter-teaser">
<div class="teaser-body"> <div class="teaser-body">
<h3 class="chapter-title">{{ chapter.title }}</h3> <h3 class="chapter-title"><span class="chapter-number">{{index}}</span> {{ chapter.title }}</h3>
<div class="chapter-description"> <div class="chapter-description">
{{ chapter.description or "" }} {{ chapter.description or "" }}
</div> </div>

View File

@@ -1,13 +1,15 @@
<div class="course-teaser"> <div class="course-teaser">
<div class="course-body"> <div class="course-body">
<h3 class="course-title"><a href="/courses/{{ course.slug }}">{{ course.title }}</a></h3> <h3 class="course-title"><a href="/courses/{{ course.name }}">{{ course.title }}</a></h3>
<div class="course-intro"> <div class="course-intro">
{{ course.short_introduction or "" }} {{ course.short_introduction or "" }}
</div> </div>
</div> </div>
<div class="course-footer"> <div class="course-footer">
<div class="course-author"> <div class="course-author">
<img class="course-author-avatar" src="{{ course.get_instructor().avatar }}" />{{ course.get_instructor().full_name }} {% with author = course.get_instructor() %}
{{ widgets.Avatar(member=author, avatar_class="avatar-medium") }} <a href="/{{author.username}}">{{ author.full_name }}</a>
{% endwith %}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,6 +1,12 @@
<form id="invite-request-form"> <form id="invite-request-form">
<input class="form-control field-width mr-5" id="invite_email" type="email" placeholder="Email Address"> <div class="row">
<a type="submit" id="submit-invite-request" class="btn btn-primary btn-lg" href="#" role="button">Request Invite</a> <div class="col-md">
<input class="form-control w-100 mr-5 mb-5 mt-2" id="invite_email" type="email" placeholder="Email Address">
</div>
<div class="col-md">
<a type="submit" id="submit-invite-request" class="btn btn-primary btn-lg" href="#" role="button">Request Invite</a>
</div>
</div>
</form> </form>
<script> <script>
frappe.ready(() => { frappe.ready(() => {
@@ -16,19 +22,19 @@
if (data.message == "OK") { if (data.message == "OK") {
var message = `<div> var message = `<div>
<p class="lead">Thanks for your interest in Mon School. We have recorded your interest and we will get back to you shortly.</p> <p class="lead alert alert-secondary">Thanks for your interest in Mon School. We have recorded your interest and we will get back to you shortly.</p>
</div>`; </div>`;
} }
else if (data.message == "invite") { else if (data.message == "invite") {
var message = `<div> var message = `<div>
<p class="lead">Email ${invite_email} has already been used to request an invitation.</p> <p class="lead alert alert-secondary">Email ${invite_email} has already been used to request an invitation.</p>
</div>`; </div>`;
} }
else if (data.message == "user") { else if (data.message == "user") {
var message = `<div> var message = `<div>
<p class="lead">Looks like there is already an account with email ${invite_email}. Would you like to <a href="/login">login</a>.</p> <p class="lead alert alert-secondary">Looks like there is already an account with email ${invite_email}. Would you like to <a href="/login">login</a>?</p>
</div>`; </div>`;
} }

View File

@@ -9,7 +9,8 @@
<a href="sketches/{{sketch.sketch_id}}">{{sketch.title}}</a> <a href="sketches/{{sketch.sketch_id}}">{{sketch.title}}</a>
</div> </div>
<div class="sketch-author"> <div class="sketch-author">
by {{sketch.get_owner().full_name}} {% set owner = sketch.get_owner() %}
by <a href="/{{owner.username}}">{{owner.full_name}}</a>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,3 +1,4 @@
community.patches.set_email_preferences community.patches.set_email_preferences
community.patches.change_name_for_community_members community.patches.change_name_for_community_members
community.patches.save_abbr_for_community_members community.patches.save_abbr_for_community_members
community.patches.create_mentor_request_email_templates

View File

@@ -0,0 +1,31 @@
from __future__ import unicode_literals
import frappe, os
from frappe import _
def execute():
frappe.reload_doc("email", "doctype", "email_template")
base_path = frappe.get_app_path("community", "templates", "emails")
if not frappe.db.exists("Email Template", _('Mentor Request Creation Template')):
response = frappe.read_file(os.path.join(base_path, "mentor_request_creation_email.html"))
frappe.get_doc({
'doctype': 'Email Template',
'name': _("Mentor Request Creation Template"),
'response': response,
'subject': _('Request for Mentorship'),
'owner': frappe.session.user
}).insert(ignore_permissions=True)
frappe.db.set_value("LMS Settings", None, "mentor_request_creation", _('Mentor Request Creation Template'))
if not frappe.db.exists("Email Template", _('Mentor Request Status Update Template')):
response = frappe.read_file(os.path.join(base_path, "mentor_request_status_update_email.html"))
frappe.get_doc({
'doctype': 'Email Template',
'name': _("Mentor Request Status Update Template"),
'response': response,
'subject': _('The status of your application has changed.'),
'owner': frappe.session.user
}).insert(ignore_permissions=True)
frappe.db.set_value("LMS Settings", None, "mentor_request_status_update", _('Mentor Request Status Update Template'))

View File

@@ -3,8 +3,8 @@
"public/css/lms.css" "public/css/lms.css"
], ],
"css/community.css": [ "css/community.css": [
"public/css/vars.css",
"public/css/style.css", "public/css/style.css",
"public/css/vars.css",
"public/css/style.less" "public/css/style.less"
] ]
} }

View File

@@ -34,82 +34,8 @@ body {
margin: 0px; margin: 0px;
} }
.course-header {
margin-top: 20px;
padding: 20px;
background: var(--header-bg);
color: var(--header-color);
border-radius: 9px;
}
.course-author-avatar { /* .course-details {
width: 20px;
height: 20px;
border-radius: 50%;
margin-right: 20px;
}
.course-header h1 {
color: inherit;
}
.course-type {
text-transform: uppercase;
font-size: 1.0em;
color: var(--tag-color);
}
.sidebar {
background: var(--sidebar-bg);
margin: 20px 0px;
border-radius: 10px;
padding: 1px 20px 20px 20px;
color: var(--text-color);
}
.sidebar h3 {
margin-top: 20px;
color: var(--c2);
}
.sidebar-batch {
background: var(--sidebar-bg);
color: var(--text-color);
position: fixed;
left: 0;
height: 100%;
}
.sidebar-batch a {
padding: 16px 8px 8px 16px;
display: block;
}
.instructor {
padding: 10px;
}
.instructor-title {
font-weight: bold;
}
.instructor-subtitle {
font-size: 0.8em;
color: var(--text-color);
}
.sidebar .notice {
padding: 10px;
border-radius: 10px;
border: 1px dashed var(--text-color);
}
.sidebar .notice a {
color: inherit;
text-decoration: underline;
}
.course-details {
margin: 20px 0px; margin: 20px 0px;
} }
@@ -118,7 +44,7 @@ body {
font-size: 1.4em; font-size: 1.4em;
font-weight: bold; font-weight: bold;
margin: 20px 0px 10px 0px; margin: 20px 0px 10px 0px;
} } */
.chapter-plan { .chapter-plan {
border-radius: 10px; border-radius: 10px;
@@ -133,7 +59,7 @@ body {
font-weight: bold; font-weight: bold;
} }
.chapter-number { /* .chapter-number {
background: var(--text-color); background: var(--text-color);
color: white; color: white;
border-radius: 50%; border-radius: 50%;
@@ -146,7 +72,7 @@ body {
.chapter-description { .chapter-description {
margin: 20px 0px; margin: 20px 0px;
} } */
.lessons { .lessons {
padding-left: 20px; padding-left: 20px;
@@ -302,4 +228,4 @@ img.profile-photo {
color: #2D005A; color: #2D005A;
font-weight: 600; font-weight: 600;
line-height: 51px; line-height: 51px;
} }

View File

@@ -5,6 +5,11 @@ body {
font-family: "Inter", sans-serif; font-family: "Inter", sans-serif;
} }
h2 {
margin: 20px 0px;
color: black;
}
.teaser { .teaser {
background: white; background: white;
border-radius: 9px; border-radius: 9px;
@@ -122,3 +127,108 @@ section.lightgray {
padding: 2rem 0px; padding: 2rem 0px;
margin-top: 2rem; margin-top: 2rem;
} }
.course-type {
text-transform: uppercase;
font-size: 1.0em;
font-weight: bold;
color: var(--tag-color);
}
.course-header {
margin-top: 20px;
}
/*
.course-header {
margin-top: 20px;
padding: 20px;
background: var(--header-bg);
color: var(--header-color);
border-radius: 9px;
}
.course-author-avatar {
width: 20px;
height: 20px;
border-radius: 50%;
margin-right: 20px;
}
.course-header h1 {
color: inherit;
}
*/
// .gray-section {
// background:#F6F6F6;
// border: 1px solid #C4C4C4;
// padding: 20px;
// margin: 20px 0px;
// }
.instructor-title {
font-weight: bold;
color: black;
}
.instructor-subtitle {
font-size: 0.8em;
color: var(--text-color);
}
// .mentors-wrapper {
// .gray-section();
// }
.chapter-number {
background: var(--text-color);
color: white;
border-radius: 50%;
height: 24px;
min-width: 24px;
align-items: center;
padding: 5px 8px 2px 8px;
margin-right: 5px;
}
.sidebar {
background: var(--sidebar-bg);
border: 1px solid var(--sidebar-border);
margin: 20px 0px;
border-radius: 10px;
padding: 1px 20px 20px 20px;
}
.sidebar h3 {
margin-top: 20px;
color: black;
}
.sidebar-batch {
background: var(--sidebar-bg);
color: var(--text-color);
position: fixed;
left: 0;
height: 100%;
}
.sidebar-batch a {
padding: 16px 8px 8px 16px;
display: block;
}
.sidebar .notice {
margin-top: 10px;
padding: 10px;
border-radius: 10px;
border: 1px dashed var(--text-color);
}
.sidebar .notice a {
color: inherit;
text-decoration: underline;
}

View File

@@ -1,4 +1,7 @@
/* Define all your css variables here. */ /* Define all your css variables here. */
:root { :root {
--primary-color: #08B74F; --primary-color: #08B74F;
--tag-color: #737373;
--sidebar-bg: #F6F6F6;
--sidebar-border: #C4C4C4;
} }

22
community/query.py Normal file
View File

@@ -0,0 +1,22 @@
"""Utilities to find docs.
"""
import frappe
def find_all(doctype, order_by=None, **filters):
"""Queries the database for documents of a doctype matching given filters.
"""
rows = frappe.db.get_all(doctype,
filters=filters,
fields='*',
order_by=order_by)
return [frappe.get_doc(dict(row, doctype=doctype)) for row in rows]
def find(doctype, **filters):
"""Queries the database for a document of given doctype matching given filters.
"""
rows = frappe.db.get_all(doctype,
filters=filters,
fields='*')
if rows:
row = rows[0]
return frappe.get_doc(dict(row, doctype=doctype))

View File

@@ -0,0 +1,9 @@
<div>
<p>Dear {{ member_name }},</p>
<br>
<p>You've applied to become a mentor for this course. Your request is currently under review.</p>
<p>If you are not any more interested to mentor the course {{ course }}, you can <a href="{{ course_url }}">cancel your application</a>.</p>
<br>
<p>Thanks and Regards,</p>
<p>Team Community.</p>
</div>

View File

@@ -0,0 +1,8 @@
<div>
<p>Dear {{ member_name }},</p>
<br>
<p>Your request to join us as a mentor for the course {{ course }} has been {{ status }}.</p>
<br>
<p>Thanks and Regards,</p>
<p>Team Community.</p>
</div>

View File

@@ -14,7 +14,8 @@ from frappe.utils.jinja import get_jenv
# When {{widgets.SomeWidget()}} is called, it looks for # When {{widgets.SomeWidget()}} is called, it looks for
# widgets/SomeWidgets.html in each of these modules. # widgets/SomeWidgets.html in each of these modules.
MODULES = [ MODULES = [
"lms" "lms",
"community"
] ]
def update_website_context(context): def update_website_context(context):
@@ -35,8 +36,9 @@ class Widgets:
'<div>Hello, World!</div>' '<div>Hello, World!</div>'
""" """
def __getattr__(self, name): def __getattr__(self, name):
widget_globals = {"widgets": self}
if not name.startswith("__"): if not name.startswith("__"):
return Widget(name) return Widget(name, widget_globals)
else: else:
raise AttributeError(name) raise AttributeError(name)
@@ -50,11 +52,13 @@ class Widget:
>>> w(name="World!") >>> w(name="World!")
'<div>Hello, World!</div>' '<div>Hello, World!</div>'
""" """
def __init__(self, name): def __init__(self, name, widget_globals={}):
self.widget_globals = widget_globals
self.name = name self.name = name
def __call__(self, **kwargs): def __call__(self, **kwargs):
# the widget could be in any of the modules # the widget could be in any of the modules
paths = [f"{module}/widgets/{self.name}.html" for module in MODULES] paths = [f"{module}/widgets/{self.name}.html" for module in MODULES]
env = get_jenv() env = get_jenv()
kwargs.update(self.widget_globals)
return env.get_or_select_template(paths).render(kwargs) return env.get_or_select_template(paths).render(kwargs)

View File

@@ -9,19 +9,16 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{{ Sidebar(course_slug, batch_code) }} {{ Sidebar(course.name, batch.name) }}
<div class="container"> <div class="container">
{{ CourseBasicDetail(course)}} {{ CourseBasicDetail(course)}}
{{ InstructorsSection(instructor) }} {{ InstructorsSection(course.get_instructor()) }}
{% if batch.description %} {{ BatchDetails(batch)}}
{{ BatchDetails(batch.description) }}
{% endif %}
{{ MentorsSection(mentors, True, course.name) }}
</div> </div>
{% endblock %} {% endblock %}
{% macro CourseBasicDetail(course) %} {% macro CourseBasicDetail(course) %}
<h2>{{course.name}}</h2> <h2>{{course.title}}</h2>
<div class="course-description"> <div class="course-description">
{{course.short_introduction}} {{course.short_introduction}}
</div> </div>
@@ -36,11 +33,25 @@
<div>{{frappe.utils.md_to_html(course.description)}}</div> <div>{{frappe.utils.md_to_html(course.description)}}</div>
{% endmacro %} {% endmacro %}
{% macro BatchDetails(description) %} {% macro BatchDetails(batch) %}
<div class="mt-5"> <h2>About the Batch</h2>
<h3>About the Batch</h3>
<div> <div class="batch">
{{ frappe.utils.md_to_html(description) }} <div class="batch-details">
</div> <div>Session every {{batch.sessions_on}}</div>
<div>{{frappe.utils.format_time(batch.start_time, "short")}} -
{{frappe.utils.format_time(batch.end_time, "short")}}</div>
<div>Starting {{frappe.utils.format_date(batch.start_date, "medium")}}</div>
<div class="course-type" style="color: #888; padding: 10px 0px;">mentors</div>
{% for m in batch.get_mentors() %}
<div>
{% if m.photo_url %}
<img class="profile-photo" src="{{m.photo_url}}">
{% endif %}
<span class="instructor-title">{{m.full_name}}</span>
</div>
{% endfor %}
</div>
</div> </div>
{% endmacro %} {% endmacro %}

View File

@@ -1,21 +1,21 @@
import frappe import frappe
from community.www.courses.utils import redirect_if_not_a_member, get_course, get_instructor, get_batch from community.lms.models import Course
def get_context(context): def get_context(context):
context.no_cache = 1 context.no_cache = 1
context.course_slug = frappe.form_dict["course"]
context.course = get_course(context.course_slug)
context.batch_code = frappe.form_dict["batch"]
redirect_if_not_a_member(context.course_slug, context.batch_code)
context.instructor = get_instructor(context.course.owner) course_name = frappe.form_dict["course"]
context.batch = get_batch(context.batch_code) batch_name = frappe.form_dict["batch"]
context.mentors = get_mentors(context.batch.name)
def get_mentors(batch): course = Course.find(course_name)
mentors = [] if not course:
memberships = frappe.get_all("LMS Batch Membership", {"batch": batch, "member_type": "Mentor"}, ["member"]) context.template = "www/404.html"
for membership in memberships: return
member = frappe.get_doc("Community Member", membership.member)
mentors.append(member) batch = course.get_batch(batch_name)
return mentors if not batch:
frappe.local.flags.redirect_location = "/courses/" + course_name
raise frappe.Redirect
context.course = course
context.batch = batch

View File

@@ -11,22 +11,23 @@
<div class="course-header"> <div class="course-header">
<div class="course-type">course</div> <div class="course-type">course</div>
<h1 id="course-title" data-course="{{course.name}}">{{course.title}}</h1> <h1 id="course-title" data-course="{{course.name}}">{{course.title}}</h1>
<div class="course-short-intro">{{ course.short_introduction }}</div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-lg-9 col-md-12"> <div class="col-lg-8 col-md-12">
<div class="course-details"> <div class="course-details">
{{ CourseVideo(course) }}
{{ CourseDescription(course) }} {{ CourseDescription(course) }}
{{ BatchSection(course) }} {{ BatchSection(course) }}
{{ CourseOutline(course) }} {{ CourseOutline(course) }}
</div> </div>
</div> </div>
<div class="col-lg-4 col-md-12">
<div class="col-lg-3 col-md-12">
<div class="sidebar"> <div class="sidebar">
{{ InstructorsSection(course.get_instructor()) }} {{ InstructorsSection(course.get_instructor()) }}
</div> </div>
<div class="sidebar"> <div class="sidebar">
{{ MentorsSection(course.get_mentors(), course.is_mentor(frappe.session.user), course.name) }} {{ MentorsSection(course.get_mentors(), course.is_mentor(frappe.session.user), course.name) }}
</div> </div>
@@ -35,14 +36,7 @@
</div> </div>
{% endblock %} {% endblock %}
{% macro CourseVideo(course) %}
{% macro CourseDescription(course) %}
<h2>Course Description</h2>
<div class="course-description">
{{ course.short_introduction }}
</div>
{% if course.video_link %} {% if course.video_link %}
<div class="preview-video"> <div class="preview-video">
<iframe <iframe
@@ -57,6 +51,14 @@
{% endif %} {% endif %}
{% endmacro %} {% endmacro %}
{% macro CourseDescription(course) %}
<h2>Course Description</h2>
<div class="course-description">
{{ course.short_introduction }}
</div>
{% endmacro %}
{% macro BatchSection(course) %} {% macro BatchSection(course) %}
{% if course.is_mentor(frappe.session.user) %} {% if course.is_mentor(frappe.session.user) %}
{{ BatchSectionForMentors(course, course.get_batches(mentor=frappe.session.user)) }} {{ BatchSectionForMentors(course, course.get_batches(mentor=frappe.session.user)) }}
@@ -76,9 +78,7 @@
{% for m in batch.get_mentors() %} {% for m in batch.get_mentors() %}
<div> <div>
{% if m.photo_url %} {{ widgets.Avatar(member=m, avatar_class="avatar-medium" ) }}
<img class="profile-photo" src="{{m.photo_url}}">
{% endif %}
<span class="instructor-title">{{m.full_name}}</span> <span class="instructor-title">{{m.full_name}}</span>
</div> </div>
{% endfor %} {% endfor %}
@@ -86,7 +86,7 @@
<div class="cta"> <div class="cta">
<div class=""> <div class="">
{% if can_manage %} {% if can_manage %}
<button >Manage</button> <a href="/courses/{{course.name}}/{{batch.name}}/about" class="btn btn-secondary">Manage</a>
{% else %} {% else %}
<button class="join-batch" data-batch="{{ batch.name | urlencode }}">Join this Batch</button> <button class="join-batch" data-batch="{{ batch.name | urlencode }}">Join this Batch</button>
{% endif %} {% endif %}
@@ -99,7 +99,7 @@
<h2>Your Batches</h2> <h2>Your Batches</h2>
{% if mentor_batches %} {% if mentor_batches %}
<div class="message"> <div class="alert alert-secondary">
You are a mentor for this course. Manage your batches or create a new batch from here. You are a mentor for this course. Manage your batches or create a new batch from here.
</div> </div>
@@ -136,6 +136,6 @@
<h2>Course Outline</h2> <h2>Course Outline</h2>
{% for chapter in course.get_chapters() %} {% for chapter in course.get_chapters() %}
{{ widgets.ChapterTeaser(chapter=chapter)}} {{ widgets.ChapterTeaser(index=loop.index, chapter=chapter)}}
{% endfor %} {% endfor %}
{% endmacro %} {% endmacro %}

View File

@@ -6,7 +6,7 @@ frappe.ready(() => {
course: decodeURIComponent($("#course-title").attr("data-course")), course: decodeURIComponent($("#course-title").attr("data-course")),
}, },
'callback': (data) => { 'callback': (data) => {
if (data.message) { if (data.message > 0) {
$("#mentor-request").addClass("hide"); $("#mentor-request").addClass("hide");
$("#already-applied").removeClass("hide") $("#already-applied").removeClass("hide")
} }

View File

@@ -7,12 +7,12 @@ def get_context(context):
context.no_cache = 1 context.no_cache = 1
try: try:
course_slug = frappe.form_dict["course"] course_name = frappe.form_dict["course"]
except KeyError: except KeyError:
frappe.local.flags.redirect_location = "/courses" frappe.local.flags.redirect_location = "/courses"
raise frappe.Redirect raise frappe.Redirect
course = Course.find(course_slug) course = Course.find(course_name)
if course is None: if course is None:
frappe.local.flags.redirect_location = "/courses" frappe.local.flags.redirect_location = "/courses"
raise frappe.Redirect raise frappe.Redirect

View File

@@ -26,11 +26,11 @@
{% macro course_card(course) %} {% macro course_card(course) %}
<div class="card mb-5 w-100"> <div class="card mb-5 w-100">
<div class="card-body"> <div class="card-body">
<h5 class="card-title"><a href="/courses/{{course.slug}}">{{course.title}}</a></h5> <h5 class="card-title"><a href="/courses/{{course.name}}">{{course.title}}</a></h5>
{% if course.description %} {% if course.description %}
<p class="card-text">{{ frappe.utils.md_to_html(course.description[:250]) }}</p> <p class="card-text">{{ frappe.utils.md_to_html(course.description[:250]) }}</p>
{% endif %} {% endif %}
<a href="/courses/{{course.slug}}" class="card-link">See more &rarr;</a> <a href="/courses/{{course.name}}" class="card-link">See more &rarr;</a>
</div> </div>
</div> </div>
{% endmacro %} {% endmacro %}

View File

@@ -7,7 +7,7 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{{ Sidebar(course, batch_code) }} {{ Sidebar(course.name, batch.name) }}
<div class="container"> <div class="container">
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -1,8 +1,21 @@
import frappe import frappe
from community.www.courses.utils import redirect_if_not_a_member from community.lms.models import Course
def get_context(context): def get_context(context):
context.no_cache = 1 context.no_cache = 1
context.course = frappe.form_dict["course"]
context.batch_code = frappe.form_dict["batch"] course_name = frappe.form_dict["course"]
redirect_if_not_a_member(context.course, context.batch_code) batch_name = frappe.form_dict["batch"]
course = Course.find(course_name)
if not course:
context.template = "www/404.html"
return
batch = course.get_batch(batch_name)
if not batch:
frappe.local.flags.redirect_location = "/courses/" + course_name
raise frappe.Redirect
context.course = course
context.batch = batch

View File

@@ -14,12 +14,14 @@ def get_member_with_name(name):
def get_batch(code): def get_batch(code):
try: try:
return frappe.db.get_value("LMS Batch", {"code": code}, ["name", "description"], as_dict=True) print("get_batch", code)
return frappe.db.get_value("LMS Batch", {"name": code}, ["name", "description"], as_dict=True)
except frappe.DoesNotExistError: except frappe.DoesNotExistError:
print("Error: notfound")
return return
def is_member_of_batch(batch_code): def is_member_of_batch(batch_code):
membership = frappe.get_all("LMS Batch Membership", {"batch": get_batch(batch_code).name, "member": get_member_with_email()}) membership = frappe.get_all("LMS Batch Membership", {"batch": batch_code, "member": get_member_with_email()})
if len(membership): if len(membership):
return True return True
return False return False
@@ -29,9 +31,9 @@ def redirect_if_not_a_member(course,batch_code):
frappe.local.flags.redirect_location = "/courses/" + course frappe.local.flags.redirect_location = "/courses/" + course
raise frappe.Redirect raise frappe.Redirect
def get_course(slug): def get_course(name):
try: try:
return frappe.get_doc("LMS Course", {"slug": slug}) return frappe.get_doc("LMS Course", {"name": name})
except frappe.DoesNotExistError: except frappe.DoesNotExistError:
return return
@@ -54,4 +56,4 @@ def get_batch_members(batch):
if membership.member_type == "Mentor": if membership.member_type == "Mentor":
member.is_mentor = True member.is_mentor = True
members.append(member) members.append(member)
return members return members

View File

@@ -182,8 +182,8 @@
{% for course in courses %} {% for course in courses %}
<div class="dashboard__course"> <div class="dashboard__course">
<div class="dashboard__courseHeader"> <div class="dashboard__courseHeader">
<a class="text-decoration-none" target="_blank" href="/courses/{{course.slug}}"> <a class="text-decoration-none" target="_blank" href="/courses/{{course.name}}">
<h5 class="w-75">{{ course.name }}</h5> <h5 class="w-75">{{ course.title }}</h5>
</a> </a>
{% if course.member_type %} {% if course.member_type %}
<div class="dashboard__badge"> <div class="dashboard__badge">

View File

@@ -1,31 +1,31 @@
{% macro InstructorsSection(instructor) %} {% macro InstructorsSection(instructor) %}
<h3>Instructor</h3> <h3>Instructor</h3>
<div class="instructor"> <div class="instructor">
<div class="instructor-title">{{instructor.full_name}}</div> <div class="instructor-title">{{instructor.full_name}}</div>
<div class="instructor-subtitle">Created {{instructor.get_course_count()}} courses</div> <div class="instructor-subtitle">Created {{instructor.get_course_count()}} courses</div>
</div> </div>
{% endmacro %} {% endmacro %}
{% macro MentorsSection(mentors, is_mentor, course_name) %} {% macro MentorsSection(mentors, is_mentor, course_name) %}
<h3>Mentors</h3> <h3>Mentors</h3>
{% for m in mentors %} {% for m in mentors %}
<div class="instructor"> <div class="instructor">
<div class="instructor-title">{{m.full_name}}</div> <div class="instructor-title">{{m.full_name}}</div>
<div class="instructor-subtitle">Mentored {{m.get_batch_count()}} batches</div> <div class="instructor-subtitle">Mentored {{m.get_batch_count()}} batches</div>
</div> </div>
{% endfor %} {% endfor %}
{% if not is_mentor %} {% if not is_mentor %}
<div id="mentor-request" class="notice"> <div id="mentor-request" class="notice">
Interested to become a mentor? Interested to become a mentor?
<div><a id="apply-now" data-course="{{course_name | urlencode}}" href="">Apply Now!</a></div> <div><a id="apply-now" data-course="{{course_name | urlencode}}" href="">Apply Now!</a></div>
</div> </div>
<div id="already-applied" class="notice hide"> <div id="already-applied" class="notice hide">
You've applied to become a mentor for this course. Your request is currently under review. You've applied to become a mentor for this course. Your request is currently under review.
If you are not any more interested to mentor this course, you can <a id="cancel-request" data-course="{{course_name | urlencode}}" href="">cancel your application</a>. If you are not any more interested to mentor this course, you can <a id="cancel-request" data-course="{{course_name | urlencode}}" href="">cancel your application</a>.
</div> </div>
{% endif %} {% endif %}
{% endmacro %} {% endmacro %}

View File

@@ -108,30 +108,16 @@
<div class="tab-pane fade py-4 show active" role="tabpanel" id="home"> <div class="tab-pane fade py-4 show active" role="tabpanel" id="home">
<div class='container'> <div class='container'>
<div class="row row-cols-1 row-cols-xl-5 row-cols-lg-4 row-cols-md-3 row-cols-sm-2"> <div class="row row-cols-1 row-cols-xl-5 row-cols-lg-4 row-cols-md-3 row-cols-sm-2">
{% if sketches %} {% if sketches %}
{% for sketch in sketches %} {% for sketch in sketches %}
<div class="col m-4"> <div class="col m-4">
<div class="card sketch-card" style="width: 200px;"> {{ widgets.SketchTeaser(sketch=sketch) }}
<div class="card-img-top">
<a href="/sketches/{{sketch.sketch_id}}">
{{ sketch.to_svg() }}
</a>
</div>
<div class="card-footer">
<div class="sketch-title">
<a href="sketches/{{sketch.sketch_id}}">{{sketch.title}}</a>
</div>
<div class="sketch-author">
by {{sketch.get_owner_name()}}
</div>
</div>
</div>
</div> </div>
{% endfor %} {% endfor %}
{% endif %} {% endif %}
</div> </div>
{% if not sketches %} {% if not sketches %}
<p class="text-center">{{member.full_name}} has not created any skecth yet.</p> <p class="text-center">{{member.full_name}} has not created any skecth yet.</p>
{% endif %} {% endif %}
</div> </div>
</div> </div>