diff --git a/lms/hooks.py b/lms/hooks.py index e0b1461c..ab6ac7b4 100644 --- a/lms/hooks.py +++ b/lms/hooks.py @@ -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 diff --git a/lms/lms/doctype/course_lesson/course_lesson.json b/lms/lms/doctype/course_lesson/course_lesson.json index eaee75f2..3fd75f37 100644 --- a/lms/lms/doctype/course_lesson/course_lesson.json +++ b/lms/lms/doctype/course_lesson/course_lesson.json @@ -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", diff --git a/lms/lms/doctype/lms_batch/lms_batch.json b/lms/lms/doctype/lms_batch/lms_batch.json index 26965356..a7949ce3 100644 --- a/lms/lms/doctype/lms_batch/lms_batch.json +++ b/lms/lms/doctype/lms_batch/lms_batch.json @@ -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", diff --git a/lms/lms/doctype/lms_batch/lms_batch.py b/lms/lms/doctype/lms_batch/lms_batch.py index 5a0b0467..3c6e034f 100644 --- a/lms/lms/doctype/lms_batch/lms_batch.py +++ b/lms/lms/doctype/lms_batch/lms_batch.py @@ -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() diff --git a/lms/lms/doctype/lms_course/lms_course.py b/lms/lms/doctype/lms_course/lms_course.py index cd6f184f..57c81d6b 100644 --- a/lms/lms/doctype/lms_course/lms_course.py +++ b/lms/lms/doctype/lms_course/lms_course.py @@ -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, diff --git a/lms/lms/utils.py b/lms/lms/utils.py index e41fe117..ec3e4489 100644 --- a/lms/lms/utils.py +++ b/lms/lms/utils.py @@ -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 diff --git a/lms/patches.txt b/lms/patches.txt index a0c423b3..b8e25bf8 100644 --- a/lms/patches.txt +++ b/lms/patches.txt @@ -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 \ No newline at end of file +lms.patches.v1_0.rename_classes_in_navbar +lms.patches.v1_0.publish_batches \ No newline at end of file diff --git a/lms/patches/v1_0/publish_batches.py b/lms/patches/v1_0/publish_batches.py new file mode 100644 index 00000000..4ef5a8e2 --- /dev/null +++ b/lms/patches/v1_0/publish_batches.py @@ -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) diff --git a/lms/plugins.py b/lms/plugins.py index 69571476..59dcd3ab 100644 --- a/lms/plugins.py +++ b/lms/plugins.py @@ -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""" + + """ + + def video_renderer(src): return ( f"" diff --git a/lms/public/css/style.css b/lms/public/css/style.css index 77f435fb..c8105e46 100644 --- a/lms/public/css/style.css +++ b/lms/public/css/style.css @@ -2339,4 +2339,8 @@ select { .batch-course-list .cards-parent { row-gap: 3rem +} + +.embed-tool__caption { + display: none; } \ No newline at end of file diff --git a/lms/public/js/common_functions.js b/lms/public/js/common_functions.js index cdafb590..8da2d052 100644 --- a/lms/public/js/common_functions.js +++ b/lms/public/js/common_functions.js @@ -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"), diff --git a/lms/www/batch/edit.html b/lms/www/batch/edit.html index 6fa88e22..7ad0b37b 100644 --- a/lms/www/batch/edit.html +++ b/lms/www/batch/edit.html @@ -66,13 +66,8 @@ {% macro CreateLesson() %}
-
-
- {{ _("Title") }} -
-
- {{ _("Something Short and Concise") }} -
+
+ {{ _("Title") }}
@@ -86,6 +81,19 @@
+
+
+ {{ _("Instructor Notes") }} +
+
+ {{ _("These notes will only be visible to the Course Creator, Course Evaluaor and Moderator.") }} +
+
+ {% if lesson.instructor_notes %} +
{{ lesson.instructor_notes }}
+ {% endif %} +
+
@@ -117,9 +125,9 @@ }; {% endif %} - {{ include_script('controls.bundle.js') }} + {% endblock %} diff --git a/lms/www/batch/edit.js b/lms/www/batch/edit.js index dd0ae722..d0efee8b 100644 --- a/lms/www/batch/edit.js +++ b/lms/www/batch/edit.js @@ -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: "", + }, + pdf: { + regex: /(https?:\/\/.*\.pdf)/, + embedUrl: "<%= remote_id %>", + html: "", + }, + }, + }, + }, 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"); +}; diff --git a/lms/www/batch/learn.html b/lms/www/batch/learn.html index e368d7b2..d2d778ff 100644 --- a/lms/www/batch/learn.html +++ b/lms/www/batch/learn.html @@ -149,17 +149,28 @@ {% if show_lesson %} {% if is_instructor and not lesson.include_in_preview %} -
+
{{ _("This lesson is not available for preview. As you are the Instructor of the course only you can see it.") }} ×
{% endif %} + {% if lesson.instructor_notes and (is_moderator or instructor or is_evaluator) %} +
+
+ {{ _("Instructor Notes") }} +
+
+ {{ lesson.instructor_notes }} +
+
+ {% endif %} + {{ render_html(lesson) }} {% else %} {% set course_link = "" + _('here') + "" %} -
+
{{ _("There is no preview available for this lesson. Please join the course to access it. Click {0} to enroll.").format(course_link) }} diff --git a/lms/www/batch/learn.py b/lms/www/batch/learn.py index ebf2eb31..c1fde031 100644 --- a/lms/www/batch/learn.py +++ b/lms/www/batch/learn.py @@ -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: diff --git a/lms/www/batches/batch.py b/lms/www/batches/batch.py index f70823cd..05d7b001 100644 --- a/lms/www/batches/batch.py +++ b/lms/www/batches/batch.py @@ -40,6 +40,7 @@ def get_context(context): "amount", "currency", "batch_details", + "published", ], as_dict=True, ) diff --git a/lms/www/batches/batch_details.py b/lms/www/batches/batch_details.py index d7d59b61..6707da54 100644 --- a/lms/www/batches/batch_details.py +++ b/lms/www/batches/batch_details.py @@ -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) diff --git a/lms/www/batches/index.html b/lms/www/batches/index.html index bf5f998f..21178371 100644 --- a/lms/www/batches/index.html +++ b/lms/www/batches/index.html @@ -7,8 +7,8 @@
{{ 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 @@ {% endmacro %} -{% macro BatchTabs(past_batches, upcoming_batches, my_batches) %} +{% macro BatchTabs(past_batches, upcoming_batches, private_batches, my_batches) %}