From db71f1271b1f97a7d1c8e10a334ec27695a90e76 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Wed, 27 Sep 2023 17:59:36 +0530 Subject: [PATCH 1/4] feat: editor js for instructor notes --- .../doctype/course_lesson/course_lesson.json | 4 +- lms/lms/utils.py | 5 - lms/public/css/style.css | 16 ++ lms/www/batch/edit.html | 47 +++-- lms/www/batch/edit.js | 192 ++++++++++-------- lms/www/batch/learn.html | 45 +--- lms/www/batch/learn.py | 4 + 7 files changed, 174 insertions(+), 139 deletions(-) diff --git a/lms/lms/doctype/course_lesson/course_lesson.json b/lms/lms/doctype/course_lesson/course_lesson.json index 3fd75f37..4bd3c65d 100644 --- a/lms/lms/doctype/course_lesson/course_lesson.json +++ b/lms/lms/doctype/course_lesson/course_lesson.json @@ -135,13 +135,13 @@ }, { "fieldname": "instructor_notes", - "fieldtype": "Text", + "fieldtype": "Markdown Editor", "label": "Instructor Notes" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-08-31 21:47:06.314995", + "modified": "2023-09-27 15:45:54.738573", "modified_by": "Administrator", "module": "LMS", "name": "Course Lesson", diff --git a/lms/lms/utils.py b/lms/lms/utils.py index f66db678..dfefe26f 100644 --- a/lms/lms/utils.py +++ b/lms/lms/utils.py @@ -152,11 +152,6 @@ def get_lesson_details(chapter): ) 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_html = markdown_to_html( - lesson_details.instructor_notes - ) - lessons.append(lesson_details) return lessons diff --git a/lms/public/css/style.css b/lms/public/css/style.css index a6ea778b..5cd1fb3f 100644 --- a/lms/public/css/style.css +++ b/lms/public/css/style.css @@ -160,6 +160,10 @@ textarea.field-input { position: unset; } +.codex-editor--narrow .ce-toolbar__actions { + right: 100%; +} + .lesson-editor { border: 1px solid var(--gray-300); border-radius: var(--border-radius-md); @@ -2453,4 +2457,16 @@ select { .batch-details { width: 100%; } +} + +.collapse-section { + font-size: var(--text-lg); + cursor: pointer; +} + +.collapse-section.collapsed .icon { + transition: all 0.5s; + -webkit-transform: rotate(180deg); + -moz-transform: rotate(180deg); + transform: rotate(180deg); } \ No newline at end of file diff --git a/lms/www/batch/edit.html b/lms/www/batch/edit.html index 641f5256..5f47b3a0 100644 --- a/lms/www/batch/edit.html +++ b/lms/www/batch/edit.html @@ -75,41 +75,54 @@
-
-
- {{ _("Instructor Notes") }} + -
- {{ _("These notes will only be visible to the Course Creator, Course Evaluaor and Moderator.") }} + +
+
-
- {% if lesson.instructor_notes %} -
{{ lesson.instructor_notes }}
+ + {% if lesson.body %} +
{{ lesson.body }}
{% endif %}
-
+ +
+
+
+ {% if lesson.instructor_notes %} +
{{ lesson.instructor_notes }}
{% endif %}
- {% endmacro %} diff --git a/lms/www/batch/edit.js b/lms/www/batch/edit.js index 69f4c463..9b124812 100644 --- a/lms/www/batch/edit.js +++ b/lms/www/batch/edit.js @@ -1,110 +1,129 @@ frappe.ready(() => { let self = this; - + frappe.require("controls.bundle.js"); 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(); + parse_string_to_lesson("lesson"); } - setup_editor(); + if ($("#current-instructor-notes").length) { + parse_string_to_lesson("notes"); + } + + setup_editor_for_lesson_content(); + setup_editor_for_instructor_notes(); $("#save-lesson").click((e) => { save_lesson(e); }); }); -const setup_editor = () => { +const setup_editor_for_lesson_content = () => { 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"], - config: { - levels: [4, 5, 6], - defaultLevel: 5, - }, - icon: ` - - `, - }, - paragraph: { - class: Paragraph, - inlineToolbar: true, - config: { - preserveBlank: true, - }, - }, - youtube: YouTubeVideo, - quiz: Quiz, - upload: Upload, - }, + tools: get_tools(), data: { - blocks: self.blocks ? self.blocks : [], + blocks: self.lesson_blocks || [], }, }); }; -const parse_string_to_lesson = () => { - let lesson_content = $("#current-lesson-content").html(); - let lesson_blocks = []; +const setup_editor_for_instructor_notes = () => { + self.instructor_notes_editor = new EditorJS({ + holder: "instructor-notes", + tools: get_tools(), + data: { + blocks: self.notes_blocks || [], + }, + }); +}; - lesson_content.split("\n").forEach((block) => { +const get_tools = () => { + return { + 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"], + config: { + levels: [4, 5, 6], + defaultLevel: 5, + }, + icon: ` + + `, + }, + paragraph: { + class: Paragraph, + inlineToolbar: true, + config: { + preserveBlank: true, + }, + }, + youtube: YouTubeVideo, + quiz: Quiz, + upload: Upload, + }; +}; + +const parse_string_to_lesson = (type) => { + let content; + let blocks = []; + + if (type == "lesson") { + content = $("#current-lesson-content").html(); + } else if (type == "notes") { + content = $("#current-instructor-notes").html(); + } + + content.split("\n").forEach((block) => { if (block.includes("{{ YouTubeVideo")) { - let youtube_id = block.match(/'([^']+)'/)[1]; - lesson_blocks.push({ + let youtube_id = block.match(/\(["']([^"']+?)["']\)/)[1]; + blocks.push({ type: "youtube", data: { youtube: youtube_id, }, }); } else if (block.includes("{{ Quiz")) { - let quiz = block.match(/'([^']+)'/)[1]; - lesson_blocks.push({ + let quiz = block.match(/\(["']([^"']+?)["']\)/)[1]; + blocks.push({ type: "quiz", data: { quiz: quiz, }, }); } else if (block.includes("{{ Video")) { - let video = block.match(/'([^']+)'/)[1]; - lesson_blocks.push({ + let video = block.match(/\(["']([^"']+?)["']\)/)[1]; + blocks.push({ type: "upload", data: { file_url: video, }, }); } else if (block.includes("{{ Embed")) { - let embed = block.match(/'([^']+)'/)[1]; - lesson_blocks.push({ + let embed = block.match(/\(["']([^"']+?)["']\)/)[1]; + blocks.push({ type: "embed", data: { service: embed.split("|||")[0], @@ -113,7 +132,7 @@ const parse_string_to_lesson = () => { }); } else if (block.includes("![]")) { let image = block.match(/\((.*?)\)/)[1]; - lesson_blocks.push({ + blocks.push({ type: "upload", data: { file_url: image, @@ -121,7 +140,7 @@ const parse_string_to_lesson = () => { }); } else if (block.includes("#")) { let level = (block.match(/#/g) || []).length; - lesson_blocks.push({ + blocks.push({ type: "header", data: { text: block.replace(/#/g, "").trim(), @@ -129,7 +148,7 @@ const parse_string_to_lesson = () => { }, }); } else { - lesson_blocks.push({ + blocks.push({ type: "paragraph", data: { text: block, @@ -138,16 +157,25 @@ const parse_string_to_lesson = () => { } }); - this.blocks = lesson_blocks; + if (type == "lesson") { + this.lesson_blocks = blocks; + } else if (type == "notes") { + this.notes_blocks = blocks; + } }; const save_lesson = (e) => { self.editor.save().then((outputData) => { - parse_lesson_to_string(outputData); + parse_content_to_string(outputData, "lesson"); + + self.instructor_notes_editor.save().then((outputData) => { + parse_content_to_string(outputData, "notes"); + save(); + }); }); }; -const parse_lesson_to_string = (data) => { +const parse_content_to_string = (data, type) => { let lesson_content = ""; data.blocks.forEach((block) => { if (block.type == "youtube") { @@ -175,24 +203,28 @@ const parse_lesson_to_string = (data) => { }|||${block.data.embed.replace(/&/g, "&")}") }}\n`; } }); - save(lesson_content); + if (type == "lesson") { + this.lesson_content_data = lesson_content; + } else if (type == "notes") { + this.instructor_notes_data = lesson_content; + } }; -const save = (lesson_content) => { - validate_mandatory(lesson_content); +const save = () => { + console.log(this.instructor_notes_data); + console.log(this.lesson_content_data); + validate_mandatory(this.lesson_content_data); let lesson = $("#lesson-title").data("lesson"); - frappe.call({ method: "lms.lms.doctype.lms_course.lms_course.save_lesson", args: { title: $("#lesson-title").val(), - body: lesson_content, + body: this.lesson_content_data, chapter: $("#lesson-title").data("chapter"), preview: $("#preview").prop("checked") ? 1 : 0, idx: $("#lesson-title").data("index"), lesson: lesson ? lesson : "", - instructor_notes: - this.instructor_notes.get_values().instructor_notes, + instructor_notes: this.instructor_notes_data, }, callback: (data) => { frappe.show_alert({ diff --git a/lms/www/batch/learn.html b/lms/www/batch/learn.html index a4720d3a..7c34b3b6 100644 --- a/lms/www/batch/learn.html +++ b/lms/www/batch/learn.html @@ -156,12 +156,17 @@ {% endif %} {% if lesson.instructor_notes and (is_moderator or instructor or is_evaluator) %} -
-
- {{ _("Instructor Notes") }} +
+ -
- {{ lesson.instructor_notes_html }} +
+ {{ instructor_notes }}
{% endif %} @@ -227,36 +232,6 @@
{% endmacro %} - - -{% macro HelpArticle() %} -
-

{{ _("Embed Components") }}

-

- {{ _("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.") }} -

-
    -
  1. - {{ _("YouTube Video") }} -

    To get the YouTube Video ID, follow the steps mentioned below.

    -
      -
    • - {{ _("Upload the video on youtube.") }} -
    • -
    • - {{ _("When you share a youtube video, it shows a URL") }} -
    • -
    • - {{ _("Copy the last parameter of the URL and paste it here.") }} -
    • -
    -
  2. -
-
-{% endmacro %} - - {% macro Discussions() %} {% set topics_count = frappe.db.count("Discussion Topic", { diff --git a/lms/www/batch/learn.py b/lms/www/batch/learn.py index c1fde031..c77233b7 100644 --- a/lms/www/batch/learn.py +++ b/lms/www/batch/learn.py @@ -1,6 +1,7 @@ import frappe from frappe import _ from frappe.utils import cstr, flt +from lms.lms.md import markdown_to_html from lms.lms.utils import ( get_lesson_url, @@ -46,6 +47,9 @@ def get_context(context): context.is_moderator = has_course_moderator_role() context.is_evaluator = has_course_evaluator_role() + if context.lesson.instructor_notes: + context.instructor_notes = markdown_to_html(context.lesson.instructor_notes) + context.show_lesson = ( context.membership or (context.lesson and context.lesson.include_in_preview) From d5387a0d1ab749c5519eedf8f570d3af1be40676 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Wed, 27 Sep 2023 19:57:37 +0530 Subject: [PATCH 2/4] test: fix course creation test --- cypress/e2e/course_creation.cy.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/cypress/e2e/course_creation.cy.js b/cypress/e2e/course_creation.cy.js index 48f3c5dd..98ae26cc 100644 --- a/cypress/e2e/course_creation.cy.js +++ b/cypress/e2e/course_creation.cy.js @@ -33,15 +33,16 @@ describe("Course Creation", () => { cy.get("#lesson-title").type("Test Lesson"); // Content - cy.get(".ce-block").click().type("{enter}"); - cy.get(".ce-toolbar__plus").click(); - cy.get('[data-item-name="youtube"]').click(); + cy.get(".collapse-section.collapsed:first").click(); + cy.get("#lesson-content .ce-block").click().type("{enter}"); + cy.get("#lesson-content .ce-toolbar__plus").click(); + cy.get('#lesson-content [data-item-name="youtube"]').click(); cy.get('input[data-fieldname="youtube"]').type("GoDtyItReto"); cy.button("Insert").click(); cy.wait(1000); - cy.get(".ce-block:last").click().type("{enter}"); - cy.get(".ce-block:last") + cy.get("#lesson-content .ce-block:last").click().type("{enter}"); + cy.get("#lesson-content .ce-block:last") .click() .type( "This is an extremely big paragraph that is meant to test the UI. This is a very long paragraph. It contains more than once sentence. Its meant to be this long as this is a UI test. Its unbearably long and I'm not sure why I'm typing this much. I'm just going to keep typing until I feel like its long enough. I think its long enough now. I'm going to stop typing now." From cc7832614b954c9c9f6450719f45ffc7dbc401d9 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Thu, 28 Sep 2023 10:14:10 +0530 Subject: [PATCH 3/4] fix: hide course header for students if no courses in batch --- lms/www/batches/batch_details.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lms/www/batches/batch_details.html b/lms/www/batches/batch_details.html index 8d1d9ee9..d8eeeb1a 100644 --- a/lms/www/batches/batch_details.html +++ b/lms/www/batches/batch_details.html @@ -171,6 +171,7 @@ {% macro CourseList(courses) %} +{% if courses | length or is_moderator %}
@@ -214,6 +215,7 @@
{% endif %}
+{% endif %} {% endmacro %} {%- block script %} From bf3c6bc6be1acd510000ed0ceddd3a670b352bdf Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Thu, 28 Sep 2023 10:39:30 +0530 Subject: [PATCH 4/4] test: change lesson sequence --- cypress/e2e/course_creation.cy.js | 13 +++++-------- lms/www/batch/learn.html | 2 +- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/cypress/e2e/course_creation.cy.js b/cypress/e2e/course_creation.cy.js index 98ae26cc..2be9a71c 100644 --- a/cypress/e2e/course_creation.cy.js +++ b/cypress/e2e/course_creation.cy.js @@ -34,19 +34,16 @@ describe("Course Creation", () => { // Content cy.get(".collapse-section.collapsed:first").click(); - cy.get("#lesson-content .ce-block").click().type("{enter}"); + cy.get("#lesson-content .ce-block") + .click() + .type( + "This is an extremely big paragraph that is meant to test the UI. This is a very long paragraph. It contains more than once sentence. Its meant to be this long as this is a UI test. Its unbearably long and I'm not sure why I'm typing this much. I'm just going to keep typing until I feel like its long enough. I think its long enough now. I'm going to stop typing now. {enter}" + ); cy.get("#lesson-content .ce-toolbar__plus").click(); cy.get('#lesson-content [data-item-name="youtube"]').click(); cy.get('input[data-fieldname="youtube"]').type("GoDtyItReto"); cy.button("Insert").click(); cy.wait(1000); - - cy.get("#lesson-content .ce-block:last").click().type("{enter}"); - cy.get("#lesson-content .ce-block:last") - .click() - .type( - "This is an extremely big paragraph that is meant to test the UI. This is a very long paragraph. It contains more than once sentence. Its meant to be this long as this is a UI test. Its unbearably long and I'm not sure why I'm typing this much. I'm just going to keep typing until I feel like its long enough. I think its long enough now. I'm going to stop typing now." - ); cy.button("Save").click(); // View Course diff --git a/lms/www/batch/learn.html b/lms/www/batch/learn.html index 7c34b3b6..60279848 100644 --- a/lms/www/batch/learn.html +++ b/lms/www/batch/learn.html @@ -155,7 +155,7 @@
{% endif %} - {% if lesson.instructor_notes and (is_moderator or instructor or is_evaluator) %} + {% if instructor_notes and (is_moderator or instructor or is_evaluator) %}