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",
"Video": "lms.plugins.video_renderer",
"Assignment": "lms.plugins.assignment_renderer",
"Embed": "lms.plugins.embed_renderer",
}
# page_renderer to manage profile pages

View File

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

View File

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

View File

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

View File

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

View File

@@ -145,11 +145,14 @@ def get_lesson_details(chapter):
"quiz_id",
"question",
"file_type",
"instructor_notes",
],
as_dict=True,
)
lesson_details.number = flt(f"{chapter.idx}.{row.idx}")
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)
return lessons

View File

@@ -66,4 +66,5 @@ lms.patches.v1_0.revert_class_registration #18-08-2023
lms.patches.v1_0.rename_lms_batch_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_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):
return (
f"<video controls width='100%'><source src={quote(src)} type='video/mp4'></video>"

View File

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

View File

@@ -261,6 +261,24 @@ const open_batch_dialog = () => {
reqd: 1,
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",
label: __("Start Date"),
@@ -297,12 +315,6 @@ const open_batch_dialog = () => {
fieldname: "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",
label: __("Category"),

View File

@@ -66,13 +66,8 @@
{% macro CreateLesson() %}
<article class="field-parent">
<div class="field-group">
<div>
<div class="field-label">
{{ _("Title") }}
</div>
<div class="field-description">
{{ _("Something Short and Concise") }}
</div>
<div class="field-label">
{{ _("Title") }}
</div>
<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 %}>
@@ -86,6 +81,19 @@
</label>
</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>
<div class="field-label">
@@ -117,9 +125,9 @@
};
</script>
{% 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/paragraph@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 %}

View File

@@ -1,7 +1,15 @@
frappe.ready(() => {
frappe.telemetry.capture("on_lesson_creation_page", "lms");
let self = this;
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) {
parse_string_to_lesson();
}
@@ -18,6 +26,27 @@ const setup_editor = () => {
self.editor = new EditorJS({
holder: "lesson-content",
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: {
class: Header,
inlineToolbar: ["bold", "italic", "link"],
@@ -76,6 +105,15 @@ const parse_string_to_lesson = () => {
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("![]")) {
let image = block.match(/\((.*?)\)/)[1];
lesson_blocks.push({
@@ -131,6 +169,15 @@ const parse_lesson_to_string = (data) => {
"#".repeat(block.data.level) + ` ${block.data.text}\n`;
} else if (block.type == "paragraph") {
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);
@@ -149,6 +196,8 @@ const save = (lesson_content) => {
preview: $("#preview").prop("checked") ? 1 : 0,
idx: $("#lesson-title").data("index"),
lesson: lesson ? lesson : "",
instructor_notes:
this.instructor_notes.get_values().instructor_notes,
},
callback: (data) => {
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 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.") }}
<a href="#" class="close" data-dismiss="alert" aria-label="close">&times;</a>
</div>
{% 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) }}
{% else %}
{% 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.
Please join the course to access it.
Click {0} to enroll.").format(course_link) }}

View File

@@ -2,7 +2,12 @@ import frappe
from frappe import _
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 (
get_common_context,
redirect_to_lesson,
@@ -37,20 +42,23 @@ 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.instructor = is_instructor(context.course.name)
context.is_moderator = has_course_moderator_role()
context.is_evaluator = has_course_evaluator_role()
context.show_lesson = (
context.membership
or (context.lesson and context.lesson.include_in_preview)
or instructor
or has_course_moderator_role()
or context.instructor
or context.is_moderator
or context.is_evaluator
)
if not context.lesson:
context.lesson = frappe._dict()
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."))
context.lesson.edit_mode = True
else:

View File

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

View File

@@ -1,4 +1,5 @@
import frappe
from frappe import _
from lms.lms.utils import has_course_moderator_role, has_course_evaluator_role
from lms.www.utils import is_student
@@ -23,10 +24,17 @@ def get_context(context):
"start_time",
"end_time",
"seat_count",
"published",
],
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(
"Batch Course",
{"parent": batch_name},
@@ -44,6 +52,4 @@ def get_context(context):
context.student_count = frappe.db.count("Batch Student", {"parent": batch_name})
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)

View File

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

View File

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