Merge pull request #297 from pateljannat/redesign-school
This commit is contained in:
@@ -1,14 +1,14 @@
|
|||||||
{% set color = member.get_palette() %}
|
{% 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>
|
||||||
|
|||||||
@@ -161,7 +161,27 @@ 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.lms.utils.get_membership",
|
||||||
|
"school.lms.utils.get_lessons",
|
||||||
|
"school.lms.utils.get_tags",
|
||||||
|
"school.lms.utils.get_instructors",
|
||||||
|
"school.lms.utils.get_students",
|
||||||
|
"school.lms.utils.get_average_rating",
|
||||||
|
"school.lms.utils.is_certified",
|
||||||
|
"school.lms.utils.get_lesson_index",
|
||||||
|
"school.lms.utils.get_lesson_url",
|
||||||
|
"school.lms.utils.get_chapters",
|
||||||
|
"school.lms.utils.get_slugified_chapter_title",
|
||||||
|
"school.lms.utils.get_progress",
|
||||||
|
"school.lms.utils.render_html",
|
||||||
|
"school.lms.utils.is_mentor",
|
||||||
|
"school.lms.utils.is_cohort_staff",
|
||||||
|
"school.lms.utils.get_mentors",
|
||||||
|
"school.lms.utils.get_reviews",
|
||||||
|
"school.lms.utils.is_eligible_to_review",
|
||||||
],
|
],
|
||||||
"filters": []
|
"filters": []
|
||||||
}
|
}
|
||||||
|
|||||||
0
school/lms/doctype/course_instructor/__init__.py
Normal file
0
school/lms/doctype/course_instructor/__init__.py
Normal file
32
school/lms/doctype/course_instructor/course_instructor.json
Normal file
32
school/lms/doctype/course_instructor/course_instructor.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"creation": "2022-02-07 11:39:59.998762",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"instructor"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "instructor",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Instructor",
|
||||||
|
"options": "User"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2022-02-07 11:41:42.943250",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "LMS",
|
||||||
|
"name": "Course Instructor",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Copyright (c) 2022, Frappe and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
class CourseInstructor(Document):
|
||||||
|
pass
|
||||||
@@ -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.lms.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
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
from school.lms.utils import get_membership
|
||||||
|
|
||||||
class Exercise(Document):
|
class Exercise(Document):
|
||||||
def get_user_submission(self):
|
def get_user_submission(self):
|
||||||
@@ -35,8 +36,7 @@ class Exercise(Document):
|
|||||||
if old_submission and old_submission.solution == code:
|
if old_submission and old_submission.solution == code:
|
||||||
return old_submission
|
return old_submission
|
||||||
|
|
||||||
course = frappe.get_doc("LMS Course", self.course)
|
member = get_membership(self.course, frappe.session.user)
|
||||||
member = course.get_membership(frappe.session.user)
|
|
||||||
|
|
||||||
doc = frappe.get_doc(
|
doc = frappe.get_doc(
|
||||||
doctype="Exercise Submission",
|
doctype="Exercise Submission",
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ from frappe.model.document import Document
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from school.lms.doctype.lms_batch_membership.lms_batch_membership import create_membership
|
from school.lms.doctype.lms_batch_membership.lms_batch_membership import create_membership
|
||||||
from school.query import find, find_all
|
from school.query import find, find_all
|
||||||
|
from school.lms.utils import is_mentor
|
||||||
|
|
||||||
class LMSBatch(Document):
|
class LMSBatch(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
self.validate_if_mentor()
|
self.validate_if_mentor()
|
||||||
|
|
||||||
def validate_if_mentor(self):
|
def validate_if_mentor(self):
|
||||||
course = frappe.get_doc("LMS Course", self.course)
|
if not is_mentor(self.course, frappe.session.user):
|
||||||
if not course.is_mentor(frappe.session.user):
|
|
||||||
frappe.throw(_("You are not a mentor of the course {0}").format(course.title))
|
frappe.throw(_("You are not a mentor of the course {0}").format(course.title))
|
||||||
|
|
||||||
def after_insert(self):
|
def after_insert(self):
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class TestLMSBatchMembership(unittest.TestCase):
|
|||||||
"short_introduction": "Test Course",
|
"short_introduction": "Test Course",
|
||||||
"description": "Test Course"
|
"description": "Test Course"
|
||||||
})
|
})
|
||||||
course.insert()
|
course.insert(ignore_permissions=True)
|
||||||
|
|
||||||
self.new_user("mentor@test.com", "Test Mentor")
|
self.new_user("mentor@test.com", "Test Mentor")
|
||||||
# without this, the creating batch will fail
|
# without this, the creating batch will fail
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"video_link",
|
"video_link",
|
||||||
"image",
|
"image",
|
||||||
"column_break_3",
|
"column_break_3",
|
||||||
"instructor",
|
"instructors",
|
||||||
"tags",
|
"tags",
|
||||||
"section_break_7",
|
"section_break_7",
|
||||||
"is_published",
|
"is_published",
|
||||||
@@ -104,12 +104,11 @@
|
|||||||
"options": "Chapter Reference"
|
"options": "Chapter Reference"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "instructor",
|
"fieldname": "instructors",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Table MultiSelect",
|
||||||
"in_list_view": 1,
|
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Instructor",
|
"label": "Instructors",
|
||||||
"options": "User"
|
"options": "Course Instructor"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "section_break_7",
|
"fieldname": "section_break_7",
|
||||||
@@ -168,7 +167,7 @@
|
|||||||
"link_fieldname": "course"
|
"link_fieldname": "course"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2021-09-30 10:36:48.759995",
|
"modified": "2022-02-07 11:41:39.735325",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Course",
|
"name": "LMS Course",
|
||||||
@@ -190,6 +189,7 @@
|
|||||||
"search_fields": "title",
|
"search_fields": "title",
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
"title_field": "title",
|
"title_field": "title",
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# Copyright (c) 2021, Frappe and contributors
|
||||||
# Copyright (c) 2021, FOSS United and contributors
|
|
||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
@@ -9,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.lms.utils import get_chapters
|
||||||
|
|
||||||
class LMSCourse(Document):
|
class LMSCourse(Document):
|
||||||
|
|
||||||
@@ -99,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.
|
||||||
@@ -143,53 +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_instructor(self):
|
|
||||||
if self.instructor:
|
|
||||||
return frappe.get_doc("User", self.instructor)
|
|
||||||
return frappe.get_doc("User", self.owner)
|
|
||||||
|
|
||||||
def get_chapters(self):
|
|
||||||
"""Returns all chapters of this course.
|
|
||||||
"""
|
|
||||||
chapters = []
|
|
||||||
for row in self.chapters:
|
|
||||||
chapter_details = frappe.db.get_value("Course Chapter", row.chapter,
|
|
||||||
["name", "title", "description"],
|
|
||||||
as_dict=True)
|
|
||||||
chapter_details.idx = row.idx
|
|
||||||
chapters.append(chapter_details)
|
|
||||||
return chapters
|
|
||||||
|
|
||||||
def get_lessons(self, chapter=None):
|
|
||||||
""" If chapter is passed, returns lessons of only that chapter.
|
|
||||||
Else returns lessons of all chapters of the course """
|
|
||||||
lessons = []
|
|
||||||
|
|
||||||
if chapter:
|
|
||||||
return self.get_lesson_details(chapter)
|
|
||||||
|
|
||||||
for chapter in self.get_chapters():
|
|
||||||
lesson = self.get_lesson_details(chapter)
|
|
||||||
lessons += lesson
|
|
||||||
|
|
||||||
return lessons
|
|
||||||
|
|
||||||
def get_lesson_details(self, chapter):
|
|
||||||
lessons = []
|
|
||||||
lesson_list = frappe.get_all("Lesson Reference", {"parent": chapter.name},
|
|
||||||
["lesson", "idx"], order_by="idx")
|
|
||||||
for row in lesson_list:
|
|
||||||
lesson_details = frappe.get_doc("Course Lesson", row.lesson)
|
|
||||||
lesson_details.number = flt("{}.{}".format(chapter.idx, row.idx))
|
|
||||||
lessons.append(lesson_details)
|
|
||||||
return lessons
|
|
||||||
|
|
||||||
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:
|
||||||
@@ -201,15 +129,6 @@ class LMSCourse(Document):
|
|||||||
batch_names = {m.batch for m in memberships}
|
batch_names = {m.batch for m in memberships}
|
||||||
return [b for b in batches if b.name in batch_names]
|
return [b for b in batches if b.name in batch_names]
|
||||||
|
|
||||||
def get_upcoming_batches(self):
|
|
||||||
now = frappe.utils.nowdate()
|
|
||||||
batches = find_all("LMS Batch",
|
|
||||||
course=self.name,
|
|
||||||
start_date=[">", now],
|
|
||||||
status="Active",
|
|
||||||
visibility="Public")
|
|
||||||
return batches
|
|
||||||
|
|
||||||
def get_cohorts(self):
|
def get_cohorts(self):
|
||||||
return find_all("Cohort", course=self.name, order_by="creation")
|
return find_all("Cohort", course=self.name, order_by="creation")
|
||||||
|
|
||||||
@@ -217,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):
|
||||||
@@ -258,133 +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_membership(self, member, batch=None):
|
|
||||||
filters = {
|
|
||||||
"member": member,
|
|
||||||
"course": self.name
|
|
||||||
}
|
|
||||||
if batch:
|
|
||||||
filters["batch"] = batch
|
|
||||||
|
|
||||||
membership = frappe.db.get_value("LMS Batch Membership",
|
|
||||||
filters,
|
|
||||||
["name", "batch", "current_lesson", "member_type", "progress"],
|
|
||||||
as_dict=True)
|
|
||||||
|
|
||||||
if membership and membership.batch:
|
|
||||||
membership.batch_title = frappe.db.get_value("LMS Batch", membership.batch, "title")
|
|
||||||
return membership
|
|
||||||
|
|
||||||
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_tags(self):
|
|
||||||
return self.tags.split(",") if self.tags else []
|
|
||||||
|
|
||||||
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)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class TestLMSCourse(unittest.TestCase):
|
|||||||
"short_introduction": title,
|
"short_introduction": title,
|
||||||
"description": title
|
"description": title
|
||||||
})
|
})
|
||||||
doc.insert()
|
doc.insert(ignore_permissions=True)
|
||||||
return doc
|
return doc
|
||||||
|
|
||||||
def test_new_course(self):
|
def test_new_course(self):
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Copyright (c) 2021, FOSS United and contributors
|
# Copyright (c) 2021, Frappe and contributors
|
||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class TestLMSQuiz(unittest.TestCase):
|
|||||||
frappe.get_doc({
|
frappe.get_doc({
|
||||||
"doctype": "LMS Quiz",
|
"doctype": "LMS Quiz",
|
||||||
"title": "Test Quiz"
|
"title": "Test Quiz"
|
||||||
}).save()
|
}).save(ignore_permissions=True)
|
||||||
|
|
||||||
def test_with_multiple_options(self):
|
def test_with_multiple_options(self):
|
||||||
quiz = frappe.get_doc("LMS Quiz", "Test Quiz")
|
quiz = frappe.get_doc("LMS Quiz", "Test Quiz")
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import re
|
import re
|
||||||
|
import frappe
|
||||||
|
from frappe.utils import flt, cint
|
||||||
|
from school.lms.md import markdown_to_html
|
||||||
|
|
||||||
RE_SLUG_NOTALLOWED = re.compile("[^a-z0-9]+")
|
RE_SLUG_NOTALLOWED = re.compile("[^a-z0-9]+")
|
||||||
|
|
||||||
@@ -28,3 +31,230 @@ def slugify(title, used_slugs=[]):
|
|||||||
return new_slug
|
return new_slug
|
||||||
count = count+1
|
count = count+1
|
||||||
|
|
||||||
|
def get_membership(course, member, batch=None):
|
||||||
|
filters = {
|
||||||
|
"member": member,
|
||||||
|
"course": course
|
||||||
|
}
|
||||||
|
if batch:
|
||||||
|
filters["batch"] = batch
|
||||||
|
|
||||||
|
membership = frappe.db.get_value("LMS Batch Membership",
|
||||||
|
filters,
|
||||||
|
["name", "batch", "current_lesson", "member_type", "progress"],
|
||||||
|
as_dict=True)
|
||||||
|
|
||||||
|
if membership and membership.batch:
|
||||||
|
membership.batch_title = frappe.db.get_value("LMS Batch", membership.batch, "title")
|
||||||
|
return membership
|
||||||
|
|
||||||
|
def get_chapters(course):
|
||||||
|
"""Returns all chapters of this course.
|
||||||
|
"""
|
||||||
|
chapters = frappe.get_all("Chapter Reference", {"parent": course},
|
||||||
|
["idx", "chapter"], order_by="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
|
||||||
|
|
||||||
|
def get_lessons(course, chapter=None):
|
||||||
|
""" If chapter is passed, returns lessons of only that chapter.
|
||||||
|
Else returns lessons of all chapters of the course """
|
||||||
|
lessons = []
|
||||||
|
if chapter:
|
||||||
|
return get_lesson_details(chapter)
|
||||||
|
|
||||||
|
for chapter in get_chapters(course):
|
||||||
|
lesson = get_lesson_details(chapter)
|
||||||
|
lessons += lesson
|
||||||
|
|
||||||
|
return lessons
|
||||||
|
|
||||||
|
def get_lesson_details(chapter):
|
||||||
|
lessons = []
|
||||||
|
lesson_list = frappe.get_all("Lesson Reference",
|
||||||
|
{"parent": chapter.name},
|
||||||
|
["lesson", "idx"],
|
||||||
|
order_by="idx")
|
||||||
|
|
||||||
|
for row in lesson_list:
|
||||||
|
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))
|
||||||
|
lessons.append(lesson_details)
|
||||||
|
return lessons
|
||||||
|
|
||||||
|
def get_tags(course):
|
||||||
|
tags = frappe.db.get_value("LMS Course", course, "tags")
|
||||||
|
return tags.split(",") if tags else []
|
||||||
|
|
||||||
|
def get_instructors(course):
|
||||||
|
instructor_details = []
|
||||||
|
instructors = frappe.get_all("Course Instructor", {"parent": course},
|
||||||
|
["instructor"], order_by="idx")
|
||||||
|
if not instructors:
|
||||||
|
instructors = frappe.db.get_value("LMS Course", course, "owner").split(" ")
|
||||||
|
for instructor in 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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|
||||||
|
|||||||
@@ -1,61 +1,73 @@
|
|||||||
{% set membership = course.get_membership(frappe.session.user) %}
|
{% set membership = get_membership(course.name, frappe.session.user) %}
|
||||||
{% set progress = frappe.utils.cint(membership.progress) %}
|
{% set progress = frappe.utils.cint(membership.progress) %}
|
||||||
<div class="common-card-style course-card" data-course="{{ course.name }}">
|
<div class="common-card-style course-card" data-course="{{ course.name }}">
|
||||||
|
|
||||||
<div class="course-image {% if not course.image %}default-image{% endif %}" {% if course.image %}
|
<div class="course-image {% if not course.image %}default-image{% endif %}" {% if course.image %}
|
||||||
style="background-image: url( {{ course.image }} );" {% endif %}>
|
style="background-image: url( {{ course.image }} );" {% endif %}>
|
||||||
<div class="course-tags">
|
<div class="course-tags">
|
||||||
{% for tag in course.get_tags() %}
|
{% for tag in get_tags(course.name) %}
|
||||||
<div class="course-card-pills">{{ tag }}</div>
|
<div class="course-card-pills">{{ tag }}</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if membership and not read_only %}
|
|
||||||
{% if progress < 100 %} <div class="course-card-pills dark-pills ml-auto">{{ frappe.utils.rounded(progress) }}%
|
|
||||||
{{ _("Completed") }}
|
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% if not course.image %}
|
||||||
<div class="course-card-pills dark-pills ml-auto"> <img src="/assets/school/icons/check.svg"> {{ _("Completed")
|
<div class="default-image-text">{{ course.title[0] }}</div>
|
||||||
}}</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% if not course.image %}
|
|
||||||
<div class="default-image-text">{{ course.title[0] }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="course-card-content">
|
<div class="course-card-content">
|
||||||
<div class="course-card-meta muted-text">
|
<div class="course-card-meta">
|
||||||
{% if course.get_chapters() | length %}
|
{% if get_lessons(course.name) | length %}
|
||||||
<span>
|
<span>
|
||||||
{{ course.get_chapters() | length }} {{ _("Chapters") }}
|
{{ get_lessons(course.name) | length }} {{ _("Lessons") }}
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if course.get_chapters() | length and course.get_upcoming_batches() | length %}
|
|
||||||
<span class="font-weight-bold ml-3 mr-3"> . </span>
|
|
||||||
{% endif %}
|
|
||||||
{% if course.get_upcoming_batches() | length %}
|
|
||||||
<span class="">
|
|
||||||
{{ course.get_upcoming_batches() | length }} {{ _("Open Batches") }}
|
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-heading course-card-title">{{ course.title }}</div>
|
<div class="card-heading course-card-title">{{ course.title }}</div>
|
||||||
<div {% if not read_only %} class="mb-4" {% endif %}>
|
|
||||||
<span class="zindex">
|
{% if membership and not read_only %}
|
||||||
{{ widgets.Avatar(member=course.get_instructor(), avatar_class="avatar-small") }}
|
<div class="progress">
|
||||||
<a class="button-links" href="{{ get_profile_url(course.get_instructor().username) }}">
|
<div class="progress-bar" role="progressbar" aria-valuenow="{{ progress }}"
|
||||||
|
aria-valuemin="0" aria-valuemax="100" style="width:{{ progress }}%">
|
||||||
|
<span class="sr-only"> {{ progress }} Complete</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="progress-percent">{{ progress }}% Completed</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="zindex course-card-footer">
|
||||||
|
<span class="">
|
||||||
|
{% set instructors = get_instructors(course.name) %}
|
||||||
|
{% set ins_len = instructors | length %}
|
||||||
|
{% for instructor in instructors %}
|
||||||
|
{% if ins_len > 1 and loop.index == 1 %}
|
||||||
|
<div class="avatar-group overlap">
|
||||||
|
{% endif %}
|
||||||
|
{{ widgets.Avatar(member=instructor, avatar_class="avatar-small") }}
|
||||||
|
|
||||||
|
{% if ins_len > 1 and loop.index == ins_len %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
<a class="button-links" href="{{ get_profile_url(instructors[0].username) }}">
|
||||||
<span class="course-instructor">
|
<span class="course-instructor">
|
||||||
{{ course.get_instructor().full_name }}
|
{% if ins_len == 1 %}
|
||||||
|
{{ instructors[0].full_name }}
|
||||||
|
{% else %}
|
||||||
|
{% set suffix = "other" if ins_len - 1 == 1 else "others" %}
|
||||||
|
{{ instructors[0].full_name.split(" ")[0] }} and {{ ins_len - 1 }} {{ suffix }}
|
||||||
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
|
{% 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" />
|
||||||
@@ -69,45 +81,28 @@
|
|||||||
<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 %}
|
||||||
<div class="view-course-link is-default">
|
|
||||||
{{ _("Get Certificate") }}
|
|
||||||
</div>
|
|
||||||
<a class="stretched-link" href="/courses/{{ course.name }}/{{ certificate }}"></a>
|
<a class="stretched-link" href="/courses/{{ course.name }}/{{ certificate }}"></a>
|
||||||
|
|
||||||
{% elif course.enable_certification and progress == 100 %}
|
{% elif course.enable_certification and progress == 100 %}
|
||||||
<div class="view-course-link is-default" id="certification" data-course="{{ course.name }}">
|
<a class="stretched-link" id="certification" data-course="{{ course.name }}"></a>
|
||||||
{{ _("Get Certificate") }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% elif progress == 100 %}
|
{% elif progress == 100 %}
|
||||||
<div class="view-course-link is-default">
|
|
||||||
{{ _("Course Completed") }}
|
|
||||||
</div>
|
|
||||||
<a class="stretched-link" href="/courses/{{ course.name }}"></a>
|
<a class="stretched-link" href="/courses/{{ course.name }}"></a>
|
||||||
|
|
||||||
{% elif course.upcoming %}
|
{% elif course.upcoming %}
|
||||||
<div class="view-course-link is-secondary border">
|
|
||||||
{{ _("Upcoming Course") }}
|
|
||||||
</div>
|
|
||||||
<a class="stretched-link" href="/courses/{{ course.name }}"></a>
|
<a class="stretched-link" href="/courses/{{ course.name }}"></a>
|
||||||
|
|
||||||
{% elif membership %}
|
{% elif membership %}
|
||||||
<div class="view-course-link is-primary">
|
<a class="stretched-link" href="{{ get_lesson_url(course.name, lesson_index) }}{{ query_parameter }}"></a>
|
||||||
{{ _("Continue Course") }}
|
|
||||||
</div>
|
|
||||||
<a class="stretched-link" href="{{ course.get_learn_url(lesson_index) }}{{ query_parameter }}"></a>
|
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="view-course-link is-default">
|
|
||||||
{{ _("View Course") }}
|
|
||||||
</div>
|
|
||||||
<a class="stretched-link" href="/courses/{{ course.name }}"></a>
|
<a class="stretched-link" href="/courses/{{ course.name }}"></a>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -117,21 +112,12 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
frappe.ready(() => {
|
frappe.ready(() => {
|
||||||
trim_course_titles();
|
|
||||||
|
|
||||||
$("#certification").unbind().click((e) => {
|
$("#certification").unbind().click((e) => {
|
||||||
create_certificate(e);
|
create_certificate(e);
|
||||||
});
|
});
|
||||||
})
|
|
||||||
|
|
||||||
var trim_course_titles = () => {
|
})
|
||||||
$(".course-card-title").each((i, element) => {
|
|
||||||
var title = $(element).text();
|
|
||||||
var length = $(window).width() <= 375 ? 60 : 65;
|
|
||||||
var suffix = title.length > length ? "..." : "";
|
|
||||||
$(element).text(title.substring(0, length) + suffix);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var create_certificate = (e) => {
|
var create_certificate = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
<div class="course-teaser">
|
|
||||||
<div class="course-body">
|
|
||||||
<h3 class="course-title"><a href="/courses/{{ course.name }}">{{ course.title }}</a></h3>
|
|
||||||
<div class="course-intro">
|
|
||||||
{{ course.short_introduction or "" }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="course-footer">
|
|
||||||
{% set batch = course.get_student_batch(frappe.session.user) %}
|
|
||||||
{% if batch %}
|
|
||||||
<a class="btn btn-secondary pull-right" href="/courses/{{course.name}}/{{batch.name}}/learn">Resume Course</a>
|
|
||||||
{% endif %}
|
|
||||||
<div class="course-author">
|
|
||||||
{% with author = course.get_instructor() %}
|
|
||||||
{{ widgets.Avatar(member=author, avatar_class="avatar-medium") }} <a href="{{get_profile_url(author.username)}}">{{ author.full_name }}</a>
|
|
||||||
{% endwith %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">×</span>
|
<span aria-hidden="true">×</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>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class TestCustomUser(unittest.TestCase):
|
|||||||
"doctype": "User",
|
"doctype": "User",
|
||||||
"email": "test_with_basic_username@example.com",
|
"email": "test_with_basic_username@example.com",
|
||||||
"first_name": "Username"
|
"first_name": "Username"
|
||||||
}).insert()
|
}).insert(ignore_permissions=True)
|
||||||
self.assertEqual(new_user.username, "username")
|
self.assertEqual(new_user.username, "username")
|
||||||
|
|
||||||
def test_without_username(self):
|
def test_without_username(self):
|
||||||
@@ -23,7 +23,7 @@ class TestCustomUser(unittest.TestCase):
|
|||||||
"doctype": "User",
|
"doctype": "User",
|
||||||
"email": "test-without-username@example.com",
|
"email": "test-without-username@example.com",
|
||||||
"first_name": "Username"
|
"first_name": "Username"
|
||||||
}).insert()
|
}).insert(ignore_permissions=True)
|
||||||
self.assertTrue(new_user.username)
|
self.assertTrue(new_user.username)
|
||||||
|
|
||||||
def test_with_illegal_characters(self):
|
def test_with_illegal_characters(self):
|
||||||
@@ -31,7 +31,7 @@ class TestCustomUser(unittest.TestCase):
|
|||||||
"doctype": "User",
|
"doctype": "User",
|
||||||
"email": "test_with_illegal_characters@example.com",
|
"email": "test_with_illegal_characters@example.com",
|
||||||
"first_name": "Username$$"
|
"first_name": "Username$$"
|
||||||
}).insert()
|
}).insert(ignore_permissions=True)
|
||||||
self.assertEqual(new_user.username[:8], "username")
|
self.assertEqual(new_user.username[:8], "username")
|
||||||
|
|
||||||
def test_with_underscore_at_end(self):
|
def test_with_underscore_at_end(self):
|
||||||
@@ -39,7 +39,7 @@ class TestCustomUser(unittest.TestCase):
|
|||||||
"doctype": "User",
|
"doctype": "User",
|
||||||
"email": "test_with_underscore_at_end@example.com",
|
"email": "test_with_underscore_at_end@example.com",
|
||||||
"first_name": "Username___"
|
"first_name": "Username___"
|
||||||
}).insert()
|
}).insert(ignore_permissions=True)
|
||||||
self.assertNotEqual(new_user.username[-1], "_")
|
self.assertNotEqual(new_user.username[-1], "_")
|
||||||
|
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ class TestCustomUser(unittest.TestCase):
|
|||||||
"doctype": "User",
|
"doctype": "User",
|
||||||
"email": "test_with_short_first_name@example.com",
|
"email": "test_with_short_first_name@example.com",
|
||||||
"first_name": "USN"
|
"first_name": "USN"
|
||||||
}).insert()
|
}).insert(ignore_permissions=True)
|
||||||
self.assertGreaterEqual(len(new_user.username), 4)
|
self.assertGreaterEqual(len(new_user.username), 4)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -84,35 +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_palette(self):
|
|
||||||
"""
|
|
||||||
Returns a color unique to each member for Avatar """
|
|
||||||
|
|
||||||
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 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.
|
||||||
@@ -152,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
|
||||||
@@ -162,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)
|
||||||
@@ -173,6 +147,35 @@ 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):
|
||||||
|
"""
|
||||||
|
Returns a color unique to each member for Avatar """
|
||||||
|
|
||||||
|
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(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]
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
def sign_up(email, full_name, verify_terms):
|
def sign_up(email, full_name, verify_terms):
|
||||||
|
|||||||
@@ -21,3 +21,4 @@ execute:frappe.delete_doc("DocType", "LMS Topic") #06-10-2021
|
|||||||
school.patches.v0_0.add_progress_to_membership #20-10-2021
|
school.patches.v0_0.add_progress_to_membership #20-10-2021
|
||||||
execute:frappe.delete_doc("Workspace", "LMS", ignore_missing=True, force=True) #24-10-2021
|
execute:frappe.delete_doc("Workspace", "LMS", ignore_missing=True, force=True) #24-10-2021
|
||||||
execute:frappe.delete_doc("Custom Field", "User-verify_age", ignore_missing=True, force=True)
|
execute:frappe.delete_doc("Custom Field", "User-verify_age", ignore_missing=True, force=True)
|
||||||
|
school.patches.v0_0.multiple_instructors #08-02-2022
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import rounded
|
from frappe.utils import rounded
|
||||||
|
from school.lms.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)
|
||||||
|
|||||||
14
school/patches/v0_0/multiple_instructors.py
Normal file
14
school/patches/v0_0/multiple_instructors.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
frappe.reload_doc("lms", "doctype", "lms_course")
|
||||||
|
courses = frappe.get_all("LMS Course", fields=["name", "instructor"])
|
||||||
|
for course in courses:
|
||||||
|
doc = frappe.get_doc({
|
||||||
|
"doctype": "Course Instructor",
|
||||||
|
"parent": course.name,
|
||||||
|
"parentfield": "instructors",
|
||||||
|
"parenttype": "LMS Course",
|
||||||
|
"instructor": course.instructor
|
||||||
|
})
|
||||||
|
doc.save()
|
||||||
@@ -9,7 +9,8 @@
|
|||||||
--text-2xl: 20px;
|
--text-2xl: 20px;
|
||||||
--text-3xl: 22px;
|
--text-3xl: 22px;
|
||||||
--text-4xl: 44px;
|
--text-4xl: 44px;
|
||||||
--navbar-shadow: 0px 1px 8px rgba(0, 0, 0, 0.08)
|
--navbar-shadow: 0px 1px 8px rgba(0, 0, 0, 0.08);
|
||||||
|
--gray-750: #505A62;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type=checkbox] {
|
input[type=checkbox] {
|
||||||
@@ -56,15 +57,16 @@ input[type=checkbox] {
|
|||||||
.course-card-pills {
|
.course-card-pills {
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
margin-right: .5rem;
|
margin-right: 1rem;
|
||||||
border-radius: 4px;
|
border-radius: 6px;
|
||||||
padding: 4px 6px;
|
padding: 3.5px 8px;
|
||||||
font-size: 10px;
|
font-size: 11px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
letter-spacing: 0.011em;
|
letter-spacing: 0.011em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-weight: bold;
|
font-weight: 600;
|
||||||
box-shadow: var(--shadow-base);
|
box-shadow: var(--popover-box-shadow);
|
||||||
|
color: var(--gray-900);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark-pills {
|
.dark-pills {
|
||||||
@@ -77,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;
|
||||||
@@ -94,6 +95,8 @@ input[type=checkbox] {
|
|||||||
|
|
||||||
.course-card {
|
.course-card {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 350px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.muted-text {
|
.muted-text {
|
||||||
@@ -102,17 +105,25 @@ input[type=checkbox] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.course-card-meta {
|
.course-card-meta {
|
||||||
margin: 16px 0px 8px;
|
margin: 0.75rem 0 0.5rem;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--gray-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-card-meta-2 {
|
||||||
|
color: var(--gray-750);
|
||||||
}
|
}
|
||||||
|
|
||||||
.course-card-content {
|
.course-card-content {
|
||||||
width: 100%;
|
padding: 0 1.25rem 1.25rem;
|
||||||
padding: 0px 1rem 1.25rem;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 350px) {
|
@media (max-width: 350px) {
|
||||||
.course-card-content {
|
.course-card-content {
|
||||||
padding: 0px 10px 20px;
|
padding: 0px 10px 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,8 +135,8 @@ input[type=checkbox] {
|
|||||||
|
|
||||||
.course-card-title {
|
.course-card-title {
|
||||||
font-size: 1.125rem;
|
font-size: 1.125rem;
|
||||||
margin-bottom: 1.5rem;
|
font-weight: 600;
|
||||||
height: 56px;
|
margin-bottom: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 360px) {
|
@media (max-width: 360px) {
|
||||||
@@ -145,16 +156,24 @@ input[type=checkbox] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.course-instructor {
|
.course-instructor {
|
||||||
margin-left: 8px;
|
margin-left: 0.625rem;
|
||||||
font-size: 12px;
|
font-size: 0.875rem;
|
||||||
line-height: 135%;
|
color: var(--gray-750);
|
||||||
color: var(--text-color-dark);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.course-student-count {
|
.course-student-count {
|
||||||
display: flex;
|
display: flex;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
float: right;
|
float: right;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-card-footer {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-card-footer .avatar-group {
|
||||||
|
display: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.view-course-link {
|
.view-course-link {
|
||||||
@@ -1181,13 +1200,19 @@ input[type=checkbox] {
|
|||||||
|
|
||||||
.progress {
|
.progress {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 8px;
|
height: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar {
|
.progress-bar {
|
||||||
background-color: var(--primary-color);
|
background-color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.progress-percent {
|
||||||
|
margin: 0.5rem 0 1.3rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
.progress-percentage {
|
.progress-percentage {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z" stroke="#192734" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M8.00001 1.33331L10.06 5.50665L14.6667 6.17998L11.3333 9.42665L12.12 14.0133L8.00001 11.8466L3.88001 14.0133L4.66668 9.42665L1.33334 6.17998L5.94001 5.50665L8.00001 1.33331Z" stroke="#687178" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 278 B After Width: | Height: | Size: 353 B |
@@ -1,4 +1,4 @@
|
|||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M20 21V19C20 17.9391 19.5786 16.9217 18.8284 16.1716C18.0783 15.4214 17.0609 15 16 15H8C6.93913 15 5.92172 15.4214 5.17157 16.1716C4.42143 16.9217 4 17.9391 4 19V21" stroke="#192734" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M13.3333 14V12.6667C13.3333 11.9594 13.0524 11.2811 12.5523 10.781C12.0522 10.281 11.3739 10 10.6667 10H5.33332C4.62608 10 3.9478 10.281 3.4477 10.781C2.94761 11.2811 2.66666 11.9594 2.66666 12.6667V14" stroke="#687178" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path d="M12 11C14.2091 11 16 9.20914 16 7C16 4.79086 14.2091 3 12 3C9.79086 3 8 4.79086 8 7C8 9.20914 9.79086 11 12 11Z" stroke="#192734" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M8.00001 7.33333C9.47277 7.33333 10.6667 6.13943 10.6667 4.66667C10.6667 3.19391 9.47277 2 8.00001 2C6.52725 2 5.33334 3.19391 5.33334 4.66667C5.33334 6.13943 6.52725 7.33333 8.00001 7.33333Z" stroke="#687178" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 532 B After Width: | Height: | Size: 649 B |
@@ -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">×</a>
|
<a href="#" class="close" data-dismiss="alert" aria-label="close">×</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>
|
||||||
|
|||||||
@@ -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
|
||||||
from frappe.utils import cstr
|
from school.lms.utils import get_lesson_url
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,12 +129,14 @@
|
|||||||
<div class="course-home-headings">
|
<div class="course-home-headings">
|
||||||
Creator
|
Creator
|
||||||
</div>
|
</div>
|
||||||
{{ widgets.MemberCard(member=course.get_instructor(), show_course_count=True, avatar_class="avatar-large") }}
|
{% for instructor in get_instructors(course.name) %}
|
||||||
|
{{ widgets.MemberCard(member=instructor, show_course_count=True, avatar_class="avatar-large") }}
|
||||||
|
{% 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">
|
||||||
@@ -184,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 %}
|
||||||
@@ -211,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>
|
||||||
|
|||||||
@@ -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.lms.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:
|
||||||
|
|||||||
@@ -19,13 +19,13 @@
|
|||||||
|
|
||||||
{% include "school/templates/search_course/search_course.html" %}
|
{% include "school/templates/search_course/search_course.html" %}
|
||||||
<div class="course-list">
|
<div class="course-list">
|
||||||
{% set title = _("Live Courses") %}
|
|
||||||
{% set courses = live_courses %}
|
{% set courses = live_courses %}
|
||||||
|
{% set title = _("All Live Courses ({0})").format(courses | length) %}
|
||||||
{% set classes = "live-courses" %}
|
{% set classes = "live-courses" %}
|
||||||
{% include "school/templates/course_list.html" %}
|
{% include "school/templates/course_list.html" %}
|
||||||
|
|
||||||
{% set title = _("Upcoming Courses") %}
|
|
||||||
{% set courses = upcoming_courses %}
|
{% set courses = upcoming_courses %}
|
||||||
|
{% set title = _("All Upcoming Courses ({0})").format(courses | length) %}
|
||||||
{% set classes = "upcoming-courses mt-10" %}
|
{% set classes = "upcoming-courses mt-10" %}
|
||||||
{% include "school/templates/course_list.html" %}
|
{% include "school/templates/course_list.html" %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,21 +6,21 @@ def get_context(context):
|
|||||||
context.live_courses, context.upcoming_courses = get_courses()
|
context.live_courses, context.upcoming_courses = get_courses()
|
||||||
context.restriction = check_profile_restriction()
|
context.restriction = check_profile_restriction()
|
||||||
context.metatags = {
|
context.metatags = {
|
||||||
"title": "All Courses",
|
"title": "All Live Courses",
|
||||||
"image": frappe.db.get_single_value("Website Settings", "banner_image"),
|
"image": frappe.db.get_single_value("Website Settings", "banner_image"),
|
||||||
"description": "This page lists all the courses published on our website",
|
"description": "This page lists all the courses published on our website",
|
||||||
"keywords": "All Courses, Courses, Learn"
|
"keywords": "All Courses, Courses, Learn"
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_courses():
|
def get_courses():
|
||||||
course_names = frappe.get_all("LMS Course",
|
courses = frappe.get_all("LMS Course",
|
||||||
filters={"is_published": True},
|
filters={"is_published": True},
|
||||||
fields=["name", "upcoming"])
|
fields=["name", "upcoming", "title", "image", "enable_certification"])
|
||||||
|
|
||||||
live_courses, upcoming_courses = [], []
|
live_courses, upcoming_courses = [], []
|
||||||
for course in course_names:
|
for course in courses:
|
||||||
if course.upcoming:
|
if course.upcoming:
|
||||||
upcoming_courses.append(frappe.get_doc("LMS Course", course.name))
|
upcoming_courses.append(course)
|
||||||
else:
|
else:
|
||||||
live_courses.append(frappe.get_doc("LMS Course", course.name))
|
live_courses.append(course)
|
||||||
return live_courses, upcoming_courses
|
return live_courses, upcoming_courses
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from school.lms.models import Course
|
from school.lms.utils import slugify, get_membership, get_lessons, get_batch
|
||||||
|
|
||||||
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 = course.get_membership(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
|
||||||
@@ -31,5 +31,5 @@ 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
|
||||||
Reference in New Issue
Block a user