feat: editor js for instructor notes

This commit is contained in:
Jannat Patel
2023-09-27 17:59:36 +05:30
parent 5614a6203f
commit db71f1271b
7 changed files with 174 additions and 139 deletions

View File

@@ -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",

View File

@@ -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

View File

@@ -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);
}

View File

@@ -75,41 +75,54 @@
</div>
<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 %}>
<span>{{ _("Show preview of this lesson to Guest users.") }}</span>
</label>
</div>
<div class="field-group">
<div class="field-label">
{{ _("Instructor Notes") }}
<div class="collapse-section collapsed" data-toggle="collapse" data-target="#lesson-content-section">
<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 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 id="instructor-notes"></div>
{% if lesson.instructor_notes %}
<div id="current-instructor-notes" class="hide">{{ lesson.instructor_notes }}</div>
{% if lesson.body %}
<div id="current-lesson-content" class="hide">{{ lesson.body }}</div>
{% endif %}
</div>
<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">
{{ _("Content") }}
{{ _("Instructor Notes") }}
</div>
<div class="field-description">
{{ _("Add your lesson content here") }}
<div class="field-description mb-2">
{{ _("These notes will only be visible to the Course Creator, Course Evaluaor and Moderator.") }}
</div>
</div>
<div id="lesson-content" class="lesson-editor"></div>
{% if lesson.body %}
<div id="current-lesson-content" class="hide">{{ lesson.body }}</div>
</div>
<div id="instructor-notes-section" class="collapse">
<div id="instructor-notes" class="lesson-editor"></div>
</div>
{% if lesson.instructor_notes %}
<div id="current-instructor-notes" class="hide">{{ lesson.instructor_notes }}</div>
{% endif %}
</div>
</article>
{% endmacro %}

View File

@@ -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: "<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,
},
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: "<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")) {
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(/&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) => {
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({

View File

@@ -156,12 +156,17 @@
{% 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 class="alert alert-secondary mb-4">
<div class="bold-heading collapse-section collapsed" data-toggle="collapse" data-target="#instructor-notes">
<svg class="icon icon-md pull-right">
<use href="#icon-up-line"></use>
</svg>
<div>
{{ _("Instructor Notes") }}
</div>
</div>
<div>
{{ lesson.instructor_notes_html }}
<div class="collapse" id="instructor-notes">
{{ instructor_notes }}
</div>
</div>
{% endif %}
@@ -227,36 +232,6 @@
</div>
{% 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 -->
{% macro Discussions() %}
{% set topics_count = frappe.db.count("Discussion Topic", {

View File

@@ -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)