Merge pull request #494 from pateljannat/move-lessons
feat: move chapters and lessons
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -8,3 +8,5 @@ lms/public/dist
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
|
node_modules
|
||||||
|
package-lock.json
|
||||||
@@ -306,3 +306,43 @@ def save_lesson(
|
|||||||
lesson_reference.save(ignore_permissions=True)
|
lesson_reference.save(ignore_permissions=True)
|
||||||
|
|
||||||
return doc.name
|
return doc.name
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def reorder_lesson(old_chapter, old_lesson_array, new_chapter, new_lesson_array):
|
||||||
|
if old_chapter == new_chapter:
|
||||||
|
sort_lessons(new_chapter, new_lesson_array)
|
||||||
|
else:
|
||||||
|
sort_lessons(old_chapter, old_lesson_array)
|
||||||
|
sort_lessons(new_chapter, new_lesson_array)
|
||||||
|
|
||||||
|
|
||||||
|
def sort_lessons(chapter, lesson_array):
|
||||||
|
lesson_array = json.loads(lesson_array)
|
||||||
|
for les in lesson_array:
|
||||||
|
ref = frappe.get_all("Lesson Reference", {"lesson": les}, ["name", "idx"])
|
||||||
|
if ref:
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Lesson Reference",
|
||||||
|
ref[0].name,
|
||||||
|
{
|
||||||
|
"parent": chapter,
|
||||||
|
"idx": lesson_array.index(les) + 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def reorder_chapter(chapter_array):
|
||||||
|
chapter_array = json.loads(chapter_array)
|
||||||
|
|
||||||
|
for chap in chapter_array:
|
||||||
|
ref = frappe.get_all("Chapter Reference", {"chapter": chap}, ["name", "idx"])
|
||||||
|
if ref:
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Chapter Reference",
|
||||||
|
ref[0].name,
|
||||||
|
{
|
||||||
|
"idx": chapter_array.index(chap) + 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if course.name and (course.edit_mode or chapters | length) %}
|
{% if course.name and (course.edit_mode or chapters | length) %}
|
||||||
<div class="course-home-headings" id="outline-heading">
|
<div class="course-home-headings" id="outline-heading" data-course="{{ course.name }}">
|
||||||
{{ _("Course Content") }}
|
{{ _("Course Content") }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -23,9 +23,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if chapters | length %}
|
{% if chapters | length %}
|
||||||
|
<div class="chapter-dropzone">
|
||||||
{% for chapter in chapters %}
|
{% for chapter in chapters %}
|
||||||
<div class="chapter-parent {% if course.edit_mode %} chapter-edit {% endif %} ">
|
<div class="chapter-parent {% if course.edit_mode %} chapter-edit {% endif %}" data-chapter="{{ chapter.name }}">
|
||||||
<div class="chapter-title" {% if not course.edit_mode %} data-toggle="collapse" aria-expanded="false"
|
<div class="chapter-title" {% if not course.edit_mode %} data-toggle="collapse" aria-expanded="false"
|
||||||
data-target="#{{ get_slugified_chapter_title(chapter.title) }}" {% endif %} >
|
data-target="#{{ get_slugified_chapter_title(chapter.title) }}" {% endif %} >
|
||||||
{% if not course.edit_mode %}
|
{% if not course.edit_mode %}
|
||||||
@@ -42,13 +42,13 @@
|
|||||||
{% if chapter.description or course.edit_mode %}
|
{% if chapter.description or course.edit_mode %}
|
||||||
<div {% if course.edit_mode %} contenteditable="true" {% endif %} class="chapter-description
|
<div {% if course.edit_mode %} contenteditable="true" {% endif %} class="chapter-description
|
||||||
{% if not course.edit_mode %} mx-8 mb-2 {% endif %} "
|
{% if not course.edit_mode %} mx-8 mb-2 {% endif %} "
|
||||||
data-placeholder="{{ _('Short Description') }}">{{ chapter.description }}</div>
|
data-placeholder="{{ _('Short Description') }}">{% if chapter.description %}{{ chapter.description }}{% endif %}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if course.edit_mode %}
|
{% if course.edit_mode %}
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<button class="btn btn-sm btn-secondary btn-save-chapter"
|
<button class="btn btn-sm btn-secondary btn-save-chapter"
|
||||||
data-index="{{ loop.index }}" data-chapter="{{ chapter.name }}"> {{ _('Save') }} </button>
|
data-index="{{ loop.index }}"> {{ _('Save') }} </button>
|
||||||
<a class="btn btn-sm btn-secondary btn-lesson ml-2"
|
<a class="btn btn-sm btn-secondary btn-lesson ml-2"
|
||||||
href="/courses/{{ course.name }}/learn/{{loop.index}}.{{ lessons | length + 1 }}?edit=1"> {{ _("New Lesson") }} </a>
|
href="/courses/{{ course.name }}/learn/{{loop.index}}.{{ lessons | length + 1 }}?edit=1"> {{ _("New Lesson") }} </a>
|
||||||
</div>
|
</div>
|
||||||
@@ -56,19 +56,22 @@
|
|||||||
|
|
||||||
{% set is_instructor = is_instructor(course.name) %}
|
{% set is_instructor = is_instructor(course.name) %}
|
||||||
|
|
||||||
{% if lessons | length %}
|
{% if course.edit_mode %}
|
||||||
<div class="lessons">
|
<div class="course-meta mt-8 font-weight-bold"> {{ _("Lessons") }}: </div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if course.edit_mode %}
|
<div class="lessons {% if course.edit_mode %} lesson-dropzone {% endif %}">
|
||||||
<b class="course-meta"> {{ _("Lessons") }}: </b>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
|
{% if lessons | length %}
|
||||||
|
|
||||||
{% for lesson in lessons %}
|
{% for lesson in lessons %}
|
||||||
{% set active = membership.current_lesson == lesson.name %}
|
{% set active = membership.current_lesson == lesson.name %}
|
||||||
<div class="lesson-info {% if active and not course.edit_mode %} active-lesson {% endif %}">
|
<div data-lesson="{{ lesson.name }}" class="lesson-info {% if active and not course.edit_mode %} active-lesson {% endif %}">
|
||||||
|
|
||||||
{% if membership or lesson.include_in_preview or is_instructor or has_course_moderator_role() %}
|
{% if membership or lesson.include_in_preview or is_instructor or has_course_moderator_role() %}
|
||||||
<a class="lesson-links" data-course="{{ course.name }}"
|
<a class="lesson-links"
|
||||||
{% if is_instructor and not lesson.include_in_preview %}
|
{% if is_instructor and not lesson.include_in_preview %}
|
||||||
title="{{ _('This lesson is not available for preview. As you are the Instructor of the course only you can see it.') }}"
|
title="{{ _('This lesson is not available for preview. As you are the Instructor of the course only you can see it.') }}"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -89,7 +92,7 @@
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="no-preview" title="This lesson is not available for preview" data-course="{{ course.name }}">
|
<div class="no-preview" title="This lesson is not available for preview">
|
||||||
<div class="lesson-links">
|
<div class="lesson-links">
|
||||||
<svg class="icon icon-sm mr-2">
|
<svg class="icon icon-sm mr-2">
|
||||||
<use class="" href="#icon-lock-gray">
|
<use class="" href="#icon-lock-gray">
|
||||||
@@ -102,12 +105,13 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@@ -119,87 +123,3 @@
|
|||||||
{{ widgets.NoPreviewModal(course=course, membership=membership) }}
|
{{ widgets.NoPreviewModal(course=course, membership=membership) }}
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<script>
|
|
||||||
|
|
||||||
frappe.ready(() => {
|
|
||||||
|
|
||||||
expand_the_active_chapter();
|
|
||||||
|
|
||||||
$(".chapter-title").unbind().click((e) => {
|
|
||||||
rotate_chapter_icon(e);
|
|
||||||
});
|
|
||||||
|
|
||||||
$(".no-preview").click((e) => {
|
|
||||||
show_no_preview_dialog(e);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const expand_the_first_chapter = () => {
|
|
||||||
let elements = $(".course-home-outline .collapse");
|
|
||||||
elements.each((i, element) => {
|
|
||||||
if (i < 1) {
|
|
||||||
show_section(element);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const expand_the_active_chapter = () => {
|
|
||||||
|
|
||||||
/* Find anchor matching the URL for course details page */
|
|
||||||
let selector = $(`a[href="${decodeURIComponent(window.location.pathname)}"]`).parent();
|
|
||||||
if (!selector.length) {
|
|
||||||
selector = $(`a[href^="${decodeURIComponent(window.location.pathname)}"]`).parent();
|
|
||||||
}
|
|
||||||
if (selector.length && $(".course-details-page").length) {
|
|
||||||
$(".lesson-info").removeClass("active-lesson");
|
|
||||||
$(".lesson-info").each((i, elem) => {
|
|
||||||
let href = $(elem).find("use").attr("href");
|
|
||||||
href.endsWith("blue") && $(elem).find("use").attr("href", href.substring(0, href.length - 5));
|
|
||||||
})
|
|
||||||
|
|
||||||
selector.addClass("active-lesson");
|
|
||||||
|
|
||||||
show_section(selector.parent().parent());
|
|
||||||
}
|
|
||||||
|
|
||||||
/* For course home page */
|
|
||||||
else if ($(".active-lesson").length) {
|
|
||||||
selector = $(".active-lesson")
|
|
||||||
show_section(selector.parent().parent());
|
|
||||||
}
|
|
||||||
|
|
||||||
/* If no active chapter then exapand the first chapter */
|
|
||||||
else {
|
|
||||||
expand_the_first_chapter();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const show_section = (element) => {
|
|
||||||
$(element).addClass("show");
|
|
||||||
$(element).siblings(".chapter-title").children(".chapter-icon").css("transform", "rotate(90deg)");
|
|
||||||
$(element).siblings(".chapter-title").attr("aria-expanded", true);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const rotate_chapter_icon = (e) => {
|
|
||||||
let icon = $(e.currentTarget).children(".chapter-icon");
|
|
||||||
if (icon.css("transform") == "none") {
|
|
||||||
icon.css("transform", "rotate(90deg)");
|
|
||||||
} else {
|
|
||||||
icon.css("transform", "none");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const show_no_preview_dialog = (e) => {
|
|
||||||
$("#no-preview-modal").modal("show");
|
|
||||||
};
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|||||||
@@ -1593,7 +1593,6 @@ li {
|
|||||||
|
|
||||||
.chapter-edit .lessons {
|
.chapter-edit .lessons {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
margin-top: 2rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chapter-parent {
|
.chapter-parent {
|
||||||
|
|||||||
@@ -31,8 +31,54 @@ frappe.ready(() => {
|
|||||||
generate_graph("Lesson Completion", "#lesson-completion");
|
generate_graph("Lesson Completion", "#lesson-completion");
|
||||||
generate_course_completion_graph();
|
generate_course_completion_graph();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expand_the_active_chapter();
|
||||||
|
|
||||||
|
$(".chapter-title")
|
||||||
|
.unbind()
|
||||||
|
.click((e) => {
|
||||||
|
rotate_chapter_icon(e);
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".no-preview").click((e) => {
|
||||||
|
show_no_preview_dialog(e);
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".lesson-dropzone").each((i, el) => {
|
||||||
|
setSortable(el);
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".chapter-dropzone").each((i, el) => {
|
||||||
|
setSortable(el);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const setSortable = (el) => {
|
||||||
|
new Sortable(el, {
|
||||||
|
group: {
|
||||||
|
name: "les",
|
||||||
|
pull: "les",
|
||||||
|
put: "les",
|
||||||
|
},
|
||||||
|
onEnd: (e) => {
|
||||||
|
if ($(e.item).hasClass("lesson-info")) reorder_lesson(e);
|
||||||
|
else reorder_chapter(e);
|
||||||
|
},
|
||||||
|
onMove: (e) => {
|
||||||
|
if (
|
||||||
|
$(e.dragged).hasClass("lesson-info") &&
|
||||||
|
$(e.to).hasClass("chapter-dropzone")
|
||||||
|
)
|
||||||
|
return false;
|
||||||
|
if (
|
||||||
|
$(e.dragged).hasClass("chapter-edit") &&
|
||||||
|
$(e.to).hasClass("lesson-dropzone")
|
||||||
|
)
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const setup_file_size = () => {
|
const setup_file_size = () => {
|
||||||
frappe.provide("frappe.form.formatters");
|
frappe.provide("frappe.form.formatters");
|
||||||
frappe.form.formatters.FileSize = file_size;
|
frappe.form.formatters.FileSize = file_size;
|
||||||
@@ -49,7 +95,7 @@ const file_size = (value) => {
|
|||||||
|
|
||||||
const join_course = (e) => {
|
const join_course = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
let course = $(e.currentTarget).attr("data-course");
|
let course = $("#outline-heading").attr("data-course");
|
||||||
if (frappe.session.user == "Guest") {
|
if (frappe.session.user == "Guest") {
|
||||||
window.location.href = `/login?redirect-to=/courses/${course}`;
|
window.location.href = `/login?redirect-to=/courses/${course}`;
|
||||||
return;
|
return;
|
||||||
@@ -83,7 +129,7 @@ const join_course = (e) => {
|
|||||||
|
|
||||||
const notify_user = (e) => {
|
const notify_user = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
var course = decodeURIComponent($(e.currentTarget).attr("data-course"));
|
var course = decodeURIComponent($("#outline-heading").attr("data-course"));
|
||||||
if (frappe.session.user == "Guest") {
|
if (frappe.session.user == "Guest") {
|
||||||
window.location.href = `/login?redirect-to=/courses/${course}`;
|
window.location.href = `/login?redirect-to=/courses/${course}`;
|
||||||
return;
|
return;
|
||||||
@@ -120,7 +166,7 @@ const add_chapter = (e) => {
|
|||||||
|
|
||||||
let next_index = $("[data-index]").last().data("index") + 1 || 1;
|
let next_index = $("[data-index]").last().data("index") + 1 || 1;
|
||||||
let add_after = $(`.chapter-parent:last`).length
|
let add_after = $(`.chapter-parent:last`).length
|
||||||
? $(`.chapter-parent:last`)
|
? $(`.chapter-dropzone`)
|
||||||
: $("#outline-heading");
|
: $("#outline-heading");
|
||||||
|
|
||||||
$(`<div class="chapter-parent chapter-edit new-chapter">
|
$(`<div class="chapter-parent chapter-edit new-chapter">
|
||||||
@@ -157,7 +203,7 @@ const save_chapter = (e) => {
|
|||||||
title: parent.find(".chapter-title-main").text(),
|
title: parent.find(".chapter-title-main").text(),
|
||||||
chapter_description: parent.find(".chapter-description").text(),
|
chapter_description: parent.find(".chapter-description").text(),
|
||||||
idx: target.data("index"),
|
idx: target.data("index"),
|
||||||
chapter: target.data("chapter") ? target.data("chapter") : "",
|
chapter: parent.data("chapter") ? parent.data("chapter") : "",
|
||||||
},
|
},
|
||||||
callback: (data) => {
|
callback: (data) => {
|
||||||
frappe.show_alert({
|
frappe.show_alert({
|
||||||
@@ -226,3 +272,116 @@ const change_hash = (e) => {
|
|||||||
const open_tab = () => {
|
const open_tab = () => {
|
||||||
$(`a[href="${window.location.hash}"]`).click();
|
$(`a[href="${window.location.hash}"]`).click();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const expand_the_first_chapter = () => {
|
||||||
|
let elements = $(".course-home-outline .collapse");
|
||||||
|
elements.each((i, element) => {
|
||||||
|
if (i < 1) {
|
||||||
|
show_section(element);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const expand_the_active_chapter = () => {
|
||||||
|
/* Find anchor matching the URL for course details page */
|
||||||
|
let selector = $(
|
||||||
|
`a[href="${decodeURIComponent(window.location.pathname)}"]`
|
||||||
|
).parent();
|
||||||
|
|
||||||
|
if (!selector.length) {
|
||||||
|
selector = $(
|
||||||
|
`a[href^="${decodeURIComponent(window.location.pathname)}"]`
|
||||||
|
).parent();
|
||||||
|
}
|
||||||
|
if (selector.length && $(".course-details-page").length) {
|
||||||
|
expand_for_course_details(selector);
|
||||||
|
} else if ($(".active-lesson").length) {
|
||||||
|
/* For course home page */
|
||||||
|
selector = $(".active-lesson");
|
||||||
|
show_section(selector.parent().parent());
|
||||||
|
} else {
|
||||||
|
/* If no active chapter then exapand the first chapter */
|
||||||
|
expand_the_first_chapter();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const expand_for_course_details = (selector) => {
|
||||||
|
$(".lesson-info").removeClass("active-lesson");
|
||||||
|
$(".lesson-info").each((i, elem) => {
|
||||||
|
let href = $(elem).find("use").attr("href");
|
||||||
|
href.endsWith("blue") &&
|
||||||
|
$(elem)
|
||||||
|
.find("use")
|
||||||
|
.attr("href", href.substring(0, href.length - 5));
|
||||||
|
});
|
||||||
|
selector.addClass("active-lesson");
|
||||||
|
|
||||||
|
show_section(selector.parent().parent());
|
||||||
|
};
|
||||||
|
|
||||||
|
const show_section = (element) => {
|
||||||
|
$(element).addClass("show");
|
||||||
|
$(element)
|
||||||
|
.siblings(".chapter-title")
|
||||||
|
.children(".chapter-icon")
|
||||||
|
.css("transform", "rotate(90deg)");
|
||||||
|
$(element).siblings(".chapter-title").attr("aria-expanded", true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const rotate_chapter_icon = (e) => {
|
||||||
|
let icon = $(e.currentTarget).children(".chapter-icon");
|
||||||
|
if (icon.css("transform") == "none") {
|
||||||
|
icon.css("transform", "rotate(90deg)");
|
||||||
|
} else {
|
||||||
|
icon.css("transform", "none");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const show_no_preview_dialog = (e) => {
|
||||||
|
$("#no-preview-modal").modal("show");
|
||||||
|
};
|
||||||
|
|
||||||
|
const reorder_lesson = (e) => {
|
||||||
|
let old_chapter = $(e.from).closest(".chapter-edit").data("chapter");
|
||||||
|
let new_chapter = $(e.to).closest(".chapter-edit").data("chapter");
|
||||||
|
|
||||||
|
if (old_chapter == new_chapter && e.oldIndex == e.newIndex) return;
|
||||||
|
|
||||||
|
frappe.call({
|
||||||
|
method: "lms.lms.doctype.lms_course.lms_course.reorder_lesson",
|
||||||
|
args: {
|
||||||
|
old_chapter: old_chapter,
|
||||||
|
old_lesson_array: $(e.from)
|
||||||
|
.children()
|
||||||
|
.map((i, e) => $(e).data("lesson"))
|
||||||
|
.get(),
|
||||||
|
new_chapter: new_chapter,
|
||||||
|
new_lesson_array: $(e.to)
|
||||||
|
.children()
|
||||||
|
.map((i, e) => $(e).data("lesson"))
|
||||||
|
.get(),
|
||||||
|
},
|
||||||
|
callback: (data) => {
|
||||||
|
window.location.reload();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const reorder_chapter = (e) => {
|
||||||
|
if (e.oldIndex == e.newIndex) return;
|
||||||
|
|
||||||
|
frappe.call({
|
||||||
|
method: "lms.lms.doctype.lms_course.lms_course.reorder_chapter",
|
||||||
|
args: {
|
||||||
|
new_index: e.newIndex + 1,
|
||||||
|
chapter_array: $(e.to)
|
||||||
|
.children()
|
||||||
|
.map((i, e) => $(e).data("chapter"))
|
||||||
|
.get(),
|
||||||
|
},
|
||||||
|
callback: (data) => {
|
||||||
|
window.location.reload();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user