Merge pull request #368 from pateljannat/video-and-quiz-field

This commit is contained in:
Jannat Patel
2022-09-02 14:58:17 +05:30
committed by GitHub
8 changed files with 187 additions and 93 deletions

View File

@@ -15,6 +15,10 @@
"include_in_preview", "include_in_preview",
"index_label", "index_label",
"section_break_6", "section_break_6",
"youtube",
"column_break_9",
"quiz_id",
"section_break_11",
"body", "body",
"help_section", "help_section",
"help" "help"
@@ -81,11 +85,31 @@
"label": "Course", "label": "Course",
"options": "LMS Course", "options": "LMS Course",
"read_only": 1 "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, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2022-05-02 17:16:12.450460", "modified": "2022-09-02 11:30:15.450624",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "Course Lesson", "name": "Course Lesson",

View File

@@ -4,6 +4,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from ...md import find_macros from ...md import find_macros
from lms.lms.utils import get_course_progress, get_lesson_url 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): class CourseLesson(Document):
def validate(self): def validate(self):
self.check_and_create_folder() 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): def on_update(self):
dynamic_documents = ["Exercise", "Quiz"] dynamic_documents = ["Exercise", "Quiz"]

View File

@@ -249,7 +249,7 @@ def save_chapter(course, title, chapter_description, idx, chapter):
@frappe.whitelist() @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: if lesson:
doc = frappe.get_doc("Course Lesson", lesson) doc = frappe.get_doc("Course Lesson", lesson)
else: else:
@@ -261,7 +261,9 @@ def save_lesson(title, body, chapter, preview, idx, lesson):
"chapter": chapter, "chapter": chapter,
"title": title, "title": title,
"body": body, "body": body,
"include_in_preview": preview "include_in_preview": preview,
"youtube": youtube,
"quiz_id": quiz_id
}) })
doc.save(ignore_permissions=True) doc.save(ignore_permissions=True)

View File

@@ -7,6 +7,7 @@ from frappe import _
RE_SLUG_NOTALLOWED = re.compile("[^a-z0-9]+") RE_SLUG_NOTALLOWED = re.compile("[^a-z0-9]+")
def slugify(title, used_slugs=[]): def slugify(title, used_slugs=[]):
"""Converts title to a slug. """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") membership.batch_title = frappe.db.get_value("LMS Batch", membership.batch, "title")
return membership return membership
def get_chapters(course): def get_chapters(course):
"""Returns all chapters of this course. """Returns all chapters of this course.
""" """
@@ -85,6 +87,7 @@ def get_lessons(course, chapter=None):
return lessons return lessons
def get_lesson_details(chapter): def get_lesson_details(chapter):
lessons = [] lessons = []
lesson_list = frappe.get_all("Lesson Reference", lesson_list = frappe.get_all("Lesson Reference",
@@ -94,10 +97,11 @@ def get_lesson_details(chapter):
for row in lesson_list: for row in lesson_list:
lesson_details = frappe.db.get_value("Course Lesson", row.lesson, 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.number = flt("{}.{}".format(chapter.idx, row.idx))
lesson_details.icon = "icon-list" lesson_details.icon = "icon-list"
macros = find_macros(lesson_details.body) macros = find_macros(lesson_details.body)
for macro in macros: for macro in macros:
if macro[0] == "YouTubeVideo": if macro[0] == "YouTubeVideo":
lesson_details.icon = "icon-video" lesson_details.icon = "icon-video"
@@ -106,10 +110,12 @@ def get_lesson_details(chapter):
lessons.append(lesson_details) lessons.append(lesson_details)
return lessons return lessons
def get_tags(course): def get_tags(course):
tags = frappe.db.get_value("LMS Course", course, "tags") tags = frappe.db.get_value("LMS Course", course, "tags")
return tags.split(",") if tags else [] return tags.split(",") if tags else []
def get_instructors(course): def get_instructors(course):
instructor_details = [] instructor_details = []
instructors = frappe.get_all("Course Instructor", {"parent": course}, instructors = frappe.get_all("Course Instructor", {"parent": course},
@@ -123,6 +129,7 @@ def get_instructors(course):
as_dict=True)) as_dict=True))
return instructor_details return instructor_details
def get_students(course, batch=None): def get_students(course, batch=None):
"""Returns (email, full_name, username) of all the students of this batch as a list of dict. """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, filters,
["member"]) ["member"])
def get_average_rating(course): def get_average_rating(course):
ratings = [review.rating for review in get_reviews(course)] ratings = [review.rating for review in get_reviews(course)]
if not len(ratings): if not len(ratings):
return None return None
return sum(ratings)/len(ratings) return sum(ratings)/len(ratings)
def get_reviews(course): def get_reviews(course):
reviews = frappe.get_all("LMS Course Review", reviews = frappe.get_all("LMS Course Review",
{ {
@@ -164,6 +173,7 @@ def get_reviews(course):
return reviews return reviews
def get_sorted_reviews(course): def get_sorted_reviews(course):
rating_count = rating_percent = frappe._dict() rating_count = rating_percent = frappe._dict()
keys = ["5.0", "4.0", "3.0", "2.0", "1.0"] keys = ["5.0", "4.0", "3.0", "2.0", "1.0"]
@@ -180,6 +190,7 @@ def get_sorted_reviews(course):
return rating_percent return rating_percent
def is_certified(course): def is_certified(course):
certificate = frappe.get_all("LMS Certificate", certificate = frappe.get_all("LMS Certificate",
{ {
@@ -190,6 +201,7 @@ def is_certified(course):
return certificate[0].name return certificate[0].name
return return
def get_lesson_index(lesson_name): def get_lesson_index(lesson_name):
"""Returns the {chapter_index}.{lesson_index} for the lesson. """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}" return f"{chapter.idx}.{lesson.idx}"
def get_lesson_url(course, lesson_number): def get_lesson_url(course, lesson_number):
if not lesson_number: if not lesson_number:
return return
@@ -224,8 +237,16 @@ def get_progress(course, lesson):
}, },
["status"]) ["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): def is_mentor(course, email):
"""Checks if given user is a mentor for this course. """Checks if given user is a mentor for this course.
@@ -238,6 +259,7 @@ def is_mentor(course, email):
"mentor": email "mentor": email
}) })
def is_cohort_staff(course, user_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. """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) return frappe.db.exists(staff) or frappe.db.exists(mentor)
def get_mentors(course): def get_mentors(course):
"""Returns the list of all mentors for this course. """Returns the list of all mentors for this course.
""" """
@@ -269,6 +292,7 @@ def get_mentors(course):
course_mentors.append(member) course_mentors.append(member)
return course_mentors return course_mentors
def is_eligible_to_review(course, membership): def is_eligible_to_review(course, membership):
""" Checks if user is eligible to review the course """ """ Checks if user is eligible to review the course """
if not membership: if not membership:
@@ -281,6 +305,7 @@ def is_eligible_to_review(course, membership):
return False return False
return True return True
def get_course_progress(course, member=None): def get_course_progress(course, member=None):
""" Returns the course progress of the session user """ """ Returns the course progress of the session user """
lesson_count = len(get_lessons(course)) 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 precision = cint(frappe.db.get_default("float_precision")) or 3
return flt(((completed_lessons/lesson_count) * 100), precision) return flt(((completed_lessons/lesson_count) * 100), precision)
def get_initial_members(course): def get_initial_members(course):
members = frappe.get_all("LMS Batch Membership", members = frappe.get_all("LMS Batch Membership",
{ {
@@ -310,12 +336,15 @@ def get_initial_members(course):
return member_details return member_details
def is_instructor(course): def is_instructor(course):
return len(list(filter(lambda x: x.name == frappe.session.user, get_instructors(course)))) > 0 return len(list(filter(lambda x: x.name == frappe.session.user, get_instructors(course)))) > 0
def convert_number_to_character(number): def convert_number_to_character(number):
return string.ascii_uppercase[number] return string.ascii_uppercase[number]
def get_signup_optin_checks(): def get_signup_optin_checks():
mapper = frappe._dict({ mapper = frappe._dict({
@@ -343,6 +372,7 @@ def get_signup_optin_checks():
return (", ").join(links) return (", ").join(links)
def get_popular_courses(): def get_popular_courses():
courses = frappe.get_all("LMS Course", {"published": 1, "upcoming": 0}) courses = frappe.get_all("LMS Course", {"published": 1, "upcoming": 0})
course_membership = [] course_membership = []
@@ -356,6 +386,7 @@ def get_popular_courses():
course_membership = sorted(course_membership, key = lambda x: x.get("members"), reverse=True) course_membership = sorted(course_membership, key = lambda x: x.get("members"), reverse=True)
return course_membership[:3] return course_membership[:3]
def get_evaluation_details(course, member=None): def get_evaluation_details(course, member=None):
info = frappe.db.get_value("LMS Course", course, ["grant_certificate_after", "max_attempts", "duration"], as_dict=True) 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", { 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 "no_of_attempts": no_of_attempts
}) })
def format_amount(amount, currency): def format_amount(amount, currency):
amount_reduced = amount / 1000 amount_reduced = amount / 1000
if amount_reduced < 1: if amount_reduced < 1:
@@ -397,6 +429,7 @@ def first_lesson_exists(course):
return True return True
def redirect_to_courses_list(): def redirect_to_courses_list():
frappe.local.flags.redirect_location = "/courses" frappe.local.flags.redirect_location = "/courses"
raise frappe.Redirect raise frappe.Redirect

View File

@@ -145,7 +145,7 @@ def get_enrolled_courses():
} }
def get_course_membership(member, member_type=None): def get_course_membership(member, member_type=None):
""" Returns all memberships of the user """ """ Returns all memberships of the user. """
filters = { filters = {
"member": member "member": member
} }
@@ -156,8 +156,7 @@ def get_course_membership(member, member_type=None):
def get_authored_courses(member, only_published=True): 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 = [] course_details = []
filters = { filters = {

View File

@@ -1658,7 +1658,7 @@ li {
padding: 1rem; padding: 1rem;
} }
.help-article { .medium {
font-size: var(--text-base); font-size: var(--text-base);
} }

View File

@@ -115,7 +115,7 @@
{% if lesson.edit_mode %} {% if lesson.edit_mode %}
{{ EditLesson(lesson) }} {{ EditLesson(lesson) }}
{% else %} {% else %}
{{ render_html(lesson.body) }} {{ render_html(lesson.body, lesson.youtube, lesson.quiz_id) }}
{% endif %} {% endif %}
{% else %} {% else %}
@@ -181,20 +181,34 @@
<!-- Edit Lesson --> <!-- Edit Lesson -->
{% macro EditLesson(lesson) %} {% macro EditLesson(lesson) %}
<div id="body" {% if lesson.body %} data-body="{{ lesson.body }}" {% endif %}></div>
<label class="preview"> <div class="medium mt-2" contenteditable="true" data-placeholder="{{ _('YouTube Video ID') }}"
<input {% if lesson.include_in_preview %} checked {% endif %} type="checkbox" id="youtube">{% if lesson.youtube %}{{ lesson.youtube }}{% endif %}</div>
id="preview"> {{ _("Show preview of this lesson to Guest users.") }} <div id="body" {% if lesson.body %} data-body="{{ lesson.body }}" {% endif %}></div>
</label> <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"> <label class="preview">
<button class="btn btn-primary btn-sm btn-lesson pull-right ml-2"> {{ _("Save") }} </button> <input {% if lesson.include_in_preview %} checked {% endif %} type="checkbox"
{% if lesson.name %} id="preview"> {{ _("Show preview of this lesson to Guest users.") }}
<button class="btn btn-secondary btn-sm pull-right btn-back ml-2"> {{ _("Back to Lesson") }} </button> </label>
<a class="btn btn-secondary btn-sm pull-right" href="/quizzes"> {{ _("Create Quiz") }} </a>
{% endif %}
<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="attachments-parent">
<div class="attachment-controls"> <div class="attachment-controls">
<div class="show-attachments" data-toggle="collapse" data-target="#collapse-attachments" aria-expanded="false"> <div class="show-attachments" data-toggle="collapse" data-target="#collapse-attachments" aria-expanded="false">
@@ -214,97 +228,111 @@
</div> </div>
<table class="attachments common-card-style collapse hide" id="collapse-attachments"></table> <table class="attachments common-card-style collapse hide" id="collapse-attachments"></table>
</div> </div>
</div>
{{ HelpArticle() }}
{% endmacro %} {% endmacro %}
<!-- Help Article --> <!-- Help Article -->
{% macro HelpArticle() %} {% macro HelpArticle() %}
<div class="help-article"> <div class="medium">
<h3> {{ _("Embed Components") }} </h3> <h3> {{ _("Embed Components") }} </h3>
<p> <p>
{{ _("You can add additional content to the lesson using a special syntax. The table below mentions {{ _("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.") }} all types of dynamic content that you can add to the lessons and the syntax for the same.") }}
</p> </p>
<table class="table w-100"> <ol>
<tr> <li>
<th style="width: 20%;"> {{ _("Content Type") }} </th> <b> {{ _("YouTube Video") }} </b>
<th style="width: 40%;"> {{ _("Syntax") }} </th> <p> To get the YouTube Video ID, follow the steps mentioned below. </p>
<th> {{ _("Description") }} </th> <ul class="px-4">
</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> <li>
{{ _("Upload the video on youtube.") }} {{ _("Upload the video on youtube.") }}
</li> </li>
<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>
<li> <li>
{{ _("On clicking it, it provides an iframe. Copy the source (src) of the iframe and {{ _("Copy the last parameter of the URL and paste it here.") }}
paste it here.") }}
</li> </li>
</ul> </ul>
</td> </li>
</tr> </ol>
<tr> <!-- <table class="table w-100">
<td> <tr>
{{ _("Quiz") }} <th style="width: 20%;"> {{ _("Content Type") }} </th>
</td> <th style="width: 40%;"> {{ _("Syntax") }} </th>
<td> <th> {{ _("Description") }} </th>
{% raw %} {{ Quiz('Quiz ID') }} {% endraw %} </tr>
</td> <tr>
<td> <td>
{% set quiz_link = "<a href='/quizzes'> Quiz List </a>" %} {{ _("YouTube Video") }}
{{ _("Copy and paste the syntax in the editor. Replace 'Quiz ID' with the Id of the Quiz. </td>
You can get the Id of the quiz from the {0}.").format(quiz_link) }} <td>
</td> {% raw %} {{ YouTubeVideo('Video ID') }} {% endraw %}
</tr> </td>
</table> <td>
</div> <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 %} {% endmacro %}
<!-- Discussions Component --> <!-- Discussions Component -->
{% macro Discussions() %} {% macro Discussions() %}
{% set topics_count = frappe.db.count("Discussion Topic", { {% set topics_count = frappe.db.count("Discussion Topic", {
"reference_doctype": "Course Lesson", "reference_doctype": "Course Lesson",
"reference_docname": lesson.name "reference_docname": lesson.name
}) %} }) %}
{% set is_instructor = frappe.session.user == course.instructor %} {% set is_instructor = frappe.session.user == course.instructor %}
{% set condition = is_instructor if is_instructor else membership %} {% set condition = is_instructor if is_instructor else membership %}
{% set doctype, docname = _("Course Lesson"), lesson.name %} {% set doctype, docname = _("Course Lesson"), lesson.name %}
{% set title = "Questions" if topics_count else "" %} {% set title = "Questions" if topics_count else "" %}
{% set cta_title = "Ask a Question" %} {% set cta_title = "Ask a Question" %}
{% set button_name = _("Start Learning") %} {% set button_name = _("Start Learning") %}
{% set redirect_to = "/courses/" + course.name %} {% set redirect_to = "/courses/" + course.name %}
{% set empty_state_title = _("Have a doubt?") %} {% set empty_state_title = _("Have a doubt?") %}
{% set empty_state_subtitle = _("Post it here, our mentors will help you out.") %} {% set empty_state_subtitle = _("Post it here, our mentors will help you out.") %}
{% include "frappe/templates/discussions/discussions_section.html" %} {% include "frappe/templates/discussions/discussions_section.html" %}
{% endmacro %} {% endmacro %}
<!-- Scripts --> <!-- Scripts -->
{%- block script %} {%- block script %}
{{ super() }} {{ super() }}
<script type="text/javascript"> <script type="text/javascript">
var page_context = {{ page_context | tojson }}; var page_context = {{ page_context | tojson }};
</script> </script>
{{ include_script('controls.bundle.js') }} {{ include_script('controls.bundle.js') }}
{% for ext in page_extensions %} {% for ext in page_extensions %}
{{ ext.render_footer() }} {{ ext.render_footer() }}
{% endfor %} {% endfor %}
{%- endblock %} {%- endblock %}

View File

@@ -490,7 +490,9 @@ const save_lesson = (e) => {
method: "lms.lms.doctype.lms_course.lms_course.save_lesson", method: "lms.lms.doctype.lms_course.lms_course.save_lesson",
args: { args: {
"title": $("#title").text(), "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"), "chapter": $("#title").data("chapter"),
"preview": $("#preview").prop("checked") ? 1 : 0, "preview": $("#preview").prop("checked") ? 1 : 0,
"idx": $("#title").data("index"), "idx": $("#title").data("index"),