feat: publish batches

This commit is contained in:
Jannat Patel
2023-09-12 15:09:13 +05:30
19 changed files with 218 additions and 33 deletions

View File

@@ -304,6 +304,7 @@ lms_markdown_macro_renderers = {
"YouTubeVideo": "lms.plugins.youtube_video_renderer", "YouTubeVideo": "lms.plugins.youtube_video_renderer",
"Video": "lms.plugins.video_renderer", "Video": "lms.plugins.video_renderer",
"Assignment": "lms.plugins.assignment_renderer", "Assignment": "lms.plugins.assignment_renderer",
"Embed": "lms.plugins.embed_renderer",
} }
# page_renderer to manage profile pages # page_renderer to manage profile pages

View File

@@ -24,6 +24,7 @@
"file_type", "file_type",
"section_break_11", "section_break_11",
"body", "body",
"instructor_notes",
"help_section", "help_section",
"help" "help"
], ],
@@ -131,11 +132,16 @@
{ {
"fieldname": "column_break_15", "fieldname": "column_break_15",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"fieldname": "instructor_notes",
"fieldtype": "Text",
"label": "Instructor Notes"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2023-05-02 12:42:16.926753", "modified": "2023-08-31 21:47:06.314995",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "Course Lesson", "name": "Course Lesson",

View File

@@ -14,6 +14,7 @@
"column_break_4", "column_break_4",
"start_time", "start_time",
"end_time", "end_time",
"published",
"section_break_rgfj", "section_break_rgfj",
"medium", "medium",
"category", "category",
@@ -192,11 +193,17 @@
"fieldtype": "Text Editor", "fieldtype": "Text Editor",
"label": "Batch Details", "label": "Batch Details",
"reqd": 1 "reqd": 1
},
{
"default": "0",
"fieldname": "published",
"fieldtype": "Check",
"label": "Published"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2023-08-23 17:35:42.750754", "modified": "2023-09-12 12:30:06.565104",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Batch", "name": "LMS Batch",

View File

@@ -201,6 +201,7 @@ def create_batch(
amount=0, amount=0,
currency=None, currency=None,
name=None, name=None,
published=0,
): ):
frappe.only_for("Moderator") frappe.only_for("Moderator")
if name: if name:
@@ -223,6 +224,7 @@ def create_batch(
"paid_batch": paid_batch, "paid_batch": paid_batch,
"amount": amount, "amount": amount,
"currency": currency, "currency": currency,
"published": published,
} }
) )
doc.save() doc.save()

View File

@@ -281,6 +281,7 @@ def save_lesson(
preview, preview,
idx, idx,
lesson, lesson,
instructor_notes=None,
youtube=None, youtube=None,
quiz_id=None, quiz_id=None,
question=None, question=None,
@@ -296,6 +297,7 @@ def save_lesson(
"chapter": chapter, "chapter": chapter,
"title": title, "title": title,
"body": body, "body": body,
"instructor_notes": instructor_notes,
"include_in_preview": preview, "include_in_preview": preview,
"youtube": youtube, "youtube": youtube,
"quiz_id": quiz_id, "quiz_id": quiz_id,

View File

@@ -145,11 +145,14 @@ def get_lesson_details(chapter):
"quiz_id", "quiz_id",
"question", "question",
"file_type", "file_type",
"instructor_notes",
], ],
as_dict=True, as_dict=True,
) )
lesson_details.number = flt(f"{chapter.idx}.{row.idx}") lesson_details.number = flt(f"{chapter.idx}.{row.idx}")
lesson_details.icon = get_lesson_icon(lesson_details.body) lesson_details.icon = get_lesson_icon(lesson_details.body)
if lesson_details.instructor_notes:
lesson_details.instructor_notes = markdown_to_html(lesson_details.instructor_notes)
lessons.append(lesson_details) lessons.append(lesson_details)
return lessons return lessons

View File

@@ -67,3 +67,4 @@ lms.patches.v1_0.rename_lms_batch_doctype
lms.patches.v1_0.rename_lms_batch_membership_doctype lms.patches.v1_0.rename_lms_batch_membership_doctype
lms.patches.v1_0.rename_lms_class_to_lms_batch lms.patches.v1_0.rename_lms_class_to_lms_batch
lms.patches.v1_0.rename_classes_in_navbar lms.patches.v1_0.rename_classes_in_navbar
lms.patches.v1_0.publish_batches

View File

@@ -0,0 +1,8 @@
import frappe
def execute():
batches = frappe.get_all("LMS Batch", pluck="name")
for batch in batches:
frappe.db.set_value("LMS Batch", batch, "Published", 1)

View File

@@ -155,6 +155,28 @@ def youtube_video_renderer(video_id):
""" """
def embed_renderer(details):
type = details.split("|||")[0]
src = details.split("|||")[1]
width = "100%"
height = "400"
if type == "pdf":
width = "75%"
height = "600"
return f"""
<iframe width={width} height={height}
src={src}
title="Embedded Content"
frameborder="0"
style="border-radius: var(--border-radius-lg)"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen>
</iframe>
"""
def video_renderer(src): def video_renderer(src):
return ( return (
f"<video controls width='100%'><source src={quote(src)} type='video/mp4'></video>" f"<video controls width='100%'><source src={quote(src)} type='video/mp4'></video>"

View File

@@ -2340,3 +2340,7 @@ select {
.batch-course-list .cards-parent { .batch-course-list .cards-parent {
row-gap: 3rem row-gap: 3rem
} }
.embed-tool__caption {
display: none;
}

View File

@@ -261,6 +261,24 @@ const open_batch_dialog = () => {
reqd: 1, reqd: 1,
default: batch_info && batch_info.title, default: batch_info && batch_info.title,
}, },
{
fieldtype: "Check",
label: __("Published"),
fieldname: "published",
default: batch_info && batch_info.published,
},
{
fieldtype: "Column Break",
},
{
fieldtype: "Int",
label: __("Seat Count"),
fieldname: "seat_count",
default: batch_info && batch_info.seat_count,
},
{
fieldtype: "Section Break",
},
{ {
fieldtype: "Date", fieldtype: "Date",
label: __("Start Date"), label: __("Start Date"),
@@ -297,12 +315,6 @@ const open_batch_dialog = () => {
fieldname: "end_time", fieldname: "end_time",
default: batch_info && batch_info.end_time, default: batch_info && batch_info.end_time,
}, },
{
fieldtype: "Int",
label: __("Seat Count"),
fieldname: "seat_count",
default: batch_info && batch_info.seat_count,
},
{ {
fieldtype: "Link", fieldtype: "Link",
label: __("Category"), label: __("Category"),

View File

@@ -66,13 +66,8 @@
{% macro CreateLesson() %} {% macro CreateLesson() %}
<article class="field-parent"> <article class="field-parent">
<div class="field-group"> <div class="field-group">
<div> <div class="field-label">
<div class="field-label"> {{ _("Title") }}
{{ _("Title") }}
</div>
<div class="field-description">
{{ _("Something Short and Concise") }}
</div>
</div> </div>
<div class=""> <div class="">
<input id="lesson-title" type="text" class="field-input" data-index="{{ lesson_index }}" data-chapter="{{ chapter }}" data-course="{{ course.name }}" {% if lesson.name %} data-lesson="{{ lesson.name }}" value="{{ lesson.title }}" {% endif %}> <input id="lesson-title" type="text" class="field-input" data-index="{{ lesson_index }}" data-chapter="{{ chapter }}" data-course="{{ course.name }}" {% if lesson.name %} data-lesson="{{ lesson.name }}" value="{{ lesson.title }}" {% endif %}>
@@ -86,6 +81,19 @@
</label> </label>
</div> </div>
<div class="field-group">
<div class="field-label">
{{ _("Instructor Notes") }}
</div>
<div class="field-description">
{{ _("These notes will only be visible to the Course Creator, Course Evaluaor and Moderator.") }}
</div>
<div id="instructor-notes"></div>
{% if lesson.instructor_notes %}
<div id="current-instructor-notes" class="hide">{{ lesson.instructor_notes }}</div>
{% endif %}
</div>
<div class="field-group"> <div class="field-group">
<div> <div>
<div class="field-label"> <div class="field-label">
@@ -117,9 +125,9 @@
}; };
</script> </script>
{% endif %} {% endif %}
{{ include_script('controls.bundle.js') }}
<script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@latest"></script> <script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/paragraph@latest"></script> <script src="https://cdn.jsdelivr.net/npm/@editorjs/paragraph@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/header@latest"></script> <script src="https://cdn.jsdelivr.net/npm/@editorjs/header@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/embed@latest"></script>
{% endblock %} {% endblock %}

View File

@@ -1,7 +1,15 @@
frappe.ready(() => { frappe.ready(() => {
frappe.telemetry.capture("on_lesson_creation_page", "lms");
let self = this; let self = this;
this.quiz_in_lesson = []; this.quiz_in_lesson = [];
frappe.telemetry.capture("on_lesson_creation_page", "lms");
if ($("#instructor-notes").length) {
frappe.require("controls.bundle.js", () => {
make_instructor_notes_component();
});
}
if ($("#current-lesson-content").length) { if ($("#current-lesson-content").length) {
parse_string_to_lesson(); parse_string_to_lesson();
} }
@@ -18,6 +26,27 @@ const setup_editor = () => {
self.editor = new EditorJS({ self.editor = new EditorJS({
holder: "lesson-content", holder: "lesson-content",
tools: { tools: {
embed: {
class: Embed,
config: {
services: {
youtube: true,
vimeo: true,
codepen: true,
slides: {
regex: /https:\/\/docs\.google\.com\/presentation\/d\/e\/([A-Za-z0-9_-]+)\/pub/,
embedUrl:
"https://docs.google.com/presentation/d/e/<%= remote_id %>/embed",
html: "<iframe width='100%' height='300' frameborder='0' allowfullscreen='true'></iframe>",
},
pdf: {
regex: /(https?:\/\/.*\.pdf)/,
embedUrl: "<%= remote_id %>",
html: "<iframe width='100%' height='600px' frameborder='0'></iframe>",
},
},
},
},
header: { header: {
class: Header, class: Header,
inlineToolbar: ["bold", "italic", "link"], inlineToolbar: ["bold", "italic", "link"],
@@ -76,6 +105,15 @@ const parse_string_to_lesson = () => {
file_url: video, file_url: video,
}, },
}); });
} else if (block.includes("{{ Embed")) {
let embed = block.match(/'([^']+)'/)[1];
lesson_blocks.push({
type: "embed",
data: {
service: embed.split("|||")[0],
embed: embed.split("|||")[1],
},
});
} else if (block.includes("![]")) { } else if (block.includes("![]")) {
let image = block.match(/\((.*?)\)/)[1]; let image = block.match(/\((.*?)\)/)[1];
lesson_blocks.push({ lesson_blocks.push({
@@ -131,6 +169,15 @@ const parse_lesson_to_string = (data) => {
"#".repeat(block.data.level) + ` ${block.data.text}\n`; "#".repeat(block.data.level) + ` ${block.data.text}\n`;
} else if (block.type == "paragraph") { } else if (block.type == "paragraph") {
lesson_content += `${block.data.text}\n`; lesson_content += `${block.data.text}\n`;
} else if (block.type == "embed") {
if (block.data.service == "pdf") {
if (!block.data.embed.startsWith(window.location.origin)) {
frappe.throw(__("Invalid PDF URL"));
}
}
lesson_content += `{{ Embed("${
block.data.service
}|||${block.data.embed.replace(/&amp;/g, "&")}") }}\n`;
} }
}); });
save(lesson_content); save(lesson_content);
@@ -149,6 +196,8 @@ const save = (lesson_content) => {
preview: $("#preview").prop("checked") ? 1 : 0, preview: $("#preview").prop("checked") ? 1 : 0,
idx: $("#lesson-title").data("index"), idx: $("#lesson-title").data("index"),
lesson: lesson ? lesson : "", lesson: lesson ? lesson : "",
instructor_notes:
this.instructor_notes.get_values().instructor_notes,
}, },
callback: (data) => { callback: (data) => {
frappe.show_alert({ frappe.show_alert({
@@ -466,3 +515,20 @@ class Upload {
}; };
} }
} }
const make_instructor_notes_component = () => {
this.instructor_notes = new frappe.ui.FieldGroup({
fields: [
{
fieldname: "instructor_notes",
fieldtype: "Text Editor",
default: $("#current-instructor-notes").html(),
},
],
body: $("#instructor-notes").get(0),
});
this.instructor_notes.make();
$("#instructor-notes .form-section:last").removeClass("empty-section");
$("#instructor-notes .frappe-control").removeClass("hide-control");
$("#instructor-notes .form-column").addClass("p-0");
};

View File

@@ -149,17 +149,28 @@
{% if show_lesson %} {% if show_lesson %}
{% if is_instructor and not lesson.include_in_preview %} {% if is_instructor and not lesson.include_in_preview %}
<div class="medium alert alert-info alert-dismissible mb-4"> <div class="alert alert-info alert-dismissible mb-4">
{{ _("This lesson is not available for preview. As you are the Instructor of the course only you can see it.") }} {{ _("This lesson is not available for preview. As you are the Instructor of the course only you can see it.") }}
<a href="#" class="close" data-dismiss="alert" aria-label="close">&times;</a> <a href="#" class="close" data-dismiss="alert" aria-label="close">&times;</a>
</div> </div>
{% endif %} {% endif %}
{% if lesson.instructor_notes and (is_moderator or instructor or is_evaluator) %}
<div class="alert alert-info mb-4">
<div class="bold-heading mb-2">
{{ _("Instructor Notes") }}
</div>
<div>
{{ lesson.instructor_notes }}
</div>
</div>
{% endif %}
{{ render_html(lesson) }} {{ render_html(lesson) }}
{% else %} {% else %}
{% set course_link = "<a class='enroll-in-course' data-course=" + course.name | urlencode + " href=''>" + _('here') + "</a>" %} {% set course_link = "<a class='enroll-in-course' data-course=" + course.name | urlencode + " href=''>" + _('here') + "</a>" %}
<div class="alert alert-info medium mb-0"> <div class="alert alert-info mb-0">
{{ _("There is no preview available for this lesson. {{ _("There is no preview available for this lesson.
Please join the course to access it. Please join the course to access it.
Click {0} to enroll.").format(course_link) }} Click {0} to enroll.").format(course_link) }}

View File

@@ -2,7 +2,12 @@ import frappe
from frappe import _ from frappe import _
from frappe.utils import cstr, flt from frappe.utils import cstr, flt
from lms.lms.utils import get_lesson_url, has_course_moderator_role, is_instructor from lms.lms.utils import (
get_lesson_url,
has_course_moderator_role,
is_instructor,
has_course_evaluator_role,
)
from lms.www.utils import ( from lms.www.utils import (
get_common_context, get_common_context,
redirect_to_lesson, redirect_to_lesson,
@@ -37,20 +42,23 @@ def get_context(context):
redirect_to_lesson(context.course, index_) redirect_to_lesson(context.course, index_)
context.lesson = get_current_lesson_details(lesson_number, context) context.lesson = get_current_lesson_details(lesson_number, context)
instructor = is_instructor(context.course.name) context.instructor = is_instructor(context.course.name)
context.is_moderator = has_course_moderator_role()
context.is_evaluator = has_course_evaluator_role()
context.show_lesson = ( context.show_lesson = (
context.membership context.membership
or (context.lesson and context.lesson.include_in_preview) or (context.lesson and context.lesson.include_in_preview)
or instructor or context.instructor
or has_course_moderator_role() or context.is_moderator
or context.is_evaluator
) )
if not context.lesson: if not context.lesson:
context.lesson = frappe._dict() context.lesson = frappe._dict()
if frappe.form_dict.get("edit"): if frappe.form_dict.get("edit"):
if not instructor and not has_course_moderator_role(): if not context.instructor and not context.is_moderator:
raise frappe.PermissionError(_("You do not have permission to access this page.")) raise frappe.PermissionError(_("You do not have permission to access this page."))
context.lesson.edit_mode = True context.lesson.edit_mode = True
else: else:

View File

@@ -40,6 +40,7 @@ def get_context(context):
"amount", "amount",
"currency", "currency",
"batch_details", "batch_details",
"published",
], ],
as_dict=True, as_dict=True,
) )

View File

@@ -1,4 +1,5 @@
import frappe import frappe
from frappe import _
from lms.lms.utils import has_course_moderator_role, has_course_evaluator_role from lms.lms.utils import has_course_moderator_role, has_course_evaluator_role
from lms.www.utils import is_student from lms.www.utils import is_student
@@ -23,10 +24,17 @@ def get_context(context):
"start_time", "start_time",
"end_time", "end_time",
"seat_count", "seat_count",
"published",
], ],
as_dict=1, as_dict=1,
) )
context.is_moderator = has_course_moderator_role()
context.is_evaluator = has_course_evaluator_role()
if not context.is_moderator and not context.batch_info.published:
raise frappe.PermissionError(_("You do not have permission to access this page."))
context.courses = frappe.get_all( context.courses = frappe.get_all(
"Batch Course", "Batch Course",
{"parent": batch_name}, {"parent": batch_name},
@@ -44,6 +52,4 @@ def get_context(context):
context.student_count = frappe.db.count("Batch Student", {"parent": batch_name}) context.student_count = frappe.db.count("Batch Student", {"parent": batch_name})
context.seats_left = context.batch_info.seat_count - context.student_count context.seats_left = context.batch_info.seat_count - context.student_count
context.is_moderator = has_course_moderator_role()
context.is_evaluator = has_course_evaluator_role()
context.is_student = is_student(batch_name) context.is_student = is_student(batch_name)

View File

@@ -7,8 +7,8 @@
<div class="common-page-style lms-page-style"> <div class="common-page-style lms-page-style">
<div class="container"> <div class="container">
{{ Header() }} {{ Header() }}
{% if past_batches | length or upcoming_batches | length %} {% if past_batches | length or upcoming_batches | length or private_batches | length %}
{{ BatchTabs(past_batches, upcoming_batches, my_batches) }} {{ BatchTabs(past_batches, upcoming_batches, private_batches, my_batches) }}
{% else %} {% else %}
{{ EmptyState() }} {{ EmptyState() }}
{% endif %} {% endif %}
@@ -27,7 +27,7 @@
</header> </header>
{% endmacro %} {% endmacro %}
{% macro BatchTabs(past_batches, upcoming_batches, my_batches) %} {% macro BatchTabs(past_batches, upcoming_batches, private_batches, my_batches) %}
<article> <article>
<ul class="nav lms-nav" id="courses-tab"> <ul class="nav lms-nav" id="courses-tab">
@@ -49,6 +49,15 @@
</span> </span>
</a> </a>
</li> </li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#private">
{{ _("Private") }}
<span class="course-list-count">
{{ private_batches | length }}
</span>
</a>
</li>
{% endif %} {% endif %}
{% if frappe.session.user != "Guest" %} {% if frappe.session.user != "Guest" %}
@@ -75,6 +84,10 @@
<div class="tab-pane" id="past" role="tabpanel" aria-labelledby="past"> <div class="tab-pane" id="past" role="tabpanel" aria-labelledby="past">
{{ BatchCard(past_batches, show_price=False, label="Archived") }} {{ BatchCard(past_batches, show_price=False, label="Archived") }}
</div> </div>
<div class="tab-pane" id="private" role="tabpanel" aria-labelledby="private">
{{ BatchCard(private_batches, show_price=False, label="Private") }}
</div>
{% endif %} {% endif %}
{% if frappe.session.user != "Guest" %} {% if frappe.session.user != "Guest" %}

View File

@@ -19,25 +19,29 @@ def get_context(context):
"amount", "amount",
"currency", "currency",
"seat_count", "seat_count",
"published",
], ],
order_by="start_date", order_by="start_date",
) )
past_batches, upcoming_batches = [], [] past_batches, upcoming_batches, private_batches = [], [], []
for batch in batches: for batch in batches:
batch.student_count = frappe.db.count("Batch Student", {"parent": batch.name}) batch.student_count = frappe.db.count("Batch Student", {"parent": batch.name})
batch.course_count = frappe.db.count("Batch Course", {"parent": batch.name}) batch.course_count = frappe.db.count("Batch Course", {"parent": batch.name})
batch.seats_left = ( batch.seats_left = (
batch.seat_count - batch.student_count if batch.seat_count else None batch.seat_count - batch.student_count if batch.seat_count else None
) )
print(batch.seat_count, batch.student_count, batch.seats_left) print(batch.name, batch.published)
if getdate(batch.start_date) < getdate(): if not batch.published:
private_batches.append(batch)
elif getdate(batch.start_date) < getdate():
past_batches.append(batch) past_batches.append(batch)
else: else:
upcoming_batches.append(batch) upcoming_batches.append(batch)
context.past_batches = sorted(past_batches, key=lambda d: d.start_date) context.past_batches = sorted(past_batches, key=lambda d: d.start_date)
context.upcoming_batches = sorted(upcoming_batches, key=lambda d: d.start_date) context.upcoming_batches = sorted(upcoming_batches, key=lambda d: d.start_date)
context.private_batches = sorted(private_batches, key=lambda d: d.start_date)
if frappe.session.user != "Guest": if frappe.session.user != "Guest":
my_batches_info = [] my_batches_info = []