chore: fixed conflicts

This commit is contained in:
Jannat Patel
2023-05-15 19:44:50 +05:30
49 changed files with 2345 additions and 1203 deletions

View File

@@ -5,39 +5,50 @@ describe("Course Creation", () => {
// Create a course
cy.get("a.btn").contains("Create a Course").click();
cy.wait(1000);
cy.url().should("include", "/courses/new-course");
cy.button("Add Tag").click();
cy.get(".course-card-pills").type("Test");
cy.url().should("include", "/courses/new-course/edit");
cy.get("#title").type("Test Course");
cy.get("#intro").type("Test Course Short Introduction");
cy.get("#video-link").type("-LPmw2Znl2c");
cy.get("#published").check();
cy.get("#description").type("Test Course Description");
cy.get("#video-link").type("-LPmw2Znl2c");
cy.get("#tags-input").type("Test");
cy.get("#published").check();
cy.wait(1000);
cy.button("Save Course Details").click();
cy.button("Save").click();
// Add Chapter
cy.wait(3000);
cy.button("New Chapter").click();
cy.get(".new-chapter .chapter-title-main").type("Test Chapter");
cy.get(".new-chapter .chapter-description").type(
"Test Chapter Description"
);
cy.get(".new-chapter .btn-save-chapter").click();
cy.wait(1000);
cy.link("Course Outline").click();
cy.wait(1000);
cy.get(".edit-header .btn-add-chapter").click();
cy.get("#chapter-title").type("Test Chapter");
cy.get("#chapter-description").type("Test Chapter Description");
cy.button("Save").click();
// Add Lesson
cy.wait(3000);
cy.get(".chapter-parent .btn-lesson").click();
cy.wait(3000);
cy.get("#title").type("Test Lesson");
cy.get("#youtube").type("GoDtyItReto");
cy.get("#body").type("Test Lesson Content");
cy.wait(1000);
cy.get(".btn-lesson").click();
cy.link("Add Lesson").click();
cy.wait(1000);
cy.get("#lesson-title").type("Test Lesson");
// Content
cy.get(".ce-block").click().type("{enter}");
cy.get(".ce-toolbar__plus").click();
cy.get('[data-item-name="youtube"]').click();
cy.get('input[data-fieldname="youtube"]').type("GoDtyItReto");
cy.button("Insert").click();
cy.wait(1000);
cy.get(".ce-block:last").click().type("{enter}");
cy.get(".ce-block:last")
.click()
.type(
"This is an extremely big paragraph that is meant to test the UI. This is a very long paragraph. It contains more than once sentence. Its meant to be this long as this is a UI test. Its unbearably long and I'm not sure why I'm typing this much. I'm just going to keep typing until I feel like its long enough. I think its long enough now. I'm going to stop typing now."
);
cy.button("Save").click();
// View Course
cy.wait(3000);
cy.wait(1000);
cy.visit("/courses");
cy.get(".course-card-title:first").contains("Test Course");
cy.get(".course-card:first").click();
@@ -59,7 +70,7 @@ describe("Course Creation", () => {
cy.get(".lesson-info:first").click();
// View Lesson
cy.wait(3000);
cy.wait(1000);
cy.url().should("include", "learn/1.1");
cy.get("#title").contains("Test Lesson");
cy.get(".lesson-video iframe").should(
@@ -67,7 +78,9 @@ describe("Course Creation", () => {
"src",
"https://www.youtube.com/embed/GoDtyItReto"
);
cy.get(".lesson-content-card").contains("Test Lesson Content");
cy.get(".lesson-content-card").contains(
"This is an extremely big paragraph that is meant to test the UI. This is a very long paragraph. It contains more than once sentence. Its meant to be this long as this is a UI test. Its unbearably long and I'm not sure why I'm typing this much. I'm just going to keep typing until I feel like its long enough. I think its long enough now. I'm going to stop typing now."
);
// Add Discussion
cy.get(".reply").click();
@@ -79,7 +92,7 @@ describe("Course Creation", () => {
cy.get(".submit-discussion").click();
// View Discussion
cy.wait(3000);
cy.wait(1000);
cy.get(".discussion-topic-title:first").contains("Question Title");
cy.get(".sidebar-parent:first").click();
cy.get(".reply-text").contains(

View File

@@ -138,12 +138,18 @@ fixtures = ["Custom Field", "Function", "Industry"]
website_route_rules = [
{"from_route": "/sketches/<sketch>", "to_route": "sketches/sketch"},
{"from_route": "/courses/<course>", "to_route": "courses/course"},
{"from_route": "/courses/<course>/edit", "to_route": "courses/create"},
{"from_route": "/courses/<course>/outline", "to_route": "courses/outline"},
{"from_route": "/courses/<course>/<certificate>", "to_route": "courses/certificate"},
{"from_route": "/courses/<course>/learn", "to_route": "batch/learn"},
{
"from_route": "/courses/<course>/learn/<int:chapter>.<int:lesson>",
"to_route": "batch/learn",
},
{
"from_route": "/courses/<course>/learn/<int:chapter>.<int:lesson>/edit",
"to_route": "batch/edit",
},
{"from_route": "/quizzes", "to_route": "batch/quiz_list"},
{"from_route": "/quizzes/<quizname>", "to_route": "batch/quiz"},
{"from_route": "/classes/<classname>", "to_route": "classes/class"},

View File

@@ -3,7 +3,9 @@
# import frappe
from frappe.model.document import Document
from frappe.utils.telemetry import capture
class CourseChapter(Document):
pass
def after_insert(self):
capture("chapter_created", "lms")

View File

@@ -4,9 +4,8 @@
import frappe
from frappe import _
from frappe.model.document import Document
from lms.lms.utils import get_course_progress, get_lesson_url
from frappe.utils.telemetry import capture
from lms.lms.utils import get_course_progress
from ...md import find_macros
@@ -24,6 +23,9 @@ class CourseLesson(Document):
for section in dynamic_documents:
self.update_lesson_name_in_document(section)
def after_insert(self):
capture("lesson_created", "lms")
def update_lesson_name_in_document(self, section):
doctype_map = {"Exercise": "LMS Exercise", "Quiz": "LMS Quiz"}
macros = find_macros(self.body)

View File

@@ -9,7 +9,10 @@ from lms.lms.utils import is_certified
class LMSCertificate(Document):
def before_insert(self):
def validate(self):
self.validate_duplicate_certificate()
def validate_duplicate_certificate(self):
certificates = frappe.get_all(
"LMS Certificate", {"member": self.member, "course": self.course}
)
@@ -20,6 +23,18 @@ class LMSCertificate(Document):
_("{0} is already certified for the course {1}").format(full_name, course_name)
)
def after_insert(self):
share = frappe.get_doc(
{
"doctype": "DocShare",
"read": 1,
"share_doctype": "LMS Certificate",
"share_name": self.name,
"user": self.member,
}
)
share.save(ignore_permissions=True)
@frappe.whitelist()
def create_certificate(course):

View File

@@ -14,8 +14,10 @@
"paid_class",
"column_break_4",
"seat_count",
"description",
"start_time",
"end_time",
"section_break_6",
"description",
"students",
"courses",
"custom_component"
@@ -84,11 +86,21 @@
"fieldname": "seat_count",
"fieldtype": "Int",
"label": "Seat Count"
},
{
"fieldname": "start_time",
"fieldtype": "Time",
"label": "Start Time"
},
{
"fieldname": "end_time",
"fieldtype": "Time",
"label": "End Time"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-05-03 17:52:28.226169",
"modified": "2023-05-03 23:07:06.725720",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Class",

View File

@@ -12,6 +12,8 @@ import json
class LMSClass(Document):
def validate(self):
if self.seat_count:
self.validate_seats_left()
self.validate_duplicate_students()
self.validate_membership()
@@ -36,6 +38,10 @@ class LMSClass(Document):
if not frappe.db.exists(filters):
frappe.get_doc(filters).save()
def validate_seats_left(self):
if cint(self.seat_count) < len(self.students):
frappe.throw(_("There are no seats available in this class."))
@frappe.whitelist()
def add_student(email, class_name):
@@ -147,7 +153,16 @@ def authenticate():
@frappe.whitelist()
def create_class(title, start_date, end_date, description=None, name=None):
def create_class(
title,
start_date,
end_date,
description=None,
seat_count=0,
start_time=None,
end_time=None,
name=None,
):
if name:
class_details = frappe.get_doc("LMS Class", name)
else:
@@ -159,6 +174,9 @@ def create_class(title, start_date, end_date, description=None, name=None):
"start_date": start_date,
"end_date": end_date,
"description": description,
"seat_count": seat_count,
"start_time": start_time,
"end_time": end_time,
}
)
class_details.save()

View File

@@ -57,7 +57,7 @@
},
{
"fieldname": "description",
"fieldtype": "Markdown Editor",
"fieldtype": "Text Editor",
"label": "Description",
"reqd": 1
},
@@ -260,7 +260,7 @@
}
],
"make_attachments_public": 1,
"modified": "2023-05-02 09:45:54.826328",
"modified": "2023-05-11 17:08:19.763405",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Course",

View File

@@ -2,13 +2,12 @@
# For license information, please see license.txt
import json
import random
import frappe
from frappe.model.document import Document
from frappe.utils import cint
from frappe.utils.telemetry import capture
from lms.lms.utils import get_chapters
from ...utils import generate_slug, validate_image
@@ -43,6 +42,9 @@ class LMSCourse(Document):
if not self.upcoming and self.has_value_changed("upcoming"):
self.send_email_to_interested_users()
def after_insert(self):
capture("course_created", "lms")
def send_email_to_interested_users(self):
interested_users = frappe.get_all(
"LMS Course Interest", {"course": self.name}, ["name", "user"]
@@ -72,7 +74,10 @@ class LMSCourse(Document):
def autoname(self):
if not self.name:
self.name = generate_slug(self.title, "LMS Course")
title = self.title
if self.title == "New Course":
title = self.title + str(random.randint(0, 99))
self.name = generate_slug(title, "LMS Course")
def __repr__(self):
return f"<Course#{self.name}>"

View File

@@ -192,3 +192,10 @@ def check_input_answers(question, answer):
return 1
return 0
@frappe.whitelist()
def get_user_quizzes():
return frappe.get_all(
"LMS Quiz", filters={"owner": frappe.session.user}, fields=["name", "title"]
)

View File

@@ -93,18 +93,24 @@ def get_chapters(course):
return chapters
def get_lessons(course, chapter=None):
def get_lessons(course, chapter=None, get_details=True):
"""If chapter is passed, returns lessons of only that chapter.
Else returns lessons of all chapters of the course"""
lessons = []
lesson_count = 0
if chapter:
return get_lesson_details(chapter)
if get_details:
return get_lesson_details(chapter)
else:
return frappe.db.count("Lesson Reference", {"parent": chapter.name})
for chapter in get_chapters(course):
lesson = get_lesson_details(chapter)
lessons += lesson
if get_details:
lessons += get_lesson_details(chapter)
else:
lesson_count += frappe.db.count("Lesson Reference", {"parent": chapter.name})
return lessons
return lessons if get_details else lesson_count
def get_lesson_details(chapter):
@@ -135,8 +141,8 @@ def get_lesson_details(chapter):
macros = find_macros(lesson_details.body)
for macro in macros:
if macro[0] == "YouTubeVideo":
lesson_details.icon = "icon-video"
if macro[0] == "YouTubeVideo" or macro[0] == "Video":
lesson_details.icon = "icon-youtube"
elif macro[0] == "Quiz":
lesson_details.icon = "icon-quiz"
lessons.append(lesson_details)
@@ -495,12 +501,17 @@ def can_create_courses(member=None):
if not member:
member = frappe.session.user
if frappe.session.user == "Guest":
return False
if has_course_instructor_role(member) or has_course_moderator_role(member):
return True
portal_course_creation = frappe.db.get_single_value(
"LMS Settings", "portal_course_creation"
)
return frappe.session.user != "Guest" and (
portal_course_creation == "Anyone" or has_course_instructor_role(member)
)
return portal_course_creation == "Anyone"
def has_course_moderator_role(member=None):
@@ -611,15 +622,17 @@ def get_filtered_membership(course, memberships):
def show_start_learing_cta(course, membership):
return (
not course.disable_self_learning
and not membership
and not course.upcoming
and not check_profile_restriction()
and not is_instructor(course.name)
and course.status == "Approved"
and has_lessons(course)
)
if course.disable_self_learning or course.upcoming:
return False
if is_instructor(course.name):
return False
if course.status != "Approved":
return False
if not has_lessons(course):
return False
if not membership:
return True
def has_lessons(course):

View File

@@ -1,81 +1,66 @@
{% set chapters = get_chapters(course.name) %}
{% set is_instructor = is_instructor(course.name) %}
{% if course.edit_mode or chapters | length %}
{% if chapters | length %}
<div class="course-home-outline">
{% if course.edit_mode and course.name %}
<button class="btn btn-sm btn-secondary btn-chapter pull-right"> {{ _("New Chapter") }} </button>
{% endif %}
{% if course.name and (course.edit_mode or chapters | length) %}
<div class="course-home-headings" id="outline-heading" data-course="{{ course.name }}">
{% if not lesson_page %}
<div class="page-title mb-8" id="outline-heading" data-course="{{ course.name }}">
{{ _("Course Content") }}
</div>
{% endif %}
{% if course.edit_mode and course.name and not chapters | length %}
<div class="chapter-parent chapter-edit new-chapter">
<div contenteditable="true" data-placeholder="{{ _('Chapter Name') }}" class="chapter-title-main"></div>
<div class="chapter-description small my-2" contenteditable="true" data-placeholder="{{ _('Short Description') }}"></div>
<button class="btn btn-sm btn-secondary d-block btn-save-chapter" data-index="1"> {{ _('Save') }} </button>
</div>
<!-- <div class="mb-2">
<span>
{{ chapters | length }} chapters
</span>
<span>
. {{ get_lessons(course.name, None, False) }} lessons
</span>
</div> -->
{% endif %}
{% if chapters | length %}
<div class="chapter-dropzone">
<div>
{% for chapter in chapters %}
<div class="chapter-parent {% if course.edit_mode %} chapter-edit {% endif %}" data-chapter="{{ chapter.name }}">
<div class="chapter-title" {% if not course.edit_mode %} data-toggle="collapse" aria-expanded="false"
data-target="#{{ get_slugified_chapter_title(chapter.title) }}" {% endif %} >
{% if not course.edit_mode %}
{% set lessons = get_lessons(course.name, chapter) %}
<div class="chapter-parent" data-chapter="{{ chapter.name }}">
<div class="chapter-title" data-toggle="collapse" aria-expanded="false"
data-target="#{{ get_slugified_chapter_title(chapter.title) }}">
<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 class="chapter-title-main">
{{ chapter.title }}
</div>
<!-- <div class="small ml-auto">
{{ lessons | length }} lessons
</div> -->
</div>
{% set lessons = get_lessons(course.name, chapter) %}
<div class="chapter-content {% if not course.edit_mode %} collapse navbar-collapse {% endif %} "
id="{{ get_slugified_chapter_title(chapter.title) }}">
<div class="chapter-content collapse navbar-collapse" id="{{ get_slugified_chapter_title(chapter.title) }}">
{% 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') }}">{% if chapter.description %}{{ chapter.description }}{% endif %}</div>
{% endif %}
{% if course.edit_mode %}
<div class="mt-2">
<button class="btn btn-sm btn-secondary btn-save-chapter"
data-index="{{ loop.index }}"> {{ _('Save') }} </button>
<a class="btn btn-sm btn-secondary btn-lesson ml-2"
href="/courses/{{ course.name }}/learn/{{loop.index}}.{{ lessons | length + 1 }}?edit=1"> {{ _("New Lesson") }} </a>
{% if chapter.description %}
<div class="chapter-description">
{{ chapter.description }}
</div>
{% endif %}
{% set is_instructor = is_instructor(course.name) %}
{% if course.edit_mode %}
<div class="course-meta mt-8 font-weight-bold"> {{ _("Lessons") }}: </div>
{% endif %}
<div class="lessons {% if course.edit_mode %} lesson-dropzone {% endif %}">
<div class="lessons">
{% if lessons | length %}
{% for lesson in lessons %}
{% set active = membership.current_lesson == lesson.name %}
<div data-lesson="{{ lesson.name }}" class="lesson-info {% if active and not course.edit_mode %} active-lesson {% endif %}">
<div data-lesson="{{ lesson.name }}" class="lesson-info {% if active %} active-lesson {% endif %}">
{% if membership or lesson.include_in_preview or is_instructor or has_course_moderator_role() %}
<a class="lesson-links"
<a class="lesson-links" href="{{ get_lesson_url(course.name, lesson.number) }}{{course.query_parameter}}"
{% if is_instructor and not lesson.include_in_preview %}
title="{{ _('This lesson is not available for preview. As you are the Instructor of the course only you can see it.') }}"
{% endif %}
href="{{ get_lesson_url(course.name, lesson.number) }}{% if course.edit_mode and is_instructor %}?edit=1{% endif %}{{course.query_parameter}}">
{% endif %}>
<svg class="icon icon-sm mr-2">
<use class="" href="#{{ lesson.icon }}">
@@ -84,8 +69,8 @@
<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 class="icon icon-md lesson-progress-tick ml-auto {{ get_progress(course.name, lesson.name) != 'Complete' and 'hide' }}">
<use class="" href="#icon-success">
</svg>
{% endif %}

View File

@@ -9,13 +9,14 @@
"label": "Enrollments"
}
],
"content": "[{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Get Started</b></span>\",\"col\":12}},{\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"/courses\\\" draggable=\\\"false\\\">Visit LMS Portal</a>\",\"col\":4}},{\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"/courses/new-course\\\" draggable=\\\"false\\\">Create a Course</a>\",\"col\":4}},{\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"/app/web-page/new-web-page-1\\\">Setup a Home Page</a>\",\"col\":4}},{\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"/app/website-settings/Website%20Settings\\\">Website Settings</a>\",\"col\":4}},{\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"https://docs.frappelms.com\\\">Documentation</a>\",\"col\":4}},{\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"https://frappe.school/courses/introducing-frappe-lms\\\">Video Tutorials</a>\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Signups\",\"col\":6}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Enrollments\",\"col\":6}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span style=\\\"font-size: 18px;\\\"><b>Statistics</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Users\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Course\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Enrollments\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Course Completed\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Certificate\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Evaluation\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Master</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Course Data\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Course Stats\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Certification\",\"col\":4}}]",
"content": "[{\"id\":\"jNO4sdKxHu\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Get Started</b></span>\",\"col\":12}},{\"id\":\"5s0qRBc4rY\",\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"/courses\\\" draggable=\\\"false\\\">Visit LMS Portal</a>\",\"col\":4}},{\"id\":\"lGMuNLpmv-\",\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"/courses/new-course/edit\\\">Create a Course</a>\",\"col\":4}},{\"id\":\"3TVyc9AkPy\",\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"/app/web-page/new-web-page-1\\\">Setup a Home Page</a>\",\"col\":4}},{\"id\":\"9zcbqpu2gm\",\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"/app/lms-settings/LMS%20Settings\\\">LMS Setting</a>\",\"col\":4}},{\"id\":\"0ATmnKmXjc\",\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"https://docs.frappelms.com\\\">Documentation</a>\",\"col\":4}},{\"id\":\"7tGB2TYPmn\",\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"https://frappe.school/courses/introducing-frappe-lms\\\">Video Tutorials</a>\",\"col\":4}},{\"id\":\"C128a4abjX\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"5q4sPiv2ci\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Signups\",\"col\":6}},{\"id\":\"8NSaRaEV5u\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Enrollments\",\"col\":6}},{\"id\":\"kMuzko0uAU\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"iuvIOHmztI\",\"type\":\"header\",\"data\":{\"text\":\"<span style=\\\"font-size: 18px;\\\"><b>Statistics</b></span>\",\"col\":12}},{\"id\":\"l0VTd66Uy2\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Users\",\"col\":4}},{\"id\":\"wAWZin1KKk\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Course\",\"col\":4}},{\"id\":\"RLrIlFx0Hd\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Enrollments\",\"col\":4}},{\"id\":\"OuhWkhCQmq\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Course Completed\",\"col\":4}},{\"id\":\"3g8QmNqUXG\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Certificate\",\"col\":4}},{\"id\":\"EZsdsujs8N\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Evaluation\",\"col\":4}},{\"id\":\"s-nfsFQbGV\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"jeOBWBzHEa\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Master</b></span>\",\"col\":12}},{\"id\":\"sVhgfS5GIh\",\"type\":\"card\",\"data\":{\"card_name\":\"Course Data\",\"col\":4}},{\"id\":\"Iea0snm4Fg\",\"type\":\"card\",\"data\":{\"card_name\":\"Course Stats\",\"col\":4}},{\"id\":\"bZB7RqOl6a\",\"type\":\"card\",\"data\":{\"card_name\":\"Certification\",\"col\":4}}]",
"creation": "2021-10-21 17:20:01.358903",
"docstatus": 0,
"doctype": "Workspace",
"hide_custom": 0,
"icon": "education",
"idx": 0,
"is_hidden": 0,
"label": "LMS",
"links": [
{
@@ -143,10 +144,11 @@
"type": "Link"
}
],
"modified": "2022-12-28 17:45:18.539185",
"modified": "2023-05-11 15:41:25.514442",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS",
"number_cards": [],
"owner": "Administrator",
"parent_page": "",
"public": 1,

View File

@@ -300,7 +300,7 @@ def on_session_creation(login_manager):
frappe.local.response["home_page"] = "/courses"
@frappe.whitelist(allow_guest=True)
@frappe.whitelist()
def search_users(start=0, text=""):
or_filters = get_or_filters(text)
count = len(get_users(or_filters, 0, 900000000, text))

View File

@@ -53,3 +53,4 @@ lms.patches.v0_0.add_evaluator_to_assignment #09-04-2023
lms.patches.v0_0.share_certificates
execute:frappe.delete_doc("Web Form", "class", ignore_missing=True, force=True)
lms.patches.v0_0.amend_course_and_lesson_editor_fields
lms.patches.v0_0.convert_course_description_to_html #11-05-2023

View File

@@ -1,11 +1,218 @@
:root {
--text-3-5xl: 24px;
--text-3-8xl: 34px;
--text-4xl: 36px;
--text-3-5xl: 24px;
--text-3-8xl: 34px;
--text-4xl: 36px;
--checkbox-gradient: linear-gradient(180deg, #3d4142 -124.51%, var(--primary) 100%);
}
body {
background-color: #FFFFFF;
.nav-link .course-list-count {
border-radius: var(--border-radius-md);
padding: 0 0.3rem;
font-size: var(--text-sm);
border: 1px solid var(--gray-600)
}
.nav-link.active .course-list-count {
border: 1px solid var(--primary-color)
}
.page-title {
font-size: 1.25rem;
font-weight: 600;
color: var(--gray-900);
line-height: 160%;
letter-spacing: 0.005em;
}
.sticky {
position: sticky;
top: -1px;
z-index: 100;
}
.is-pinned {
background: #FFFFFF;
padding: 0.5rem 0;
border-bottom: 1px solid var(--gray-300);
}
.field-parent {
margin-top: 2rem;
}
.frappe-control .ql-editor:not(.read-mode) {
background-color: #FFFFFF;
}
.ql-toolbar.ql-snow, .ql-container.ql-snow {
border: 1px solid var(--gray-300);
}
.rating .icon {
background: var(--gray-200);
border-radius: var(--border-radius-md);
padding: var(--padding-xs);
}
.rating .star-click {
--star-fill: var(--orange-500);
background: var(--gray-200);
border-radius: var(--border-radius-md);
padding: var(--padding-xs);
}
.cta-parent {
display: flex;
margin-bottom: 1rem;
}
.all-cta {
flex: 1
}
.field-label {
color: var(--gray-900);
font-weight: 600;
}
.field-input {
border: 1px solid var(--gray-300);
border-radius: var(--border-radius-md);
padding: 0.5rem;
width: 100%;
margin-top: 0.25rem;
}
.field-input:focus-visible {
outline: none;
}
.field-group {
margin-bottom: 1.5rem;
}
.field-description {
font-size: var(--text-md);
}
.invisible-input {
border: none;
}
.invisible-input:focus-visible {
outline: none;
}
.image-preview {
width: 280px;
height: 178px;
border-radius: var(--border-radius-sm);
border: 1px solid var(--gray-300);
margin-top: 1rem;
}
textarea.field-input {
height: 300px;
}
.outline-lesson {
padding: 0.75rem 0;
border-bottom: 1px solid var(--gray-300);
}
.common-card-style .outline-lesson:last-of-type {
border-bottom: none;
}
.level {
justify-content: start;
}
.icon-bg {
background: var(--gray-100);
padding: 0.5rem;
border-radius: var(--border-radius-md);
margin: 0 0.5rem;
}
.quiz-modal {
min-height: 500px;
}
.ce-block__content {
max-width: 100%;
padding: 0 0.5rem;
margin: 0;
}
.ce-toolbar__content {
position: unset;
}
.lesson-editor {
border: 1px solid var(--gray-300);
border-radius: var(--border-radius-md);
padding-top: 0.5rem;
}
.lesson-parent .breadcrumb {
border-bottom: 1px solid var(--gray-300);
margin-bottom: 2rem;
padding-bottom: 1rem;
}
.form-width {
width: 50%;
}
@media (max-width: 768px) {
.form-width {
width: 75%;
}
}
.clickable {
color: var(--gray-900);
font-weight: 500;
}
.clickable:hover {
color: var(--gray-900);
text-decoration: none;
}
.codex-editor path {
stroke: var(--gray-800);
}
.drag-handle {
cursor: move;
}
.edit-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.btn-default:not(:disabled):not(.disabled).active {
color: white;
background-color: var(--primary);
border: none;
}
.field-label.reqd::after {
content: " *";
color: var(--red-400);
}
.error-message {
color: var(--red-500);
font-size: var(--text-sm);
}
.lessons {
margin-left: 1.5rem;
}
input[type=checkbox] {
@@ -74,7 +281,7 @@ input[type=checkbox] {
font-weight: 600;
color: var(--gray-900);
width: fit-content;
box-shadow: var(--shadow-sm);
border: 1px solid var(--gray-300);
}
.dark-pills {
@@ -87,9 +294,7 @@ input[type=checkbox] {
}
.common-page-style {
padding: 2rem 0 5rem;
padding-top: 3rem;
background-color: var(--bg-color);
padding: 1.25rem 0 5rem;
font-size: var(--text-base);
}
@@ -98,7 +303,7 @@ input[type=checkbox] {
background: #FFFFFF;
border-radius: var(--border-radius-md);
position: relative;
box-shadow: var(--shadow-base);
border: 1px solid var(--gray-300)
}
.course-card {
@@ -199,9 +404,7 @@ input[type=checkbox] {
.cards-parent {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
-moz-column-gap: 40px;
column-gap: 40px;
row-gap: 40px;
grid-gap: 2rem;
align-items: center;
}
@@ -231,12 +434,12 @@ input[type=checkbox] {
}
.button-links {
color: var(--gray-900);
color: inherit;
}
.button-links:hover {
text-decoration: none;
color: var(--gray-900);
color: inherit;
}
.icon-background {
@@ -290,21 +493,21 @@ input[type=checkbox] {
}
.course-card-wide {
width: 50%;
font-size: var(--text-base);
width: 50%;
margin-bottom: 2rem;
}
@media (max-width: 1000px) {
.course-card-wide {
width: 75%;
margin: 0 auto;
margin: 0 auto 2rem;
}
}
@media (max-width: 768px) {
.course-card-wide {
width: 100%;
margin: 0;
margin: 0 0 2rem;
}
}
@@ -342,15 +545,14 @@ input[type=checkbox] {
}
.wide-button {
padding: 0.5rem 6rem;
font-weight: 500;
width: 100%;
padding: 0.3rem 4rem;
width: 100%;
}
@media (max-width: 768px) {
.wide-button {
padding: 0.5rem 4rem;
}
.wide-button {
padding: 0.3rem 4rem;
}
}
.is-secondary {
@@ -388,7 +590,7 @@ input[type=checkbox] {
.course-home-page {
background-color: #FFFFFF;
padding-top: 4rem;
padding-top: 2.5rem;
}
.chapter-title {
@@ -397,12 +599,20 @@ input[type=checkbox] {
color: var(--gray-900);
display: flex;
align-items: center;
padding-bottom: 0.5rem;
padding-bottom: 0.25rem;
padding-right: 0.5rem;
font-size: var(--text-lg);
}
.chapter-title:last-child {
padding-bottom: 0;
}
.chapter-description {
color: var(--gray-900);
font-size: var(--text-sm);
margin-left: 2rem;
margin-bottom: 0.5rem;
}
.course-content-parent .chapter-description {
@@ -414,8 +624,7 @@ input[type=checkbox] {
}
.reviews-parent {
padding-bottom: 5rem;
color: var(--gray-900);
color: var(--gray-900);
}
.lesson-info {
@@ -426,6 +635,7 @@ input[type=checkbox] {
.lesson-links {
display: flex;
align-items: center;
padding: 0.5rem;
color: var(--gray-900);
font-size: var(--text-base);
@@ -438,10 +648,6 @@ input[type=checkbox] {
border-radius: var(--border-radius-md);
}
.lessons {
margin-left: 1.5rem;
}
.member-card {
display: flex;
flex-direction: column;
@@ -580,7 +786,7 @@ input[type=checkbox] {
}
.course-details-outline {
margin-top: 1rem;
margin-top: 2.5rem;
}
.lesson-content {
@@ -589,7 +795,7 @@ input[type=checkbox] {
}
.lesson-content-card {
margin-top: 2rem;
margin-top: 1.5rem;
}
.lesson-content-card .alert-dismissible .close {
@@ -599,7 +805,7 @@ input[type=checkbox] {
.course-content-parent {
display: grid;
grid-gap: 2rem;
grid-template-columns: 2fr minmax(600px, 5fr);
grid-template-columns: 1fr 3fr;
}
@media (max-width: 1024px) {
@@ -610,25 +816,12 @@ input[type=checkbox] {
}
.course-content-parent .course-home-headings {
margin: 0 0 1rem;
margin: 0 0 0.5rem;
width: 100%;
}
.lesson-pagination {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 2rem
}
.lesson-pagination-parent {
margin-top: 1rem;
}
@media (max-width: 768px) {
.lesson-pagination-parent {
margin-left: 0px;
}
margin: 2rem 0;
}
.lesson-video {
@@ -640,12 +833,6 @@ input[type=checkbox] {
border-radius: var(--border-radius-md);
}
.lesson-title {
display: flex;
align-items: center;
margin-bottom: 1rem;
}
.profile-page {
padding-top: 0;
}
@@ -803,8 +990,8 @@ input[type=checkbox] {
}
.progress-percent {
margin: 0.5rem 0;
font-size: var(--text-base);
margin: 0.5rem 0;
font-size: var(--text-sm);
}
pre {
@@ -906,12 +1093,12 @@ pre {
}
.empty-state {
background: var(--gray-200);
border-radius: var(--border-radius-lg);
padding: 4rem;
display: flex;
justify-content: center;
align-items: center;
border: 1px solid var(--gray-300);
border-radius: var(--border-radius-lg);
padding: 4rem;
display: flex;
justify-content: center;
align-items: center;
}
.empty-state-text {
@@ -946,9 +1133,9 @@ pre {
.search-course {
background-position: 1rem;
text-indent: 1rem;
font-size: var(--text-base);
padding: 1.5rem;
width: 100%;
font-size: var(--text-base);
padding: 1.5rem;
width: 100%;
box-shadow: none;
}
@@ -1201,8 +1388,7 @@ pre {
}
.course-head-container {
color: var(--gray-900);
background-color: var(--gray-50);
border-bottom: 1px solid var(--gray-300);
}
.seperator {
@@ -1210,16 +1396,16 @@ pre {
}
.course-overlay-card {
background-color: white;
border-radius: var(--border-radius-lg);
box-shadow: var(--shadow-sm);
overflow: auto;
width: fit-content;
position: absolute;
top: 10%;
right: 7%;
max-width: 400px;
z-index: 4;
background-color: white;
border-radius: var(--border-radius-lg);
border: 1px solid var(--gray-300);
overflow: auto;
width: fit-content;
position: absolute;
top: 10%;
right: 7%;
width: 350px;
z-index: 4;
}
@media (max-width: 1000px) {
@@ -1339,20 +1525,16 @@ pre {
margin: 0 1rem;
}
.avg-rating-stars {
background: var(--gray-200);
border-radius: 100px;
padding: 0.5rem 0.75rem;
margin: 1.25rem 0 0.5rem;
.course-card-wide .avg-rating-stars {
margin-top: 2rem;
}
.reviews-parent .progress {
width: 200px;
color: var(--gray-900);
}
.reviews-parent .progress-bar {
background-color: var(--gray-600);
background-color: var(--primary-color);
}
.course-home-top-container {
@@ -1486,14 +1668,6 @@ li {
margin-bottom: 1.25rem;
}
.course-card-wide .avatar .standard-image {
border: 1px solid var(--gray-400);
}
.lesson-progress-tick {
margin: 0 0.5rem
}
.no-preview {
color: var(--gray-600);
}
@@ -1553,20 +1727,6 @@ li {
}
}
[contenteditable="true"] {
outline: none;
background-color: var(--bg-light-gray);
border-radius: var(--border-radius);
border: 1px solid var(--gray-300);
padding: 0.5rem 0.75rem;
color: var(--gray-900);
}
[contenteditable="true"]:empty:before {
content: attr(data-placeholder);
color: var(--gray-600);
}
.course-image-attachment {
margin-top: 0.25rem;
background-color: var(--bg-light-gray);
@@ -1595,12 +1755,12 @@ li {
margin-bottom: 1rem;
}
.chapter-edit .chapter-title {
padding: 0.5rem 0;
.chapter-parent:last-child {
margin-bottom: 0;
}
.course-card-pills[contenteditable="true"] {
box-shadow: none;
.chapter-edit .chapter-title {
padding: 0.5rem 0;
}
.preview {
@@ -1613,14 +1773,6 @@ li {
margin-bottom: 0;
}
.quiz-card {
border: 1px solid var(--dark-border-color);
border-radius: var(--border-radius);
padding: 1.25rem;
margin-top: 1.25rem;
font-size: var(--text-base);
}
.option-input {
width: 45%;
margin-right: 1rem;
@@ -1789,6 +1941,10 @@ li {
justify-content: flex-end !important;
}
.modal-footer .btn:first-child {
margin-right: 0.5rem;
}
.modal-header .modal-title {
color: var(--gray-900);
line-height: 1.5rem;
@@ -1802,7 +1958,7 @@ li {
}
.course-description-section {
padding-bottom: 4rem;
padding-bottom: 2.5rem;
}
input::file-selector-button {
@@ -1945,11 +2101,11 @@ select {
.result-row {
display: block;
padding: 1rem;
border-top: 1px solid var(--gray-300);
font-weight: 500;
color: var(--gray-900);
font-size: var(--text-base);
cursor: pointer;
border-top: 1px solid var(--gray-300);
font-weight: 500;
color: var(--gray-900);
font-size: var(--text-base);
cursor: pointer;
}
.result-row:hover {
@@ -2019,8 +2175,8 @@ select {
.lms-card-parent {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
grid-gap: 1.5rem;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
grid-gap: 1.5rem;
}
.answer-indicator {
@@ -2036,4 +2192,5 @@ select {
.answer-indicator.failure {
background-color: var(--red-50);
}
}

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" id="icon-quiz" viewBox="0 0 1024 1024" stroke="#1F272E">
<path d="M512 0C229.232 0 0 229.232 0 512c0 282.784 229.232 512 512 512 282.784 0 512.017-229.216 512.017-512C1024.017 229.232 794.785 0 512 0zm0 961.008c-247.024 0-448-201.984-448-449.01 0-247.024 200.976-448 448-448s448.017 200.977 448.017 448S759.025 961.009 512 961.009zm-47.056-160.529h80.512v-81.248h-80.512zm46.112-576.944c-46.88 0-85.503 12.64-115.839 37.889-30.336 25.263-45.088 75.855-44.336 117.775l1.184 2.336h73.44c0-25.008 8.336-60.944 25.008-73.84 16.656-12.88 36.848-19.328 60.56-19.328 27.328 0 48.336 7.424 63.073 22.271 14.72 14.848 22.063 36.08 22.063 63.664 0 23.184-5.44 42.976-16.368 59.376-10.96 16.4-29.328 39.841-55.088 70.322-26.576 23.967-42.992 43.231-49.232 57.807-6.256 14.592-9.504 40.768-9.744 78.512h76.96c0-23.68 1.503-41.136 4.496-52.336 2.975-11.184 11.504-23.823 25.568-37.888 30.224-29.152 54.496-57.664 72.88-85.551 18.336-27.857 27.52-58.593 27.52-92.193 0-46.88-14.176-83.408-42.577-109.568-28.416-26.176-68.272-39.248-119.568-39.248z" fill="#1F272E"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -5,9 +5,39 @@
<svg id="icon-video-blue" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 9C1 4.58172 4.58172 1 9 1C11.1217 1 13.1566 1.84286 14.6569 3.34315C16.1571 4.84343 17 6.87827 17 9C17 13.4182 13.4182 17 9 17C4.58172 17 1 13.4182 1 9ZM8.00636 12.0679L11.8766 9.51133C12.0614 9.40191 12.174 9.2084 12.174 9C12.174 8.79161 12.0614 8.59809 11.8766 8.48867L8.00636 5.932C7.79102 5.78453 7.51 5.75869 7.2694 5.86422C7.0288 5.96977 6.86529 6.1906 6.84063 6.44334V11.5567C6.86529 11.8094 7.0288 12.0302 7.2694 12.1358C7.51 12.2413 7.79102 12.2155 8.00636 12.0679Z" fill="#2D95F0"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" id="icon-quiz" viewBox="0 0 1024 1024" stroke="#1F272E">
<path d="M512 0C229.232 0 0 229.232 0 512c0 282.784 229.232 512 512 512 282.784 0 512.017-229.216 512.017-512C1024.017 229.232 794.785 0 512 0zm0 961.008c-247.024 0-448-201.984-448-449.01 0-247.024 200.976-448 448-448s448.017 200.977 448.017 448S759.025 961.009 512 961.009zm-47.056-160.529h80.512v-81.248h-80.512zm46.112-576.944c-46.88 0-85.503 12.64-115.839 37.889-30.336 25.263-45.088 75.855-44.336 117.775l1.184 2.336h73.44c0-25.008 8.336-60.944 25.008-73.84 16.656-12.88 36.848-19.328 60.56-19.328 27.328 0 48.336 7.424 63.073 22.271 14.72 14.848 22.063 36.08 22.063 63.664 0 23.184-5.44 42.976-16.368 59.376-10.96 16.4-29.328 39.841-55.088 70.322-26.576 23.967-42.992 43.231-49.232 57.807-6.256 14.592-9.504 40.768-9.744 78.512h76.96c0-23.68 1.503-41.136 4.496-52.336 2.975-11.184 11.504-23.823 25.568-37.888 30.224-29.152 54.496-57.664 72.88-85.551 18.336-27.857 27.52-58.593 27.52-92.193 0-46.88-14.176-83.408-42.577-109.568-28.416-26.176-68.272-39.248-119.568-39.248z" fill="#1F272E"/>
<svg width="16" height="16" viewBox="0 0 16 16" id="icon-youtube" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_3613_984)">
<mask id="mask0_3613_984" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
<path d="M16 0H0V16H16V0Z" fill="white"/>
</mask>
<g mask="url(#mask0_3613_984)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.77778 12.1111H13.2222C13.7132 12.1111 14.1111 11.7132 14.1111 11.2222V2.77778C14.1111 2.28686 13.7132 1.88889 13.2222 1.88889H2.77778C2.28686 1.88889 1.88889 2.28686 1.88889 2.77778V11.2222C1.88889 11.7132 2.28686 12.1111 2.77778 12.1111ZM13.2222 13C14.2041 13 15 12.2041 15 11.2222V2.77778C15 1.79594 14.2041 1 13.2222 1H2.77778C1.79594 1 1 1.79594 1 2.77778V11.2222C1 12.2041 1.79594 13 2.77778 13H13.2222ZM5.99989 4.76006C5.99989 4.22072 6.60701 3.90461 7.04886 4.21391L10.328 6.50932C10.7072 6.77472 10.7072 7.33622 10.328 7.60163L7.04887 9.89707C6.60701 10.2063 5.99989 9.89022 5.99989 9.35084V4.76006ZM6.88878 5.18688V8.92409L9.55822 7.05548L6.88878 5.18688ZM5 13.5556C4.75454 13.5556 4.55556 13.7546 4.55556 14C4.55556 14.2454 4.75454 14.4444 5 14.4444H11C11.2454 14.4444 11.4444 14.2454 11.4444 14C11.4444 13.7546 11.2454 13.5556 11 13.5556H5Z" fill="#525252"/>
</g>
</g>
<defs>
<clipPath id="clip0_3613_984">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>
<svg width="16" height="16" id="icon-quiz" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_3613_978)">
<mask id="mask0_3613_978" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
<path d="M16 0H0V16H16V0Z" fill="white"/>
</mask>
<g mask="url(#mask0_3613_978)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.1111 8C14.1111 11.3751 11.3751 14.1111 8 14.1111C4.62492 14.1111 1.88889 11.3751 1.88889 8C1.88889 4.62492 4.62492 1.88889 8 1.88889C11.3751 1.88889 14.1111 4.62492 14.1111 8ZM15 8C15 11.866 11.866 15 8 15C4.13401 15 1 11.866 1 8C1 4.13401 4.13401 1 8 1C11.866 1 15 4.13401 15 8ZM7.37988 9.72462V9.79298H8.39062V9.72462C8.39062 9.4512 8.41992 9.23307 8.47852 9.07031C8.53711 8.90427 8.62826 8.76269 8.75196 8.64551C8.87891 8.52832 9.04329 8.4095 9.24516 8.28907C9.56089 8.09375 9.80987 7.85775 9.99218 7.58106C10.1745 7.30111 10.2656 6.96093 10.2656 6.56055C10.2656 6.17644 10.1745 5.83952 9.99218 5.5498C9.81316 5.26009 9.56089 5.03386 9.23538 4.87109C8.90987 4.70834 8.52734 4.62695 8.08789 4.62695C7.69401 4.62695 7.33268 4.70345 7.0039 4.85644C6.67513 5.00619 6.41146 5.22916 6.21289 5.52539C6.01432 5.81836 5.90852 6.17644 5.89551 6.59961H6.96972C6.986 6.34896 7.04948 6.14551 7.16016 5.98926C7.27084 5.82975 7.40756 5.71257 7.57031 5.6377C7.73633 5.56283 7.90885 5.52539 8.08789 5.52539C8.28972 5.52539 8.47364 5.56771 8.63964 5.65235C8.80892 5.73698 8.94071 5.8558 9.0352 6.00879C9.1328 6.16179 9.1816 6.34244 9.1816 6.55078C9.1816 6.81771 9.11004 7.0472 8.96676 7.23926C8.82356 7.42806 8.64453 7.58594 8.42969 7.71289C8.21159 7.84961 8.02278 7.98958 7.86328 8.13281C7.70703 8.27278 7.58659 8.46322 7.50196 8.7041C7.42057 8.94169 7.37988 9.28187 7.37988 9.72462ZM7.37988 11.8682C7.51986 12.0016 7.69076 12.0684 7.89258 12.0684C8.09765 12.0684 8.26855 12.0016 8.40527 11.8682C8.54524 11.7315 8.61524 11.5638 8.61524 11.3652C8.61524 11.1634 8.54524 10.9957 8.40527 10.8623C8.26855 10.7256 8.09765 10.6572 7.89258 10.6572C7.69076 10.6572 7.51986 10.7256 7.37988 10.8623C7.24316 10.9957 7.17481 11.1634 7.17481 11.3652C7.17481 11.5638 7.24316 11.7315 7.37988 11.8682Z" fill="#525252"/>
</g>
</g>
<defs>
<clipPath id="clip0_3613_978">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>
<svg id="icon-quiz-blue" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1511_749)">
<path d="M512 0C229.232 0 0 229.232 0 512C0 794.784 229.232 1024 512 1024C794.784 1024 1024.02 794.784 1024.02 512C1024.02 229.232 794.785 0 512 0ZM512 961.008C264.976 961.008 64 759.024 64 511.998C64 264.974 264.976 63.998 512 63.998C759.024 63.998 960.017 264.975 960.017 511.998C960.017 759.021 759.025 961.009 512 961.009V961.008ZM464.944 800.479H545.456V719.231H464.944V800.479ZM511.056 223.535C464.176 223.535 425.553 236.175 395.217 261.424C364.881 286.687 350.129 337.279 350.881 379.199L352.065 381.535H425.505C425.505 356.527 433.841 320.591 450.513 307.695C467.169 294.815 487.361 288.367 511.073 288.367C538.401 288.367 559.409 295.791 574.146 310.638C588.866 325.486 596.209 346.718 596.209 374.302C596.209 397.486 590.769 417.278 579.841 433.678C568.881 450.078 550.513 473.519 524.753 504C498.177 527.967 481.761 547.231 475.521 561.807C469.265 576.399 466.017 602.575 465.777 640.319H542.737C542.737 616.639 544.24 599.183 547.233 587.983C550.208 576.799 558.737 564.16 572.801 550.095C603.025 520.943 627.297 492.431 645.681 464.544C664.017 436.687 673.201 405.951 673.201 372.351C673.201 325.471 659.025 288.943 630.624 262.783C602.208 236.607 562.352 223.535 511.056 223.535V223.535Z" fill="#2D95F0" stroke="#2D95F0"/>
@@ -74,4 +104,11 @@
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
<svg width="20" height="20" viewBox="0 0 20 20" id="icon-success" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 18.75C14.8325 18.75 18.75 14.8325 18.75 10C18.75 5.16751 14.8325 1.25 10 1.25C5.16751 1.25 1.25 5.16751 1.25 10C1.25 14.8325 5.16751 18.75 10 18.75ZM13.966 7.48104C14.1856 7.21471 14.1477 6.8208 13.8813 6.60122C13.615 6.38164 13.2211 6.41954 13.0015 6.68587L8.68984 11.9155L7.01289 9.74823C6.80165 9.47524 6.40911 9.42517 6.13611 9.6364C5.86311 9.84764 5.81304 10.2402 6.02428 10.5132L8.18004 13.2993C8.29633 13.4495 8.47467 13.5388 8.66468 13.5417C8.85468 13.5447 9.0357 13.461 9.15658 13.3144L13.966 7.48104Z" fill="#171717"/>
</svg>
<svg width="16" height="16" id="icon-drag" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 3C4 3.82843 4.67157 4.5 5.5 4.5C6.32843 4.5 7 3.82843 7 3C7 2.17157 6.32843 1.5 5.5 1.5C4.67157 1.5 4 2.17157 4 3ZM5.5 9.5C4.67157 9.5 4 8.82843 4 8C4 7.17157 4.67157 6.5 5.5 6.5C6.32843 6.5 7 7.17157 7 8C7 8.82843 6.32843 9.5 5.5 9.5ZM5.5 14.5C4.67157 14.5 4 13.8284 4 13C4 12.1716 4.67157 11.5 5.5 11.5C6.32843 11.5 7 12.1716 7 13C7 13.8284 6.32843 14.5 5.5 14.5ZM9 3C9 3.82843 9.67157 4.5 10.5 4.5C11.3284 4.5 12 3.82843 12 3C12 2.17157 11.3284 1.5 10.5 1.5C9.67157 1.5 9 2.17157 9 3ZM10.5 9.5C9.67157 9.5 9 8.82843 9 8C9 7.17157 9.67157 6.5 10.5 6.5C11.3284 6.5 12 7.17157 12 8C12 8.82843 11.3284 9.5 10.5 9.5ZM10.5 14.5C9.67157 14.5 9 13.8284 9 13C9 12.1716 9.67157 11.5 10.5 11.5C11.3284 11.5 12 12.1716 12 13C12 13.8284 11.3284 14.5 10.5 14.5Z" fill="#171717"/>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -0,0 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" id="icon-upload" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 14.5C11.5899 14.5 14.5 11.5899 14.5 8C14.5 4.41015 11.5899 1.5 8 1.5C4.41015 1.5 1.5 4.41015 1.5 8C1.5 11.5899
4.41015 14.5 8 14.5Z" stroke="#505A62" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 4.75V11.1351" stroke="#505A62" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.29102 7.45833L7.99935 4.75L10.7077 7.45833" stroke="#505A62" stroke-miterlimit="10" stroke-linecap="round"
stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 637 B

View File

@@ -0,0 +1,10 @@
<svg id="icon-youtube" width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_779_38008)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.125 13.625H14.875C15.4273 13.625 15.875 13.1773 15.875 12.625V3.125C15.875 2.57272 15.4273 2.125 14.875 2.125H3.125C2.57272 2.125 2.125 2.57272 2.125 3.125V12.625C2.125 13.1773 2.57272 13.625 3.125 13.625ZM14.875 14.625C15.9796 14.625 16.875 13.7296 16.875 12.625V3.125C16.875 2.02043 15.9796 1.125 14.875 1.125H3.125C2.02043 1.125 1.125 2.02043 1.125 3.125V12.625C1.125 13.7296 2.02043 14.625 3.125 14.625H14.875ZM6.74988 5.35507C6.74988 4.74831 7.43289 4.39269 7.92997 4.74065L11.619 7.32298C12.0456 7.62156 12.0456 8.25325 11.619 8.55183L7.92998 11.1342C7.43289 11.4821 6.74988 11.1265 6.74988 10.5197V5.35507ZM7.74988 5.83524V10.0396L10.753 7.93741L7.74988 5.83524ZM5.625 15.25C5.34886 15.25 5.125 15.4739 5.125 15.75C5.125 16.0261 5.34886 16.25 5.625 16.25H12.375C12.6511 16.25 12.875 16.0261 12.875 15.75C12.875 15.4739 12.6511 15.25 12.375 15.25H5.625Z" fill="#171717"/>
</g>
<defs>
<clipPath id="clip0_779_38008">
<rect width="18" height="18" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,5 +1,6 @@
frappe.ready(() => {
setup_file_size();
pin_header();
$(".join-batch").click((e) => {
join_course(e);
@@ -9,14 +10,6 @@ frappe.ready(() => {
notify_user(e);
});
$(".btn-chapter").click((e) => {
add_chapter(e);
});
$(document).on("click", ".btn-save-chapter", (e) => {
save_chapter(e);
});
$(".nav-link").click((e) => {
change_hash(e);
});
@@ -44,43 +37,21 @@ frappe.ready(() => {
show_no_preview_dialog(e);
});
$(".lesson-dropzone").each((i, el) => {
setSortable(el);
});
$(".chapter-dropzone").each((i, el) => {
setSortable(el);
});
$("#create-class").click((e) => {
open_class_dialog(e);
});
});
const setSortable = (el) => {
new Sortable(el, {
group: {
name: "les",
pull: "les",
put: "les",
},
onEnd: (e) => {
if ($(e.item).hasClass("lesson-info")) reorder_lesson(e);
else reorder_chapter(e);
},
onMove: (e) => {
if (
$(e.dragged).hasClass("lesson-info") &&
$(e.to).hasClass("chapter-dropzone")
)
return false;
if (
$(e.dragged).hasClass("chapter-edit") &&
$(e.to).hasClass("lesson-dropzone")
)
return false;
},
});
const pin_header = () => {
const el = document.querySelector(".sticky");
if (el) {
const observer = new IntersectionObserver(
([e]) =>
e.target.classList.toggle("is-pinned", e.intersectionRatio < 1),
{ threshold: [1] }
);
observer.observe(el);
}
};
const setup_file_size = () => {
@@ -162,65 +133,6 @@ const notify_user = (e) => {
});
};
const add_chapter = (e) => {
if ($(".new-chapter").length) {
scroll_to_chapter_container();
return;
}
let next_index = $("[data-index]").last().data("index") + 1 || 1;
let add_after = $(`.chapter-parent:last`).length
? $(`.chapter-dropzone`)
: $("#outline-heading");
$(`<div class="chapter-parent chapter-edit new-chapter">
<div contenteditable="true" data-placeholder="${__(
"Chapter Name"
)}" class="chapter-title-main"></div>
<div class="chapter-description small my-2" contenteditable="true"
data-placeholder="${__("Short Description")}"></div>
<button class="btn btn-sm btn-secondary d-block btn-save-chapter"
data-index="${next_index}"> ${__("Save")} </button>
</div>`).insertAfter(add_after);
scroll_to_chapter_container();
};
const scroll_to_chapter_container = () => {
$([document.documentElement, document.body]).animate(
{
scrollTop: $(".new-chapter").offset().top,
},
1000
);
$(".new-chapter").find(".chapter-title-main").focus();
};
const save_chapter = (e) => {
let target = $(e.currentTarget);
let parent = target.closest(".chapter-parent");
frappe.call({
method: "lms.lms.doctype.lms_course.lms_course.save_chapter",
args: {
course: $("#title").data("course"),
title: parent.find(".chapter-title-main").text(),
chapter_description: parent.find(".chapter-description").text(),
idx: target.data("index"),
chapter: parent.data("chapter") ? parent.data("chapter") : "",
},
callback: (data) => {
frappe.show_alert({
message: __("Saved"),
indicator: "green",
});
setTimeout(() => {
window.location.reload();
}, 1000);
},
});
};
const generate_graph = (chart_name, element, type = "line") => {
let date = frappe.datetime;
@@ -346,50 +258,6 @@ const show_no_preview_dialog = (e) => {
$("#no-preview-modal").modal("show");
};
const reorder_lesson = (e) => {
let old_chapter = $(e.from).closest(".chapter-edit").data("chapter");
let new_chapter = $(e.to).closest(".chapter-edit").data("chapter");
if (old_chapter == new_chapter && e.oldIndex == e.newIndex) return;
frappe.call({
method: "lms.lms.doctype.lms_course.lms_course.reorder_lesson",
args: {
old_chapter: old_chapter,
old_lesson_array: $(e.from)
.children()
.map((i, e) => $(e).data("lesson"))
.get(),
new_chapter: new_chapter,
new_lesson_array: $(e.to)
.children()
.map((i, e) => $(e).data("lesson"))
.get(),
},
callback: (data) => {
window.location.reload();
},
});
};
const reorder_chapter = (e) => {
if (e.oldIndex == e.newIndex) return;
frappe.call({
method: "lms.lms.doctype.lms_course.lms_course.reorder_chapter",
args: {
new_index: e.newIndex + 1,
chapter_array: $(e.to)
.children()
.map((i, e) => $(e).data("chapter"))
.get(),
},
callback: (data) => {
window.location.reload();
},
});
};
const open_class_dialog = (e) => {
this.class_dialog = new frappe.ui.Dialog({
title: __("New Class"),
@@ -415,6 +283,30 @@ const open_class_dialog = (e) => {
reqd: 1,
default: class_info && class_info.end_date,
},
{
fieldtype: "Column Break",
},
{
fieldtype: "Int",
label: __("Seat Count"),
fieldname: "seat_count",
default: class_info && class_info.seat_count,
},
{
fieldtype: "Time",
label: __("Start Time"),
fieldname: "start_time",
default: class_info && class_info.start_time,
},
{
fieldtype: "Time",
label: __("End Time"),
fieldname: "end_time",
default: class_info && class_info.end_time,
},
{
fieldtype: "Section Break",
},
{
fieldtype: "Small Text",
label: __("Description"),
@@ -438,6 +330,9 @@ const create_class = (values) => {
start_date: values.start_date,
end_date: values.end_date,
description: values.description,
seat_count: values.seat_count,
start_time: values.start_time,
end_time: values.end_time,
name: class_info && class_info.name,
},
callback: (r) => {

View File

@@ -1,3 +1,4 @@
import "./profile.js";
import "./common_functions.js";
import "../../../../frappe/frappe/public/js/frappe/ui/chart.js";
import "../../../../frappe/frappe/public/js/telemetry/index.js";

View File

@@ -1,9 +1,5 @@
{% extends "templates/base.html" %}
{% block meta_block %}
{% include "templates/includes/meta_block.html" %}
{% endblock %}
{% block content %}
{% include "public/icons/symbol-defs.svg" %}
{% include "lms/templates/onboarding_header.html" %}

View File

@@ -1,11 +1,9 @@
{% if not course.upcoming %}
<div class="reviews-parent">
{% set reviews = get_reviews(course.name) %}
<div class="course-home-headings mb-5"> {{ _("Reviews") }} </div>
<div class="page-title mb-5"> {{ _("Reviews") }} </div>
{% set avg_rating = get_average_rating(course.name) %}
{% if avg_rating %}
<div class="reviews-header">
<div class="text-center">
@@ -13,21 +11,21 @@
{{ frappe.utils.flt(avg_rating, frappe.get_system_settings("float_precision") or 3) }}
</div>
<div class="course-meta"> {{ reviews | length }} {{ _("ratings") }} </div>
<div class="avg-rating-stars">
<div class="rating">
{% for i in [1, 2, 3, 4, 5] %}
<svg class="icon icon-md {% if i <= frappe.utils.ceil(avg_rating) %} star-click {% endif %}" data-rating="{{ i }}">
<svg class="icon icon-lg {% if i <= frappe.utils.ceil(avg_rating) %} star-click {% endif %}" data-rating="{{ i }}">
<use href="#icon-star"></use>
</svg>
{% endfor %}
</div>
</div>
<div class="course-meta">
{{ frappe.utils.flt(avg_rating, frappe.get_system_settings("float_precision") or 3) }} {{ _("out of 5 ") }}
</div>
<div class="course-meta"> {{ reviews | length }} {{ _("ratings") }} </div>
<!--
-->
<div class="mt-5">
{% include "lms/templates/reviews_cta.html" %}

131
lms/www/batch/edit.html Normal file
View File

@@ -0,0 +1,131 @@
{% extends "lms/templates/lms_base.html" %}
{% block title %}
{% if lesson.title %}
{{ lesson.title }} - {{ course.title }}
{% else %}
{{ _("New Lesson") }}
{% endif %}
{% endblock %}
{% block content %}
<main class="common-page-style">
{{ Header() }}
<div class="container form-width" id="course-outline" {% if course.name %} data-course="{{ course.name }}" {% endif %}>
{{ CreateLesson() }}
</div>
</main>
{% endblock %}
{% macro Header() %}
<header class="sticky">
<div class="container form-width">
<div class="edit-header">
<div>
<div class="page-title">
{{ course.title if course.name else _("Course Outline") }}
</div>
<div class="vertically-center small">
<a class="dark-links" href="/courses/{{ course.name }}/edit">
{{ _("Course Details") }}
</a>
<img class="icon icon-sm mr-0" src="/assets/lms/icons/chevron-right.svg">
<a class="dark-links" href="/courses/{{ course.name }}/outline">
{{ _("Course Outline") }}
</a>
<img class="icon icon-sm mr-0" src="/assets/lms/icons/chevron-right.svg">
<span class="breadcrumb-destination">
{{ _("New Lesson") }}
</span>
</div>
</div>
<div class="align-self-center">
{% if lesson.name %}
<a class="btn btn-default btn-sm mr-2" href="{{ get_lesson_url(course.name, lesson_number) }}">
<span>
{{ _("Back to Lesson") }}
</span>
</a>
{% endif %}
<button class="btn btn-primary btn-sm" id="save-lesson">
<span>
{{ _("Save") }}
</span>
</button>
</div>
</div>
</div>
</header>
{% endmacro %}
{% macro CreateLesson() %}
<article class="field-parent">
<div class="field-group">
<div>
<div class="field-label">
{{ _("Title") }}
</div>
<div class="field-description">
{{ _("Something Short and Concise") }}
</div>
</div>
<div class="">
<input id="lesson-title" type="text" class="field-input" data-index="{{ lesson_index }}" data-chapter="{{ chapter }}" data-course="{{ course.name }}" {% if lesson.name %} data-lesson="{{ lesson.name }}" value="{{ lesson.title }}" {% endif %}>
</div>
</div>
<div class="field-group">
<label for="published" class="vertically-center">
<input type="checkbox" id="preview" {% if lesson.include_in_preview %} checked {% endif %}>
<span>{{ _("Show preview of this lesson to Guest users.") }}</span>
</label>
</div>
<div class="field-group">
<div>
<div class="field-label">
{{ _("Content") }}
</div>
<div class="field-description">
{{ _("Add your lesson content here") }}
</div>
</div>
<div id="lesson-content" class="lesson-editor"></div>
{% if lesson.body %}
<div id="current-lesson-content" class="hide">{{ lesson.body }}</div>
{% endif %}
</div>
</article>
{% endmacro %}
{%- block script %}
{{ super() }}
{% if is_moderator %}
<script>
frappe.boot.user = {
"can_create": [],
"can_select": ["LMS Quiz"],
"can_read": ["LMS Quiz"]
};
frappe.router = {
slug (name) {
return name.toLowerCase().replace(/ /g, "-");
}
}
</script>
{% endif %}
{{ include_script('controls.bundle.js') }}
<script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/paragraph@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/header@latest"></script>
{% endblock %}

394
lms/www/batch/edit.js Normal file
View File

@@ -0,0 +1,394 @@
frappe.ready(() => {
frappe.telemetry.capture("on_lesson_creation_page", "lms");
let self = this;
if ($("#current-lesson-content").length) {
parse_string_to_lesson();
}
setup_editor();
fetch_quiz_list();
$("#save-lesson").click((e) => {
save_lesson(e);
});
});
const setup_editor = () => {
self.editor = new EditorJS({
holder: "lesson-content",
tools: {
header: {
class: Header,
inlineToolbar: ["bold", "italic", "link"],
config: {
levels: [4, 5, 6],
defaultLevel: 5,
},
icon: `<svg class="icon icon-sm" style="">
<use class="" href="#icon-header"></use>
</svg>`,
},
paragraph: {
class: Paragraph,
inlineToolbar: true,
config: {
preserveBlank: true,
},
},
youtube: YouTubeVideo,
quiz: Quiz,
upload: Upload,
},
data: {
blocks: self.blocks ? self.blocks : [],
},
});
};
const parse_string_to_lesson = () => {
let lesson_content = $("#current-lesson-content").html();
let lesson_blocks = [];
lesson_content.split("\n").forEach((block) => {
if (block.includes("{{ YouTubeVideo")) {
let youtube_id = block.match(/'([^']+)'/)[1];
lesson_blocks.push({
type: "youtube",
data: {
youtube: youtube_id,
},
});
} else if (block.includes("{{ Quiz")) {
let quiz = block.match(/'([^']+)'/)[1];
lesson_blocks.push({
type: "quiz",
data: {
quiz: quiz,
},
});
} else if (block.includes("{{ Video")) {
let video = block.match(/'([^']+)'/)[1];
lesson_blocks.push({
type: "upload",
data: {
file_url: video,
},
});
} else if (block.includes("![]")) {
let image = block.match(/\((.*?)\)/)[1];
lesson_blocks.push({
type: "upload",
data: {
file_url: image,
},
});
} else if (block.includes("#")) {
let level = (block.match(/#/g) || []).length;
lesson_blocks.push({
type: "header",
data: {
text: block.replace(/#/g, "").trim(),
level: level,
},
});
} else {
lesson_blocks.push({
type: "paragraph",
data: {
text: block,
},
});
}
});
this.blocks = lesson_blocks;
};
const save_lesson = (e) => {
self.editor.save().then((outputData) => {
parse_lesson_to_string(outputData);
});
};
const parse_lesson_to_string = (data) => {
let lesson_content = "";
data.blocks.forEach((block) => {
if (block.type == "youtube") {
lesson_content += `{{ YouTubeVideo("${block.data.youtube}") }}\n`;
} else if (block.type == "quiz") {
lesson_content += `{{ Quiz("${block.data.quiz}") }}\n`;
} else if (block.type == "upload") {
let url = block.data.file_url;
lesson_content += block.data.is_video
? `{{ Video("${url}") }}\n`
: `![](${url})`;
} else if (block.type == "header") {
lesson_content +=
"#".repeat(block.data.level) + ` ${block.data.text}\n`;
} else if (block.type == "paragraph") {
lesson_content += `${block.data.text}\n`;
}
});
save(lesson_content);
};
const save = (lesson_content) => {
validate_mandatory(lesson_content);
let lesson = $("#lesson-title").data("lesson");
frappe.call({
method: "lms.lms.doctype.lms_course.lms_course.save_lesson",
args: {
title: $("#lesson-title").val(),
body: lesson_content,
chapter: $("#lesson-title").data("chapter"),
preview: $("#preview").prop("checked") ? 1 : 0,
idx: $("#lesson-title").data("index"),
lesson: lesson ? lesson : "",
},
callback: (data) => {
frappe.show_alert({
message: __("Saved"),
indicator: "green",
});
setTimeout(() => {
window.location.href = window.location.href.split("?")[0];
}, 1000);
},
});
};
const validate_mandatory = (lesson_content) => {
if (!$("#lesson-title").val()) {
let error = $("p")
.addClass("error-message")
.text(__("Please enter a Lesson Title"));
$(error).insertAfter("#lesson-title");
$("#lesson-title").focus();
throw "Title is mandatory";
}
if (!lesson_content.trim()) {
let error = $("p")
.addClass("error-message")
.text(__("Please enter some content for the lesson"));
$(error).insertAfter("#lesson-content");
document
.getElementById("lesson-content")
.scrollIntoView({ block: "start" });
throw "Lesson Content is mandatory";
}
};
const fetch_quiz_list = () => {
frappe.call({
method: "lms.lms.doctype.lms_quiz.lms_quiz.get_user_quizzes",
callback: (r) => {
self.quiz_list = r.message;
},
});
};
const is_video = (url) => {
let video_types = ["mov", "mp4", "mkv"];
let video_extension = url.split(".").pop();
return video_types.indexOf(video_extension) >= 0;
};
class YouTubeVideo {
constructor({ data }) {
this.data = data;
}
static get toolbox() {
return {
title: "YouTube Video",
icon: `<img src="/assets/lms/icons/video.svg" width="15" height="15">`,
};
}
render() {
this.wrapper = document.createElement("div");
if (this.data && this.data.youtube) {
$(this.wrapper).html(this.render_youtube(this.data.youtube));
} else {
this.render_youtube_dialog();
}
return this.wrapper;
}
render_youtube_dialog() {
let self = this;
let youtubedialog = new frappe.ui.Dialog({
title: __("YouTube Video"),
fields: [
{
fieldname: "youtube",
fieldtype: "Data",
label: __("YouTube Video ID"),
reqd: 1,
},
{
fieldname: "instructions_section_break",
fieldtype: "Section Break",
label: __("Instructions:"),
},
{
fieldname: "instructions",
fieldtype: "HTML",
label: __("Instructions"),
options: __(
"Enter the YouTube Video ID. The ID is the part of the URL after <code>watch?v=</code>. For example, if the URL is <code>https://www.youtube.com/watch?v=QH2-TGUlwu4</code>, the ID is <code>QH2-TGUlwu4</code>"
),
},
],
primary_action_label: __("Insert"),
primary_action(values) {
youtubedialog.hide();
self.youtube = values.youtube;
$(self.wrapper).html(self.render_youtube(values.youtube));
},
});
youtubedialog.show();
}
render_youtube(youtube) {
return `<iframe width="100%" height="400"
src="https://www.youtube.com/embed/${youtube}"
title="YouTube video player"
frameborder="0"
style="border-radius: var(--border-radius-lg); margin: 1rem 0;"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen>
</iframe>`;
}
save(block_content) {
return {
youtube: this.data.youtube || this.youtube,
};
}
}
class Quiz {
static get toolbox() {
return {
title: "Quiz",
icon: `<img src="/assets/lms/icons/quiz.svg" width="15" height="15">`,
};
}
constructor({ data }) {
this.data = data;
}
render() {
this.wrapper = document.createElement("div");
if (this.data && this.data.quiz) {
$(this.wrapper).html(this.render_quiz(this.data.quiz));
} else {
this.render_quiz_dialog();
}
return this.wrapper;
}
render_quiz_dialog() {
let self = this;
let quizdialog = new frappe.ui.Dialog({
title: __("Select a Quiz"),
fields: [
{
fieldname: "quiz",
fieldtype: "Link",
label: __("Quiz"),
reqd: 1,
options: "LMS Quiz",
},
],
primary_action_label: __("Insert"),
primary_action(values) {
self.quiz = values.quiz;
quizdialog.hide();
$(self.wrapper).html(self.render_quiz(self.quiz));
},
secondary_action_label: __("Create New"),
secondary_action: () => {
window.location.href = `/quizzes`;
},
});
quizdialog.show();
setTimeout(() => {
$(".modal-body").css("min-height", "200px");
$(".modal-body input").focus();
}, 1000);
}
render_quiz(quiz) {
return `<div class="common-card-style p-2 my-2 bold-heading">
Quiz: ${quiz}
</div>`;
}
save(block_content) {
return {
quiz: this.data.quiz || this.quiz,
};
}
}
class Upload {
static get toolbox() {
return {
title: "Upload",
icon: `<img src="/assets/lms/icons/upload.svg" width="15" height="15">`,
};
}
constructor({ data }) {
this.data = data;
}
render() {
this.wrapper = document.createElement("div");
if (this.data && this.data.file_url) {
$(this.wrapper).html(this.render_upload(this.data.file_url));
} else {
this.render_upload_dialog();
}
return this.wrapper;
}
render_upload_dialog() {
let self = this;
new frappe.ui.FileUploader({
disable_file_browser: true,
folder: "Home/Attachments",
make_attachments_public: true,
restrictions: {
allowed_file_types: ["image/*", "video/*"],
},
on_success: (file_doc) => {
self.file_url = file_doc.file_url;
$(self.wrapper).html(self.render_upload(self.file_url));
},
});
}
render_upload(url) {
this.is_video = is_video(url);
if (this.is_video) {
return `<video controls width='100%'>
<source src=${encodeURI(url)} type='video/mp4'>
</video>`;
} else {
return `<img src=${encodeURI(url)} width='100%'>`;
}
}
save(block_content) {
return {
file_url: this.data.file_url || this.file_url,
is_video: this.is_video,
};
}
}

22
lms/www/batch/edit.py Normal file
View File

@@ -0,0 +1,22 @@
import frappe
from lms.www.utils import get_current_lesson_details, get_common_context
from lms.lms.utils import is_instructor, has_course_moderator_role
from frappe import _
def get_context(context):
get_common_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.lesson_number = lesson_number
context.chapter = frappe.db.get_value(
"Chapter Reference", {"idx": chapter_index, "parent": context.course.name}, "chapter"
)
context.lesson = get_current_lesson_details(lesson_number, context, True)
context.is_moderator = has_course_moderator_role()
instructor = is_instructor(context.course.name)
if not instructor and not has_course_moderator_role():
raise frappe.PermissionError(_("You do not have permission to access this page."))

View File

@@ -3,11 +3,7 @@
{% block title %}
{% if lesson.title %}
{{ lesson.title }} - {{ course.title }}
{% else %}
{{ _("New Lesson") }}
{% endif %}
{% endblock %}
@@ -21,16 +17,35 @@
{% block page_content %}
<div class="common-page-style lesson-page">
<div class="common-page-style">
<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="bold-heading mb-4">
{{ course.title }}
</div>
{% if membership %}
<div class="">
<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 %}
<div class="course-details-outline">
{{ widgets.CourseOutline(course=course, membership=membership, lesson_page=True) }}
</div>
</div>
<div class="lesson-pagination-parent">
<div class="lesson-parent">
{{ BreadCrumb(course, lesson) }}
{{ LessonContent(lesson) }}
{% if not lesson.edit_mode and course.status == "Approved" and not course.upcoming %}
{% if course.status == "Approved" and not course.upcoming %}
{{ Discussions() }}
{% endif %}
</div>
@@ -57,25 +72,26 @@
{% 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>
<div>
<div class="pull-right">
{% if get_progress(course.name, lesson.name) == 'Complete' %}
<span id="status-indicator" class="indicator-pill green">{{ _("COMPLETED") }}</span>
{% endif %}
<!-- Title -->
<div class="course-home-headings title mb-0 {% if membership %} is-member {% endif %}
<!-- Edit Button -->
{% if (is_instructor or has_course_moderator_role()) %}
<a class="btn btn-secondary btn-sm ml-2" href="{{ get_lesson_url(course.name, lesson_number) }}/edit">
{{ _("Edit") }}
</a>
{% endif %}
</div>
<div class="course-home-headings title {% 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>
{% if get_progress(course.name, lesson.name) == 'Complete' %}
<span id="status-indicator" class="indicator-pill green">{{ _("COMPLETED") }}</span>
{% endif %}
<!-- Edit Button -->
{% if (is_instructor or has_course_moderator_role()) and not lesson.edit_mode %}
<button class="btn btn-secondary btn-sm ml-2 btn-edit"> {{ _("Edit") }} </button>
{% endif %}
</div>
<!-- Instructors -->
@@ -107,21 +123,17 @@
</div>
<!-- Lesson Content -->
<div class="markdown-source lesson-content-card {% if lesson.edit_mode %} mb-0 mt-2 {% endif %} ">
<div class="markdown-source lesson-content-card">
{% if show_lesson %}
{% if is_instructor and not lesson.include_in_preview and not lesson.edit_mode %}
{% if is_instructor and not lesson.include_in_preview %}
<div class="medium alert alert-info 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">&times;</a>
</div>
{% endif %}
{% if lesson.edit_mode %}
{{ EditLesson(lesson) }}
{% else %}
{{ render_html(lesson) }}
{% endif %}
{% else %}
{% set course_link = "<a class='join-batch' data-course=" + course.name | urlencode + " href=''>" + _('here') + "</a>" %}
@@ -133,9 +145,8 @@
{% endif %}
</div>
{% if not lesson.edit_mode %}
{{ pagination(prev_url, next_url) }}
{% endif %}
</div>
{% endmacro %}
@@ -146,74 +157,20 @@
{% if prev_url or next_url %}
<div class="lesson-pagination">
{% if prev_url %}
<div>
<a class="btn btn-secondary btn-sm prev" href="{{ prev_url }}">
{{ _("Previous") }}
</a>
</div>
<a class="btn btn-secondary btn-sm prev" href="{{ prev_url }}">
{{ _("Previous Lesson") }}
</a>
{% endif %}
{% if next_url %}
<div>
<a class="btn btn-primary btn-sm next ml-2 " href="{{ next_url }}">
{{ _("Next") }}
</a>
</div>
<a class="btn btn-primary btn-sm next pull-right" href="{{ next_url }}">
{{ _("Next Lesson") }}
</a>
{% endif %}
</div>
{% endif %}
{% endmacro %}
<!-- Edit Lesson -->
{% macro EditLesson(lesson) %}
<div class="d-flex mt-2 medium">
<div class="flex-grow-1" contenteditable="true" data-placeholder="{{ _('YouTube Video ID') }}"
id="youtube">{% if lesson.youtube %}{{ lesson.youtube }}{% endif %}</div>
<div class="flex-grow-1 ml-2" contenteditable="true" data-placeholder="{{ _('Quiz ID') }}"
id="quiz-id">{% if lesson.quiz_id %}{{ lesson.quiz_id }}{% endif %}</div>
</div>
{% if lesson.body %}
<div class="body-data hide"> {{ lesson.body }} </div>
{% endif %}
<div id="body"></div>
<div class="d-flex medium mx-0 mb-4">
<div class="flex-grow-1" contenteditable="true" data-placeholder="{{ _('Assignment Question') }}"
id="assignment-question">{% if lesson.question %}{{ lesson.question }}{% endif %}</div>
<select class="btn btn-default ml-2" id="file-type" data-type="{{ lesson.file_type }}">
<option selected> {{ _("File Type") }} </option>
<option value="Image"> {{ _("Image") }} </option>
<option value="Document"> {{ _("Document") }} </option>
<option value="PDF"> {{ _("PDF") }} </option>
</select>
</div>
<label class="preview" for="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 %}
{{ UploadAttachments() }}
</div>
{{ HelpArticle() }}
{% endmacro %}
{% macro UploadAttachments() %}
<div class="attachments-parent">
<div class="attachment-controls">
@@ -290,7 +247,6 @@
<script type="text/javascript">
var page_context = {{ page_context | tojson }};
</script>
{{ include_script('controls.bundle.js') }}
{% for ext in page_extensions %}
{{ ext.render_footer() }}
{% endfor %}

View File

@@ -1,7 +1,6 @@
frappe.ready(() => {
this.marked_as_complete = false;
this.quiz_submitted = false;
this.file_type;
this.answer = [];
this.is_correct = [];
let self = this;
@@ -12,8 +11,6 @@ frappe.ready(() => {
save_current_lesson();
set_file_type();
$(".option").click((e) => {
enable_check(e);
});
@@ -64,24 +61,12 @@ 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];
});
@@ -99,16 +84,6 @@ frappe.ready(() => {
}
});
}
if ($("#body").length) {
make_editor();
}
$("#file-type").change((e) => {
$("#file-type option:selected").each(function () {
self.file_type = $(this).val();
});
});
});
const save_current_lesson = () => {
@@ -526,98 +501,3 @@ 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");
let self = this;
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"].value,
youtube: $("#youtube").text(),
quiz_id: $("#quiz-id").text(),
chapter: $("#title").data("chapter"),
preview: $("#preview").prop("checked") ? 1 : 0,
idx: $("#title").data("index"),
lesson: lesson ? lesson : "",
question: $("#assignment-question").text(),
file_type: self.file_type,
},
callback: (data) => {
frappe.show_alert({
message: __("Saved"),
indicator: "green",
});
setTimeout(() => {
window.location.href = window.location.href.split("?")[0];
}, 1000);
},
});
};
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}') }}`
: `![](${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: "Text Editor",
default: $(".body-data").html(),
},
],
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");
};
const set_file_type = () => {
let self = this;
let file_type = $("#file-type").data("type");
if (file_type) {
$("#file-type option").each((i, elem) => {
if ($(elem).val() == file_type) {
$(elem).attr("selected", true);
self.file_type = file_type;
}
});
}
};

View File

@@ -3,7 +3,11 @@ from frappe import _
from frappe.utils import cstr, flt
from lms.lms.utils import get_lesson_url, has_course_moderator_role, is_instructor
from lms.www.utils import get_common_context, redirect_to_lesson
from lms.www.utils import (
get_common_context,
redirect_to_lesson,
get_current_lesson_details,
)
def get_context(context):
@@ -12,6 +16,7 @@ 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_number = lesson_number
context.lesson_index = lesson_index
context.chapter = frappe.db.get_value(
"Chapter Reference", {"idx": chapter_index, "parent": context.course.name}, "chapter"
@@ -23,6 +28,7 @@ def get_context(context):
context.lesson = get_current_lesson_details(lesson_number, context)
instructor = is_instructor(context.course.name)
context.show_lesson = (
context.membership
or (context.lesson and context.lesson.include_in_preview)
@@ -62,20 +68,6 @@ def get_context(context):
}
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):
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)

View File

@@ -1,101 +1,144 @@
{% extends "templates/base.html" %}
{% block title %}
{{ _("Quiz List") }}
{{ quiz.title if quiz.name else _("Quiz Details") }}
{% endblock %}
{% block content %}
<div class="common-page-style" style="background-color: var(--fg-color);">
<div class="container">
{{ BreadCrumb(quiz) }}
{{ QuizCard(quiz) }}
<div class="common-page-style">
{{ Header() }}
<div class="container form-width">
{{ QuizForm(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>
{% macro QuizForm(quiz) %}
<div>
{{ QuizDetails(quiz) }}
{% if quiz.questions %}
{% for question in quiz.questions %}
{{ Question(question, loop.index) }}
{% endfor %}
{% endif %}
<div id="question-template" class="hide">
{{ Question({}, 0) }}
</div>
</div>
{% endmacro %}
{% macro Header() %}
<header class="sticky">
<div class="container form-width">
<div class="edit-header">
<div>
<div class="page-title">
{{ _("Quiz Details") }}
</div>
<div class="vertically-center small">
<a class="dark-links" href="/quizzes">
{{ _("Quiz List") }}
</a>
<img class="icon icon-sm mr-0" src="/assets/lms/icons/chevron-right.svg">
<span class="breadcrumb-destination">{{ quiz.title if quiz.title else _("New Quiz") }}</span>
</div>
</div>
{% macro QuizCard(quiz) %}
<div style="width: 60%;">
<div class="align-self-center">
<button class="btn btn-default btn-sm btn-add-question">
{{ _("Add Question") }}
</button>
<button class="btn btn-primary btn-sm btn-save-question">
{{ _("Save") }}
</button>
</div>
<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>
</div>
</div>
</header>
{% endmacro %}
{% macro QuizDetails(quiz) %}
<div class="field-parent">
<div class="field-group">
<div>
<div class="field-label">
{{ _("Title") }}
</div>
<div class="field-description">
{{ _("Give your quiz a title") }}
</div>
</div>
<div class="">
<input type="text" class="field-input" id="quiz-title" {% if quiz.name %} value="{{ quiz.title }}" data-name="{{ quiz.name }}" {% endif %}>
</div>
</div>
</div>
{% endmacro %}
{% 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>
{% macro Question(question, index) %}
{% set type = question.type if question.type else "Choices" %}
<div class="common-card-style column-card field-parent question-card" data-index="{{ index }}">
<div class="field-group">
<div>
<div class="field-label question-label">
{{ _("Question") }} {{ index }}
</div>
</div>
<div class="">
<input type="text" class="field-input question" {% if question.name %} value="{{ question.question }}" data-question="{{ question.name }}" {% endif %}>
</div>
</div>
<select value="{{ question.type }}" class="input-with-feedback form-control ellipsis type" maxlength="140" data-fieldtype="Select" data-fieldname="type" placeholder="" data-doctype="LMS Quiz Question">
{% for option in ["Choices", "User Input"] %}
<option value="{{ option }}" {% if question.type == option %} selected {% endif %} > {{ _(option) }} </option>
{% endfor %}
</select>
<div class="field-group">
<div class="vertically-center justify-content-between">
<div class="field-label">
{{ _("Question Type") }}
</div>
<div class="btn-group btn-group-toggle type align-self-center" data-toggle="buttons">
<label class="btn btn-default btn-sm active question-type">
<input type="radio" name="type-{{ index }}" data-type="Choices" {% if type == "Choices" %} checked {% endif %}>
{{ _("Choices") }}
</label>
<label class="btn btn-default btn-sm question-type">
<input type="radio" name="type-{{ index }}" data-type="User Input" {% if type == "User Input" %} checked {% endif %}>
{{ _("User Input") }}
</label>
</div>
</div>
</div>
<div class="">
{% for i in range(1,5) %}
{% set num = frappe.utils.cstr(i) %}
{% set option = question["option_" + num] %}
{% set explanation = question["explanation_" + num] %}
<div class="option-group mt-4 {% if question.type == 'User Input' %} hide {% endif %} ">
<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="{{ _('Explain the option') }}"
class="option-input">{% if explanation %}{{ explanation }}{% endif %}</div>
<div class="option-checkbox">
<label class="mb-0">
<input type="checkbox" {% if question['is_correct_' + num] %} checked {% endif %}>
{{ _("Is Correct") }}
</label>
</div>
</div>
</div>
{% set possible_answer = question["possibility_" + num] %}
<div class="possibility-group mt-4 {% if question.type == 'Choices' %} hide {% endif %}">
<label class=""> {{ _("Possible Answer") }} {{ num }} </label>
<div class="control-input-wrapper">
<div class="control-input">
<div contenteditable="true" class="input-with-feedback form-control bold possibility-{{ num }}" style="height: 100px;" spellcheck="false">{% if possible_answer %}{{possible_answer}}{% endif %}</div>
<div class="field-group">
<div class="options-group {% if type == 'User Input' %} hide {% endif %}">
<input type="text" placeholder="Option" class="field-input option-{{ num }}" {% if option %} value="{{ option }}" {% endif %}>
<input type="text" placeholder="Explanation" class="field-input explanation-{{ num }}" {% if explanation %} value="{{ explanation }}" {% endif %}>
<label class="vertically-center mt-1">
<input type="checkbox" class="correct-{{ num }}" {% if question['is_correct_' + num] %} checked {% endif %}>
{{ _("Is Correct") }}
</label>
</div>
<div class="answers-group {% if type == 'Choices' %} hide {% endif %}">
<div class="field-label">
{{ _("Possible Answers") }} {{ num }}
</div>
<textarea class="field-input possibility-{{ num }}"
style="height: 100px;">{% if possible_answer %}{{ possible_answer }}{% endif %}</textarea>
</div>
</div>
{% endfor %}
</div>
{% endfor %}
{% endif %}
<div class="mt-4">
<button class="btn btn-secondary btn-sm btn-question"> {{ _("New Question") }} </button>
{% if quiz.name %}
<button class="btn btn-secondary btn-sm copy-quiz-id ml-2" data-name="'{{ quiz.name }}'">
{{ _("Copy Quiz ID") }}
</button>
{% endif %}
<button class="btn btn-primary btn-sm btn-save-question ml-2 {% if not quiz.name %} hide {% endif %}">
{{ _("Save Quiz") }}
</button>
</div>
</div>
{% endmacro %}
{% endmacro %}

View File

@@ -1,10 +1,10 @@
frappe.ready(() => {
if (!$(".quiz-card").length) {
if ($(".question-card").length <= 1) {
add_question();
}
$(".btn-question").click((e) => {
add_question();
$(".btn-add-question").click((e) => {
add_question(true);
});
$(".btn-save-question").click((e) => {
@@ -15,106 +15,69 @@ frappe.ready(() => {
frappe.utils.copy_to_clipboard($(e.currentTarget).data("name"));
});
$(document).on("change", ".type", function () {
toggle_form($(this));
$(document).on("click", ".question-type", (e) => {
toggle_form($(e.currentTarget));
});
get_questions();
});
const toggle_form = (el) => {
let type = el.val();
if (type === "Choices") {
el.siblings(".option-group").removeClass("hide");
el.siblings(".possibility-group").addClass("hide");
} else if (type === "User Input") {
el.siblings(".option-group").addClass("hide");
el.siblings(".possibility-group").removeClass("hide");
if ($(el).hasClass("active")) {
let type = $(el).find("input").data("type");
if (type == "Choices") {
$(el)
.closest(".field-parent")
.find(".options-group")
.removeClass("hide");
$(el)
.closest(".field-parent")
.find(".answers-group")
.addClass("hide");
} else {
$(el)
.closest(".field-parent")
.find(".options-group")
.addClass("hide");
$(el)
.closest(".field-parent")
.find(".answers-group")
.removeClass("hide");
}
}
};
const add_question = () => {
let add_after = $(".quiz-card").length
? $(".quiz-card:last")
: $("#quiz-title");
let question_template = `<div class="quiz-card new-quiz-card">
<div contenteditable="true" data-placeholder="${__(
"Question"
)}" class="question mb-4"></div>
<select value="{{ question.type }}" class="input-with-feedback form-control ellipsis type" maxlength="140" data-fieldtype="Select" data-fieldname="type" placeholder="" data-doctype="LMS Quiz Question">
<option value="Choices"> ${__("Choices")} </option>
<option value="User Input"> ${__("User Input")} </option>
</select>
</div>`;
$(question_template).insertAfter(add_after);
get_question_template();
$(".btn-save-question").removeClass("hide");
const add_question = (scroll = false) => {
let template = $("#question-template").html();
let index = $(".question-card:nth-last-child(2)").data("index") + 1 || 1;
template = update_index(template, index);
$(template).insertBefore($("#question-template"));
scroll && scroll_to_question_container();
};
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()
: $(".type:last");
question_template = $(option_template).insertAfter(add_after);
});
Array.from({ length: 4 }, (x, num) => {
let possibility_template = get_possibility_template(num + 1);
let add_after = $(".quiz-card:last .possibility-group").length
? $(".quiz-card:last .possibility-group").last()
: $(".quiz-card:last .option-group:last");
question_template = $(possibility_template).insertAfter(add_after);
});
};
const get_possibility_template = (num) => {
return `<div class="possibility-group mt-4 hide">
<label class=""> ${__("Possible Answer")} ${num} </label>
<div class="control-input-wrapper">
<div class="control-input">
<div contenteditable="true" class="input-with-feedback form-control bold possibility-{{ num }}" style="height: 100px;" spellcheck="false"></div>
</div>
</div>
</div>`;
};
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 update_index = (template, index) => {
const $template = $(template);
$template.attr("data-index", index);
$template.find(".question-label").text("Question " + index);
$template.find(".question-type input").attr("name", "type-" + index);
return $template.prop("outerHTML");
};
const save_question = (e) => {
if (!$("#quiz-title").text()) {
if (!$("#quiz-title").val()) {
frappe.throw(__("Quiz Title is mandatory."));
}
frappe.call({
method: "lms.lms.doctype.lms_quiz.lms_quiz.save_quiz",
args: {
quiz_title: $("#quiz-title").text(),
quiz_title: $("#quiz-title").val(),
questions: get_questions(),
quiz: $("#quiz-title").data("name") || "",
},
callback: (data) => {
window.location.href = "/quizzes";
window.location.href = `/quizzes/${data.message}`;
},
});
};
@@ -122,41 +85,37 @@ const save_question = (e) => {
const get_questions = () => {
let questions = [];
$(".quiz-card").each((i, el) => {
if (!$(el).find(".question").text()) return;
$(".field-parent").each((i, el) => {
if (!$(el).find(".question").val()) return;
let details = {};
let correct_options = 0;
let possibilities = 0;
details["element"] = el;
details["question"] = $(el).find(".question").text();
details["question"] = $(el).find(".question").val();
details["question_name"] =
$(el).find(".question").data("question") || "";
details["type"] = $(el).find(".type").val();
details["type"] = $(el).find("label.active").find("input").data("type");
Array.from({ length: 4 }, (x, i) => {
let num = i + 1;
if (details.type == "Choices") {
details[`option_${num}`] = $(el)
.find(`.option-${num} .option-input:first`)
.text();
details[`explanation_${num}`] = $(el)
.find(`.option-${num} .option-input:last`)
.text();
details[`option_${num}`] = $(el).find(`.option-${num}`).val();
details[`explanation_${num}`] = $(el)
.find(`.explanation-${num}`)
.val();
let is_correct = $(el).find(`.correct-${num}`).prop("checked");
let is_correct = $(el)
.find(`.option-${num} .option-checkbox`)
.find("input")
.prop("checked");
if (is_correct) correct_options += 1;
details[`is_correct_${num}`] = is_correct;
} else {
let possible_answer = $(el)
.find(`.possibility-${num}`)
.text()
.val()
.trim();
if (possible_answer) possibilities += 1;
details[`possibility_${num}`] = possible_answer;
@@ -197,15 +156,15 @@ const validate_mandatory = (details, correct_options, possibilities) => {
};
const scroll_to_question_container = () => {
scroll_to_element(".new-quiz-card:last");
$(".new-quiz-card").find(".question").focus();
scroll_to_element(".question-card:nth-last-child(2)");
$(".question-card:nth-last-child(2)").find(".question").focus();
};
const scroll_to_element = (element) => {
if ($(element).length)
$([document.documentElement, document.body]).animate(
{
scrollTop: $(element).offset().top,
scrollTop: $(element).offset().top - 100,
},
1000
);

View File

@@ -6,79 +6,55 @@
{% block content %}
<div class="common-page-style">
<div class="container">
<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="container form-width">
{{ Header() }}
{% if quiz_list | length %}
<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>
<td> </td>
</tr>
{% for quiz in quiz_list %}
<tr class="quiz-row" data-name="{{ quiz.name }}">
<td> {{ loop.index }} </td>
<td>
{{ quiz.title }}
</td>
<td>
{{ quiz.name }}
</td>
<td>
<a class="btn btn-secondary btn-sm copy-quiz-id" data-name="{{ quiz.name }}">
{{ _("Copy Quiz ID") }}
</a>
</td>
</tr>
{% endfor %}
</table>
</div>
{{ QuizList(quiz_list) }}
{% else %}
<div class="empty-state">
<img class="icon icon-xl" src="/assets/lms/icons/comment.svg">
<div class="empty-state-text">
<div class="empty-state-heading">
{{ _("You have not created any quiz yet.") }}
</div>
<div class="course-meta ">
{{ _("Create a quiz and add it to your course to engage your users.") }}
</div>
</div>
</div>
{{ EmptyState() }}
{% endif %}
</div>
</div>
{% endblock %}
{% macro Header() %}
<header class="sticky">
<div class="edit-header">
<div class="page-title">
{{ _("Quiz List") }}
</div>
{% block script %}
<script>
frappe.ready(() => {
<a class="btn btn-primary btn-sm align-self-center" href="/quizzes/new-quiz">
{{ _("Add Quiz") }}
</a>
</div>
</header>
{% endmacro %}
$(".copy-quiz-id").click((e) => {
e.preventDefault();
frappe.utils.copy_to_clipboard($(e.currentTarget).data("name"));
});
{% macro QuizList(quiz_list) %}
<div class="mt-5">
<ul class="list-unstyled">
{% for quiz in quiz_list %}
<li class="mt-2">
<a class="clickable" href="/quizzes/{{ quiz.name }}">
{{ quiz.title }}
</a>
</li>
{% endfor %}
</ul>
</div>
{% endmacro %}
$(".quiz-row").click((e) => {
if (!$(e.target).hasClass("copy-quiz-id")) {
window.location.href = `/quizzes/${$(e.currentTarget).data('name')}`;
}
});
});
</script>
{% endblock %}
{% macro EmptyState() %}
<div class="empty-state mt-5">
<img class="icon icon-xl" src="/assets/lms/icons/comment.svg">
<div class="empty-state-text">
<div class="empty-state-heading">
{{ _("You have not created any quiz yet.") }}
</div>
<div class="course-meta ">
{{ _("Create a quiz and add it to your course to engage your users.") }}
</div>
</div>
</div>
{% endmacro %}

View File

@@ -13,7 +13,17 @@ def get_context(context):
context.class_info = frappe.db.get_value(
"LMS Class",
class_name,
["name", "title", "start_date", "end_date", "description", "custom_component"],
[
"name",
"title",
"start_date",
"end_date",
"description",
"custom_component",
"seat_count",
"start_time",
"end_time",
],
as_dict=True,
)

View File

@@ -5,22 +5,20 @@
{% block page_content %}
<div class="common-page-style pt-0 pb-0">
<div class="common-page-style">
<div class="course-home-top-container">
{{ CourseHomeHeader(course) }}
<div class="course-home-page">
<div class="container">
<div class="course-body-container">
{{ CourseHeaderOverlay(course) }}
{{ CourseSettings(course) }}
{{ Description(course) }}
{{ Save(course) }}
{{ widgets.CourseOutline(course=course, membership=membership, is_user_interested=is_user_interested) }}
{% if not course.edit_mode and course.status == "Approved" and not frappe.utils.cint(course.upcoming) %}
{% if course.status == "Approved" and not frappe.utils.cint(course.upcoming) %}
{% include "lms/templates/reviews.html" %}
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
@@ -29,10 +27,10 @@
{% macro CourseHomeHeader(course) %}
<div class="course-head-container">
<div class="container pt-8 pb-10">
<div class="container">
<div class="course-card-wide">
{{ BreadCrumb(course) }}
{{ CourseCardWide(course) }}
{{ BreadCrumb(course) }}
{{ CourseCardWide(course) }}
</div>
</div>
</div>
@@ -53,72 +51,35 @@
{% 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 class="course-card-pills">
{{ tag }}
</div>
{% endfor %}
{% if course.edit_mode %}
<button class="btn btn-default btn-sm btn-tag"> {{ _("Add Tag") }} </button>
</div>
<div id="title" {% if course.name %} data-course="{{ course.name | urlencode }}" {% endif %} class="page-title">
{% if course.title %} {{ course.title }} {% endif %}
</div>
<div id="intro">
{% if course.short_introduction %}
{{ course.short_introduction }}
{% 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>
{% if not course.upcoming %}
<div class="avg-rating-stars">
<div class="rating">
{% for i in [1, 2, 3, 4, 5] %}
<svg class="icon icon-lg {% if i <= frappe.utils.ceil(avg_rating) %} star-click {% endif %}" data-rating="{{ i }}">
<use href="#icon-star"></use>
</svg>
{% endfor %}
</div>
<div class="course-image-attachment {% if not course.image %} hide {% endif %} ">
<a {% if course.image %} href="{{ course.image }}" {% endif %} 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>
</div>
{% endif %}
{% if not course.edit_mode %}
<div class="mt-8">
<div class="mt-2">
<div class="bold-heading">{{ _("Instructors") }}:</div>
{% for instructor in get_instructors(course.name) %}
<div class="mt-1">
@@ -129,9 +90,8 @@
</div>
{% endfor %}
</div>
{% endif %}
{% if membership and not course.edit_mode %}
{% if membership %}
{% set progress = frappe.utils.cint(membership.progress) %}
<div class="mt-8">
<div class="progress-percent m-0">{{ progress }}% {{ _("Completed") }}</div>
@@ -147,7 +107,6 @@
<!-- Overlay -->
{% macro CourseHeaderOverlay(course) %}
{% if not course.edit_mode %}
<div class="course-overlay-card">
{% if course.video_link %}
@@ -156,7 +115,10 @@
{% endif %}
<div class="course-overlay-content">
<div class="course-overlay-title"> {{ course.title }} </div>
<div class="cta-parent">
{{ CTASection(course, membership) }}
</div>
{{ Notes(course) }}
@@ -164,97 +126,47 @@
<svg class="icon icon-md mr-1">
<use class="" href="#icon-users">
</svg>
{{ get_students(course.name) | length }} {{ _("Enrolled") }}
{{ format_number(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") }}
{{ get_lessons(course.name, None, False) }} {{ _("Lessons") }}
</div>
{% endif %}
{% if course.paid_certificate %}
{% if course.enable_certification %}
<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>
{{ _("Get Certified") }}
</div>
{% endif %}
{{ CTASection(course, membership) }}
</div>
</div>
{{ SlotModal(course) }}
{% endif %}
{% endmacro %}
<!-- Description -->
{% macro Description(course) %}
{% if course.edit_mode %}
{% if course.description %}
<div class="description-data hide">{{ course.description }}</div>
{% endif %}
<div id="description"></div>
{% else %}
<div class="course-description-section">
{{ frappe.utils.md_to_html(course.description) }}
</div>
{% endif %}
<div class="course-description-section">
{{ course.description }}
</div>
{% endmacro %}
<!-- Course Settings -->
{% macro CourseSettings(course) %}
{% if course.edit_mode and has_course_moderator_role() %}
<div class="mb-4">
<label for="published" class="mb-0">
<input type="checkbox" id="published" {% if course.published %} checked {% endif %}>
{{ _("Published") }}
</label>
<label for="upcoming" class="mb-0 ml-20">
<input type="checkbox" id="upcoming" {% if course.upcoming %} checked {% endif %}>
{{ _("Upcoming") }}
</label>
</div>
{% endif %}
{% endmacro %}
<!-- Save -->
{% macro Save(course) %}
{% if course.edit_mode %}
<div class="mb-16">
<button class="btn btn-primary btn-sm btn-save-course">
{{ _("Save Course Details") }}
</button>
{% if course.name %}
<a class="btn btn-secondary btn-sm btn-exit-edit ml-2" href="/courses/{{ course.name }}">
{{ _("Back to Course") }}
</a>
{% endif %}
</div>
{% endif %}
{% 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="page-title"> {{ _("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 %}
@@ -291,62 +203,73 @@
{% 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 is_instructor(course.name) and not course.published and course.status != "Under Review" %}
<div class="btn btn-primary wide-button" id="submit-for-review" data-course="{{ course.name | urlencode }}">
{{ _("Submit for Review") }}
</div>
<div class="all-cta">
{% if is_instructor(course.name) and not course.published and course.status != "Under Review" %}
<div class="btn btn-primary wide-button" 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") }}
</a>
{% elif course.upcoming and not is_user_interested and not is_instructor(course.name) %}
<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 Cohorts") }}
</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") }}
</a>
{% elif show_start_learing_cta(course, membership) %}
<div class="btn btn-primary wide-button join-batch" data-course="{{ course.name | urlencode }}">
{{ _("Start Learning") }}
</div>
{% endif %}
{% set progress = frappe.utils.cint(membership.progress) %}
{% if membership and course.enable_certification %}
{% if certificate %}
<a class="btn btn-secondary wide-button mt-2" href="/courses/{{ course.name }}/{{ certificate }}">
{{ _("Get Certificate") }}
{% 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") }}
</a>
{% elif eligible_for_evaluation %}
<a class="btn btn-secondary wide-button mt-2" id="apply-certificate" data-course="{{ course.name }}">
{{ _("Apply for Certificate") }}
{% elif course.upcoming and not is_user_interested and not is_instructor(course.name) %}
<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 Cohorts") }}
</a>
{% elif course.grant_certificate_after == "Completion" and progress == 100 %}
<div class="btn btn-secondary wide-button is-secondary mt-2" id="certification" data-course="{{ course.name }}">
{{ _("Get Certificate") }}
{% 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") }}
</a>
{% elif show_start_learing_cta(course, membership) %}
<div class="btn btn-primary wide-button join-batch" data-course="{{ course.name | urlencode }}">
{{ _("Start Learning") }}
</div>
{% endif %}
{% endif %}
{% if is_instructor(course.name) or has_course_moderator_role() %}
<a class="btn btn-secondary wide-button mt-2" href="/courses/{{ course.name }}?edit=1"> {{ _("Edit Course") }} </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-2" href="/courses/{{ course.name }}/{{ certificate }}">
{{ _("Get Certificate") }}
</a>
{% elif eligible_for_evaluation %}
<a class="btn btn-secondary wide-button mt-2" 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 mt-2" id="certification" data-course="{{ course.name }}">
{{ _("Get Certificate") }}
</div>
{% endif %}
{% endif %}
{% if is_instructor(course.name) or has_course_moderator_role() %}
<a class="btn btn-secondary wide-button mt-2" title="Edit Course" href="/courses/{{ course.name }}/edit">
<!-- <svg class="icon icon-md">
<use href="#icon-edit"></use>
</svg> -->
{{ _("Edit") }}
</a>
{% endif %}
</div>
{% endmacro %}
@@ -429,8 +352,3 @@
</div>
{% endmacro %}
{%- block script %}
{{ super() }}
{{ include_script('controls.bundle.js') }}
{% endblock %}

View File

@@ -1,14 +1,6 @@
frappe.ready(() => {
hide_wrapped_mentor_cards();
$("#cancel-request").click((e) => {
cancel_mentor_request(e);
});
$(".view-all-mentors").click((e) => {
view_all_mentors(e);
});
$(".review-link").click((e) => {
show_review_dialog(e);
});
@@ -48,30 +40,6 @@ frappe.ready(() => {
$(document).on("click", ".slot", (e) => {
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);
});
if ($("#description").length) {
make_editor();
}
});
const hide_wrapped_mentor_cards = () => {
@@ -92,42 +60,6 @@ const hide_wrapped_mentor_cards = () => {
}
};
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", "");
}
};
const show_review_dialog = (e) => {
e.preventDefault();
$("#review-modal").modal("show");
@@ -326,84 +258,3 @@ 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: this.code_field_group.fields_dict["code_md"].value,
course: $("#title").data("course")
? $("#title").data("course")
: "",
published: $("#published").prop("checked") ? 1 : 0,
upcoming: $("#upcoming").prop("checked") ? 1 : 0,
},
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();
};
const make_editor = () => {
this.code_field_group = new frappe.ui.FieldGroup({
fields: [
{
fieldname: "code_md",
fieldtype: "Text Editor",
default: $(".description-data").html(),
},
],
body: $("#description").get(0),
});
this.code_field_group.make();
$("#description .form-section:last").removeClass("empty-section");
$("#description .frappe-control").removeClass("hide-control");
$("#description .form-column").addClass("p-0");
};

View File

@@ -9,6 +9,7 @@ from lms.lms.utils import (
is_certified,
is_instructor,
redirect_to_courses_list,
get_average_rating,
)
@@ -33,6 +34,7 @@ def get_context(context):
context.membership = None
else:
set_course_context(context, course_name)
context.avg_rating = get_average_rating(context.course.name)
def set_course_context(context, course_name):
@@ -67,7 +69,7 @@ def set_course_context(context, course_name):
course.edit_mode = True
if course is None:
redirect_to_courses_list()
raise frappe.PermissionError(_("This is not a valid course URL."))
related_courses = frappe.get_all(
"Related Courses", {"parent": course.name}, ["course"]

177
lms/www/courses/create.html Normal file
View File

@@ -0,0 +1,177 @@
{% extends "lms/templates/lms_base.html" %}
{% block title %}
{{ course.title if course and course.title else _("New Course") }}
{% endblock %}
{% block content %}
<main class="common-page-style">
{{ Header() }}
<div class="container form-width">
{{ CreateCourse() }}
</div>
</main>
{% endblock %}
{% macro Header() %}
<header class="sticky">
<div class="container form-width">
<div class="edit-header">
<div class="page-title"> {{ _("Course Details") }} </div>
<div class="align-self-center">
{% if course.name %}
<a class="btn btn-default btn-sm mr-2" href="/courses/{{ course.name }}">
{{ _("Back to Course") }}
</a>
<a class="btn btn-default btn-sm mr-2" href="/courses/{{ course.name }}/outline">
{{ _("Course Outline") }}
</a>
{% endif %}
<button class="btn btn-primary btn-sm btn-save-course">
{{ _("Save") }}
</button>
</div>
</div>
</div>
</header>
{% endmacro %}
{% macro CreateCourse() %}
<div class="field-parent">
<div class="field-group">
<div>
<div class="field-label reqd">
{{ _("Title") }}
</div>
<div class="field-description">
{{ _("Something Short and Concise") }}
</div>
</div>
<div class="">
<input id="title" type="text" class="field-input" {% if course.title %} data-course="{{ course.name }}" value="{{ course.title }}" {% endif %}>
</div>
</div>
<div class="field-group">
<div>
<div class="field-label reqd">
{{ _("Short Introduction") }}
</div>
<div class="field-description">
{{ _("A one line breif description") }}
</div>
</div>
<div class="">
<input id="intro" type="text" class="field-input" {% if course.short_introduction %} value="{{ course.short_introduction }}" {% endif %}>
</div>
</div>
<div class="field-group">
<div>
<div class="field-label reqd">
{{ _("Course Description") }}
</div>
<div class="field-description">
{{ _("Add a detailed description") }}
</div>
</div>
<div id="description" class=""></div>
{% if course.description %}
<div id="description-data" class="hide">
{{ course.description }}
</div>
{% endif %}
</div>
<div class="field-group">
<div>
<div class="field-label">
{{ _("Preview Video ID") }}
</div>
<div class="field-description">
{{ _("Enter the Preview Video ID. The ID is the part of the URL after <code>watch?v=</code>. For example, if the URL is <code>https://www.youtube.com/watch?v=QH2-TGUlwu4</code>, the ID is <code>QH2-TGUlwu4</code>") }}
</div>
</div>
<div class="">
<input id="video-link" type="text" class="field-input" {% if course.video_link %} value="{{ course.video_link }}" {% endif %}>
</div>
</div>
<div class="field-group">
<div>
<div class="field-label">
{{ _("Tags") }}
</div>
<div class="field-description">
{{ _("Add suitable tags") }}
</div>
</div>
<div class="tags field-input">
{% for tag in get_tags(course.name) %}
<button class="btn btn-secondary btn-sm mr-2 text-uppercase">
{{ tag }}
<span class="btn-remove">
<svg class="icon icon-sm">
<use class="" href="#icon-close"></use>
</svg>
</span>
</button>
{% endfor %}
<input type="text" class="invisible-input" id="tags-input">
</div>
</div>
<div class="field-group vertically-center">
<label for="published" class="vertically-center mb-0">
<input type="checkbox" id="published" {% if course.published %} checked {% endif %}>
{{ _("Published") }}
</label>
<label for="upcoming" class="vertically-center mb-0 ml-20">
<input type="checkbox" id="upcoming" {% if course.upcoming %} checked {% endif %}>
{{ _("Upcoming") }}
</label>
</div>
<div class="field-group">
<div>
<div class="field-label">
{{ _("Course Image") }}
</div>
<div class="field-description">
{{ _("Add an appropriate image") }}
</div>
</div>
<div class="">
<button class="btn btn-secondary btn-sm btn-upload mt-2">
{{ _("Upload Image") }}
</button>
</div>
<img {% if course.image %} class="image-preview" src="{{ course.image }}" {% endif %}>
</div>
<div class="field-group">
<div class="field-label">
{{ _("Instructor") }}
</div>
<div class="mt-2">
{{ widgets.Avatar(member=member, avatar_class="avatar-medium") }}
<span class="ml-2">
{{ member.full_name }}
</span>
</div>
</div>
</div>
{% endmacro %}
{%- block script %}
{{ super() }}
{{ include_script('controls.bundle.js') }}
{% endblock %}

166
lms/www/courses/create.js Normal file
View File

@@ -0,0 +1,166 @@
frappe.ready(() => {
frappe.telemetry.capture("on_course_creation_page", "lms");
$(".tags").click((e) => {
e.preventDefault();
$("#tags-input").focus();
});
$("#tags-input").focusout((e) => {
create_tag(e);
});
$(document).on("click", ".btn-remove", (e) => {
$(e.target).parent().parent().remove();
});
$(".btn-save-course").click((e) => {
save_course(e);
});
if ($("#description").length) {
make_editor();
}
$(".field-input").focusout((e) => {
if ($(e.currentTarget).siblings(".error-message")) {
$(e.currentTarget).siblings(".error-message").remove();
}
});
$("#tags-input").focus((e) => {
$(e.target).keypress((e) => {
if (e.which == 13) {
create_tag(e);
}
});
});
$(".btn-upload").click((e) => {
upload_file(e);
});
});
const create_tag = (e) => {
if ($(e.target).val() == "") {
return;
}
let tag = `<button class="btn btn-secondary btn-sm mr-2 text-uppercase">
${$(e.target).val()}
<span class="btn-remove">
<svg class="icon icon-sm">
<use class="" href="#icon-close"></use>
</svg>
</span>
</button>`;
$(tag).insertBefore("#tags-input");
$(e.target).val("");
};
const save_course = (e) => {
validate_mandatory();
let tags = $(".tags button")
.map((i, el) => $(el).text().trim())
.get();
frappe.call({
method: "lms.lms.doctype.lms_course.lms_course.save_course",
args: {
tags: tags.join(", "),
title: $("#title").val(),
short_introduction: $("#intro").val(),
video_link: $("#video-link").val(),
image: $(".image-preview").attr("src"),
description: this.description.fields_dict["description"].value,
course: $("#title").data("course")
? $("#title").data("course")
: "",
published: $("#published").prop("checked") ? 1 : 0,
upcoming: $("#upcoming").prop("checked") ? 1 : 0,
},
callback: (data) => {
frappe.show_alert({
message: __("Saved"),
indicator: "green",
});
setTimeout(() => {
window.location.href = `/courses/${data.message}/edit`;
}, 1000);
},
});
};
const validate_mandatory = () => {
let fields = $(".field-label.reqd");
fields.each((i, el) => {
let input = $(el).closest(".field-group").find(".field-input");
if (input.length && input.val().trim() == "") {
if (input.siblings(".error-message").length == 0) {
scroll_to_element(input);
throw_error(el, input);
}
throw `${$(el).text().trim()} is mandatory`;
}
});
if (!strip_html(this.description.fields_dict["description"].value)) {
scroll_to_element("#description");
throw_error(
"#description",
this.description.fields_dict["description"].parent
);
throw "Description is mandatory";
}
};
const throw_error = (el, input) => {
let error = document.createElement("p");
error.classList.add("error-message");
error.innerText = `Please enter a ${$(el).text().trim()}`;
$(error).insertAfter($(input));
};
const scroll_to_element = (element) => {
if ($(element).length) {
$([document.documentElement, document.body]).animate(
{
scrollTop: $(element).offset().top - 100,
},
1000
);
}
};
const make_editor = () => {
this.description = new frappe.ui.FieldGroup({
fields: [
{
fieldname: "description",
fieldtype: "Text Editor",
default: $("#description-data").html(),
},
],
body: $("#description").get(0),
});
this.description.make();
$("#description .form-section:last").removeClass("empty-section");
$("#description .frappe-control").removeClass("hide-control");
$("#description .form-column").addClass("p-0");
};
const upload_file = (e) => {
new frappe.ui.FileUploader({
disable_file_browser: true,
folder: "Home/Attachments",
make_attachments_public: true,
restrictions: {
allowed_file_types: ["image/*"],
},
on_success: (file_doc) => {
$(e.target)
.parent()
.siblings("img")
.addClass("image-preview")
.attr("src", file_doc.file_url);
},
});
};

54
lms/www/courses/create.py Normal file
View File

@@ -0,0 +1,54 @@
import frappe
from lms.lms.utils import redirect_to_courses_list, can_create_courses
from frappe import _
def get_context(context):
context.no_cache = 1
try:
course_name = frappe.form_dict["course"]
except KeyError:
redirect_to_courses_list()
if not can_create_courses():
message = "You do not have permission to access this page."
if frappe.session.user == "Guest":
message = "Please login to access this page."
raise frappe.PermissionError(_(message))
if course_name == "new-course":
context.course = frappe._dict()
context.course.edit_mode = True
context.membership = None
elif not frappe.db.exists("LMS Course", course_name):
redirect_to_courses_list()
else:
set_course_context(context, course_name)
context.member = frappe.db.get_value(
"User", frappe.session.user, ["full_name", "username"], as_dict=True
)
def set_course_context(context, course_name):
fields = [
"name",
"title",
"short_introduction",
"description",
"image",
"published",
"upcoming",
"disable_self_learning",
"status",
"video_link",
"enable_certification",
"grant_certificate_after",
"paid_certificate",
"price_certificate",
"currency",
"max_attempts",
]
context.course = frappe.db.get_value("LMS Course", course_name, fields, as_dict=True)

View File

@@ -7,7 +7,7 @@
{% block page_content %}
<div class="common-page-style">
<div class="common-page-style pt-8">
<div class="container">
{% if restriction %}
{% set profile_link = "<a href='/edit-profile'> profile </a>" %}
@@ -32,7 +32,7 @@
{% endif %}
{% if show_creators_section %}
<a class="btn btn-default btn-sm ml-2" href="/courses/new-course">
<a class="btn btn-default btn-sm ml-2" href="/courses/new-course/edit">
{{ _("Create a Course") }}
</a>
{% endif %}
@@ -42,27 +42,36 @@
</a>
</div>
<div class="course-home-headings">
<div class="page-title mb-6">
{{ _("All Courses") }}
</div>
<ul class="nav lms-nav" id="courses-tab">
<li class="nav-item">
<a class="nav-link active" data-toggle="tab" href="#live">
{{ _("Live") }} ({{ live_courses | length }})
{{ _("Live") }}
<span class="course-list-count">
{{ live_courses | length }}
</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#upcoming">
{{ _("Upcoming") }} ({{ upcoming_courses | length }})
{{ _("Upcoming") }}
<span class="course-list-count">
{{ upcoming_courses | length }}
</span>
</a>
</li>
{% if frappe.session.user != "Guest" %}
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#courses-enrolled">
{{ _("Enrolled") }} ({{ enrolled_courses | length }})
{{ _("Enrolled") }}
<span class="course-list-count">
{{ enrolled_courses | length }}
</span>
</a>
</li>
{% endif %}
@@ -70,7 +79,10 @@
{% if show_creators_section %}
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#courses-created">
{{ _("Created") }} ({{ created_courses | length }})
{{ _("Created") }}
<span class="course-list-count">
{{ created_courses | length }}
</span>
</a>
</li>
{% endif %}
@@ -78,7 +90,10 @@
{% if show_review_section %}
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#courses-under-review">
{{ _("Under Review") }} ({{ review_courses | length }})
{{ _("Under Review") }}
<span class="course-list-count">
{{ review_courses | length }}
</span>
</a>
</li>
{% endif %}

View File

@@ -0,0 +1,194 @@
{% extends "lms/templates/lms_base.html" %}
{% block title %}
{{ _("Outline") }} - {{ course.title }}
{% endblock %}
{% block page_content %}
<main class="common-page-style">
{{ Header() }}
<div class="container form-width" id="course-outline" {% if course.name %} data-course="{{ course.name }}" {% endif %}>
{% if chapters | length %}
{{ Outline(chapters) }}
{% else %}
{{ EmptyState() }}
{% endif %}
{{ CreateChapter() }}
</div>
</main>
{% endblock %}
{% macro Header() %}
<header class="sticky">
<div class="container form-width">
<div class="edit-header">
<div>
<div class="page-title">
{{ course.title if course.name else _("Course Outline") }}
</div>
<div class="vertically-center small">
<a class="dark-links" href="/courses/{{ course.name }}/edit">{{ _("Course Details") }}</a>
<img class="icon icon-sm mr-0" src="/assets/lms/icons/chevron-right.svg">
<span class="breadcrumb-destination">{{ _("Course Outline") }}</span>
</div>
</div>
<button class="btn btn-primary btn-sm btn-add-chapter align-self-center">
<span>
{{ _("Add Chapter") }}
</span>
</button>
</div>
</div>
</header>
{% endmacro %}
{% macro Outline(chapters) %}
{% if chapters %}
<div class="chapter-dropzone">
{% for chapter in chapters %}
{% set chapter_index = loop.index %}
{% set lessons = get_lessons(course.name, chapter) %}
<div class="common-card-style column-card chapter-container p-4 my-5" data-chapter="{{ chapter.name }}" data-idx="{{ loop.index }}">
<div class="level">
<div class="drag-handle">
<svg class="icon icon-xs level-item mr-2">
<use class="" href="#icon-drag"></use>
</svg>
</div>
<div class="bold-heading chapters-title">
{{ chapter.title }}
</div>
</div>
{% if chapter.description %}
<div class="mb-2 ml-5 chapter-description">
{{ chapter.description }}
</div>
{% endif %}
{% if lessons | length %}
<div class="lesson-dropzone">
{% for lesson in lessons %}
<div class="outline-lesson level" data-lesson="{{ lesson.name }}">
<div class="drag-handle">
<svg class="icon icon-xs level-item mr-2">
<use class="" href="#icon-drag"></use>
</svg>
</div>
<div>
<a class="clickable" href="/courses/{{ course.name }}/learn/{{ chapter_index }}.{{ loop.index }}/edit">
{{ lesson.title }}
</a>
</div>
</div>
{% endfor %}
</div>
{% endif %}
<div class="align-self-start mt-4">
<a class="btn btn-secondary btn-sm" href="/courses/{{ course.name }}/learn/{{ loop.index }}.{{ lessons | length + 1 }}/edit">
<span>
{{ _("Add Lesson") }}
</span>
</a>
<button class="btn btn-secondary btn-sm ml-2 edit-chapter">
<span>
{{ _("Edit") }}
</span>
</button>
</div>
</div>
{% endfor %}
</div>
{% endif %}
{% endmacro %}
{% macro CreateChapter() %}
<div class="modal fade chapter-modal" id="chapter-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="modal-title">{{ _("New Chapter") }}</div>
</div>
<div class="modal-body">
<article id="create-chapter">
<div class="chapter-container">
<div class="field-group">
<div>
<div class="field-label reqd">
{{ _("Chapter Title") }}
</div>
<div class="field-description">
{{ _("Something Short and Concise") }}
</div>
</div>
<div class="">
<input id="chapter-title" type="text" class="field-input">
</div>
</div>
<div class="field-group">
<div>
<div class="field-label">
{{ _("Short Description") }}
</div>
<div class="field-description">
{{ _("A breif description about this chapter.") }}
</div>
</div>
<div class="">
<input id="chapter-description" type="text" class="field-input">
</div>
</div>
</div>
</article>
</div>
<div class="modal-footer">
<button class="btn btn-secondary btn-sm mr-2" data-dismiss="modal" aria-label="Close">
{{ _("Discard") }}
</button>
<button class="btn btn-primary btn-sm align-self-start" id="save-chapter">
{{ _("Save") }}
</button>
</div>
</div>
</div>
</div>
{% endmacro %}
{% macro EmptyState() %}
<article class="empty-state my-5">
<div class="text-center">
<div class="bold-heading">
{{ _("You have not added any chapter yet") }}
</div>
<div>
{{ _("Create and manage your chapters from here.") }}
</div>
<div class="mt-4">
<button class="btn btn-default btn-sm btn-add-chapter">
<span>
{{ _("Add Chapter") }}
</span>
</button>
</div>
</div>
</article>
{% endmacro %}

141
lms/www/courses/outline.js Normal file
View File

@@ -0,0 +1,141 @@
frappe.ready(() => {
frappe.telemetry.capture("on_course_outline_page", "lms");
$(".btn-add-chapter").click((e) => {
show_chapter_modal(e);
});
$(".edit-chapter").click((e) => {
show_chapter_modal(e);
});
$("#save-chapter").click((e) => {
save_chapter(e);
});
$(".lesson-dropzone").each((i, el) => {
setSortable(el);
});
$(".chapter-dropzone").each((i, el) => {
setSortable(el);
});
});
const show_chapter_modal = (e) => {
e.preventDefault();
$("#chapter-modal").modal("show");
let parent = $(e.currentTarget).closest(".chapter-container");
if (parent) {
$("#chapter-title").val($.trim(parent.find(".chapters-title").text()));
$("#chapter-description").val(
$.trim(parent.find(".chapter-description").text())
);
$("#chapter-modal").data("chapter", parent.data("chapter"));
$("#chapter-modal").data("idx", parent.data("idx"));
}
};
const save_chapter = (e) => {
validate_mandatory();
let parent = $("#chapter-modal");
frappe.call({
method: "lms.lms.doctype.lms_course.lms_course.save_chapter",
args: {
course: $("#course-outline").data("course"),
title: $("#chapter-title").val(),
chapter_description: $("#chapter-description").val(),
idx: parent.data("idx") || $(".chapter-container").length,
chapter: parent.data("chapter") || null,
},
callback: (data) => {
frappe.show_alert({
message: __("Saved"),
indicator: "green",
});
setTimeout(() => {
window.location.reload();
}, 1000);
},
});
};
const validate_mandatory = () => {
if (!$("#chapter-title").val()) {
let error = $("p")
.addClass("error-message")
.text("Chapter title is required");
$(error).insertAfter("#chapter-title");
throw __("Chapter title is required");
}
};
const setSortable = (el) => {
new Sortable(el, {
group: "drag",
handle: ".drag-handle",
animation: 150,
fallbackOnBody: true,
swapThreshold: 0.65,
onEnd: (e) => {
if ($(e.item).hasClass("outline-lesson")) reorder_lesson(e);
else reorder_chapter(e);
},
onMove: (e) => {
if (
$(e.dragged).hasClass("outline-lesson") &&
$(e.to).hasClass("chapter-dropzone")
)
return false;
if (
$(e.dragged).hasClass("chapter-edit") &&
$(e.to).hasClass("lesson-dropzone")
)
return false;
},
});
};
const reorder_lesson = (e) => {
let old_chapter = $(e.from).closest(".chapter-container").data("chapter");
let new_chapter = $(e.to).closest(".chapter-container").data("chapter");
if (old_chapter == new_chapter && e.oldIndex == e.newIndex) return;
frappe.call({
method: "lms.lms.doctype.lms_course.lms_course.reorder_lesson",
args: {
old_chapter: old_chapter,
old_lesson_array: $(e.from)
.children()
.map((i, e) => $(e).data("lesson"))
.get(),
new_chapter: new_chapter,
new_lesson_array: $(e.to)
.children()
.map((i, e) => $(e).data("lesson"))
.get(),
},
callback: (data) => {
window.location.reload();
},
});
};
const reorder_chapter = (e) => {
if (e.oldIndex == e.newIndex) return;
frappe.call({
method: "lms.lms.doctype.lms_course.lms_course.reorder_chapter",
args: {
new_index: e.newIndex + 1,
chapter_array: $(e.to)
.children()
.map((i, e) => $(e).data("chapter"))
.get(),
},
callback: (data) => {
window.location.reload();
},
});
};

View File

@@ -0,0 +1,23 @@
import frappe
from frappe import _
from lms.lms.utils import get_chapters, can_create_courses, redirect_to_courses_list
def get_context(context):
context.no_cache = 1
course_name = frappe.form_dict["course"]
if not frappe.db.exists("LMS Course", course_name):
redirect_to_courses_list()
if not can_create_courses():
message = "You do not have permission to access this page."
if frappe.session.user == "Guest":
message = "Please login to access this page."
raise frappe.PermissionError(_(message))
context.course = frappe.db.get_value(
"LMS Course", course_name, ["name", "title"], as_dict=True
)
context.chapters = get_chapters(context.course.name)

View File

@@ -7,4 +7,12 @@ def get_context(context):
except KeyError:
frappe.local.flags.redirect_location = "/jobs"
raise frappe.Redirect
context.job = frappe.get_doc("Job Opportunity", job)
context.metatags = {
"title": context.job.job_title,
"image": context.job.company_logo,
"description": f"Job Posting for {context.job.job_title} by {context.job.company_name}",
"keywords": "Job Opening, Job Posting, Job Opportunity, Job Vacancy, Job, Vacancy, Opening, Opportunity, Vacancy",
}

View File

@@ -8,7 +8,9 @@
<div class="common-page-style">
<div class="container">
{% if frappe.session.user != "Guest" %}
<input class="search pull-right" id="search-user" placeholder="{{ _('Search') }}">
{% endif %}
<div class="course-home-headings">{{ _("People") }} </div>
<div class="empty-state alert alert-dismissible hide" id="search-empty-state">

View File

@@ -1,6 +1,8 @@
import frappe
from lms.lms.utils import get_lesson_url, get_lessons, get_membership
from frappe.utils import cstr
from lms.lms.utils import redirect_to_courses_list
def get_common_context(context):
@@ -18,12 +20,13 @@ def get_common_context(context):
as_dict=True,
)
if not course:
context.template = "www/404.html"
return
redirect_to_courses_list()
context.course = course
context.lessons = get_lessons(course.name)
membership = get_membership(course.name, frappe.session.user, batch_name)
context.membership = membership
context.progress = frappe.utils.cint(membership.progress) if membership else 0
context.batch = membership.batch if membership and membership.batch else None
context.course.query_parameter = (
"?batch=" + membership.batch if membership and membership.batch else ""
@@ -40,3 +43,17 @@ def redirect_to_lesson(course, index_="1.1"):
get_lesson_url(course.name, index_) + course.query_parameter
)
raise frappe.Redirect
def get_current_lesson_details(lesson_number, context, is_edit=False):
details_list = list(filter(lambda x: cstr(x.number) == lesson_number, context.lessons))
if not len(details_list):
if is_edit:
return None
else:
redirect_to_lesson(context.course)
lesson_info = details_list[0]
lesson_info.body = lesson_info.body.replace('"', "'")
return lesson_info

View File

@@ -21,6 +21,8 @@
"dependencies": {
"@4tw/cypress-drag-drop": "^2",
"@cypress/code-coverage": "^3",
"@editorjs/header": "^2.7.0",
"@editorjs/list": "^1.8.0",
"@testing-library/cypress": "^8",
"@testing-library/dom": "8.17.1",
"cypress-real-events": "^1.7.6"

View File

@@ -203,6 +203,16 @@
"@babel/helper-validator-identifier" "^7.19.1"
to-fast-properties "^2.0.0"
"@codexteam/icons@^0.0.4":
version "0.0.4"
resolved "https://registry.yarnpkg.com/@codexteam/icons/-/icons-0.0.4.tgz#8b72dcd3f3a1b0d880bdceb2abebd74b46d3ae13"
integrity sha512-V8N/TY2TGyas4wLrPIFq7bcow68b3gu8DfDt1+rrHPtXxcexadKauRJL6eQgfG7Z0LCrN4boLRawR4S9gjIh/Q==
"@codexteam/icons@^0.0.5":
version "0.0.5"
resolved "https://registry.yarnpkg.com/@codexteam/icons/-/icons-0.0.5.tgz#d17f39b6a0497c6439f57dd42711817a3dd3679c"
integrity sha512-s6H2KXhLz2rgbMZSkRm8dsMJvyUNZsEjxobBEg9ztdrb1B2H3pEzY6iTwI4XUPJWJ3c3qRKwV4TrO3J5jUdoQA==
"@colors/colors@1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
@@ -264,6 +274,20 @@
debug "^3.1.0"
lodash.once "^4.1.1"
"@editorjs/header@^2.7.0":
version "2.7.0"
resolved "https://registry.yarnpkg.com/@editorjs/header/-/header-2.7.0.tgz#755d104a9210a8e2d9ccf22b175b2a93bdbb2330"
integrity sha512-4fGKGe2ZYblVqR/P/iw5ieG00uXInFgNMftBMqJRYcB2hUPD30kuu7Sn6eJDcLXoKUMOeqi8Z2AlUxYAmvw7zQ==
dependencies:
"@codexteam/icons" "^0.0.5"
"@editorjs/list@^1.8.0":
version "1.8.0"
resolved "https://registry.yarnpkg.com/@editorjs/list/-/list-1.8.0.tgz#c64b88679f23c0129ffac589004300832c345d3b"
integrity sha512-Vq6cjyTXBzgegYv/MtTfuDdiz59yGhDEc/yAVXr6lmvoWAFs9cJ4TLuh4/9SbrbhIptcQLDvUjMDKmRrV6v2NQ==
dependencies:
"@codexteam/icons" "^0.0.4"
"@istanbuljs/load-nyc-config@^1.0.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"