Merge pull request #626 from pateljannat/ins-notes-changes

feat: editor js for instructor notes
This commit is contained in:
Jannat Patel
2023-09-28 11:32:00 +05:30
committed by GitHub
9 changed files with 185 additions and 150 deletions

View File

@@ -33,19 +33,17 @@ describe("Course Creation", () => {
cy.get("#lesson-title").type("Test Lesson"); cy.get("#lesson-title").type("Test Lesson");
// Content // Content
cy.get(".ce-block").click().type("{enter}"); cy.get(".collapse-section.collapsed:first").click();
cy.get(".ce-toolbar__plus").click(); cy.get("#lesson-content .ce-block")
cy.get('[data-item-name="youtube"]').click(); .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.get('input[data-fieldname="youtube"]').type("GoDtyItReto");
cy.button("Insert").click(); cy.button("Insert").click();
cy.wait(1000); cy.wait(1000);
cy.get(".ce-block:last").click().type("{enter}");
cy.get(".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(); cy.button("Save").click();
// View Course // View Course

View File

@@ -135,13 +135,13 @@
}, },
{ {
"fieldname": "instructor_notes", "fieldname": "instructor_notes",
"fieldtype": "Text", "fieldtype": "Markdown Editor",
"label": "Instructor Notes" "label": "Instructor Notes"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2023-08-31 21:47:06.314995", "modified": "2023-09-27 15:45:54.738573",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "Course Lesson", "name": "Course Lesson",

View File

@@ -152,11 +152,6 @@ def get_lesson_details(chapter):
) )
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_html = markdown_to_html(
lesson_details.instructor_notes
)
lessons.append(lesson_details) lessons.append(lesson_details)
return lessons return lessons

View File

@@ -160,6 +160,10 @@ textarea.field-input {
position: unset; position: unset;
} }
.codex-editor--narrow .ce-toolbar__actions {
right: 100%;
}
.lesson-editor { .lesson-editor {
border: 1px solid var(--gray-300); border: 1px solid var(--gray-300);
border-radius: var(--border-radius-md); border-radius: var(--border-radius-md);
@@ -2453,4 +2457,16 @@ select {
.batch-details { .batch-details {
width: 100%; 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);
} }

View File

@@ -75,41 +75,54 @@
</div> </div>
<div class="field-group"> <div class="field-group">
<label for="published" class="vertically-center"> <label for="preview" class="vertically-center">
<input type="checkbox" id="preview" {% if lesson.include_in_preview %} checked {% endif %}> <input type="checkbox" id="preview" {% if lesson.include_in_preview %} checked {% endif %}>
<span>{{ _("Show preview of this lesson to Guest users.") }}</span> <span>{{ _("Show preview of this lesson to Guest users.") }}</span>
</label> </label>
</div> </div>
<div class="field-group"> <div class="field-group">
<div class="field-label"> <div class="collapse-section collapsed" data-toggle="collapse" data-target="#lesson-content-section">
{{ _("Instructor Notes") }} <svg class="icon icon-sm pull-right">
<use href="#icon-up-line"></use>
</svg>
<div class="field-label">
{{ _("Content") }}
</div>
<div class="field-description mb-2">
{{ _("Add your lesson content here") }}
</div>
</div> </div>
<div class="field-description">
{{ _("These notes will only be visible to the Course Creator, Course Evaluaor and Moderator.") }} <div id="lesson-content-section" class="collapse">
<div id="lesson-content" class="lesson-editor"></div>
</div> </div>
<div id="instructor-notes"></div>
{% if lesson.instructor_notes %} {% if lesson.body %}
<div id="current-instructor-notes" class="hide">{{ lesson.instructor_notes }}</div> <div id="current-lesson-content" class="hide">{{ lesson.body }}</div>
{% endif %} {% endif %}
</div> </div>
<div class="field-group"> <div class="field-group">
<div> <div class="collapse-section collapsed" data-toggle="collapse" data-target="#instructor-notes-section">
<svg class="icon icon-sm pull-right">
<use href="#icon-up-line"></use>
</svg>
<div class="field-label"> <div class="field-label">
{{ _("Content") }} {{ _("Instructor Notes") }}
</div> </div>
<div class="field-description"> <div class="field-description mb-2">
{{ _("Add your lesson content here") }} {{ _("These notes will only be visible to the Course Creator, Course Evaluaor and Moderator.") }}
</div> </div>
</div> </div>
<div id="lesson-content" class="lesson-editor"></div> <div id="instructor-notes-section" class="collapse">
{% if lesson.body %} <div id="instructor-notes" class="lesson-editor"></div>
<div id="current-lesson-content" class="hide">{{ lesson.body }}</div> </div>
{% if lesson.instructor_notes %}
<div id="current-instructor-notes" class="hide">{{ lesson.instructor_notes }}</div>
{% endif %} {% endif %}
</div> </div>
</article> </article>
{% endmacro %} {% endmacro %}

View File

@@ -1,110 +1,129 @@
frappe.ready(() => { frappe.ready(() => {
let self = this; let self = this;
frappe.require("controls.bundle.js");
frappe.telemetry.capture("on_lesson_creation_page", "lms"); 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("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").click((e) => {
save_lesson(e); save_lesson(e);
}); });
}); });
const setup_editor = () => { const setup_editor_for_lesson_content = () => {
self.editor = new EditorJS({ self.editor = new EditorJS({
holder: "lesson-content", holder: "lesson-content",
tools: { tools: get_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"],
config: {
levels: [4, 5, 6],
defaultLevel: 5,
},
icon: `<svg class="icon icon-sm" style="">
<use class="" href="#icon-header"></use>
</svg>`,
},
paragraph: {
class: Paragraph,
inlineToolbar: true,
config: {
preserveBlank: true,
},
},
youtube: YouTubeVideo,
quiz: Quiz,
upload: Upload,
},
data: { data: {
blocks: self.blocks ? self.blocks : [], blocks: self.lesson_blocks || [],
}, },
}); });
}; };
const parse_string_to_lesson = () => { const setup_editor_for_instructor_notes = () => {
let lesson_content = $("#current-lesson-content").html(); self.instructor_notes_editor = new EditorJS({
let lesson_blocks = []; 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: "<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"],
config: {
levels: [4, 5, 6],
defaultLevel: 5,
},
icon: `<svg class="icon icon-sm" style="">
<use class="" href="#icon-header"></use>
</svg>`,
},
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")) { if (block.includes("{{ YouTubeVideo")) {
let youtube_id = block.match(/'([^']+)'/)[1]; let youtube_id = block.match(/\(["']([^"']+?)["']\)/)[1];
lesson_blocks.push({ blocks.push({
type: "youtube", type: "youtube",
data: { data: {
youtube: youtube_id, youtube: youtube_id,
}, },
}); });
} else if (block.includes("{{ Quiz")) { } else if (block.includes("{{ Quiz")) {
let quiz = block.match(/'([^']+)'/)[1]; let quiz = block.match(/\(["']([^"']+?)["']\)/)[1];
lesson_blocks.push({ blocks.push({
type: "quiz", type: "quiz",
data: { data: {
quiz: quiz, quiz: quiz,
}, },
}); });
} else if (block.includes("{{ Video")) { } else if (block.includes("{{ Video")) {
let video = block.match(/'([^']+)'/)[1]; let video = block.match(/\(["']([^"']+?)["']\)/)[1];
lesson_blocks.push({ blocks.push({
type: "upload", type: "upload",
data: { data: {
file_url: video, file_url: video,
}, },
}); });
} else if (block.includes("{{ Embed")) { } else if (block.includes("{{ Embed")) {
let embed = block.match(/'([^']+)'/)[1]; let embed = block.match(/\(["']([^"']+?)["']\)/)[1];
lesson_blocks.push({ blocks.push({
type: "embed", type: "embed",
data: { data: {
service: embed.split("|||")[0], service: embed.split("|||")[0],
@@ -113,7 +132,7 @@ const parse_string_to_lesson = () => {
}); });
} else if (block.includes("![]")) { } else if (block.includes("![]")) {
let image = block.match(/\((.*?)\)/)[1]; let image = block.match(/\((.*?)\)/)[1];
lesson_blocks.push({ blocks.push({
type: "upload", type: "upload",
data: { data: {
file_url: image, file_url: image,
@@ -121,7 +140,7 @@ const parse_string_to_lesson = () => {
}); });
} else if (block.includes("#")) { } else if (block.includes("#")) {
let level = (block.match(/#/g) || []).length; let level = (block.match(/#/g) || []).length;
lesson_blocks.push({ blocks.push({
type: "header", type: "header",
data: { data: {
text: block.replace(/#/g, "").trim(), text: block.replace(/#/g, "").trim(),
@@ -129,7 +148,7 @@ const parse_string_to_lesson = () => {
}, },
}); });
} else { } else {
lesson_blocks.push({ blocks.push({
type: "paragraph", type: "paragraph",
data: { data: {
text: block, 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) => { const save_lesson = (e) => {
self.editor.save().then((outputData) => { 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 = ""; let lesson_content = "";
data.blocks.forEach((block) => { data.blocks.forEach((block) => {
if (block.type == "youtube") { if (block.type == "youtube") {
@@ -175,24 +203,28 @@ const parse_lesson_to_string = (data) => {
}|||${block.data.embed.replace(/&amp;/g, "&")}") }}\n`; }|||${block.data.embed.replace(/&amp;/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) => { const save = () => {
validate_mandatory(lesson_content); console.log(this.instructor_notes_data);
console.log(this.lesson_content_data);
validate_mandatory(this.lesson_content_data);
let lesson = $("#lesson-title").data("lesson"); let lesson = $("#lesson-title").data("lesson");
frappe.call({ frappe.call({
method: "lms.lms.doctype.lms_course.lms_course.save_lesson", method: "lms.lms.doctype.lms_course.lms_course.save_lesson",
args: { args: {
title: $("#lesson-title").val(), title: $("#lesson-title").val(),
body: lesson_content, body: this.lesson_content_data,
chapter: $("#lesson-title").data("chapter"), chapter: $("#lesson-title").data("chapter"),
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: instructor_notes: this.instructor_notes_data,
this.instructor_notes.get_values().instructor_notes,
}, },
callback: (data) => { callback: (data) => {
frappe.show_alert({ frappe.show_alert({

View File

@@ -155,13 +155,18 @@
</div> </div>
{% endif %} {% endif %}
{% if lesson.instructor_notes and (is_moderator or instructor or is_evaluator) %} {% if instructor_notes and (is_moderator or instructor or is_evaluator) %}
<div class="alert alert-info mb-4"> <div class="alert alert-secondary mb-4">
<div class="bold-heading mb-2"> <div class="bold-heading collapse-section collapsed" data-toggle="collapse" data-target="#instructor-notes">
{{ _("Instructor Notes") }} <svg class="icon icon-md pull-right">
<use href="#icon-up-line"></use>
</svg>
<div>
{{ _("Instructor Notes") }}
</div>
</div> </div>
<div> <div class="collapse" id="instructor-notes">
{{ lesson.instructor_notes_html }} {{ instructor_notes }}
</div> </div>
</div> </div>
{% endif %} {% endif %}
@@ -227,36 +232,6 @@
</div> </div>
{% endmacro %} {% endmacro %}
<!-- Help Article -->
{% macro HelpArticle() %}
<div class="medium">
<h3> {{ _("Embed Components") }} </h3>
<p>
{{ _("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.") }}
</p>
<ol>
<li>
<b> {{ _("YouTube Video") }} </b>
<p> To get the YouTube Video ID, follow the steps mentioned below. </p>
<ul class="px-4">
<li>
{{ _("Upload the video on youtube.") }}
</li>
<li>
{{ _("When you share a youtube video, it shows a URL") }}
</li>
<li>
{{ _("Copy the last parameter of the URL and paste it here.") }}
</li>
</ul>
</li>
</ol>
</div>
{% endmacro %}
<!-- Discussions Component --> <!-- Discussions Component -->
{% macro Discussions() %} {% macro Discussions() %}
{% set topics_count = frappe.db.count("Discussion Topic", { {% set topics_count = frappe.db.count("Discussion Topic", {

View File

@@ -1,6 +1,7 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import cstr, flt from frappe.utils import cstr, flt
from lms.lms.md import markdown_to_html
from lms.lms.utils import ( from lms.lms.utils import (
get_lesson_url, get_lesson_url,
@@ -46,6 +47,9 @@ def get_context(context):
context.is_moderator = has_course_moderator_role() context.is_moderator = has_course_moderator_role()
context.is_evaluator = has_course_evaluator_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.show_lesson = (
context.membership context.membership
or (context.lesson and context.lesson.include_in_preview) or (context.lesson and context.lesson.include_in_preview)

View File

@@ -172,6 +172,7 @@
{% macro CourseList(courses) %} {% macro CourseList(courses) %}
{% if courses | length or is_moderator %}
<div class="batch-course-list"> <div class="batch-course-list">
<div class="flex align-center"> <div class="flex align-center">
@@ -215,6 +216,7 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% endif %}
{% endmacro %} {% endmacro %}