Merge pull request #76 from fossunited/course-page

Refactored the course page
This commit is contained in:
Anand Chitipothu
2021-05-06 13:55:42 +05:30
committed by GitHub
23 changed files with 290 additions and 203 deletions

View File

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

View File

@@ -6,6 +6,7 @@ from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from community.www.courses.utils import get_member_with_email
from community.query import find, find_all
class LMSBatch(Document):
def validate(self):
@@ -23,10 +24,16 @@ class LMSBatch(Document):
"LMS Batch Membership",
{"batch": self.name, "member_type": "Mentor"},
["member"])
for membership in memberships:
member = frappe.get_doc("Community Member", membership.member)
mentors.append(member)
return mentors
member_names = [m['member'] for m in memberships]
return find_all("Community Member", name=["IN", member_names])
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()
def get_messages(batch):

View File

@@ -40,6 +40,6 @@ def create_membership(batch, course=None, member=None, member_type="Student", ro
"member": member
}).save(ignore_permissions=True)
if course:
course_slug = frappe.db.get_value("LMS Course", {"title": course}, ["slug"])
course_slug = frappe.db.get_value("LMS Course", {"title": course}, ["name"])
return course_slug
return "OK"

View File

@@ -2,7 +2,6 @@
"actions": [],
"allow_guest_to_view": 1,
"allow_rename": 1,
"autoname": "field:title",
"creation": "2021-03-01 16:49:33.622422",
"doctype": "DocType",
"editable_grid": 1,
@@ -89,7 +88,7 @@
"link_fieldname": "course"
}
],
"modified": "2021-05-03 05:52:30.396824",
"modified": "2021-05-06 11:15:45.728976",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Course",

View File

@@ -6,13 +6,18 @@ from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from ...utils import slugify
from community.query import find, find_all
class LMSCourse(Document):
@staticmethod
def find(slug):
"""Returns the course with specified slug.
def find(name):
"""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
def find_all():
@@ -20,10 +25,6 @@ class LMSCourse(Document):
"""
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):
result = frappe.get_all(
'LMS Course',
@@ -32,7 +33,7 @@ class LMSCourse(Document):
return slugify(title, used_slugs=slugs)
def __repr__(self):
return f"<Course#{self.name} {self.slug}>"
return f"<Course#{self.name}>"
def get_topic(self, slug):
"""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
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):
batches = find_all("LMS Batch", course=self.name)
if mentor:
@@ -139,24 +143,8 @@ class LMSCourse(Document):
now = frappe.utils.nowdate()
batches = find_all("LMS Batch",
course=self.name,
start_date=[">", now])
start_date=[">", now],
status="Active",
visibility="Public")
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):
course = self.new_course("Test Course")
assert course.title == "Test Course"
assert course.slug == "test-course"
assert course.name == "test-course"
assert course.get_mentors() == []
def test_find_all(self):
@@ -41,7 +41,7 @@ class TestLMSCourse(unittest.TestCase):
# now we should find one course
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
def _test_add_mentors(self):

View File

@@ -1,6 +1,6 @@
<div class="chapter-teaser">
<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">
{{ chapter.description or "" }}
</div>

View File

@@ -1,6 +1,6 @@
<div class="course-teaser">
<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">
{{ course.short_introduction or "" }}
</div>

View File

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

View File

@@ -34,82 +34,8 @@ body {
margin: 0px;
}
.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;
}
.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 {
/* .course-details {
margin: 20px 0px;
}
@@ -118,7 +44,7 @@ body {
font-size: 1.4em;
font-weight: bold;
margin: 20px 0px 10px 0px;
}
} */
.chapter-plan {
border-radius: 10px;
@@ -133,7 +59,7 @@ body {
font-weight: bold;
}
.chapter-number {
/* .chapter-number {
background: var(--text-color);
color: white;
border-radius: 50%;
@@ -146,7 +72,7 @@ body {
.chapter-description {
margin: 20px 0px;
}
} */
.lessons {
padding-left: 20px;
@@ -302,4 +228,4 @@ img.profile-photo {
color: #2D005A;
font-weight: 600;
line-height: 51px;
}
}

View File

@@ -5,6 +5,11 @@ body {
font-family: "Inter", sans-serif;
}
h2 {
margin: 20px 0px;
color: black;
}
.teaser {
background: white;
border-radius: 9px;
@@ -122,3 +127,108 @@ section.lightgray {
padding: 2rem 0px;
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. */
:root {
--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

@@ -9,19 +9,16 @@
{% endblock %}
{% block content %}
{{ Sidebar(course_slug, batch_code) }}
{{ Sidebar(course.name, batch.name) }}
<div class="container">
{{ CourseBasicDetail(course)}}
{{ InstructorsSection(instructor) }}
{% if batch.description %}
{{ BatchDetails(batch.description) }}
{% endif %}
{{ MentorsSection(mentors, True, course.name) }}
{{ InstructorsSection(course.get_instructor()) }}
{{ BatchDetails(batch)}}
</div>
{% endblock %}
{% macro CourseBasicDetail(course) %}
<h2>{{course.name}}</h2>
<h2>{{course.title}}</h2>
<div class="course-description">
{{course.short_introduction}}
</div>
@@ -36,11 +33,25 @@
<div>{{frappe.utils.md_to_html(course.description)}}</div>
{% endmacro %}
{% macro BatchDetails(description) %}
<div class="mt-5">
<h3>About the Batch</h3>
<div>
{{ frappe.utils.md_to_html(description) }}
</div>
{% macro BatchDetails(batch) %}
<h2>About the Batch</h2>
<div class="batch">
<div class="batch-details">
<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>
{% endmacro %}

View File

@@ -1,21 +1,21 @@
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):
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)
context.batch = get_batch(context.batch_code)
context.mentors = get_mentors(context.batch.name)
course_name = frappe.form_dict["course"]
batch_name = frappe.form_dict["batch"]
def get_mentors(batch):
mentors = []
memberships = frappe.get_all("LMS Batch Membership", {"batch": batch, "member_type": "Mentor"}, ["member"])
for membership in memberships:
member = frappe.get_doc("Community Member", membership.member)
mentors.append(member)
return mentors
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

@@ -11,22 +11,23 @@
<div class="course-header">
<div class="course-type">course</div>
<h1 id="course-title" data-course="{{course.name}}">{{course.title}}</h1>
<div class="course-short-intro">{{ course.short_introduction }}</div>
</div>
<div class="row">
<div class="col-lg-9 col-md-12">
<div class="col-lg-8 col-md-12">
<div class="course-details">
{{ CourseVideo(course) }}
{{ CourseDescription(course) }}
{{ BatchSection(course) }}
{{ CourseOutline(course) }}
</div>
</div>
<div class="col-lg-3 col-md-12">
<div class="col-lg-4 col-md-12">
<div class="sidebar">
{{ InstructorsSection(course.get_instructor()) }}
</div>
<div class="sidebar">
{{ MentorsSection(course.get_mentors(), course.is_mentor(frappe.session.user), course.name) }}
</div>
@@ -35,14 +36,7 @@
</div>
{% endblock %}
{% macro CourseDescription(course) %}
<h2>Course Description</h2>
<div class="course-description">
{{ course.short_introduction }}
</div>
{% macro CourseVideo(course) %}
{% if course.video_link %}
<div class="preview-video">
<iframe
@@ -57,6 +51,14 @@
{% endif %}
{% endmacro %}
{% macro CourseDescription(course) %}
<h2>Course Description</h2>
<div class="course-description">
{{ course.short_introduction }}
</div>
{% endmacro %}
{% macro BatchSection(course) %}
{% if course.is_mentor(frappe.session.user) %}
{{ BatchSectionForMentors(course, course.get_batches(mentor=frappe.session.user)) }}
@@ -84,7 +86,7 @@
<div class="cta">
<div class="">
{% if can_manage %}
<button >Manage</button>
<a href="/courses/{{course.name}}/{{batch.name}}/about" class="btn btn-secondary">Manage</a>
{% else %}
<button class="join-batch" data-batch="{{ batch.name | urlencode }}">Join this Batch</button>
{% endif %}
@@ -134,6 +136,6 @@
<h2>Course Outline</h2>
{% for chapter in course.get_chapters() %}
{{ widgets.ChapterTeaser(chapter=chapter)}}
{{ widgets.ChapterTeaser(index=loop.index, chapter=chapter)}}
{% endfor %}
{% endmacro %}

View File

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

View File

@@ -26,11 +26,11 @@
{% macro course_card(course) %}
<div class="card mb-5 w-100">
<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 %}
<p class="card-text">{{ frappe.utils.md_to_html(course.description[:250]) }}</p>
{% 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>
{% endmacro %}

View File

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

View File

@@ -1,8 +1,21 @@
import frappe
from community.www.courses.utils import redirect_if_not_a_member
from community.lms.models import Course
def get_context(context):
context.no_cache = 1
context.course = frappe.form_dict["course"]
context.batch_code = frappe.form_dict["batch"]
redirect_if_not_a_member(context.course, context.batch_code)
course_name = frappe.form_dict["course"]
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):
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:
print("Error: notfound")
return
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):
return True
return False
@@ -29,9 +31,9 @@ def redirect_if_not_a_member(course,batch_code):
frappe.local.flags.redirect_location = "/courses/" + course
raise frappe.Redirect
def get_course(slug):
def get_course(name):
try:
return frappe.get_doc("LMS Course", {"slug": slug})
return frappe.get_doc("LMS Course", {"name": name})
except frappe.DoesNotExistError:
return
@@ -54,4 +56,4 @@ def get_batch_members(batch):
if membership.member_type == "Mentor":
member.is_mentor = True
members.append(member)
return members
return members

View File

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

View File

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