feat: edit existing lesson
This commit is contained in:
@@ -121,8 +121,7 @@ textarea.field-input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.outline-lesson {
|
.outline-lesson {
|
||||||
border-bottom: 1px solid var(--gray-300);
|
padding: 0.5rem 0;
|
||||||
padding: 1rem 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.common-card-style .outline-lesson:last-of-type {
|
.common-card-style .outline-lesson:last-of-type {
|
||||||
@@ -160,6 +159,12 @@ textarea.field-input {
|
|||||||
padding-top: 0.5rem;
|
padding-top: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lesson-parent .breadcrumb {
|
||||||
|
border-bottom: 1px solid var(--gray-300);
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: #FFFFFF;
|
background-color: #FFFFFF;
|
||||||
}
|
}
|
||||||
@@ -764,15 +769,12 @@ input[type=checkbox] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.course-content-parent .course-home-headings {
|
.course-content-parent .course-home-headings {
|
||||||
margin: 0 0 1rem;
|
margin: 0 0 0.5rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lesson-pagination {
|
.lesson-pagination {
|
||||||
display: flex;
|
margin: 2rem 0;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-top: 2rem
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.lesson-video {
|
.lesson-video {
|
||||||
@@ -937,7 +939,7 @@ input[type=checkbox] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar {
|
.progress-bar {
|
||||||
background-color: var(--accent-color);
|
background-color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-percent {
|
.progress-percent {
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="">
|
<div class="">
|
||||||
<input id="lesson-title" type="text" class="field-input" {% if lesson.name %} data-course="{{ lesson.name }}" value="{{ lesson.title }}" {% endif %}>
|
<input id="lesson-title" type="text" class="field-input" data-index="{{ lesson_index }}" data-chapter="{{ chapter }}" data-course="{{ lesson.name }}" {% if lesson.name %} data-lesson="{{ lesson.name }}" value="{{ lesson.title }}" {% endif %}>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -60,6 +60,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="lesson-content" class="lesson-editor"></div>
|
<div id="lesson-content" class="lesson-editor"></div>
|
||||||
|
{% if lesson.body %}
|
||||||
|
<div id="current-lesson-content" class="hide">{{ lesson.body }}</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@@ -86,5 +89,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{{ include_script('controls.bundle.js') }}
|
{{ include_script('controls.bundle.js') }}
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@latest"></script>
|
<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>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
frappe.ready(() => {
|
frappe.ready(() => {
|
||||||
let self = this;
|
let self = this;
|
||||||
|
if ($("#current-lesson-content").length) {
|
||||||
|
parse_string_to_lesson();
|
||||||
|
}
|
||||||
|
|
||||||
setup_editor();
|
setup_editor();
|
||||||
fetch_quiz_list();
|
fetch_quiz_list();
|
||||||
|
|
||||||
@@ -8,19 +12,102 @@ frappe.ready(() => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const parse_string_to_lesson = () => {
|
||||||
|
let lesson_content = $("#current-lesson-content").html();
|
||||||
|
let lesson_blocks = [];
|
||||||
|
|
||||||
|
lesson_content.split("\n").forEach((block) => {
|
||||||
|
if (block.includes("{{ YouTubeVideo")) {
|
||||||
|
let youtube_id = block.match(/'([^']+)'/)[1];
|
||||||
|
lesson_blocks.push({
|
||||||
|
type: "youtube",
|
||||||
|
data: {
|
||||||
|
youtube: youtube_id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (block.includes("{{ Quiz")) {
|
||||||
|
let quiz = block.match(/'([^']+)'/)[1];
|
||||||
|
lesson_blocks.push({
|
||||||
|
type: "quiz",
|
||||||
|
data: {
|
||||||
|
quiz: quiz,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
lesson_blocks.push({
|
||||||
|
type: "paragraph",
|
||||||
|
data: {
|
||||||
|
text: block,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.blocks = lesson_blocks;
|
||||||
|
};
|
||||||
|
|
||||||
const setup_editor = () => {
|
const setup_editor = () => {
|
||||||
self.editor = new EditorJS({
|
self.editor = new EditorJS({
|
||||||
holder: "lesson-content",
|
holder: "lesson-content",
|
||||||
tools: {
|
tools: {
|
||||||
youtube: YouTubeVideo,
|
youtube: YouTubeVideo,
|
||||||
quiz: Quiz,
|
quiz: Quiz,
|
||||||
|
paragraph: {
|
||||||
|
class: Paragraph,
|
||||||
|
inlineToolbar: true,
|
||||||
|
config: {
|
||||||
|
preserveBlank: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
blocks: self.blocks ? self.blocks : [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const save_lesson = (e) => {
|
const save_lesson = (e) => {
|
||||||
self.editor.save().then((outputData) => {
|
self.editor.save().then((outputData) => {
|
||||||
parse_lesson(outputData);
|
parse_lesson_to_string(outputData);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const parse_lesson_to_string = (data) => {
|
||||||
|
let lesson_content = "";
|
||||||
|
data.blocks.forEach((block) => {
|
||||||
|
if (block.type == "youtube") {
|
||||||
|
lesson_content += `{{ YouTubeVideo("${block.data.youtube}") }}\n`;
|
||||||
|
} else if (block.type == "quiz") {
|
||||||
|
lesson_content += `{{ Quiz("${block.data.quiz}") }}\n`;
|
||||||
|
} else if (block.type == "paragraph") {
|
||||||
|
lesson_content += `${block.data.text}\n`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
save(lesson_content);
|
||||||
|
};
|
||||||
|
|
||||||
|
const save = (lesson_content) => {
|
||||||
|
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,
|
||||||
|
chapter: $("#lesson-title").data("chapter"),
|
||||||
|
preview: 0,
|
||||||
|
idx: $("#lesson-title").data("index"),
|
||||||
|
lesson: lesson ? lesson : "",
|
||||||
|
},
|
||||||
|
callback: (data) => {
|
||||||
|
frappe.show_alert({
|
||||||
|
message: __("Saved"),
|
||||||
|
indicator: "green",
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = window.location.href.split("?")[0];
|
||||||
|
}, 1000);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -33,9 +120,11 @@ const fetch_quiz_list = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const parse_lesson = (data) => {};
|
|
||||||
|
|
||||||
class YouTubeVideo {
|
class YouTubeVideo {
|
||||||
|
constructor({ data }) {
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
static get toolbox() {
|
static get toolbox() {
|
||||||
return {
|
return {
|
||||||
title: "YouTube Video",
|
title: "YouTube Video",
|
||||||
@@ -43,8 +132,17 @@ class YouTubeVideo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let self = this;
|
|
||||||
this.wrapper = document.createElement("div");
|
this.wrapper = document.createElement("div");
|
||||||
|
if (this.data && this.data.youtube) {
|
||||||
|
$(this.wrapper).html(this.render_youtube(this.data.youtube));
|
||||||
|
} else {
|
||||||
|
this.render_youtube_dialog();
|
||||||
|
}
|
||||||
|
return this.wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
render_youtube_dialog() {
|
||||||
|
let self = this;
|
||||||
let youtubedialog = new frappe.ui.Dialog({
|
let youtubedialog = new frappe.ui.Dialog({
|
||||||
title: __("YouTube Video"),
|
title: __("YouTube Video"),
|
||||||
fields: [
|
fields: [
|
||||||
@@ -59,23 +157,26 @@ class YouTubeVideo {
|
|||||||
primary_action(values) {
|
primary_action(values) {
|
||||||
youtubedialog.hide();
|
youtubedialog.hide();
|
||||||
self.youtube = values.youtube;
|
self.youtube = values.youtube;
|
||||||
$(self.wrapper).html(` <iframe width="100%" height="400"
|
$(self.wrapper).html(self.render_youtube(values.youtube));
|
||||||
src="https://www.youtube.com/embed/${self.youtube}"
|
|
||||||
title="YouTube video player"
|
|
||||||
frameborder="0"
|
|
||||||
style="border-radius: var(--border-radius-lg); margin: 1rem 0;"
|
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
||||||
allowfullscreen>
|
|
||||||
</iframe>`);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
youtubedialog.show();
|
youtubedialog.show();
|
||||||
return this.wrapper;
|
}
|
||||||
|
|
||||||
|
render_youtube(youtube) {
|
||||||
|
return `<iframe width="100%" height="400"
|
||||||
|
src="https://www.youtube.com/embed/${youtube}"
|
||||||
|
title="YouTube video player"
|
||||||
|
frameborder="0"
|
||||||
|
style="border-radius: var(--border-radius-lg); margin: 1rem 0;"
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||||
|
allowfullscreen>
|
||||||
|
</iframe>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
save(block_content) {
|
save(block_content) {
|
||||||
return {
|
return {
|
||||||
youtube: this.youtube,
|
youtube: this.data.youtube || this.youtube,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,8 +188,21 @@ class Quiz {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor({ data }) {
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
this.wrapper = document.createElement("div");
|
this.wrapper = document.createElement("div");
|
||||||
|
if (this.data && this.data.quiz) {
|
||||||
|
$(this.wrapper).html(this.render_quiz(this.data.quiz));
|
||||||
|
} else {
|
||||||
|
this.render_quiz_dialog();
|
||||||
|
}
|
||||||
|
return this.wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
render_quiz_dialog() {
|
||||||
let self = this;
|
let self = this;
|
||||||
let quizdialog = new frappe.ui.Dialog({
|
let quizdialog = new frappe.ui.Dialog({
|
||||||
title: __("Select a Quiz"),
|
title: __("Select a Quiz"),
|
||||||
@@ -105,11 +219,7 @@ class Quiz {
|
|||||||
primary_action(values) {
|
primary_action(values) {
|
||||||
self.quiz = values.quiz;
|
self.quiz = values.quiz;
|
||||||
quizdialog.hide();
|
quizdialog.hide();
|
||||||
$(self.wrapper).html(
|
$(self.wrapper).html(self.render_quiz(self.quiz));
|
||||||
`<div class="common-card-style p-2 my-2 bold-heading">
|
|
||||||
Quiz: ${self.quiz}
|
|
||||||
</div>`
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
quizdialog.show();
|
quizdialog.show();
|
||||||
@@ -117,12 +227,17 @@ class Quiz {
|
|||||||
$(".modal-body").css("min-height", "300px");
|
$(".modal-body").css("min-height", "300px");
|
||||||
$(".modal-body input").focus();
|
$(".modal-body input").focus();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
return this.wrapper;
|
}
|
||||||
|
|
||||||
|
render_quiz(quiz) {
|
||||||
|
return `<div class="common-card-style p-2 my-2 bold-heading">
|
||||||
|
Quiz: ${quiz}
|
||||||
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
save(block_content) {
|
save(block_content) {
|
||||||
return {
|
return {
|
||||||
quiz: this.quiz,
|
quiz: this.data.quiz || this.quiz,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,7 +251,6 @@ class Video {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
this.wrapper = document.createElement("div");
|
this.wrapper = document.createElement("div");
|
||||||
let self = this;
|
|
||||||
new frappe.ui.FileUploader({
|
new frappe.ui.FileUploader({
|
||||||
disable_file_browser: true,
|
disable_file_browser: true,
|
||||||
folder: "Home/Attachments",
|
folder: "Home/Attachments",
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ def get_context(context):
|
|||||||
chapter_index = frappe.form_dict.get("chapter")
|
chapter_index = frappe.form_dict.get("chapter")
|
||||||
lesson_index = frappe.form_dict.get("lesson")
|
lesson_index = frappe.form_dict.get("lesson")
|
||||||
lesson_number = f"{chapter_index}.{lesson_index}"
|
lesson_number = f"{chapter_index}.{lesson_index}"
|
||||||
|
context.lesson_index = lesson_index
|
||||||
|
context.chapter = frappe.db.get_value(
|
||||||
|
"Chapter Reference", {"idx": chapter_index, "parent": context.course.name}, "chapter"
|
||||||
|
)
|
||||||
context.lesson = get_current_lesson_details(lesson_number, context, True)
|
context.lesson = get_current_lesson_details(lesson_number, context, True)
|
||||||
context.is_moderator = has_course_moderator_role()
|
context.is_moderator = has_course_moderator_role()
|
||||||
instructor = is_instructor(context.course.name)
|
instructor = is_instructor(context.course.name)
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
{{ widgets.CourseOutline(course=course, membership=membership, lesson_page=True) }}
|
{{ widgets.CourseOutline(course=course, membership=membership, lesson_page=True) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="lesson-pagination-parent">
|
<div class="lesson-parent">
|
||||||
{{ BreadCrumb(course, lesson) }}
|
{{ BreadCrumb(course, lesson) }}
|
||||||
{{ LessonContent(lesson) }}
|
{{ LessonContent(lesson) }}
|
||||||
{% if course.status == "Approved" and not course.upcoming %}
|
{% if course.status == "Approved" and not course.upcoming %}
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="page-title title {% if membership %} is-member {% endif %}
|
<div class="course-home-headings title {% if membership %} is-member {% endif %}
|
||||||
{% if membership or is_instructor %} eligible-for-submission {% endif %}" id="title"
|
{% if membership or is_instructor %} eligible-for-submission {% endif %}" id="title"
|
||||||
data-index="{{ lesson_index }}" data-course="{{ course.name }}" data-chapter="{{ chapter }}"
|
data-index="{{ lesson_index }}" data-course="{{ course.name }}" data-chapter="{{ chapter }}"
|
||||||
{% if lesson.name %} data-lesson="{{ lesson.name }}" {% endif %}
|
{% if lesson.name %} data-lesson="{{ lesson.name }}" {% endif %}
|
||||||
@@ -155,19 +155,15 @@
|
|||||||
{% if prev_url or next_url %}
|
{% if prev_url or next_url %}
|
||||||
<div class="lesson-pagination">
|
<div class="lesson-pagination">
|
||||||
{% if prev_url %}
|
{% if prev_url %}
|
||||||
<div>
|
<a class="btn btn-secondary btn-sm prev" href="{{ prev_url }}">
|
||||||
<a class="btn btn-secondary btn-sm prev" href="{{ prev_url }}">
|
{{ _("Previous Lesson") }}
|
||||||
{{ _("Previous") }}
|
</a>
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if next_url %}
|
{% if next_url %}
|
||||||
<div>
|
<a class="btn btn-primary btn-sm next pull-right" href="{{ next_url }}">
|
||||||
<a class="btn btn-primary btn-sm next ml-2 " href="{{ next_url }}">
|
{{ _("Next Lesson") }}
|
||||||
{{ _("Next") }}
|
</a>
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ const save_course = (e) => {
|
|||||||
indicator: "green",
|
indicator: "green",
|
||||||
});
|
});
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.href = `/courses/${data.message}`;
|
window.location.href = `/courses/${data.message}/outline`;
|
||||||
}, 1000);
|
}, 1000);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -52,9 +52,12 @@
|
|||||||
{{ chapter.title }}
|
{{ chapter.title }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if chapter.description %}
|
||||||
<div class="mb-2 ml-5 chapter-description">
|
<div class="mb-2 ml-5 chapter-description">
|
||||||
{{ chapter.description }}
|
{{ chapter.description }}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% for lesson in lessons %}
|
{% for lesson in lessons %}
|
||||||
<div class="outline-lesson level">
|
<div class="outline-lesson level">
|
||||||
|
|||||||
Reference in New Issue
Block a user