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",
"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

@@ -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)

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 "/" 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

View File

@@ -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 = {

View File

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

View File

@@ -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 @@
<!-- 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">
<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">
@@ -214,97 +228,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> {{ _("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>
<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">
<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('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>
</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

@@ -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"),