Merge pull request #357 from pateljannat/new-course-ui
This commit is contained in:
@@ -1,6 +1,16 @@
|
||||
{% extends "templates/base.html" %}
|
||||
{% from "www/macros/livecode.html" import LiveCodeEditorJS, LiveCodeEditor with context %}
|
||||
{% block title %} {{ lesson.title }} - {{ course.title }} {% endblock %}
|
||||
|
||||
{% block title %}
|
||||
|
||||
{% if lesson.title %}
|
||||
{{ lesson.title }} - {{ course.title }}
|
||||
{% else %}
|
||||
{{ _("New Lesson") }}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block head_include %}
|
||||
{% include "public/icons/symbol-defs.svg" %}
|
||||
@@ -12,137 +22,257 @@
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div class="common-page-style lesson-page">
|
||||
<div class="container course-details-page">
|
||||
{{ BreadCrumb(course, lesson) }}
|
||||
<div class="course-content-parent">
|
||||
<div class="course-details-outline">
|
||||
{{ widgets.CourseOutline(course=course, membership=membership) }}
|
||||
</div>
|
||||
<div class="lesson-pagination-parent">
|
||||
{{ LessonContent(lesson) }}
|
||||
{{ Discussions() }}
|
||||
</div>
|
||||
<div class="container course-details-page">
|
||||
{{ BreadCrumb(course, lesson) }}
|
||||
<div class="course-content-parent">
|
||||
<div class="course-details-outline">
|
||||
{{ widgets.CourseOutline(course=course, membership=membership) }}
|
||||
</div>
|
||||
<div class="lesson-pagination-parent">
|
||||
{{ LessonContent(lesson) }}
|
||||
{% if not lesson.edit_mode %} {{ Discussions() }} {% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
<!-- BreadCrumb -->
|
||||
{% macro BreadCrumb(course, lesson) %}
|
||||
<div class="breadcrumb">
|
||||
<a class="dark-links" href="/courses">{{ _("All Courses") }}</a>
|
||||
<img class="ml-1 mr-1" src="/assets/lms/icons/chevron-right.svg">
|
||||
<a class="dark-links" href="/courses/{{ course.name }}">{{ course.title }}</a>
|
||||
<img class="ml-1 mr-1" src="/assets/lms/icons/chevron-right.svg">
|
||||
<span class="breadcrumb-destination">{{ lesson.title }}</span>
|
||||
<span class="breadcrumb-destination">{{ lesson.title if lesson.title else _("New Lesson") }}</span>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Lesson Details -->
|
||||
{% macro LessonContent(lesson) %}
|
||||
{% set instructors = get_instructors(course.name) %}
|
||||
{% set is_instructor = is_instructor(course.name) %}
|
||||
<div class="common-card-style lesson-content">
|
||||
<div class="lesson-title">
|
||||
<div class="course-home-headings title mb-0
|
||||
{% if membership %} is-member {% endif %}
|
||||
{% if membership or is_instructor %} eligible-for-submission {% endif %}" data-lesson="{{ lesson.name }}"
|
||||
data-course="{{ course.name }}">{{ lesson.title }}</div>
|
||||
<span class="lesson-progress {{hide if get_progress(course.name, lesson.name) != 'Complete' else ''}}">{{ _("COMPLETED") }}</span>
|
||||
<div class="lesson-title">
|
||||
|
||||
{% if is_instructor %}
|
||||
<a class="button is-default button-links ml-auto" href="/lesson?name={{ lesson.name }}"> {{ _("Edit") }} </a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- Title -->
|
||||
<div class="course-home-headings title mb-0 {% if membership %} is-member {% endif %}
|
||||
{% if membership or is_instructor %} eligible-for-submission {% endif %}" id="title"
|
||||
{% if lesson.edit_mode %} data-placeholder="{{ _('Title') }}" contenteditable="true" {% endif %}
|
||||
data-index="{{ lesson_index }}" data-course="{{ course.name }}" data-chapter="{{ chapter }}"
|
||||
{% if lesson.name %} data-lesson="{{ lesson.name }}" {% endif %}
|
||||
>{% if lesson.title %}{{ lesson.title }}{% endif %}</div>
|
||||
<span class="lesson-progress {{ hide if get_progress(course.name, lesson.name) != 'Complete' else ''}}">{{ _("COMPLETED") }}</span>
|
||||
|
||||
<div class="d-flex align-items-center">
|
||||
{% set ins_len = instructors | length %}
|
||||
{% for instructor in instructors %}
|
||||
{% if ins_len > 1 and loop.index == 1 %}
|
||||
<div class="avatar-group overlap">
|
||||
{% endif %}
|
||||
{{ widgets.Avatar(member=instructor, avatar_class="avatar-small") }}
|
||||
|
||||
{% if ins_len > 1 and loop.index == ins_len %}
|
||||
<!-- Edit Button -->
|
||||
{% if is_instructor and not lesson.edit_mode %}
|
||||
<button class="button is-default button-links ml-auto btn-edit"> {{ _("Edit") }} </button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<a class="button-links ml-1" href="{{ get_profile_url(instructors[0].username) }}">
|
||||
<span class="course-meta">
|
||||
{% if ins_len == 1 %}
|
||||
{{ instructors[0].full_name }}
|
||||
{% else %}
|
||||
{% set suffix = _("other") if ins_len - 1 == 1 else _("others") %}
|
||||
{{ instructors[0].full_name.split(" ")[0] }} and {{ ins_len - 1 }} {{ suffix }}
|
||||
{% endif %}
|
||||
</span>
|
||||
</a>
|
||||
<div class="ml-5 course-meta"> {{ frappe.utils.format_date(lesson.creation, "medium") }} </div>
|
||||
</div>
|
||||
|
||||
<div class="markdown-source lesson-content-card">
|
||||
{% if membership or lesson.include_in_preview or is_instructor %}
|
||||
{% if is_instructor and not lesson.include_in_preview %}
|
||||
<div class="small alert alert-secondary alert-dismissible mt-4 mb-4">
|
||||
{{ _("This lesson is not available for preview. As you are the Instructor of the course only you can see it.") }}
|
||||
<a href="#" class="close" data-dismiss="alert" aria-label="close">×</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{{ render_html(lesson.body) }}
|
||||
{% else %}
|
||||
<div class="">
|
||||
<div class="button is-primary pull-right join-batch" data-course="{{ course.name | urlencode }}"> {{ _("Start Learning") }} </div>
|
||||
<div class=""> {{ _("This lesson is not available for preview. Please join the course to access it.") }} </div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- Instructors -->
|
||||
<div class="d-flex align-items-center">
|
||||
{% set ins_len = instructors | length %}
|
||||
{% for instructor in instructors %}
|
||||
{% if ins_len > 1 and loop.index == 1 %}
|
||||
<div class="avatar-group overlap">
|
||||
{% endif %}
|
||||
{{ widgets.Avatar(member=instructor, avatar_class="avatar-small") }}
|
||||
|
||||
{{ pagination(prev_url, next_url) }}
|
||||
{% if ins_len > 1 and loop.index == ins_len %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<a class="button-links ml-1" href="{{ get_profile_url(instructors[0].username) }}">
|
||||
<span class="course-meta">
|
||||
{% if ins_len == 1 %}
|
||||
{{ instructors[0].full_name }}
|
||||
{% else %}
|
||||
{% set suffix = _("other") if ins_len - 1 == 1 else _("others") %}
|
||||
{{ instructors[0].full_name.split(" ")[0] }} and {{ ins_len - 1 }} {{ suffix }}
|
||||
{% endif %}
|
||||
</span>
|
||||
</a>
|
||||
<div class="ml-5 course-meta"> {{ frappe.utils.format_date(lesson.creation, "medium") }} </div>
|
||||
</div>
|
||||
|
||||
<!-- Lesson Content -->
|
||||
<div class="markdown-source lesson-content-card {% if lesson.edit_mode %} mb-0 mt-2 {% endif %} ">
|
||||
{% if membership or lesson.include_in_preview or is_instructor %}
|
||||
|
||||
{% if is_instructor and not lesson.include_in_preview and not lesson.edit_mode %}
|
||||
<div class="small alert alert-secondary alert-dismissible mb-4">
|
||||
{{ _("This lesson is not available for preview. As you are the Instructor of the course only you can see it.") }}
|
||||
<a href="#" class="close" data-dismiss="alert" aria-label="close">×</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if lesson.edit_mode %}
|
||||
{{ EditLesson(lesson) }}
|
||||
{% else %}
|
||||
{{ render_html(lesson.body) }}
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<div class="">
|
||||
<div class="btn btn-primary pull-right join-batch" data-course="{{ course.name | urlencode }}"> {{ _("Start Learning") }} </div>
|
||||
<div class=""> {{ _("This lesson is not available for preview. Please join the course to access it.") }} </div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if not lesson.edit_mode %}
|
||||
{{ pagination(prev_url, next_url) }}
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Pagination -->
|
||||
{% macro pagination(prev_url, next_url) %}
|
||||
<div class="lesson-pagination">
|
||||
<div>
|
||||
{% if prev_url %}
|
||||
<a class="btn btn-secondary dark-links prev" href="{{ prev_url }}">
|
||||
<img class="mr-2" src="/assets/lms/icons/left-arrow.svg">
|
||||
{{ _("Prev") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{% if prev_url %}
|
||||
<a class="button is-secondary dark-links prev" href="{{ prev_url }}">
|
||||
<img class="mr-2" src="/assets/lms/icons/left-arrow.svg">
|
||||
{{ _("Prev") }}
|
||||
</a>
|
||||
{% if not is_mentor(course.name, frappe.session.user) and membership %}
|
||||
{% set progress = get_progress(course.name, lesson.name) %}
|
||||
<div class="custom-checkbox {% if progress == 'Complete' %} hide {% endif %}">
|
||||
<label class="quiz-label">
|
||||
<input class="mark-progress" type="checkbox" checked>
|
||||
<img class="empty-checkbox" />
|
||||
<span class="small">{{ _("Mark as complete on moving to the next lesson") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="btn btn-default mark-progress {{ progress }} {% if progress == 'Incomplete' or progress == None %} hide {% endif %}"
|
||||
data-progress="Incomplete">
|
||||
{{ _("Mark as Incomplete") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
{% if not is_mentor(course.name, frappe.session.user) and membership %}
|
||||
{% set progress = get_progress(course.name, lesson.name) %}
|
||||
<div class="custom-checkbox {% if progress == 'Complete' %} hide {% endif %}">
|
||||
<label class="quiz-label">
|
||||
<input class="mark-progress" type="checkbox" checked>
|
||||
<img class="empty-checkbox" />
|
||||
<span class="small">{{ _("Mark as complete on moving to the next lesson") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="button is-secondary mark-progress {{ progress }} {% if progress == 'Incomplete' or progress == None %} hide {% endif %}"
|
||||
data-progress="Incomplete">
|
||||
{{ _("Mark as Incomplete") }}
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
<div>
|
||||
<a class="button is-primary next {% if membership.progress|int == 100 and not next_url %} hide {% endif %}"
|
||||
{% if next_url %} data-href="{{ next_url }}" {% endif %} href="">
|
||||
{% if next_url %} {{ _("Next") }} {% else %} {{ _("Mark as Complete") }} {% endif %}
|
||||
<img class="ml-2" src="/assets/lms/icons/side-arrow-white.svg">
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a class="btn btn-primary next {% if not next_url and (membership.progress|int == 100 or is_instructor) %} hide {% endif %}"
|
||||
{% if next_url %} data-href="{{ next_url }}" {% endif %} href="">
|
||||
{% if next_url %} {{ _("Next") }} {% else %} {{ _("Mark as Complete") }} {% endif %}
|
||||
<img class="ml-2" src="/assets/lms/icons/side-arrow-white.svg">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Edit Lesson -->
|
||||
{% macro EditLesson(lesson) %}
|
||||
<div id="body" {% if lesson.body %} data-body="{{ lesson.body }}" {% endif %}></div>
|
||||
|
||||
<label class="preview">
|
||||
<input {% if lesson.include_in_preview %} checked {% endif %} type="checkbox"
|
||||
id="preview"> {{ _("Show preview of this lesson to Guest users.") }}
|
||||
</label>
|
||||
|
||||
<div class="mt-4">
|
||||
<button class="btn btn-primary btn-sm btn-lesson pull-right ml-2"> {{ _("Save") }} </button>
|
||||
{% if lesson.name %}
|
||||
<button class="btn btn-secondary btn-sm pull-right btn-back ml-2"> {{ _("Back to Lesson") }} </button>
|
||||
<a class="btn btn-secondary btn-sm pull-right" href="/quizzes"> {{ _("Create Quiz") }} </a>
|
||||
{% endif %}
|
||||
|
||||
<div class="attachments-parent">
|
||||
<div class="attachment-controls">
|
||||
<div class="show-attachments" data-toggle="collapse" data-target="#collapse-attachments" aria-expanded="false">
|
||||
<svg class="icon icon-sm">
|
||||
<use class="" href="#icon-attachment">
|
||||
</svg>
|
||||
<span class="attachment-count" data-count="0">0 {{ _("attachments") }}</span>
|
||||
</div>
|
||||
<div class="add-attachment">
|
||||
<span class="btn btn-sm btn-secondary">
|
||||
<svg class="icon icon-sm">
|
||||
<use class="" href="#icon-upload">
|
||||
</svg>
|
||||
{{ _("Upload Attachments") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<table class="attachments common-card-style collapse hide" id="collapse-attachments"></table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ HelpArticle() }}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Help Article -->
|
||||
{% macro HelpArticle() %}
|
||||
<div class="help-article">
|
||||
<h3> {{ _("Help Article") }} </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>
|
||||
<table class="table w-100">
|
||||
<tr>
|
||||
<th style="width: 20%;"> {{ _("Content Type") }} </th>
|
||||
<th style="width: 40%;"> {{ _("Syntax") }} </th>
|
||||
<th> {{ _("Description") }} </th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
{{ _("YouTube Video") }}
|
||||
</td>
|
||||
<td>
|
||||
{% raw %} {{ YouTubeVideo("embed_src") }} {% endraw %}
|
||||
</td>
|
||||
<td>
|
||||
<span>
|
||||
{{ _("Copy and paste the syntax in the editor. Replace 'embed_src' with the embed source
|
||||
that YouTube provides. To get the source, follow the steps mentioned below.") }}
|
||||
</span>
|
||||
<ul class="p-4">
|
||||
<li>
|
||||
{{ _("Upload the video on youtube.") }}
|
||||
</li>
|
||||
<li>
|
||||
{{ _("When you share a youtube video, it shows an option called Embed.") }}
|
||||
</li>
|
||||
<li>
|
||||
{{ _("On clicking it, it provides an iframe. Copy the source (src) of the iframe and
|
||||
paste it here.") }}
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
{{ _("Quiz") }}
|
||||
</td>
|
||||
<td>
|
||||
{% raw %} {{ Quiz("lms_quiz_id") }} {% endraw %}
|
||||
</td>
|
||||
<td>
|
||||
{% set quiz_link = "<a href='/quizzes'> Quiz List </a>" %}
|
||||
{{ _("Copy and paste the syntax in the editor. Replace 'lms_quiz_id' with the ID of the Quiz. You can get the ID of the quiz from the {0}.").format(quiz_link) }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Discussions Component -->
|
||||
{% macro Discussions() %}
|
||||
{% set topics_count = frappe.db.count("Discussion Topic",
|
||||
{"reference_doctype": "Course Lesson", "reference_docname": lesson.name}) %}
|
||||
@@ -155,17 +285,17 @@
|
||||
{% set redirect_to = "/courses/" + course.name %}
|
||||
{% set empty_state_title = _("Have a doubt?") %}
|
||||
{% set empty_state_subtitle = _("Post it here, our mentors will help you out.") %}
|
||||
|
||||
{% include "frappe/templates/discussions/discussions_section.html" %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Scripts -->
|
||||
{%- block script %}
|
||||
{{ super() }}
|
||||
|
||||
<script type="text/javascript">
|
||||
var page_context = {{ page_context | tojson }};
|
||||
var page_context = {{ page_context | tojson }};
|
||||
</script>
|
||||
|
||||
{{ include_script('controls.bundle.js') }}
|
||||
{% for ext in page_extensions %}
|
||||
{{ ext.render_footer() }}
|
||||
{% endfor %}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
frappe.ready(() => {
|
||||
|
||||
localStorage.removeItem($("#quiz-title").text());
|
||||
localStorage.removeItem($("#quiz-title").data("name"));
|
||||
|
||||
fetch_assignments();
|
||||
|
||||
save_current_lesson();
|
||||
@@ -45,37 +46,65 @@ frappe.ready(() => {
|
||||
clear_work(e);
|
||||
});
|
||||
|
||||
$(".btn-lesson").click((e) => {
|
||||
save_lesson(e);
|
||||
});
|
||||
|
||||
$(".add-attachment").click((e) => {
|
||||
show_upload_modal();
|
||||
});
|
||||
|
||||
$(".btn-start-quiz").click((e) => {
|
||||
$("#start-banner").addClass("hide");
|
||||
$("#quiz-form").removeClass("hide");
|
||||
mark_active_question();
|
||||
});
|
||||
|
||||
$(".btn-edit").click((e) => {
|
||||
window.location.href = `${window.location.href}?edit=1`;
|
||||
});
|
||||
|
||||
$(".btn-back").click((e) => {
|
||||
window.location.href = window.location.href.split("?")[0];
|
||||
})
|
||||
|
||||
$(document).on("click", ".copy-link", (e) => {
|
||||
frappe.utils.copy_to_clipboard($(e.currentTarget).data("link"));
|
||||
$(".attachments").collapse("hide");
|
||||
});
|
||||
|
||||
if ($("#quiz-title").data("max-attempts")) {
|
||||
window.addEventListener("beforeunload", (e) => {
|
||||
e.returnValue = "";
|
||||
$(".active-question").length && quiz_summary();
|
||||
});
|
||||
}
|
||||
|
||||
if ($("#body").length) {
|
||||
make_editor();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const save_current_lesson = () => {
|
||||
if ($(".title").hasClass("is-member")) {
|
||||
frappe.call("lms.lms.api.save_current_lesson", {
|
||||
course_name: $(".title").attr("data-course"),
|
||||
lesson_name: $(".title").attr("data-lesson")
|
||||
})
|
||||
}
|
||||
if ($(".title").hasClass("is-member")) {
|
||||
frappe.call("lms.lms.api.save_current_lesson", {
|
||||
course_name: $(".title").attr("data-course"),
|
||||
lesson_name: $(".title").attr("data-lesson")
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const enable_check = (e) => {
|
||||
if ($(".option:checked").length) {
|
||||
$("#check").removeAttr("disabled");
|
||||
$(".custom-checkbox").removeClass("active-option");
|
||||
$(".option:checked").closest(".custom-checkbox").addClass("active-option");
|
||||
}
|
||||
if ($(".option:checked").length) {
|
||||
$("#check").removeAttr("disabled");
|
||||
$(".custom-checkbox").removeClass("active-option");
|
||||
$(".option:checked").closest(".custom-checkbox").addClass("active-option");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const mark_active_question = (e = undefined) => {
|
||||
$(".timer").addClass("hide");
|
||||
calculate_and_display_time(100);
|
||||
@@ -92,82 +121,88 @@ const mark_active_question = (e = undefined) => {
|
||||
initialize_timer();
|
||||
};
|
||||
|
||||
|
||||
const mark_progress = (e) => {
|
||||
/* Prevent default only for Next button anchor tag and not for progress checkbox */
|
||||
if ($(e.currentTarget).prop("nodeName") != "INPUT")
|
||||
e.preventDefault();
|
||||
else
|
||||
return;
|
||||
/* Prevent default only for Next button anchor tag and not for progress checkbox */
|
||||
if ($(e.currentTarget).prop("nodeName") != "INPUT")
|
||||
e.preventDefault();
|
||||
else
|
||||
return;
|
||||
|
||||
const target = $(e.currentTarget).attr("data-progress") ? $(e.currentTarget) : $("input.mark-progress");
|
||||
const current_status = $(".lesson-progress").hasClass("hide") ? "Incomplete": "Complete";
|
||||
const target = $(e.currentTarget).attr("data-progress") ? $(e.currentTarget) : $("input.mark-progress");
|
||||
const current_status = $(".lesson-progress").hasClass("hide") ? "Incomplete": "Complete";
|
||||
|
||||
let status = "Incomplete";
|
||||
if (target.prop("nodeName") == "INPUT" && target.prop("checked")) {
|
||||
status = "Complete";
|
||||
}
|
||||
let status = "Incomplete";
|
||||
if (target.prop("nodeName") == "INPUT" && target.prop("checked")) {
|
||||
status = "Complete";
|
||||
}
|
||||
|
||||
if (status != current_status) {
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.course_lesson.course_lesson.save_progress",
|
||||
args: {
|
||||
lesson: $(".title").attr("data-lesson"),
|
||||
course: $(".title").attr("data-course"),
|
||||
status: status
|
||||
},
|
||||
callback: (data) => {
|
||||
change_progress_indicators(status, e);
|
||||
show_certificate_if_course_completed(data);
|
||||
if (status != current_status) {
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.course_lesson.course_lesson.save_progress",
|
||||
args: {
|
||||
lesson: $(".title").attr("data-lesson"),
|
||||
course: $(".title").attr("data-course"),
|
||||
status: status
|
||||
},
|
||||
callback: (data) => {
|
||||
change_progress_indicators(status, e);
|
||||
show_certificate_if_course_completed(data);
|
||||
move_to_next_lesson(status, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
move_to_next_lesson(status, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
move_to_next_lesson(status, e);
|
||||
};
|
||||
|
||||
|
||||
const change_progress_indicators = (status, e) => {
|
||||
if (status == "Complete") {
|
||||
$(".lesson-progress").removeClass("hide");
|
||||
$(".active-lesson .lesson-progress-tick").removeClass("hide");
|
||||
}
|
||||
else {
|
||||
$(".lesson-progress").addClass("hide");
|
||||
$(".active-lesson .lesson-progress-tick").addClass("hide");
|
||||
}
|
||||
if (status == "Incomplete" && !$(e.currentTarget).hasClass("next")) {
|
||||
$(e.currentTarget).addClass("hide");
|
||||
$("input.mark-progress").prop("checked", false).closest(".custom-checkbox").removeClass("hide");
|
||||
}
|
||||
if (status == "Complete") {
|
||||
$(".lesson-progress").removeClass("hide");
|
||||
$(".active-lesson .lesson-progress-tick").removeClass("hide");
|
||||
}
|
||||
else {
|
||||
$(".lesson-progress").addClass("hide");
|
||||
$(".active-lesson .lesson-progress-tick").addClass("hide");
|
||||
}
|
||||
if (status == "Incomplete" && !$(e.currentTarget).hasClass("next")) {
|
||||
$(e.currentTarget).addClass("hide");
|
||||
$("input.mark-progress").prop("checked", false).closest(".custom-checkbox").removeClass("hide");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const show_certificate_if_course_completed = (data) => {
|
||||
if (data.message == 100 && !$(".next").attr("data-next") && $("#certification").hasClass("hide")) {
|
||||
$("#certification").removeClass("hide");
|
||||
$(".next").addClass("hide");
|
||||
}
|
||||
if (data.message == 100 && !$(".next").attr("data-next") && $("#certification").hasClass("hide")) {
|
||||
$("#certification").removeClass("hide");
|
||||
$(".next").addClass("hide");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const move_to_next_lesson = (status, e) => {
|
||||
if ($(e.currentTarget).hasClass("next") && $(e.currentTarget).attr("data-href")) {
|
||||
window.location.href = $(e.currentTarget).attr("data-href");
|
||||
}
|
||||
else if (status == "Complete") {
|
||||
$("input.mark-progress").closest(".custom-checkbox").addClass("hide");
|
||||
$("div.mark-progress").removeClass("hide");
|
||||
$(".next").addClass("hide");
|
||||
}
|
||||
else {
|
||||
$("input.mark-progress").closest(".custom-checkbox").removeClass("hide");
|
||||
$("div.mark-progress").addClass("hide");
|
||||
$(".next").removeClass("hide");
|
||||
}
|
||||
if ($(e.currentTarget).hasClass("next") && $(e.currentTarget).attr("data-href")) {
|
||||
window.location.href = $(e.currentTarget).attr("data-href");
|
||||
}
|
||||
else if (status == "Complete") {
|
||||
$("input.mark-progress").closest(".custom-checkbox").addClass("hide");
|
||||
$("div.mark-progress").removeClass("hide");
|
||||
$(".next").addClass("hide");
|
||||
}
|
||||
else {
|
||||
$("input.mark-progress").closest(".custom-checkbox").removeClass("hide");
|
||||
$("div.mark-progress").addClass("hide");
|
||||
$(".next").removeClass("hide");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const quiz_summary = (e=undefined) => {
|
||||
e && e.preventDefault();
|
||||
var quiz_name = $("#quiz-title").text();
|
||||
var total_questions = $(".question").length;
|
||||
let quiz_name = $("#quiz-title").data("name");
|
||||
let total_questions = $(".question").length;
|
||||
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_quiz.lms_quiz.quiz_summary",
|
||||
args: {
|
||||
@@ -175,7 +210,7 @@ const quiz_summary = (e=undefined) => {
|
||||
"results": localStorage.getItem(quiz_name)
|
||||
},
|
||||
callback: (data) => {
|
||||
var message = data.message == total_questions ? __("Excellent Work 👏") : __("Better luck next time")
|
||||
let message = data.message == total_questions ? __("Excellent Work 👏") : __("Better luck next time")
|
||||
$(".question").addClass("hide");
|
||||
$("#summary").addClass("hide");
|
||||
$("#quiz-form").parent().prepend(
|
||||
@@ -183,20 +218,21 @@ const quiz_summary = (e=undefined) => {
|
||||
<div class="font-weight-bold">${data.message}/${total_questions}</div></div>`);
|
||||
$("#try-again").removeClass("hide");
|
||||
}
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const try_quiz_again = (e) => {
|
||||
window.location.reload();
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
|
||||
const check_answer = (e=undefined) => {
|
||||
e && e.preventDefault();
|
||||
clearInterval(self.timer);
|
||||
$(".timer").addClass("hide");
|
||||
var quiz_name = $("#quiz-title").text();
|
||||
var total_questions = $(".question").length;
|
||||
var current_index = $(".active-question").attr("data-qt-index");
|
||||
let total_questions = $(".question").length;
|
||||
let current_index = $(".active-question").attr("data-qt-index");
|
||||
|
||||
$(".explanation").removeClass("hide");
|
||||
$("#check").addClass("hide");
|
||||
@@ -212,193 +248,208 @@ const check_answer = (e=undefined) => {
|
||||
else {
|
||||
$("#next").removeClass("hide");
|
||||
}
|
||||
var [answer, is_correct] = parse_options();
|
||||
add_to_local_storage(quiz_name, current_index, answer, is_correct);
|
||||
let [answer, is_correct] = parse_options();
|
||||
add_to_local_storage(current_index, answer, is_correct);
|
||||
};
|
||||
|
||||
|
||||
const parse_options = () => {
|
||||
var answer = [];
|
||||
var is_correct = [];
|
||||
$(".active-question input").each((i, element) => {
|
||||
var correct = parseInt($(element).attr("data-correct"));
|
||||
if ($(element).prop("checked")) {
|
||||
answer.push(decodeURIComponent($(element).val()));
|
||||
correct && is_correct.push(1);
|
||||
correct ? add_icon(element, "check") : add_icon(element, "wrong");
|
||||
}
|
||||
else {
|
||||
correct && is_correct.push(0);
|
||||
correct ? add_icon(element, "minus-circle-green") : add_icon(element, "minus-circle");
|
||||
}
|
||||
})
|
||||
return [answer, is_correct];
|
||||
let answer = [];
|
||||
let is_correct = [];
|
||||
$(".active-question input").each((i, element) => {
|
||||
let correct = parseInt($(element).attr("data-correct"));
|
||||
if ($(element).prop("checked")) {
|
||||
answer.push(decodeURIComponent($(element).val()));
|
||||
correct && is_correct.push(1);
|
||||
correct ? add_icon(element, "check") : add_icon(element, "wrong");
|
||||
}
|
||||
else {
|
||||
correct && is_correct.push(0);
|
||||
correct ? add_icon(element, "minus-circle-green") : add_icon(element, "minus-circle");
|
||||
}
|
||||
});
|
||||
return [answer, is_correct];
|
||||
};
|
||||
|
||||
|
||||
const add_icon = (element, icon) => {
|
||||
$(element).closest(".custom-checkbox").removeClass("active-option");
|
||||
var label = $(element).siblings(".option-text").text();
|
||||
$(element).siblings(".option-text").html(`
|
||||
<div>
|
||||
<img class="mr-3" src="/assets/lms/icons/${icon}.svg">
|
||||
${label}
|
||||
</div>
|
||||
`);
|
||||
//$(element).parent().empty().html(`<div class="option-text"><img class="mr-3" src="/assets/lms/icons/${icon}.svg"> ${label}</div>`);
|
||||
$(element).closest(".custom-checkbox").removeClass("active-option");
|
||||
let label = $(element).siblings(".option-text").text();
|
||||
$(element).siblings(".option-text").html(`
|
||||
<div>
|
||||
<img class="mr-3" src="/assets/lms/icons/${icon}.svg">
|
||||
${label}
|
||||
</div>
|
||||
`);
|
||||
//$(element).parent().empty().html(`<div class="option-text"><img class="mr-3" src="/assets/lms/icons/${icon}.svg"> ${label}</div>`);
|
||||
};
|
||||
|
||||
const add_to_local_storage = (quiz_name, current_index, answer, is_correct) => {
|
||||
var quiz_stored = JSON.parse(localStorage.getItem(quiz_name));
|
||||
var quiz_obj = {
|
||||
"question_index": current_index,
|
||||
"answer": answer.join(),
|
||||
"is_correct": is_correct
|
||||
}
|
||||
quiz_stored ? quiz_stored.push(quiz_obj) : quiz_stored = [quiz_obj]
|
||||
localStorage.setItem(quiz_name, JSON.stringify(quiz_stored))
|
||||
|
||||
const add_to_local_storage = (current_index, answer, is_correct) => {
|
||||
let quiz_name = $("#quiz-title").data("name")
|
||||
let quiz_stored = JSON.parse(localStorage.getItem(quiz_name));
|
||||
|
||||
let quiz_obj = {
|
||||
"question_index": current_index,
|
||||
"answer": answer.join(),
|
||||
"is_correct": is_correct
|
||||
}
|
||||
|
||||
quiz_stored ? quiz_stored.push(quiz_obj) : quiz_stored = [quiz_obj]
|
||||
localStorage.setItem(quiz_name, JSON.stringify(quiz_stored))
|
||||
};
|
||||
|
||||
|
||||
const create_certificate = (e) => {
|
||||
e.preventDefault();
|
||||
course = $(".title").attr("data-course");
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_certificate.lms_certificate.create_certificate",
|
||||
args: {
|
||||
"course": course
|
||||
},
|
||||
callback: (data) => {
|
||||
window.location.href = `/courses/${course}/${data.message.name}`;
|
||||
}
|
||||
})
|
||||
e.preventDefault();
|
||||
course = $(".title").attr("data-course");
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_certificate.lms_certificate.create_certificate",
|
||||
args: {
|
||||
"course": course
|
||||
},
|
||||
callback: (data) => {
|
||||
window.location.href = `/courses/${course}/${data.message.name}`;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const attach_work = (e) => {
|
||||
const target = $(e.currentTarget);
|
||||
let files = target.siblings(".attach-file").prop("files")
|
||||
if (files && files.length) {
|
||||
files = add_files(files)
|
||||
return_as_dataurl(files)
|
||||
files.map((file) => {
|
||||
upload_file(file, target);
|
||||
})
|
||||
}
|
||||
const target = $(e.currentTarget);
|
||||
let files = target.siblings(".attach-file").prop("files")
|
||||
if (files && files.length) {
|
||||
files = add_files(files)
|
||||
return_as_dataurl(files)
|
||||
files.map((file) => {
|
||||
upload_file(file, target);
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const upload_file = (file, target) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let xhr = new XMLHttpRequest();
|
||||
return new Promise((resolve, reject) => {
|
||||
let xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState == XMLHttpRequest.DONE) {
|
||||
if (xhr.status === 200) {
|
||||
let response = JSON.parse(xhr.responseText)
|
||||
create_lesson_work(response.message, target);
|
||||
} else if (xhr.status === 403) {
|
||||
let response = JSON.parse(xhr.responseText);
|
||||
frappe.msgprint(`Not permitted. ${response._error_message || ''}`);
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState == XMLHttpRequest.DONE) {
|
||||
if (xhr.status === 200) {
|
||||
let response = JSON.parse(xhr.responseText)
|
||||
create_lesson_work(response.message, target);
|
||||
} else if (xhr.status === 403) {
|
||||
let response = JSON.parse(xhr.responseText);
|
||||
frappe.msgprint(`Not permitted. ${response._error_message || ''}`);
|
||||
|
||||
} else if (xhr.status === 413) {
|
||||
frappe.msgprint('Size exceeds the maximum allowed file size.');
|
||||
} else if (xhr.status === 413) {
|
||||
frappe.msgprint('Size exceeds the maximum allowed file size.');
|
||||
|
||||
} else {
|
||||
frappe.msgprint(xhr.status === 0 ? 'XMLHttpRequest Error' : `${xhr.status} : ${xhr.statusText}`);
|
||||
} else {
|
||||
frappe.msgprint(xhr.status === 0 ? 'XMLHttpRequest Error' : `${xhr.status} : ${xhr.statusText}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
xhr.open('POST', '/api/method/upload_file', true);
|
||||
xhr.setRequestHeader('Accept', 'application/json');
|
||||
xhr.setRequestHeader('X-Frappe-CSRF-Token', frappe.csrf_token);
|
||||
xhr.open('POST', '/api/method/upload_file', true);
|
||||
xhr.setRequestHeader('Accept', 'application/json');
|
||||
xhr.setRequestHeader('X-Frappe-CSRF-Token', frappe.csrf_token);
|
||||
|
||||
let form_data = new FormData();
|
||||
if (file.file_obj) {
|
||||
form_data.append('file', file.file_obj, `${frappe.session.user}-${file.name}`);
|
||||
form_data.append('folder', `${$(".title").attr("data-lesson")} ${$(".title").attr("data-course")}`)
|
||||
}
|
||||
let form_data = new FormData();
|
||||
if (file.file_obj) {
|
||||
form_data.append('file', file.file_obj, `${frappe.session.user}-${file.name}`);
|
||||
form_data.append('folder', `${$(".title").attr("data-lesson")} ${$(".title").attr("data-course")}`)
|
||||
}
|
||||
|
||||
xhr.send(form_data);
|
||||
});
|
||||
};
|
||||
|
||||
xhr.send(form_data);
|
||||
});
|
||||
}
|
||||
|
||||
const create_lesson_work = (file, target) => {
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lesson_assignment.lesson_assignment.upload_assignment",
|
||||
args: {
|
||||
assignment: file.file_url,
|
||||
lesson: $(".title").attr("data-lesson"),
|
||||
identifier: target.siblings(".attach-file").attr("id")
|
||||
},
|
||||
callback: (data) => {
|
||||
target.siblings(".attach-file").addClass("hide");
|
||||
target.siblings(".preview-work").removeClass("hide");
|
||||
target.siblings(".preview-work").find("a").attr("href", file.file_url).text(file.file_name)
|
||||
target.addClass("hide");
|
||||
}
|
||||
});
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lesson_assignment.lesson_assignment.upload_assignment",
|
||||
args: {
|
||||
assignment: file.file_url,
|
||||
lesson: $(".title").attr("data-lesson"),
|
||||
identifier: target.siblings(".attach-file").attr("id")
|
||||
},
|
||||
callback: (data) => {
|
||||
target.siblings(".attach-file").addClass("hide");
|
||||
target.siblings(".preview-work").removeClass("hide");
|
||||
target.siblings(".preview-work").find("a").attr("href", file.file_url).text(file.file_name)
|
||||
target.addClass("hide");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const return_as_dataurl = (files) => {
|
||||
let promises = files.map(file =>
|
||||
frappe.dom.file_to_base64(file.file_obj)
|
||||
.then(dataurl => {
|
||||
file.dataurl = dataurl;
|
||||
this.on_success && this.on_success(file);
|
||||
})
|
||||
);
|
||||
return Promise.all(promises);
|
||||
}
|
||||
let promises = files.map(file =>
|
||||
frappe.dom.file_to_base64(file.file_obj)
|
||||
.then(dataurl => {
|
||||
file.dataurl = dataurl;
|
||||
this.on_success && this.on_success(file);
|
||||
})
|
||||
);
|
||||
return Promise.all(promises);
|
||||
};
|
||||
|
||||
|
||||
const add_files = (files) => {
|
||||
files = Array.from(files).map(file => {
|
||||
let is_image = file.type.startsWith('image');
|
||||
return {
|
||||
file_obj: file,
|
||||
cropper_file: file,
|
||||
crop_box_data: null,
|
||||
optimize: this.attach_doc_image ? true : false,
|
||||
name: file.name,
|
||||
doc: null,
|
||||
progress: 0,
|
||||
total: 0,
|
||||
failed: false,
|
||||
request_succeeded: false,
|
||||
error_message: null,
|
||||
uploading: false,
|
||||
private: !is_image
|
||||
}
|
||||
});
|
||||
return files
|
||||
files = Array.from(files).map(file => {
|
||||
let is_image = file.type.startsWith('image');
|
||||
return {
|
||||
file_obj: file,
|
||||
cropper_file: file,
|
||||
crop_box_data: null,
|
||||
optimize: this.attach_doc_image ? true : false,
|
||||
name: file.name,
|
||||
doc: null,
|
||||
progress: 0,
|
||||
total: 0,
|
||||
failed: false,
|
||||
request_succeeded: false,
|
||||
error_message: null,
|
||||
uploading: false,
|
||||
private: !is_image
|
||||
}
|
||||
});
|
||||
return files
|
||||
};
|
||||
|
||||
|
||||
const clear_work = (e) => {
|
||||
const target = $(e.currentTarget);
|
||||
const parent = target.closest(".preview-work");
|
||||
parent.addClass("hide");
|
||||
parent.siblings(".attach-file").removeClass("hide").val(null);
|
||||
parent.siblings(".submit-work").removeClass("hide");
|
||||
const target = $(e.currentTarget);
|
||||
const parent = target.closest(".preview-work");
|
||||
parent.addClass("hide");
|
||||
parent.siblings(".attach-file").removeClass("hide").val(null);
|
||||
parent.siblings(".submit-work").removeClass("hide");
|
||||
};
|
||||
|
||||
|
||||
const fetch_assignments = () => {
|
||||
if ($(".attach-file").length <= 0)
|
||||
return;
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lesson_assignment.lesson_assignment.get_assignment",
|
||||
args: {
|
||||
"lesson": $(".title").attr("data-lesson")
|
||||
},
|
||||
callback: (data) => {
|
||||
if (data.message && data.message.length) {
|
||||
const assignments = data.message;
|
||||
for (let i in assignments) {
|
||||
let target = $(`#${assignments[i]["id"]}`);
|
||||
target.addClass("hide");
|
||||
target.siblings(".submit-work").addClass("hide");
|
||||
target.siblings(".preview-work").removeClass("hide");
|
||||
target.siblings(".preview-work").find("a").attr("href", assignments[i]["assignment"]).text(assignments[i]["file_name"]);
|
||||
if ($(".attach-file").length <= 0)
|
||||
return;
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lesson_assignment.lesson_assignment.get_assignment",
|
||||
args: {
|
||||
"lesson": $(".title").attr("data-lesson")
|
||||
},
|
||||
callback: (data) => {
|
||||
if (data.message && data.message.length) {
|
||||
const assignments = data.message;
|
||||
for (let i in assignments) {
|
||||
let target = $(`#${assignments[i]["id"]}`);
|
||||
target.addClass("hide");
|
||||
target.siblings(".submit-work").addClass("hide");
|
||||
target.siblings(".preview-work").removeClass("hide");
|
||||
target.siblings(".preview-work").find("a").attr("href", assignments[i]["assignment"]).text(assignments[i]["file_name"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const initialize_timer = () => {
|
||||
this.time_left = $(".timer").data("time");
|
||||
calculate_and_display_time(100, this.time_left);
|
||||
@@ -423,6 +474,7 @@ const initialize_timer = () => {
|
||||
}, 100);
|
||||
};
|
||||
|
||||
|
||||
const calculate_and_display_time = (percent_time) => {
|
||||
$(".timer .progress-bar").attr("aria-valuenow", percent_time);
|
||||
$(".timer .progress-bar").attr("aria-valuemax", percent_time);
|
||||
@@ -430,3 +482,81 @@ const calculate_and_display_time = (percent_time) => {
|
||||
let progress_color = percent_time < 20 ? "red" : "var(--primary-color)";
|
||||
$(".timer .progress-bar").css("background-color", progress_color);
|
||||
};
|
||||
|
||||
|
||||
const save_lesson = (e) => {
|
||||
let lesson = $("#title").data("lesson");
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_course.lms_course.save_lesson",
|
||||
args: {
|
||||
"title": $("#title").text(),
|
||||
"body": this.code_field_group.fields_dict["code_md"].last_value,
|
||||
"chapter": $("#title").data("chapter"),
|
||||
"preview": $("#preview").prop("checked") ? 1 : 0,
|
||||
"idx": $("#title").data("index"),
|
||||
"lesson": lesson ? lesson : ""
|
||||
},
|
||||
callback: (data) => {
|
||||
window.location.href = window.location.href.split("?")[0];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const show_upload_modal = () => {
|
||||
new frappe.ui.FileUploader({
|
||||
folder: "Home/Attachments",
|
||||
restrictions: {
|
||||
allowed_file_types: ['image/*', 'video/*']
|
||||
},
|
||||
on_success: (file_doc) => {
|
||||
$(".attachments").append(build_attachment_table(file_doc));
|
||||
let count = $(".attachment-count").data("count") + 1;
|
||||
$(".attachment-count").data("count", count);
|
||||
$(".attachment-count").html(__(`${count} attachments`));
|
||||
$(".attachments").removeClass("hide");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const build_attachment_table = (file_doc) => {
|
||||
let video_types = ["mov", "mp4", "mkv"];
|
||||
let video_extension = file_doc.file_url.split(".").pop();
|
||||
let is_video = video_types.indexOf(video_extension) >= 0;
|
||||
let link = is_video ? `{{ Video('${file_doc.file_url}') }}` : ``;
|
||||
|
||||
return $(`
|
||||
<tr class="attachment-row">
|
||||
<td>${file_doc.file_name}</td>
|
||||
<td class="">
|
||||
<a class="button is-secondary button-links copy-link" data-link="${link}"
|
||||
data-name="${file_doc.file_name}" > ${__("Copy Link")}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
`);
|
||||
};
|
||||
|
||||
|
||||
const make_editor = () => {
|
||||
this.code_field_group = new frappe.ui.FieldGroup({
|
||||
fields: [
|
||||
{
|
||||
fieldname: "code_md",
|
||||
fieldtype: "Code",
|
||||
options: "Markdown",
|
||||
wrap: true,
|
||||
max_lines: Infinity,
|
||||
min_lines: 20,
|
||||
default: $("#body").data("body"),
|
||||
depends_on: 'eval:doc.type=="Markdown"',
|
||||
}
|
||||
],
|
||||
body: $("#body").get(0),
|
||||
});
|
||||
this.code_field_group.make();
|
||||
$("#body .form-section:last").removeClass("empty-section");
|
||||
$("#body .frappe-control").removeClass("hide-control");
|
||||
$("#body .form-column").addClass("p-0");
|
||||
};
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
from re import I
|
||||
import frappe
|
||||
from lms.www.utils import get_common_context, redirect_to_lesson
|
||||
from lms.lms.utils import get_lesson_url
|
||||
from lms.lms.utils import get_lesson_url, is_instructor, redirect_to_courses_list
|
||||
from frappe.utils import cstr, flt
|
||||
from lms.www import batch
|
||||
|
||||
def get_context(context):
|
||||
get_common_context(context)
|
||||
@@ -11,6 +9,12 @@ def get_context(context):
|
||||
chapter_index = frappe.form_dict.get("chapter")
|
||||
lesson_index = frappe.form_dict.get("lesson")
|
||||
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")
|
||||
|
||||
if not chapter_index or not lesson_index:
|
||||
if context.batch:
|
||||
index_ = get_lesson_index(context.course, context.batch, frappe.session.user) or "1.1"
|
||||
@@ -19,11 +23,19 @@ def get_context(context):
|
||||
redirect_to_lesson(context.course, index_)
|
||||
|
||||
context.lesson = get_current_lesson_details(lesson_number, context)
|
||||
neighbours = get_neighbours(lesson_number, context.lessons)
|
||||
context.next_url = get_url(neighbours["next"], context.course)
|
||||
context.prev_url = get_url(neighbours["prev"], context.course)
|
||||
if not context.lesson:
|
||||
context.lesson = frappe._dict()
|
||||
|
||||
meta_info = context.lesson.title + " - " + context.course.title
|
||||
if frappe.form_dict.get("edit"):
|
||||
if not is_instructor(context.course.name):
|
||||
redirect_to_courses_list()
|
||||
context.lesson.edit_mode = True
|
||||
else:
|
||||
neighbours = get_neighbours(lesson_number, context.lessons)
|
||||
context.next_url = get_url(neighbours["next"], context.course)
|
||||
context.prev_url = get_url(neighbours["prev"], context.course)
|
||||
|
||||
meta_info = context.lesson.title + " - " + context.course.title if context.lesson.title else "New Lesson"
|
||||
context.metatags = {
|
||||
"title": meta_info,
|
||||
"keywords": meta_info,
|
||||
@@ -34,23 +46,34 @@ def get_context(context):
|
||||
context.page_context = {
|
||||
"course": context.course.name,
|
||||
"batch": context.get("batch") and context.batch.name,
|
||||
"lesson": context.lesson.name,
|
||||
"lesson": context.lesson.name if context.lesson.name else "New Lesson",
|
||||
"is_member": context.membership is not None
|
||||
}
|
||||
|
||||
|
||||
def get_current_lesson_details(lesson_number, context):
|
||||
details_list = list(filter(lambda x: cstr(x.number) == lesson_number, context.lessons))
|
||||
|
||||
if not len(details_list):
|
||||
redirect_to_lesson(context.course)
|
||||
return details_list[0]
|
||||
if frappe.form_dict.get("edit"):
|
||||
return None
|
||||
else:
|
||||
redirect_to_lesson(context.course)
|
||||
|
||||
lesson_info = details_list[0]
|
||||
lesson_info.body = lesson_info.body.replace("\"", "'")
|
||||
return lesson_info
|
||||
|
||||
|
||||
def get_url(lesson_number, course):
|
||||
return get_lesson_url(course.name, lesson_number) and get_lesson_url(course.name, lesson_number) + course.query_parameter
|
||||
|
||||
|
||||
def get_lesson_index(course, batch, user):
|
||||
lesson = batch.get_current_lesson(user)
|
||||
return lesson and course.get_lesson_index(lesson)
|
||||
|
||||
|
||||
def get_page_extensions(context):
|
||||
default_value = ["lms.plugins.PageExtension"]
|
||||
classnames = frappe.get_hooks("lms_lesson_page_extensions") or default_value
|
||||
@@ -59,6 +82,7 @@ def get_page_extensions(context):
|
||||
e.set_context(context)
|
||||
return extensions
|
||||
|
||||
|
||||
def get_neighbours(current, lessons):
|
||||
current = flt(current)
|
||||
numbers = sorted(lesson.number for lesson in lessons)
|
||||
|
||||
77
lms/www/batch/quiz.html
Normal file
77
lms/www/batch/quiz.html
Normal file
@@ -0,0 +1,77 @@
|
||||
{% extends "templates/base.html" %}
|
||||
{% block title %}
|
||||
{{ _("Quiz List") }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block head_include %}
|
||||
{% include "public/icons/symbol-defs.svg" %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div class="common-page-style" style="background-color: var(--fg-color);">
|
||||
<div class="container">
|
||||
{{ BreadCrumb(quiz) }}
|
||||
{{ QuizCard(quiz) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% macro BreadCrumb(quiz) %}
|
||||
<div class="breadcrumb">
|
||||
<a class="dark-links" href="/quizzes">{{ _("Quizzes") }}</a>
|
||||
<img class="ml-1 mr-1" src="/assets/lms/icons/chevron-right.svg">
|
||||
<span class="breadcrumb-destination">{{ quiz.title if quiz.title else _("New Quiz") }}</span>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro QuizCard(quiz) %}
|
||||
<div style="width: 60%;">
|
||||
|
||||
<div class="course-home-headings mb-2" data-placeholder="{{ _('Quiz Title') }}" id="quiz-title"
|
||||
{% if quiz.name %} data-name="{{ quiz.name }}" {% endif %}
|
||||
contenteditable="true" >{% if quiz.title %}{{ quiz.title }}{% endif %}</div>
|
||||
|
||||
|
||||
{% if quiz.questions %}
|
||||
{% for question in quiz.questions %}
|
||||
<div class="quiz-card">
|
||||
<div contenteditable="true" data-placeholder="{{ _('Question') }}" data-question="{{ question.name }}"
|
||||
class="question mb-4">{% if question.question %} {{ question.question }} {% endif %}</div>
|
||||
|
||||
{% for i in range(1,5) %}
|
||||
{% set num = frappe.utils.cstr(i) %}
|
||||
{% set option = question["option_" + num] %}
|
||||
{% set explanation = question["explanation_" + num] %}
|
||||
|
||||
<div class="mt-4">
|
||||
<label class=""> {{ _("Option") }} {{ num }} </label>
|
||||
<div class="d-flex justify-content-between option-{{ num }}">
|
||||
<div contenteditable="true" data-placeholder="{{ _('Option') }}"
|
||||
class="option-input">{% if option %}{{ option }}{% endif %}</div>
|
||||
<div contenteditable="true" data-placeholder="{{ _('Explanation') }}"
|
||||
class="option-input">{% if explanation %}{{ explanation }}{% endif %}</div>
|
||||
<div class="option-checkbox">
|
||||
<input type="checkbox" {% if question['is_correct_' + num] %} checked {% endif %}>
|
||||
<label class="mb-0"> {{ _("Is Correct") }} </label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
<div class="mt-4">
|
||||
<button class="btn btn-secondary btn-sm btn-question"> {{ _("New Question") }} </button>
|
||||
<button class="btn btn-primary btn-sm btn-save-question ml-2
|
||||
{% if not quiz.name %} hide {% endif %}"> {{ _("Save Quiz") }} </button>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
106
lms/www/batch/quiz.js
Normal file
106
lms/www/batch/quiz.js
Normal file
@@ -0,0 +1,106 @@
|
||||
frappe.ready(() => {
|
||||
|
||||
$(".btn-question").click((e) => {
|
||||
add_question(e);
|
||||
});
|
||||
|
||||
$(".btn-save-question").click((e) => {
|
||||
save_question(e);
|
||||
});
|
||||
|
||||
get_questions()
|
||||
|
||||
});
|
||||
|
||||
|
||||
const add_question = (e) => {
|
||||
let add_after = $(".quiz-card").length ? $(".quiz-card:last") : $("#quiz-title");
|
||||
let question_template = `<div class="quiz-card">
|
||||
<div contenteditable="true" data-placeholder="${__("Question")}" class="question mb-4"></div>
|
||||
</div>`;
|
||||
$(question_template).insertAfter(add_after);
|
||||
get_question_template();
|
||||
$(".btn-save-question").removeClass("hide");
|
||||
};
|
||||
|
||||
|
||||
const get_question_template = () => {
|
||||
Array.from({length: 4}, (x, num) => {
|
||||
let option_template = get_option_template(num + 1);
|
||||
let add_after = $(".quiz-card:last .option-group").length ? $(".quiz-card:last .option-group").last() : $(".question:last");
|
||||
question_template = $(option_template).insertAfter(add_after);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const get_option_template = (num) => {
|
||||
return `<div class="option-group mt-4">
|
||||
<label class="">${__("Option")} ${num}</label>
|
||||
<div class="d-flex justify-content-between option-${num}">
|
||||
<div contenteditable="true" data-placeholder="${ __("Option") }"
|
||||
class="option-input"></div>
|
||||
<div contenteditable="true" data-placeholder="${ __('Explanation') }"
|
||||
class="option-input"></div>
|
||||
<div class="option-checkbox">
|
||||
<input type="checkbox">
|
||||
<label class="mb-0"> ${ __("Is Correct") } </label>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
};
|
||||
|
||||
|
||||
const save_question = (e) => {
|
||||
if (!$("#quiz-title").text()) {
|
||||
frappe.throw(__("Quiz Title is mandatory."));
|
||||
}
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_quiz.lms_quiz.save_quiz",
|
||||
args: {
|
||||
"quiz_title": $("#quiz-title").text(),
|
||||
"questions": get_questions(),
|
||||
"quiz": $("#quiz-title").data("name") || ""
|
||||
},
|
||||
callback: (data) => {
|
||||
window.location.href = "/quizzes";
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const get_questions = () => {
|
||||
let questions = [];
|
||||
|
||||
$(".quiz-card").each((i, el) => {
|
||||
if (!$(el).find(".question").text())
|
||||
return;
|
||||
|
||||
let details = {};
|
||||
let one_correct_option = false;
|
||||
details["question"] = $(el).find(".question").text();
|
||||
details["question_name"] = $(el).find(".question").data("question") || "";
|
||||
|
||||
Array.from({length: 4}, (x, i) => {
|
||||
let num = i + 1;
|
||||
|
||||
details[`option_${num}`] = $(el).find(`.option-${num} .option-input:first`).text();
|
||||
details[`explanation_${num}`] = $(el).find(`.option-${num} .option-input:last`).text();
|
||||
|
||||
let is_correct = $(el).find(`.option-${num} .option-checkbox`).find("input").prop("checked");
|
||||
if (is_correct)
|
||||
one_correct_option = true;
|
||||
|
||||
details[`is_correct_${num}`] = is_correct;
|
||||
});
|
||||
|
||||
if (!details["option_1"] || !details["option_2"])
|
||||
frappe.throw(__("Each question must have at least two options."))
|
||||
|
||||
if (!one_correct_option)
|
||||
frappe.throw(__("Each question must have at least one correct option."))
|
||||
|
||||
questions.push(details);
|
||||
});
|
||||
|
||||
return questions;
|
||||
};
|
||||
21
lms/www/batch/quiz.py
Normal file
21
lms/www/batch/quiz.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import frappe
|
||||
from frappe.utils import cstr
|
||||
|
||||
|
||||
def get_context(context):
|
||||
quizname = frappe.form_dict["quizname"]
|
||||
if quizname == "new-quiz":
|
||||
context.quiz = frappe._dict()
|
||||
context.quiz.edit_mode = 1
|
||||
else:
|
||||
fields_arr = ["name","question"]
|
||||
for num in range(1,5):
|
||||
fields_arr.append("option_" + cstr(num))
|
||||
fields_arr.append("is_correct_" + cstr(num))
|
||||
fields_arr.append("explanation_" + cstr(num))
|
||||
|
||||
context.quiz = frappe.db.get_value("LMS Quiz", quizname, ["title", "name"], as_dict=1)
|
||||
context.quiz.questions = frappe.get_all("LMS Quiz Question", {
|
||||
"parent": quizname
|
||||
}, fields_arr,
|
||||
order_by="idx")
|
||||
52
lms/www/batch/quiz_list.html
Normal file
52
lms/www/batch/quiz_list.html
Normal file
@@ -0,0 +1,52 @@
|
||||
{% extends "templates/base.html" %}
|
||||
{% block title %}
|
||||
{{ _("Quiz List") }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block head_include %}
|
||||
{% include "public/icons/symbol-defs.svg" %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div class="common-page-style">
|
||||
<div class="container">
|
||||
{% if quiz_list | length %}
|
||||
<a class="btn btn-secondary btn-sm pull-right" href="/quizzes/new-quiz"> {{ _("Add Quiz") }} </a>
|
||||
<div class="course-home-headings"> {{ _("Quiz List") }} </div>
|
||||
<div class="common-card-style">
|
||||
<table class="table">
|
||||
<tr style="background-color: var(--fg-hover-color); font-weight: bold">
|
||||
<td style="width: 10%;"> {{ _("No.") }} </td>
|
||||
<td style="width: 45%;"> {{ _("Title") }} </td>
|
||||
<td> {{ _("ID") }} </td>
|
||||
</tr>
|
||||
{% for quiz in quiz_list %}
|
||||
<tr style="position: relative; color: var(--text-color);">
|
||||
<td> {{ loop.index }} </td>
|
||||
<td>
|
||||
<a class="button-links" href="/quizzes/{{ quiz.name }}">{{ quiz.title }}</a>
|
||||
</td>
|
||||
<td>
|
||||
<a class="button-links" href="/quizzes/{{ quiz.name }}">{{ quiz.name }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-text">
|
||||
<div class="empty-state-heading">{{ _("You have not created any quiz yet.") }}</div>
|
||||
<div class="course-meta mb-6">{{ _("Create a quiz and add it to your course to engage users.") }}</div>
|
||||
<a class="btn btn-secondary btn-sm"
|
||||
href="{% if frappe.session.user == 'Guest' %} /login?redirect-to=/quizzes {% else %} /quizzes/new-quiz {% endif %}">
|
||||
{{ _("Add Quiz") }} </a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
4
lms/www/batch/quiz_list.py
Normal file
4
lms/www/batch/quiz_list.py
Normal file
@@ -0,0 +1,4 @@
|
||||
import frappe
|
||||
|
||||
def get_context(context):
|
||||
context.quiz_list = frappe.get_all("LMS Quiz", {"owner": frappe.session.user}, ["name", "title"])
|
||||
@@ -1,100 +1,351 @@
|
||||
{% extends "templates/base.html" %}
|
||||
{% block title %}{{ course.title }}
|
||||
{% block title %}
|
||||
{{ course.title if course.title else _("New Course") }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block head_include %}
|
||||
{% include "public/icons/symbol-defs.svg" %}
|
||||
<link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
|
||||
{% include "public/icons/symbol-defs.svg" %}
|
||||
<link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div class="common-page-style pt-0 pb-0">
|
||||
<div class="course-home-top-container">
|
||||
{{ CourseHomeHeader(course) }}
|
||||
<div class="course-home-page">
|
||||
<div class="container">
|
||||
<div class="course-body-container">
|
||||
{{ CourseHeaderOverlay(course) }}
|
||||
{{ Description(course) }}
|
||||
{{ widgets.CourseOutline(course=course, membership=membership, is_user_interested=is_user_interested) }}
|
||||
{{ widgets.Reviews(course=course, membership=membership) }}
|
||||
<div class="course-home-top-container">
|
||||
{{ CourseHomeHeader(course) }}
|
||||
<div class="course-home-page">
|
||||
<div class="container">
|
||||
<div class="course-body-container">
|
||||
{{ CourseHeaderOverlay(course) }}
|
||||
{{ Description(course) }}
|
||||
{{ Save(course) }}
|
||||
{{ widgets.CourseOutline(course=course, membership=membership, is_user_interested=is_user_interested) }}
|
||||
{% if not course.edit_mode %}
|
||||
{{ widgets.Reviews(course=course, membership=membership) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ RelatedCourses(course) }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% macro CourseHomeHeader(course) %}
|
||||
<div class="course-head-container">
|
||||
<div class="container pt-8 pb-10">
|
||||
<div class="course-card-wide">
|
||||
{{ BreadCrumb(course) }}
|
||||
{{ CourseCardWide(course) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
<!-- BreadCrumb -->
|
||||
|
||||
{% macro BreadCrumb(course) %}
|
||||
<div class="breadcrumb">
|
||||
<a class="dark-links" href="/courses">{{ _("All Courses") }}</a>
|
||||
<img class="ml-1 mr-1" src="/assets/lms/icons/chevron-right.svg">
|
||||
<span class="breadcrumb-destination">{{ course.title }}</span>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
<!-- Course Card -->
|
||||
|
||||
{% macro CourseCardWide(course) %}
|
||||
<div class="d-flex align-items-center mt-8">
|
||||
{% for tag in get_tags(course.name) %}
|
||||
<div class="course-card-pills">{{ tag }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="course-card-wide-title">
|
||||
{{ course.title }}
|
||||
</div>
|
||||
<div class="">
|
||||
{{ course.short_introduction }}
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<div class="bold-heading">{{ _("Instructors") }}:</div>
|
||||
{% for instructor in get_instructors(course.name) %}
|
||||
<div class="mt-1">
|
||||
{{ widgets.Avatar(member=instructor, avatar_class="avatar-small") }}
|
||||
<a class="button-links" href="{{ get_profile_url(instructor.username) }}">
|
||||
<span class="course-instructor"> {{ instructor.full_name }} </span>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if membership %}
|
||||
{% set progress = frappe.utils.cint(membership.progress) %}
|
||||
<div class="mt-8">
|
||||
<div class="progress-percent m-0">{{ progress }}% {{ _("Completed") }}</div>
|
||||
<div class="progress" title="{{ progress }}% Completed">
|
||||
<div class="progress-bar" role="progressbar" aria-valuenow="{{ progress }}"
|
||||
aria-valuemin="0" aria-valuemax="100" style="width:{{ progress }}%">
|
||||
<div class="container pt-8 pb-10">
|
||||
<div class="course-card-wide">
|
||||
{{ BreadCrumb(course) }}
|
||||
{{ CourseCardWide(course) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro%}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- BreadCrumb -->
|
||||
{% macro BreadCrumb(course) %}
|
||||
<div class="breadcrumb">
|
||||
<a class="dark-links" href="/courses">{{ _("All Courses") }}</a>
|
||||
<img class="ml-1 mr-1" src="/assets/lms/icons/chevron-right.svg">
|
||||
<span class="breadcrumb-destination">{{ course.title if course.title else _("New Course") }}</span>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Course Card -->
|
||||
{% macro CourseCardWide(course) %}
|
||||
<div class="d-flex align-items-center mt-8">
|
||||
{% for tag in get_tags(course.name) %}
|
||||
<div class="course-card-pills" {% if course.edit_mode %} contenteditable="true" {% endif %}>{{ tag }}
|
||||
{% if course.edit_mode %}
|
||||
<span class="btn-delete-tag">
|
||||
<svg class="icon icon-sm">
|
||||
<use class="" href="#icon-close"></use>
|
||||
</svg>
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if course.edit_mode %}
|
||||
<button class="btn btn-default btn-sm btn-tag"> {{ _("Add Tag") }} </button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div {% if course.edit_mode %} data-placeholder="{{ _('Title') }}" contenteditable="true" {% endif %}
|
||||
id="title" {% if course.name %} data-course="{{ course.name | urlencode }}" {% endif %}
|
||||
class="course-card-wide-title">{% if course.title %} {{ course.title }} {% endif %}</div>
|
||||
|
||||
<div {% if course.edit_mode %} contenteditable="true" data-placeholder="{{ _('Short Introduction') }}"
|
||||
{% endif %} id="intro" >{% if course.short_introduction %} {{ course.short_introduction }} {% endif %}</div>
|
||||
|
||||
{% if course.edit_mode %}
|
||||
<div class="preview-video-header">
|
||||
<div class="d-block mt-1" contenteditable="true" id="video-link"
|
||||
data-placeholder=" {{ _('Preview Video Link') }} ">{% if course.video_link %}{{ course.video_link }}{% endif %}</div>
|
||||
|
||||
<div class="preview-info">
|
||||
<div class="tool-tip">
|
||||
<div class="tooltiptext">
|
||||
<span>
|
||||
{{ _('If you have a video that provides a teaser or preview of the course, you can add it here.') }}
|
||||
</span>
|
||||
<span>
|
||||
{{ _("Follow the steps mentioned below for the same.") }}
|
||||
</span>
|
||||
<ul>
|
||||
<li>
|
||||
{{ _("Upload the video on youtube.") }}
|
||||
</li>
|
||||
<li>
|
||||
{{ _("When you share a youtube video, it shows an option called Embed.") }}
|
||||
</li>
|
||||
<li>
|
||||
{{ _("On clicking it, it provides an iframe. Copy the source (src) of the iframe and paste it here.") }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<svg class="icon icon-md">
|
||||
<use href="#icon-solid-info"></use>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="course-image-attachment {% if not course.image %} hide {% endif %} ">
|
||||
<a href="{{ course.image }}" id="image" target="_blank"> {{ course.image }} </a>
|
||||
<button class="btn btn-sm btn-default btn-clear ml-4"> {{ _("Clear") }} </button>
|
||||
</div>
|
||||
<a class="btn btn-default btn-sm btn-attach mt-1 {% if course.image %} hide {% endif %}"> {{ _("Attach Image") }} </a>
|
||||
{% endif %}
|
||||
|
||||
{% if not course.edit_mode %}
|
||||
<div class="mt-8">
|
||||
<div class="bold-heading">{{ _("Instructors") }}:</div>
|
||||
{% for instructor in get_instructors(course.name) %}
|
||||
<div class="mt-1">
|
||||
{{ widgets.Avatar(member=instructor, avatar_class="avatar-small") }}
|
||||
<a class="button-links" href="{{ get_profile_url(instructor.username) }}">
|
||||
<span class="course-instructor"> {{ instructor.full_name }} </span>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if membership and not course.edit_mode %}
|
||||
{% set progress = frappe.utils.cint(membership.progress) %}
|
||||
<div class="mt-8">
|
||||
<div class="progress-percent m-0">{{ progress }}% {{ _("Completed") }}</div>
|
||||
<div class="progress" title="{{ progress }}% Completed">
|
||||
<div class="progress-bar" role="progressbar" aria-valuenow="{{ progress }}"
|
||||
aria-valuemin="0" aria-valuemax="100" style="width:{{ progress }}%">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Overlay -->
|
||||
{% macro CourseHeaderOverlay(course) %}
|
||||
<div class="course-overlay-card">
|
||||
{% if not course.edit_mode %}
|
||||
<div class="course-overlay-card">
|
||||
|
||||
{% if course.video_link %}
|
||||
<iframe class="preview-video" src="{{ course.video_link }}"></iframe>
|
||||
{% endif %}
|
||||
{% if course.video_link %}
|
||||
<iframe class="preview-video" src="{{ course.video_link }}"></iframe>
|
||||
{% endif %}
|
||||
|
||||
<div class="course-overlay-content">
|
||||
<div class="course-overlay-title"> {{ course.title }} </div>
|
||||
<div class="course-overlay-content">
|
||||
<div class="course-overlay-title"> {{ course.title }} </div>
|
||||
|
||||
{{ Notes(course) }}
|
||||
|
||||
<div class="vertically-center mb-3">
|
||||
<svg class="icon icon-md mr-1">
|
||||
<use class="" href="#icon-users">
|
||||
</svg>
|
||||
{{ get_students(course.name) | length }} {{ _("Enrolled") }}
|
||||
</div>
|
||||
|
||||
{% if get_lessons(course.name) | length %}
|
||||
<div class="vertically-center mb-3">
|
||||
<svg class="icon icon-md mr-1">
|
||||
<use href="#icon-education"></use>
|
||||
</svg>
|
||||
{{ get_lessons(course.name) | length }} {{ _("Lessons") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if course.paid_certificate %}
|
||||
<div class="vertically-center mb-3">
|
||||
<svg class="icon icon-md mr-1">
|
||||
<use href="#icon-badge"></use>
|
||||
</svg>
|
||||
<span class="certificate-price" data-price="{{ course.price_certificate }}">
|
||||
{{ format_amount(course.price_certificate, course.currency) }}
|
||||
</span>
|
||||
<span class="indicator-pill green ml-3"> {{ _("Get Certified") }} </span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{{ CTASection(course, membership) }}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{{ SlotModal(course) }}
|
||||
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Description -->
|
||||
{% macro Description(course) %}
|
||||
<div class="course-description-section" {% if course.edit_mode %} style="min-height: 100px" {% endif %}
|
||||
{% if course.edit_mode %} contenteditable="true" {% endif %} id="description"
|
||||
data-placeholder="Description">{% if course.description %}{{ frappe.utils.md_to_html(course.description) }}{% endif %}</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Save -->
|
||||
{% macro Save(course) %}
|
||||
{% if course.edit_mode %}
|
||||
<div class="my-4">
|
||||
<button class="btn btn-primary btn-md btn-save-course">
|
||||
{{ _("Save Course Details") }}
|
||||
</button>
|
||||
{% if course.name %}
|
||||
<a class="btn btn-secondary btn-md btn-exit-edit ml-2" href="/courses/{{ course.name }}">
|
||||
{{ _("Back to Course") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro CourseCreator(course) %}
|
||||
<div class="course-home-headings"> {{ _("Course Creators") }} </div>
|
||||
<div class="common-card-style course-creators-card">
|
||||
{% set instructors = get_instructors(course.name) %}
|
||||
{% for instructor in instructors %}
|
||||
<div class="d-flex align-items-center">
|
||||
{{ widgets.Avatar(member=instructor, avatar_class="avatar-medium") }}
|
||||
<div class="ml-4">
|
||||
<div class="course-creator-name"> {{ instructor.full_name }} </div>
|
||||
<div class="course-meta"> {{ get_authored_courses(instructor.name) | length }} {{ _("Courses Created") }} </div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Related Courses Section -->
|
||||
{% macro RelatedCourses(course) %}
|
||||
{% if course.related_courses | length %}
|
||||
<div class="related-courses">
|
||||
<div class="container">
|
||||
<div class="course-home-headings"> {{ _("Other Courses") }} </div>
|
||||
<div class="carousel slide" id="carouselExampleControls" data-ride="carousel" data-interval="false">
|
||||
<div class="carousel-inner">
|
||||
{% for crs in course.related_courses %}
|
||||
{% if loop.index % 3 == 1 %}
|
||||
<div class="carousel-item {% if loop.index == 1 %} active {% endif %}"><div class="cards-parent">
|
||||
{% endif %}
|
||||
{{ widgets.CourseCard(course=crs, read_only=False) }}
|
||||
{% if loop.index % 3 == 0 or loop.index == course.related_courses | length %} </div> </div> {% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if course.related_courses | length > 3 %}
|
||||
<div class="slider-controls">
|
||||
<a class="carousel-control-prev" href="#carouselExampleControls" role="button" data-slide="prev">
|
||||
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
||||
</a>
|
||||
|
||||
<a class="carousel-control-next" href="#carouselExampleControls" role="button" data-slide="next">
|
||||
<span class="carousel-control-next-icon" aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- CTA's -->
|
||||
{% macro CTASection(course, membership) %}
|
||||
{% set lesson_index = get_lesson_index(membership.current_lesson) if membership and
|
||||
membership.current_lesson else "1.1" if first_lesson_exists(course.name) else None %}
|
||||
|
||||
{% if show_start_learing_cta %}
|
||||
<div class="btn btn-primary wide-button join-batch" data-course="{{ course.name | urlencode }}">
|
||||
{{ _("Start Learning") }}
|
||||
<img class="ml-2" src="/assets/lms/icons/white-arrow.svg" />
|
||||
</div>
|
||||
|
||||
{% elif is_instructor(course.name) and not course.published and course.status != "Under Review" %}
|
||||
<div class="btn btn-primary wide-button mb-2" id="submit-for-review" data-course="{{ course.name | urlencode }}">
|
||||
{{ _("Submit for Review") }}
|
||||
</div>
|
||||
|
||||
{% elif is_instructor(course.name) and lesson_index %}
|
||||
<a class="btn btn-primary wide-button" id="continue-learning"
|
||||
href="{{ get_lesson_url(course.name, lesson_index) }}{{ course.query_parameter }}">
|
||||
{{ _("Checkout Course") }} <img class="ml-2" src="/assets/lms/icons/white-arrow.svg" />
|
||||
</a>
|
||||
|
||||
{% elif course.upcoming and not is_user_interested %}
|
||||
<div class="btn btn-secondary wide-button notify-me" data-course="{{course.name | urlencode}}">
|
||||
{{ _("Notify me when available") }}
|
||||
</div>
|
||||
|
||||
{% elif is_cohort_staff(course.name, frappe.session.user) %}
|
||||
<a class="btn btn-secondary button-links wide-button" href="/courses/{{course.name}}/manage">
|
||||
{{ _("Manage the course") }}
|
||||
</a>
|
||||
|
||||
{% elif membership %}
|
||||
<a class="btn btn-primary wide-button" id="continue-learning"
|
||||
href="{{ get_lesson_url(course.name, lesson_index) }}{{ course.query_parameter }}">
|
||||
{{ _("Continue Learning") }} <img class="ml-2" src="/assets/lms/icons/white-arrow.svg" />
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% set progress = frappe.utils.cint(membership.progress) %}
|
||||
|
||||
{% if membership and course.enable_certification %}
|
||||
{% if certificate %}
|
||||
<a class="btn btn-secondary wide-button mt-3" href="/courses/{{ course.name }}/{{ certificate }}">
|
||||
{{ _("Get Certificate") }}
|
||||
</a>
|
||||
|
||||
{% elif eligible_for_evaluation %}
|
||||
<a class="btn btn-secondary wide-button mt-3" id="apply-certificate" data-course="{{ course.name }}">
|
||||
{{ _("Apply for Certificate") }}
|
||||
</a>
|
||||
|
||||
{% elif course.grant_certificate_after == "Completion" and progress == 100 %}
|
||||
<div class="btn btn-secondary wide-button is-secondary mt-3" id="certification" data-course="{{ course.name }}">
|
||||
{{ _("Get Certificate") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if is_instructor(course.name) %}
|
||||
<a class="btn btn-secondary wide-button" href="/courses/{{ course.name }}?edit=1"> {{ _("Edit Course") }} </a>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Notes and Messages -->
|
||||
{% macro Notes(course) %}
|
||||
<div id="interest-alert" class="{% if not is_user_interested %} hide {% endif %}">
|
||||
{{ _("You have opted to be notified for this course. You will receive an email when the course becomes available.") }}
|
||||
</div>
|
||||
@@ -110,211 +361,56 @@
|
||||
{{ _("Your course is currently under review. Once the review is complete, the System Admins will publish it on the website.") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if no_of_attempts and no_of_attempts >= course.max_attempts %}
|
||||
<p> {{ _("You have exceeded the maximum number of attempts allowed to appear for evaluations of this course.") }} </p>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% if is_instructor(course.name) %}
|
||||
<div class="vertically-center mb-4">
|
||||
<a class="button is-default button-links mr-2" href="/course?name={{ course.name }}"> {{ _("Edit") }} </a>
|
||||
<a class="button is-default button-links mr-2" href="/chapter">
|
||||
<svg class="icon icon-sm mr-1"><use href="#icon-add"></use></svg>
|
||||
{{ _("Add Chapter") }}
|
||||
</a>
|
||||
<a class="button is-default button-links mr-2" href="/lesson">
|
||||
<svg class="icon icon-sm mr-1"><use href="#icon-add"></use></svg>
|
||||
{{ _("Add Lesson") }}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="vertically-center mb-3">
|
||||
<svg class="icon icon-md mr-1">
|
||||
<use class="" href="#icon-users">
|
||||
</svg>
|
||||
{{ get_students(course.name) | length }} {{ _("Enrolled") }}
|
||||
</div>
|
||||
|
||||
{% if get_lessons(course.name) | length %}
|
||||
<div class="vertically-center mb-3">
|
||||
<svg class="icon icon-md mr-1">
|
||||
<use href="#icon-education"></use>
|
||||
</svg>
|
||||
{{ get_lessons(course.name) | length }} {{ _("Lessons") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if course.paid_certificate %}
|
||||
<div class="vertically-center mb-3">
|
||||
<svg class="icon icon-md mr-1">
|
||||
<use href="#icon-badge"></use>
|
||||
</svg>
|
||||
<span class="certificate-price" data-price="{{ course.price_certificate }}">
|
||||
{{ format_amount(course.price_certificate, course.currency) }}
|
||||
</span>
|
||||
<span class="indicator-pill green ml-3"> {{ _("Get Certified") }} </span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% set lesson_index = get_lesson_index(membership.current_lesson) if membership and
|
||||
membership.current_lesson
|
||||
else '1.1' %}
|
||||
|
||||
{% if show_start_learing_cta %}
|
||||
<div class="button wide-button is-primary join-batch" data-course="{{ course.name | urlencode }}">
|
||||
{{ _("Start Learning") }}
|
||||
<img class="ml-2" src="/assets/lms/icons/white-arrow.svg" />
|
||||
</div>
|
||||
|
||||
{% elif is_instructor(course.name) and not course.published and course.status == "Under Review" %}
|
||||
<div class="button wide-button is-primary" id="submit-for-review" data-course="{{ course.name | urlencode }}">
|
||||
{{ _("Submit for Review") }}
|
||||
<img class="ml-2" src="/assets/lms/icons/white-arrow.svg" />
|
||||
</div>
|
||||
|
||||
{% elif is_instructor(course.name) %}
|
||||
<a class="button wide-button is-primary" id="continue-learning"
|
||||
href="{{ get_lesson_url(course.name, lesson_index) }}{{ course.query_parameter }}">
|
||||
{{ _("Checkout Course") }} <img class="ml-2" src="/assets/lms/icons/white-arrow.svg" />
|
||||
</a>
|
||||
|
||||
{% elif course.upcoming and not is_user_interested %}
|
||||
<div class="button wide-button is-default notify-me" data-course="{{course.name | urlencode}}">
|
||||
{{ _("Notify me when available") }}
|
||||
</div>
|
||||
|
||||
{% elif is_cohort_staff(course.name, frappe.session.user) %}
|
||||
<a class="button wide-button is-secondary"
|
||||
href="/courses/{{course.name}}/manage"
|
||||
style="color: inherit;">
|
||||
{{ _("Manage the course") }}
|
||||
</a>
|
||||
|
||||
{% elif membership %}
|
||||
<a class="button wide-button is-primary" id="continue-learning"
|
||||
href="{{ get_lesson_url(course.name, lesson_index) }}{{ course.query_parameter }}">
|
||||
{{ _("Continue Learning") }} <img class="ml-2" src="/assets/lms/icons/white-arrow.svg" />
|
||||
</a>
|
||||
{% endif %}
|
||||
{% set progress = frappe.utils.cint(membership.progress) %}
|
||||
{% if membership and course.enable_certification %}
|
||||
{% if certificate %}
|
||||
<a class="button wide-button is-secondary mt-3" href="/courses/{{ course.name }}/{{ certificate }}">
|
||||
{{ _("Get Certificate") }}
|
||||
</a>
|
||||
{% elif eligible_for_evaluation %}
|
||||
<a class="button wide-button is-secondary mt-3" id="apply-certificate" data-course="{{ course.name }}">
|
||||
{{ _("Apply for Certificate") }}
|
||||
</a>
|
||||
{% elif course.grant_certificate_after == "Completion" and progress == 100 %}
|
||||
<div class="button wide-button is-secondary mt-3" id="certification" data-course="{{ course.name }}">
|
||||
{{ _("Get Certificate") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal for Slots -->
|
||||
{% macro SlotModal(course) %}
|
||||
<div class="modal fade" id="slot-modal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div class="font-weight-bold">{{ _("Pick a Slot") }}</div>
|
||||
<button type="button" class="close close-slot-modal" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="slot-form">
|
||||
<p class="">{{ _("This course requires you to complete an evaluation to get certified. Please pick a slot based on your convenience for the evaluations. ") }}</p>
|
||||
<div class="form-group">
|
||||
<div class="clearfix">
|
||||
<label class="control-label reqd" style="padding-right: 0px;">{{ _("Date") }}</label>
|
||||
</div>
|
||||
<div class="control-input-wrapper">
|
||||
<div class="control-input">
|
||||
<input type="date" class="input-with-feedback form-control bold" data-fieldtype="Date" data-course="{{ course.name | urlencode }}"
|
||||
id="slot-date" min="{{ frappe.utils.format_date(frappe.utils.add_days(frappe.utils.getdate(), 1), 'yyyy-mm-dd') }}">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div class="font-weight-bold">{{ _("Pick a Slot") }}</div>
|
||||
<button type="button" class="close close-slot-modal" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="slot-form">
|
||||
<p class="">{{ _("This course requires you to complete an evaluation to get certified. Please pick a slot based on your convenience for the evaluations. ") }}</p>
|
||||
<div class="form-group">
|
||||
<div class="clearfix">
|
||||
<label class="control-label reqd" style="padding-right: 0px;">{{ _("Date") }}</label>
|
||||
</div>
|
||||
<div class="control-input-wrapper">
|
||||
<div class="control-input">
|
||||
<input type="date" class="input-with-feedback form-control bold" data-fieldtype="Date" data-course="{{ course.name | urlencode }}"
|
||||
id="slot-date" min="{{ frappe.utils.format_date(frappe.utils.add_days(frappe.utils.getdate(), 1), 'yyyy-mm-dd') }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="clearfix">
|
||||
<label class="control-label reqd slot-label hide" style="padding-right: 0px;">{{ _("Slots") }}</label>
|
||||
<div class="form-group">
|
||||
<div class="clearfix">
|
||||
<label class="control-label reqd slot-label hide" style="padding-right: 0px;">{{ _("Slots") }}</label>
|
||||
</div>
|
||||
<div class="control-input-wrapper">
|
||||
<div class="control-input">
|
||||
<div class="slots"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-input-wrapper">
|
||||
<div class="control-input">
|
||||
<div class="slots"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p id="no-slots-message" class="small text-danger hide"> {{ _("There are no slots available on this day.") }} </p>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="button is-primary" data-course="{{ course.name | urlencode}}" id="submit-slot">
|
||||
{{ _("Submit") }}</div>
|
||||
</div>
|
||||
<p id="no-slots-message" class="small text-danger hide"> {{ _("There are no slots available on this day.") }} </p>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="button is-primary" data-course="{{ course.name | urlencode}}" id="submit-slot">
|
||||
{{ _("Submit") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro Description(course) %}
|
||||
<div class="course-description-section">
|
||||
{{ frappe.utils.md_to_html(course.description) }}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro CourseCreator(course) %}
|
||||
<div class="course-home-headings"> {{ _("Course Creators") }} </div>
|
||||
|
||||
<div class="common-card-style course-creators-card">
|
||||
{% set instructors = get_instructors(course.name) %}
|
||||
{% for instructor in instructors %}
|
||||
<div class="d-flex align-items-center">
|
||||
{{ widgets.Avatar(member=instructor, avatar_class="avatar-medium") }}
|
||||
<div class="ml-4">
|
||||
<div class="course-creator-name"> {{ instructor.full_name }} </div>
|
||||
<div class="course-meta"> {{ get_authored_courses(instructor.name) | length }} {{ _("Courses Created") }} </div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro RelatedCourses(course) %}
|
||||
{% if course.related_courses | length %}
|
||||
<div class="related-courses">
|
||||
<div class="container">
|
||||
<div class="course-home-headings"> {{ _("Other Courses") }} </div>
|
||||
<div class="carousel slide" id="carouselExampleControls" data-ride="carousel" data-interval="false">
|
||||
<div class="carousel-inner">
|
||||
{% for crs in course.related_courses %}
|
||||
{% if loop.index % 3 == 1 %}
|
||||
<div class="carousel-item {% if loop.index == 1 %} active {% endif %}"><div class="cards-parent">
|
||||
{% endif %}
|
||||
{{ widgets.CourseCard(course=crs, read_only=False) }}
|
||||
{% if loop.index % 3 == 0 or loop.index == course.related_courses | length %} </div> </div> {% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if course.related_courses | length > 3 %}
|
||||
<div class="slider-controls">
|
||||
<a class="carousel-control-prev" href="#carouselExampleControls" role="button" data-slide="prev">
|
||||
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
||||
</a>
|
||||
|
||||
<a class="carousel-control-next" href="#carouselExampleControls" role="button" data-slide="next">
|
||||
<span class="carousel-control-next-icon" aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endif%}
|
||||
{% endmacro %}
|
||||
|
||||
@@ -50,116 +50,144 @@ frappe.ready(() => {
|
||||
select_slot(e);
|
||||
});
|
||||
|
||||
$(".btn-attach").click((e) => {
|
||||
show_upload_modal(e);
|
||||
});
|
||||
|
||||
$(".btn-clear").click((e) => {
|
||||
clear_image(e);
|
||||
});
|
||||
|
||||
$(".btn-tag").click((e) => {
|
||||
add_tag(e);
|
||||
});
|
||||
|
||||
$(".btn-save-course").click((e) => {
|
||||
save_course(e);
|
||||
});
|
||||
|
||||
$(".btn-delete-tag").click((e) => {
|
||||
remove_tag(e);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
var hide_wrapped_mentor_cards = () => {
|
||||
var offset_top_prev;
|
||||
|
||||
$(".member-parent .member-card").each(function () {
|
||||
var offset_top = $(this).offset().top;
|
||||
if (offset_top > offset_top_prev) {
|
||||
$(this).addClass('wrapped').slideUp("fast");
|
||||
const hide_wrapped_mentor_cards = () => {
|
||||
let offset_top_prev;
|
||||
|
||||
$(".member-parent .member-card").each(function () {
|
||||
var offset_top = $(this).offset().top;
|
||||
if (offset_top > offset_top_prev) {
|
||||
$(this).addClass('wrapped').slideUp("fast");
|
||||
}
|
||||
if (!offset_top_prev) {
|
||||
offset_top_prev = offset_top;
|
||||
}
|
||||
});
|
||||
|
||||
if ($(".wrapped").length < 1) {
|
||||
$(".view-all-mentors").hide();
|
||||
}
|
||||
if (!offset_top_prev) {
|
||||
offset_top_prev = offset_top;
|
||||
};
|
||||
|
||||
|
||||
const cancel_mentor_request = (e) => {
|
||||
e.preventDefault();
|
||||
frappe.call({
|
||||
"method": "lms.lms.doctype.lms_mentor_request.lms_mentor_request.cancel_request",
|
||||
"args": {
|
||||
"course": decodeURIComponent($(e.currentTarget).attr("data-course"))
|
||||
},
|
||||
"callback": (data) => {
|
||||
if (data.message == "OK") {
|
||||
$("#mentor-request").removeClass("hide");
|
||||
$("#already-applied").addClass("hide")
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const view_all_mentors = (e) => {
|
||||
$(".wrapped").each((i, element) => {
|
||||
$(element).slideToggle("slow");
|
||||
})
|
||||
var text_element = $(".view-all-mentors .course-instructor .all-mentors-text");
|
||||
var text = text_element.text() == "View all mentors" ? "View less" : "View all mentors";
|
||||
text_element.text(text);
|
||||
|
||||
if ($(".mentor-icon").css("transform") == "none") {
|
||||
$(".mentor-icon").css("transform", "rotate(180deg)");
|
||||
} else {
|
||||
$(".mentor-icon").css("transform", "");
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
if ($(".wrapped").length < 1) {
|
||||
$(".view-all-mentors").hide();
|
||||
}
|
||||
}
|
||||
const show_review_dialog = (e) => {
|
||||
e.preventDefault();
|
||||
$("#review-modal").modal("show");
|
||||
};
|
||||
|
||||
var cancel_mentor_request = (e) => {
|
||||
e.preventDefault()
|
||||
frappe.call({
|
||||
"method": "lms.lms.doctype.lms_mentor_request.lms_mentor_request.cancel_request",
|
||||
"args": {
|
||||
"course": decodeURIComponent($(e.currentTarget).attr("data-course"))
|
||||
},
|
||||
"callback": (data) => {
|
||||
if (data.message == "OK") {
|
||||
$("#mentor-request").removeClass("hide");
|
||||
$("#already-applied").addClass("hide")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var view_all_mentors = (e) => {
|
||||
$(".wrapped").each((i, element) => {
|
||||
$(element).slideToggle("slow");
|
||||
})
|
||||
var text_element = $(".view-all-mentors .course-instructor .all-mentors-text");
|
||||
var text = text_element.text() == "View all mentors" ? "View less" : "View all mentors";
|
||||
text_element.text(text);
|
||||
const highlight_rating = (e) => {
|
||||
var rating = $(e.currentTarget).attr("data-rating");
|
||||
$(".icon-rating").removeClass("star-click");
|
||||
$(".icon-rating").each((i, elem) => {
|
||||
if (i <= rating-1) {
|
||||
$(elem).addClass("star-click");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if ($(".mentor-icon").css("transform") == "none") {
|
||||
$(".mentor-icon").css("transform", "rotate(180deg)");
|
||||
} else {
|
||||
$(".mentor-icon").css("transform", "");
|
||||
}
|
||||
}
|
||||
|
||||
var show_review_dialog = (e) => {
|
||||
e.preventDefault();
|
||||
$("#review-modal").modal("show");
|
||||
}
|
||||
|
||||
var highlight_rating = (e) => {
|
||||
var rating = $(e.currentTarget).attr("data-rating");
|
||||
$(".icon-rating").removeClass("star-click");
|
||||
$(".icon-rating").each((i, elem) => {
|
||||
if (i <= rating-1) {
|
||||
$(elem).addClass("star-click");
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var submit_review = (e) => {
|
||||
e.preventDefault();
|
||||
var rating = $(".rating-field").children(".star-click").length;
|
||||
var review = $(".review-field").val();
|
||||
if (!rating) {
|
||||
$(".error-field").text("Please provide a rating.");
|
||||
return;
|
||||
}
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_course_review.lms_course_review.submit_review",
|
||||
args: {
|
||||
"rating": rating,
|
||||
"review": review,
|
||||
"course": decodeURIComponent($(e.currentTarget).attr("data-course"))
|
||||
},
|
||||
callback: (data) => {
|
||||
if (data.message == "OK") {
|
||||
$(".review-modal").modal("hide");
|
||||
window.location.reload();
|
||||
}
|
||||
e.preventDefault();
|
||||
var rating = $(".rating-field").children(".star-click").length;
|
||||
var review = $(".review-field").val();
|
||||
if (!rating) {
|
||||
$(".error-field").text("Please provide a rating.");
|
||||
return;
|
||||
}
|
||||
})
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_course_review.lms_course_review.submit_review",
|
||||
args: {
|
||||
"rating": rating,
|
||||
"review": review,
|
||||
"course": decodeURIComponent($(e.currentTarget).attr("data-course"))
|
||||
},
|
||||
callback: (data) => {
|
||||
if (data.message == "OK") {
|
||||
$(".review-modal").modal("hide");
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const create_certificate = (e) => {
|
||||
e.preventDefault();
|
||||
course = $(e.currentTarget).attr("data-course");
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_certificate.lms_certificate.create_certificate",
|
||||
args: {
|
||||
"course": course
|
||||
},
|
||||
callback: (data) => {
|
||||
window.location.href = `/courses/${course}/${data.message.name}`;
|
||||
}
|
||||
})
|
||||
e.preventDefault();
|
||||
course = $(e.currentTarget).attr("data-course");
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_certificate.lms_certificate.create_certificate",
|
||||
args: {
|
||||
"course": course
|
||||
},
|
||||
callback: (data) => {
|
||||
window.location.href = `/courses/${course}/${data.message.name}`;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const element_not_in_viewport = (el) => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
return rect.bottom < 0 || rect.right < 0 || rect.left > window.innerWidth || rect.top > window.innerHeight;
|
||||
const rect = el.getBoundingClientRect();
|
||||
return rect.bottom < 0 || rect.right < 0 || rect.left > window.innerWidth || rect.top > window.innerHeight;
|
||||
};
|
||||
|
||||
|
||||
const submit_for_review = (e) => {
|
||||
let course = $(e.currentTarget).data("course");
|
||||
frappe.call({
|
||||
@@ -184,12 +212,12 @@ const submit_for_review = (e) => {
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const apply_cetificate = (e) => {
|
||||
$("#slot-modal").modal("show");
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
const submit_slot = (e) => {
|
||||
e.preventDefault();
|
||||
const slot = window.selected_slot;
|
||||
@@ -215,6 +243,7 @@ const submit_slot = (e) => {
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const display_slots = (e) => {
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.course_evaluator.course_evaluator.get_schedule",
|
||||
@@ -249,18 +278,81 @@ const display_slots = (e) => {
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const select_slot = (e) => {
|
||||
$(".slot").removeClass("btn-outline-primary");
|
||||
$(e.currentTarget).addClass("btn-outline-primary");
|
||||
window.selected_slot = $(e.currentTarget);
|
||||
};
|
||||
|
||||
|
||||
const format_time = (time) => {
|
||||
let date = moment(new Date()).format("ddd MMM DD YYYY");
|
||||
return moment(`${date} ${time}`).format("HH:mm a");
|
||||
};
|
||||
|
||||
|
||||
const close_slot_modal = (e) => {
|
||||
$("#slot-date").val("");
|
||||
$(".slot-label").addClass("hide");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const show_upload_modal = () => {
|
||||
new frappe.ui.FileUploader({
|
||||
folder: "Home/Attachments",
|
||||
restrictions: {
|
||||
allowed_file_types: ['image/*']
|
||||
},
|
||||
on_success: (file_doc) => {
|
||||
$(".course-image-attachment").removeClass("hide");
|
||||
$(".course-image-attachment a").attr("href", file_doc.file_url).text(file_doc.file_url);
|
||||
$(".btn-attach").addClass("hide");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const clear_image = () => {
|
||||
$(".course-image-attachment").addClass("hide");
|
||||
$(".course-image-attachment a").removeAttr("href");
|
||||
$(".btn-attach").removeClass("hide");
|
||||
};
|
||||
|
||||
|
||||
const add_tag = (e) => {
|
||||
$(`<div class="course-card-pills" contenteditable="true"
|
||||
data-placeholder="${__('Tag')}"></div>`).insertBefore(`.btn-tag`);
|
||||
};
|
||||
|
||||
|
||||
const save_course = (e) => {
|
||||
let tags = $('.course-card-pills').map((i, el) => $(el).text().trim()).get();
|
||||
tags = tags.filter(word => word.trim().length > 0);
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_course.lms_course.save_course",
|
||||
args: {
|
||||
"tags": tags.join(", "),
|
||||
"title": $("#title").text(),
|
||||
"short_introduction": $("#intro").text(),
|
||||
"video_link": $("#video-link").text(),
|
||||
"image": $("#image").attr("href"),
|
||||
"description": $("#description").text(),
|
||||
"course": $("#title").data("course") ? $("#title").data("course") : ""
|
||||
},
|
||||
callback: (data) => {
|
||||
frappe.show_alert({
|
||||
message: __("Saved"),
|
||||
indicator: "green",
|
||||
});
|
||||
setTimeout(() => {
|
||||
window.location.href = `/courses/${data.message}?edit=1`;
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const remove_tag = (e) => {
|
||||
$(e.currentTarget).closest(".course-card-pills").remove();
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import frappe
|
||||
from lms.lms.doctype.lms_settings.lms_settings import check_profile_restriction
|
||||
from lms.lms.utils import get_membership, is_instructor, is_certified, get_evaluation_details
|
||||
from frappe.utils import add_months, getdate
|
||||
from lms.lms.utils import get_membership, is_instructor, is_certified, get_evaluation_details, redirect_to_courses_list
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
@@ -9,15 +8,30 @@ def get_context(context):
|
||||
try:
|
||||
course_name = frappe.form_dict["course"]
|
||||
except KeyError:
|
||||
frappe.local.flags.redirect_location = "/courses"
|
||||
raise frappe.Redirect
|
||||
redirect_to_courses_list()
|
||||
|
||||
if course_name == "new-course":
|
||||
if frappe.session.user == "Guest":
|
||||
redirect_to_courses_list()
|
||||
context.course = frappe._dict()
|
||||
context.course.edit_mode = True
|
||||
context.membership = None
|
||||
else:
|
||||
set_course_context(context, course_name)
|
||||
|
||||
|
||||
def set_course_context(context, course_name):
|
||||
course = frappe.db.get_value("LMS Course", course_name,
|
||||
["name", "title", "image", "short_introduction", "description", "published", "upcoming", "disable_self_learning",
|
||||
"status", "video_link", "enable_certification", "grant_certificate_after", "paid_certificate",
|
||||
"price_certificate", "currency", "max_attempts", "duration"],
|
||||
as_dict=True)
|
||||
|
||||
if frappe.form_dict.get("edit"):
|
||||
if not is_instructor(course.name):
|
||||
redirect_to_courses_list()
|
||||
course.edit_mode = True
|
||||
|
||||
if course is None:
|
||||
frappe.local.flags.redirect_location = "/courses"
|
||||
raise frappe.Redirect
|
||||
@@ -50,11 +64,13 @@ def get_context(context):
|
||||
"keywords": course.title
|
||||
}
|
||||
|
||||
|
||||
def get_user_interest(course):
|
||||
return frappe.db.count("LMS Course Interest", {
|
||||
"course": course,
|
||||
"user": frappe.session.user
|
||||
})
|
||||
|
||||
|
||||
def show_start_learing_cta(course, membership, restriction):
|
||||
return not course.disable_self_learning and not membership and not course.upcoming and not restriction.get("restrict") and not is_instructor(course.name)
|
||||
|
||||
@@ -15,16 +15,16 @@
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
|
||||
{% include "lms/templates/search_course/search_course.html" %}
|
||||
|
||||
<div class="course-list">
|
||||
{% set courses = live_courses %}
|
||||
{% set title = _("All Live Courses ({0})").format(courses | length) %}
|
||||
{% set title = _("Live Courses ({0})").format(courses | length) %}
|
||||
{% set classes = "live-courses" %}
|
||||
{% include "lms/templates/course_list.html" %}
|
||||
|
||||
{% set courses = upcoming_courses %}
|
||||
{% set title = _("All Upcoming Courses ({0})").format(courses | length) %}
|
||||
{% set title = _("Upcoming Courses ({0})").format(courses | length) %}
|
||||
{% set classes = "upcoming-courses mt-10" %}
|
||||
{% include "lms/templates/course_list.html" %}
|
||||
</div>
|
||||
|
||||
@@ -2,48 +2,48 @@
|
||||
{% block title %}{{ _("Dashboard")}}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
{% set portal_course_creation = frappe.db.get_single_value("LMS Settings", "portal_course_creation") %}
|
||||
{% set show_creators_section = portal_course_creation == "Anyone" or has_course_instructor_role() %}
|
||||
|
||||
<div class="common-page-style dashboard">
|
||||
<div class="container">
|
||||
{% if portal_course_creation %}
|
||||
<a class="button is-default button-links pull-right hide" id="create-course-link" href="/course">
|
||||
<svg class="icon icon-sm mr-1"><use href="#icon-add"></use></svg>
|
||||
{{ _("New Course")}}
|
||||
</a>
|
||||
{% endif %}
|
||||
<ul class="nav" id="courses-tab">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-toggle="tab" href="#courses-enrolled"> {{ _("Courses Enrolled") }} </a>
|
||||
</li>
|
||||
{% if portal_course_creation %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#courses-created">{{ _("Courses Created") }}
|
||||
<div class="container">
|
||||
|
||||
{% if show_creators_section %}
|
||||
<a class="btn btn-secondary btn-md pull-right" id="create-course-link" href="/courses/new-course">
|
||||
{{ _("Create a Course")}}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<div class="border-bottom mb-4"></div>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="courses-enrolled" role="tabpanel" aria-labelledby="courses-enrolled">
|
||||
{% include "lms/lms/web_template/courses_enrolled/courses_enrolled.html" %}
|
||||
</div>
|
||||
{% if portal_course_creation %}
|
||||
<div class="tab-pane fade" id="courses-created" role="tabpanel" aria-labelledby="courses-created">
|
||||
{% include "lms/templates/courses_created.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<ul class="nav" id="courses-tab">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-toggle="tab" href="#courses-enrolled"> {{ _("Courses Enrolled") }} </a>
|
||||
</li>
|
||||
{% if show_creators_section %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#courses-created">{{ _("Courses Created") }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
<div class="border-bottom mb-4"></div>
|
||||
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="courses-enrolled" role="tabpanel" aria-labelledby="courses-enrolled">
|
||||
{% include "lms/lms/web_template/courses_enrolled/courses_enrolled.html" %}
|
||||
</div>
|
||||
{% if show_creators_section %}
|
||||
<div class="tab-pane fade" id="courses-created" role="tabpanel" aria-labelledby="courses-created">
|
||||
{% include "lms/templates/courses_created.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
|
||||
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
|
||||
<script>
|
||||
frappe.ready(() => {
|
||||
$('#courses-tab a[data-toggle="tab"]').on('shown.bs.tab', (e) => {
|
||||
let link = $("#create-course-link");
|
||||
$(e.currentTarget).attr("href") == "#courses-created" ? link.removeClass("hide") : link.addClass("hide");
|
||||
});
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user