Merge pull request #357 from pateljannat/new-course-ui

This commit is contained in:
Jannat Patel
2022-08-26 17:39:34 +05:30
committed by GitHub
45 changed files with 2304 additions and 1516 deletions

View File

@@ -135,10 +135,10 @@ website_route_rules = [
{"from_route": "/sketches/<sketch>", "to_route": "sketches/sketch"},
{"from_route": "/courses/<course>", "to_route": "courses/course"},
{"from_route": "/courses/<course>/<certificate>", "to_route": "courses/certificate"},
{"from_route": "/hackathons/<hackathon>", "to_route": "hackathons/hackathon"},
{"from_route": "/hackathons/<hackathon>/<project>", "to_route": "hackathons/project"},
{"from_route": "/courses/<course>/learn", "to_route": "batch/learn"},
{"from_route": "/courses/<course>/learn/<int:chapter>.<int:lesson>", "to_route": "batch/learn"},
{"from_route": "/quizzes", "to_route": "batch/quiz_list"},
{"from_route": "/quizzes/<quizname>", "to_route": "batch/quiz"},
{"from_route": "/courses/<course>/progress", "to_route": "batch/progress"},
{"from_route": "/courses/<course>/join", "to_route": "batch/join"},
{"from_route": "/courses/<course>/manage", "to_route": "cohorts"},
@@ -190,7 +190,9 @@ jinja = {
"lms.lms.utils.convert_number_to_character",
"lms.lms.utils.get_signup_optin_checks",
"lms.lms.utils.get_popular_courses",
"lms.lms.utils.format_amount"
"lms.lms.utils.format_amount",
"lms.lms.utils.first_lesson_exists",
"lms.lms.utils.has_course_instructor_role"
],
"filters": []
}

View File

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

View File

@@ -2,103 +2,128 @@
// For license information, please see license.txt
frappe.ui.form.on('Course Lesson', {
setup: function (frm) {
frm.trigger('setup_help');
},
setup_help(frm) {
frm.get_field('help').html(`
<p>You can add some more additional content to the lesson using a special syntax. The table below mentions all types of dynamic content that you can add to the lessons and the syntax for the same.</p>
<div class="row font-weight-bold mb-3">
<div class="col-sm-8">
Content Type
</div>
<div class="col-sm-4">
Syntax
</div>
</div>
setup: function (frm) {
frm.trigger('setup_help');
},
setup_help(frm) {
let quiz_link = `<a href="/app/lms-quiz"> ${__("Quiz List")} </a>`;
let exercise_link = `<a href="/app/exercise"> ${__("Exercise List")} </a>`;
let file_link = `<a href="/app/file"> ${__("File DocType")} </a>`;
<div class="row mb-3">
<div class="col-sm-8">
Video
</div>
<div class="col-sm-4">
{{ Video("url_of_source") }}
</div>
</div>
<div class="row mb-3">
<div class="col-sm-8">
YouTube Video
</div>
<div class="col-sm-4">
{{ YouTubeVideo("unique_embed_id") }}
</div>
</div>
<div class="row mb-3">
<div class="col-sm-8">
Exercise
</div>
<div class="col-sm-4">
{{ Exercise("exercise_name") }}
</div>
</div>
<div class="row mb-3">
<div class="col-sm-8">
Quiz
</div>
<div class="col-sm-4">
{{ Quiz("lms_quiz_name") }}
</div>
</div>
<div class="row mb-3">
<div class="col-sm-8">
Assignment
</div>
<div class="col-sm-4">
{{ Assignment("id-filetype") }}
</div>
</div>
<hr>
<div class="row font-weight-bold mb-3">
<div class="col-sm-8">
Supported File Types for Assignment
</div>
<div class="col-sm-4">
Syntax
</div>
</div>
<div class="row mb-3">
<div class="col-sm-8">
.doc,.docx,.xml,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document
</div>
<div class="col-sm-4">
Document
</div>
</div>
<div class="row mb-3">
<div class="col-sm-8">
.pdf
</div>
<div class="col-sm-4">
PDF
</div>
</div>
<div class="row mb-3">
<div class="col-sm-8">
.png, .jpg, .jpeg
</div>
<div class="col-sm-4">
Image
</div>
</div>
`);
}
frm.get_field('help').html(`
<p>${__("You can add some more additional content to the lesson using a special syntax. The table below mentions all types of dynamic content that you can add to the lessons and the syntax for the same.")}</p>
<table class="table">
<tr style="background-color: var(--fg-hover-color); font-weight: bold">
<th style="width: 20%;">
${__("Content Type")}
</th>
<th style="width: 40%;">
${__("Syntax")}
</th>
<th>
${__("Description")}
</th>
</tr>
<tr>
<td>
${__("YouTube Video")}
</td>
<td>
{{ YouTubeVideo("unique_embed_id") }}
</td>
<td>
<span>
${ __("Copy and paste the syntax in the editor. Replace 'embed_src' with the embed source that YouTube provides. To get the source, follow the steps mentioned below.") }
</span>
<ul class="p-4">
<li>
${ __("Upload the video on youtube.") }
</li>
<li>
${ __("When you share a youtube video, it shows an option called Embed.") }
</li>
<li>
${ __("On clicking it, it provides an iframe. Copy the source (src) of the iframe and paste it here.") }
</li>
</ul>
</td>
</tr>
<tr>
<td>
${__("Quiz")}
</td>
<td>
{{ Quiz("lms_quiz_id") }}
</td>
<td>
${ __("Copy and paste the syntax in the editor. Replace 'lms_quiz_id' with the ID of the Quiz you want to add. You can get the ID of the quiz from the {0}.", [quiz_link]) }
</td>
</tr>
<tr>
<td>
${__("Video")}
</td>
<td>
{{ Video("url_of_source") }}
</td>
<td>
${ __("Upload a video from your local machine to the {0}. Copy and paste this syntax in the editor. Replace 'url_of_source' with the File URL field of the document you created in the File DocType.", [file_link]) }
</td>
</tr>
<tr>
<td>
${"Exercise"}
</td>
<td>
{{ Exercise("exercise_id") }}
</td>
<td>
${ __("Copy and paste the syntax in the editor. Replace 'exercise_id' with the ID of the Exercise you want to add. You can get the ID of the exercise from the {0}.", [exercise_link]) }
</td>
</tr>
<tr>
<td>
${__("Assignment")}
</td>
<td>
{{ Assignment("id-filetype") }}
</td>
</tr>
</table>
<hr>
<table class="table">
<tr style="background-color: var(--fg-hover-color); font-weight: bold">
<th style="width: 90%">
${__("Supported File Types for Assignment")}
</th>
<th>
${__("Syntax")}
</th>
</tr>
<tr>
<td>
.doc, .docx, .xml
<td>
${__("Document")}
</td>
</tr>
<tr>
<td>
.pdf
</td>
<td>
${__("PDF")}
</td>
</tr>
<tr>
<td>
.png, .jpg, .jpeg
</td>
<td>
${__("Image")}
</td>
</tr>
</table>
`);
}
});

View File

@@ -29,7 +29,7 @@ class CourseLesson(Document):
e = frappe.get_doc(doctype_map[section], name)
e.lesson = self.name
e.index_ = index
e.save()
e.save(ignore_permissions=True)
index += 1
self.update_orphan_documents(doctype_map[section], documents)

View File

@@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
import json
from ...utils import slugify
from ...utils import generate_slug
from frappe.utils import flt, cint
from lms.lms.utils import get_chapters
@@ -59,22 +59,11 @@ class LMSCourse(Document):
frappe.enqueue(method=frappe.sendmail, queue='short', timeout=300, is_async=True, **email_args)
frappe.db.set_value("LMS Course Interest", user.name, "email_sent", True)
@staticmethod
def find(name):
"""Returns the course with specified name.
"""
return find("LMS Course", published=True, name=name)
def autoname(self):
if not self.name:
self.name = self.generate_slug(title=self.title)
self.name = generate_slug(self.title, "LMS Course")
def generate_slug(self, title):
result = frappe.get_all(
'LMS Course',
fields=['name'])
slugs = set([row['name'] for row in result])
return slugify(title, used_slugs=slugs)
def __repr__(self):
return f"<Course#{self.name}>"
@@ -203,3 +192,91 @@ def submit_for_review(course):
return "No Chp"
frappe.db.set_value("LMS Course", course, "status", "Under Review")
return "OK"
@frappe.whitelist()
def save_course(tags, title, short_introduction, video_link, description, course, image=None):
if course:
doc = frappe.get_doc("LMS Course", course)
else:
doc = frappe.get_doc({
"doctype": "LMS Course"
})
doc.update({
"title": title,
"short_introduction": short_introduction,
"video_link": video_link,
"image": image,
"description": description,
"tags": tags
})
doc.save(ignore_permissions=True)
return doc.name
@frappe.whitelist()
def save_chapter(course, title, chapter_description, idx, chapter):
if chapter:
doc = frappe.get_doc("Course Chapter", chapter)
else:
doc = frappe.get_doc({
"doctype": "Course Chapter"
})
doc.update({
"course": course,
"title": title,
"description": chapter_description
})
doc.save(ignore_permissions=True)
if chapter:
chapter_reference = frappe.get_doc("Chapter Reference", {"chapter": chapter})
else:
chapter_reference = frappe.get_doc({
"doctype": "Chapter Reference",
"parent": course,
"parenttype": "LMS Course",
"parentfield": "chapters",
"idx": idx
})
chapter_reference.update({"chapter": doc.name})
chapter_reference.save(ignore_permissions=True)
return doc.name
@frappe.whitelist()
def save_lesson(title, body, chapter, preview, idx, lesson):
if lesson:
doc = frappe.get_doc("Course Lesson", lesson)
else:
doc = frappe.get_doc({
"doctype": "Course Lesson"
})
doc.update({
"chapter": chapter,
"title": title,
"body": body,
"include_in_preview": preview
})
doc.save(ignore_permissions=True)
if lesson:
lesson_reference = frappe.get_doc("Lesson Reference", {"lesson": lesson})
else:
lesson_reference = frappe.get_doc({
"doctype": "Lesson Reference",
"parent": chapter,
"parenttype": "Course Chapter",
"parentfield": "lessons",
"idx": idx
})
lesson_reference.update({"lesson": doc.name})
lesson_reference.save(ignore_permissions=True)
return doc.name

View File

@@ -2,7 +2,6 @@
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:title",
"creation": "2021-06-07 10:50:17.893625",
"doctype": "DocType",
"editable_grid": 1,
@@ -18,7 +17,9 @@
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title",
"reqd": 1,
"unique": 1
},
{
@@ -49,11 +50,10 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-05-16 14:47:55.364743",
"modified": "2022-08-19 17:54:41.685182",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Quiz",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{

View File

@@ -5,11 +5,20 @@ import frappe
from frappe.model.document import Document
import json
from frappe import _
from frappe.utils import cstr
from lms.lms.utils import generate_slug
class LMSQuiz(Document):
def autoname(self):
if not self.name:
self.name = generate_slug(self.title, "LMS Quiz")
def validate(self):
self.validate_correct_answers()
def validate_correct_answers(self):
for question in self.questions:
correct_options = self.get_correct_options(question)
@@ -18,12 +27,14 @@ class LMSQuiz(Document):
question.multiple = 1
if not len(correct_options):
frappe.throw(_("At least one answer must be correct for this question: {0}").format(frappe.bold(question.question)))
frappe.throw(_("At least one option must be correct for this question: {0}").format(frappe.bold(question.question)))
def get_correct_options(self, question):
correct_option_fields = ["is_correct_1", "is_correct_2", "is_correct_3", "is_correct_4"]
return list(filter(lambda x: question.get(x) == 1, correct_option_fields))
def get_last_submission_details(self):
"""Returns the latest submission for this user.
"""
@@ -43,6 +54,7 @@ class LMSQuiz(Document):
if result:
return result[0]
@frappe.whitelist()
def quiz_summary(quiz, results):
score = 0
@@ -73,3 +85,45 @@ def quiz_summary(quiz, results):
}).save(ignore_permissions=True)
return score
@frappe.whitelist()
def save_quiz(quiz_title, questions, quiz):
if quiz:
doc = frappe.get_doc("LMS Quiz", quiz)
else:
doc = frappe.get_doc({
"doctype": "LMS Quiz",
})
doc.update({
"title": quiz_title
})
doc.save(ignore_permissions=True)
for index, row in enumerate(json.loads(questions)):
if row["question_name"]:
question_doc = frappe.get_doc("LMS Quiz Question", row["question_name"])
else:
question_doc = frappe.get_doc({
"doctype": "LMS Quiz Question",
"parent": doc.name,
"parenttype": "LMS Quiz",
"parentfield": "questions",
"idx": index + 1
})
question_doc.update({
"question": row["question"]
})
for num in range(1,5):
question_doc.update({
"option_" + cstr(num): row["option_" + cstr(num)],
"explanation_" + cstr(num): row["explanation_" + cstr(num)],
"is_correct_" + cstr(num): row["is_correct_" + cstr(num)]
})
question_doc.save(ignore_permissions=True)
return doc.name

View File

@@ -15,7 +15,7 @@ class TestLMSQuiz(unittest.TestCase):
}).save(ignore_permissions=True)
def test_with_multiple_options(self):
quiz = frappe.get_doc("LMS Quiz", "Test Quiz")
quiz = frappe.get_doc("LMS Quiz", "test-quiz")
quiz.append("questions", {
"question": "Question multiple",
"option_1": "Option 1",
@@ -27,7 +27,7 @@ class TestLMSQuiz(unittest.TestCase):
self.assertTrue(quiz.questions[0].multiple)
def test_with_no_correct_option(self):
quiz = frappe.get_doc("LMS Quiz", "Test Quiz")
quiz = frappe.get_doc("LMS Quiz", "test-quiz")
quiz.append("questions", {
"question": "Question no correct option",
"option_1": "Option 1",
@@ -37,5 +37,5 @@ class TestLMSQuiz(unittest.TestCase):
@classmethod
def tearDownClass(cls) -> None:
frappe.db.delete("LMS Quiz", "Test Quiz")
frappe.db.delete("LMS Quiz Question", {"parent": "Test Quiz"})
frappe.db.delete("LMS Quiz", "test-quiz")
frappe.db.delete("LMS Quiz Question", {"parent": "test-quiz"})

View File

@@ -6,8 +6,8 @@
"engine": "InnoDB",
"field_order": [
"show_search",
"portal_course_creation",
"force_profile_completion",
"portal_course_creation",
"column_break_2",
"search_placeholder",
"custom_certificate_template",
@@ -110,10 +110,11 @@
"fieldtype": "Column Break"
},
{
"default": "0",
"default": "Course Instructor Role",
"fieldname": "portal_course_creation",
"fieldtype": "Check",
"label": "Enable Course Creation from Portal"
"fieldtype": "Select",
"label": "Course Creation Access Through Website To",
"options": "Course Instructor Role\nAnyone"
},
{
"fieldname": "column_break_9",
@@ -149,7 +150,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2022-05-09 09:55:24.519269",
"modified": "2022-08-22 10:02:59.988499",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Settings",

View File

@@ -33,6 +33,15 @@ def slugify(title, used_slugs=[]):
return new_slug
count = count+1
def generate_slug(title, doctype):
result = frappe.get_all(
doctype,
fields=['name'])
slugs = set([row['name'] for row in result])
return slugify(title, used_slugs=slugs)
def get_membership(course, member, batch=None):
filters = {
"member": member,
@@ -53,6 +62,8 @@ def get_membership(course, member, batch=None):
def get_chapters(course):
"""Returns all chapters of this course.
"""
if not course:
return []
chapters = frappe.get_all("Chapter Reference", {"parent": course},
["idx", "chapter"], order_by="idx")
for chapter in chapters:
@@ -373,3 +384,26 @@ def format_amount(amount, currency):
return amount
precision = 0 if amount % 1000 == 0 else 1
return _("{0}k").format(fmt_money(amount_reduced, precision, currency))
def first_lesson_exists(course):
first_chapter = frappe.db.get_value("Chapter Reference", {"parent": course, "idx": 1}, "name")
if not first_chapter:
return False
first_lesson = frappe.db.get_value("Lesson Reference", {"parent": first_chapter, "idx": 1}, "name")
if not first_lesson:
return False
return True
def redirect_to_courses_list():
frappe.local.flags.redirect_location = "/courses"
raise frappe.Redirect
def has_course_instructor_role():
return frappe.db.get_value("Has Role", {
"parent": frappe.session.user,
"role": "Course Instructor"
}, "name")

View File

@@ -1,11 +0,0 @@
frappe.ready(function() {
frappe.web_form.after_save = () => {
frappe.show_alert({
message:__("Chapter has been saved successfully. Go back to the course and add this chapter to the chapters table."),
indicator:'green'
}, 3);
setTimeout(() => {
window.location.href = `/courses/${frappe.web_form.doc.course}`;
}, 3000);
}
});

View File

@@ -1,89 +0,0 @@
{
"accept_payment": 0,
"allow_comments": 0,
"allow_delete": 0,
"allow_edit": 1,
"allow_incomplete": 0,
"allow_multiple": 1,
"allow_print": 0,
"amount": 0.0,
"amount_based_on_field": 0,
"apply_document_permissions": 1,
"button_label": "Save",
"creation": "2022-03-07 18:41:07.058806",
"doc_type": "Course Chapter",
"docstatus": 0,
"doctype": "Web Form",
"idx": 0,
"is_multi_step_form": 0,
"is_standard": 1,
"login_required": 1,
"max_attachment_size": 0,
"modified": "2022-03-14 18:48:01.704325",
"modified_by": "Administrator",
"module": "LMS",
"name": "chapter",
"owner": "Administrator",
"payment_button_label": "Buy Now",
"published": 1,
"route": "chapter",
"route_to_success_link": 0,
"show_attachments": 0,
"show_in_grid": 0,
"show_sidebar": 0,
"sidebar_items": [],
"success_url": "/chapter",
"title": "Chapter",
"web_form_fields": [
{
"allow_read_on_all_link_options": 0,
"fieldname": "course",
"fieldtype": "Link",
"hidden": 0,
"label": "Course",
"max_length": 0,
"max_value": 0,
"options": "LMS Course",
"read_only": 0,
"reqd": 1,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "title",
"fieldtype": "Data",
"hidden": 0,
"label": "Title",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 1,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "description",
"fieldtype": "Small Text",
"hidden": 0,
"label": "Description",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "lessons",
"fieldtype": "Table",
"hidden": 0,
"label": "Lessons",
"max_length": 0,
"max_value": 0,
"options": "Lesson Reference",
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
}
]
}

View File

@@ -1,5 +0,0 @@
import frappe
def get_context(context):
# do your magic here
pass

View File

@@ -1,6 +0,0 @@
frappe.ready(function() {
frappe.web_form.after_save = () => {
let route = frappe.web_form.doc.name ? `/courses/${frappe.web_form.doc.name}` : `/course`;
window.location.href = route;
}
});

View File

@@ -1,147 +0,0 @@
{
"accept_payment": 0,
"allow_comments": 0,
"allow_delete": 0,
"allow_edit": 1,
"allow_incomplete": 0,
"allow_multiple": 1,
"allow_print": 0,
"amount": 0.0,
"amount_based_on_field": 0,
"apply_document_permissions": 1,
"button_label": "Save",
"creation": "2022-03-07 14:40:41.262163",
"custom_css": "",
"doc_type": "LMS Course",
"docstatus": 0,
"doctype": "Web Form",
"idx": 0,
"is_multi_step_form": 0,
"is_standard": 1,
"login_required": 1,
"max_attachment_size": 0,
"modified": "2022-03-11 12:32:22.512115",
"modified_by": "Administrator",
"module": "LMS",
"name": "course",
"owner": "Administrator",
"payment_button_label": "Buy Now",
"published": 1,
"route": "course",
"route_to_success_link": 0,
"show_attachments": 0,
"show_in_grid": 0,
"show_sidebar": 0,
"sidebar_items": [],
"success_url": "/course",
"title": "Course",
"web_form_fields": [
{
"allow_read_on_all_link_options": 0,
"fieldname": "title",
"fieldtype": "Data",
"hidden": 0,
"label": "Title",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 1,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "tags",
"fieldtype": "Data",
"hidden": 0,
"label": "Tags",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "",
"fieldtype": "Column Break",
"hidden": 0,
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "video_link",
"fieldtype": "Data",
"hidden": 0,
"label": "Video Embed Link",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "image",
"fieldtype": "Attach Image",
"hidden": 0,
"label": "Preview Image",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "",
"fieldtype": "Section Break",
"hidden": 0,
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "short_introduction",
"fieldtype": "Small Text",
"hidden": 0,
"label": "Short Introduction",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 1,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "description",
"fieldtype": "Text",
"hidden": 0,
"label": "Description",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 1,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "chapters",
"fieldtype": "Table",
"hidden": 0,
"label": "Chapters",
"max_length": 0,
"max_value": 0,
"options": "Chapter Reference",
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
}
]
}

View File

@@ -1,5 +0,0 @@
import frappe
def get_context(context):
# do your magic here
pass

View File

@@ -1,97 +0,0 @@
frappe.ready(function() {
frappe.web_form.after_load = () => {
add_file_upload_component();
fetch_course();
};
frappe.web_form.after_save = () => {
show_success_message();
};
$(document).on("click", ".add-attachment", (e) => {
show_upload_modal();
});
$(document).on("click", ".copy-link", (e) => {
frappe.utils.copy_to_clipboard($(e.currentTarget).data("link"));
$(".attachments").collapse("hide");
});
});
const fetch_course = () => {
frappe.call({
method: "lms.lms.doctype.course_lesson.course_lesson.get_lesson_info",
args: {
"chapter": frappe.web_form.doc.chapter
},
callback: (data) => {
this.course = data.message;
}
});
}
const show_upload_modal = () => {
new frappe.ui.FileUploader({
folder: "Home/Attachments",
restrictions: {
allowed_file_types: ['image/*']
},
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 show_success_message = () => {
frappe.show_alert({
message: __(`Lesson has been saved successfully. Go back to the chapter and add this lesson to the lessons table.`),
indicator:'green'
}, 3);
setTimeout(() => {
window.location.href = `/courses/${this.course}`;
}, 3000);
};
const add_file_upload_component = () => {
$(get_attachment_controls_html()).insertBefore($(`[data-fieldname="include_in_preview"]`).first());
};
const get_attachment_controls_html = () => {
return `
<div class="attachments-parent">
<div class="attachment-controls">
<div class="show-attachments" data-toggle="collapse" data-target="#collapse-attachments" aria-expanded="false">
<svg class="icon icon-sm">
<use class="" href="#icon-attachment">
</svg>
<span class="attachment-count" data-count="0">0 {{ _("attachments") }}</span>
</div>
<div class="add-attachment">
<span class="button is-secondary">
<svg class="icon icon-sm">
<use class="" href="#icon-upload">
</svg>
{{ _("Upload Attachments") }}
</span>
</div>
</div>
<table class="attachments common-card-style collapse hide" id="collapse-attachments"></table>
</div>
`;
};
const build_attachment_table = (file_doc) => {
return $(`
<tr class="attachment-row">
<td>${file_doc.file_name}</td>
<td class=""><a class="button is-secondary button-links copy-link" data-link="![](${file_doc.file_url})"
data-name="${file_doc.file_name}" > {{ _("Copy Link") }} </a></td>
</tr>
`);
};

View File

@@ -1,91 +0,0 @@
{
"accept_payment": 0,
"allow_comments": 0,
"allow_delete": 0,
"allow_edit": 1,
"allow_incomplete": 0,
"allow_multiple": 1,
"allow_print": 0,
"amount": 0.0,
"amount_based_on_field": 0,
"apply_document_permissions": 1,
"button_label": "Save",
"creation": "2022-03-07 18:41:42.549831",
"custom_css": "#introduction {\n font-size: var(--text-base);\n}",
"doc_type": "Course Lesson",
"docstatus": 0,
"doctype": "Web Form",
"idx": 0,
"introduction_text": "<div class=\"ql-editor read-mode\">\n <p>Create lessons for your course. For adding content, use markdown syntax. You can add some additional content to the lesson using a special syntax. The table below mentions all types of dynamic content that you can add to the lessons and the syntax for the same.</p>\n <table class=\"table table-bordered\">\n <tbody>\n <tr>\n <td data-row=\"row-wxk1\">\n <strong>Content Type</strong>\n </td>\n <td data-row=\"row-wxk1\">\n <strong> Syntax </strong>\n </td>\n </tr>\n <tr>\n <td data-row=\"row-r57s\"> Assignment </td>\n <td data-row=\"row-r57s\">{{ Assignment(\"id-filetype\") }}</td>\n </tr>\n <tr>\n <td data-row=\"row-3b6n\"> Quiz </td>\n <td data-row=\"row-3b6n\">{{ Quiz(\"lms_quiz_name\") }}</td>\n </tr>\n <tr>\n <td data-row=\"insert-table\"> YouTube Video </td>\n <td data-row=\"insert-table\">{{ YouTubeVideo(\"unique_embed_id\") }}</td>\n </tr>\n </tbody>\n </table>\n <p> <b> Note: </b> You can also attach videos from Vimeo by including the iframe embed of the video to the lesson. </p>\n</div>",
"is_multi_step_form": 0,
"is_standard": 1,
"login_required": 1,
"max_attachment_size": 0,
"modified": "2022-04-25 10:26:12.691050",
"modified_by": "Administrator",
"module": "LMS",
"name": "lesson",
"owner": "Administrator",
"payment_button_label": "Buy Now",
"published": 1,
"route": "lesson",
"route_to_success_link": 0,
"show_attachments": 0,
"show_in_grid": 0,
"show_sidebar": 0,
"sidebar_items": [],
"success_url": "",
"title": "Lesson",
"web_form_fields": [
{
"allow_read_on_all_link_options": 0,
"fieldname": "chapter",
"fieldtype": "Link",
"hidden": 0,
"label": "Course Chapter",
"max_length": 0,
"max_value": 0,
"options": "Course Chapter",
"read_only": 0,
"reqd": 1,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "title",
"fieldtype": "Data",
"hidden": 0,
"label": "Title",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 1,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"default": "0",
"fieldname": "include_in_preview",
"fieldtype": "Check",
"hidden": 0,
"label": "Include In Preview",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "body",
"fieldtype": "Text",
"hidden": 0,
"label": "Content",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 1,
"show_in_filter": 0
}
]
}

View File

@@ -1,5 +0,0 @@
import frappe
def get_context(context):
# do your magic here
pass

View File

@@ -1,185 +1,185 @@
{% if get_chapters(course.name) | length %}
<div class="course-home-outline">
<div class="course-home-headings">
{{ _("Course Content") }}
</div>
{% for chapter in get_chapters(course.name) %}
<div class="">
<div class="chapter-title" data-target="#{{ get_slugified_chapter_title(chapter.title) }}"
data-toggle="collapse" aria-expanded="false">
<img class="chapter-icon" src="/assets/lms/icons/chevron-right.svg">
<div>{{ chapter.title }}</div>
{% if course.edit_mode and course.name %}
<button class="btn btn-md btn-secondary btn-chapter pull-right"> {{ _("New Chapter") }} </button>
{% endif %}
{% if course.name and (course.edit_mode or get_chapters(course.name) | length) %}
<div class="course-home-headings" id="outline-heading">
{{ _("Course Content") }}
</div>
{% endif %}
<div class="chapter-content collapse navbar-collapse" id="{{ get_slugified_chapter_title(chapter.title) }}">
{% if get_chapters(course.name) | length %}
{% if chapter.description %}
<div class="chapter-description muted-text">
{{ chapter.description }}
{% for chapter in get_chapters(course.name) %}
<div class="chapter-parent {% if course.edit_mode %} chapter-edit {% endif %} ">
<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 %}
<img class="chapter-icon" src="/assets/lms/icons/chevron-right.svg">
{% endif %}
<div class="w-100 chapter-title-main" {% if course.edit_mode %} contenteditable="true" {% endif %} >{{ chapter.title }}</div>
</div>
{% endif %}
{% set is_instructor = is_instructor(course.name) %}
<div class="lessons">
{% set lessons = get_lessons(course.name, chapter) %}
{% for lesson in get_lessons(course.name, chapter) %}
{% set active = membership.current_lesson == lesson.name %}
<div class="lesson-info {% if active %} active-lesson {% endif %}">
<div class="chapter-content {% if not course.edit_mode %} collapse navbar-collapse {% endif %} "
id="{{ get_slugified_chapter_title(chapter.title) }}">
{% if membership or lesson.include_in_preview %}
<a class="lesson-links" href="{{ get_lesson_url(course.name, lesson.number) }}{{course.query_parameter}}"
data-course="{{ course.name }}">
<svg class="icon icon-sm mr-2">
<use class="" href="#{{ lesson.icon }}">
</svg>
<span>{{ lesson.title }}</span>
{% if membership %}
<svg class="icon icon-sm lesson-progress-tick {{ get_progress(course.name, lesson.name) != 'Complete' and 'hide' }}">
<use class="" href="#icon-green-check">
</svg>
{% endif %}
{% if chapter.description or course.edit_mode %}
<div {% if course.edit_mode %} contenteditable="true" {% endif %} class="chapter-description
{% if not course.edit_mode %} mx-8 mb-2 {% endif %} "
data-placeholder="{{ _('Short Description') }}">{{ chapter.description }}</div>
{% endif %}
</a>
{% elif is_instructor and not lesson.include_in_preview %}
<a class="lesson-links"
title="This lesson is not available for preview. As you are the Instructor of the course only you can see it."
href="{{ get_lesson_url(course.name, lesson.number) }}{{course.query_parameter}}"
data-course="{{ course.name }}">
<svg class="icon icon-sm mr-2">
<use class="" href="#icon-lock">
</svg>
<div>{{ lesson.title }}</div>
</a>
{% else %}
<div class="no-preview" title="This lesson is not available for preview" data-course="{{ course.name }}">
<div class="lesson-links">
<svg class="icon icon-sm mr-2">
<use class="" href="#icon-lock-gray">
</svg>
<div>{{ lesson.title }}</div>
</div>
{% if course.edit_mode %}
<div class="mt-2">
<button class="btn btn-sm btn-secondary btn-save-chapter"
data-index="{{ loop.index }}" data-chapter="{{ chapter.name }}"> {{ _('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>
</div>
{% endif %}
{% set is_instructor = is_instructor(course.name) %}
{% if lessons | length %}
<div class="lessons">
{% if course.edit_mode %}
<b class="course-meta"> {{ _("Lessons") }}: </b>
{% endif %}
{% for lesson in lessons %}
{% set active = membership.current_lesson == lesson.name %}
<div class="lesson-info {% if active and not course.edit_mode %} active-lesson {% endif %}">
{% if membership or lesson.include_in_preview or is_instructor %}
<a class="lesson-links" data-course="{{ course.name }}"
{% 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}}">
<svg class="icon icon-sm mr-2">
<use class="" href="#{{ lesson.icon }}">
</svg>
<span>{{ lesson.title }}</span>
{% if membership %}
<svg class="icon icon-sm lesson-progress-tick {{ get_progress(course.name, lesson.name) != 'Complete' and 'hide' }}">
<use class="" href="#icon-green-check">
</svg>
{% endif %}
</a>
{% else %}
<div class="no-preview" title="This lesson is not available for preview" data-course="{{ course.name }}">
<div class="lesson-links">
<svg class="icon icon-sm mr-2">
<use class="" href="#icon-lock-gray">
</svg>
<div>{{ lesson.title }}</div>
</div>
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% endfor %}
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
{% endfor %}
{% endif %}
</div>
<!-- No Preview Modal -->
<div class="modal fade no-preview-modal" id="no-preview-modal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<div class="font-weight-bold">{{ _("Not available for preview") }}</div>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
{% if is_user_interested %}
<div class=""> {{ _("You have opted to be notified for this course. You will receive an email when the course becomes available.") }} </div>
{% else %}
<div class=""> {{ _("This lesson is not available for preview. Please join the course to access it.") }} </div>
{% endif %}
</div>
{% if not is_user_interested %}
<div class="modal-footer">
{% if course.upcoming %}
<div class="button is-primary notify-me" data-course="{{course.name | urlencode}}">
{{ _("Notify me when available") }}
</div>
{% else %}
<div class="button is-primary join-batch" data-course="{{ course.name | urlencode}}">
{{ _("Start Learning") }}</div>
{% endif %}
</div>
{% endif %}
</div>
</div>
</div>
{% endif %}
{{ widgets.NoPreviewModal(course=course) }}
<script>
frappe.ready(() => {
expand_the_active_chapter();
expand_the_active_chapter();
$(".chapter-title").unbind().click((e) => {
rotate_chapter_icon(e);
});
$(".chapter-title").unbind().click((e) => {
rotate_chapter_icon(e);
});
$(".no-preview").click((e) => {
show_no_preview_dialog(e);
});
$(".no-preview").click((e) => {
show_no_preview_dialog(e);
});
});
const expand_the_first_chapter = () => {
let elements = $(".course-home-outline .collapse");
elements.each((i, element) => {
if (i < 1) {
show_section(element);
return false;
}
});
let elements = $(".course-home-outline .collapse");
elements.each((i, element) => {
if (i < 1) {
show_section(element);
return false;
}
});
};
const expand_the_active_chapter = () => {
/* Find anchor matching the URL for course details page */
let selector = $(`a[href="${decodeURIComponent(window.location.pathname)}"]`).parent();
if (!selector.length) {
selector = $(`a[href^="${decodeURIComponent(window.location.pathname)}"]`).parent();
}
if (selector.length && $(".course-details-page").length) {
$(".lesson-info").removeClass("active-lesson");
$(".lesson-info").each((i, elem) => {
let href = $(elem).find("use").attr("href");
href.endsWith("blue") && $(elem).find("use").attr("href", href.substring(0, href.length - 5));
})
/* Find anchor matching the URL for course details page */
let selector = $(`a[href="${decodeURIComponent(window.location.pathname)}"]`).parent();
if (!selector.length) {
selector = $(`a[href^="${decodeURIComponent(window.location.pathname)}"]`).parent();
}
if (selector.length && $(".course-details-page").length) {
$(".lesson-info").removeClass("active-lesson");
$(".lesson-info").each((i, elem) => {
let href = $(elem).find("use").attr("href");
href.endsWith("blue") && $(elem).find("use").attr("href", href.substring(0, href.length - 5));
})
selector.addClass("active-lesson");
selector.addClass("active-lesson");
show_section(selector.parent().parent());
}
show_section(selector.parent().parent());
}
/* For course home page */
else if ($(".active-lesson").length) {
selector = $(".active-lesson")
show_section(selector.parent().parent());
}
/* For course home page */
else if ($(".active-lesson").length) {
selector = $(".active-lesson")
show_section(selector.parent().parent());
}
/* If no active chapter then exapand the first chapter */
else {
expand_the_first_chapter();
}
/* If no active chapter then exapand the first chapter */
else {
expand_the_first_chapter();
}
};
const show_section = (element) => {
$(element).addClass("show");
$(element).siblings(".chapter-title").children(".chapter-icon").css("transform", "rotate(90deg)");
$(element).siblings(".chapter-title").attr("aria-expanded", true);
$(element).addClass("show");
$(element).siblings(".chapter-title").children(".chapter-icon").css("transform", "rotate(90deg)");
$(element).siblings(".chapter-title").attr("aria-expanded", true);
};
const rotate_chapter_icon = (e) => {
let icon = $(e.currentTarget).children(".chapter-icon");
if (icon.css("transform") == "none") {
icon.css("transform", "rotate(90deg)");
} else {
icon.css("transform", "none");
}
let icon = $(e.currentTarget).children(".chapter-icon");
if (icon.css("transform") == "none") {
icon.css("transform", "rotate(90deg)");
} else {
icon.css("transform", "none");
}
};
const show_no_preview_dialog = (e) => {
$("#no-preview-modal").modal("show");
};

View File

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

View File

@@ -29,3 +29,5 @@ execute:frappe.delete_doc("Workspace", "School", ignore_missing=True, force=True
lms.patches.v0_0.move_certification_to_certificate
lms.patches.v0_0.quiz_submission_member
lms.patches.v0_0.delete_old_module_docs #08-07-2022
lms.patches.v0_0.delete_course_web_forms #21-08-2022
lms.patches.v0_0.create_course_instructor_role

View File

@@ -0,0 +1,10 @@
import frappe
def execute():
if not frappe.db.exists("Role", "Course Instructor"):
role = frappe.get_doc({
"doctype": "Role",
"role_name": "Course Instructor",
"home_page": "/dashboard",
})
role.save(ignore_permissions=True)

View File

@@ -0,0 +1,6 @@
import frappe
def execute():
frappe.db.delete("Web Form", "lesson")
frappe.db.delete("Web Form", "chapter")
frappe.db.delete("Web Form", "course")

View File

@@ -60,6 +60,7 @@ input[type=checkbox] {
font-weight: 600;
color: var(--gray-900);
width: fit-content;
box-shadow: var(--shadow-sm);
}
.dark-pills {
@@ -328,7 +329,7 @@ input[type=checkbox] {
.wide-button {
padding: 0.5rem 6rem;
font-weight: 500;
width: inherit;
width: 100%;
}
@media (max-width: 768px) {
@@ -376,16 +377,16 @@ input[type=checkbox] {
}
.chapter-title {
cursor: pointer;
border-radius: var(--border-radius-lg);
padding: 0.5rem 0;
color: var(--gray-900);
display: flex;
align-items: center;
cursor: pointer;
border-radius: var(--border-radius-lg);
color: var(--gray-900);
display: flex;
align-items: center;
}
.chapter-description {
margin: 0 2rem 0.5rem;
color: var(--gray-900);
font-size: var(--text-sm);
}
.course-content-parent .chapter-description {
@@ -408,11 +409,10 @@ input[type=checkbox] {
}
.lesson-links {
display: flex;
align-items: center;
padding: 0.5rem;
color: var(--gray-900);
font-size: var(--text-base);
display: flex;
align-items: center;
padding: 0.5rem;
color: var(--gray-900);
}
.lesson-links:hover {
@@ -423,7 +423,7 @@ input[type=checkbox] {
}
.lessons {
margin-left: 2rem;
margin-left: 1.5rem;
}
.member-card {
@@ -438,9 +438,9 @@ input[type=checkbox] {
}
.break {
flex-basis: 100%;
flex-grow: 1;
margin: .5rem 0;
flex-basis: 100%;
flex-grow: 1;
margin: 0.5rem 0;
}
.course-home-headings {
@@ -593,7 +593,8 @@ input[type=checkbox] {
}
.course-content-parent .course-home-headings {
margin: 0px 0px 1rem;
margin: 0 0 1rem;
width: 100%;
}
.lesson-pagination {
@@ -909,8 +910,9 @@ pre {
}
.empty-state-text {
flex: 1;
margin-left: 1.25rem;
flex: 1;
margin-left: 1.25rem;
text-align: center;
}
.empty-state-heading {
@@ -934,11 +936,49 @@ pre {
background-repeat: no-repeat;
text-indent: 1.5rem;
background-position: 1rem 0.7rem;
float: right;
width: 25%;
width: 30%;
box-shadow: var(--shadow-sm);
}
.course-search-header {
float: right;
width: 80%;
display: flex;
justify-content: flex-end;
align-items: center;
}
@media (max-width: 1250px) {
.search {
width: 40%;
}
}
@media (max-width: 1000px) {
.search {
width: 55%;
}
.course-search-header {
width: 75%;
}
}
@media (max-width: 650px) {
.course-search-header {
width: 60%;
}
}
@media (max-width: 550px) {
.course-search-header {
float: none;
width: 100%;
justify-content: space-between;
margin-bottom: 1rem;
}
}
.section-heading {
font-size: var(--text-4xl);
}
@@ -1068,6 +1108,7 @@ pre {
.live-courses .course-home-headings {
margin-bottom: 1.5rem;
width: fit-content;
}
@media (min-width: 768px) {
@@ -1148,8 +1189,8 @@ pre {
}
.course-head-container {
color: var(--gray-900);
background-color: var(--gray-200);
color: var(--gray-900);
background-color: var(--gray-50);
}
.seperator {
@@ -1418,8 +1459,7 @@ pre {
}
.attachments-parent {
float: right;
font-size: var(--text-sm);
color: var(--text-color);
}
li {
@@ -1500,3 +1540,124 @@ li {
margin-top: 1rem;
}
}
[contenteditable] {
outline: none;
background-color: var(--bg-light-gray);
border-radius: var(--border-radius);
border: 1px dashed var(--gray-600);
padding: 0.5rem 0.75rem;
color: var(--gray-900);
}
[contenteditable]:empty:before {
content: attr(data-placeholder);
color: var(--gray-600);
}
.course-image-attachment {
margin-top: 0.25rem;
background-color: var(--bg-light-gray);
border-radius: var(--border-radius);
border: 1px dashed var(--gray-600);
padding: 0.5rem 0.75rem;
width: fit-content;
}
.btn-delete-tag {
cursor: pointer;
}
.chapter-edit {
border: 1px solid var(--dark-border-color);
border-radius: var(--border-radius);
padding: 1rem;
margin-bottom: 1rem;
}
.chapter-edit .lessons {
margin-left: 0;
margin-top: 2rem;
}
.chapter-parent {
margin-bottom: 1rem;
}
.chapter-edit .chapter-title {
padding: 0.5rem 0;
}
.course-card-pills[contenteditable] {
box-shadow: none;
}
.preview {
display: flex;
align-items: center;
font-size: var(--text-md);
}
.table {
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;
}
.option-checkbox {
width: 15%;
display: flex;
align-items: center;
}
.preview-video-header {
position: relative;
}
.preview-info {
position: absolute;
top: 0;
right: -30px;
}
.tool-tip {
position: relative;
display: inline-block;
}
.tool-tip .tooltiptext {
visibility: hidden;
width: 30rem;
background-color: var(--gray-800);
color: var(--fg-color);
padding: 1rem;
border-radius: var(--border-radius-md);
position: absolute;
z-index: 1;
opacity: 0;
transition: opacity 0.3s;
}
.tool-tip:hover .tooltiptext {
visibility: visible;
opacity: 1;
}
.tooltiptext ul {
padding: 1rem;
}
.help-article {
font-size: var(--text-base);
}

View File

@@ -1,4 +1,7 @@
frappe.ready(() => {
setup_vue_and_file_size();
$(".join-batch").click((e) => {
join_course(e);
});
@@ -7,8 +10,38 @@ frappe.ready(() => {
notify_user(e);
});
$(".btn-chapter").click((e) => {
add_chapter(e);
});
$(document).on("click", ".btn-save-chapter", (e) => {
save_chapter(e);
});
});
const setup_vue_and_file_size = () => {
frappe.require("/assets/frappe/node_modules/vue/dist/vue.js", () => {
Vue.prototype.__ = window.__;
Vue.prototype.frappe = window.frappe;
});
frappe.provide("frappe.form.formatters");
frappe.form.formatters.FileSize = file_size;
};
const file_size = (value) => {
if(value > 1048576) {
value = flt(flt(value) / 1048576, 1) + "M";
} else if (value > 1024) {
value = flt(flt(value) / 1024, 1) + "K";
}
return value;
};
const join_course = (e) => {
e.preventDefault();
let course = $(e.currentTarget).attr("data-course");
@@ -40,12 +73,13 @@ const join_course = (e) => {
})
};
const notify_user = (e) => {
e.preventDefault();
var course = decodeURIComponent($(e.currentTarget).attr("data-course"));
if (frappe.session.user == "Guest") {
window.location.href = `/login?redirect-to=/courses/${course}`;
return;
window.location.href = `/login?redirect-to=/courses/${course}`;
return;
}
frappe.call({
@@ -65,3 +99,59 @@ 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-parent:last`) : $("#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": target.data("chapter") ? target.data("chapter") : ""
},
callback: (data) => {
frappe.show_alert({
message: __("Saved"),
indicator: "green",
});
setTimeout(() => {
window.location.reload();
}, 1000)
}
});
};

View File

@@ -1,23 +1,20 @@
{% set courses = get_authored_courses(frappe.session.user, only_published=False) %}
{% if courses | length %}
<div class="cards-parent">
{% for course in courses %}
{{ widgets.CourseCard(course=course) }}
{% endfor %}
{% for course in courses %}
{{ widgets.CourseCard(course=course) }}
{% endfor %}
</div>
{% else %}
<div class="empty-state">
<div>
<img class="icon icon-xl" src="/assets/lms/icons/comment.svg">
</div>
<div class="empty-state-text">
<div class="empty-state-heading">{{ _("No courses created") }}</div>
<div class="course-meta">{{ _("Help others learn something new.") }}</div>
</div>
<div>
<a class="button is-secondary button-links" href="/course?new=1">
{{ _("Create a Course") }}
</a>
</div>
<div>
<img class="icon icon-xl" src="/assets/lms/icons/comment.svg">
</div>
<div class="empty-state-text">
<div class="empty-state-heading">{{ _("No courses created") }}</div>
<div class="course-meta">{{ _("Help others learn something new.") }}</div>
</div>
</div>
{% endif %}

View File

@@ -5,7 +5,8 @@
<div> {{ _("Your latest score is {0}.").format(last_attempt_score) }} </div>
</div>
{% else %}
<div id="quiz-title" class="hide" data-max-attempts="{{ quiz.max_attempts }}">{{ quiz.title }}</div>
<div id="quiz-title" class="hide" data-name="{{ quiz.name }}"
data-max-attempts="{{ quiz.max_attempts }}">{{ quiz.title }}</div>
<div class="">
<div id="start-banner" class="text-center">

View File

@@ -1,19 +1,22 @@
{% set show_search = frappe.db.get_single_value("LMS Settings", "show_search") %}
{% set search_placeholder = frappe.db.get_single_value("LMS Settings", "search_placeholder") %}
{% set portal_course_creation = frappe.db.get_single_value("LMS Settings", "portal_course_creation") %}
{% if show_search %}
<input class="search" id="search-course" placeholder="{{ _(search_placeholder) }}">
<div class="course-search-header">
<input class="search" id="search-course" placeholder="{{ _(search_placeholder) }}">
{% if portal_course_creation == "Anyone" or has_course_instructor_role() %}
<a class="btn btn-secondary btn-md ml-2" href="/courses/new-course"> {{ _("Create a Course") }} </a>
{% endif %}
</div>
<div class="empty-state alert alert-dismissible search-empty-state hide">
<a href="#" class="close-search-empty-state" aria-label="close">&times;</a>
<div>
<img class="icon icon-xl" src="/assets/frappe/images/ui-states/search-empty-state.svg">
</div>
<div class="empty-state-text">
<div class="empty-state-heading">{{ _("No results found") }}</div>
<div class="course-meta">{{ _("Try some other keyword or explore our list of courses.") }}</div>
</div>
<a href="#" class="close-search-empty-state" aria-label="close">&times;</a>
<div>
<img class="icon icon-xl" src="/assets/frappe/images/ui-states/search-empty-state.svg">
</div>
<div class="empty-state-text">
<div class="empty-state-heading">{{ _("No results found") }}</div>
<div class="course-meta">{{ _("Try some other keyword or explore our list of courses.") }}</div>
</div>
</div>
<script> {% include "lms/templates/search_course/search_course.js" %} </script>
{% endif %}

View File

@@ -1,6 +1,16 @@
{% extends "templates/base.html" %}
{% from "www/macros/livecode.html" import LiveCodeEditorJS, LiveCodeEditor with context %}
{% block title %} {{ lesson.title }} - {{ course.title }} {% endblock %}
{% block title %}
{% if lesson.title %}
{{ lesson.title }} - {{ course.title }}
{% else %}
{{ _("New Lesson") }}
{% endif %}
{% endblock %}
{% block head_include %}
{% include "public/icons/symbol-defs.svg" %}
@@ -12,137 +22,257 @@
{% endblock %}
{% block content %}
<div class="common-page-style lesson-page">
<div class="container course-details-page">
{{ BreadCrumb(course, lesson) }}
<div class="course-content-parent">
<div class="course-details-outline">
{{ widgets.CourseOutline(course=course, membership=membership) }}
</div>
<div class="lesson-pagination-parent">
{{ LessonContent(lesson) }}
{{ Discussions() }}
</div>
<div class="container course-details-page">
{{ BreadCrumb(course, lesson) }}
<div class="course-content-parent">
<div class="course-details-outline">
{{ widgets.CourseOutline(course=course, membership=membership) }}
</div>
<div class="lesson-pagination-parent">
{{ LessonContent(lesson) }}
{% if not lesson.edit_mode %} {{ Discussions() }} {% endif %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
<!-- BreadCrumb -->
{% macro BreadCrumb(course, lesson) %}
<div class="breadcrumb">
<a class="dark-links" href="/courses">{{ _("All Courses") }}</a>
<img class="ml-1 mr-1" src="/assets/lms/icons/chevron-right.svg">
<a class="dark-links" href="/courses/{{ course.name }}">{{ course.title }}</a>
<img class="ml-1 mr-1" src="/assets/lms/icons/chevron-right.svg">
<span class="breadcrumb-destination">{{ lesson.title }}</span>
<span class="breadcrumb-destination">{{ lesson.title if lesson.title else _("New Lesson") }}</span>
</div>
{% endmacro %}
<!-- Lesson Details -->
{% macro LessonContent(lesson) %}
{% set instructors = get_instructors(course.name) %}
{% set is_instructor = is_instructor(course.name) %}
<div class="common-card-style lesson-content">
<div class="lesson-title">
<div class="course-home-headings title mb-0
{% if membership %} is-member {% endif %}
{% if membership or is_instructor %} eligible-for-submission {% endif %}" data-lesson="{{ lesson.name }}"
data-course="{{ course.name }}">{{ lesson.title }}</div>
<span class="lesson-progress {{hide if get_progress(course.name, lesson.name) != 'Complete' else ''}}">{{ _("COMPLETED") }}</span>
<div class="lesson-title">
{% if is_instructor %}
<a class="button is-default button-links ml-auto" href="/lesson?name={{ lesson.name }}"> {{ _("Edit") }} </a>
{% endif %}
</div>
<!-- Title -->
<div class="course-home-headings title mb-0 {% if membership %} is-member {% endif %}
{% if membership or is_instructor %} eligible-for-submission {% endif %}" id="title"
{% if lesson.edit_mode %} data-placeholder="{{ _('Title') }}" contenteditable="true" {% endif %}
data-index="{{ lesson_index }}" data-course="{{ course.name }}" data-chapter="{{ chapter }}"
{% if lesson.name %} data-lesson="{{ lesson.name }}" {% endif %}
>{% if lesson.title %}{{ lesson.title }}{% endif %}</div>
<span class="lesson-progress {{ hide if get_progress(course.name, lesson.name) != 'Complete' else ''}}">{{ _("COMPLETED") }}</span>
<div class="d-flex align-items-center">
{% set ins_len = instructors | length %}
{% for instructor in instructors %}
{% if ins_len > 1 and loop.index == 1 %}
<div class="avatar-group overlap">
{% endif %}
{{ widgets.Avatar(member=instructor, avatar_class="avatar-small") }}
{% if ins_len > 1 and loop.index == ins_len %}
<!-- Edit Button -->
{% if is_instructor and not lesson.edit_mode %}
<button class="button is-default button-links ml-auto btn-edit"> {{ _("Edit") }} </button>
{% endif %}
</div>
{% endif %}
{% endfor %}
<a class="button-links ml-1" href="{{ get_profile_url(instructors[0].username) }}">
<span class="course-meta">
{% if ins_len == 1 %}
{{ instructors[0].full_name }}
{% else %}
{% set suffix = _("other") if ins_len - 1 == 1 else _("others") %}
{{ instructors[0].full_name.split(" ")[0] }} and {{ ins_len - 1 }} {{ suffix }}
{% endif %}
</span>
</a>
<div class="ml-5 course-meta"> {{ frappe.utils.format_date(lesson.creation, "medium") }} </div>
</div>
<div class="markdown-source lesson-content-card">
{% if membership or lesson.include_in_preview or is_instructor %}
{% if is_instructor and not lesson.include_in_preview %}
<div class="small alert alert-secondary alert-dismissible mt-4 mb-4">
{{ _("This lesson is not available for preview. As you are the Instructor of the course only you can see it.") }}
<a href="#" class="close" data-dismiss="alert" aria-label="close">&times;</a>
</div>
{% endif %}
{{ render_html(lesson.body) }}
{% else %}
<div class="">
<div class="button is-primary pull-right join-batch" data-course="{{ course.name | urlencode }}"> {{ _("Start Learning") }} </div>
<div class=""> {{ _("This lesson is not available for preview. Please join the course to access it.") }} </div>
</div>
{% endif %}
</div>
<!-- Instructors -->
<div class="d-flex align-items-center">
{% set ins_len = instructors | length %}
{% for instructor in instructors %}
{% if ins_len > 1 and loop.index == 1 %}
<div class="avatar-group overlap">
{% endif %}
{{ widgets.Avatar(member=instructor, avatar_class="avatar-small") }}
{{ pagination(prev_url, next_url) }}
{% if ins_len > 1 and loop.index == ins_len %}
</div>
{% endif %}
{% endfor %}
<a class="button-links ml-1" href="{{ get_profile_url(instructors[0].username) }}">
<span class="course-meta">
{% if ins_len == 1 %}
{{ instructors[0].full_name }}
{% else %}
{% set suffix = _("other") if ins_len - 1 == 1 else _("others") %}
{{ instructors[0].full_name.split(" ")[0] }} and {{ ins_len - 1 }} {{ suffix }}
{% endif %}
</span>
</a>
<div class="ml-5 course-meta"> {{ frappe.utils.format_date(lesson.creation, "medium") }} </div>
</div>
<!-- Lesson Content -->
<div class="markdown-source lesson-content-card {% if lesson.edit_mode %} mb-0 mt-2 {% endif %} ">
{% if membership or lesson.include_in_preview or is_instructor %}
{% if is_instructor and not lesson.include_in_preview and not lesson.edit_mode %}
<div class="small alert alert-secondary alert-dismissible mb-4">
{{ _("This lesson is not available for preview. As you are the Instructor of the course only you can see it.") }}
<a href="#" class="close" data-dismiss="alert" aria-label="close">&times;</a>
</div>
{% endif %}
{% if lesson.edit_mode %}
{{ EditLesson(lesson) }}
{% else %}
{{ render_html(lesson.body) }}
{% endif %}
{% else %}
<div class="">
<div class="btn btn-primary pull-right join-batch" data-course="{{ course.name | urlencode }}"> {{ _("Start Learning") }} </div>
<div class=""> {{ _("This lesson is not available for preview. Please join the course to access it.") }} </div>
</div>
{% endif %}
</div>
{% if not lesson.edit_mode %}
{{ pagination(prev_url, next_url) }}
{% endif %}
</div>
{% endmacro %}
<!-- Pagination -->
{% macro pagination(prev_url, next_url) %}
<div class="lesson-pagination">
<div>
{% if prev_url %}
<a class="btn btn-secondary dark-links prev" href="{{ prev_url }}">
<img class="mr-2" src="/assets/lms/icons/left-arrow.svg">
{{ _("Prev") }}
</a>
{% endif %}
</div>
<div>
{% if prev_url %}
<a class="button is-secondary dark-links prev" href="{{ prev_url }}">
<img class="mr-2" src="/assets/lms/icons/left-arrow.svg">
{{ _("Prev") }}
</a>
{% if not is_mentor(course.name, frappe.session.user) and membership %}
{% set progress = get_progress(course.name, lesson.name) %}
<div class="custom-checkbox {% if progress == 'Complete' %} hide {% endif %}">
<label class="quiz-label">
<input class="mark-progress" type="checkbox" checked>
<img class="empty-checkbox" />
<span class="small">{{ _("Mark as complete on moving to the next lesson") }}</span>
</label>
</div>
<div class="btn btn-default mark-progress {{ progress }} {% if progress == 'Incomplete' or progress == None %} hide {% endif %}"
data-progress="Incomplete">
{{ _("Mark as Incomplete") }}
</div>
{% endif %}
</div>
{% if not is_mentor(course.name, frappe.session.user) and membership %}
{% set progress = get_progress(course.name, lesson.name) %}
<div class="custom-checkbox {% if progress == 'Complete' %} hide {% endif %}">
<label class="quiz-label">
<input class="mark-progress" type="checkbox" checked>
<img class="empty-checkbox" />
<span class="small">{{ _("Mark as complete on moving to the next lesson") }}</span>
</label>
</div>
<div class="button is-secondary mark-progress {{ progress }} {% if progress == 'Incomplete' or progress == None %} hide {% endif %}"
data-progress="Incomplete">
{{ _("Mark as Incomplete") }}
</div>
{% endif %}
<div>
<a class="button is-primary next {% if membership.progress|int == 100 and not next_url %} hide {% endif %}"
{% if next_url %} data-href="{{ next_url }}" {% endif %} href="">
{% if next_url %} {{ _("Next") }} {% else %} {{ _("Mark as Complete") }} {% endif %}
<img class="ml-2" src="/assets/lms/icons/side-arrow-white.svg">
</a>
</div>
<div>
<a class="btn btn-primary next {% if not next_url and (membership.progress|int == 100 or is_instructor) %} hide {% endif %}"
{% if next_url %} data-href="{{ next_url }}" {% endif %} href="">
{% if next_url %} {{ _("Next") }} {% else %} {{ _("Mark as Complete") }} {% endif %}
<img class="ml-2" src="/assets/lms/icons/side-arrow-white.svg">
</a>
</div>
</div>
{% endmacro %}
<!-- Edit Lesson -->
{% macro EditLesson(lesson) %}
<div id="body" {% if lesson.body %} data-body="{{ lesson.body }}" {% endif %}></div>
<label class="preview">
<input {% if lesson.include_in_preview %} checked {% endif %} type="checkbox"
id="preview"> {{ _("Show preview of this lesson to Guest users.") }}
</label>
<div class="mt-4">
<button class="btn btn-primary btn-sm btn-lesson pull-right ml-2"> {{ _("Save") }} </button>
{% if lesson.name %}
<button class="btn btn-secondary btn-sm pull-right btn-back ml-2"> {{ _("Back to Lesson") }} </button>
<a class="btn btn-secondary btn-sm pull-right" href="/quizzes"> {{ _("Create Quiz") }} </a>
{% endif %}
<div class="attachments-parent">
<div class="attachment-controls">
<div class="show-attachments" data-toggle="collapse" data-target="#collapse-attachments" aria-expanded="false">
<svg class="icon icon-sm">
<use class="" href="#icon-attachment">
</svg>
<span class="attachment-count" data-count="0">0 {{ _("attachments") }}</span>
</div>
<div class="add-attachment">
<span class="btn btn-sm btn-secondary">
<svg class="icon icon-sm">
<use class="" href="#icon-upload">
</svg>
{{ _("Upload Attachments") }}
</span>
</div>
</div>
<table class="attachments common-card-style collapse hide" id="collapse-attachments"></table>
</div>
</div>
{{ HelpArticle() }}
{% endmacro %}
<!-- Help Article -->
{% macro HelpArticle() %}
<div class="help-article">
<h3> {{ _("Help Article") }} </h3>
<p>
{{ _("You can add additional content to the lesson using a special syntax. The table below mentions
all types of dynamic content that you can add to the lessons and the syntax for the same.") }}
</p>
<table class="table w-100">
<tr>
<th style="width: 20%;"> {{ _("Content Type") }} </th>
<th style="width: 40%;"> {{ _("Syntax") }} </th>
<th> {{ _("Description") }} </th>
</tr>
<tr>
<td>
{{ _("YouTube Video") }}
</td>
<td>
{% raw %} {{ YouTubeVideo("embed_src") }} {% endraw %}
</td>
<td>
<span>
{{ _("Copy and paste the syntax in the editor. Replace 'embed_src' with the embed source
that YouTube provides. To get the source, follow the steps mentioned below.") }}
</span>
<ul class="p-4">
<li>
{{ _("Upload the video on youtube.") }}
</li>
<li>
{{ _("When you share a youtube video, it shows an option called Embed.") }}
</li>
<li>
{{ _("On clicking it, it provides an iframe. Copy the source (src) of the iframe and
paste it here.") }}
</li>
</ul>
</td>
</tr>
<tr>
<td>
{{ _("Quiz") }}
</td>
<td>
{% raw %} {{ Quiz("lms_quiz_id") }} {% endraw %}
</td>
<td>
{% set quiz_link = "<a href='/quizzes'> Quiz List </a>" %}
{{ _("Copy and paste the syntax in the editor. Replace 'lms_quiz_id' with the ID of the Quiz. You can get the ID of the quiz from the {0}.").format(quiz_link) }}
</td>
</tr>
</table>
</div>
{% endmacro %}
<!-- Discussions Component -->
{% macro Discussions() %}
{% set topics_count = frappe.db.count("Discussion Topic",
{"reference_doctype": "Course Lesson", "reference_docname": lesson.name}) %}
@@ -155,17 +285,17 @@
{% set redirect_to = "/courses/" + course.name %}
{% set empty_state_title = _("Have a doubt?") %}
{% set empty_state_subtitle = _("Post it here, our mentors will help you out.") %}
{% include "frappe/templates/discussions/discussions_section.html" %}
{% endmacro %}
<!-- Scripts -->
{%- block script %}
{{ super() }}
<script type="text/javascript">
var page_context = {{ page_context | tojson }};
var page_context = {{ page_context | tojson }};
</script>
{{ include_script('controls.bundle.js') }}
{% for ext in page_extensions %}
{{ ext.render_footer() }}
{% endfor %}

View File

@@ -1,6 +1,7 @@
frappe.ready(() => {
localStorage.removeItem($("#quiz-title").text());
localStorage.removeItem($("#quiz-title").data("name"));
fetch_assignments();
save_current_lesson();
@@ -45,37 +46,65 @@ frappe.ready(() => {
clear_work(e);
});
$(".btn-lesson").click((e) => {
save_lesson(e);
});
$(".add-attachment").click((e) => {
show_upload_modal();
});
$(".btn-start-quiz").click((e) => {
$("#start-banner").addClass("hide");
$("#quiz-form").removeClass("hide");
mark_active_question();
});
$(".btn-edit").click((e) => {
window.location.href = `${window.location.href}?edit=1`;
});
$(".btn-back").click((e) => {
window.location.href = window.location.href.split("?")[0];
})
$(document).on("click", ".copy-link", (e) => {
frappe.utils.copy_to_clipboard($(e.currentTarget).data("link"));
$(".attachments").collapse("hide");
});
if ($("#quiz-title").data("max-attempts")) {
window.addEventListener("beforeunload", (e) => {
e.returnValue = "";
$(".active-question").length && quiz_summary();
});
}
if ($("#body").length) {
make_editor();
}
});
const save_current_lesson = () => {
if ($(".title").hasClass("is-member")) {
frappe.call("lms.lms.api.save_current_lesson", {
course_name: $(".title").attr("data-course"),
lesson_name: $(".title").attr("data-lesson")
})
}
if ($(".title").hasClass("is-member")) {
frappe.call("lms.lms.api.save_current_lesson", {
course_name: $(".title").attr("data-course"),
lesson_name: $(".title").attr("data-lesson")
});
}
};
const enable_check = (e) => {
if ($(".option:checked").length) {
$("#check").removeAttr("disabled");
$(".custom-checkbox").removeClass("active-option");
$(".option:checked").closest(".custom-checkbox").addClass("active-option");
}
if ($(".option:checked").length) {
$("#check").removeAttr("disabled");
$(".custom-checkbox").removeClass("active-option");
$(".option:checked").closest(".custom-checkbox").addClass("active-option");
}
};
const mark_active_question = (e = undefined) => {
$(".timer").addClass("hide");
calculate_and_display_time(100);
@@ -92,82 +121,88 @@ const mark_active_question = (e = undefined) => {
initialize_timer();
};
const mark_progress = (e) => {
/* Prevent default only for Next button anchor tag and not for progress checkbox */
if ($(e.currentTarget).prop("nodeName") != "INPUT")
e.preventDefault();
else
return;
/* Prevent default only for Next button anchor tag and not for progress checkbox */
if ($(e.currentTarget).prop("nodeName") != "INPUT")
e.preventDefault();
else
return;
const target = $(e.currentTarget).attr("data-progress") ? $(e.currentTarget) : $("input.mark-progress");
const current_status = $(".lesson-progress").hasClass("hide") ? "Incomplete": "Complete";
const target = $(e.currentTarget).attr("data-progress") ? $(e.currentTarget) : $("input.mark-progress");
const current_status = $(".lesson-progress").hasClass("hide") ? "Incomplete": "Complete";
let status = "Incomplete";
if (target.prop("nodeName") == "INPUT" && target.prop("checked")) {
status = "Complete";
}
let status = "Incomplete";
if (target.prop("nodeName") == "INPUT" && target.prop("checked")) {
status = "Complete";
}
if (status != current_status) {
frappe.call({
method: "lms.lms.doctype.course_lesson.course_lesson.save_progress",
args: {
lesson: $(".title").attr("data-lesson"),
course: $(".title").attr("data-course"),
status: status
},
callback: (data) => {
change_progress_indicators(status, e);
show_certificate_if_course_completed(data);
if (status != current_status) {
frappe.call({
method: "lms.lms.doctype.course_lesson.course_lesson.save_progress",
args: {
lesson: $(".title").attr("data-lesson"),
course: $(".title").attr("data-course"),
status: status
},
callback: (data) => {
change_progress_indicators(status, e);
show_certificate_if_course_completed(data);
move_to_next_lesson(status, e);
}
});
}
else
move_to_next_lesson(status, e);
}
});
}
else
move_to_next_lesson(status, e);
};
const change_progress_indicators = (status, e) => {
if (status == "Complete") {
$(".lesson-progress").removeClass("hide");
$(".active-lesson .lesson-progress-tick").removeClass("hide");
}
else {
$(".lesson-progress").addClass("hide");
$(".active-lesson .lesson-progress-tick").addClass("hide");
}
if (status == "Incomplete" && !$(e.currentTarget).hasClass("next")) {
$(e.currentTarget).addClass("hide");
$("input.mark-progress").prop("checked", false).closest(".custom-checkbox").removeClass("hide");
}
if (status == "Complete") {
$(".lesson-progress").removeClass("hide");
$(".active-lesson .lesson-progress-tick").removeClass("hide");
}
else {
$(".lesson-progress").addClass("hide");
$(".active-lesson .lesson-progress-tick").addClass("hide");
}
if (status == "Incomplete" && !$(e.currentTarget).hasClass("next")) {
$(e.currentTarget).addClass("hide");
$("input.mark-progress").prop("checked", false).closest(".custom-checkbox").removeClass("hide");
}
};
const show_certificate_if_course_completed = (data) => {
if (data.message == 100 && !$(".next").attr("data-next") && $("#certification").hasClass("hide")) {
$("#certification").removeClass("hide");
$(".next").addClass("hide");
}
if (data.message == 100 && !$(".next").attr("data-next") && $("#certification").hasClass("hide")) {
$("#certification").removeClass("hide");
$(".next").addClass("hide");
}
};
const move_to_next_lesson = (status, e) => {
if ($(e.currentTarget).hasClass("next") && $(e.currentTarget).attr("data-href")) {
window.location.href = $(e.currentTarget).attr("data-href");
}
else if (status == "Complete") {
$("input.mark-progress").closest(".custom-checkbox").addClass("hide");
$("div.mark-progress").removeClass("hide");
$(".next").addClass("hide");
}
else {
$("input.mark-progress").closest(".custom-checkbox").removeClass("hide");
$("div.mark-progress").addClass("hide");
$(".next").removeClass("hide");
}
if ($(e.currentTarget).hasClass("next") && $(e.currentTarget).attr("data-href")) {
window.location.href = $(e.currentTarget).attr("data-href");
}
else if (status == "Complete") {
$("input.mark-progress").closest(".custom-checkbox").addClass("hide");
$("div.mark-progress").removeClass("hide");
$(".next").addClass("hide");
}
else {
$("input.mark-progress").closest(".custom-checkbox").removeClass("hide");
$("div.mark-progress").addClass("hide");
$(".next").removeClass("hide");
}
};
const quiz_summary = (e=undefined) => {
e && e.preventDefault();
var quiz_name = $("#quiz-title").text();
var total_questions = $(".question").length;
let quiz_name = $("#quiz-title").data("name");
let total_questions = $(".question").length;
frappe.call({
method: "lms.lms.doctype.lms_quiz.lms_quiz.quiz_summary",
args: {
@@ -175,7 +210,7 @@ const quiz_summary = (e=undefined) => {
"results": localStorage.getItem(quiz_name)
},
callback: (data) => {
var message = data.message == total_questions ? __("Excellent Work 👏") : __("Better luck next time")
let message = data.message == total_questions ? __("Excellent Work 👏") : __("Better luck next time")
$(".question").addClass("hide");
$("#summary").addClass("hide");
$("#quiz-form").parent().prepend(
@@ -183,20 +218,21 @@ const quiz_summary = (e=undefined) => {
<div class="font-weight-bold">${data.message}/${total_questions}</div></div>`);
$("#try-again").removeClass("hide");
}
})
});
};
const try_quiz_again = (e) => {
window.location.reload();
window.location.reload();
};
const check_answer = (e=undefined) => {
e && e.preventDefault();
clearInterval(self.timer);
$(".timer").addClass("hide");
var quiz_name = $("#quiz-title").text();
var total_questions = $(".question").length;
var current_index = $(".active-question").attr("data-qt-index");
let total_questions = $(".question").length;
let current_index = $(".active-question").attr("data-qt-index");
$(".explanation").removeClass("hide");
$("#check").addClass("hide");
@@ -212,193 +248,208 @@ const check_answer = (e=undefined) => {
else {
$("#next").removeClass("hide");
}
var [answer, is_correct] = parse_options();
add_to_local_storage(quiz_name, current_index, answer, is_correct);
let [answer, is_correct] = parse_options();
add_to_local_storage(current_index, answer, is_correct);
};
const parse_options = () => {
var answer = [];
var is_correct = [];
$(".active-question input").each((i, element) => {
var correct = parseInt($(element).attr("data-correct"));
if ($(element).prop("checked")) {
answer.push(decodeURIComponent($(element).val()));
correct && is_correct.push(1);
correct ? add_icon(element, "check") : add_icon(element, "wrong");
}
else {
correct && is_correct.push(0);
correct ? add_icon(element, "minus-circle-green") : add_icon(element, "minus-circle");
}
})
return [answer, is_correct];
let answer = [];
let is_correct = [];
$(".active-question input").each((i, element) => {
let correct = parseInt($(element).attr("data-correct"));
if ($(element).prop("checked")) {
answer.push(decodeURIComponent($(element).val()));
correct && is_correct.push(1);
correct ? add_icon(element, "check") : add_icon(element, "wrong");
}
else {
correct && is_correct.push(0);
correct ? add_icon(element, "minus-circle-green") : add_icon(element, "minus-circle");
}
});
return [answer, is_correct];
};
const add_icon = (element, icon) => {
$(element).closest(".custom-checkbox").removeClass("active-option");
var label = $(element).siblings(".option-text").text();
$(element).siblings(".option-text").html(`
<div>
<img class="mr-3" src="/assets/lms/icons/${icon}.svg">
${label}
</div>
`);
//$(element).parent().empty().html(`<div class="option-text"><img class="mr-3" src="/assets/lms/icons/${icon}.svg"> ${label}</div>`);
$(element).closest(".custom-checkbox").removeClass("active-option");
let label = $(element).siblings(".option-text").text();
$(element).siblings(".option-text").html(`
<div>
<img class="mr-3" src="/assets/lms/icons/${icon}.svg">
${label}
</div>
`);
//$(element).parent().empty().html(`<div class="option-text"><img class="mr-3" src="/assets/lms/icons/${icon}.svg"> ${label}</div>`);
};
const add_to_local_storage = (quiz_name, current_index, answer, is_correct) => {
var quiz_stored = JSON.parse(localStorage.getItem(quiz_name));
var quiz_obj = {
"question_index": current_index,
"answer": answer.join(),
"is_correct": is_correct
}
quiz_stored ? quiz_stored.push(quiz_obj) : quiz_stored = [quiz_obj]
localStorage.setItem(quiz_name, JSON.stringify(quiz_stored))
const add_to_local_storage = (current_index, answer, is_correct) => {
let quiz_name = $("#quiz-title").data("name")
let quiz_stored = JSON.parse(localStorage.getItem(quiz_name));
let quiz_obj = {
"question_index": current_index,
"answer": answer.join(),
"is_correct": is_correct
}
quiz_stored ? quiz_stored.push(quiz_obj) : quiz_stored = [quiz_obj]
localStorage.setItem(quiz_name, JSON.stringify(quiz_stored))
};
const create_certificate = (e) => {
e.preventDefault();
course = $(".title").attr("data-course");
frappe.call({
method: "lms.lms.doctype.lms_certificate.lms_certificate.create_certificate",
args: {
"course": course
},
callback: (data) => {
window.location.href = `/courses/${course}/${data.message.name}`;
}
})
e.preventDefault();
course = $(".title").attr("data-course");
frappe.call({
method: "lms.lms.doctype.lms_certificate.lms_certificate.create_certificate",
args: {
"course": course
},
callback: (data) => {
window.location.href = `/courses/${course}/${data.message.name}`;
}
});
};
const attach_work = (e) => {
const target = $(e.currentTarget);
let files = target.siblings(".attach-file").prop("files")
if (files && files.length) {
files = add_files(files)
return_as_dataurl(files)
files.map((file) => {
upload_file(file, target);
})
}
const target = $(e.currentTarget);
let files = target.siblings(".attach-file").prop("files")
if (files && files.length) {
files = add_files(files)
return_as_dataurl(files)
files.map((file) => {
upload_file(file, target);
})
}
};
const upload_file = (file, target) => {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState == XMLHttpRequest.DONE) {
if (xhr.status === 200) {
let response = JSON.parse(xhr.responseText)
create_lesson_work(response.message, target);
} else if (xhr.status === 403) {
let response = JSON.parse(xhr.responseText);
frappe.msgprint(`Not permitted. ${response._error_message || ''}`);
xhr.onreadystatechange = () => {
if (xhr.readyState == XMLHttpRequest.DONE) {
if (xhr.status === 200) {
let response = JSON.parse(xhr.responseText)
create_lesson_work(response.message, target);
} else if (xhr.status === 403) {
let response = JSON.parse(xhr.responseText);
frappe.msgprint(`Not permitted. ${response._error_message || ''}`);
} else if (xhr.status === 413) {
frappe.msgprint('Size exceeds the maximum allowed file size.');
} else if (xhr.status === 413) {
frappe.msgprint('Size exceeds the maximum allowed file size.');
} else {
frappe.msgprint(xhr.status === 0 ? 'XMLHttpRequest Error' : `${xhr.status} : ${xhr.statusText}`);
} else {
frappe.msgprint(xhr.status === 0 ? 'XMLHttpRequest Error' : `${xhr.status} : ${xhr.statusText}`);
}
}
}
}
}
xhr.open('POST', '/api/method/upload_file', true);
xhr.setRequestHeader('Accept', 'application/json');
xhr.setRequestHeader('X-Frappe-CSRF-Token', frappe.csrf_token);
xhr.open('POST', '/api/method/upload_file', true);
xhr.setRequestHeader('Accept', 'application/json');
xhr.setRequestHeader('X-Frappe-CSRF-Token', frappe.csrf_token);
let form_data = new FormData();
if (file.file_obj) {
form_data.append('file', file.file_obj, `${frappe.session.user}-${file.name}`);
form_data.append('folder', `${$(".title").attr("data-lesson")} ${$(".title").attr("data-course")}`)
}
let form_data = new FormData();
if (file.file_obj) {
form_data.append('file', file.file_obj, `${frappe.session.user}-${file.name}`);
form_data.append('folder', `${$(".title").attr("data-lesson")} ${$(".title").attr("data-course")}`)
}
xhr.send(form_data);
});
};
xhr.send(form_data);
});
}
const create_lesson_work = (file, target) => {
frappe.call({
method: "lms.lms.doctype.lesson_assignment.lesson_assignment.upload_assignment",
args: {
assignment: file.file_url,
lesson: $(".title").attr("data-lesson"),
identifier: target.siblings(".attach-file").attr("id")
},
callback: (data) => {
target.siblings(".attach-file").addClass("hide");
target.siblings(".preview-work").removeClass("hide");
target.siblings(".preview-work").find("a").attr("href", file.file_url).text(file.file_name)
target.addClass("hide");
}
});
frappe.call({
method: "lms.lms.doctype.lesson_assignment.lesson_assignment.upload_assignment",
args: {
assignment: file.file_url,
lesson: $(".title").attr("data-lesson"),
identifier: target.siblings(".attach-file").attr("id")
},
callback: (data) => {
target.siblings(".attach-file").addClass("hide");
target.siblings(".preview-work").removeClass("hide");
target.siblings(".preview-work").find("a").attr("href", file.file_url).text(file.file_name)
target.addClass("hide");
}
});
};
const return_as_dataurl = (files) => {
let promises = files.map(file =>
frappe.dom.file_to_base64(file.file_obj)
.then(dataurl => {
file.dataurl = dataurl;
this.on_success && this.on_success(file);
})
);
return Promise.all(promises);
}
let promises = files.map(file =>
frappe.dom.file_to_base64(file.file_obj)
.then(dataurl => {
file.dataurl = dataurl;
this.on_success && this.on_success(file);
})
);
return Promise.all(promises);
};
const add_files = (files) => {
files = Array.from(files).map(file => {
let is_image = file.type.startsWith('image');
return {
file_obj: file,
cropper_file: file,
crop_box_data: null,
optimize: this.attach_doc_image ? true : false,
name: file.name,
doc: null,
progress: 0,
total: 0,
failed: false,
request_succeeded: false,
error_message: null,
uploading: false,
private: !is_image
}
});
return files
files = Array.from(files).map(file => {
let is_image = file.type.startsWith('image');
return {
file_obj: file,
cropper_file: file,
crop_box_data: null,
optimize: this.attach_doc_image ? true : false,
name: file.name,
doc: null,
progress: 0,
total: 0,
failed: false,
request_succeeded: false,
error_message: null,
uploading: false,
private: !is_image
}
});
return files
};
const clear_work = (e) => {
const target = $(e.currentTarget);
const parent = target.closest(".preview-work");
parent.addClass("hide");
parent.siblings(".attach-file").removeClass("hide").val(null);
parent.siblings(".submit-work").removeClass("hide");
const target = $(e.currentTarget);
const parent = target.closest(".preview-work");
parent.addClass("hide");
parent.siblings(".attach-file").removeClass("hide").val(null);
parent.siblings(".submit-work").removeClass("hide");
};
const fetch_assignments = () => {
if ($(".attach-file").length <= 0)
return;
frappe.call({
method: "lms.lms.doctype.lesson_assignment.lesson_assignment.get_assignment",
args: {
"lesson": $(".title").attr("data-lesson")
},
callback: (data) => {
if (data.message && data.message.length) {
const assignments = data.message;
for (let i in assignments) {
let target = $(`#${assignments[i]["id"]}`);
target.addClass("hide");
target.siblings(".submit-work").addClass("hide");
target.siblings(".preview-work").removeClass("hide");
target.siblings(".preview-work").find("a").attr("href", assignments[i]["assignment"]).text(assignments[i]["file_name"]);
if ($(".attach-file").length <= 0)
return;
frappe.call({
method: "lms.lms.doctype.lesson_assignment.lesson_assignment.get_assignment",
args: {
"lesson": $(".title").attr("data-lesson")
},
callback: (data) => {
if (data.message && data.message.length) {
const assignments = data.message;
for (let i in assignments) {
let target = $(`#${assignments[i]["id"]}`);
target.addClass("hide");
target.siblings(".submit-work").addClass("hide");
target.siblings(".preview-work").removeClass("hide");
target.siblings(".preview-work").find("a").attr("href", assignments[i]["assignment"]).text(assignments[i]["file_name"]);
}
}
}
}
});
}
});
};
const initialize_timer = () => {
this.time_left = $(".timer").data("time");
calculate_and_display_time(100, this.time_left);
@@ -423,6 +474,7 @@ const initialize_timer = () => {
}, 100);
};
const calculate_and_display_time = (percent_time) => {
$(".timer .progress-bar").attr("aria-valuenow", percent_time);
$(".timer .progress-bar").attr("aria-valuemax", percent_time);
@@ -430,3 +482,81 @@ const calculate_and_display_time = (percent_time) => {
let progress_color = percent_time < 20 ? "red" : "var(--primary-color)";
$(".timer .progress-bar").css("background-color", progress_color);
};
const save_lesson = (e) => {
let lesson = $("#title").data("lesson");
frappe.call({
method: "lms.lms.doctype.lms_course.lms_course.save_lesson",
args: {
"title": $("#title").text(),
"body": this.code_field_group.fields_dict["code_md"].last_value,
"chapter": $("#title").data("chapter"),
"preview": $("#preview").prop("checked") ? 1 : 0,
"idx": $("#title").data("index"),
"lesson": lesson ? lesson : ""
},
callback: (data) => {
window.location.href = window.location.href.split("?")[0];
}
});
};
const show_upload_modal = () => {
new frappe.ui.FileUploader({
folder: "Home/Attachments",
restrictions: {
allowed_file_types: ['image/*', 'video/*']
},
on_success: (file_doc) => {
$(".attachments").append(build_attachment_table(file_doc));
let count = $(".attachment-count").data("count") + 1;
$(".attachment-count").data("count", count);
$(".attachment-count").html(__(`${count} attachments`));
$(".attachments").removeClass("hide");
},
});
};
const build_attachment_table = (file_doc) => {
let video_types = ["mov", "mp4", "mkv"];
let video_extension = file_doc.file_url.split(".").pop();
let is_video = video_types.indexOf(video_extension) >= 0;
let link = is_video ? `{{ Video('${file_doc.file_url}') }}` : `![](${file_doc.file_url})`;
return $(`
<tr class="attachment-row">
<td>${file_doc.file_name}</td>
<td class="">
<a class="button is-secondary button-links copy-link" data-link="${link}"
data-name="${file_doc.file_name}" > ${__("Copy Link")}
</a>
</td>
</tr>
`);
};
const make_editor = () => {
this.code_field_group = new frappe.ui.FieldGroup({
fields: [
{
fieldname: "code_md",
fieldtype: "Code",
options: "Markdown",
wrap: true,
max_lines: Infinity,
min_lines: 20,
default: $("#body").data("body"),
depends_on: 'eval:doc.type=="Markdown"',
}
],
body: $("#body").get(0),
});
this.code_field_group.make();
$("#body .form-section:last").removeClass("empty-section");
$("#body .frappe-control").removeClass("hide-control");
$("#body .form-column").addClass("p-0");
};

View File

@@ -1,9 +1,7 @@
from re import I
import frappe
from lms.www.utils import get_common_context, redirect_to_lesson
from lms.lms.utils import get_lesson_url
from lms.lms.utils import get_lesson_url, is_instructor, redirect_to_courses_list
from frappe.utils import cstr, flt
from lms.www import batch
def get_context(context):
get_common_context(context)
@@ -11,6 +9,12 @@ def get_context(context):
chapter_index = frappe.form_dict.get("chapter")
lesson_index = frappe.form_dict.get("lesson")
lesson_number = f"{chapter_index}.{lesson_index}"
context.lesson_index = lesson_index
context.chapter = frappe.db.get_value("Chapter Reference", {
"idx": chapter_index,
"parent": context.course.name
}, "chapter")
if not chapter_index or not lesson_index:
if context.batch:
index_ = get_lesson_index(context.course, context.batch, frappe.session.user) or "1.1"
@@ -19,11 +23,19 @@ def get_context(context):
redirect_to_lesson(context.course, index_)
context.lesson = get_current_lesson_details(lesson_number, context)
neighbours = get_neighbours(lesson_number, context.lessons)
context.next_url = get_url(neighbours["next"], context.course)
context.prev_url = get_url(neighbours["prev"], context.course)
if not context.lesson:
context.lesson = frappe._dict()
meta_info = context.lesson.title + " - " + context.course.title
if frappe.form_dict.get("edit"):
if not is_instructor(context.course.name):
redirect_to_courses_list()
context.lesson.edit_mode = True
else:
neighbours = get_neighbours(lesson_number, context.lessons)
context.next_url = get_url(neighbours["next"], context.course)
context.prev_url = get_url(neighbours["prev"], context.course)
meta_info = context.lesson.title + " - " + context.course.title if context.lesson.title else "New Lesson"
context.metatags = {
"title": meta_info,
"keywords": meta_info,
@@ -34,23 +46,34 @@ def get_context(context):
context.page_context = {
"course": context.course.name,
"batch": context.get("batch") and context.batch.name,
"lesson": context.lesson.name,
"lesson": context.lesson.name if context.lesson.name else "New Lesson",
"is_member": context.membership is not None
}
def get_current_lesson_details(lesson_number, context):
details_list = list(filter(lambda x: cstr(x.number) == lesson_number, context.lessons))
if not len(details_list):
redirect_to_lesson(context.course)
return details_list[0]
if frappe.form_dict.get("edit"):
return None
else:
redirect_to_lesson(context.course)
lesson_info = details_list[0]
lesson_info.body = lesson_info.body.replace("\"", "'")
return lesson_info
def get_url(lesson_number, course):
return get_lesson_url(course.name, lesson_number) and get_lesson_url(course.name, lesson_number) + course.query_parameter
def get_lesson_index(course, batch, user):
lesson = batch.get_current_lesson(user)
return lesson and course.get_lesson_index(lesson)
def get_page_extensions(context):
default_value = ["lms.plugins.PageExtension"]
classnames = frappe.get_hooks("lms_lesson_page_extensions") or default_value
@@ -59,6 +82,7 @@ def get_page_extensions(context):
e.set_context(context)
return extensions
def get_neighbours(current, lessons):
current = flt(current)
numbers = sorted(lesson.number for lesson in lessons)

77
lms/www/batch/quiz.html Normal file
View File

@@ -0,0 +1,77 @@
{% extends "templates/base.html" %}
{% block title %}
{{ _("Quiz List") }}
{% endblock %}
{% block head_include %}
{% include "public/icons/symbol-defs.svg" %}
{% endblock %}
{% block content %}
<div class="common-page-style" style="background-color: var(--fg-color);">
<div class="container">
{{ BreadCrumb(quiz) }}
{{ QuizCard(quiz) }}
</div>
</div>
{% endblock %}
{% macro BreadCrumb(quiz) %}
<div class="breadcrumb">
<a class="dark-links" href="/quizzes">{{ _("Quizzes") }}</a>
<img class="ml-1 mr-1" src="/assets/lms/icons/chevron-right.svg">
<span class="breadcrumb-destination">{{ quiz.title if quiz.title else _("New Quiz") }}</span>
</div>
{% endmacro %}
{% macro QuizCard(quiz) %}
<div style="width: 60%;">
<div class="course-home-headings mb-2" data-placeholder="{{ _('Quiz Title') }}" id="quiz-title"
{% if quiz.name %} data-name="{{ quiz.name }}" {% endif %}
contenteditable="true" >{% if quiz.title %}{{ quiz.title }}{% endif %}</div>
{% if quiz.questions %}
{% for question in quiz.questions %}
<div class="quiz-card">
<div contenteditable="true" data-placeholder="{{ _('Question') }}" data-question="{{ question.name }}"
class="question mb-4">{% if question.question %} {{ question.question }} {% endif %}</div>
{% for i in range(1,5) %}
{% set num = frappe.utils.cstr(i) %}
{% set option = question["option_" + num] %}
{% set explanation = question["explanation_" + num] %}
<div class="mt-4">
<label class=""> {{ _("Option") }} {{ num }} </label>
<div class="d-flex justify-content-between option-{{ num }}">
<div contenteditable="true" data-placeholder="{{ _('Option') }}"
class="option-input">{% if option %}{{ option }}{% endif %}</div>
<div contenteditable="true" data-placeholder="{{ _('Explanation') }}"
class="option-input">{% if explanation %}{{ explanation }}{% endif %}</div>
<div class="option-checkbox">
<input type="checkbox" {% if question['is_correct_' + num] %} checked {% endif %}>
<label class="mb-0"> {{ _("Is Correct") }} </label>
</div>
</div>
</div>
{% endfor %}
</div>
{% endfor %}
{% endif %}
<div class="mt-4">
<button class="btn btn-secondary btn-sm btn-question"> {{ _("New Question") }} </button>
<button class="btn btn-primary btn-sm btn-save-question ml-2
{% if not quiz.name %} hide {% endif %}"> {{ _("Save Quiz") }} </button>
</div>
</div>
{% endmacro %}

106
lms/www/batch/quiz.js Normal file
View File

@@ -0,0 +1,106 @@
frappe.ready(() => {
$(".btn-question").click((e) => {
add_question(e);
});
$(".btn-save-question").click((e) => {
save_question(e);
});
get_questions()
});
const add_question = (e) => {
let add_after = $(".quiz-card").length ? $(".quiz-card:last") : $("#quiz-title");
let question_template = `<div class="quiz-card">
<div contenteditable="true" data-placeholder="${__("Question")}" class="question mb-4"></div>
</div>`;
$(question_template).insertAfter(add_after);
get_question_template();
$(".btn-save-question").removeClass("hide");
};
const get_question_template = () => {
Array.from({length: 4}, (x, num) => {
let option_template = get_option_template(num + 1);
let add_after = $(".quiz-card:last .option-group").length ? $(".quiz-card:last .option-group").last() : $(".question:last");
question_template = $(option_template).insertAfter(add_after);
});
};
const get_option_template = (num) => {
return `<div class="option-group mt-4">
<label class="">${__("Option")} ${num}</label>
<div class="d-flex justify-content-between option-${num}">
<div contenteditable="true" data-placeholder="${ __("Option") }"
class="option-input"></div>
<div contenteditable="true" data-placeholder="${ __('Explanation') }"
class="option-input"></div>
<div class="option-checkbox">
<input type="checkbox">
<label class="mb-0"> ${ __("Is Correct") } </label>
</div>
</div>
</div>`;
};
const save_question = (e) => {
if (!$("#quiz-title").text()) {
frappe.throw(__("Quiz Title is mandatory."));
}
frappe.call({
method: "lms.lms.doctype.lms_quiz.lms_quiz.save_quiz",
args: {
"quiz_title": $("#quiz-title").text(),
"questions": get_questions(),
"quiz": $("#quiz-title").data("name") || ""
},
callback: (data) => {
window.location.href = "/quizzes";
}
});
};
const get_questions = () => {
let questions = [];
$(".quiz-card").each((i, el) => {
if (!$(el).find(".question").text())
return;
let details = {};
let one_correct_option = false;
details["question"] = $(el).find(".question").text();
details["question_name"] = $(el).find(".question").data("question") || "";
Array.from({length: 4}, (x, i) => {
let num = i + 1;
details[`option_${num}`] = $(el).find(`.option-${num} .option-input:first`).text();
details[`explanation_${num}`] = $(el).find(`.option-${num} .option-input:last`).text();
let is_correct = $(el).find(`.option-${num} .option-checkbox`).find("input").prop("checked");
if (is_correct)
one_correct_option = true;
details[`is_correct_${num}`] = is_correct;
});
if (!details["option_1"] || !details["option_2"])
frappe.throw(__("Each question must have at least two options."))
if (!one_correct_option)
frappe.throw(__("Each question must have at least one correct option."))
questions.push(details);
});
return questions;
};

21
lms/www/batch/quiz.py Normal file
View File

@@ -0,0 +1,21 @@
import frappe
from frappe.utils import cstr
def get_context(context):
quizname = frappe.form_dict["quizname"]
if quizname == "new-quiz":
context.quiz = frappe._dict()
context.quiz.edit_mode = 1
else:
fields_arr = ["name","question"]
for num in range(1,5):
fields_arr.append("option_" + cstr(num))
fields_arr.append("is_correct_" + cstr(num))
fields_arr.append("explanation_" + cstr(num))
context.quiz = frappe.db.get_value("LMS Quiz", quizname, ["title", "name"], as_dict=1)
context.quiz.questions = frappe.get_all("LMS Quiz Question", {
"parent": quizname
}, fields_arr,
order_by="idx")

View File

@@ -0,0 +1,52 @@
{% extends "templates/base.html" %}
{% block title %}
{{ _("Quiz List") }}
{% endblock %}
{% block head_include %}
{% include "public/icons/symbol-defs.svg" %}
{% endblock %}
{% block content %}
<div class="common-page-style">
<div class="container">
{% if quiz_list | length %}
<a class="btn btn-secondary btn-sm pull-right" href="/quizzes/new-quiz"> {{ _("Add Quiz") }} </a>
<div class="course-home-headings"> {{ _("Quiz List") }} </div>
<div class="common-card-style">
<table class="table">
<tr style="background-color: var(--fg-hover-color); font-weight: bold">
<td style="width: 10%;"> {{ _("No.") }} </td>
<td style="width: 45%;"> {{ _("Title") }} </td>
<td> {{ _("ID") }} </td>
</tr>
{% for quiz in quiz_list %}
<tr style="position: relative; color: var(--text-color);">
<td> {{ loop.index }} </td>
<td>
<a class="button-links" href="/quizzes/{{ quiz.name }}">{{ quiz.title }}</a>
</td>
<td>
<a class="button-links" href="/quizzes/{{ quiz.name }}">{{ quiz.name }}</a>
</td>
</tr>
{% endfor %}
</table>
</div>
{% else %}
<div class="empty-state">
<div class="empty-state-text">
<div class="empty-state-heading">{{ _("You have not created any quiz yet.") }}</div>
<div class="course-meta mb-6">{{ _("Create a quiz and add it to your course to engage users.") }}</div>
<a class="btn btn-secondary btn-sm"
href="{% if frappe.session.user == 'Guest' %} /login?redirect-to=/quizzes {% else %} /quizzes/new-quiz {% endif %}">
{{ _("Add Quiz") }} </a>
</div>
</div>
{% endif %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,4 @@
import frappe
def get_context(context):
context.quiz_list = frappe.get_all("LMS Quiz", {"owner": frappe.session.user}, ["name", "title"])

View File

@@ -1,100 +1,351 @@
{% extends "templates/base.html" %}
{% block title %}{{ course.title }}
{% block title %}
{{ course.title if course.title else _("New Course") }}
{% endblock %}
{% block head_include %}
{% include "public/icons/symbol-defs.svg" %}
<link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
{% include "public/icons/symbol-defs.svg" %}
<link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
{% endblock %}
{% block content %}
<div class="common-page-style pt-0 pb-0">
<div class="course-home-top-container">
{{ CourseHomeHeader(course) }}
<div class="course-home-page">
<div class="container">
<div class="course-body-container">
{{ CourseHeaderOverlay(course) }}
{{ Description(course) }}
{{ widgets.CourseOutline(course=course, membership=membership, is_user_interested=is_user_interested) }}
{{ widgets.Reviews(course=course, membership=membership) }}
<div class="course-home-top-container">
{{ CourseHomeHeader(course) }}
<div class="course-home-page">
<div class="container">
<div class="course-body-container">
{{ CourseHeaderOverlay(course) }}
{{ Description(course) }}
{{ Save(course) }}
{{ widgets.CourseOutline(course=course, membership=membership, is_user_interested=is_user_interested) }}
{% if not course.edit_mode %}
{{ widgets.Reviews(course=course, membership=membership) }}
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
{{ RelatedCourses(course) }}
</div>
{% endblock %}
{% macro CourseHomeHeader(course) %}
<div class="course-head-container">
<div class="container pt-8 pb-10">
<div class="course-card-wide">
{{ BreadCrumb(course) }}
{{ CourseCardWide(course) }}
</div>
</div>
</div>
{% endmacro %}
<!-- BreadCrumb -->
{% macro BreadCrumb(course) %}
<div class="breadcrumb">
<a class="dark-links" href="/courses">{{ _("All Courses") }}</a>
<img class="ml-1 mr-1" src="/assets/lms/icons/chevron-right.svg">
<span class="breadcrumb-destination">{{ course.title }}</span>
</div>
{% endmacro %}
<!-- Course Card -->
{% macro CourseCardWide(course) %}
<div class="d-flex align-items-center mt-8">
{% for tag in get_tags(course.name) %}
<div class="course-card-pills">{{ tag }}</div>
{% endfor %}
</div>
<div class="course-card-wide-title">
{{ course.title }}
</div>
<div class="">
{{ course.short_introduction }}
</div>
<div class="mt-8">
<div class="bold-heading">{{ _("Instructors") }}:</div>
{% for instructor in get_instructors(course.name) %}
<div class="mt-1">
{{ widgets.Avatar(member=instructor, avatar_class="avatar-small") }}
<a class="button-links" href="{{ get_profile_url(instructor.username) }}">
<span class="course-instructor"> {{ instructor.full_name }} </span>
</a>
</div>
{% endfor %}
</div>
{% if membership %}
{% set progress = frappe.utils.cint(membership.progress) %}
<div class="mt-8">
<div class="progress-percent m-0">{{ progress }}% {{ _("Completed") }}</div>
<div class="progress" title="{{ progress }}% Completed">
<div class="progress-bar" role="progressbar" aria-valuenow="{{ progress }}"
aria-valuemin="0" aria-valuemax="100" style="width:{{ progress }}%">
<div class="container pt-8 pb-10">
<div class="course-card-wide">
{{ BreadCrumb(course) }}
{{ CourseCardWide(course) }}
</div>
</div>
</div>
{% endif %}
{% endmacro%}
{% endmacro %}
<!-- BreadCrumb -->
{% macro BreadCrumb(course) %}
<div class="breadcrumb">
<a class="dark-links" href="/courses">{{ _("All Courses") }}</a>
<img class="ml-1 mr-1" src="/assets/lms/icons/chevron-right.svg">
<span class="breadcrumb-destination">{{ course.title if course.title else _("New Course") }}</span>
</div>
{% endmacro %}
<!-- Course Card -->
{% macro CourseCardWide(course) %}
<div class="d-flex align-items-center mt-8">
{% for tag in get_tags(course.name) %}
<div class="course-card-pills" {% if course.edit_mode %} contenteditable="true" {% endif %}>{{ tag }}
{% if course.edit_mode %}
<span class="btn-delete-tag">
<svg class="icon icon-sm">
<use class="" href="#icon-close"></use>
</svg>
</span>
{% endif %}
</div>
{% endfor %}
{% if course.edit_mode %}
<button class="btn btn-default btn-sm btn-tag"> {{ _("Add Tag") }} </button>
{% endif %}
</div>
<div {% if course.edit_mode %} data-placeholder="{{ _('Title') }}" contenteditable="true" {% endif %}
id="title" {% if course.name %} data-course="{{ course.name | urlencode }}" {% endif %}
class="course-card-wide-title">{% if course.title %} {{ course.title }} {% endif %}</div>
<div {% if course.edit_mode %} contenteditable="true" data-placeholder="{{ _('Short Introduction') }}"
{% endif %} id="intro" >{% if course.short_introduction %} {{ course.short_introduction }} {% endif %}</div>
{% if course.edit_mode %}
<div class="preview-video-header">
<div class="d-block mt-1" contenteditable="true" id="video-link"
data-placeholder=" {{ _('Preview Video Link') }} ">{% if course.video_link %}{{ course.video_link }}{% endif %}</div>
<div class="preview-info">
<div class="tool-tip">
<div class="tooltiptext">
<span>
{{ _('If you have a video that provides a teaser or preview of the course, you can add it here.') }}
</span>
<span>
{{ _("Follow the steps mentioned below for the same.") }}
</span>
<ul>
<li>
{{ _("Upload the video on youtube.") }}
</li>
<li>
{{ _("When you share a youtube video, it shows an option called Embed.") }}
</li>
<li>
{{ _("On clicking it, it provides an iframe. Copy the source (src) of the iframe and paste it here.") }}
</li>
</ul>
</div>
<svg class="icon icon-md">
<use href="#icon-solid-info"></use>
</svg>
</div>
</div>
</div>
<div class="course-image-attachment {% if not course.image %} hide {% endif %} ">
<a href="{{ course.image }}" id="image" target="_blank"> {{ course.image }} </a>
<button class="btn btn-sm btn-default btn-clear ml-4"> {{ _("Clear") }} </button>
</div>
<a class="btn btn-default btn-sm btn-attach mt-1 {% if course.image %} hide {% endif %}"> {{ _("Attach Image") }} </a>
{% endif %}
{% if not course.edit_mode %}
<div class="mt-8">
<div class="bold-heading">{{ _("Instructors") }}:</div>
{% for instructor in get_instructors(course.name) %}
<div class="mt-1">
{{ widgets.Avatar(member=instructor, avatar_class="avatar-small") }}
<a class="button-links" href="{{ get_profile_url(instructor.username) }}">
<span class="course-instructor"> {{ instructor.full_name }} </span>
</a>
</div>
{% endfor %}
</div>
{% endif %}
{% if membership and not course.edit_mode %}
{% set progress = frappe.utils.cint(membership.progress) %}
<div class="mt-8">
<div class="progress-percent m-0">{{ progress }}% {{ _("Completed") }}</div>
<div class="progress" title="{{ progress }}% Completed">
<div class="progress-bar" role="progressbar" aria-valuenow="{{ progress }}"
aria-valuemin="0" aria-valuemax="100" style="width:{{ progress }}%">
</div>
</div>
</div>
{% endif %}
{% endmacro %}
<!-- Overlay -->
{% macro CourseHeaderOverlay(course) %}
<div class="course-overlay-card">
{% if not course.edit_mode %}
<div class="course-overlay-card">
{% if course.video_link %}
<iframe class="preview-video" src="{{ course.video_link }}"></iframe>
{% endif %}
{% if course.video_link %}
<iframe class="preview-video" src="{{ course.video_link }}"></iframe>
{% endif %}
<div class="course-overlay-content">
<div class="course-overlay-title"> {{ course.title }} </div>
<div class="course-overlay-content">
<div class="course-overlay-title"> {{ course.title }} </div>
{{ Notes(course) }}
<div class="vertically-center mb-3">
<svg class="icon icon-md mr-1">
<use class="" href="#icon-users">
</svg>
{{ get_students(course.name) | length }} {{ _("Enrolled") }}
</div>
{% if get_lessons(course.name) | length %}
<div class="vertically-center mb-3">
<svg class="icon icon-md mr-1">
<use href="#icon-education"></use>
</svg>
{{ get_lessons(course.name) | length }} {{ _("Lessons") }}
</div>
{% endif %}
{% if course.paid_certificate %}
<div class="vertically-center mb-3">
<svg class="icon icon-md mr-1">
<use href="#icon-badge"></use>
</svg>
<span class="certificate-price" data-price="{{ course.price_certificate }}">
{{ format_amount(course.price_certificate, course.currency) }}
</span>
<span class="indicator-pill green ml-3"> {{ _("Get Certified") }} </span>
</div>
{% endif %}
{{ CTASection(course, membership) }}
</div>
</div>
{{ SlotModal(course) }}
{% endif %}
{% endmacro %}
<!-- Description -->
{% macro Description(course) %}
<div class="course-description-section" {% if course.edit_mode %} style="min-height: 100px" {% endif %}
{% if course.edit_mode %} contenteditable="true" {% endif %} id="description"
data-placeholder="Description">{% if course.description %}{{ frappe.utils.md_to_html(course.description) }}{% endif %}</div>
{% endmacro %}
<!-- Save -->
{% macro Save(course) %}
{% if course.edit_mode %}
<div class="my-4">
<button class="btn btn-primary btn-md btn-save-course">
{{ _("Save Course Details") }}
</button>
{% if course.name %}
<a class="btn btn-secondary btn-md btn-exit-edit ml-2" href="/courses/{{ course.name }}">
{{ _("Back to Course") }}
</a>
{% endif %}
</div>
{% endif %}
{% endmacro %}
{% macro CourseCreator(course) %}
<div class="course-home-headings"> {{ _("Course Creators") }} </div>
<div class="common-card-style course-creators-card">
{% set instructors = get_instructors(course.name) %}
{% for instructor in instructors %}
<div class="d-flex align-items-center">
{{ widgets.Avatar(member=instructor, avatar_class="avatar-medium") }}
<div class="ml-4">
<div class="course-creator-name"> {{ instructor.full_name }} </div>
<div class="course-meta"> {{ get_authored_courses(instructor.name) | length }} {{ _("Courses Created") }} </div>
</div>
</div>
{% endfor %}
</div>
{% endmacro %}
<!-- Related Courses Section -->
{% macro RelatedCourses(course) %}
{% if course.related_courses | length %}
<div class="related-courses">
<div class="container">
<div class="course-home-headings"> {{ _("Other Courses") }} </div>
<div class="carousel slide" id="carouselExampleControls" data-ride="carousel" data-interval="false">
<div class="carousel-inner">
{% for crs in course.related_courses %}
{% if loop.index % 3 == 1 %}
<div class="carousel-item {% if loop.index == 1 %} active {% endif %}"><div class="cards-parent">
{% endif %}
{{ widgets.CourseCard(course=crs, read_only=False) }}
{% if loop.index % 3 == 0 or loop.index == course.related_courses | length %} </div> </div> {% endif %}
{% endfor %}
</div>
{% if course.related_courses | length > 3 %}
<div class="slider-controls">
<a class="carousel-control-prev" href="#carouselExampleControls" role="button" data-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
</a>
<a class="carousel-control-next" href="#carouselExampleControls" role="button" data-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
</a>
</div>
{% endif %}
</div>
</div>
</div>
{% endif %}
{% endmacro %}
<!-- CTA's -->
{% macro CTASection(course, membership) %}
{% set lesson_index = get_lesson_index(membership.current_lesson) if membership and
membership.current_lesson else "1.1" if first_lesson_exists(course.name) else None %}
{% if show_start_learing_cta %}
<div class="btn btn-primary wide-button join-batch" data-course="{{ course.name | urlencode }}">
{{ _("Start Learning") }}
<img class="ml-2" src="/assets/lms/icons/white-arrow.svg" />
</div>
{% elif is_instructor(course.name) and not course.published and course.status != "Under Review" %}
<div class="btn btn-primary wide-button mb-2" id="submit-for-review" data-course="{{ course.name | urlencode }}">
{{ _("Submit for Review") }}
</div>
{% elif is_instructor(course.name) and lesson_index %}
<a class="btn btn-primary wide-button" id="continue-learning"
href="{{ get_lesson_url(course.name, lesson_index) }}{{ course.query_parameter }}">
{{ _("Checkout Course") }} <img class="ml-2" src="/assets/lms/icons/white-arrow.svg" />
</a>
{% elif course.upcoming and not is_user_interested %}
<div class="btn btn-secondary wide-button notify-me" data-course="{{course.name | urlencode}}">
{{ _("Notify me when available") }}
</div>
{% elif is_cohort_staff(course.name, frappe.session.user) %}
<a class="btn btn-secondary button-links wide-button" href="/courses/{{course.name}}/manage">
{{ _("Manage the course") }}
</a>
{% elif membership %}
<a class="btn btn-primary wide-button" id="continue-learning"
href="{{ get_lesson_url(course.name, lesson_index) }}{{ course.query_parameter }}">
{{ _("Continue Learning") }} <img class="ml-2" src="/assets/lms/icons/white-arrow.svg" />
</a>
{% endif %}
{% set progress = frappe.utils.cint(membership.progress) %}
{% if membership and course.enable_certification %}
{% if certificate %}
<a class="btn btn-secondary wide-button mt-3" href="/courses/{{ course.name }}/{{ certificate }}">
{{ _("Get Certificate") }}
</a>
{% elif eligible_for_evaluation %}
<a class="btn btn-secondary wide-button mt-3" id="apply-certificate" data-course="{{ course.name }}">
{{ _("Apply for Certificate") }}
</a>
{% elif course.grant_certificate_after == "Completion" and progress == 100 %}
<div class="btn btn-secondary wide-button is-secondary mt-3" id="certification" data-course="{{ course.name }}">
{{ _("Get Certificate") }}
</div>
{% endif %}
{% endif %}
{% if is_instructor(course.name) %}
<a class="btn btn-secondary wide-button" href="/courses/{{ course.name }}?edit=1"> {{ _("Edit Course") }} </a>
{% endif %}
{% endmacro %}
<!-- Notes and Messages -->
{% macro Notes(course) %}
<div id="interest-alert" class="{% if not is_user_interested %} hide {% endif %}">
{{ _("You have opted to be notified for this course. You will receive an email when the course becomes available.") }}
</div>
@@ -110,211 +361,56 @@
{{ _("Your course is currently under review. Once the review is complete, the System Admins will publish it on the website.") }}
</div>
{% endif %}
{% if no_of_attempts and no_of_attempts >= course.max_attempts %}
<p> {{ _("You have exceeded the maximum number of attempts allowed to appear for evaluations of this course.") }} </p>
{% endif %}
{% endmacro %}
{% if is_instructor(course.name) %}
<div class="vertically-center mb-4">
<a class="button is-default button-links mr-2" href="/course?name={{ course.name }}"> {{ _("Edit") }} </a>
<a class="button is-default button-links mr-2" href="/chapter">
<svg class="icon icon-sm mr-1"><use href="#icon-add"></use></svg>
{{ _("Add Chapter") }}
</a>
<a class="button is-default button-links mr-2" href="/lesson">
<svg class="icon icon-sm mr-1"><use href="#icon-add"></use></svg>
{{ _("Add Lesson") }}
</a>
</div>
{% endif %}
<div class="vertically-center mb-3">
<svg class="icon icon-md mr-1">
<use class="" href="#icon-users">
</svg>
{{ get_students(course.name) | length }} {{ _("Enrolled") }}
</div>
{% if get_lessons(course.name) | length %}
<div class="vertically-center mb-3">
<svg class="icon icon-md mr-1">
<use href="#icon-education"></use>
</svg>
{{ get_lessons(course.name) | length }} {{ _("Lessons") }}
</div>
{% endif %}
{% if course.paid_certificate %}
<div class="vertically-center mb-3">
<svg class="icon icon-md mr-1">
<use href="#icon-badge"></use>
</svg>
<span class="certificate-price" data-price="{{ course.price_certificate }}">
{{ format_amount(course.price_certificate, course.currency) }}
</span>
<span class="indicator-pill green ml-3"> {{ _("Get Certified") }} </span>
</div>
{% endif %}
{% set lesson_index = get_lesson_index(membership.current_lesson) if membership and
membership.current_lesson
else '1.1' %}
{% if show_start_learing_cta %}
<div class="button wide-button is-primary join-batch" data-course="{{ course.name | urlencode }}">
{{ _("Start Learning") }}
<img class="ml-2" src="/assets/lms/icons/white-arrow.svg" />
</div>
{% elif is_instructor(course.name) and not course.published and course.status == "Under Review" %}
<div class="button wide-button is-primary" id="submit-for-review" data-course="{{ course.name | urlencode }}">
{{ _("Submit for Review") }}
<img class="ml-2" src="/assets/lms/icons/white-arrow.svg" />
</div>
{% elif is_instructor(course.name) %}
<a class="button wide-button is-primary" id="continue-learning"
href="{{ get_lesson_url(course.name, lesson_index) }}{{ course.query_parameter }}">
{{ _("Checkout Course") }} <img class="ml-2" src="/assets/lms/icons/white-arrow.svg" />
</a>
{% elif course.upcoming and not is_user_interested %}
<div class="button wide-button is-default notify-me" data-course="{{course.name | urlencode}}">
{{ _("Notify me when available") }}
</div>
{% elif is_cohort_staff(course.name, frappe.session.user) %}
<a class="button wide-button is-secondary"
href="/courses/{{course.name}}/manage"
style="color: inherit;">
{{ _("Manage the course") }}
</a>
{% elif membership %}
<a class="button wide-button is-primary" id="continue-learning"
href="{{ get_lesson_url(course.name, lesson_index) }}{{ course.query_parameter }}">
{{ _("Continue Learning") }} <img class="ml-2" src="/assets/lms/icons/white-arrow.svg" />
</a>
{% endif %}
{% set progress = frappe.utils.cint(membership.progress) %}
{% if membership and course.enable_certification %}
{% if certificate %}
<a class="button wide-button is-secondary mt-3" href="/courses/{{ course.name }}/{{ certificate }}">
{{ _("Get Certificate") }}
</a>
{% elif eligible_for_evaluation %}
<a class="button wide-button is-secondary mt-3" id="apply-certificate" data-course="{{ course.name }}">
{{ _("Apply for Certificate") }}
</a>
{% elif course.grant_certificate_after == "Completion" and progress == 100 %}
<div class="button wide-button is-secondary mt-3" id="certification" data-course="{{ course.name }}">
{{ _("Get Certificate") }}
</div>
{% endif %}
{% endif %}
</div>
</div>
<!-- Modal for Slots -->
{% macro SlotModal(course) %}
<div class="modal fade" id="slot-modal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<div class="font-weight-bold">{{ _("Pick a Slot") }}</div>
<button type="button" class="close close-slot-modal" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form id="slot-form">
<p class="">{{ _("This course requires you to complete an evaluation to get certified. Please pick a slot based on your convenience for the evaluations. ") }}</p>
<div class="form-group">
<div class="clearfix">
<label class="control-label reqd" style="padding-right: 0px;">{{ _("Date") }}</label>
</div>
<div class="control-input-wrapper">
<div class="control-input">
<input type="date" class="input-with-feedback form-control bold" data-fieldtype="Date" data-course="{{ course.name | urlencode }}"
id="slot-date" min="{{ frappe.utils.format_date(frappe.utils.add_days(frappe.utils.getdate(), 1), 'yyyy-mm-dd') }}">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<div class="font-weight-bold">{{ _("Pick a Slot") }}</div>
<button type="button" class="close close-slot-modal" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form id="slot-form">
<p class="">{{ _("This course requires you to complete an evaluation to get certified. Please pick a slot based on your convenience for the evaluations. ") }}</p>
<div class="form-group">
<div class="clearfix">
<label class="control-label reqd" style="padding-right: 0px;">{{ _("Date") }}</label>
</div>
<div class="control-input-wrapper">
<div class="control-input">
<input type="date" class="input-with-feedback form-control bold" data-fieldtype="Date" data-course="{{ course.name | urlencode }}"
id="slot-date" min="{{ frappe.utils.format_date(frappe.utils.add_days(frappe.utils.getdate(), 1), 'yyyy-mm-dd') }}">
</div>
</div>
</div>
</div>
<div class="form-group">
<div class="clearfix">
<label class="control-label reqd slot-label hide" style="padding-right: 0px;">{{ _("Slots") }}</label>
<div class="form-group">
<div class="clearfix">
<label class="control-label reqd slot-label hide" style="padding-right: 0px;">{{ _("Slots") }}</label>
</div>
<div class="control-input-wrapper">
<div class="control-input">
<div class="slots"></div>
</div>
</div>
</div>
<div class="control-input-wrapper">
<div class="control-input">
<div class="slots"></div>
</div>
</div>
</div>
<p id="no-slots-message" class="small text-danger hide"> {{ _("There are no slots available on this day.") }} </p>
</form>
</div>
<div class="modal-footer">
<div class="button is-primary" data-course="{{ course.name | urlencode}}" id="submit-slot">
{{ _("Submit") }}</div>
</div>
<p id="no-slots-message" class="small text-danger hide"> {{ _("There are no slots available on this day.") }} </p>
</form>
</div>
<div class="modal-footer">
<div class="button is-primary" data-course="{{ course.name | urlencode}}" id="submit-slot">
{{ _("Submit") }}</div>
</div>
</div>
</div>
</div>
</div>
{% endmacro %}
{% macro Description(course) %}
<div class="course-description-section">
{{ frappe.utils.md_to_html(course.description) }}
</div>
{% endmacro %}
{% macro CourseCreator(course) %}
<div class="course-home-headings"> {{ _("Course Creators") }} </div>
<div class="common-card-style course-creators-card">
{% set instructors = get_instructors(course.name) %}
{% for instructor in instructors %}
<div class="d-flex align-items-center">
{{ widgets.Avatar(member=instructor, avatar_class="avatar-medium") }}
<div class="ml-4">
<div class="course-creator-name"> {{ instructor.full_name }} </div>
<div class="course-meta"> {{ get_authored_courses(instructor.name) | length }} {{ _("Courses Created") }} </div>
</div>
</div>
{% endfor %}
</div>
{% endmacro %}
{% macro RelatedCourses(course) %}
{% if course.related_courses | length %}
<div class="related-courses">
<div class="container">
<div class="course-home-headings"> {{ _("Other Courses") }} </div>
<div class="carousel slide" id="carouselExampleControls" data-ride="carousel" data-interval="false">
<div class="carousel-inner">
{% for crs in course.related_courses %}
{% if loop.index % 3 == 1 %}
<div class="carousel-item {% if loop.index == 1 %} active {% endif %}"><div class="cards-parent">
{% endif %}
{{ widgets.CourseCard(course=crs, read_only=False) }}
{% if loop.index % 3 == 0 or loop.index == course.related_courses | length %} </div> </div> {% endif %}
{% endfor %}
</div>
{% if course.related_courses | length > 3 %}
<div class="slider-controls">
<a class="carousel-control-prev" href="#carouselExampleControls" role="button" data-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
</a>
<a class="carousel-control-next" href="#carouselExampleControls" role="button" data-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
</a>
</div>
{% endif %}
</div>
</div>
</div>
{% endif%}
{% endmacro %}

View File

@@ -50,116 +50,144 @@ frappe.ready(() => {
select_slot(e);
});
$(".btn-attach").click((e) => {
show_upload_modal(e);
});
$(".btn-clear").click((e) => {
clear_image(e);
});
$(".btn-tag").click((e) => {
add_tag(e);
});
$(".btn-save-course").click((e) => {
save_course(e);
});
$(".btn-delete-tag").click((e) => {
remove_tag(e);
});
});
var hide_wrapped_mentor_cards = () => {
var offset_top_prev;
$(".member-parent .member-card").each(function () {
var offset_top = $(this).offset().top;
if (offset_top > offset_top_prev) {
$(this).addClass('wrapped').slideUp("fast");
const hide_wrapped_mentor_cards = () => {
let offset_top_prev;
$(".member-parent .member-card").each(function () {
var offset_top = $(this).offset().top;
if (offset_top > offset_top_prev) {
$(this).addClass('wrapped').slideUp("fast");
}
if (!offset_top_prev) {
offset_top_prev = offset_top;
}
});
if ($(".wrapped").length < 1) {
$(".view-all-mentors").hide();
}
if (!offset_top_prev) {
offset_top_prev = offset_top;
};
const cancel_mentor_request = (e) => {
e.preventDefault();
frappe.call({
"method": "lms.lms.doctype.lms_mentor_request.lms_mentor_request.cancel_request",
"args": {
"course": decodeURIComponent($(e.currentTarget).attr("data-course"))
},
"callback": (data) => {
if (data.message == "OK") {
$("#mentor-request").removeClass("hide");
$("#already-applied").addClass("hide")
}
}
});
};
const view_all_mentors = (e) => {
$(".wrapped").each((i, element) => {
$(element).slideToggle("slow");
})
var text_element = $(".view-all-mentors .course-instructor .all-mentors-text");
var text = text_element.text() == "View all mentors" ? "View less" : "View all mentors";
text_element.text(text);
if ($(".mentor-icon").css("transform") == "none") {
$(".mentor-icon").css("transform", "rotate(180deg)");
} else {
$(".mentor-icon").css("transform", "");
}
};
});
if ($(".wrapped").length < 1) {
$(".view-all-mentors").hide();
}
}
const show_review_dialog = (e) => {
e.preventDefault();
$("#review-modal").modal("show");
};
var cancel_mentor_request = (e) => {
e.preventDefault()
frappe.call({
"method": "lms.lms.doctype.lms_mentor_request.lms_mentor_request.cancel_request",
"args": {
"course": decodeURIComponent($(e.currentTarget).attr("data-course"))
},
"callback": (data) => {
if (data.message == "OK") {
$("#mentor-request").removeClass("hide");
$("#already-applied").addClass("hide")
}
}
})
}
var view_all_mentors = (e) => {
$(".wrapped").each((i, element) => {
$(element).slideToggle("slow");
})
var text_element = $(".view-all-mentors .course-instructor .all-mentors-text");
var text = text_element.text() == "View all mentors" ? "View less" : "View all mentors";
text_element.text(text);
const highlight_rating = (e) => {
var rating = $(e.currentTarget).attr("data-rating");
$(".icon-rating").removeClass("star-click");
$(".icon-rating").each((i, elem) => {
if (i <= rating-1) {
$(elem).addClass("star-click");
}
});
};
if ($(".mentor-icon").css("transform") == "none") {
$(".mentor-icon").css("transform", "rotate(180deg)");
} else {
$(".mentor-icon").css("transform", "");
}
}
var show_review_dialog = (e) => {
e.preventDefault();
$("#review-modal").modal("show");
}
var highlight_rating = (e) => {
var rating = $(e.currentTarget).attr("data-rating");
$(".icon-rating").removeClass("star-click");
$(".icon-rating").each((i, elem) => {
if (i <= rating-1) {
$(elem).addClass("star-click");
}
})
}
var submit_review = (e) => {
e.preventDefault();
var rating = $(".rating-field").children(".star-click").length;
var review = $(".review-field").val();
if (!rating) {
$(".error-field").text("Please provide a rating.");
return;
}
frappe.call({
method: "lms.lms.doctype.lms_course_review.lms_course_review.submit_review",
args: {
"rating": rating,
"review": review,
"course": decodeURIComponent($(e.currentTarget).attr("data-course"))
},
callback: (data) => {
if (data.message == "OK") {
$(".review-modal").modal("hide");
window.location.reload();
}
e.preventDefault();
var rating = $(".rating-field").children(".star-click").length;
var review = $(".review-field").val();
if (!rating) {
$(".error-field").text("Please provide a rating.");
return;
}
})
frappe.call({
method: "lms.lms.doctype.lms_course_review.lms_course_review.submit_review",
args: {
"rating": rating,
"review": review,
"course": decodeURIComponent($(e.currentTarget).attr("data-course"))
},
callback: (data) => {
if (data.message == "OK") {
$(".review-modal").modal("hide");
window.location.reload();
}
}
});
};
const create_certificate = (e) => {
e.preventDefault();
course = $(e.currentTarget).attr("data-course");
frappe.call({
method: "lms.lms.doctype.lms_certificate.lms_certificate.create_certificate",
args: {
"course": course
},
callback: (data) => {
window.location.href = `/courses/${course}/${data.message.name}`;
}
})
e.preventDefault();
course = $(e.currentTarget).attr("data-course");
frappe.call({
method: "lms.lms.doctype.lms_certificate.lms_certificate.create_certificate",
args: {
"course": course
},
callback: (data) => {
window.location.href = `/courses/${course}/${data.message.name}`;
}
});
};
const element_not_in_viewport = (el) => {
const rect = el.getBoundingClientRect();
return rect.bottom < 0 || rect.right < 0 || rect.left > window.innerWidth || rect.top > window.innerHeight;
const rect = el.getBoundingClientRect();
return rect.bottom < 0 || rect.right < 0 || rect.left > window.innerWidth || rect.top > window.innerHeight;
};
const submit_for_review = (e) => {
let course = $(e.currentTarget).data("course");
frappe.call({
@@ -184,12 +212,12 @@ const submit_for_review = (e) => {
});
};
const apply_cetificate = (e) => {
$("#slot-modal").modal("show");
};
const submit_slot = (e) => {
e.preventDefault();
const slot = window.selected_slot;
@@ -215,6 +243,7 @@ const submit_slot = (e) => {
});
};
const display_slots = (e) => {
frappe.call({
method: "lms.lms.doctype.course_evaluator.course_evaluator.get_schedule",
@@ -249,18 +278,81 @@ const display_slots = (e) => {
});
};
const select_slot = (e) => {
$(".slot").removeClass("btn-outline-primary");
$(e.currentTarget).addClass("btn-outline-primary");
window.selected_slot = $(e.currentTarget);
};
const format_time = (time) => {
let date = moment(new Date()).format("ddd MMM DD YYYY");
return moment(`${date} ${time}`).format("HH:mm a");
};
const close_slot_modal = (e) => {
$("#slot-date").val("");
$(".slot-label").addClass("hide");
}
};
const show_upload_modal = () => {
new frappe.ui.FileUploader({
folder: "Home/Attachments",
restrictions: {
allowed_file_types: ['image/*']
},
on_success: (file_doc) => {
$(".course-image-attachment").removeClass("hide");
$(".course-image-attachment a").attr("href", file_doc.file_url).text(file_doc.file_url);
$(".btn-attach").addClass("hide");
},
});
};
const clear_image = () => {
$(".course-image-attachment").addClass("hide");
$(".course-image-attachment a").removeAttr("href");
$(".btn-attach").removeClass("hide");
};
const add_tag = (e) => {
$(`<div class="course-card-pills" contenteditable="true"
data-placeholder="${__('Tag')}"></div>`).insertBefore(`.btn-tag`);
};
const save_course = (e) => {
let tags = $('.course-card-pills').map((i, el) => $(el).text().trim()).get();
tags = tags.filter(word => word.trim().length > 0);
frappe.call({
method: "lms.lms.doctype.lms_course.lms_course.save_course",
args: {
"tags": tags.join(", "),
"title": $("#title").text(),
"short_introduction": $("#intro").text(),
"video_link": $("#video-link").text(),
"image": $("#image").attr("href"),
"description": $("#description").text(),
"course": $("#title").data("course") ? $("#title").data("course") : ""
},
callback: (data) => {
frappe.show_alert({
message: __("Saved"),
indicator: "green",
});
setTimeout(() => {
window.location.href = `/courses/${data.message}?edit=1`;
}, 1000);
}
});
};
const remove_tag = (e) => {
$(e.currentTarget).closest(".course-card-pills").remove();
};

View File

@@ -1,7 +1,6 @@
import frappe
from lms.lms.doctype.lms_settings.lms_settings import check_profile_restriction
from lms.lms.utils import get_membership, is_instructor, is_certified, get_evaluation_details
from frappe.utils import add_months, getdate
from lms.lms.utils import get_membership, is_instructor, is_certified, get_evaluation_details, redirect_to_courses_list
def get_context(context):
context.no_cache = 1
@@ -9,15 +8,30 @@ def get_context(context):
try:
course_name = frappe.form_dict["course"]
except KeyError:
frappe.local.flags.redirect_location = "/courses"
raise frappe.Redirect
redirect_to_courses_list()
if course_name == "new-course":
if frappe.session.user == "Guest":
redirect_to_courses_list()
context.course = frappe._dict()
context.course.edit_mode = True
context.membership = None
else:
set_course_context(context, course_name)
def set_course_context(context, course_name):
course = frappe.db.get_value("LMS Course", course_name,
["name", "title", "image", "short_introduction", "description", "published", "upcoming", "disable_self_learning",
"status", "video_link", "enable_certification", "grant_certificate_after", "paid_certificate",
"price_certificate", "currency", "max_attempts", "duration"],
as_dict=True)
if frappe.form_dict.get("edit"):
if not is_instructor(course.name):
redirect_to_courses_list()
course.edit_mode = True
if course is None:
frappe.local.flags.redirect_location = "/courses"
raise frappe.Redirect
@@ -50,11 +64,13 @@ def get_context(context):
"keywords": course.title
}
def get_user_interest(course):
return frappe.db.count("LMS Course Interest", {
"course": course,
"user": frappe.session.user
})
def show_start_learing_cta(course, membership, restriction):
return not course.disable_self_learning and not membership and not course.upcoming and not restriction.get("restrict") and not is_instructor(course.name)

View File

@@ -15,16 +15,16 @@
</div>
{% else %}
{% include "lms/templates/search_course/search_course.html" %}
<div class="course-list">
{% set courses = live_courses %}
{% set title = _("All Live Courses ({0})").format(courses | length) %}
{% set title = _("Live Courses ({0})").format(courses | length) %}
{% set classes = "live-courses" %}
{% include "lms/templates/course_list.html" %}
{% set courses = upcoming_courses %}
{% set title = _("All Upcoming Courses ({0})").format(courses | length) %}
{% set title = _("Upcoming Courses ({0})").format(courses | length) %}
{% set classes = "upcoming-courses mt-10" %}
{% include "lms/templates/course_list.html" %}
</div>

View File

@@ -2,48 +2,48 @@
{% block title %}{{ _("Dashboard")}}
{% endblock %}
{% block content %}
{% set portal_course_creation = frappe.db.get_single_value("LMS Settings", "portal_course_creation") %}
{% set show_creators_section = portal_course_creation == "Anyone" or has_course_instructor_role() %}
<div class="common-page-style dashboard">
<div class="container">
{% if portal_course_creation %}
<a class="button is-default button-links pull-right hide" id="create-course-link" href="/course">
<svg class="icon icon-sm mr-1"><use href="#icon-add"></use></svg>
{{ _("New Course")}}
</a>
{% endif %}
<ul class="nav" id="courses-tab">
<li class="nav-item">
<a class="nav-link active" data-toggle="tab" href="#courses-enrolled"> {{ _("Courses Enrolled") }} </a>
</li>
{% if portal_course_creation %}
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#courses-created">{{ _("Courses Created") }}
<div class="container">
{% if show_creators_section %}
<a class="btn btn-secondary btn-md pull-right" id="create-course-link" href="/courses/new-course">
{{ _("Create a Course")}}
</a>
</li>
{% endif %}
</ul>
<div class="border-bottom mb-4"></div>
<div class="tab-content">
<div class="tab-pane active" id="courses-enrolled" role="tabpanel" aria-labelledby="courses-enrolled">
{% include "lms/lms/web_template/courses_enrolled/courses_enrolled.html" %}
</div>
{% if portal_course_creation %}
<div class="tab-pane fade" id="courses-created" role="tabpanel" aria-labelledby="courses-created">
{% include "lms/templates/courses_created.html" %}
</div>
{% endif %}
{% endif %}
<ul class="nav" id="courses-tab">
<li class="nav-item">
<a class="nav-link active" data-toggle="tab" href="#courses-enrolled"> {{ _("Courses Enrolled") }} </a>
</li>
{% if show_creators_section %}
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#courses-created">{{ _("Courses Created") }}
</a>
</li>
{% endif %}
</ul>
<div class="border-bottom mb-4"></div>
<div class="tab-content">
<div class="tab-pane active" id="courses-enrolled" role="tabpanel" aria-labelledby="courses-enrolled">
{% include "lms/lms/web_template/courses_enrolled/courses_enrolled.html" %}
</div>
{% if show_creators_section %}
<div class="tab-pane fade" id="courses-created" role="tabpanel" aria-labelledby="courses-created">
{% include "lms/templates/courses_created.html" %}
</div>
{% endif %}
</div>
</div>
</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
<script>
frappe.ready(() => {
$('#courses-tab a[data-toggle="tab"]').on('shown.bs.tab', (e) => {
let link = $("#create-course-link");
$(e.currentTarget).attr("href") == "#courses-created" ? link.removeClass("hide") : link.addClass("hide");
});
})
</script>
{% endblock %}