feat: new course ui

This commit is contained in:
Jannat Patel
2022-07-28 18:22:57 +05:30
parent f0c89cbbba
commit 6dd12e111d
10 changed files with 421 additions and 193 deletions

View File

@@ -190,7 +190,8 @@ jinja = {
"lms.lms.utils.convert_number_to_character",
"lms.lms.utils.get_signup_optin_checks",
"lms.lms.utils.get_popular_courses",
"lms.lms.utils.format_amount"
"lms.lms.utils.format_amount",
"lms.lms.utils.first_lesson_exists"
],
"filters": []
}

View File

@@ -1,5 +1,6 @@
{
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "format: JOB-{#####}",
"creation": "2022-02-07 12:01:41.074418",
@@ -13,7 +14,7 @@
"column_break_5",
"type",
"status",
"section_break_6",
"section_break_6",``
"description",
"company_details_section",
"company_name",
@@ -113,7 +114,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-02-24 12:37:45.666484",
"modified": "2022-07-28 13:41:29.224332",
"modified_by": "Administrator",
"module": "Job",
"name": "Job Opportunity",

View File

@@ -2,6 +2,7 @@
# For license information, please see license.txt
from __future__ import unicode_literals
from codecs import ignore_errors
import frappe
from frappe.model.document import Document
import json
@@ -203,3 +204,47 @@ def submit_for_review(course):
return "No Chp"
frappe.db.set_value("LMS Course", course, "status", "Under Review")
return "OK"
@frappe.whitelist()
def save_course(tags, title, short_introduction, video_link, image, description, course):
if course:
doc = frappe.get_doc("LMS Course", course)
else:
doc = frappe.get_doc({
"doctype": "LMS Course"
})
doc.update({
"title": title,
"short_introduction": short_introduction,
"video_link": video_link,
"image": image,
"description": description,
"tags": tags
})
doc.save(ignore_permissions=True)
return doc.name
@frappe.whitelist()
def save_chapter(course, chapter, chapter_description, idx):
chapter = frappe.get_doc({
"doctype": "Course Chapter",
"course": course,
"title": chapter,
"description": chapter_description
})
chapter.save(ignore_permissions=True)
chapter_reference = frappe.get_doc({
"doctype": "Chapter Reference",
"parent": course,
"chapter": chapter.name,
"parenttype": "LMS Course",
"parentfield": "chapters",
"idx": idx
})
chapter_reference.save(ignore_permissions=True)
return chapter.name

View File

@@ -53,6 +53,8 @@ def get_membership(course, member, batch=None):
def get_chapters(course):
"""Returns all chapters of this course.
"""
if not course:
return []
chapters = frappe.get_all("Chapter Reference", {"parent": course},
["idx", "chapter"], order_by="idx")
for chapter in chapters:
@@ -373,3 +375,15 @@ def format_amount(amount, currency):
return amount
precision = 0 if amount % 1000 == 0 else 1
return _("{0}k").format(fmt_money(amount_reduced, precision, currency))
def first_lesson_exists(course):
first_chapter = frappe.db.get_value("Chapter Reference", {"parent": course, "idx": 1}, "name")
if not first_chapter:
return False
first_lesson = frappe.db.get_value("Lesson Reference", {"parent": first_chapter, "idx": 1}, "name")
if not first_lesson:
return False
return True

View File

@@ -1,109 +1,98 @@
{% if get_chapters(course.name) | length %}
<div class="course-home-outline">
<div class="course-home-headings">
{{ _("Course Content") }}
</div>
{% for chapter in get_chapters(course.name) %}
<div class="">
<div class="chapter-title" data-target="#{{ get_slugified_chapter_title(chapter.title) }}"
data-toggle="collapse" aria-expanded="false">
<img class="chapter-icon" src="/assets/lms/icons/chevron-right.svg">
<div>{{ chapter.title }}</div>
<div class="course-home-headings">
{{ _("Course Content") }}
</div>
<div class="chapter-content collapse navbar-collapse" id="{{ get_slugified_chapter_title(chapter.title) }}">
{% if chapter.description %}
<div class="chapter-description muted-text">
{{ chapter.description }}
{% for chapter in get_chapters(course.name) %}
<div class="">
<div class="chapter-title" data-target="#{{ get_slugified_chapter_title(chapter.title) }}"
data-toggle="collapse" aria-expanded="false">
{% if not course.edit_mode %}
<img class="chapter-icon" src="/assets/lms/icons/chevron-right.svg">
{% endif %}
<div class="w-100 chapter-title-main" {% if course.edit_mode %} contenteditable="true" {% endif %} >{{ chapter.title }}</div>
</div>
{% endif %}
{% set is_instructor = is_instructor(course.name) %}
<div class="lessons">
<div class="chapter-content collapse navbar-collapse" id="{{ get_slugified_chapter_title(chapter.title) }}">
{% for lesson in get_lessons(course.name, chapter) %}
{% set active = membership.current_lesson == lesson.name %}
<div class="lesson-info {% if active %} active-lesson {% endif %}">
{% if membership or lesson.include_in_preview %}
<a class="lesson-links" href="{{ get_lesson_url(course.name, lesson.number) }}{{course.query_parameter}}"
data-course="{{ course.name }}">
<svg class="icon icon-sm mr-2">
<use class="" href="#{{ lesson.icon }}">
</svg>
<span>{{ lesson.title }}</span>
{% if membership %}
<svg class="icon icon-sm lesson-progress-tick {{ get_progress(course.name, lesson.name) != 'Complete' and 'hide' }}">
<use class="" href="#icon-green-check">
</svg>
{% endif %}
</a>
{% elif is_instructor and not lesson.include_in_preview %}
<a class="lesson-links"
title="This lesson is not available for preview. As you are the Instructor of the course only you can see it."
href="{{ get_lesson_url(course.name, lesson.number) }}{{course.query_parameter}}"
data-course="{{ course.name }}">
<svg class="icon icon-sm mr-2">
<use class="" href="#icon-lock">
</svg>
<div>{{ lesson.title }}</div>
</a>
{% else %}
<div class="no-preview" title="This lesson is not available for preview" data-course="{{ course.name }}">
<div class="lesson-links">
<svg class="icon icon-sm mr-2">
<use class="" href="#icon-lock-gray">
</svg>
<div>{{ lesson.title }}</div>
</div>
</div>
{% if chapter.description or course.edit_mode %}
<div {% if course.edit_mode %} contenteditable="true" {% endif %} class="chapter-description
{% if not course.edit_mode %} mx-8 mb-2 {% endif %} "
data-placeholder="{{ _('Short Description') }}">{{ chapter.description }}</div>
{% endif %}
</div>
{% endfor %}
{% if course.edit_mode %}
<button class="btn btn-sm btn-secondary d-block btn-save-chapter mt-2 mb-8"> {{ _('Save') }} </button>
{% endif %}
{% set is_instructor = is_instructor(course.name) %}
<div class="lessons">
{% for lesson in get_lessons(course.name, chapter) %}
{% set active = membership.current_lesson == lesson.name %}
<div class="lesson-info {% if active and not course.edit_mode %} active-lesson {% endif %}">
{% if membership or lesson.include_in_preview %}
<a class="lesson-links" href="{{ get_lesson_url(course.name, lesson.number) }}{{course.query_parameter}}"
data-course="{{ course.name }}">
<svg class="icon icon-sm mr-2">
<use class="" href="#{{ lesson.icon }}">
</svg>
<span>{{ lesson.title }}</span>
{% if membership %}
<svg class="icon icon-sm lesson-progress-tick {{ get_progress(course.name, lesson.name) != 'Complete' and 'hide' }}">
<use class="" href="#icon-green-check">
</svg>
{% endif %}
</a>
{% elif is_instructor and not lesson.include_in_preview %}
<a class="lesson-links"
title="This lesson is not available for preview. As you are the Instructor of the course only you can see it."
href="{{ get_lesson_url(course.name, lesson.number) }}{{course.query_parameter}}"
data-course="{{ course.name }}">
<svg class="icon icon-sm mr-2">
<use class="" href="#icon-lock">
</svg>
<div>{{ lesson.title }}</div>
</a>
{% else %}
<div class="no-preview" title="This lesson is not available for preview" data-course="{{ course.name }}">
<div class="lesson-links">
<svg class="icon icon-sm mr-2">
<use class="" href="#icon-lock-gray">
</svg>
<div>{{ lesson.title }}</div>
</div>
</div>
{% endif %}
</div>
{% endfor %}
</div>
</div>
</div>
</div>
{% endfor %}
{% endfor %}
<div>
<button class="btn btn-md btn-secondary btn-chapter" data-index=" {{ chapters | length + 1 }} "> {{ _("Add Chapter") }} </button>
</div>
</div>
<!-- No Preview Modal -->
<div class="modal fade no-preview-modal" id="no-preview-modal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<div class="font-weight-bold">{{ _("Not available for preview") }}</div>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
{% if is_user_interested %}
<div class=""> {{ _("You have opted to be notified for this course. You will receive an email when the course becomes available.") }} </div>
{% else %}
<div class=""> {{ _("This lesson is not available for preview. Please join the course to access it.") }} </div>
{% endif %}
</div>
{% if not is_user_interested %}
<div class="modal-footer">
{% if course.upcoming %}
<div class="button is-primary notify-me" data-course="{{course.name | urlencode}}">
{{ _("Notify me when available") }}
</div>
{% else %}
<div class="button is-primary join-batch" data-course="{{ course.name | urlencode}}">
{{ _("Start Learning") }}</div>
{% endif %}
</div>
{% endif %}
</div>
{{ widgets.NoPreviewModal(course=course) }}
{% elif course.edit_mode and course.name %}
<div class="course-home-outline">
<div class="course-home-headings">
{{ _("Course Content") }}
</div>
<div>
<button class="btn btn-md btn-secondary btn-chapter" data-index="1"> {{ _("Add Chapter") }} </button>
</div>
</div>
{% endif %}

View File

@@ -0,0 +1,32 @@
<div class="modal fade no-preview-modal" id="no-preview-modal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<div class="font-weight-bold">{{ _("Not available for preview") }}</div>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
{% if is_user_interested %}
<div class=""> {{ _("You have opted to be notified for this course. You will receive an email when the course becomes available.") }} </div>
{% else %}
<div class=""> {{ _("This lesson is not available for preview. Please join the course to access it.") }} </div>
{% endif %}
</div>
{% if not is_user_interested %}
<div class="modal-footer">
{% if course.upcoming %}
<div class="button is-primary notify-me" data-course="{{course.name | urlencode}}">
{{ _("Notify me when available") }}
</div>
{% else %}
<div class="button is-primary join-batch" data-course="{{ course.name | urlencode}}">
{{ _("Start Learning") }}</div>
{% endif %}
</div>
{% endif %}
</div>
</div>
</div>

View File

@@ -376,16 +376,17 @@ input[type=checkbox] {
}
.chapter-title {
cursor: pointer;
border-radius: var(--border-radius-lg);
padding: 0.5rem 0;
color: var(--gray-900);
display: flex;
align-items: center;
cursor: pointer;
border-radius: var(--border-radius-lg);
padding: 0.5rem 0;
color: var(--gray-900);
display: flex;
align-items: center;
}
.chapter-description {
margin: 0 2rem 0.5rem;
color: var(--gray-900);
font-size: var(--text-sm);
}
.course-content-parent .chapter-description {
@@ -1500,3 +1501,30 @@ li {
margin-top: 1rem;
}
}
[contenteditable] {
border: 1px solid var(--gray-400);
padding: 0.5rem;
border-radius: var(--border-radius);
}
[contenteditable] {
outline: none;
}
[contenteditable]:empty:before{
content: attr(data-placeholder);
color: var(--gray-600);
}
.course-image-attachment {
margin-top: 1rem;
border: 1px solid var(--gray-400);
border-radius: var(--border-radius);
padding: 0.25rem 0.5rem;
width: fit-content;
}
.btn-delete-tag {
cursor: pointer;
}

View File

@@ -1,5 +1,7 @@
frappe.ready(() => {
setup_vue_and_file_size();
hide_wrapped_mentor_cards();
$("#cancel-request").click((e) => {
@@ -50,116 +52,165 @@ 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 setup_vue_and_file_size = () => {
frappe.require("/assets/frappe/node_modules/vue/dist/vue.js", () => {
Vue.prototype.__ = window.__;
Vue.prototype.frappe = window.frappe;
});
frappe.provide("frappe.form.formatters");
frappe.form.formatters.FileSize = file_size;
};
const file_size = (value) => {
if(value > 1048576) {
value = flt(flt(value) / 1048576, 1) + "M";
} else if (value > 1024) {
value = flt(flt(value) / 1024, 1) + "K";
}
if (!offset_top_prev) {
offset_top_prev = offset_top;
return value;
};
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 ($(".wrapped").length < 1) {
$(".view-all-mentors").hide();
}
}
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")
}
}
});
};
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")
}
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", "");
}
})
}
};
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);
if ($(".mentor-icon").css("transform") == "none") {
$(".mentor-icon").css("transform", "rotate(180deg)");
} else {
$(".mentor-icon").css("transform", "");
}
}
const show_review_dialog = (e) => {
e.preventDefault();
$("#review-modal").modal("show");
};
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");
}
})
}
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");
}
});
};
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 +235,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 +266,7 @@ const submit_slot = (e) => {
});
};
const display_slots = (e) => {
frappe.call({
method: "lms.lms.doctype.course_evaluator.course_evaluator.get_schedule",
@@ -249,18 +301,73 @@ 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="${__('Tags')}"></div>`).insertBefore(`.btn-tag`);
};
const save_course = (e) => {
let course = $("#title").data("course");
frappe.call({
method: "lms.lms.doctype.lms_course.lms_course.save_course",
args: {
"tags": $('.course-card-pills').map((i, el) => $(el).text().trim()).get().join(", "),
"title": $("#title").text(),
"short_introduction": $("#intro").text(),
"video_link": $("#video-link").text(),
"image": $("#image").attr("href"),
"description": $("#description").text(),
"course": course ? course : ""
},
callback: (data) => {
window.location.href = `/courses/${data.message}?edit=1`;
}
});
};
const remove_tag = (e) => {
$(e.currentTarget).closest(".course-card-pills").remove();
};

View File

@@ -1,17 +1,25 @@
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
def get_context(context):
context.no_cache = 1
try:
print(frappe.form_dict)
course_name = frappe.form_dict["course"]
except KeyError:
frappe.local.flags.redirect_location = "/courses"
raise frappe.Redirect
if course_name == "new-course":
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",
@@ -43,6 +51,9 @@ def get_context(context):
if context.course.upcoming:
context.is_user_interested = get_user_interest(context.course.name)
if frappe.form_dict.get("edit"):
context.course.edit_mode = True
context.metatags = {
"title": course.title,
"image": course.image,

View File

@@ -7,7 +7,7 @@
<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">
<a class="button is-default button-links pull-right hide" id="create-course-link" href="/courses/new-course">
<svg class="icon icon-sm mr-1"><use href="#icon-add"></use></svg>
{{ _("New Course")}}
</a>