fix: get_doc references

This commit is contained in:
Jannat Patel
2022-02-10 09:53:08 +05:30
parent e17637d27b
commit fd5b7976e7
21 changed files with 334 additions and 286 deletions

View File

@@ -1,14 +1,14 @@
{% set color = get_palette(member.full_name) %} {% set color = get_palette(member.full_name) %}
<a class="button-links" href="{{ get_profile_url(member.username) }}"> <span class="avatar {{ avatar_class }}" title="{{ member.full_name }}">
<span class="avatar {{ avatar_class }}" title="{{ member.full_name }}"> <a class="button-links" href="{{ get_profile_url(member.username) }}">
{% if member.user_image %} {% if member.user_image %}
<img class="avatar-frame standard-image" style="object-fit: cover;" src="{{ member.user_image }}" title="{{ member.full_name }}"> <img class="avatar-frame standard-image" style="object-fit: cover;" src="{{ member.user_image }}" title="{{ member.full_name }}">
</img> </img>
{% else %} {% else %}
<span class="avatar-frame standard-image" title="{{ member.full_name }}" <span class="avatar-frame standard-image" title="{{ member.full_name }}"
style="background-color: var({{color[0]}}); color: var({{color[1]}});"> style="background-color: var({{color[0]}}); color: var({{color[1]}});">
{{ frappe.utils.get_abbr(member.full_name) }} {{ frappe.utils.get_abbr(member.full_name) }}
</span>
{% endif %}
</span> </span>
{% endif %}
</a> </a>
</span>

View File

@@ -162,10 +162,26 @@ update_website_context = [
jinja = { jinja = {
"methods": [ "methods": [
"school.page_renderers.get_profile_url", "school.page_renderers.get_profile_url",
"school.overrides.user.get_authored_courses",
"school.overrides.user.get_palette", "school.overrides.user.get_palette",
"school.www.utils.get_membership", "school.www.utils.get_membership",
"school.www.utils.get_lessons", "school.www.utils.get_lessons",
"school.www.utils.get_tags" "school.www.utils.get_tags",
"school.www.utils.get_instructors",
"school.www.utils.get_students",
"school.www.utils.get_average_rating",
"school.www.utils.is_certified",
"school.www.utils.get_lesson_index",
"school.www.utils.get_lesson_url",
"school.www.utils.get_chapters",
"school.www.utils.get_slugified_chapter_title",
"school.www.utils.get_progress",
"school.www.utils.render_html",
"school.www.utils.is_mentor",
"school.www.utils.is_cohort_staff",
"school.www.utils.get_mentors",
"school.www.utils.get_reviews",
"school.www.utils.is_eligible_to_review",
], ],
"filters": [] "filters": []
} }

View File

@@ -5,7 +5,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
from ...md import markdown_to_html, find_macros from ...md import find_macros
from school.www.utils import get_course_progress
class CourseLesson(Document): class CourseLesson(Document):
def validate(self): def validate(self):
@@ -57,9 +58,6 @@ class CourseLesson(Document):
folder = frappe.get_doc(args) folder = frappe.get_doc(args)
folder.save(ignore_permissions=True) folder.save(ignore_permissions=True)
def render_html(self):
return markdown_to_html(self.body)
def get_exercises(self): def get_exercises(self):
if not self.body: if not self.body:
return [] return []
@@ -107,7 +105,6 @@ def save_progress(lesson, course, status):
"status": status, "status": status,
}).save(ignore_permissions=True) }).save(ignore_permissions=True)
course_details = frappe.get_doc("LMS Course", course) progress = get_course_progress(course)
progress = course_details.get_course_progress()
frappe.db.set_value("LMS Batch Membership", membership, "progress", progress) frappe.db.set_value("LMS Batch Membership", membership, "progress", progress)
return progress return progress

View File

@@ -8,7 +8,7 @@ import json
from ...utils import slugify from ...utils import slugify
from school.query import find, find_all from school.query import find, find_all
from frappe.utils import flt, cint from frappe.utils import flt, cint
from ...utils import slugify from school.www.utils import get_chapters
class LMSCourse(Document): class LMSCourse(Document):
@@ -98,31 +98,7 @@ class LMSCourse(Document):
}) })
doc.insert() doc.insert()
def get_mentors(self):
"""Returns the list of all mentors for this course.
"""
course_mentors = []
mentors = frappe.get_all("LMS Course Mentor Mapping", {"course": self.name}, ["mentor"])
for mentor in mentors:
member = frappe.get_doc("User", mentor.mentor)
member.batch_count = frappe.db.count("LMS Batch Membership",
{
"member": member.name,
"member_type": "Mentor"
})
course_mentors.append(member)
return course_mentors
def is_mentor(self, email):
"""Checks if given user is a mentor for this course.
"""
if not email:
return False
return frappe.db.count("LMS Course Mentor Mapping",
{
"course": self.name,
"mentor": email
})
def get_student_batch(self, email): def get_student_batch(self, email):
"""Returns the batch the given student is part of. """Returns the batch the given student is part of.
@@ -142,12 +118,6 @@ class LMSCourse(Document):
fieldname="batch") fieldname="batch")
return batch_name and frappe.get_doc("LMS Batch", batch_name) return batch_name and frappe.get_doc("LMS Batch", batch_name)
def get_slugified_chapter_title(self, chapter):
return slugify(chapter)
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:
@@ -166,36 +136,8 @@ class LMSCourse(Document):
name = frappe.get_value("Cohort", {"course": self.name, "slug": cohort_slug}) name = frappe.get_value("Cohort", {"course": self.name, "slug": cohort_slug})
return name and frappe.get_doc("Cohort", name) 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.
"""
lesson = frappe.db.get_value("Lesson Reference", {"lesson": lesson_name}, ["idx", "parent"], as_dict=True)
if not lesson:
return None
chapter = frappe.db.get_value("Chapter Reference", {"chapter": lesson.parent}, ["idx"], as_dict=True)
if not chapter:
return None
return f"{chapter.idx}.{lesson.idx}"
def reindex_exercises(self): def reindex_exercises(self):
for i, c in enumerate(self.get_chapters(), start=1): for i, c in enumerate(get_chapters(self.name), start=1):
self._reindex_exercises_in_chapter(c, i) self._reindex_exercises_in_chapter(c, i)
def _reindex_exercises_in_chapter(self, c, index): def _reindex_exercises_in_chapter(self, c, index):
@@ -207,117 +149,12 @@ class LMSCourse(Document):
exercise.save() exercise.save()
i += 1 i += 1
def get_learn_url(self, lesson_number):
if not lesson_number:
return
return f"/courses/{self.name}/learn/{lesson_number}"
def get_all_memberships(self, member): def get_all_memberships(self, member):
all_memberships = frappe.get_all("LMS Batch Membership", {"member": member, "course": self.name}, ["batch"]) all_memberships = frappe.get_all("LMS Batch Membership", {"member": member, "course": self.name}, ["batch"])
for membership in all_memberships: for membership in all_memberships:
membership.batch_title = frappe.db.get_value("LMS Batch", membership.batch, "title") membership.batch_title = frappe.db.get_value("LMS Batch", membership.batch, "title")
return all_memberships return all_memberships
def get_students(self, batch=None):
"""Returns (email, full_name, username) of all the students of this batch as a list of dict.
"""
filters = {
"course": self.name,
"member_type": "Student"
}
if batch:
filters["batch"] = batch
memberships = frappe.get_all(
"LMS Batch Membership",
filters,
["member"])
member_names = [m['member'] for m in memberships]
return find_all("User", name=["IN", member_names])
def get_reviews(self):
reviews = frappe.get_all("LMS Course Review",
{
"course": self.name
},
["review", "rating", "owner"],
order_by= "creation desc")
out_of_ratings = frappe.db.get_all("DocField",
{
"parent": "LMS Course Review",
"fieldtype": "Rating"
},
["options"])
out_of_ratings = (len(out_of_ratings) and out_of_ratings[0].options) or 5
for review in reviews:
review.rating = review.rating * out_of_ratings
review.owner_details = frappe.get_doc("User", review.owner)
return reviews
def is_eligible_to_review(self, membership):
""" Checks if user is eligible to review the course """
if not membership:
return False
if frappe.db.count("LMS Course Review",
{
"course": self.name,
"owner": frappe.session.user
}):
return False
return True
def get_average_rating(self):
ratings = [review.rating for review in self.get_reviews()]
if not len(ratings):
return None
return sum(ratings)/len(ratings)
def get_progress(self, lesson):
return frappe.db.get_value("LMS Course Progress",
{
"course": self.name,
"owner": frappe.session.user,
"lesson": lesson
},
["status"])
def get_course_progress(self, member=None):
""" Returns the course progress of the session user """
lesson_count = len(self.get_lessons())
if not lesson_count:
return 0
completed_lessons = frappe.db.count("LMS Course Progress",
{
"course": self.name,
"owner": member or frappe.session.user,
"status": "Complete"
})
precision = cint(frappe.db.get_default("float_precision")) or 3
return flt(((completed_lessons/lesson_count) * 100), precision)
def get_neighbours(self, current, lessons):
current = flt(current)
numbers = sorted(lesson.number for lesson in lessons)
index = numbers.index(current)
return {
"prev": numbers[index-1] if index-1 >= 0 else None,
"next": numbers[index+1] if index+1 < len(numbers) else None
}
def is_certified(self):
certificate = frappe.get_all("LMS Certification",
{
"student": frappe.session.user,
"course": self.name
})
if len(certificate):
return certificate[0].name
return
@frappe.whitelist() @frappe.whitelist()
def reindex_exercises(doc): def reindex_exercises(doc):
course_data = json.loads(doc) course_data = json.loads(doc)

View File

@@ -105,7 +105,7 @@ def sanitize_html(html, macro):
any broken tags. This makes sures that all those things are fixed any broken tags. This makes sures that all those things are fixed
before passing to the etree parser. before passing to the etree parser.
""" """
soup = BeautifulSoup(html, features="html5lib") soup = BeautifulSoup(html, features="lxml")
nodes = soup.body.children nodes = soup.body.children
classname = "" classname = ""
if macro == "YouTubeVideo": if macro == "YouTubeVideo":

View File

@@ -2,7 +2,8 @@
<h2 class="section-title">{{ title }}</h2> <h2 class="section-title">{{ title }}</h2>
<div class="cards-parent mt-10"> <div class="cards-parent mt-10">
{% for course_row in courses %} {% for course_row in courses %}
{% set course = frappe.get_doc("LMS Course", course_row.course) %} {% set course = frappe.db.get_value("LMS Course", course_row.course,
["name", "upcoming", "title", "image", "enable_certification"], as_dict=True) %}
{{ widgets.CourseCard(course=course, read_only=False) }} {{ widgets.CourseCard(course=course, read_only=False) }}
{% endfor %} {% endfor %}
</div> </div>

View File

@@ -1,12 +1,12 @@
<div> <div>
<div class="chapter-title small-title" data-target="#{{ course.get_slugified_chapter_title(chapter.title) }}" <div class="chapter-title small-title" data-target="#{{ get_slugified_chapter_title(chapter.title) }}"
data-toggle="collapse" aria-expanded="false"> data-toggle="collapse" aria-expanded="false">
<img class="chapter-icon" src="/assets/school/icons/chevron-right.svg"> <img class="chapter-icon" src="/assets/school/icons/chevron-right.svg">
{{ index }}. {{ chapter.title }} {{ index }}. {{ chapter.title }}
</div> </div>
<div class="chapter-content collapse navbar-collapse" id="{{ course.get_slugified_chapter_title(chapter.title) }}"> <div class="chapter-content collapse navbar-collapse" id="{{ get_slugified_chapter_title(chapter.title) }}">
{% if chapter.description %} {% if chapter.description %}
<div class="chapter-description muted-text"> <div class="chapter-description muted-text">
@@ -17,17 +17,17 @@
{% set is_instructor = frappe.session.user == course.instructor %} {% set is_instructor = frappe.session.user == course.instructor %}
<div class="lessons"> <div class="lessons">
{% for lesson in course.get_lessons(chapter) %} {% for lesson in get_lessons(course.name, chapter) %}
<div class="lesson-info {% if membership.current_lesson == lesson.name %} active-lesson {% endif %}"> <div class="lesson-info {% if membership.current_lesson == lesson.name %} active-lesson {% endif %}">
{% if membership or lesson.include_in_preview %} {% if membership or lesson.include_in_preview %}
<a class="lesson-links" href="{{ course.get_learn_url(lesson.number) }}{{course.query_parameter}}" <a class="lesson-links" href="{{ get_lesson_url(course.name, lesson.number) }}{{course.query_parameter}}"
data-course="{{ course.name }}"> data-course="{{ course.name }}">
{{ lesson.title }} {{ lesson.title }}
{% if membership %} {% if membership %}
<img class="ml-1 lesson-progress-tick {{ course.get_progress(lesson.name) != 'Complete' and 'hide' }}" <img class="ml-1 lesson-progress-tick {{ get_progress(course.name, lesson.name) != 'Complete' and 'hide' }}"
src="/assets/school/icons/check.svg"> src="/assets/school/icons/check.svg">
{% endif %} {% endif %}
@@ -36,7 +36,7 @@
{% elif is_instructor and not lesson.include_in_preview %} {% elif is_instructor and not lesson.include_in_preview %}
<a class="lesson-links" <a class="lesson-links"
title="This lesson is not available for preview. As you are the Instructor of the course only you can see it." title="This lesson is not available for preview. As you are the Instructor of the course only you can see it."
href="{{ course.get_learn_url(lesson.number) }}{{course.query_parameter}}" href="{{ course.get_lesson_url(lesson.number) }}{{course.query_parameter}}"
data-course="{{ course.name }}"> data-course="{{ course.name }}">
{{ lesson.title }} {{ lesson.title }}
<img class="ml-2" src="/assets/school/icons/lock.svg"> <img class="ml-2" src="/assets/school/icons/lock.svg">
@@ -58,7 +58,7 @@
</div> </div>
</div> </div>
{% if index != course.get_chapters() | length %} {% if index != get_chapters(course.name) | length %}
<div class="card-divider"></div> <div class="card-divider"></div>
{% endif %} {% endif %}

View File

@@ -34,23 +34,38 @@
<div class="progress-percentage">{{ progress }}% Completed</div> <div class="progress-percentage">{{ progress }}% Completed</div>
{% endif %} {% endif %}
<div class="course-card-footer"> <div class="zindex course-card-footer">
<span> <span class="">
{{ widgets.Avatar(member=course.get_instructor(), avatar_class="avatar-small") }} {% set instructors = get_instructors(course.name) %}
<a class="button-links" href="{{ get_profile_url(course.get_instructor().username) }}"> {% for instructor in instructors %}
{% if instructors | length > 1 and loop.index == 1 %}
<div class="avatar-group left overlap">
{% endif %}
{{ widgets.Avatar(member=instructor, avatar_class="avatar-small") }}
{% if instructors | length > 1 and loop.index == instructors | length %}
</div>
{% endif %}
{% endfor %}
<a class="button-links" href="{{ get_profile_url(instructors[0].username) }}">
<span class="course-instructor"> <span class="course-instructor">
{{ instructor.full_name }} {% if instructors | length == 1 %}
{{ instructors[0].full_name }}
{% else %}
{{ instructors[0].full_name.split(" ")[0] }} and {{ instructors | length - 1 }} others
{% endif %}
</span> </span>
</a> </a>
</span> </span>
{% endfor %} {% set student_count = get_students(course.name) | length %}
<span class="course-student-count"> <span class="course-student-count">
{% if course.get_students() | length %} {% if student_count %}
<span class="vertically-center mr-4"> <span class="vertically-center mr-4">
<img class="icon-background" src="/assets/school/icons/user.svg" /> <img class="icon-background" src="/assets/school/icons/user.svg" />
{{ course.get_students() | length }} {{ student_count }}
</span> {% endif %} </span>
{% set avg_rating = course.get_average_rating() %} {% endif %}
{% set avg_rating = get_average_rating(course.name) %}
{% if avg_rating %} {% if avg_rating %}
<span class="vertically-center"> <span class="vertically-center">
<img class="icon-background" src="/assets/school/icons/rating.svg" /> <img class="icon-background" src="/assets/school/icons/rating.svg" />
@@ -64,11 +79,11 @@
<a class="stretched-link" href="/courses/{{ course.name }}"></a> <a class="stretched-link" href="/courses/{{ course.name }}"></a>
{% else %} {% else %}
{% set lesson_index = course.get_lesson_index(membership.current_lesson) if membership and {% set lesson_index = get_lesson_index(membership.current_lesson) if membership and
membership.current_lesson else '1.1' %} membership.current_lesson else '1.1' %}
{% set query_parameter = "?batch=" + membership.batch if membership and {% set query_parameter = "?batch=" + membership.batch if membership and
membership.batch else "" %} membership.batch else "" %}
{% set certificate = course.is_certified() %} {% set certificate = is_certified(course.name) %}
{% if certificate %} {% if certificate %}
<a class="stretched-link" href="/courses/{{ course.name }}/{{ certificate }}"></a> <a class="stretched-link" href="/courses/{{ course.name }}/{{ certificate }}"></a>
@@ -83,7 +98,7 @@
<a class="stretched-link" href="/courses/{{ course.name }}"></a> <a class="stretched-link" href="/courses/{{ course.name }}"></a>
{% elif membership %} {% elif membership %}
<a class="stretched-link" href="{{ course.get_learn_url(lesson_index) }}{{ query_parameter }}"></a> <a class="stretched-link" href="{{ get_lesson_url(course.name, lesson_index) }}{{ query_parameter }}"></a>
{% else %} {% else %}
<a class="stretched-link" href="/courses/{{ course.name }}"></a> <a class="stretched-link" href="/courses/{{ course.name }}"></a>

View File

@@ -1,10 +1,10 @@
{% if course.get_chapters() | length %} {% if get_chapters(course.name) | length %}
<div class=""> <div class="">
<div class="course-home-headings"> <div class="course-home-headings">
Course Outline Course Outline
</div> </div>
<div class="common-card-style course-outline"> <div class="common-card-style course-outline">
{% for chapter in course.get_chapters() %} {% for chapter in get_chapters(course.name) %}
{{ widgets.ChapterTeaser(index=loop.index, chapter=chapter, course=course, batch=batch, membership=membership) }} {{ widgets.ChapterTeaser(index=loop.index, chapter=chapter, course=course, batch=batch, membership=membership) }}
{% endfor %} {% endfor %}
</div> </div>

View File

@@ -3,7 +3,7 @@
<div class="small-title member-card-title {% if show_course_count %} font-weight-bold {% endif %}"> <div class="small-title member-card-title {% if show_course_count %} font-weight-bold {% endif %}">
{{ member.full_name }} {{ member.full_name }}
</div> </div>
{% set course_count = member.get_authored_courses() | length %} {% set course_count = get_authored_courses(member.name) | length %}
{% if show_course_count and course_count > 0 %} {% if show_course_count and course_count > 0 %}
{% set suffix = "Courses" if course_count > 1 else "Course" %} {% set suffix = "Courses" if course_count > 1 else "Course" %}
<div class="small-title"> <div class="small-title">

View File

@@ -1,11 +1,11 @@
{% if not course.upcoming %} {% if not course.upcoming %}
<div class="reviews-parent"> <div class="reviews-parent">
{% set reviews = course.get_reviews() %} {% set reviews = get_reviews(course.name) %}
<div class="mb-5"> <div class="mb-5">
<span class="course-home-headings">Reviews</span> <span class="course-home-headings"> {{ _("Reviews") }} </span>
{% if course.is_eligible_to_review(membership) and reviews | length %} {% if is_eligible_to_review(course.name, membership) and reviews | length %}
<span class="review-link button is-secondary pull-right"> <span class="review-link button is-secondary pull-right">
Write a review {{ _("Write a review") }}
</span> </span>
{% endif %} {% endif %}
</div> </div>
@@ -41,9 +41,9 @@
<img class="icon icon-xl" src="/assets/frappe/icons/timeless/message.svg"> <img class="icon icon-xl" src="/assets/frappe/icons/timeless/message.svg">
<div class="course-home-headings mt-4 mb-0" style="color: inherit;"> {{ _("Review the course") }} </div> <div class="course-home-headings mt-4 mb-0" style="color: inherit;"> {{ _("Review the course") }} </div>
<div class="small mb-6"> {{ _("Help us improve our course material.") }} </div> <div class="small mb-6"> {{ _("Help us improve our course material.") }} </div>
{% if course.is_eligible_to_review(membership) %} {% if is_eligible_to_review(course.name, membership) %}
<span class="review-link button is-secondary ml-auto mr-auto mt-3"> <span class="review-link button is-secondary ml-auto mr-auto mt-3">
Write a review {{ _("Write a review") }}
</span> </span>
{% elif frappe.session.user == "Guest" %} {% elif frappe.session.user == "Guest" %}
<a class="button is-secondary dark-links ml-auto mr-auto mt-3" href="/login?redirect-to=/courses/{{ course.name }}"> {{ _("Login") }} </a> <a class="button is-secondary dark-links ml-auto mr-auto mt-3" href="/login?redirect-to=/courses/{{ course.name }}"> {{ _("Login") }} </a>
@@ -59,7 +59,7 @@
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<div class="font-weight-bold">Write a Review</div> <div class="font-weight-bold">{{ _("Write a review") }}</div>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"> <button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
@@ -68,7 +68,7 @@
<form class="review-form" id="review-form"> <form class="review-form" id="review-form">
<div class="form-group"> <div class="form-group">
<div class="clearfix"> <div class="clearfix">
<label class="control-label reqd" style="padding-right: 0px;">Rating</label> <label class="control-label reqd" style="padding-right: 0px;">{{ _("Rating") }}</label>
</div> </div>
<div class="control-input-wrapper"> <div class="control-input-wrapper">
<div class="control-input"> <div class="control-input">
@@ -85,7 +85,7 @@
<div class="form-group"> <div class="form-group">
<div class="clearfix"> <div class="clearfix">
<label class="control-label reqd" style="padding-right: 0px;">Review</label> <label class="control-label reqd" style="padding-right: 0px;">{{ _("Review") }}</label>
</div> </div>
<div class="control-input-wrapper"> <div class="control-input-wrapper">
<div class="control-input"> <div class="control-input">
@@ -100,7 +100,7 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<div class="button submit-review is-primary" data-course="{{ course.name | urlencode}}" id="submit-review"> <div class="button submit-review is-primary" data-course="{{ course.name | urlencode}}" id="submit-review">
Submit</div> {{ _("Submit") }}</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -84,14 +84,7 @@ class CustomUser(User):
self.profile_complete = all_fields_have_value self.profile_complete = all_fields_have_value
def get_authored_courses(self) -> int:
"""Returns the number of courses authored by this user.
"""
return frappe.get_all(
'LMS Course', {
'instructor': self.name,
'is_published': True
})
def get_batch_count(self) -> int: def get_batch_count(self) -> int:
"""Returns the number of batches authored by this user. """Returns the number of batches authored by this user.
@@ -131,7 +124,8 @@ class CustomUser(User):
for map in mapping: for map in mapping:
if frappe.db.get_value("LMS Course", map.course, "is_published"): if frappe.db.get_value("LMS Course", map.course, "is_published"):
course = frappe.get_doc("LMS Course", map.course) course = frappe.db.get_value("LMS Course", map.course,
["name", "upcoming", "title", "image", "enable_certification"], as_dict=True)
mentored_courses.append(course) mentored_courses.append(course)
return mentored_courses return mentored_courses
@@ -141,7 +135,8 @@ class CustomUser(User):
completed = [] completed = []
memberships = self.get_course_membership("Student") memberships = self.get_course_membership("Student")
for membership in memberships: for membership in memberships:
course = frappe.get_doc("LMS Course", membership.course) course = frappe.db.get_value("LMS Course", membership.course,
["name", "upcoming", "title", "image", "enable_certification"], as_dict=True)
progress = cint(membership.progress) progress = cint(membership.progress)
if progress < 100: if progress < 100:
in_progress.append(course) in_progress.append(course)
@@ -152,6 +147,14 @@ class CustomUser(User):
"in_progress": in_progress, "in_progress": in_progress,
"completed": completed "completed": completed
} }
def get_authored_courses(member):
"""Returns the number of courses authored by this user.
"""
return frappe.get_all(
'LMS Course', {
'instructor': member,
'is_published': True
})
def get_palette(full_name): def get_palette(full_name):
""" """

View File

@@ -1,5 +1,6 @@
import frappe import frappe
from frappe.utils import rounded from frappe.utils import rounded
from school.www.utils import get_course_progress
def execute(): def execute():
frappe.reload_doc("lms", "doctype", "lms_batch_membership") frappe.reload_doc("lms", "doctype", "lms_batch_membership")
@@ -14,9 +15,8 @@ def execute():
if current_course != membership.course: if current_course != membership.course:
current_course = membership.course current_course = membership.course
course_details = frappe.get_doc("LMS Course", current_course) progress = rounded(get_course_progress(current_course, membership.member))
progress = rounded(course_details.get_course_progress(membership.member))
frappe.db.set_value("LMS Batch Membership", membership.name, "progress", progress) frappe.db.set_value("LMS Batch Membership", membership.name, "progress", progress)
frappe.db.delete("Prepared Report", {"ref_report_doctype": "Course Progress Summary"}) frappe.db.delete("Prepared Report", {"ref_report_doctype": "Course Progress Summary"})
frappe.db.set_value("Report", "Course Progress Summary", "prepared_report", 0) frappe.db.set_value("Report", "Course Progress Summary", "prepared_report", 0)

View File

@@ -79,7 +79,6 @@ input[type=checkbox] {
} }
.common-page-style { .common-page-style {
background: #F4F5F6;
padding-bottom: 5rem; padding-bottom: 5rem;
min-height: 60vh; min-height: 60vh;
padding-top: 3rem; padding-top: 3rem;
@@ -171,6 +170,10 @@ input[type=checkbox] {
margin-top: auto; margin-top: auto;
} }
.course-card-footer .avatar-group {
display: inherit;
}
.view-course-link { .view-course-link {
height: 32px; height: 32px;
border-radius: 4px; border-radius: 4px;

View File

@@ -40,7 +40,7 @@
{% if membership or is_instructor %} eligible-for-submission {% endif %}" data-lesson="{{ lesson.name }}" {% if membership or is_instructor %} eligible-for-submission {% endif %}" data-lesson="{{ lesson.name }}"
data-course="{{ course.name }}"> data-course="{{ course.name }}">
{{ lesson.title }} {{ lesson.title }}
<span class="lesson-progress {{hide if course.get_progress(lesson.name) != 'Complete' else ''}}">COMPLETED</span> <span class="lesson-progress {{hide if get_progress(course.name, lesson.name) != 'Complete' else ''}}">COMPLETED</span>
</div> </div>
@@ -52,7 +52,7 @@
<a href="#" class="close" data-dismiss="alert" aria-label="close">&times;</a> <a href="#" class="close" data-dismiss="alert" aria-label="close">&times;</a>
</small> </small>
{% endif %} {% endif %}
{{ lesson.render_html() }}</div> {{ render_html(lesson.body) }}</div>
{% else %} {% else %}
<div class="common-card-style lesson-content-card"> <div class="common-card-style lesson-content-card">
<div class="w-25 text-center" style="margin: 0 auto;"> <div class="w-25 text-center" style="margin: 0 auto;">
@@ -78,8 +78,8 @@
</div> </div>
{% if not course.is_mentor(frappe.session.user) and membership %} {% if not is_mentor(course.name, frappe.session.user) and membership %}
{% set progress = course.get_progress(lesson.name) %} {% set progress = get_progress(course.name, lesson.name) %}
<div class="custom-checkbox {% if progress == 'Complete' %} hide {% endif %}"> <div class="custom-checkbox {% if progress == 'Complete' %} hide {% endif %}">
<label class="quiz-label"> <label class="quiz-label">
<input class="option mark-progress" type="checkbox" checked> <input class="option mark-progress" type="checkbox" checked>

View File

@@ -1,28 +1,27 @@
from re import I from re import I
import frappe import frappe
from . import utils from school.www.utils import get_common_context, redirect_to_lesson, get_lesson_url
from frappe.utils import cstr from frappe.utils import cstr, flt
from school.www import batch from school.www import batch
def get_context(context): def get_context(context):
utils.get_common_context(context) get_common_context(context)
chapter_index = frappe.form_dict.get("chapter") chapter_index = frappe.form_dict.get("chapter")
lesson_index = frappe.form_dict.get("lesson") lesson_index = frappe.form_dict.get("lesson")
lesson_number = f"{chapter_index}.{lesson_index}" lesson_number = f"{chapter_index}.{lesson_index}"
if not chapter_index or not lesson_index: if not chapter_index or not lesson_index:
if context.batch: if context.batch:
index_ = get_lesson_index(context.course, context.batch, frappe.session.user) or "1.1" index_ = get_lesson_index(context.course, context.batch, frappe.session.user) or "1.1"
else: else:
index_ = "1.1" index_ = "1.1"
utils.redirect_to_lesson(context.course, index_) redirect_to_lesson(context.course, index_)
context.lesson = get_current_lesson_details(lesson_number, context) context.lesson = get_current_lesson_details(lesson_number, context)
neighbours = context.course.get_neighbours(lesson_number, context.lessons) neighbours = get_neighbours(lesson_number, context.lessons)
context.next_url = get_learn_url(neighbours["next"], context.course) context.next_url = get_url(neighbours["next"], context.course)
context.prev_url = get_learn_url(neighbours["prev"], context.course) context.prev_url = get_url(neighbours["prev"], context.course)
meta_info = context.lesson.title + " - " + context.course.title meta_info = context.lesson.title + " - " + context.course.title
context.metatags = { context.metatags = {
@@ -38,15 +37,16 @@ def get_context(context):
"lesson": context.lesson.name, "lesson": context.lesson.name,
"is_member": context.membership is not None "is_member": context.membership is not None
} }
print(context)
def get_current_lesson_details(lesson_number, context): def get_current_lesson_details(lesson_number, context):
details_list = list(filter(lambda x: cstr(x.number) == lesson_number, context.lessons)) details_list = list(filter(lambda x: cstr(x.number) == lesson_number, context.lessons))
if not len(details_list): if not len(details_list):
utils.redirect_to_lesson(context.course) redirect_to_lesson(context.course)
return details_list[0] return details_list[0]
def get_learn_url(lesson_number, course): def get_url(lesson_number, course):
return course.get_learn_url(lesson_number) and course.get_learn_url(lesson_number) + course.query_parameter return get_lesson_url(course.name, lesson_number) and get_lesson_url(course.name, lesson_number) + course.query_parameter
def get_lesson_index(course, batch, user): def get_lesson_index(course, batch, user):
lesson = batch.get_current_lesson(user) lesson = batch.get_current_lesson(user)
@@ -59,3 +59,12 @@ def get_page_extensions(context):
for e in extensions: for e in extensions:
e.set_context(context) e.set_context(context)
return extensions return extensions
def get_neighbours(current, lessons):
current = flt(current)
numbers = sorted(lesson.number for lesson in lessons)
index = numbers.index(current)
return {
"prev": numbers[index-1] if index-1 >= 0 else None,
"next": numbers[index+1] if index+1 < len(numbers) else None
}

View File

@@ -49,11 +49,11 @@
</div> </div>
{% endif %} {% endif %}
{% if membership %} {% if membership %}
{% set lesson_index = course.get_lesson_index(membership.current_lesson) if membership and {% set lesson_index = get_lesson_index(membership.current_lesson) if membership and
membership.current_lesson membership.current_lesson
else '1.1' %} else '1.1' %}
<a class="button wide-button is-primary" id="continue-learning" <a class="button wide-button is-primary" id="continue-learning"
href="{{ course.get_learn_url(lesson_index) }}{{ course.query_parameter }}"> href="{{ get_lesson_url(course.name, lesson_index) }}{{ course.query_parameter }}">
Continue Learning <img class="ml-2" src="/assets/school/icons/white-arrow.svg" /> Continue Learning <img class="ml-2" src="/assets/school/icons/white-arrow.svg" />
</a> </a>
{% endif %} {% endif %}
@@ -70,7 +70,7 @@
</div> </div>
{% endif %} {% endif %}
{% if course.is_cohort_staff(frappe.session.user) %} {% if is_cohort_staff(course.name, frappe.session.user) %}
<a class="button wide-button is-secondary" <a class="button wide-button is-secondary"
href="/courses/{{course.name}}/manage" href="/courses/{{course.name}}/manage"
style="color: inherit;"> style="color: inherit;">
@@ -129,14 +129,14 @@
<div class="course-home-headings"> <div class="course-home-headings">
Creator Creator
</div> </div>
{% for instructor in course.get_instructors() %} {% for instructor in get_instructors(course.name) %}
{{ widgets.MemberCard(member=instructor, show_course_count=True, avatar_class="avatar-large") }} {{ widgets.MemberCard(member=instructor, show_course_count=True, avatar_class="avatar-large") }}
{% endfor %} {% endfor %}
</div> </div>
{% endmacro %} {% endmacro %}
{% macro Progress(course) %} {% macro Progress(course) %}
{% set certificate = course.is_certified() %} {% set certificate = is_certified(course.name) %}
{% set progress = frappe.utils.cint(membership.progress) %} {% set progress = frappe.utils.cint(membership.progress) %}
{% if progress %} {% if progress %}
<div class="course-progress-section"> <div class="course-progress-section">
@@ -186,17 +186,17 @@
{% endmacro %} {% endmacro %}
{% macro Overview(course) %} {% macro Overview(course) %}
{% set avg_rating = course.get_average_rating() %} {% set avg_rating = get_average_rating(course.name) %}
{% if course.get_students() | length or avg_rating %} {% if get_students(course.name) | length or avg_rating %}
<div class="course-overview-section"> <div class="course-overview-section">
<div class="course-home-headings"> <div class="course-home-headings">
Overview Overview
</div> </div>
<div class="common-card-style overview-card small-title"> <div class="common-card-style overview-card small-title">
{% if course.get_students() | length %} {% if get_students(course.name) | length %}
<div class="overview-item"> <div class="overview-item">
<img class="icon-background mr-1" src="/assets/school/icons/user.svg" /> <img class="icon-background mr-1" src="/assets/school/icons/user.svg" />
{{ course.get_students() | length }} Enrolled {{ get_students(course.name) | length }} Enrolled
</div> </div>
{% endif %} {% endif %}
{% if avg_rating %} {% if avg_rating %}
@@ -213,13 +213,13 @@
<!-- Mentors --> <!-- Mentors -->
{% macro Mentors(course) %} {% macro Mentors(course) %}
{% if course.get_mentors() | length %} {% if get_mentors(course.name) | length %}
<div class="course-home-mentors"> <div class="course-home-mentors">
<div class="course-home-headings"> <div class="course-home-headings">
Mentors Mentors
</div> </div>
<div class="member-parent"> <div class="member-parent">
{% for mentor in course.get_mentors() %} {% for mentor in get_mentors(course.name) %}
{{ widgets.MemberCard(member=mentor, show_course_count=False) }} {{ widgets.MemberCard(member=mentor, show_course_count=False) }}
{% endfor %} {% endfor %}
</div> </div>

View File

@@ -1,5 +1,6 @@
import frappe import frappe
from school.lms.doctype.lms_settings.lms_settings import check_profile_restriction from school.lms.doctype.lms_settings.lms_settings import check_profile_restriction
from school.www.utils import get_membership
def get_context(context): def get_context(context):
context.no_cache = 1 context.no_cache = 1
@@ -16,7 +17,7 @@ def get_context(context):
raise frappe.Redirect raise frappe.Redirect
context.course = course context.course = course
membership = course.get_membership(frappe.session.user) membership = get_membership(course.name, frappe.session.user)
context.course.query_parameter = "?batch=" + membership.batch if membership and membership.batch else "" context.course.query_parameter = "?batch=" + membership.batch if membership and membership.batch else ""
context.membership = membership context.membership = membership
if context.course.upcoming: if context.course.upcoming:

View File

@@ -15,7 +15,7 @@ def get_context(context):
def get_courses(): def get_courses():
courses = frappe.get_all("LMS Course", courses = frappe.get_all("LMS Course",
filters={"is_published": True}, filters={"is_published": True},
fields=["name", "upcoming", "title", "image"]) fields=["name", "upcoming", "title", "image", "enable_certification"])
live_courses, upcoming_courses = [], [] live_courses, upcoming_courses = [], []
for course in courses: for course in courses:

View File

@@ -29,7 +29,7 @@
<div class="profile-info"> <div class="profile-info">
<div class="profile-name-section"> <div class="profile-name-section">
<div class="profile-name"> {{ member.full_name }} </div> <div class="profile-name"> {{ member.full_name }} </div>
{% if member.get_authored_courses() | length %} {% if get_authored_courses(member.name) | length %}
<div class="creator-badge"> Creator </div> <div class="creator-badge"> Creator </div>
{% endif %} {% endif %}
{% if member.looking_for_job %} {% if member.looking_for_job %}
@@ -104,14 +104,16 @@
{% macro CoursesCreated(member, read_only) %} {% macro CoursesCreated(member, read_only) %}
{% if member.get_authored_courses() | length %} {% set authored_courses = get_authored_courses(member.name) %}
{% if authored_courses | length %}
<div class="profile-courses"> <div class="profile-courses">
<div class="course-home-headings"> <div class="course-home-headings">
Courses Created Courses Created
</div> </div>
<div class="cards-parent"> <div class="cards-parent">
{% for course in member.get_authored_courses() %} {% for course in authored_courses %}
{% set course_details = frappe.get_doc("LMS Course", course) %} {% set course_details = frappe.db.get_value("LMS Course", course,
["name", "upcoming", "title", "image", "enable_certification"], as_dict=True) %}
{{ widgets.CourseCard(course=course_details, read_only=read_only) }} {{ widgets.CourseCard(course=course_details, read_only=read_only) }}
{% endfor %} {% endfor %}
</div> </div>

View File

@@ -1,26 +1,27 @@
import frappe import frappe
from school.lms.models import Course from frappe.utils import flt, cint
from frappe.utils import flt from school.lms.utils import slugify
from school.lms.md import markdown_to_html
def get_common_context(context): def get_common_context(context):
context.no_cache = 1 context.no_cache = 1
course_name = frappe.form_dict["course"]
try: try:
batch_name = frappe.form_dict["batch"] batch_name = frappe.form_dict["batch"]
except KeyError: except KeyError:
batch_name = None batch_name = None
course = frappe.get_doc("LMS Course", course_name) course = frappe.db.get_value("LMS Course",
frappe.form_dict["course"], ["name", "title", "video_link"], as_dict=True)
if not course: if not course:
context.template = "www/404.html" context.template = "www/404.html"
return return
context.course = course context.course = course
context.lessons = course.get_lessons() context.lessons = get_lessons(course.name)
membership = get_membership(course_name, frappe.session.user, batch_name) membership = get_membership(course.name, frappe.session.user, batch_name)
context.membership = membership context.membership = membership
if membership: if membership:
batch = course.get_batch(membership.batch) batch = get_batch(course.name, membership.batch)
if batch: if batch:
context.batch = batch context.batch = batch
@@ -32,7 +33,7 @@ def get_livecode_url():
return frappe.db.get_single_value("LMS Settings", "livecode_url") return frappe.db.get_single_value("LMS Settings", "livecode_url")
def redirect_to_lesson(course, index_="1.1"): def redirect_to_lesson(course, index_="1.1"):
frappe.local.flags.redirect_location = course.get_learn_url(index_) + course.query_parameter frappe.local.flags.redirect_location = get_lesson_url(course.name, index_) + course.query_parameter
raise frappe.Redirect raise frappe.Redirect
def get_membership(course, member, batch=None): def get_membership(course, member, batch=None):
@@ -55,16 +56,18 @@ def get_membership(course, member, batch=None):
def get_chapters(course): def get_chapters(course):
"""Returns all chapters of this course. """Returns all chapters of this course.
""" """
chapters = frappe.get_all("Course Chapter", chapters = frappe.get_all("Chapter Reference", {"parent": course},
{ "course": course }, ["idx", "chapter"], order_by="idx")
["name", "title", "description", "idx"]) for chapter in chapters:
chapter_details = frappe.db.get_value("Course Chapter", { "name": chapter.chapter },
["name", "title", "description"], as_dict=True)
chapter.update(chapter_details)
return chapters return chapters
def get_lessons(course, chapter=None): def get_lessons(course, chapter=None):
""" If chapter is passed, returns lessons of only that chapter. """ If chapter is passed, returns lessons of only that chapter.
Else returns lessons of all chapters of the course """ Else returns lessons of all chapters of the course """
lessons = [] lessons = []
if chapter: if chapter:
return get_lesson_details(chapter) return get_lesson_details(chapter)
@@ -82,7 +85,8 @@ def get_lesson_details(chapter):
order_by="idx") order_by="idx")
for row in lesson_list: for row in lesson_list:
lesson_details = frappe.db.get_value("Course Lesson", row.lesson, "name", as_dict=True) lesson_details = frappe.db.get_value("Course Lesson", row.lesson,
["name", "title", "include_in_preview", "body"], as_dict=True)
lesson_details.number = flt("{}.{}".format(chapter.idx, row.idx)) lesson_details.number = flt("{}.{}".format(chapter.idx, row.idx))
lessons.append(lesson_details) lessons.append(lesson_details)
return lessons return lessons
@@ -91,11 +95,171 @@ def get_tags(course):
tags = frappe.db.get_value("LMS Course", course, "tags") tags = frappe.db.get_value("LMS Course", course, "tags")
return tags.split(",") if tags else [] return tags.split(",") if tags else []
def get_instructors(self): def get_instructors(course):
instructors = [] instructor_details = []
if self.instructors: instructors = frappe.get_all("Course Instructor", {"parent": course},
for instructor in self.instructors: ["instructor"], order_by="idx")
instructors.append(frappe.get_doc("User", instructor.instructor)) if not instructors:
else: instructors = frappe.db.get_value("LMS Course", course, "owner").split(" ")
instructors.append(frappe.get_doc("User", self.owner)) for instructor in instructors:
return instructors instructor_details.append(frappe.db.get_value("User",
instructor.instructor,
["name", "username", "full_name", "user_image"],
as_dict=True))
return instructor_details
def get_students(course, batch=None):
"""Returns (email, full_name, username) of all the students of this batch as a list of dict.
"""
filters = {
"course": course,
"member_type": "Student"
}
if batch:
filters["batch"] = batch
return frappe.get_all(
"LMS Batch Membership",
filters,
["member"])
def get_average_rating(course):
ratings = [review.rating for review in get_reviews(course)]
if not len(ratings):
return None
return sum(ratings)/len(ratings)
def get_reviews(course):
reviews = frappe.get_all("LMS Course Review",
{
"course": course
},
["review", "rating", "owner"],
order_by= "creation desc")
out_of_ratings = frappe.db.get_all("DocField",
{
"parent": "LMS Course Review",
"fieldtype": "Rating"
},
["options"])
out_of_ratings = (len(out_of_ratings) and out_of_ratings[0].options) or 5
for review in reviews:
review.rating = review.rating * out_of_ratings
review.owner_details = frappe.db.get_value("User",
review.owner, ["name", "username", "full_name", "user_image"], as_dict=True)
return reviews
def is_certified(course):
certificate = frappe.get_all("LMS Certification",
{
"student": frappe.session.user,
"course": course
})
if len(certificate):
return certificate[0].name
return
def get_lesson_index(lesson_name):
"""Returns the {chapter_index}.{lesson_index} for the lesson.
"""
lesson = frappe.db.get_value("Lesson Reference",
{"lesson": lesson_name}, ["idx", "parent"], as_dict=True)
if not lesson:
return None
chapter = frappe.db.get_value("Chapter Reference",
{"chapter": lesson.parent}, ["idx"], as_dict=True)
if not chapter:
return None
return f"{chapter.idx}.{lesson.idx}"
def get_lesson_url(course, lesson_number):
if not lesson_number:
return
return f"/courses/{course}/learn/{lesson_number}"
def get_batch(course, batch_name):
return frappe.get_all("LMS Batch", {"name": batch_name, "course": course})
def get_slugified_chapter_title(chapter):
return slugify(chapter)
def get_progress(course, lesson):
return frappe.db.get_value("LMS Course Progress", {
"course": course,
"owner": frappe.session.user,
"lesson": lesson
},
["status"])
def render_html(body):
return markdown_to_html(body)
def is_mentor(course, email):
"""Checks if given user is a mentor for this course.
"""
if not email:
return False
return frappe.db.count("LMS Course Mentor Mapping",
{
"course": course,
"mentor": email
})
def is_cohort_staff(course, user_email):
"""Returns True if the user is either a mentor or a staff for one or more active cohorts of this course.
"""
staff = {
"doctype": "Cohort Staff",
"course": course,
"email": user_email
}
mentor = {
"doctype": "Cohort Mentor",
"course": course,
"email": user_email
}
return frappe.db.exists(staff) or frappe.db.exists(mentor)
def get_mentors(course):
"""Returns the list of all mentors for this course.
"""
course_mentors = []
mentors = frappe.get_all("LMS Course Mentor Mapping", {"course": course}, ["mentor"])
for mentor in mentors:
member = frappe.db.get_value("User", mentor.mentor,
["name", "username", "full_name", "user_image"])
member.batch_count = frappe.db.count("LMS Batch Membership",
{
"member": member.name,
"member_type": "Mentor"
})
course_mentors.append(member)
return course_mentors
def is_eligible_to_review(course, membership):
""" Checks if user is eligible to review the course """
if not membership:
return False
if frappe.db.count("LMS Course Review",
{
"course": course,
"owner": frappe.session.user
}):
return False
return True
def get_course_progress(course, member=None):
""" Returns the course progress of the session user """
lesson_count = len(get_lessons(course))
if not lesson_count:
return 0
completed_lessons = frappe.db.count("LMS Course Progress",
{
"course": course,
"owner": member or frappe.session.user,
"status": "Complete"
})
precision = cint(frappe.db.get_default("float_precision")) or 3
return flt(((completed_lessons/lesson_count) * 100), precision)