feat: publish batches
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
8
lms/patches/v1_0/publish_batches.py
Normal file
8
lms/patches/v1_0/publish_batches.py
Normal 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)
|
||||
@@ -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>"
|
||||
|
||||
@@ -2339,4 +2339,8 @@ select {
|
||||
|
||||
.batch-course-list .cards-parent {
|
||||
row-gap: 3rem
|
||||
}
|
||||
|
||||
.embed-tool__caption {
|
||||
display: none;
|
||||
}
|
||||
@@ -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"),
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
|
||||
@@ -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(/&/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");
|
||||
};
|
||||
|
||||
@@ -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">×</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) }}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -40,6 +40,7 @@ def get_context(context):
|
||||
"amount",
|
||||
"currency",
|
||||
"batch_details",
|
||||
"published",
|
||||
],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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" %}
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
Reference in New Issue
Block a user