fix: conflicts

This commit is contained in:
Jannat Patel
2022-09-26 09:45:16 +05:30
52 changed files with 1484 additions and 860 deletions

View File

@@ -21,7 +21,7 @@ app_license = "AGPL"
# include js, css files in header of web template
web_include_css = "lms.bundle.css"
# web_include_css = "/assets/lms/css/lms.css"
web_include_js = "website.bundle.js"
web_include_js = ["website.bundle.js", "controls.bundle.js"]
# include custom scss in every website theme (without file extension ".scss")
# website_theme_scss = "lms/public/scss/website"
@@ -192,7 +192,10 @@ jinja = {
"lms.lms.utils.get_popular_courses",
"lms.lms.utils.format_amount",
"lms.lms.utils.first_lesson_exists",
"lms.lms.utils.has_course_instructor_role"
"lms.lms.utils.get_courses_under_review",
"lms.lms.utils.has_course_instructor_role",
"lms.lms.utils.has_course_moderator_role",
"lms.lms.utils.get_certificates"
],
"filters": []
}

View File

@@ -45,6 +45,7 @@
"fieldtype": "Column Break"
},
{
"default": "Full Time",
"fieldname": "type",
"fieldtype": "Select",
"label": "Type",
@@ -114,7 +115,8 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-07-28 13:41:29.224332",
"make_attachments_public": 1,
"modified": "2022-09-15 17:22:21.662675",
"modified_by": "Administrator",
"module": "Job",
"name": "Job Opportunity",
@@ -152,4 +154,4 @@
"sort_order": "DESC",
"states": [],
"title_field": "job_title"
}
}

View File

@@ -6,13 +6,21 @@ from frappe.model.document import Document
from frappe.utils.user import get_system_managers
from frappe import _
from frappe.utils import get_link_to_form
from lms.lms.utils import validate_image
class JobOpportunity(Document):
def validate(self):
self.validate_urls()
self.company_logo = validate_image(self.company_logo)
def validate_urls(self):
frappe.utils.validate_url(self.company_website, True)
frappe.utils.validate_url(self.application_link, True)
@frappe.whitelist()
def report(job, reason):
system_managers = get_system_managers(only_name=True)

View File

@@ -16,11 +16,11 @@
"docstatus": 0,
"doctype": "Web Form",
"idx": 0,
"is_multi_step_form": 0,
"is_standard": 1,
"list_columns": [],
"login_required": 1,
"max_attachment_size": 0,
"modified": "2022-02-24 11:31:25.290524",
"modified": "2022-09-15 17:22:43.957184",
"modified_by": "Administrator",
"module": "Job",
"name": "job-opportunity",
@@ -28,11 +28,9 @@
"payment_button_label": "Buy Now",
"published": 1,
"route": "job-opportunity",
"route_to_success_link": 1,
"show_attachments": 0,
"show_in_grid": 0,
"show_list": 1,
"show_sidebar": 0,
"sidebar_items": [],
"success_message": "",
"success_url": "/jobs",
"title": "Job Opportunity",
@@ -63,6 +61,7 @@
},
{
"allow_read_on_all_link_options": 0,
"default": "Full Time",
"fieldname": "type",
"fieldtype": "Select",
"hidden": 0,

View File

@@ -15,6 +15,10 @@
"include_in_preview",
"index_label",
"section_break_6",
"youtube",
"column_break_9",
"quiz_id",
"section_break_11",
"body",
"help_section",
"help"
@@ -81,11 +85,31 @@
"label": "Course",
"options": "LMS Course",
"read_only": 1
},
{
"description": "Quiz will appear at the bottom of the lesson.",
"fieldname": "quiz_id",
"fieldtype": "Data",
"label": "Quiz ID"
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_11",
"fieldtype": "Section Break"
},
{
"description": "YouTube Video will appear at the top of the lesson.",
"fieldname": "youtube",
"fieldtype": "Data",
"label": "YouTube Video URL"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-05-02 17:16:12.450460",
"modified": "2022-09-02 11:30:15.450624",
"modified_by": "Administrator",
"module": "LMS",
"name": "Course Lesson",

View File

@@ -4,6 +4,7 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
from ...md import find_macros
from lms.lms.utils import get_course_progress, get_lesson_url
@@ -11,6 +12,11 @@ from lms.lms.utils import get_course_progress, get_lesson_url
class CourseLesson(Document):
def validate(self):
self.check_and_create_folder()
self.validate_quiz_id()
def validate_quiz_id(self):
if self.quiz_id and not frappe.db.exists("LMS Quiz", self.quiz_id):
frappe.throw(_("Invalid Quiz ID"))
def on_update(self):
dynamic_documents = ["Exercise", "Quiz"]

View File

@@ -6,10 +6,6 @@ import unittest
from lms.lms.doctype.lms_course.test_lms_course import new_course
class TestExercise(unittest.TestCase):
def setUp(self):
frappe.db.sql('delete from `tabLMS Batch Membership`')
frappe.db.sql('delete from `tabExercise Submission`')
frappe.db.sql('delete from `tabExercise`')
def new_exercise(self):
course = new_course("Test Course")
@@ -47,3 +43,8 @@ class TestExercise(unittest.TestCase):
user_submission = e.get_user_submission()
assert user_submission is not None
assert user_submission.name == submission.name
def tearDown(self):
frappe.db.sql('delete from `tabLMS Batch Membership`')
frappe.db.sql('delete from `tabExercise Submission`')
frappe.db.sql('delete from `tabExercise`')

View File

@@ -6,19 +6,19 @@
"engine": "InnoDB",
"field_order": [
"course",
"member_type",
"batch",
"column_break_3",
"member",
"member_name",
"member_username",
"section_break_8",
"cohort",
"subgroup",
"column_break_3",
"batch",
"current_lesson",
"role",
"member_section",
"member",
"member_type",
"progress",
"column_break_12",
"member_name",
"member_username"
"current_lesson",
"progress",
"role"
],
"fields": [
{
@@ -33,7 +33,8 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Member",
"options": "User"
"options": "User",
"reqd": 1
},
{
"default": "Student",
@@ -70,7 +71,8 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Course",
"options": "LMS Course"
"options": "LMS Course",
"reqd": 1
},
{
"fieldname": "current_lesson",
@@ -103,19 +105,18 @@
"label": "Subgroup",
"options": "Cohort Subgroup"
},
{
"fieldname": "member_section",
"fieldtype": "Section Break",
"label": "Member"
},
{
"fieldname": "column_break_12",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_8",
"fieldtype": "Section Break"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-03-29 09:47:05.007133",
"modified": "2022-09-01 17:11:08.065998",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Batch Membership",

View File

@@ -3,31 +3,31 @@
frappe.ui.form.on('LMS Course', {
onload: function (frm) {
onload: function (frm) {
frm.set_query("chapter", "chapters", function () {
return {
filters: {
"course": frm.doc.name,
}
};
});
frm.set_query("chapter", "chapters", function () {
return {
filters: {
"course": frm.doc.name,
}
};
});
frm.set_query("instructor", function (doc) {
return {
filters: {
"ignore_user_type": 1,
}
};
});
frm.set_query("instructor", "instructors", function () {
return {
filters: {
"ignore_user_type": 1,
}
};
});
frm.set_query("course", "related_courses", function () {
return {
filters: {
"published": true,
}
};
});
}
frm.set_query("course", "related_courses", function () {
return {
filters: {
"published": true,
}
};
});
}
});

View File

@@ -260,7 +260,8 @@
"link_fieldname": "course"
}
],
"modified": "2022-05-19 16:59:21.933367",
"make_attachments_public": 1,
"modified": "2022-09-14 13:26:53.153822",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Course",

View File

@@ -5,15 +5,17 @@ from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
import json
from ...utils import generate_slug
from frappe.utils import flt, cint
from ...utils import generate_slug, validate_image
from frappe.utils import cint
from lms.lms.utils import get_chapters
class LMSCourse(Document):
def validate(self):
self.validate_instructors()
self.validate_status()
self.image = validate_image(self.image)
def validate_instructors(self):
if self.is_new() and not self.instructors:
@@ -25,20 +27,22 @@ class LMSCourse(Document):
"parenttype": "LMS Course"
}).save(ignore_permissions=True)
def validate_status(self):
if self.published:
self.status = "Approved"
def on_update(self):
if not self.upcoming and self.has_value_changed("upcoming"):
self.send_email_to_interested_users()
def send_email_to_interested_users(self):
interested_users = frappe.get_all("LMS Course Interest",
{
"course": self.name
},
["name", "user"])
interested_users = frappe.get_all("LMS Course Interest", {
"course": self.name
},
["name", "user"])
subject = self.title + " is available!"
args = {
"title": self.title,
@@ -68,6 +72,7 @@ class LMSCourse(Document):
def __repr__(self):
return f"<Course#{self.name}>"
def has_mentor(self, email):
"""Checks if this course has a mentor with given email.
"""
@@ -77,6 +82,7 @@ class LMSCourse(Document):
mapping = frappe.get_all("LMS Course Mentor Mapping", {"course": self.name, "mentor": email})
return mapping != []
def add_mentor(self, email):
"""Adds a new mentor to the course.
"""
@@ -97,7 +103,6 @@ class LMSCourse(Document):
doc.insert()
def get_student_batch(self, email):
"""Returns the batch the given student is part of.
@@ -116,6 +121,7 @@ class LMSCourse(Document):
fieldname="batch")
return batch_name and frappe.get_doc("LMS Batch", batch_name)
def get_batches(self, mentor=None):
batches = frappe.get_all("LMS Batch", {"course": self.name})
if mentor:
@@ -127,17 +133,21 @@ class LMSCourse(Document):
batch_names = {m.batch for m in memberships}
return [b for b in batches if b.name in batch_names]
def get_cohorts(self):
return frappe.get_all("Cohort", {"course": self.name}, order_by="creation")
def get_cohort(self, cohort_slug):
name = frappe.get_value("Cohort", {"course": self.name, "slug": cohort_slug})
return name and frappe.get_doc("Cohort", name)
def reindex_exercises(self):
for i, c in enumerate(get_chapters(self.name), start=1):
self._reindex_exercises_in_chapter(c, i)
def _reindex_exercises_in_chapter(self, c, index):
i = 1
for lesson in self.get_lessons(c):
@@ -147,12 +157,14 @@ class LMSCourse(Document):
exercise.save()
i += 1
def get_all_memberships(self, member):
all_memberships = frappe.get_all("LMS Batch Membership", {"member": member, "course": self.name}, ["batch"])
for membership in all_memberships:
membership.batch_title = frappe.db.get_value("LMS Batch", membership.batch, "title")
return all_memberships
@frappe.whitelist()
def reindex_exercises(doc):
course_data = json.loads(doc)
@@ -160,6 +172,7 @@ def reindex_exercises(doc):
course.reindex_exercises()
frappe.msgprint("All exercises in this course have been re-indexed.")
@frappe.whitelist(allow_guest=True)
def search_course(text):
search_courses = []
@@ -185,6 +198,7 @@ def search_course(text):
return courses
@frappe.whitelist()
def submit_for_review(course):
chapters = frappe.get_all("Chapter Reference", {"parent": course})
@@ -195,7 +209,7 @@ def submit_for_review(course):
@frappe.whitelist()
def save_course(tags, title, short_introduction, video_link, description, course, image=None):
def save_course(tags, title, short_introduction, video_link, description, course, published, upcoming, image=None):
if course:
doc = frappe.get_doc("LMS Course", course)
else:
@@ -209,7 +223,9 @@ def save_course(tags, title, short_introduction, video_link, description, course
"video_link": video_link,
"image": image,
"description": description,
"tags": tags
"tags": tags,
"published": cint(published),
"upcoming": cint(upcoming)
})
doc.save(ignore_permissions=True)
return doc.name
@@ -249,7 +265,7 @@ def save_chapter(course, title, chapter_description, idx, chapter):
@frappe.whitelist()
def save_lesson(title, body, chapter, preview, idx, lesson):
def save_lesson(title, body, chapter, preview, idx, lesson, youtube=None, quiz_id=None):
if lesson:
doc = frappe.get_doc("Course Lesson", lesson)
else:
@@ -261,7 +277,9 @@ def save_lesson(title, body, chapter, preview, idx, lesson):
"chapter": chapter,
"title": title,
"body": body,
"include_in_preview": preview
"include_in_preview": preview,
"youtube": youtube,
"quiz_id": quiz_id
})
doc.save(ignore_permissions=True)

View File

@@ -7,13 +7,16 @@ import frappe
from .lms_course import LMSCourse
import unittest
class TestLMSCourse(unittest.TestCase):
def test_new_course(self):
course = new_course("Test Course")
assert course.title == "Test Course"
assert course.name == "test-course"
# disabled this test as it is failing
def _test_add_mentors(self):
course = new_course("Test Course")
@@ -26,10 +29,23 @@ class TestLMSCourse(unittest.TestCase):
mentors_data = [dict(email=mentor.email, batch_count=mentor.batch_count) for mentor in mentors]
assert mentors_data == [{"email": "tester@example.com", "batch_count": 0}]
def tearDown(self):
if frappe.db.exists("User", "tester@example.com"):
frappe.delete_doc("User", "tester@example.com")
if frappe.db.exists("LMS Course", "test-course"):
frappe.db.delete("Exercise Submission", {"course": "test-course"})
frappe.db.delete("Exercise Latest Submission", {"course": "test-course"})
frappe.db.delete("Exercise", {"course": "test-course"})
frappe.db.delete("LMS Batch Membership", {"course": "test-course"})
frappe.db.delete("LMS Batch", {"course": "test-course"})
frappe.db.delete("LMS Course Mentor Mapping", {"course": "test-course"})
frappe.db.delete("Course Instructor", {"parent": "test-course"})
frappe.db.sql('delete from `tabCourse Instructor`')
frappe.delete_doc("LMS Course", "test-course")
def new_user(name, email):
user = frappe.db.exists("User", email)
if user:
@@ -46,6 +62,7 @@ def new_user(name, email):
doc.insert()
return doc
def new_course(title, additional_filters=None):
course = frappe.db.exists("LMS Course", { "title": title })
if course:
@@ -66,6 +83,7 @@ def new_course(title, additional_filters=None):
doc.insert(ignore_permissions=True)
return doc
def create_evaluator():
if not frappe.db.exists("Course Evaluator", "evaluator@example.com"):
new_user("Evaluator", "evaluator@example.com")

View File

@@ -7,6 +7,7 @@ from frappe import _
RE_SLUG_NOTALLOWED = re.compile("[^a-z0-9]+")
def slugify(title, used_slugs=[]):
"""Converts title to a slug.
@@ -59,6 +60,7 @@ def get_membership(course, member, batch=None):
membership.batch_title = frappe.db.get_value("LMS Batch", membership.batch, "title")
return membership
def get_chapters(course):
"""Returns all chapters of this course.
"""
@@ -85,6 +87,7 @@ def get_lessons(course, chapter=None):
return lessons
def get_lesson_details(chapter):
lessons = []
lesson_list = frappe.get_all("Lesson Reference",
@@ -94,10 +97,11 @@ def get_lesson_details(chapter):
for row in lesson_list:
lesson_details = frappe.db.get_value("Course Lesson", row.lesson,
["name", "title", "include_in_preview", "body", "creation"], as_dict=True)
["name", "title", "include_in_preview", "body", "creation", "youtube", "quiz_id"], as_dict=True)
lesson_details.number = flt("{}.{}".format(chapter.idx, row.idx))
lesson_details.icon = "icon-list"
macros = find_macros(lesson_details.body)
for macro in macros:
if macro[0] == "YouTubeVideo":
lesson_details.icon = "icon-video"
@@ -106,10 +110,12 @@ def get_lesson_details(chapter):
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},
@@ -123,6 +129,7 @@ def get_instructors(course):
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.
"""
@@ -137,12 +144,14 @@ def get_students(course, batch=None):
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",
{
@@ -164,6 +173,7 @@ def get_reviews(course):
return reviews
def get_sorted_reviews(course):
rating_count = rating_percent = frappe._dict()
keys = ["5.0", "4.0", "3.0", "2.0", "1.0"]
@@ -180,6 +190,7 @@ def get_sorted_reviews(course):
return rating_percent
def is_certified(course):
certificate = frappe.get_all("LMS Certificate",
{
@@ -190,6 +201,7 @@ def is_certified(course):
return certificate[0].name
return
def get_lesson_index(lesson_name):
"""Returns the {chapter_index}.{lesson_index} for the lesson.
"""
@@ -205,6 +217,7 @@ def get_lesson_index(lesson_name):
return f"{chapter.idx}.{lesson.idx}"
def get_lesson_url(course, lesson_number):
if not lesson_number:
return
@@ -224,8 +237,16 @@ def get_progress(course, lesson):
},
["status"])
def render_html(body):
return markdown_to_html(body)
def render_html(body, youtube, quiz_id):
if youtube and "/" in youtube:
youtube = youtube.split("/")[-1]
quiz_id = "{{ Quiz('" + quiz_id + "') }}" if quiz_id else ""
youtube = "{{ YouTubeVideo('" + youtube + "') }}" if youtube else ""
text = youtube + body + quiz_id
return markdown_to_html(text)
def is_mentor(course, email):
"""Checks if given user is a mentor for this course.
@@ -238,6 +259,7 @@ def is_mentor(course, email):
"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.
"""
@@ -253,6 +275,7 @@ def is_cohort_staff(course, 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.
"""
@@ -269,6 +292,7 @@ def get_mentors(course):
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:
@@ -281,6 +305,7 @@ def is_eligible_to_review(course, membership):
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))
@@ -295,6 +320,7 @@ def get_course_progress(course, member=None):
precision = cint(frappe.db.get_default("float_precision")) or 3
return flt(((completed_lessons/lesson_count) * 100), precision)
def get_initial_members(course):
members = frappe.get_all("LMS Batch Membership",
{
@@ -310,12 +336,15 @@ def get_initial_members(course):
return member_details
def is_instructor(course):
return len(list(filter(lambda x: x.name == frappe.session.user, get_instructors(course)))) > 0
def convert_number_to_character(number):
return string.ascii_uppercase[number]
def get_signup_optin_checks():
mapper = frappe._dict({
@@ -343,6 +372,7 @@ def get_signup_optin_checks():
return (", ").join(links)
def get_popular_courses():
courses = frappe.get_all("LMS Course", {"published": 1, "upcoming": 0})
course_membership = []
@@ -356,6 +386,7 @@ def get_popular_courses():
course_membership = sorted(course_membership, key = lambda x: x.get("members"), reverse=True)
return course_membership[:3]
def get_evaluation_details(course, member=None):
info = frappe.db.get_value("LMS Course", course, ["grant_certificate_after", "max_attempts", "duration"], as_dict=True)
request = frappe.db.get_value("LMS Certificate Request", {
@@ -378,6 +409,7 @@ def get_evaluation_details(course, member=None):
"no_of_attempts": no_of_attempts
})
def format_amount(amount, currency):
amount_reduced = amount / 1000
if amount_reduced < 1:
@@ -397,13 +429,43 @@ def first_lesson_exists(course):
return True
def redirect_to_courses_list():
frappe.local.flags.redirect_location = "/courses"
raise frappe.Redirect
def has_course_instructor_role():
def has_course_instructor_role(member=None):
return frappe.db.get_value("Has Role", {
"parent": frappe.session.user,
"parent": member or frappe.session.user,
"role": "Course Instructor"
}, "name")
def has_course_moderator_role(member=None):
return frappe.db.get_value("Has Role", {
"parent": member or frappe.session.user,
"role": "Course Moderator"
}, "name")
def get_courses_under_review():
return frappe.get_all("LMS Course", {
"status": "Under Review"
}, ["name", "upcoming", "title", "image", "enable_certification", "status", "published"]
)
def get_certificates(member=None):
return frappe.get_all("LMS Certificate", {
"member": member or frappe.session.user
}, ["course", "member", "issue_date", "expiry_date", "name"])
def validate_image(path):
if path and "/private" in path:
file = frappe.get_doc("File", {"file_url": path})
file.is_private = 0
file.save(ignore_permissions=True)
return file.file_url
return path

View File

@@ -18,11 +18,11 @@
"docstatus": 0,
"doctype": "Web Form",
"idx": 0,
"is_multi_step_form": 0,
"is_standard": 1,
"list_columns": [],
"login_required": 1,
"max_attachment_size": 0,
"modified": "2022-06-24 19:08:29.197279",
"modified": "2022-09-05 13:08:40.071348",
"modified_by": "Administrator",
"module": "LMS",
"name": "profile",
@@ -30,11 +30,9 @@
"payment_button_label": "Buy Now",
"published": 1,
"route": "edit-profile",
"route_to_success_link": 0,
"show_attachments": 0,
"show_in_grid": 0,
"show_list": 0,
"show_sidebar": 0,
"sidebar_items": [],
"success_url": "/profile",
"title": "Profile",
"web_form_fields": [
@@ -50,18 +48,6 @@
"reqd": 1,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "middle_name",
"fieldtype": "Data",
"hidden": 0,
"label": "Middle Name (Optional)",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "last_name",

View File

@@ -1,27 +1,31 @@
{% set enrolled = get_enrolled_courses().in_progress + get_enrolled_courses().completed %}
{% if enrolled | length %}
<div class="cards-parent">
{% for course in enrolled %}
{{ widgets.CourseCard(course=course) }}
{% endfor %}
{% for course in enrolled %}
{{ widgets.CourseCard(course=course) }}
{% endfor %}
</div>
{% else %}
{% set site_name = frappe.db.get_single_value("System Settings", "app_name") %}
<div class="empty-state">
<div class="empty-state-text">
<div class="text-center">
<div class="empty-state-heading">{{ _("You haven't enrolled for any courses") }}</div>
<div class="course-meta mb-6">{{ _("Here are a few courses we recommend for you to get started with {0}").format(site_name) }}</div>
<div class="empty-state p-5">
<div style="text-align: left; flex: 1;">
<div class="text-center">
<div class="empty-state-heading">{{ _("You haven't enrolled for any courses") }}</div>
<div class="course-meta mb-6">{{ _("Here are a few courses we recommend for you to get started with {0}").format(site_name) }}</div>
</div>
{% set recommended_courses = get_popular_courses() %}
<div class="cards-parent">
{% for course in recommended_courses %}
{% if course %}
{% set course_details = frappe.get_doc("LMS Course", course.course) %}
{{ widgets.CourseCard(course=course_details) }}
{% endif %}
{% endfor %}
</div>
</div>
{% set recommended_courses = get_popular_courses() %}
<div class="cards-parent">
{% for course in recommended_courses %}
{% if course %}
{% set course_details = frappe.get_doc("LMS Course", course.course) %}
{{ widgets.CourseCard(course=course_details) }}
{% endif %}
{% endfor %}
</div>
</div>
</div>
{% endif %}

View File

@@ -3,7 +3,7 @@
<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 %}
style="background-image: url( {{ course.image }} );" {% endif %}>
style="background-image: url( {{ course.image | urlencode }} );" {% endif %}>
<div class="course-tags">
{% for tag in get_tags(course.name) %}
<div class="course-card-pills">{{ tag }}</div>
@@ -90,10 +90,12 @@
<a class="button-links" href="{{ get_profile_url(instructors[0].username) }}">
<span class="course-instructor">
{% if ins_len == 1 %}
{{ instructors[0].full_name }}
{{ instructors[0].full_name }}
{% elif ins_len == 2 %}
{{ instructors[0].full_name.split(" ")[0] }} and {{ instructors[1].full_name.split(" ")[0] }}
{% else %}
{% set suffix = "other" if ins_len - 1 == 1 else "others" %}
{{ instructors[0].full_name.split(" ")[0] }} and {{ ins_len - 1 }} {{ suffix }}
{% set suffix = "other" if ins_len - 1 == 1 else "others" %}
{{ instructors[0].full_name.split(" ")[0] }} and {{ ins_len - 1 }} {{ suffix }}
{% endif %}
</span>
</a>

View File

@@ -1,18 +1,28 @@
<div class="course-home-outline">
{% set chapters = get_chapters(course.name) %}
{% if course.edit_mode and course.name %}
<button class="btn btn-md btn-secondary btn-chapter pull-right"> {{ _("New Chapter") }} </button>
{% endif %}
{% if course.name and (course.edit_mode or get_chapters(course.name) | length) %}
{% if course.name and (course.edit_mode or chapters | length) %}
<div class="course-home-headings" id="outline-heading">
{{ _("Course Content") }}
</div>
{% endif %}
{% if get_chapters(course.name) | length %}
{% if course.edit_mode and course.name and not chapters | length %}
<div class="chapter-parent chapter-edit new-chapter">
<div contenteditable="true" data-placeholder="{{ _('Chapter Name') }}" class="chapter-title-main"></div>
<div class="chapter-description small my-2" contenteditable="true" data-placeholder="{{ _('Short Description') }}"></div>
<button class="btn btn-sm btn-secondary d-block btn-save-chapter" data-index="1"> {{ _('Save') }} </button>
</div>
{% endif %}
{% for chapter in get_chapters(course.name) %}
{% if chapters | length %}
{% for chapter in chapters %}
<div class="chapter-parent {% if course.edit_mode %} chapter-edit {% endif %} ">
<div class="chapter-title" {% if not course.edit_mode %} data-toggle="collapse" aria-expanded="false"
data-target="#{{ get_slugified_chapter_title(chapter.title) }}" {% endif %} >
@@ -55,7 +65,7 @@
{% set active = membership.current_lesson == lesson.name %}
<div class="lesson-info {% if active and not course.edit_mode %} active-lesson {% endif %}">
{% if membership or lesson.include_in_preview or is_instructor %}
{% if membership or lesson.include_in_preview or is_instructor or has_course_moderator_role() %}
<a class="lesson-links" data-course="{{ course.name }}"
{% if is_instructor and not lesson.include_in_preview %}
title="{{ _('This lesson is not available for preview. As you are the Instructor of the course only you can see it.') }}"
@@ -185,3 +195,4 @@ const show_no_preview_dialog = (e) => {
};
</script>

View File

@@ -8,7 +8,7 @@
<div> {{ member.headline }} </div>
{% endif %}
{% set course_count = get_authored_courses(member.name) | length %}
{% set course_count = get_authored_courses(member.name, True) | length %}
{% if show_course_count and course_count > 0 %}
{% set suffix = "Courses" if course_count > 1 else "Course" %}
<div class="">

View File

@@ -1,166 +1,169 @@
{% if not course.upcoming %}
<div class="reviews-parent">
{% set reviews = get_reviews(course.name) %}
<div class="mb-5">
<span class="course-home-headings"> {{ _("Reviews") }} </span>
{% if is_eligible_to_review(course.name, membership) and reviews | length %}
<span class="review-link button is-secondary pull-right">
{{ _("Write a review") }}
</span>
{% endif %}
</div>
{% set avg_rating = get_average_rating(course.name) %}
{% if avg_rating %}
<div class="reviews-header">
<div class="text-center">
<div class="avg-rating"> {{ frappe.utils.flt(avg_rating, frappe.get_system_settings("float_precision") or 3) }} </div>
<div class="course-meta"> {{ reviews | length }} {{ _("ratings") }} </div>
<div class="avg-rating-stars">
<div class="rating">
{% for i in [1, 2, 3, 4, 5] %}
<svg class="icon icon-md {% if i <= frappe.utils.ceil(avg_rating) %} star-click {% endif %}" data-rating="{{ i }}">
<use href="#icon-star"></use>
</svg>
{% endfor %}
</div>
</div>
<div class="course-meta">
{{ frappe.utils.flt(avg_rating, frappe.get_system_settings("float_precision") or 3) }} {{ _("out of 5 ") }}
</div>
</div>
<div class="vertical-divider"></div>
{% set sorted_reviews = get_sorted_reviews(course.name) %}
<div>
{% for review in sorted_reviews %}
<div class="d-flex align-items-center mb-3">
<div class="course-meta mr-2"> {{ frappe.utils.cint(review) }} {{ _("stars") }} </div>
<div class="progress">
<div class="progress-bar" role="progressbar" aria-valuenow="{{ sorted_reviews[review] }}"
aria-valuemin="0" aria-valuemax="100" style="width:{{ sorted_reviews[review] }}%">
<span class="sr-only"> {{ sorted_reviews[review] }} Complete</span>
</div>
</div>
<div class="course-meta ml-3"> {{ frappe.utils.cint(sorted_reviews[review]) }}% </div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% if reviews | length %}
<div class="mt-12">
{% for review in reviews %}
<div class="mb-4">
<div class="d-flex align-items-center">
<div class="mr-4">
{{ widgets.Avatar(member=review.owner_details, avatar_class="avatar-medium") }}
</div>
<div>
<div class="d-flex align-items-center">
<a class="button-links mr-4" href="{{get_profile_url(review.owner_details.username) }}">
<span class="bold-heading">
{{ review.owner_details.full_name }}
</span>
</a>
<div class="frappe-timestamp course-meta" data-timestamp="{{ review.creation }}"> frappe.utils.pretty_date(review.creation) </div>
</div>
<div class="rating">
{% for i in [1, 2, 3, 4, 5] %}
<svg class="icon icon-md {% if i <= review.rating %} star-click {% endif %}" data-rating="{{ i }}">
<use href="#icon-star"></use>
</svg>
{% endfor %}
</div>
</div>
</div>
<div class="review-content"> {{ review.review }} </div>
</div>
{% if loop.index != reviews | length %}
<div class="card-divider"></div>
{% endif %}
{% endfor %}
</div>
{% else %}
<div class="empty-state">
<div>
<img class="icon icon-xl" src="/assets/lms/icons/comment.svg">
</div>
<div class="empty-state-text">
<div class="empty-state-heading">{{ _("Review the course") }}</div>
<div class="course-meta">{{ _("Help us improve our course material.") }}</div>
</div>
<div>
{% if not is_instructor(course.name) %}
{% set reviews = get_reviews(course.name) %}
<div class="mb-5">
<span class="course-home-headings"> {{ _("Reviews") }} </span>
{% if is_eligible_to_review(course.name, membership) %}
<span class="review-link button is-secondary">
{{ _("Write a review") }}
</span>
{% elif frappe.session.user == "Guest" %}
<a class="button is-primary" href="/login?redirect-to=/courses/{{ course.name }}"> {{ _("Login") }} </a>
{% elif not membership %}
<div class="button is-primary join-batch" data-course="{{ course.name | urlencode }}"> {{ _("Start Learning") }} </div>
<span class="btn btn-secondary btn-sm review-link">
{{ _("Write a review") }}
</span>
{% elif not is_instructor(course.name) and frappe.session.user == "Guest" %}
<a class="btn btn-secondary btn-s pull-rightm" href="/login?redirect-to=/courses/{{ course.name }}"> {{ _("Login") }} </a>
{% elif not is_instructor(course.name) and not membership and course.status == "Approved" %}
<div class="btn btn-secondary btn-sm join-batch pull-right" data-course="{{ course.name | urlencode }}"> {{ _("Start Learning") }} </div>
{% endif %}
{% endif %}
</div>
</div>
{% endif %}
{% set avg_rating = get_average_rating(course.name) %}
{% if avg_rating %}
<div class="reviews-header">
<div class="text-center">
<div class="avg-rating">
{{ frappe.utils.flt(avg_rating, frappe.get_system_settings("float_precision") or 3) }}
</div>
<div class="course-meta"> {{ reviews | length }} {{ _("ratings") }} </div>
<div class="avg-rating-stars">
<div class="rating">
{% for i in [1, 2, 3, 4, 5] %}
<svg class="icon icon-md {% if i <= frappe.utils.ceil(avg_rating) %} star-click {% endif %}" data-rating="{{ i }}">
<use href="#icon-star"></use>
</svg>
{% endfor %}
</div>
</div>
<div class="course-meta">
{{ frappe.utils.flt(avg_rating, frappe.get_system_settings("float_precision") or 3) }} {{ _("out of 5 ") }}
</div>
</div>
<div class="vertical-divider"></div>
{% set sorted_reviews = get_sorted_reviews(course.name) %}
<div>
{% for review in sorted_reviews %}
<div class="d-flex align-items-center mb-3">
<div class="course-meta mr-2">
{{ frappe.utils.cint(review) }} {{ _("stars") }}
</div>
<div class="progress">
<div class="progress-bar" role="progressbar" aria-valuenow="{{ sorted_reviews[review] }}"
aria-valuemin="0" aria-valuemax="100" style="width:{{ sorted_reviews[review] }}%">
<span class="sr-only"> {{ sorted_reviews[review] }} Complete</span>
</div>
</div>
<div class="course-meta ml-3"> {{ frappe.utils.cint(sorted_reviews[review]) }}% </div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% if reviews | length %}
<div class="mt-12">
{% for review in reviews %}
<div class="mb-4">
<div class="d-flex align-items-center">
<div class="mr-4">
{{ widgets.Avatar(member=review.owner_details, avatar_class="avatar-medium") }}
</div>
<div>
<div class="d-flex align-items-center">
<a class="button-links mr-4" href="{{get_profile_url(review.owner_details.username) }}">
<span class="bold-heading">
{{ review.owner_details.full_name }}
</span>
</a>
<div class="frappe-timestamp course-meta" data-timestamp="{{ review.creation }}">
{{ frappe.utils.pretty_date(review.creation) }}
</div>
</div>
<div class="rating">
{% for i in [1, 2, 3, 4, 5] %}
<svg class="icon icon-md {% if i <= review.rating %} star-click {% endif %}" data-rating="{{ i }}">
<use href="#icon-star"></use>
</svg>
{% endfor %}
</div>
</div>
</div>
<div class="review-content"> {{ review.review }} </div>
</div>
{% if loop.index != reviews | length %}
<div class="card-divider"></div>
{% endif %}
{% endfor %}
</div>
{% else %}
<div class="empty-state">
<img class="icon icon-xl" src="/assets/lms/icons/comment.svg">
<div class="empty-state-text">
<div class="empty-state-heading">{{ _("Review the course") }}</div>
<div class="course-meta">{{ _("Help us improve our course material.") }}</div>
</div>
</div>
{% endif %}
</div>
<div class="modal fade review-modal" id="review-modal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<div class="font-weight-bold">{{ _("Write a review") }}</div>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form class="review-form" id="review-form">
<div class="form-group">
<div class="clearfix">
<label class="control-label reqd" style="padding-right: 0px;">{{ _("Rating") }}</label>
</div>
<div class="control-input-wrapper">
<div class="control-input">
<div class="rating rating-field" id="rating">
{% for i in [1, 2, 3, 4, 5] %}
<svg class="icon icon-md icon-rating" data-rating="{{ i }}">
<use href="#icon-star"></use>
</svg>
{% endfor %}
</div>
</div>
</div>
</div>
<div class="form-group">
<div class="clearfix">
<label class="control-label reqd" style="padding-right: 0px;">{{ _("Review") }}</label>
<div class="modal fade review-modal" id="review-modal" tabindex="-1" role="dialog"
aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<div class="font-weight-bold">{{ _("Write a review") }}</div>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="control-input-wrapper">
<div class="control-input">
<textarea type="text" autocomplete="off" class="input-with-feedback form-control review-field"
data-fieldtype="Text" data-fieldname="feedback_comments" placeholder="" style="height: 300px;"
spellcheck="false"></textarea>
</div>
<div class="modal-body">
<form class="review-form" id="review-form">
<div class="form-group">
<div class="clearfix">
<label class="control-label reqd" style="padding-right: 0px;">{{ _("Rating") }}</label>
</div>
<div class="control-input-wrapper">
<div class="control-input">
<div class="rating rating-field" id="rating">
{% for i in [1, 2, 3, 4, 5] %}
<svg class="icon icon-md icon-rating" data-rating="{{ i }}">
<use href="#icon-star"></use>
</svg>
{% endfor %}
</div>
</div>
</div>
</div>
<div class="form-group">
<div class="clearfix">
<label class="control-label reqd" style="padding-right: 0px;">{{ _("Review") }}</label>
</div>
<div class="control-input-wrapper">
<div class="control-input">
<textarea type="text" autocomplete="off" class="input-with-feedback form-control review-field"
data-fieldtype="Text" data-fieldname="feedback_comments" placeholder="" style="height: 300px;"
spellcheck="false"></textarea>
</div>
</div>
</div>
<p class="error-field muted-text"></p>
</form>
</div>
</div>
<p class="error-field muted-text"></p>
</form>
</div>
<div class="modal-footer">
<div class="button submit-review is-primary" data-course="{{ course.name | urlencode}}" id="submit-review">
{{ _("Submit") }}</div>
</div>
<div class="modal-footer">
<div class="button submit-review is-primary" data-course="{{ course.name | urlencode}}" id="submit-review">
{{ _("Submit") }}
</div>
</div>
</div>
</div>
</div>
</div>
{% endif %}

View File

@@ -6,17 +6,22 @@ import random
import re
from frappe import _
from frappe.website.utils import is_signup_disabled
from lms.lms.utils import validate_image
import requests
from frappe.geo.country_info import get_all
from lms.widgets import Widgets
class CustomUser(User):
def validate(self):
super(CustomUser, self).validate()
self.validate_username_characters()
self.validate_skills()
self.validate_completion()
self.user_image = validate_image(self.user_image)
self.cover_image = validate_image(self.cover_image)
def validate_username_characters(self):
if len(self.username):
@@ -127,7 +132,8 @@ class CustomUser(User):
def get_enrolled_courses():
in_progress = []
completed = []
memberships = get_course_membership(frappe.session.user, member_type="Student")
memberships = get_course_membership(None, member_type="Student")
for membership in memberships:
course = frappe.db.get_value("LMS Course", membership.course, ["name", "upcoming", "title", "image",
"enable_certification", "paid_certificate", "price_certificate", "currency", "published"], as_dict=True)
@@ -144,10 +150,11 @@ def get_enrolled_courses():
"completed": completed
}
def get_course_membership(member, member_type=None):
""" Returns all memberships of the user """
def get_course_membership(member=None, member_type=None):
""" Returns all memberships of the user. """
filters = {
"member": member
"member": member or frappe.session.user
}
if member_type:
filters["member_type"] = member_type
@@ -155,24 +162,24 @@ def get_course_membership(member, member_type=None):
return frappe.get_all("LMS Batch Membership", filters, ["name", "course", "progress"])
def get_authored_courses(member, only_published=True):
"""Returns the number of courses authored by this user.
"""
def get_authored_courses(member=None, only_published=True):
""" Returns the number of courses authored by this user. """
course_details = []
filters = {
"instructor": member
}
if only_published:
filters["published"] = True
courses = frappe.get_all('LMS Course', filters)
courses = frappe.get_all("Course Instructor", {
"instructor": member or frappe.session.user
}, ["parent"])
for course in courses:
course_details.append(frappe.db.get_value("LMS Course", course,
["name", "upcoming", "title", "image", "enable_certification", "status"], as_dict=True))
detail = frappe.db.get_value("LMS Course", course.parent,
["name", "upcoming", "title", "image", "enable_certification", "status", "published"], as_dict=True)
if only_published and detail and not detail.published:
continue
course_details.append(detail)
return course_details
def get_palette(full_name):
"""
Returns a color unique to each member for Avatar """
@@ -328,3 +335,22 @@ def get_users(or_filters, start, page_length, text):
""".format(or_filters = or_filters, start=start, page_length=page_length), as_dict=1)
return users
@frappe.whitelist()
def save_role(user, role, value):
if cint(value):
doc = frappe.get_doc({
"doctype": "Has Role",
"parent": user,
"role": role,
"parenttype": "User",
"parentfield": "roles"
})
doc.save(ignore_permissions=True)
else:
frappe.db.delete("Has Role", {
"parent": user,
"role": role
})
return True

View File

@@ -30,4 +30,5 @@ lms.patches.v0_0.move_certification_to_certificate
lms.patches.v0_0.quiz_submission_member
lms.patches.v0_0.delete_old_module_docs #08-07-2022
lms.patches.v0_0.delete_course_web_forms #21-08-2022
lms.patches.v0_0.create_course_instructor_role
lms.patches.v0_0.create_course_instructor_role #29-08-2022
lms.patches.v0_0.create_course_moderator_role

View File

@@ -6,5 +6,6 @@ def execute():
"doctype": "Role",
"role_name": "Course Instructor",
"home_page": "/dashboard",
"desk_access": 0
})
role.save(ignore_permissions=True)

View File

@@ -0,0 +1,11 @@
import frappe
def execute():
if not frappe.db.exists("Role", "Course Moderator"):
role = frappe.get_doc({
"doctype": "Role",
"role_name": "Course Moderator",
"home_page": "/dashboard",
"desk_access": 0
})
role.save(ignore_permissions=True)

View File

@@ -44,37 +44,36 @@ input[type=checkbox] {
}
.course-image .course-tags {
width: 95%;
width: fit-content;
}
.course-card-pills {
background: #ffffff;
margin-left: 0;
margin-right: 1rem;
border-radius: var(--border-radius);
padding: 3.5px 8px;
font-size: 11px;
text-align: center;
letter-spacing: 0.011em;
text-transform: uppercase;
font-weight: 600;
color: var(--gray-900);
width: fit-content;
box-shadow: var(--shadow-sm);
background: #ffffff;
margin-left: 0;
margin-right: 1rem;
border-radius: var(--border-radius);
padding: 3.5px 8px;
font-size: 11px;
text-align: center;
letter-spacing: 0.011em;
text-transform: uppercase;
font-weight: 600;
color: var(--gray-900);
width: fit-content;
box-shadow: var(--shadow-sm);
}
.dark-pills {
background: rgba(25, 39, 52, 0.8);
color: #ffffff;
background: rgba(25, 39, 52, 0.8);
color: #ffffff;
}
.dark-pills img {
width: 0.75rem;
height: 0.75rem;
width: 0.75rem;
height: 0.75rem;
}
.common-page-style {
padding: 2rem 0 5rem;
min-height: 60vh;
padding-top: 3rem;
background-color: var(--bg-color);
}
@@ -126,9 +125,10 @@ input[type=checkbox] {
}
.course-card-title {
font-weight: 600;
color: var(--gray-900);
margin-bottom: 1.25rem;
font-weight: 600;
color: var(--gray-900);
margin-bottom: 1.25rem;
font-size: 1.125rem;
}
.card-divider {
@@ -661,7 +661,7 @@ input[type=checkbox] {
font-size: var(--text-sm);
display: flex;
flex-direction: column;
margin-bottom: 3rem;
margin-bottom: 2.5rem;
padding-left: 200px;
padding-right: 1rem;
box-shadow: var(--shadow-sm);
@@ -764,7 +764,7 @@ input[type=checkbox] {
}
.education-details {
margin-top: 3rem;
margin-top: 1.25rem;
}
.bold-title {
@@ -895,24 +895,22 @@ pre {
}
}
.profile-card {
flex-direction: column;
padding: 1rem 1.25rem;
.column-card {
flex-direction: column;
padding: 1.25rem;
}
.empty-state {
background: var(--gray-200);
border-radius: var(--border-radius-lg);
padding: 2rem;
padding: 4rem;
display: flex;
justify-content: space-between;
justify-content: center;
align-items: center;
}
.empty-state-text {
flex: 1;
margin-left: 1.25rem;
text-align: center;
margin-left: 1rem;
}
.empty-state-heading {
@@ -1400,19 +1398,25 @@ pre {
overflow: inherit;
}
.dashboard .nav-link {
.lms-nav .nav-link {
color: var(--text-muted);
padding: 0 0 var(--padding-md);
margin-right: var(--margin-xl);
padding: var(--padding-md) 0;
margin: 0 var(--margin-md);
}
.dashboard .nav-link.active {
.lms-nav .nav-link.active {
font-weight: 600;
border-bottom: 1px solid var(--primary);
color: var(--text-color);
}
.dashboard .nav-link:hover {
@media (min-width: 500px) {
.lms-nav .nav-item:first-child .nav-link {
margin-left: 0;
}
}
.lms-nav .nav-link:hover {
color: inherit;
}
@@ -1541,7 +1545,7 @@ li {
}
}
[contenteditable] {
[contenteditable="true"] {
outline: none;
background-color: var(--bg-light-gray);
border-radius: var(--border-radius);
@@ -1550,7 +1554,7 @@ li {
color: var(--gray-900);
}
[contenteditable]:empty:before {
[contenteditable="true"]:empty:before {
content: attr(data-placeholder);
color: var(--gray-600);
}
@@ -1588,7 +1592,7 @@ li {
padding: 0.5rem 0;
}
.course-card-pills[contenteditable] {
.course-card-pills[contenteditable="true"] {
box-shadow: none;
}
@@ -1658,6 +1662,49 @@ li {
padding: 1rem;
}
.help-article {
.medium {
font-size: var(--text-base);
}
.quiz-row {
position: relative;
color: var(--text-color);
cursor: pointer;
}
.course-creation-link {
float: right;
}
@media (max-width: 500px) {
.course-creation-link {
float: inherit;
margin-bottom: 1rem;
}
}
.indicator-pill::before {
width: 0;
height: 0;
margin-right: 0;
}
.review-link {
float: right
}
.role {
margin-bottom: 0;
cursor: pointer;
}
@media (min-width: 500px) {
.role:last-child {
margin-left: 5rem
}
}
.icon-xl {
width: 2.75rem;
height: 2.75rem;
}

View File

@@ -67,7 +67,7 @@ const join_course = (e) => {
}, 3);
setTimeout(function () {
window.location.href = `/courses/${course}/learn/1.1`;
}, 3000);
}, 1000);
}
}
})
@@ -127,7 +127,7 @@ const scroll_to_chapter_container = () => {
scrollTop: $(".new-chapter").offset().top
}, 1000);
$(".new-chapter").find(".chapter-title-main").focus();
}
};
const save_chapter = (e) => {
@@ -150,7 +150,7 @@ const save_chapter = (e) => {
});
setTimeout(() => {
window.location.reload();
}, 1000)
}, 1000);
}
});
};

45
lms/subscription_utils.py Normal file
View File

@@ -0,0 +1,45 @@
import frappe
@frappe.whitelist(allow_guest=True)
def get_add_on_details(plan: str) -> dict[str, int]:
"""
Returns the number of courses and course members to be billed under add-ons for SAAS subscription
"""
return {
"courses": get_add_on_courses(plan),
"members": get_add_on_members(plan)
}
def get_published_courses() -> int:
return frappe.db.count("LMS Course", {"published": 1})
def get_add_on_courses(plan: str) -> int:
COURSE_LIMITS = {"Lite": 5, "Pro": 20}
add_on_courses = 0
courses_included_in_plans = COURSE_LIMITS.get(plan)
if courses_included_in_plans:
published_courses = get_published_courses()
add_on_courses = published_courses - courses_included_in_plans if published_courses > courses_included_in_plans else 0
return add_on_courses
def get_add_on_members(plan: str) -> int:
MEMBER_LIMITS = {"Lite": 100, "Pro": 500}
add_on_members = 0
members_included_in_plans = MEMBER_LIMITS.get(plan)
if members_included_in_plans:
active_members = get_members()
add_on_members = active_members - members_included_in_plans if active_members > members_included_in_plans else 0
return add_on_members
def get_members() -> int:
return frappe.db.count("LMS Batch Membership")

View File

@@ -1,38 +1,44 @@
<div id="certificate-card" style="background: #FFFFFF; border-radius: 0.5rem; position: relative;
box-shadow: 0px 1px 2px rgba(25, 39, 52, 0.05), 0px 0px 4px rgba(25, 39, 52, 0.1); padding: 1rem;">
<div style="border: 10px solid var(--primary-color); display: flex; flex-direction: column; align-items: center; padding: 4rem;
justify-content: center; background-color: #FFFFFF;">
<img src="{{ logo }}" style="height: 1.5rem;">
<div style="margin-top: 4rem;">
{{ _("This certifies that") }}
</div>
<div style="font-size: 2rem; font-weight: 500; color: #192734;"> {{ member.full_name }} </div>
<div style="margin-top: 0.5rem;"> {{ _("has successfully completed the course on") }}
<b> {{ course.title }} </b> on {{ frappe.utils.format_date(certificate.issue_date, "medium") }}. </div>
<div style="font-size: 2rem; font-weight: 500; color: #192734;">
{{ member.full_name }}
</div>
<div style="margin-top: 0.5rem;">
{{ _("has successfully completed the course on") }}
<b> {{ course.title }} </b>
on {{ frappe.utils.format_date(certificate.issue_date, "medium") }}.
</div>
<div style="display: flex; justify-content: center; margin: 4rem auto 0; width: fit-content;">
{% if instructors %}
<div>
<div style="color: #192734; font-weight: bold; font-family: cursive; font-size: 1.25rem;">
{{ instructors }}
</div>
<hr style="margin: 0.5rem 0;">
<div> {{ _("Course Instructor") }} </div>
<div class="text-center"> {{ _("Course Instructor") }} </div>
</div>
{% endif %}
{% if certificate.expiry_date %}
<div style="margin-left: 2rem;">
<div style="color: #192734; font-weight: bold; font-family: cursive; font-size: 1.25rem;">
{{ frappe.utils.format_date(certificate.expiry_date, "medium") }}
</div>
<hr style="margin: 0.5rem 0;">
<div> {{ _("Expiry date") }} </div>
<div style="color: #192734; font-weight: bold; font-family: cursive; font-size: 1.25rem;">
{{ frappe.utils.format_date(certificate.expiry_date, "medium") }}
</div>
<hr style="margin: 0.5rem 0;">
<div class="text-center"> {{ _("Expiry date") }} </div>
</div>
{% endif %}
</div>
</div>
</div>

View File

@@ -0,0 +1,30 @@
{% set certificates = get_certificates(user) %}
{% if certificates | length %}
<div class="cards-parent">
{% for certificate in certificates %}
{% set course = frappe.db.get_value("LMS Course", certificate.course, ["title", "name", "image"], as_dict=True) %}
<div class="common-card-style column-card">
<div class="font-weight-bold">
{{ course.title }}
</div>
<div>
{{ _("Issued on") }} : {{ frappe.utils.format_date(certificate.issue_date, "medium") }}
</div>
<a class="stretched-link" href="/courses/{{ course.name }}/{{ certificate.name }}"></a>
</div>
{% endfor %}
</div>
{% else %}
{% set course_list_link = "<a href='/courses'>course list</a>" %}
<div class="empty-state">
<img class="icon icon-xl" src="/assets/lms/icons/comment.svg">
<div class="empty-state-text">
<div class="empty-state-heading">{{ _("No certificates") }}</div>
<div class="course-meta">{{ _("Check out the {0} to know more about certification.").format(course_list_link) }}</div>
</div>
</div>
{% endif %}

View File

@@ -1,4 +1,4 @@
{% set courses = get_authored_courses(frappe.session.user, only_published=False) %}
{% set courses = get_authored_courses(user or None, only_published or False) %}
{% if courses | length %}
<div class="cards-parent">
@@ -9,9 +9,7 @@
{% else %}
<div class="empty-state">
<div>
<img class="icon icon-xl" src="/assets/lms/icons/comment.svg">
</div>
<img class="icon icon-xl" src="/assets/lms/icons/comment.svg">
<div class="empty-state-text">
<div class="empty-state-heading">{{ _("No courses created") }}</div>
<div class="course-meta">{{ _("Help others learn something new.") }}</div>

View File

@@ -0,0 +1,18 @@
{% set courses = get_courses_under_review() %}
{% if courses | length %}
<div class="cards-parent">
{% for course in courses %}
{{ widgets.CourseCard(course=course) }}
{% endfor %}
</div>
{% else %}
<div class="empty-state">
<img class="icon icon-xl" src="/assets/lms/icons/comment.svg">
<div class="empty-state-text">
<div class="empty-state-heading">{{ _("No courses under review") }}</div>
<div class="course-meta">{{ _("When a course gets submitted for review, it will be listed here.") }}</div>
</div>
</div>
{% endif %}

View File

@@ -48,25 +48,25 @@
<script>
frappe.ready(() => {
$("#confirm").click((e) => {
frappe.call({
"method": "lms.lms.doctype.lms_batch_membership.lms_batch_membership.create_membership",
"args": {
"batch": {{ batch.name }},
"course": {{ batch.course }}
},
"callback": (data) => {
if (data.message == "OK") {
frappe.msgprint({
message: __("You are now a member of this batch!"),
clear: true
});
setTimeout(function () {
window.location.href = "/courses/{{ batch.course }}/home";
}, 2000);
}
}
})
})
})
frappe.call({
"method": "lms.lms.doctype.lms_batch_membership.lms_batch_membership.create_membership",
"args": {
"batch": {{ batch.name }},
"course": {{ batch.course }}
},
"callback": (data) => {
if (data.message == "OK") {
frappe.msgprint({
message: __("You are now a member of this batch!"),
clear: true
});
setTimeout(function () {
window.location.href = "/courses/{{ batch.course }}/home";
}, 1000);
}
}
});
});
});
</script>
{% endblock %}

View File

@@ -33,7 +33,9 @@
</div>
<div class="lesson-pagination-parent">
{{ LessonContent(lesson) }}
{% if not lesson.edit_mode %} {{ Discussions() }} {% endif %}
{% if not lesson.edit_mode and course.status == "Approved" and not course.upcoming %}
{{ Discussions() }}
{% endif %}
</div>
</div>
</div>
@@ -57,6 +59,7 @@
{% macro LessonContent(lesson) %}
{% set instructors = get_instructors(course.name) %}
{% set is_instructor = is_instructor(course.name) %}
<div class="common-card-style lesson-content">
<div class="lesson-title">
@@ -67,10 +70,10 @@
data-index="{{ lesson_index }}" data-course="{{ course.name }}" data-chapter="{{ chapter }}"
{% if lesson.name %} data-lesson="{{ lesson.name }}" {% endif %}
>{% if lesson.title %}{{ lesson.title }}{% endif %}</div>
<span class="lesson-progress {{ hide if get_progress(course.name, lesson.name) != 'Complete' else ''}}">{{ _("COMPLETED") }}</span>
<span class="indicator-pill green {{ hide if get_progress(course.name, lesson.name) != 'Complete' else ''}}">{{ _("COMPLETED") }}</span>
<!-- Edit Button -->
{% if is_instructor and not lesson.edit_mode %}
{% if (is_instructor or has_course_moderator_role()) and not lesson.edit_mode %}
<button class="button is-default button-links ml-auto btn-edit"> {{ _("Edit") }} </button>
{% endif %}
</div>
@@ -90,12 +93,14 @@
{% endfor %}
<a class="button-links ml-1" href="{{ get_profile_url(instructors[0].username) }}">
<span class="course-meta">
{% 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 %}
{% if ins_len == 1 %}
{{ instructors[0].full_name }}
{% elif ins_len == 2 %}
{{ instructors[0].full_name.split(" ")[0] }} and {{ instructors[1].full_name.split(" ")[0] }}
{% else %}
{% set suffix = "other" if ins_len - 1 == 1 else "others" %}
{{ instructors[0].full_name.split(" ")[0] }} and {{ ins_len - 1 }} {{ suffix }}
{% endif %}
</span>
</a>
<div class="ml-5 course-meta"> {{ frappe.utils.format_date(lesson.creation, "medium") }} </div>
@@ -103,7 +108,7 @@
<!-- Lesson Content -->
<div class="markdown-source lesson-content-card {% if lesson.edit_mode %} mb-0 mt-2 {% endif %} ">
{% if membership or lesson.include_in_preview or is_instructor %}
{% if show_lesson %}
{% if is_instructor and not lesson.include_in_preview and not lesson.edit_mode %}
<div class="small alert alert-secondary alert-dismissible mb-4">
@@ -115,13 +120,17 @@
{% if lesson.edit_mode %}
{{ EditLesson(lesson) }}
{% else %}
{{ render_html(lesson.body) }}
{{ render_html(lesson.body, lesson.youtube, lesson.quiz_id) }}
{% endif %}
{% else %}
{% set course_link = "<a class='join-batch' data-course=" + course.name | urlencode + " href=''>" + _('here') + "</a>" %}
<div class="">
<div class="btn btn-primary pull-right join-batch" data-course="{{ course.name | urlencode }}"> {{ _("Start Learning") }} </div>
<div class=""> {{ _("This lesson is not available for preview. Please join the course to access it.") }} </div>
<div>
{{ _("There is no preview available for this lesson.
Please join the course to access it.
Click {0} to enroll.").format(course_link) }}
</div>
</div>
{% endif %}
</div>
@@ -140,14 +149,14 @@
<div>
{% if prev_url %}
<a class="btn btn-secondary dark-links prev" href="{{ prev_url }}">
<img class="mr-2" src="/assets/lms/icons/left-arrow.svg">
{{ _("Prev") }}
{{ _("Previous") }}
</a>
{% endif %}
</div>
{% if not is_mentor(course.name, frappe.session.user) and membership %}
{% set progress = get_progress(course.name, lesson.name) %}
{% if not is_mentor(course.name, frappe.session.user) and membership %}
<div class="custom-checkbox {% if progress == 'Complete' %} hide {% endif %}">
<label class="quiz-label">
<input class="mark-progress" type="checkbox" checked>
@@ -155,18 +164,19 @@
<span class="small">{{ _("Mark as complete on moving to the next lesson") }}</span>
</label>
</div>
<div class="btn btn-default mark-progress {{ progress }} {% if progress == 'Incomplete' or progress == None %} hide {% endif %}"
data-progress="Incomplete">
{{ _("Mark as Incomplete") }}
</div>
{% endif %}
<div>
<a class="btn btn-primary next {% if not next_url and (membership.progress|int == 100 or is_instructor) %} hide {% endif %}"
{% if next_url %} data-href="{{ next_url }}" {% endif %} href="">
{% if next_url %} {{ _("Next") }} {% else %} {{ _("Mark as Complete") }} {% endif %}
<img class="ml-2" src="/assets/lms/icons/side-arrow-white.svg">
{% if not is_mentor(course.name, frappe.session.user) and membership %}
<div class="btn btn-default mark-progress {{ progress }} {% if progress == 'Incomplete' or progress == None %} hide {% endif %}"
data-progress="Incomplete">
{{ _("Mark as Incomplete") }}
</div>
{% endif %}
<a class="btn btn-primary next ml-2 {% if not next_url and (membership.progress|int == 100 or is_instructor) %} hide {% endif %}"
{% if next_url %} data-href="{{ next_url }}" {% endif %} href="">
{% if next_url %} {{ _("Next") }} {% else %} {{ _("Mark as Complete") }} {% endif %}
</a>
</div>
@@ -176,20 +186,34 @@
<!-- Edit Lesson -->
{% macro EditLesson(lesson) %}
<div id="body" {% if lesson.body %} data-body="{{ lesson.body }}" {% endif %}></div>
<label class="preview">
<input {% if lesson.include_in_preview %} checked {% endif %} type="checkbox"
id="preview"> {{ _("Show preview of this lesson to Guest users.") }}
</label>
<div class="medium mt-2" contenteditable="true" data-placeholder="{{ _('YouTube Video ID') }}"
id="youtube">{% if lesson.youtube %}{{ lesson.youtube }}{% endif %}</div>
<div id="body" {% if lesson.body %} data-body="{{ lesson.body }}" {% endif %}></div>
<div class="medium mb-4" contenteditable="true" data-placeholder="{{ _('Quiz ID') }}"
id="quiz-id">{% if lesson.quiz_id %}{{ lesson.quiz_id }}{% endif %}</div>
<div class="mt-4">
<button class="btn btn-primary btn-sm btn-lesson pull-right ml-2"> {{ _("Save") }} </button>
{% if lesson.name %}
<button class="btn btn-secondary btn-sm pull-right btn-back ml-2"> {{ _("Back to Lesson") }} </button>
<a class="btn btn-secondary btn-sm pull-right" href="/quizzes"> {{ _("Create Quiz") }} </a>
{% endif %}
<label class="preview" for="preview">
<input {% if lesson.include_in_preview %} checked {% endif %} type="checkbox" id="preview">
{{ _("Show preview of this lesson to Guest users.") }}
</label>
<div class="mt-4">
<button class="btn btn-primary btn-sm btn-lesson pull-right ml-2"> {{ _("Save") }} </button>
{% if lesson.name %}
<button class="btn btn-secondary btn-sm pull-right btn-back ml-2"> {{ _("Back to Lesson") }} </button>
<a class="btn btn-secondary btn-sm pull-right" href="/quizzes"> {{ _("Create Quiz") }} </a>
{% endif %}
{{ UploadAttachments() }}
</div>
{{ HelpArticle() }}
{% endmacro %}
{% macro UploadAttachments() %}
<div class="attachments-parent">
<div class="attachment-controls">
<div class="show-attachments" data-toggle="collapse" data-target="#collapse-attachments" aria-expanded="false">
@@ -209,94 +233,111 @@
</div>
<table class="attachments common-card-style collapse hide" id="collapse-attachments"></table>
</div>
</div>
{{ HelpArticle() }}
{% endmacro %}
<!-- Help Article -->
{% macro HelpArticle() %}
<div class="help-article">
<h3> {{ _("Help Article") }} </h3>
<p>
{{ _("You can add additional content to the lesson using a special syntax. The table below mentions
all types of dynamic content that you can add to the lessons and the syntax for the same.") }}
</p>
<table class="table w-100">
<tr>
<th style="width: 20%;"> {{ _("Content Type") }} </th>
<th style="width: 40%;"> {{ _("Syntax") }} </th>
<th> {{ _("Description") }} </th>
</tr>
<tr>
<td>
{{ _("YouTube Video") }}
</td>
<td>
{% raw %} {{ YouTubeVideo("embed_src") }} {% endraw %}
</td>
<td>
<span>
{{ _("Copy and paste the syntax in the editor. Replace 'embed_src' with the embed source
that YouTube provides. To get the source, follow the steps mentioned below.") }}
</span>
<ul class="p-4">
<div class="medium">
<h3> {{ _("Embed Components") }} </h3>
<p>
{{ _("You can add additional content to the lesson using a special syntax. The table below mentions
all types of dynamic content that you can add to the lessons and the syntax for the same.") }}
</p>
<ol>
<li>
<b> {{ _("YouTube Video") }} </b>
<p> To get the YouTube Video ID, follow the steps mentioned below. </p>
<ul class="px-4">
<li>
{{ _("Upload the video on youtube.") }}
</li>
<li>
{{ _("When you share a youtube video, it shows an option called Embed.") }}
{{ _("When you share a youtube video, it shows a URL") }}
</li>
<li>
{{ _("On clicking it, it provides an iframe. Copy the source (src) of the iframe and
paste it here.") }}
{{ _("Copy the last parameter of the URL and paste it here.") }}
</li>
</ul>
</td>
</tr>
<tr>
<td>
{{ _("Quiz") }}
</td>
<td>
{% raw %} {{ Quiz("lms_quiz_id") }} {% endraw %}
</td>
<td>
{% set quiz_link = "<a href='/quizzes'> Quiz List </a>" %}
{{ _("Copy and paste the syntax in the editor. Replace 'lms_quiz_id' with the ID of the Quiz. You can get the ID of the quiz from the {0}.").format(quiz_link) }}
</td>
</tr>
</table>
</div>
</li>
</ol>
<!-- <table class="table w-100">
<tr>
<th style="width: 20%;"> {{ _("Content Type") }} </th>
<th style="width: 40%;"> {{ _("Syntax") }} </th>
<th> {{ _("Description") }} </th>
</tr>
<tr>
<td>
{{ _("YouTube Video") }}
</td>
<td>
{% raw %} {{ YouTubeVideo('Video ID') }} {% endraw %}
</td>
<td>
<span>
{{ _("Copy and paste the syntax in the editor. Replace 'Video ID' with the embed source
that YouTube provides. To get the source, follow the steps mentioned below.") }}
</span>
<ul class="p-4">
<li>
{{ _("Upload the video on youtube.") }}
</li>
<li>
{{ _("When you share a youtube video, it shows an option called Embed.") }}
</li>
<li>
{{ _("On clicking it, it provides an iframe. Copy the source (src) of the iframe and
paste it here.") }}
</li>
</ul>
</td>
</tr>
<tr>
<td>
{{ _("Quiz") }}
</td>
<td>
{% raw %} {{ Quiz('Quiz ID') }} {% endraw %}
</td>
<td>
{% set quiz_link = "<a href='/quizzes'> Quiz List </a>" %}
{{ _("Copy and paste the syntax in the editor. Replace 'Quiz ID' with the Id of the Quiz.
You can get the Id of the quiz from the {0}.").format(quiz_link) }}
</td>
</tr>
</table> -->
</div>
{% endmacro %}
<!-- Discussions Component -->
{% macro Discussions() %}
{% set topics_count = frappe.db.count("Discussion Topic",
{"reference_doctype": "Course Lesson", "reference_docname": lesson.name}) %}
{% set is_instructor = frappe.session.user == course.instructor %}
{% set condition = is_instructor if is_instructor else membership %}
{% set doctype, docname = _("Course Lesson"), lesson.name %}
{% set title = "Questions" if topics_count else "" %}
{% set cta_title = "Ask a Question" %}
{% set button_name = _("Start Learning") %}
{% set redirect_to = "/courses/" + course.name %}
{% set empty_state_title = _("Have a doubt?") %}
{% set empty_state_subtitle = _("Post it here, our mentors will help you out.") %}
{% include "frappe/templates/discussions/discussions_section.html" %}
{% set topics_count = frappe.db.count("Discussion Topic", {
"reference_doctype": "Course Lesson",
"reference_docname": lesson.name
}) %}
{% set is_instructor = frappe.session.user == course.instructor %}
{% set condition = is_instructor if is_instructor else membership %}
{% set doctype, docname = _("Course Lesson"), lesson.name %}
{% set title = "Questions" if topics_count else "" %}
{% set cta_title = "Ask a Question" %}
{% set button_name = _("Start Learning") %}
{% set redirect_to = "/courses/" + course.name %}
{% set empty_state_title = _("Have a doubt?") %}
{% set empty_state_subtitle = _("Post it here, our mentors will help you out.") %}
{% include "frappe/templates/discussions/discussions_section.html" %}
{% endmacro %}
<!-- Scripts -->
{%- block script %}
{{ super() }}
<script type="text/javascript">
var page_context = {{ page_context | tojson }};
</script>
{{ include_script('controls.bundle.js') }}
{% for ext in page_extensions %}
{{ ext.render_footer() }}
{% endfor %}
{{ super() }}
<script type="text/javascript">
var page_context = {{ page_context | tojson }};
</script>
{{ include_script('controls.bundle.js') }}
{% for ext in page_extensions %}
{{ ext.render_footer() }}
{% endfor %}
{%- endblock %}

View File

@@ -66,7 +66,7 @@ frappe.ready(() => {
$(".btn-back").click((e) => {
window.location.href = window.location.href.split("?")[0];
})
});
$(document).on("click", ".copy-link", (e) => {
frappe.utils.copy_to_clipboard($(e.currentTarget).data("link"));
@@ -490,14 +490,22 @@ const save_lesson = (e) => {
method: "lms.lms.doctype.lms_course.lms_course.save_lesson",
args: {
"title": $("#title").text(),
"body": this.code_field_group.fields_dict["code_md"].last_value,
"body": this.code_field_group.fields_dict["code_md"].value,
"youtube": $("#youtube").text(),
"quiz_id": $("#quiz-id").text(),
"chapter": $("#title").data("chapter"),
"preview": $("#preview").prop("checked") ? 1 : 0,
"idx": $("#title").data("index"),
"lesson": lesson ? lesson : ""
},
callback: (data) => {
window.location.href = window.location.href.split("?")[0];
callback: (data) => {;
frappe.show_alert({
message: __("Saved"),
indicator: "green",
});
setTimeout(() => {
window.location.href = window.location.href.split("?")[0];
}, 1000);
}
});
};

View File

@@ -1,6 +1,6 @@
import frappe
from lms.www.utils import get_common_context, redirect_to_lesson
from lms.lms.utils import get_lesson_url, is_instructor, redirect_to_courses_list
from lms.lms.utils import get_lesson_url, has_course_moderator_role, is_instructor, redirect_to_courses_list
from frappe.utils import cstr, flt
def get_context(context):
@@ -23,11 +23,14 @@ def get_context(context):
redirect_to_lesson(context.course, index_)
context.lesson = get_current_lesson_details(lesson_number, context)
instructor = is_instructor(context.course.name)
context.show_lesson = context.membership or (context.lesson and context.lesson.include_in_preview) or instructor or has_course_moderator_role()
if not context.lesson:
context.lesson = frappe._dict()
if frappe.form_dict.get("edit"):
if not is_instructor(context.course.name):
if not instructor and not has_course_moderator_role():
redirect_to_courses_list()
context.lesson.edit_mode = True
else:

View File

@@ -52,11 +52,13 @@
<div class="d-flex justify-content-between option-{{ num }}">
<div contenteditable="true" data-placeholder="{{ _('Option') }}"
class="option-input">{% if option %}{{ option }}{% endif %}</div>
<div contenteditable="true" data-placeholder="{{ _('Explanation') }}"
<div contenteditable="true" data-placeholder="{{ _('Explain the option') }}"
class="option-input">{% if explanation %}{{ explanation }}{% endif %}</div>
<div class="option-checkbox">
<input type="checkbox" {% if question['is_correct_' + num] %} checked {% endif %}>
<label class="mb-0"> {{ _("Is Correct") }} </label>
<label class="mb-0">
<input type="checkbox" {% if question['is_correct_' + num] %} checked {% endif %}>
{{ _("Is Correct") }}
</label>
</div>
</div>
</div>
@@ -68,8 +70,17 @@
<div class="mt-4">
<button class="btn btn-secondary btn-sm btn-question"> {{ _("New Question") }} </button>
<button class="btn btn-primary btn-sm btn-save-question ml-2
{% if not quiz.name %} hide {% endif %}"> {{ _("Save Quiz") }} </button>
{% if quiz.name %}
<button class="btn btn-secondary btn-sm copy-quiz-id ml-2" data-name="'{{ quiz.name }}'">
{{ _("Copy Quiz ID") }}
</button>
{% endif %}
<button class="btn btn-primary btn-sm btn-save-question ml-2 {% if not quiz.name %} hide {% endif %}">
{{ _("Save Quiz") }}
</button>
</div>
</div>
{% endmacro %}

View File

@@ -1,21 +1,34 @@
frappe.ready(() => {
if(!$(".quiz-card").length) {
add_question();
}
$(".btn-question").click((e) => {
add_question(e);
add_question();
});
$(".btn-save-question").click((e) => {
save_question(e);
});
get_questions()
$(".copy-quiz-id").click((e) => {
frappe.utils.copy_to_clipboard($(e.currentTarget).data("name"));
});
get_questions();
});
const add_question = (e) => {
const add_question = () => {
if ($(".new-quiz-card").length) {
scroll_to_question_container();
return;
}
let add_after = $(".quiz-card").length ? $(".quiz-card:last") : $("#quiz-title");
let question_template = `<div class="quiz-card">
let question_template = `<div class="quiz-card new-quiz-card">
<div contenteditable="true" data-placeholder="${__("Question")}" class="question mb-4"></div>
</div>`;
$(question_template).insertAfter(add_after);
@@ -54,6 +67,7 @@ const save_question = (e) => {
if (!$("#quiz-title").text()) {
frappe.throw(__("Quiz Title is mandatory."));
}
frappe.call({
method: "lms.lms.doctype.lms_quiz.lms_quiz.save_quiz",
args: {
@@ -104,3 +118,11 @@ const get_questions = () => {
return questions;
};
const scroll_to_question_container = () => {
$([document.documentElement, document.body]).animate({
scrollTop: $(".new-quiz-card").offset().top
}, 1000);
$(".new-quiz-card").find(".question").focus();
}

View File

@@ -12,37 +12,55 @@
{% block content %}
<div class="common-page-style">
<div class="container">
<a class="btn btn-secondary btn-sm pull-right" href="/quizzes/new-quiz">
{{ _("Add Quiz") }}
</a>
<div class="course-home-headings">
{{ _("Quiz List") }}
</div>
{% if quiz_list | length %}
<a class="btn btn-secondary btn-sm pull-right" href="/quizzes/new-quiz"> {{ _("Add Quiz") }} </a>
<div class="course-home-headings"> {{ _("Quiz List") }} </div>
<div class="common-card-style">
<table class="table">
<tr style="background-color: var(--fg-hover-color); font-weight: bold">
<td style="width: 10%;"> {{ _("No.") }} </td>
<td style="width: 45%;"> {{ _("Title") }} </td>
<td> {{ _("ID") }} </td>
<td> </td>
</tr>
{% for quiz in quiz_list %}
<tr style="position: relative; color: var(--text-color);">
<tr class="quiz-row" data-name="{{ quiz.name }}">
<td> {{ loop.index }} </td>
<td>
<a class="button-links" href="/quizzes/{{ quiz.name }}">{{ quiz.title }}</a>
{{ quiz.title }}
</td>
<td>
<a class="button-links" href="/quizzes/{{ quiz.name }}">{{ quiz.name }}</a>
{{ quiz.name }}
</td>
<td>
<a class="btn btn-secondary btn-sm copy-quiz-id" data-name="'{{ quiz.name }}'">
{{ _("Copy Quiz ID") }}
</a>
</td>
</tr>
{% endfor %}
</table>
</div>
{% else %}
<div class="empty-state">
<img class="icon icon-xl" src="/assets/lms/icons/comment.svg">
<div class="empty-state-text">
<div class="empty-state-heading">{{ _("You have not created any quiz yet.") }}</div>
<div class="course-meta mb-6">{{ _("Create a quiz and add it to your course to engage users.") }}</div>
<a class="btn btn-secondary btn-sm"
href="{% if frappe.session.user == 'Guest' %} /login?redirect-to=/quizzes {% else %} /quizzes/new-quiz {% endif %}">
{{ _("Add Quiz") }} </a>
<div class="empty-state-heading">
{{ _("You have not created any quiz yet.") }}
</div>
<div class="course-meta ">
{{ _("Create a quiz and add it to your course to engage your users.") }}
</div>
</div>
</div>
{% endif %}
@@ -50,3 +68,22 @@
</div>
{% endblock %}
{% block script %}
<script>
frappe.ready(() => {
$(".copy-quiz-id").click((e) => {
e.preventDefault();
frappe.utils.copy_to_clipboard($(e.currentTarget).data("name"));
});
$(".quiz-row").click((e) => {
if (!$(e.target).hasClass("copy-quiz-id")) {
window.location.href = `/quizzes/${$(e.currentTarget).data('name')}`;
}
});
});
</script>
{% endblock %}

View File

@@ -5,7 +5,7 @@
<div class="common-page-style">
<div class="container">
<input class="search" id="search-user" placeholder="{{ _('Try a Name, Company, or Industry') }}">
<input class="search pull-right" id="search-user" placeholder="{{ _('Search') }}">
<div class="course-home-headings">{{ _("Community") }} </div>

View File

@@ -16,9 +16,9 @@
<a class="dark-links" href="/courses/{{ course.name }}">{{ course.title }}</a>
</div>
{% if custom_template %}
{{ custom_template }}
{{ custom_template }}
{% else %}
{% include "lms/templates/certificate.html" %}
{% include "lms/templates/certificate.html" %}
{% endif %}
<script src="/assets/lms/js/html2canvas.js"></script>

View File

@@ -19,8 +19,7 @@ def get_context(context):
context.course = frappe.db.get_value("LMS Course", course_name, ["title", "name", "image"], as_dict=True)
context.instructors = (", ").join([x.full_name for x in get_instructors(course_name)])
context.member = frappe.db.get_value("User", context.certificate.member,
["full_name"], as_dict=True)
context.member = frappe.db.get_value("User", context.certificate.member, ["full_name"], as_dict=True)
context.logo = frappe.db.get_single_value("Website Settings", "banner_image")
template_name = frappe.db.get_single_value("LMS Settings", "custom_certificate_template")

View File

@@ -6,7 +6,6 @@
{% block head_include %}
{% include "public/icons/symbol-defs.svg" %}
<link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
{% endblock %}
@@ -18,10 +17,11 @@
<div class="container">
<div class="course-body-container">
{{ CourseHeaderOverlay(course) }}
{{ CourseSettings(course) }}
{{ Description(course) }}
{{ Save(course) }}
{{ widgets.CourseOutline(course=course, membership=membership, is_user_interested=is_user_interested) }}
{% if not course.edit_mode %}
{% if not course.edit_mode and course.status == "Approved" and not frappe.utils.cint(course.upcoming) %}
{{ widgets.Reviews(course=course, membership=membership) }}
{% endif %}
</div>
@@ -114,7 +114,9 @@
</div>
<div class="course-image-attachment {% if not course.image %} hide {% endif %} ">
<a href="{{ course.image }}" id="image" target="_blank"> {{ course.image }} </a>
<a {% if course.image %} href="{{ course.image }}" {% endif %} id="image" target="_blank">
{{ course.image }}
</a>
<button class="btn btn-sm btn-default btn-clear ml-4"> {{ _("Clear") }} </button>
</div>
<a class="btn btn-default btn-sm btn-attach mt-1 {% if course.image %} hide {% endif %}"> {{ _("Attach Image") }} </a>
@@ -202,9 +204,32 @@
<!-- Description -->
{% macro Description(course) %}
<div class="course-description-section" {% if course.edit_mode %} style="min-height: 100px" {% endif %}
{% if course.edit_mode %} contenteditable="true" {% endif %} id="description"
data-placeholder="Description">{% if course.description %}{{ frappe.utils.md_to_html(course.description) }}{% endif %}</div>
{% if course.edit_mode %}
<div id="description" {% if course.description %} data-description="{{ course.description }}" {% endif %}></div>
{% else %}
<div class="course-description-section">
{{ frappe.utils.md_to_html(course.description) }}
</div>
{% endif %}
{% endmacro %}
<!-- Course Settings -->
{% macro CourseSettings(course) %}
{% if course.edit_mode and has_course_moderator_role() %}
<div class="mb-4">
<label for="published" class="mb-0">
<input type="checkbox" id="published" {% if course.published %} checked {% endif %}>
{{ _("Published") }}
</label>
<label for="upcoming" class="mb-0 ml-20">
<input type="checkbox" id="upcoming" {% if course.upcoming %} checked {% endif %}>
{{ _("Upcoming") }}
</label>
</div>
{% endif %}
{% endmacro %}
@@ -225,23 +250,6 @@
{% endmacro %}
{% macro CourseCreator(course) %}
<div class="course-home-headings"> {{ _("Course Creators") }} </div>
<div class="common-card-style course-creators-card">
{% set instructors = get_instructors(course.name) %}
{% for instructor in instructors %}
<div class="d-flex align-items-center">
{{ widgets.Avatar(member=instructor, avatar_class="avatar-medium") }}
<div class="ml-4">
<div class="course-creator-name"> {{ instructor.full_name }} </div>
<div class="course-meta"> {{ get_authored_courses(instructor.name) | length }} {{ _("Courses Created") }} </div>
</div>
</div>
{% endfor %}
</div>
{% endmacro %}
<!-- Related Courses Section -->
{% macro RelatedCourses(course) %}
{% if course.related_courses | length %}
@@ -285,7 +293,7 @@
membership.current_lesson else "1.1" if first_lesson_exists(course.name) else None %}
{% if show_start_learing_cta %}
<div class="btn btn-primary wide-button join-batch" data-course="{{ course.name | urlencode }}">
<div class="btn btn-primary wide-button join-batch mb-2" data-course="{{ course.name | urlencode }}">
{{ _("Start Learning") }}
<img class="ml-2" src="/assets/lms/icons/white-arrow.svg" />
</div>
@@ -301,7 +309,7 @@
{{ _("Checkout Course") }} <img class="ml-2" src="/assets/lms/icons/white-arrow.svg" />
</a>
{% elif course.upcoming and not is_user_interested %}
{% elif course.upcoming and not is_user_interested and not is_instructor %}
<div class="btn btn-secondary wide-button notify-me" data-course="{{course.name | urlencode}}">
{{ _("Notify me when available") }}
</div>
@@ -338,7 +346,7 @@
{% endif %}
{% endif %}
{% if is_instructor(course.name) %}
{% if is_instructor(course.name) or has_course_moderator_role() %}
<a class="btn btn-secondary wide-button" href="/courses/{{ course.name }}?edit=1"> {{ _("Edit Course") }} </a>
{% endif %}
{% endmacro %}
@@ -356,9 +364,9 @@
frappe.utils.format_time(certificate_request.start_time, "short")) }} </p>
{% endif %}
{% if course.status == "Under Review" %}
{% if course.status == "Under Review" and is_instructor(course.name) %}
<div class="mb-4">
{{ _("Your course is currently under review. Once the review is complete, the System Admins will publish it on the website.") }}
{{ _("This course is currently under review. Once the review is complete, the System Admins will publish it on the website.") }}
</div>
{% endif %}

View File

@@ -70,6 +70,10 @@ frappe.ready(() => {
remove_tag(e);
});
if ($("#description").length) {
make_editor();
}
});
@@ -206,7 +210,7 @@ const submit_for_review = (e) => {
}, 3);
setTimeout(() => {
window.location.reload();
}, 3000);
}, 1000);
}
}
});
@@ -329,6 +333,7 @@ const add_tag = (e) => {
const save_course = (e) => {
let tags = $('.course-card-pills').map((i, el) => $(el).text().trim()).get();
tags = tags.filter(word => word.trim().length > 0);
frappe.call({
method: "lms.lms.doctype.lms_course.lms_course.save_course",
args: {
@@ -337,8 +342,10 @@ const save_course = (e) => {
"short_introduction": $("#intro").text(),
"video_link": $("#video-link").text(),
"image": $("#image").attr("href"),
"description": $("#description").text(),
"course": $("#title").data("course") ? $("#title").data("course") : ""
"description": this.code_field_group.fields_dict["code_md"].value,
"course": $("#title").data("course") ? $("#title").data("course") : "",
"published": $("#published").prop("checked") ? 1 : 0,
"upcoming": $("#upcoming").prop("checked") ? 1 : 0
},
callback: (data) => {
frappe.show_alert({
@@ -356,3 +363,26 @@ const save_course = (e) => {
const remove_tag = (e) => {
$(e.currentTarget).closest(".course-card-pills").remove();
};
const make_editor = () => {
this.code_field_group = new frappe.ui.FieldGroup({
fields: [
{
fieldname: "code_md",
fieldtype: "Code",
options: "Markdown",
wrap: true,
max_lines: Infinity,
min_lines: 20,
default: $("#description").data("description"),
depends_on: 'eval:doc.type=="Markdown"',
}
],
body: $("#description").get(0),
});
this.code_field_group.make();
$("#description .form-section:last").removeClass("empty-section");
$("#description .frappe-control").removeClass("hide-control");
$("#description .form-column").addClass("p-0");
};

View File

@@ -1,6 +1,6 @@
import frappe
from lms.lms.doctype.lms_settings.lms_settings import check_profile_restriction
from lms.lms.utils import get_membership, is_instructor, is_certified, get_evaluation_details, redirect_to_courses_list
from lms.lms.utils import get_membership, has_course_moderator_role, is_instructor, is_certified, get_evaluation_details, redirect_to_courses_list
def get_context(context):
context.no_cache = 1
@@ -28,7 +28,7 @@ def set_course_context(context, course_name):
as_dict=True)
if frappe.form_dict.get("edit"):
if not is_instructor(course.name):
if not is_instructor(course.name) and not has_course_moderator_role():
redirect_to_courses_list()
course.edit_mode = True
@@ -73,4 +73,4 @@ def get_user_interest(course):
def show_start_learing_cta(course, membership, restriction):
return not course.disable_self_learning and not membership and not course.upcoming and not restriction.get("restrict") and not is_instructor(course.name)
return not course.disable_self_learning and not membership and not course.upcoming and not restriction.get("restrict") and not is_instructor(course.name) and course.status == "Approved"

View File

@@ -6,29 +6,33 @@
{% block content %}
<div class="common-page-style">
<div class="container">
{% if restriction.restrict %}
{% set profile_link = "<a href='/edit-profile'> profile </a>" %}
<div class="empty-state">
<div class="course-home-headings text-center mb-0" style="color: inherit;">{{ _("You haven't completed your profile.") }}</div>
<p class="small text-center">{{ _("Complete your {0} to access the courses.").format(profile_link) }}</p>
<div class="container">
{% if restriction.restrict %}
{% set profile_link = "<a href='/edit-profile'> profile </a>" %}
<div class="empty-state">
<div class="course-home-headings text-center mb-0" style="color: inherit;">
{{ _("You haven't completed your profile.") }}
</div>
<p class="small text-center">
{{ _("Complete your {0} to access the courses.").format(profile_link) }}
</p>
</div>
{% else %}
{% include "lms/templates/search_course/search_course.html" %}
<div class="course-list">
{% set courses = live_courses %}
{% set title = _("Live Courses ({0})").format(courses | length) %}
{% set classes = "live-courses" %}
{% include "lms/templates/course_list.html" %}
{% set courses = upcoming_courses %}
{% set title = _("Upcoming Courses ({0})").format(courses | length) %}
{% set classes = "upcoming-courses mt-10" %}
{% include "lms/templates/course_list.html" %}
</div>
{% endif %}
</div>
{% else %}
{% include "lms/templates/search_course/search_course.html" %}
<div class="course-list">
{% set courses = live_courses %}
{% set title = _("Live Courses ({0})").format(courses | length) %}
{% set classes = "live-courses" %}
{% include "lms/templates/course_list.html" %}
{% set courses = upcoming_courses %}
{% set title = _("Upcoming Courses ({0})").format(courses | length) %}
{% set classes = "upcoming-courses mt-10" %}
{% include "lms/templates/course_list.html" %}
</div>
{% endif %}
</div>
</div>
{% endblock %}

View File

@@ -1,28 +1,38 @@
{% extends "templates/base.html" %}
{% block title %}{{ _("Dashboard")}}
{% block title %}
{{ _("Dashboard")}}
{% endblock %}
{% block content %}
{% set portal_course_creation = frappe.db.get_single_value("LMS Settings", "portal_course_creation") %}
{% set show_creators_section = portal_course_creation == "Anyone" or has_course_instructor_role() %}
<div class="common-page-style dashboard">
<div class="container">
{% if show_creators_section %}
<a class="btn btn-secondary btn-md pull-right" id="create-course-link" href="/courses/new-course">
{{ _("Create a Course")}}
<a class="btn btn-secondary btn-sm course-creation-link" id="create-course-link" href="/courses/new-course">
{{ _("Create a Course") }}
</a>
{% endif %}
<ul class="nav" id="courses-tab">
<ul class="nav lms-nav" id="courses-tab">
<li class="nav-item">
<a class="nav-link active" data-toggle="tab" href="#courses-enrolled"> {{ _("Courses Enrolled") }} </a>
<a class="nav-link active" data-toggle="tab" href="#courses-enrolled">
{{ _("Courses Enrolled") }}
</a>
</li>
{% if show_creators_section %}
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#courses-created">{{ _("Courses Created") }}
<a class="nav-link" data-toggle="tab" href="#courses-created">
{{ _("Courses Created") }}
</a>
</li>
{% endif %}
{% if show_review_section %}
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#courses-under-review">
{{ _("Courses Under Review") }}
</a>
</li>
{% endif %}
@@ -38,16 +48,24 @@
<div class="tab-pane active" id="courses-enrolled" role="tabpanel" aria-labelledby="courses-enrolled">
{% include "lms/lms/web_template/courses_enrolled/courses_enrolled.html" %}
</div>
{% if show_creators_section %}
<div class="tab-pane fade" id="courses-created" role="tabpanel" aria-labelledby="courses-created">
{% include "lms/templates/courses_created.html" %}
</div>
{% endif %}
{% if show_review_section %}
<div class="tab-pane fade" id="courses-under-review" role="tabpanel" aria-labelledby="courses-under-review">
{% include "lms/templates/courses_under_review.html" %}
</div>
{% endif %}
<div class="tab-pane fade" id="notifications" role="tabpanel" aria-labelledby="notifications">
{% include "lms/templates/courses_created.html" %}
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,9 @@
import frappe
from lms.lms.utils import has_course_instructor_role, has_course_moderator_role
def get_context(context):
context.no_cache = 1
portal_course_creation = frappe.db.get_single_value("LMS Settings", "portal_course_creation")
context.show_creators_section = portal_course_creation == "Anyone" or has_course_instructor_role()
context.show_review_section = has_course_moderator_role()

View File

@@ -9,7 +9,7 @@
<div class="common-page-style">
<div class="container">
{% if allow_posting and jobs | length %}
{% if allow_posting %}
<a class="button is-primary pull-right" href="/job-opportunity?new=1">{{ _("Post a Job") }}</a>
{% endif %}
@@ -48,18 +48,11 @@
{% else %}
<div class="empty-state">
<div>
<img class="icon icon-xl" src="/assets/lms/icons/comment.svg">
</div>
<img class="icon icon-xl" src="/assets/lms/icons/comment.svg">
<div class="empty-state-text">
<div class="empty-state-heading">{{ _("No open jobs") }}</div>
<div class="course-meta">{{ _("There are no job openings at present.") }}</div>
</div>
<div>
{% if allow_posting %}
<a class="button is-secondary dark-links m-auto" href="/job-opportunity?new=1">{{ _("Post a Job") }}</a>
{% endif %}
</div>
</div>
{% endif %}
</div>

View File

@@ -3,342 +3,395 @@
<meta name="description" content="{{ member.full_name }}" />
{% endblock %}
{% block content %}
{% set read_only = member.name != frappe.session.user %}
{% set user = member.name %}
{% set courses_created = get_authored_courses(member.name, True) %}
{% set certificates = get_certificates(user) %}
<div class="common-page-style profile-page">
{{ ProfileBanner(member) }}
<div class="profile-page-body">
<div class="container">
{% set read_only = member.name != frappe.session.user %}
{{ About(member) }}
{{ EducationDetails(member) }}
{{ WorkDetails(member) }}
{{ Certification(member) }}
{{ Contact(member) }}
{{ Skills(member) }}
{{ CareerPreference(member) }}
{{ ProfileBanner(member) }}
<div class="profile-page-body">
<div class="container">
<ul class="nav lms-nav" id="courses-tab">
<li class="nav-item">
<a class="nav-link active" data-toggle="tab" href="#profile">
{{ _("Profile") }}
</a>
</li>
{% if courses_created | length %}
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#courses-created">
{{ _("Courses Created") }}
</a>
</li>
{% endif %}
{% if certificates | length %}
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#certificates">
{{ _("Certificates") }}
</a>
</li>
{% endif %}
{% if has_course_moderator_role() %}
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#settings">
{{ _("Settings") }}
</a>
</li>
{% endif %}
</ul>
<div class="border-bottom mb-4"></div>
<div class="tab-content">
<div class="tab-pane active" id="profile" role="tabpanel" aria-labelledby="profile">
<div class="common-card-style column-card mt-5">
{{ About(member) }}
{{ EducationDetails(member) }}
{{ WorkDetails(member) }}
{{ ExternalCertification(member) }}
{{ Contact(member) }}
{{ Skills(member) }}
{{ CareerPreference(member) }}
{{ ProfileTabs(profile_tabs) }}
</div>
</div>
{% if courses_created | length %}
{% set only_published = True %}
<div class="tab-pane fade" id="courses-created" role="tabpanel" aria-labelledby="courses-created">
{% include "lms/templates/courses_created.html" %}
</div>
{% endif %}
{% if certificates | length %}
<div class="tab-pane fade" id="certificates" role="tabpanel" aria-labelledby="certificates">
{% include "lms/templates/certificates_section.html" %}
</div>
{% endif %}
<div class="tab-pane fade" id="settings" role="tabpanel" aria-labelledby="settings">
{{ RoleSettings(member) }}
</div>
</div>
</div>
</div>
</div>
<div class="container">
{{ CoursesCreated(member, read_only) }}
{{ CoursesMentored(member, read_only) }}
{{ ProfileTabs(profile_tabs) }}
</div>
</div>
{% endblock %}
<!-- Banner -->
{% macro ProfileBanner(member) %}
{% set cover_image = member.cover_image if member.cover_image else "/assets/lms/images/profile-banner.png" %}
{% set enrollment = get_course_membership(frappe.session.user, member_type="Student") | length %}
{% set enrollment = get_course_membership(member.name, member_type="Student") | length %}
{% set enrollment_suffix = _("Courses") if enrollment > 1 else _("Course") %}
<div class="container">
<div class="profile-banner" style="background-image: url({{ cover_image }})">
<div class="profile-avatar">
{{ widgets.Avatar(member=member, avatar_class="avatar-square") }}
</div>
</div>
<div class="profile-info">
<div class="profile-name-section">
<div class="profile-name"> {{ member.full_name }} </div>
{% if get_authored_courses(member.name) | length %}
<div class="creator-badge"> {{ _("Creator") }} </div>
{% endif %}
{% if member.looking_for_job %}
<div class="creator-badge"> {{ _("Open Network") }} </div>
{% endif %}
{% if frappe.session.user == member.email %}
<a class="button is-secondary ml-auto mt-1" href="/edit-profile?name={{ member.email }}"> {{ _("Edit Profile") }} </a>
{% endif %}
<div class="profile-banner" style="background-image: url({{ cover_image | urlencode }})">
<div class="profile-avatar">
{{ widgets.Avatar(member=member, avatar_class="avatar-square") }}
</div>
</div>
<div class="profile-meta">
{% if member.headline %}
<div class="course-meta mr-3"> {{ member.headline }} </div>
{% endif %}
<div class="profile-info">
<div class="profile-name-section">
<div class="profile-name" data-name="{{ member.name }}"> {{ member.full_name }} </div>
{% if enrollment %}
<div class="course-meta">
<img src="/assets/lms/icons/book_plain.svg">
{{ enrollment }} {{ enrollment_suffix }} {{ _("taken") }} </div>
{% endif %}
{% if courses_created | length %}
<div class="creator-badge"> {{ _("Creator") }} </div>
{% endif %}
{% if member.looking_for_job %}
<div class="creator-badge"> {{ _("Open Network") }} </div>
{% endif %}
{% if frappe.session.user == member.email %}
<div class="ml-auto mt-1">
<a class="btn btn-secondary btn-sm" href="/dashboard"> {{ _("Visit Dashboard") }} </a>
<a class="btn btn-secondary btn-sm ml-2" href="/edit-profile/{{ member.email }}/edit"> {{ _("Edit Profile") }} </a>
</div>
{% endif %}
</div>
<div class="profile-meta">
{% if member.headline %}
<div class="course-meta mr-3"> {{ member.headline }} </div>
{% endif %}
{% if enrollment %}
<div class="course-meta">
<img src="/assets/lms/icons/book_plain.svg">
{{ enrollment }} {{ enrollment_suffix }} {{ _("taken") }}
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endmacro %}
{% macro CoursesCreated(member, read_only) %}
{% set authored_courses = get_authored_courses(member.name) %}
{% if authored_courses | length %}
<div class="profile-courses">
<div class="course-home-headings"> {{ _("Courses Created") }} </div>
<div class="cards-parent">
{% for course in authored_courses %}
{{ widgets.CourseCard(course=course, read_only=read_only) }}
{% endfor %}
</div>
</div>
{% endif %}
{% endmacro %}
<!-- Courses Mentored -->
{% macro CoursesMentored(member, read_only) %}
{% if member.get_mentored_courses() | length %}
<div class="profile-courses">
<div class="course-home-headings"> {{ _("Courses Mentored") }} </div>
<div class="cards-parent">
{% for course in member.get_mentored_courses() %}
{{ widgets.CourseCard(course=course, read_only=read_only) }}
{% endfor %}
</div>
<div class="course-home-headings"> {{ _("Courses Mentored") }} </div>
<div class="cards-parent">
{% for course in member.get_mentored_courses() %}
{{ widgets.CourseCard(course=course, read_only=read_only) }}
{% endfor %}
</div>
</div>
{% endif %}
{% endmacro %}
{% macro CoursesEnrolled(member, read_only) %}
{% set enrolled = get_enrolled_courses() %}
{% if enrolled.completed | length %}
<div class="profile-courses">
<div class="course-home-headings"> {{ _("Courses Completed") }} </div>
<div class="cards-parent">
{% for course in enrolled.completed %}
{{ widgets.CourseCard(course=course, read_only=read_only) }}
{% endfor %}
</div>
</div>
{% endif %}
{% if enrolled.in_progress | length %}
<div class="profile-courses">
<div class="course-home-headings"> {{ _("Courses In Progress") }} </div>
<div class="cards-parent">
{% for course in enrolled.in_progress %}
{{ widgets.CourseCard(course=course, read_only=read_only) }}
{% endfor %}
</div>
</div>
{% endif %}
{% endmacro %}
<!-- Profile Tabs Extension -->
{% macro ProfileTabs(profile_tabs) %}
<div>
{% for tab in profile_tabs %}
{% set slug = title.lower().replace(" ", "-") %}
<div class="tab-content">
<div class="tab-pane fade py-4 show active" role="tabpanel" id="slug">
{{ tab.render() }}
{% for tab in profile_tabs %}
{% set slug = title.lower().replace(" ", "-") %}
<div class="tab-content">
<div class="tab-pane fade py-4 show active" role="tabpanel" id="slug">
{{ tab.render() }}
</div>
</div>
</div>
{% endfor %}
{% endfor %}
</div>
{% endmacro %}
{% macro About(member) %}
{% if member.bio %}
<div class="">
<div class="common-card-style profile-card">
<div class="course-home-headings"> {{ _("About") }} </div>
<div class="description">{{ member.bio }}</div>
</div>
</div>
{% endif %}
<!-- Role Settings -->
{% macro RoleSettings(member) %}
{% if has_course_moderator_role() %}
<div class="">
<div class="common-card-style column-card">
<div class="course-home-headings"> {{ _("Role Settings") }} </div>
<div>
<label class="role">
<input type="checkbox" id="instructor" data-role="Course Instructor"
{% if has_course_instructor_role(member.name) %} checked {% endif %}>
{{ _("Course Instructor") }}
</label>
<label class="role">
<input type="checkbox" id="moderator" data-role="Course Moderator"
{% if has_course_moderator_role(member.name) %} checked {% endif %}>
{{ _("Course Moderator") }}
</label>
</div>
</div>
</div>
{% endif %}
{% endmacro %}
<!-- About Section -->
{% macro About(member) %}
<div class="course-home-headings"> {{ _("About") }} </div>
<div class="description">
{% if member.bio %}
{{ member.bio }}
{% else %}
{{ _("Hey, my name is ") }} {{ member.full_name }}
{% endif %}
</div>
{% endmacro %}
<!-- Work Preference -->
{% macro WorkPreference(member) %}
<div class="education-details">
<div class="common-card-style profile-card">
<div class="course-home-headings"> {{ _("Work Preference") }} </div>
<div class="course-home-headings mt-10"> {{ _("Work Preference") }} </div>
<div> {{ member.attire }} </div>
<div> {{ member.collaboration }} </div>
<div> {{ member.role }} </div>
<div> {{ member.location_preference }} </div>
<div> {{ member.time }} </div>
<div> {{ member.company_type }} </div>
</div>
</div>
{% endmacro %}
<!-- Career Preference -->
{% macro CareerPreference(member) %}
{% if member.preferred_functions or member.preferred_industries or member.preferred_location or member.dream_companies %}
<div class="education-details">
<div class="common-card-style profile-card">
<div class="course-home-headings"> {{ _("Career Preference") }} </div>
{% if member.preferred_functions or member.preferred_industries or member.preferred_location or member.dream_companies %}
<div class="course-home-headings mt-10"> {{ _("Career Preference") }} </div>
<div class="profile-column-grid">
{% if member.preferred_functions | length %}
<div>
<b>{{ _("Preferred Functions:") }}</b>
{% for function in member.preferred_functions %}
<div class="description">{{ function.function }}</div>
{% endfor %}
</div>
{% endif %}
{% if member.preferred_industries | length %}
<div>
<b>{{ _("Preferred Industries:") }}</b>
{% for industry in member.preferred_industries %}
<div class="description">{{ industry.industry }}</div>
{% endfor %}
</div>
{% endif %}
{% if member.preferred_functions | length %}
<div>
<b>{{ _("Preferred Functions:") }}</b>
{% for function in member.preferred_functions %}
<div class="description">{{ function.function }}</div>
{% endfor %}
</div>
{% endif %}
{% if member.preferred_location %}
<div>
<b> {{ _("Preferred Locations:") }} </b>
<div class="description"> {{ member.preferred_location }} </div>
</div>
{% endif %}
{% if member.preferred_industries | length %}
<div>
<b>{{ _("Preferred Industries:") }}</b>
{% for industry in member.preferred_industries %}
<div class="description">{{ industry.industry }}</div>
{% endfor %}
</div>
{% endif %}
{% if member.preferred_location %}
<div>
<b> {{ _("Preferred Locations:") }} </b>
<div class="description"> {{ member.preferred_location }} </div>
</div>
{% endif %}
{% if member.dream_companies %}
<div>
<b> {{ _("Dream Companies:") }} </b>
<div class="description"> {{ member.dream_companies }} </div>
</div>
{% endif %}
{% if member.dream_companies %}
<div>
<b> {{ _("Dream Companies:") }} </b>
<div class="description"> {{ member.dream_companies }} </div>
</div>
{% endif %}
</div>
</div>
</div>
{% endif %}
{% endif %}
{% endmacro %}
<!-- Contact Section -->
{% macro Contact(member) %}
{% if member.linkedin or member.medium or member.github %}
<div class="education-details">
<div class="common-card-style profile-card">
<div class="course-home-headings"> {{ _("Contact") }} </div>
{% if member.linkedin or member.medium or member.github %}
<div class="course-home-headings mt-10"> {{ _("Contact") }} </div>
<div class="profile-column-grid">
{% if member.linkedin %}
{% set linkedin = member.linkedin[:-1] if member.linkedin[-1] == "/" else member.linkedin %}
<a class="button-links description" href="{{ member.linkedin }}">
{% if member.linkedin %}
{% set linkedin = member.linkedin[:-1] if member.linkedin[-1] == "/" else member.linkedin %}
<a class="button-links description" href="{{ member.linkedin }}">
<img src="/assets/lms/icons/linkedin.svg"> {{ linkedin.split("/")[-1] }}
</a>
</a>
{% endif %}
{% if member.medium %}
<a class="button-links description" href="{{ member.medium}}">
<img src="/assets/lms/icons/medium.svg"> {{ member.medium.split("/")[-1] }}
<img src="/assets/lms/icons/medium.svg"> {{ member.medium.split("/")[-1] }}
</a>
{% endif %}
{% if member.github %}
<a class="button-links description" href="{{ member.github }}">
<img src="/assets/lms/icons/github.svg"> {{ member.github.split("/")[-1] }}
<img src="/assets/lms/icons/github.svg"> {{ member.github.split("/")[-1] }}
</a>
{% endif %}
</div>
</div>
</div>
{% endif %}
{% endif %}
{% endmacro %}
<!-- Skills -->
{% macro Skills(member) %}
{% if member.skill | length %}
<div class="education-details">
<div class="common-card-style profile-card">
<div class="course-home-headings"> {{ _("Skills")}} </div>
{% if member.skill | length %}
<div class="course-home-headings mt-10"> {{ _("Skills")}} </div>
<div class="profile-column-grid">
{% for skill in member.skill %}
<div class="description"> {{ skill.skill_name }} </div>
{% endfor %}
{% for skill in member.skill %}
<div class="description"> {{ skill.skill_name }} </div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
{% endif %}
{% endmacro %}
<!-- Education Details -->
{% macro EducationDetails(member) %}
{% if member.education %}
<div class="education-details">
<div class="common-card-style profile-card">
<div class="course-home-headings"> {{ _("Education") }} </div>
{% if member.education %}
<div class="course-home-headings mt-10"> {{ _("Education") }} </div>
<div class="profile-grid-card">
{% for edu in member.education %}
<div class="profile-card-row">
<div class="bold-title"> {{ edu.institution_name }} </div>
<div class="profile-item"> {{ edu.degree_type }} <span></span> {{ edu.major }}
{% if not member.hide_private %}
<!-- {% if edu.grade_type %} {{ edu.grade_type }} {% endif %} -->
{% if edu.grade %} <span></span> {{ edu.grade }} {% endif %}
{% endif %}
{% for edu in member.education %}
<div class="column-card-row">
<div class="bold-title"> {{ edu.institution_name }} </div>
<div class="profile-item"> {{ edu.degree_type }} <span></span> {{ edu.major }}
{% if not member.hide_private %}
<!-- {% if edu.grade_type %} {{ edu.grade_type }} {% endif %} -->
{% if edu.grade %} <span></span> {{ edu.grade }} {% endif %}
{% endif %}
</div>
<div class="description">
{% if edu.start_date %}
{{ frappe.utils.format_date(edu.start_date, "MMM YYYY") }} -
{% endif %}
{{ frappe.utils.format_date(edu.end_date, "MMM YYYY") }}
</div>
<div class="description"> {{ edu.location }} </div>
</div>
<div class="description">
{% if edu.start_date %}
{{ frappe.utils.format_date(edu.start_date, "MMM YYYY") }} -
{% endif %}
{{ frappe.utils.format_date(edu.end_date, "MMM YYYY") }} </div>
<div class="description"> {{ edu.location }} </div>
</div>
{% endfor %}
{% endfor %}
</div>
</div>
</div>
{% endif %}
{% endif %}
{% endmacro %}
<!-- Work Details -->
{% macro WorkDetails(member) %}
{% set work_details = member.work_experience + member.internship %}
{% if work_details | length %}
<div class="education-details">
<div class="common-card-style profile-card">
<div class="course-home-headings"> {{ _("Work Experience") }} </div>
<div class="profile-grid-card">
{% for work in work_details %}
<div class="">
<div class="bold-title"> {{ work.title }} </div>
<div class="profile-item"> {{ work.company }} </div>
<div class="description"> {{ frappe.utils.format_date(work.from_date, "MMM YYYY") }} -
{% if work.to_date %} {{ frappe.utils.format_date(work.to_date, "MMM YYYY") }} {% else %} Present {% endif %} </div>
<div class="description"> {{ work.location }} </div>
{% if work.description %} <div class="profile-item"> {{ work.description }} </div> {% endif %}
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
{% endmacro %}
{% set work_details = member.work_experience + member.internship %}
{% macro Certification(member) %}
{% if member.certification %}
<div class="education-details">
<div class="common-card-style profile-card">
<div class="course-home-headings"> {{ _("Certification") }} </div>
{% if work_details | length %}
<div class="course-home-headings mt-10"> {{ _("Work Experience") }} </div>
<div class="profile-grid-card">
{% for cert in member.certification %}
<div class="">
<div class="bold-title"> {{ cert.certification_name }} </div>
<div class="profile-item"> {{ cert.organization }} </div>
<div class="description"> {{ frappe.utils.format_date(cert.issue_date, "MMM YYYY") }}
{% if cert.expiration_date %} - {{ frappe.utils.format_date(cert.expiration_date, "MMM YYYY") }} {% endif %} </div>
{% if cert.description %} <div class="profile-item"> {{ cert.description }} </div> {% endif %}
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
{% for work in work_details %}
<div class="">
<div class="bold-title"> {{ work.title }} </div>
<div class="profile-item"> {{ work.company }} </div>
<div class="description">
{{ frappe.utils.format_date(work.from_date, "MMM YYYY") }} -
{% if work.to_date %} {{ frappe.utils.format_date(work.to_date, "MMM YYYY") }}
{% else %} Present {% endif %}
</div>
<div class="description"> {{ work.location }} </div>
{% if work.description %}
<div class="profile-item">
{{ work.description }}
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% endif %}
{% endmacro %}
{% block script %}
<script>
frappe.ready(() => {
if ("{{ member.name }}" == frappe.session.user) {
setTimeout(() => {
var link_array = $('.nav-link').filter((i, elem) => $(elem).text().trim() === "My Profile");
link_array.length && $(link_array[0]).addClass("active");
}, 0)
}
if ($(".profile-column-one").children().length == 0) {
$(".profile-column-one").hide();
}
});
</script>
{% endblock %}
<!-- Certifications -->
{% macro ExternalCertification(member) %}
{% if member.certification %}
<div class="course-home-headings mt-10"> {{ _("External Certification") }} </div>
<div class="profile-grid-card">
{% for cert in member.certification %}
<div class="">
<div class="bold-title"> {{ cert.certification_name }} </div>
<div class="profile-item"> {{ cert.organization }} </div>
<div class="description">
{{ frappe.utils.format_date(cert.issue_date, "MMM YYYY") }}
{% if cert.expiration_date %}
- {{ frappe.utils.format_date(cert.expiration_date, "MMM YYYY") }}
{% endif %}
</div>
{% if cert.description %}
<div class="profile-item">
{{ cert.description }}
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% endif %}
{% endmacro %}

View File

@@ -0,0 +1,42 @@
frappe.ready(() => {
make_profile_active_in_navbar();
$(".role").change((e) => {
save_role(e);
});
});
const make_profile_active_in_navbar = () => {
let member_name = $(".profile-name").data("name");
if (member_name == frappe.session.user) {
setTimeout(() => {
let link_array = $('.nav-link').filter((i, elem) => $(elem).text().trim() === "My Profile");
link_array.length && $(link_array[0]).addClass("active");
}, 0)
}
}
const save_role = (e) => {
let member_name = $(".profile-name").data("name");
let role = $(e.currentTarget).children("input");
frappe.call({
method: "lms.overrides.user.save_role",
args: {
"user": member_name,
"role": role.data("role"),
"value": role.prop("checked") ? 1 : 0
},
callback: (data) => {
if (data.message) {
frappe.show_alert({
message: __("Saved"),
indicator: "green",
});
}
}
})
}

View File

@@ -1,6 +1,6 @@
import frappe
from lms.page_renderers import get_profile_url_prefix
from urllib.parse import urlencode
def get_context(context):
context.no_cache = 1
@@ -12,13 +12,16 @@ def get_context(context):
if username:
frappe.local.flags.redirect_location = get_profile_url_prefix() + username
raise frappe.Redirect
try:
context.member = frappe.get_doc("User", {"username": username})
except:
context.template = "www/404.html"
return
context.profile_tabs = get_profile_tabs(context.member)
def get_profile_tabs(user):
"""Returns the enabled ProfileTab objects.

View File

@@ -9,7 +9,7 @@ def get_common_context(context):
batch_name = None
course = frappe.db.get_value("LMS Course",
frappe.form_dict["course"], ["name", "title", "video_link", "enable_certification"], as_dict=True)
frappe.form_dict["course"], ["name", "title", "video_link", "enable_certification", "status"], as_dict=True)
if not course:
context.template = "www/404.html"
return