diff --git a/lms/lms/doctype/course_lesson/course_lesson.json b/lms/lms/doctype/course_lesson/course_lesson.json index 6e61622b..d74a463d 100644 --- a/lms/lms/doctype/course_lesson/course_lesson.json +++ b/lms/lms/doctype/course_lesson/course_lesson.json @@ -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", diff --git a/lms/lms/doctype/course_lesson/course_lesson.py b/lms/lms/doctype/course_lesson/course_lesson.py index ac639e24..ab74884a 100644 --- a/lms/lms/doctype/course_lesson/course_lesson.py +++ b/lms/lms/doctype/course_lesson/course_lesson.py @@ -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"] diff --git a/lms/lms/doctype/lms_course/lms_course.py b/lms/lms/doctype/lms_course/lms_course.py index 76b8e097..1d8096a4 100644 --- a/lms/lms/doctype/lms_course/lms_course.py +++ b/lms/lms/doctype/lms_course/lms_course.py @@ -249,7 +249,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 +261,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) diff --git a/lms/lms/utils.py b/lms/lms/utils.py index c8c8e28a..30b201a0 100644 --- a/lms/lms/utils.py +++ b/lms/lms/utils.py @@ -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 "/" 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,6 +429,7 @@ def first_lesson_exists(course): return True + def redirect_to_courses_list(): frappe.local.flags.redirect_location = "/courses" raise frappe.Redirect diff --git a/lms/overrides/user.py b/lms/overrides/user.py index ff587647..324cf83c 100644 --- a/lms/overrides/user.py +++ b/lms/overrides/user.py @@ -145,7 +145,7 @@ def get_enrolled_courses(): } def get_course_membership(member, member_type=None): - """ Returns all memberships of the user """ + """ Returns all memberships of the user. """ filters = { "member": member } @@ -156,8 +156,7 @@ def get_course_membership(member, member_type=None): def get_authored_courses(member, only_published=True): - """Returns the number of courses authored by this user. - """ + """ Returns the number of courses authored by this user. """ course_details = [] filters = { diff --git a/lms/public/css/style.css b/lms/public/css/style.css index 69633fdb..6cdfe14b 100644 --- a/lms/public/css/style.css +++ b/lms/public/css/style.css @@ -1658,7 +1658,7 @@ li { padding: 1rem; } -.help-article { +.medium { font-size: var(--text-base); } diff --git a/lms/www/batch/learn.html b/lms/www/batch/learn.html index 12051a81..b8cfc684 100644 --- a/lms/www/batch/learn.html +++ b/lms/www/batch/learn.html @@ -115,7 +115,7 @@ {% if lesson.edit_mode %} {{ EditLesson(lesson) }} {% else %} - {{ render_html(lesson.body) }} + {{ render_html(lesson.body, lesson.youtube, lesson.quiz_id) }} {% endif %} {% else %} @@ -181,20 +181,34 @@ {% macro EditLesson(lesson) %} -
- +
{% if lesson.youtube %}{{ lesson.youtube }}{% endif %}
+
+
{% if lesson.quiz_id %}{{ lesson.quiz_id }}{% endif %}
-
- - {% if lesson.name %} - - {{ _("Create Quiz") }} - {% endif %} + +
+ + {% if lesson.name %} + + {{ _("Create Quiz") }} + {% endif %} + + {{ UploadAttachments() }} + +
+ + {{ HelpArticle() }} +{% endmacro %} + + +{% macro UploadAttachments() %}
-
- -{{ HelpArticle() }} {% endmacro %} {% macro HelpArticle() %} -
-

{{ _("Embed Components") }}

-

- {{ _("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.") }} -

- - - - - - - - - - - - - - - - -
{{ _("Content Type") }} {{ _("Syntax") }} {{ _("Description") }}
- {{ _("YouTube Video") }} - - {% raw %} {{ YouTubeVideo('Video ID') }} {% endraw %} - - - {{ _("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.") }} - -
    +
    +

    {{ _("Embed Components") }}

    +

    + {{ _("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.") }} +

    +
      +
    1. + {{ _("YouTube Video") }} +

      To get the YouTube Video ID, follow the steps mentioned below.

      +
      • {{ _("Upload the video on youtube.") }}
      • - {{ _("When you share a youtube video, it shows an option called Embed.") }} + {{ _("When you share a youtube video, it shows a URL") }}
      • - {{ _("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.") }}
      -
- {{ _("Quiz") }} - - {% raw %} {{ Quiz('Quiz ID') }} {% endraw %} - - {% set quiz_link = " Quiz List " %} - {{ _("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) }} -
-
+ + + +
{% endmacro %} {% 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 %} {%- block script %} -{{ super() }} - -{{ include_script('controls.bundle.js') }} -{% for ext in page_extensions %} -{{ ext.render_footer() }} -{% endfor %} + {{ super() }} + + {{ include_script('controls.bundle.js') }} + {% for ext in page_extensions %} + {{ ext.render_footer() }} + {% endfor %} {%- endblock %} diff --git a/lms/www/batch/learn.js b/lms/www/batch/learn.js index 0fe52cd6..197ca8a7 100644 --- a/lms/www/batch/learn.js +++ b/lms/www/batch/learn.js @@ -490,7 +490,9 @@ 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"),