Compare commits
154 Commits
feat-reply
...
version-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
919a5265c2 | ||
|
|
507d08f37c | ||
|
|
40c295aa37 | ||
|
|
2905a6af1a | ||
|
|
4cc27adb8b | ||
|
|
a5d000f702 | ||
|
|
39aa1d443d | ||
|
|
4b4086afb3 | ||
|
|
b0bb7d32ca | ||
|
|
ff1bd91223 | ||
|
|
3dad3580bb | ||
|
|
8f687145be | ||
|
|
9c405edd09 | ||
|
|
fe791dc478 | ||
|
|
42417621fa | ||
|
|
d3b3d85c84 | ||
|
|
b700013704 | ||
|
|
bac229c731 | ||
|
|
28043e634b | ||
|
|
b672108155 | ||
|
|
5e569ab0e6 | ||
|
|
43a07e53a6 | ||
|
|
fbd83196fc | ||
|
|
465f4e1e96 | ||
|
|
43d409ce64 | ||
|
|
a5fc52ec29 | ||
|
|
a9b06575d0 | ||
|
|
3070cbed3c | ||
|
|
d712881e16 | ||
|
|
991dc7f8c8 | ||
|
|
6b6c8da785 | ||
|
|
f40fbaed3e | ||
|
|
4973386dd0 | ||
|
|
13536b8bad | ||
|
|
caea7e334c | ||
|
|
b248774774 | ||
|
|
7a9d6325d5 | ||
|
|
b0d0b41502 | ||
|
|
30c89cb13c | ||
|
|
9175737b9c | ||
|
|
7ae772205a | ||
|
|
00b0a20c83 | ||
|
|
6604866342 | ||
|
|
881c3d943a | ||
|
|
d5118cc91f | ||
|
|
ac74cbdf72 | ||
|
|
01f7fc3cff | ||
|
|
85c850e5bf | ||
|
|
67dfffdd58 | ||
|
|
ae4aadb8d3 | ||
|
|
e5dc2bad6a | ||
|
|
0e2fabf139 | ||
|
|
c45a372e83 | ||
|
|
98ecb4c27c | ||
|
|
9023094326 | ||
|
|
497de05db2 | ||
|
|
cb3224664e | ||
|
|
9b532a5470 | ||
|
|
f1f9d9790b | ||
|
|
96190910a7 | ||
|
|
6484763d37 | ||
|
|
6f1e7624ec | ||
|
|
eef5bd6062 | ||
|
|
de60fbb25a | ||
|
|
fd9a638879 | ||
|
|
ddcb718a3a | ||
|
|
a17a7453e7 | ||
|
|
479be0b8ee | ||
|
|
6f40c357b3 | ||
|
|
81db6c544d | ||
|
|
be4e3aa963 | ||
|
|
6da0c07a3d | ||
|
|
b4ad10ca35 | ||
|
|
2388b878dc | ||
|
|
8cdaa7877a | ||
|
|
d314287883 | ||
|
|
b70dfc8e82 | ||
|
|
a5a7184f9a | ||
|
|
4e019d0a43 | ||
|
|
8453b54360 | ||
|
|
9f9dfdb26d | ||
|
|
9fd4984247 | ||
|
|
9ebd64f47d | ||
|
|
4316a37ed6 | ||
|
|
2d745460e8 | ||
|
|
b5258b6d9f | ||
|
|
41b076c0db | ||
|
|
9d65e5e398 | ||
|
|
7250bf7d65 | ||
|
|
4d7b247378 | ||
|
|
0aaa58cd54 | ||
|
|
014b85f12c | ||
|
|
929f97cb72 | ||
|
|
de9cb935ee | ||
|
|
9aafc176e4 | ||
|
|
0488ae8305 | ||
|
|
60fd317d98 | ||
|
|
e54435d85d | ||
|
|
3a23b91c90 | ||
|
|
69591577bf | ||
|
|
e56afba6d3 | ||
|
|
98536ce4c7 | ||
|
|
05282178dd | ||
|
|
1af547288c | ||
|
|
b4af82acbc | ||
|
|
50fbe00d23 | ||
|
|
b44428677e | ||
|
|
d67faa1610 | ||
|
|
7b3f4c29d8 | ||
|
|
a49871c5b1 | ||
|
|
e4005792af | ||
|
|
8c0c09a21b | ||
|
|
a9b05f4256 | ||
|
|
cb6013a7a6 | ||
|
|
bb23b78a4f | ||
|
|
243277012f | ||
|
|
c9ed8a4b03 | ||
|
|
d413acaef3 | ||
|
|
d6aad6cd74 | ||
|
|
ca45e43003 | ||
|
|
ad39530705 | ||
|
|
a6c2378b56 | ||
|
|
c073d2201d | ||
|
|
6d70de2eb1 | ||
|
|
48982e8f4a | ||
|
|
397128f980 | ||
|
|
1d77fd3f94 | ||
|
|
60e78e8e74 | ||
|
|
4a9ccc6fde | ||
|
|
a707095fae | ||
|
|
d4f662f65e | ||
|
|
509b1365d9 | ||
|
|
d0b236e381 | ||
|
|
fe98265636 | ||
|
|
3f7d1b1e83 | ||
|
|
52cde329c1 | ||
|
|
68b2dd6147 | ||
|
|
5fa0d022dc | ||
|
|
d996a5c53f | ||
|
|
b6dfc6ed4d | ||
|
|
c7c2ba83f3 | ||
|
|
2bffabff05 | ||
|
|
697e81df10 | ||
|
|
f1b791845b | ||
|
|
6310845cdd | ||
|
|
230cca63f3 | ||
|
|
af9f4d4b1e | ||
|
|
0111ff9c99 | ||
|
|
12bec14c92 | ||
|
|
174ea1ddd4 | ||
|
|
038a7463e1 | ||
|
|
a702909216 | ||
|
|
8effd5614f | ||
|
|
1046d28092 |
@@ -1,6 +1,6 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://www.frappelms.com/">
|
<a href="https://www.frappelms.com/">
|
||||||
<img src="https://frappelms.com/files/lms-logo-medium.png" alt="Frappe LMS" width="120px" height="25px">
|
<img src="https://frappe.io/files/lms.png" alt="Frappe LMS" width="50px" height="50px">
|
||||||
</a>
|
</a>
|
||||||
<p align="center">Easy to use, open source, learning management system.</p>
|
<p align="center">Easy to use, open source, learning management system.</p>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
5
SECURITY.md
Normal file
5
SECURITY.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
The Frappe team and community take security issues seriously. To report a security issue, please go through the information mentioned [here](https://frappe.io/security).
|
||||||
|
|
||||||
|
We appreciate your efforts to responsibly disclose your findings. We'll endeavor to respond quickly and will keep you updated throughout the process.
|
||||||
@@ -13,6 +13,6 @@ module.exports = defineConfig({
|
|||||||
openMode: 0,
|
openMode: 0,
|
||||||
},
|
},
|
||||||
e2e: {
|
e2e: {
|
||||||
baseUrl: "http://dd1:8000",
|
baseUrl: "http://pyp:8000",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
$ git clone https://github.com/frappe/lms.git
|
$ git clone https://github.com/frappe/lms.git
|
||||||
|
|
||||||
$ cd lms
|
$ cd lms
|
||||||
|
|
||||||
|
$ cd docker
|
||||||
```
|
```
|
||||||
|
|
||||||
**Step 2:** Run docker-compose
|
**Step 2:** Run docker-compose
|
||||||
|
|||||||
1
frappe-ui
Submodule
1
frappe-ui
Submodule
Submodule frappe-ui added at 2898a0bdd1
@@ -138,12 +138,12 @@
|
|||||||
"label": "User Category",
|
"label": "User Category",
|
||||||
"length": 0,
|
"length": 0,
|
||||||
"mandatory_depends_on": null,
|
"mandatory_depends_on": null,
|
||||||
"modified": "2022-04-19 13:02:18.219508",
|
"modified": "2022-04-19 13:02:18.219510",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "User-user_category",
|
"name": "User-user_category",
|
||||||
"no_copy": 0,
|
"no_copy": 0,
|
||||||
"non_negative": 0,
|
"non_negative": 0,
|
||||||
"options": "Business Owner\nManager (Sales/Marketing/Customer)\nEmployee\nStudent\nFreelancer/Just looking\nOthers",
|
"options": "\nBusiness Owner\nManager (Sales/Marketing/Customer)\nEmployee\nStudent\nFreelancer/Just looking\nOthers",
|
||||||
"permlevel": 0,
|
"permlevel": 0,
|
||||||
"precision": "",
|
"precision": "",
|
||||||
"print_hide": 0,
|
"print_hide": 0,
|
||||||
|
|||||||
12
lms/hooks.py
12
lms/hooks.py
@@ -97,8 +97,7 @@ override_doctype_class = {
|
|||||||
# Hook on document methods and events
|
# Hook on document methods and events
|
||||||
|
|
||||||
doc_events = {
|
doc_events = {
|
||||||
"Discussion Reply": {"after_insert": "lms.lms.utils.create_notification_log"},
|
"Discussion Reply": {"after_insert": "lms.lms.utils.handle_notifications"},
|
||||||
"Course Lesson": {"on_update": "lms.lms.doctype.lms_quiz.lms_quiz.update_lesson_info"},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Scheduled Tasks
|
# Scheduled Tasks
|
||||||
@@ -119,9 +118,9 @@ fixtures = ["Custom Field", "Function", "Industry"]
|
|||||||
# Overriding Methods
|
# Overriding Methods
|
||||||
# ------------------------------
|
# ------------------------------
|
||||||
#
|
#
|
||||||
# override_whitelisted_methods = {
|
override_whitelisted_methods = {
|
||||||
# "frappe.desk.doctype.event.event.get_events": "lms.event.get_events"
|
# "frappe.desk.search.get_names_for_mentions": "lms.lms.utils.get_names_for_mentions",
|
||||||
# }
|
}
|
||||||
#
|
#
|
||||||
# each overriding function accepts a `data` argument;
|
# each overriding function accepts a `data` argument;
|
||||||
# generated from the base implementation of the doctype dashboard,
|
# generated from the base implementation of the doctype dashboard,
|
||||||
@@ -174,7 +173,8 @@ website_route_rules = [
|
|||||||
"to_route": "cohorts/join",
|
"to_route": "cohorts/join",
|
||||||
},
|
},
|
||||||
{"from_route": "/users", "to_route": "profiles/profile"},
|
{"from_route": "/users", "to_route": "profiles/profile"},
|
||||||
{"from_route": "/jobs/<job>", "to_route": "jobs/job"},
|
{"from_route": "/job-openings", "to_route": "jobs_openings/index"},
|
||||||
|
{"from_route": "/job-openings/<job>", "to_route": "jobs_openings/job"},
|
||||||
{
|
{
|
||||||
"from_route": "/batches/<batchname>/students/<username>",
|
"from_route": "/batches/<batchname>/students/<username>",
|
||||||
"to_route": "/batches/progress",
|
"to_route": "/batches/progress",
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ from frappe.desk.page.setup_wizard.setup_wizard import add_all_roles_to
|
|||||||
|
|
||||||
def after_install():
|
def after_install():
|
||||||
add_pages_to_nav()
|
add_pages_to_nav()
|
||||||
|
create_batch_source()
|
||||||
|
|
||||||
|
|
||||||
def after_sync():
|
def after_sync():
|
||||||
create_lms_roles()
|
create_lms_roles()
|
||||||
set_default_home()
|
|
||||||
set_default_certificate_print_format()
|
set_default_certificate_print_format()
|
||||||
add_all_roles_to("Administrator")
|
add_all_roles_to("Administrator")
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ def add_pages_to_nav():
|
|||||||
{"label": "Courses", "url": "/courses", "parent": "Explore", "idx": 2},
|
{"label": "Courses", "url": "/courses", "parent": "Explore", "idx": 2},
|
||||||
{"label": "Batches", "url": "/batches", "parent": "Explore", "idx": 3},
|
{"label": "Batches", "url": "/batches", "parent": "Explore", "idx": 3},
|
||||||
{"label": "Statistics", "url": "/statistics", "parent": "Explore", "idx": 4},
|
{"label": "Statistics", "url": "/statistics", "parent": "Explore", "idx": 4},
|
||||||
{"label": "Jobs", "url": "/jobs", "parent": "Explore", "idx": 5},
|
{"label": "Jobs", "url": "/job-openings", "parent": "Explore", "idx": 5},
|
||||||
{"label": "People", "url": "/community", "parent": "Explore", "idx": 6},
|
{"label": "People", "url": "/community", "parent": "Explore", "idx": 6},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -64,10 +64,6 @@ def delete_lms_roles():
|
|||||||
frappe.db.delete("Role", role)
|
frappe.db.delete("Role", role)
|
||||||
|
|
||||||
|
|
||||||
def set_default_home():
|
|
||||||
frappe.db.set_single_value("Portal Settings", "default_portal_home", "/courses")
|
|
||||||
|
|
||||||
|
|
||||||
def create_course_creator_role():
|
def create_course_creator_role():
|
||||||
if not frappe.db.exists("Role", "Course Creator"):
|
if not frappe.db.exists("Role", "Course Creator"):
|
||||||
role = frappe.get_doc(
|
role = frappe.get_doc(
|
||||||
@@ -182,3 +178,20 @@ def delete_custom_fields():
|
|||||||
|
|
||||||
for field in fields:
|
for field in fields:
|
||||||
frappe.db.delete("Custom Field", {"fieldname": field})
|
frappe.db.delete("Custom Field", {"fieldname": field})
|
||||||
|
|
||||||
|
|
||||||
|
def create_batch_source():
|
||||||
|
sources = [
|
||||||
|
"Newsletter",
|
||||||
|
"LinkedIn",
|
||||||
|
"Twitter",
|
||||||
|
"Website",
|
||||||
|
"Friend/Colleague/Connection",
|
||||||
|
"Google Search",
|
||||||
|
]
|
||||||
|
|
||||||
|
for source in sources:
|
||||||
|
if not frappe.db.exists("LMS Source", source):
|
||||||
|
doc = frappe.new_doc("LMS Source")
|
||||||
|
doc.source = source
|
||||||
|
doc.save()
|
||||||
|
|||||||
@@ -4,6 +4,6 @@
|
|||||||
frappe.ui.form.on("Job Opportunity", {
|
frappe.ui.form.on("Job Opportunity", {
|
||||||
refresh: (frm) => {
|
refresh: (frm) => {
|
||||||
if (frm.doc.name)
|
if (frm.doc.name)
|
||||||
frm.add_web_link(`/jobs/${frm.doc.name}`, "See on Website");
|
frm.add_web_link(`/job-openings/${frm.doc.name}`, "See on Website");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
frappe.ready(function () {
|
frappe.ready(function () {
|
||||||
frappe.web_form.after_save = () => {
|
frappe.web_form.after_save = () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.href = `/jobs`;
|
window.location.href = `/job-openings`;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"list_columns": [],
|
"list_columns": [],
|
||||||
"login_required": 1,
|
"login_required": 1,
|
||||||
"max_attachment_size": 0,
|
"max_attachment_size": 0,
|
||||||
"modified": "2022-09-15 17:22:43.957184",
|
"modified": "2022-09-15 17:22:43.957185",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Job",
|
"module": "Job",
|
||||||
"name": "job-opportunity",
|
"name": "job-opportunity",
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
"show_list": 1,
|
"show_list": 1,
|
||||||
"show_sidebar": 0,
|
"show_sidebar": 0,
|
||||||
"success_message": "",
|
"success_message": "",
|
||||||
"success_url": "/jobs",
|
"success_url": "/job-openings",
|
||||||
"title": "Job Opportunity",
|
"title": "Job Opportunity",
|
||||||
"web_form_fields": [
|
"web_form_fields": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,11 +9,12 @@
|
|||||||
"field_order": [
|
"field_order": [
|
||||||
"student_details_section",
|
"student_details_section",
|
||||||
"student",
|
"student",
|
||||||
"payment",
|
|
||||||
"confirmation_email_sent",
|
|
||||||
"column_break_oduu",
|
|
||||||
"student_name",
|
"student_name",
|
||||||
"username"
|
"username",
|
||||||
|
"column_break_oduu",
|
||||||
|
"payment",
|
||||||
|
"source",
|
||||||
|
"confirmation_email_sent"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -59,12 +60,18 @@
|
|||||||
"fieldname": "confirmation_email_sent",
|
"fieldname": "confirmation_email_sent",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Confirmation Email Sent"
|
"label": "Confirmation Email Sent"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "source",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Source",
|
||||||
|
"options": "LMS Source"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-10-09 17:09:50.481794",
|
"modified": "2023-10-26 16:52:04.266693",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "Batch Student",
|
"name": "Batch Student",
|
||||||
|
|||||||
@@ -1,9 +1,23 @@
|
|||||||
# Copyright (c) 2022, Frappe and contributors
|
# Copyright (c) 2022, Frappe and contributors
|
||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
# import frappe
|
import frappe
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
class BatchStudent(Document):
|
class BatchStudent(Document):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def enroll_batch(batch_name):
|
||||||
|
if frappe.db.exists(
|
||||||
|
"Batch Student", {"student": frappe.session.user, "parent": batch_name}
|
||||||
|
):
|
||||||
|
frappe.throw("You are already enrolled in this batch")
|
||||||
|
enrollment = frappe.new_doc("Batch Student")
|
||||||
|
enrollment.student = frappe.session.user
|
||||||
|
enrollment.parent = batch_name
|
||||||
|
enrollment.parentfield = "students"
|
||||||
|
enrollment.parenttype = "LMS Batch"
|
||||||
|
enrollment.save(ignore_permissions=True)
|
||||||
|
|||||||
@@ -99,8 +99,14 @@ def save_progress(lesson, course, status):
|
|||||||
quizzes = [value for name, value in macros if name == "Quiz"]
|
quizzes = [value for name, value in macros if name == "Quiz"]
|
||||||
|
|
||||||
for quiz in quizzes:
|
for quiz in quizzes:
|
||||||
|
passing_percentage = frappe.db.get_value("LMS Quiz", quiz, "passing_percentage")
|
||||||
if not frappe.db.exists(
|
if not frappe.db.exists(
|
||||||
"LMS Quiz Submission", {"quiz": quiz, "owner": frappe.session.user}
|
"LMS Quiz Submission",
|
||||||
|
{
|
||||||
|
"quiz": quiz,
|
||||||
|
"owner": frappe.session.user,
|
||||||
|
"percentage": [">=", passing_percentage],
|
||||||
|
},
|
||||||
):
|
):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,11 @@ from frappe.utils.password import get_decrypted_password
|
|||||||
|
|
||||||
class InviteRequest(Document):
|
class InviteRequest(Document):
|
||||||
def on_update(self):
|
def on_update(self):
|
||||||
if self.has_value_changed("status") and self.status == "Approved":
|
if (
|
||||||
|
self.has_value_changed("status")
|
||||||
|
and self.status == "Approved"
|
||||||
|
and not frappe.flags.in_test
|
||||||
|
):
|
||||||
self.send_email()
|
self.send_email()
|
||||||
|
|
||||||
def create_user(self, password):
|
def create_user(self, password):
|
||||||
|
|||||||
@@ -2,6 +2,13 @@
|
|||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
frappe.ui.form.on("LMS Assignment Submission", {
|
frappe.ui.form.on("LMS Assignment Submission", {
|
||||||
// refresh: function(frm) {
|
onload: function (frm) {
|
||||||
// }
|
frm.set_query("member", function (doc) {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
ignore_user_type: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,13 +4,18 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import validate_url
|
from frappe.utils import validate_url, validate_email_address
|
||||||
|
from frappe.email.doctype.email_template.email_template import get_email_template
|
||||||
|
|
||||||
|
|
||||||
class LMSAssignmentSubmission(Document):
|
class LMSAssignmentSubmission(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
self.validate_duplicates()
|
self.validate_duplicates()
|
||||||
|
|
||||||
|
def after_insert(self):
|
||||||
|
if not frappe.flags.in_test:
|
||||||
|
self.send_mail()
|
||||||
|
|
||||||
def validate_duplicates(self):
|
def validate_duplicates(self):
|
||||||
if frappe.db.exists(
|
if frappe.db.exists(
|
||||||
"LMS Assignment Submission",
|
"LMS Assignment Submission",
|
||||||
@@ -23,6 +28,38 @@ class LMSAssignmentSubmission(Document):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def send_mail(self):
|
||||||
|
subject = _("New Assignment Submission")
|
||||||
|
template = "assignment_submission"
|
||||||
|
custom_template = frappe.db.get_single_value(
|
||||||
|
"LMS Settings", "assignment_submission_template"
|
||||||
|
)
|
||||||
|
|
||||||
|
args = {
|
||||||
|
"member_name": self.member_name,
|
||||||
|
"assignment_name": self.assignment,
|
||||||
|
"assignment_title": self.assignment_title,
|
||||||
|
"submission_name": self.name,
|
||||||
|
}
|
||||||
|
|
||||||
|
moderators = frappe.get_all("Has Role", {"role": "Moderator"}, pluck="parent")
|
||||||
|
for moderator in moderators:
|
||||||
|
if not validate_email_address(moderator):
|
||||||
|
moderators.remove(moderator)
|
||||||
|
|
||||||
|
if custom_template:
|
||||||
|
email_template = get_email_template(custom_template, args)
|
||||||
|
subject = email_template.get("subject")
|
||||||
|
content = email_template.get("message")
|
||||||
|
frappe.sendmail(
|
||||||
|
recipients=moderators,
|
||||||
|
subject=subject,
|
||||||
|
template=template if not custom_template else None,
|
||||||
|
content=content if custom_template else None,
|
||||||
|
args=args,
|
||||||
|
header=[subject, "green"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def upload_assignment(
|
def upload_assignment(
|
||||||
|
|||||||
@@ -28,11 +28,19 @@ frappe.ui.form.on("LMS Batch", {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (frm.doc.timetable.length && !frm.doc.timetable_legends.length) {
|
||||||
|
set_default_legends(frm);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
timetable_template: function (frm) {
|
timetable_template: function (frm) {
|
||||||
set_timetable(frm);
|
set_timetable(frm);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
refresh: (frm) => {
|
||||||
|
frm.add_web_link(`/batches/details/${frm.doc.name}`, "See on website");
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const set_timetable = (frm) => {
|
const set_timetable = (frm) => {
|
||||||
@@ -52,6 +60,7 @@ const set_timetable = (frm) => {
|
|||||||
"start_time",
|
"start_time",
|
||||||
"end_time",
|
"end_time",
|
||||||
"duration",
|
"duration",
|
||||||
|
"milestone",
|
||||||
],
|
],
|
||||||
filters: {
|
filters: {
|
||||||
parent: frm.doc.timetable_template,
|
parent: frm.doc.timetable_template,
|
||||||
@@ -82,6 +91,7 @@ const add_timetable_rows = (frm, timetable) => {
|
|||||||
.format("HH:mm")
|
.format("HH:mm")
|
||||||
: null;
|
: null;
|
||||||
child.duration = row.duration;
|
child.duration = row.duration;
|
||||||
|
child.milestone = row.milestone;
|
||||||
});
|
});
|
||||||
frm.refresh_field("timetable");
|
frm.refresh_field("timetable");
|
||||||
|
|
||||||
@@ -121,3 +131,37 @@ const add_legend_rows = (frm, legends) => {
|
|||||||
frm.refresh_field("timetable_legends");
|
frm.refresh_field("timetable_legends");
|
||||||
frm.save();
|
frm.save();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const set_default_legends = (frm) => {
|
||||||
|
const data = [
|
||||||
|
{
|
||||||
|
reference_doctype: "Course Lesson",
|
||||||
|
label: "Lesson",
|
||||||
|
color: "#449CF0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reference_doctype: "LMS Quiz",
|
||||||
|
label: "LMS Quiz",
|
||||||
|
color: "#39E4A5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reference_doctype: "LMS Assignment",
|
||||||
|
label: "LMS Assignment",
|
||||||
|
color: "#ECAD4B",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reference_doctype: "LMS Live Class",
|
||||||
|
label: "LMS Live Class",
|
||||||
|
color: "#bb8be8",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
data.forEach((detail) => {
|
||||||
|
let child = frm.add_child("timetable_legends");
|
||||||
|
child.reference_doctype = detail.reference_doctype;
|
||||||
|
child.label = detail.label;
|
||||||
|
child.color = detail.color;
|
||||||
|
});
|
||||||
|
frm.refresh_field("timetable_legends");
|
||||||
|
frm.save();
|
||||||
|
};
|
||||||
|
|||||||
@@ -15,11 +15,13 @@
|
|||||||
"start_time",
|
"start_time",
|
||||||
"end_time",
|
"end_time",
|
||||||
"published",
|
"published",
|
||||||
|
"allow_self_enrollment",
|
||||||
"section_break_rgfj",
|
"section_break_rgfj",
|
||||||
"medium",
|
"medium",
|
||||||
"category",
|
"category",
|
||||||
"column_break_flwy",
|
"column_break_flwy",
|
||||||
"seat_count",
|
"seat_count",
|
||||||
|
"evaluation_end_date",
|
||||||
"section_break_6",
|
"section_break_6",
|
||||||
"description",
|
"description",
|
||||||
"batch_details_raw",
|
"batch_details_raw",
|
||||||
@@ -45,6 +47,7 @@
|
|||||||
"column_break_iens",
|
"column_break_iens",
|
||||||
"amount",
|
"amount",
|
||||||
"currency",
|
"currency",
|
||||||
|
"amount_usd",
|
||||||
"customisations_tab",
|
"customisations_tab",
|
||||||
"section_break_ubxi",
|
"section_break_ubxi",
|
||||||
"custom_component",
|
"custom_component",
|
||||||
@@ -120,12 +123,14 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "start_time",
|
"fieldname": "start_time",
|
||||||
"fieldtype": "Time",
|
"fieldtype": "Time",
|
||||||
"label": "Start Time"
|
"label": "Start Time",
|
||||||
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "end_time",
|
"fieldname": "end_time",
|
||||||
"fieldtype": "Time",
|
"fieldtype": "Time",
|
||||||
"label": "End Time"
|
"label": "End Time",
|
||||||
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "assessment_tab",
|
"fieldname": "assessment_tab",
|
||||||
@@ -277,11 +282,29 @@
|
|||||||
"fieldname": "allow_future",
|
"fieldname": "allow_future",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Allow accessing future dates"
|
"label": "Allow accessing future dates"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "evaluation_end_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"label": "Evaluation End Date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "paid_batch",
|
||||||
|
"description": "If you set an amount here, then the USD equivalent setting will not get applied.",
|
||||||
|
"fieldname": "amount_usd",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Amount (USD)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "allow_self_enrollment",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Allow Self Enrollment"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-10-12 12:53:37.351989",
|
"modified": "2024-01-22 10:42:42.872995",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Batch",
|
"name": "LMS Batch",
|
||||||
|
|||||||
@@ -12,9 +12,7 @@ from frappe.utils import (
|
|||||||
cint,
|
cint,
|
||||||
format_date,
|
format_date,
|
||||||
format_datetime,
|
format_datetime,
|
||||||
add_to_date,
|
get_time,
|
||||||
getdate,
|
|
||||||
get_datetime,
|
|
||||||
)
|
)
|
||||||
from lms.lms.utils import get_lessons, get_lesson_index, get_lesson_url
|
from lms.lms.utils import get_lessons, get_lesson_index, get_lesson_url
|
||||||
from lms.www.utils import get_quiz_details, get_assignment_details
|
from lms.www.utils import get_quiz_details, get_assignment_details
|
||||||
@@ -31,6 +29,7 @@ class LMSBatch(Document):
|
|||||||
self.validate_membership()
|
self.validate_membership()
|
||||||
self.validate_timetable()
|
self.validate_timetable()
|
||||||
self.send_confirmation_mail()
|
self.send_confirmation_mail()
|
||||||
|
self.validate_evaluation_end_date()
|
||||||
|
|
||||||
def validate_duplicate_students(self):
|
def validate_duplicate_students(self):
|
||||||
students = [row.student for row in self.students]
|
students = [row.student for row in self.students]
|
||||||
@@ -66,11 +65,14 @@ class LMSBatch(Document):
|
|||||||
|
|
||||||
def send_confirmation_mail(self):
|
def send_confirmation_mail(self):
|
||||||
for student in self.students:
|
for student in self.students:
|
||||||
|
|
||||||
if not student.confirmation_email_sent:
|
if not student.confirmation_email_sent:
|
||||||
self.send_mail(student)
|
self.send_mail(student)
|
||||||
student.confirmation_email_sent = 1
|
student.confirmation_email_sent = 1
|
||||||
|
|
||||||
|
def validate_evaluation_end_date(self):
|
||||||
|
if self.evaluation_end_date and self.evaluation_end_date < self.end_date:
|
||||||
|
frappe.throw(_("Evaluation end date cannot be less than the batch end date."))
|
||||||
|
|
||||||
def send_mail(self, student):
|
def send_mail(self, student):
|
||||||
subject = _("Enrollment Confirmation for the Next Training Batch")
|
subject = _("Enrollment Confirmation for the Next Training Batch")
|
||||||
template = "batch_confirmation"
|
template = "batch_confirmation"
|
||||||
@@ -119,23 +121,27 @@ class LMSBatch(Document):
|
|||||||
def validate_timetable(self):
|
def validate_timetable(self):
|
||||||
for schedule in self.timetable:
|
for schedule in self.timetable:
|
||||||
if schedule.start_time and schedule.end_time:
|
if schedule.start_time and schedule.end_time:
|
||||||
if (
|
if get_time(schedule.start_time) > get_time(schedule.end_time) or get_time(
|
||||||
schedule.start_time > schedule.end_time or schedule.start_time == schedule.end_time
|
schedule.start_time
|
||||||
):
|
) == get_time(schedule.end_time):
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Row #{0} Start time cannot be greater than or equal to end time.").format(
|
_("Row #{0} Start time cannot be greater than or equal to end time.").format(
|
||||||
schedule.idx
|
schedule.idx
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if schedule.start_time < self.start_time or schedule.start_time > self.end_time:
|
if get_time(schedule.start_time) < get_time(self.start_time) or get_time(
|
||||||
|
schedule.start_time
|
||||||
|
) > get_time(self.end_time):
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Row #{0} Start time cannot be outside the batch duration.").format(
|
_("Row #{0} Start time cannot be outside the batch duration.").format(
|
||||||
schedule.idx
|
schedule.idx
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if schedule.end_time < self.start_time or schedule.end_time > self.end_time:
|
if get_time(schedule.end_time) < get_time(self.start_time) or get_time(
|
||||||
|
schedule.end_time
|
||||||
|
) > get_time(self.end_time):
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Row #{0} End time cannot be outside the batch duration.").format(schedule.idx)
|
_("Row #{0} End time cannot be outside the batch duration.").format(schedule.idx)
|
||||||
)
|
)
|
||||||
@@ -250,8 +256,10 @@ def create_batch(
|
|||||||
paid_batch=0,
|
paid_batch=0,
|
||||||
amount=0,
|
amount=0,
|
||||||
currency=None,
|
currency=None,
|
||||||
|
amount_usd=0,
|
||||||
name=None,
|
name=None,
|
||||||
published=0,
|
published=0,
|
||||||
|
evaluation_end_date=None,
|
||||||
):
|
):
|
||||||
frappe.only_for("Moderator")
|
frappe.only_for("Moderator")
|
||||||
if name:
|
if name:
|
||||||
@@ -267,7 +275,7 @@ def create_batch(
|
|||||||
"description": description,
|
"description": description,
|
||||||
"batch_details": batch_details,
|
"batch_details": batch_details,
|
||||||
"batch_details_raw": batch_details_raw,
|
"batch_details_raw": batch_details_raw,
|
||||||
"image": meta_image,
|
"meta_image": meta_image,
|
||||||
"seat_count": seat_count,
|
"seat_count": seat_count,
|
||||||
"start_time": start_time,
|
"start_time": start_time,
|
||||||
"end_time": end_time,
|
"end_time": end_time,
|
||||||
@@ -276,7 +284,9 @@ def create_batch(
|
|||||||
"paid_batch": paid_batch,
|
"paid_batch": paid_batch,
|
||||||
"amount": amount,
|
"amount": amount,
|
||||||
"currency": currency,
|
"currency": currency,
|
||||||
|
"amount_usd": amount_usd,
|
||||||
"published": published,
|
"published": published,
|
||||||
|
"evaluation_end_date": evaluation_end_date,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
doc.save()
|
doc.save()
|
||||||
@@ -325,7 +335,17 @@ def get_batch_timetable(batch):
|
|||||||
timetable = frappe.get_all(
|
timetable = frappe.get_all(
|
||||||
"LMS Batch Timetable",
|
"LMS Batch Timetable",
|
||||||
filters={"parent": batch},
|
filters={"parent": batch},
|
||||||
fields=["reference_doctype", "reference_docname", "date", "start_time", "end_time"],
|
fields=[
|
||||||
|
"reference_doctype",
|
||||||
|
"reference_docname",
|
||||||
|
"date",
|
||||||
|
"start_time",
|
||||||
|
"end_time",
|
||||||
|
"milestone",
|
||||||
|
"name",
|
||||||
|
"idx",
|
||||||
|
"parent",
|
||||||
|
],
|
||||||
order_by="date",
|
order_by="date",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -362,20 +382,26 @@ def get_timetable_details(timetable):
|
|||||||
assessment = frappe._dict({"assessment_name": entry.reference_docname})
|
assessment = frappe._dict({"assessment_name": entry.reference_docname})
|
||||||
|
|
||||||
if entry.reference_doctype == "Course Lesson":
|
if entry.reference_doctype == "Course Lesson":
|
||||||
entry.icon = "icon-list"
|
|
||||||
course = frappe.db.get_value(
|
course = frappe.db.get_value(
|
||||||
entry.reference_doctype, entry.reference_docname, "course"
|
entry.reference_doctype, entry.reference_docname, "course"
|
||||||
)
|
)
|
||||||
entry.url = get_lesson_url(course, get_lesson_index(entry.reference_docname))
|
entry.url = get_lesson_url(course, get_lesson_index(entry.reference_docname))
|
||||||
|
|
||||||
|
entry.completed = (
|
||||||
|
True
|
||||||
|
if frappe.db.exists(
|
||||||
|
"LMS Course Progress",
|
||||||
|
{"lesson": entry.reference_docname, "member": frappe.session.user},
|
||||||
|
)
|
||||||
|
else False
|
||||||
|
)
|
||||||
|
|
||||||
elif entry.reference_doctype == "LMS Quiz":
|
elif entry.reference_doctype == "LMS Quiz":
|
||||||
entry.icon = "icon-quiz"
|
|
||||||
entry.url = "/quizzes"
|
entry.url = "/quizzes"
|
||||||
details = get_quiz_details(assessment, frappe.session.user)
|
details = get_quiz_details(assessment, frappe.session.user)
|
||||||
entry.update(details)
|
entry.update(details)
|
||||||
|
|
||||||
elif entry.reference_doctype == "LMS Assignment":
|
elif entry.reference_doctype == "LMS Assignment":
|
||||||
entry.icon = "icon-quiz"
|
|
||||||
details = get_assignment_details(assessment, frappe.session.user)
|
details = get_assignment_details(assessment, frappe.session.user)
|
||||||
entry.update(details)
|
entry.update(details)
|
||||||
|
|
||||||
@@ -384,11 +410,37 @@ def get_timetable_details(timetable):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def send_email_to_students(batch, subject, message):
|
def is_milestone_complete(idx, batch):
|
||||||
frappe.only_for("Moderator")
|
previous_rows = frappe.get_all(
|
||||||
students = frappe.get_all("Batch Student", {"parent": batch}, pluck="student")
|
"LMS Batch Timetable",
|
||||||
frappe.sendmail(
|
filters={"parent": batch, "idx": ["<", cint(idx)]},
|
||||||
recipients=students,
|
fields=["reference_doctype", "reference_docname", "idx"],
|
||||||
subject=subject,
|
order_by="idx",
|
||||||
message=message,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
for row in previous_rows:
|
||||||
|
if row.reference_doctype == "Course Lesson":
|
||||||
|
if not frappe.db.exists(
|
||||||
|
"LMS Course Progress",
|
||||||
|
{"member": frappe.session.user, "lesson": row.reference_docname},
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if row.reference_doctype == "LMS Quiz":
|
||||||
|
passing_percentage = frappe.db.get_value(
|
||||||
|
row.reference_doctype, row.reference_docname, "passing_percentage"
|
||||||
|
)
|
||||||
|
if not frappe.db.exists(
|
||||||
|
"LMS Quiz Submission",
|
||||||
|
{"quiz": row.reference_docname, "member": frappe.session.user},
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if row.reference_doctype == "LMS Assignment":
|
||||||
|
if not frappe.db.exists(
|
||||||
|
"LMS Assignment Submission",
|
||||||
|
{"assignment": row.reference_docname, "member": frappe.session.user},
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|||||||
@@ -16,7 +16,8 @@
|
|||||||
"column_break_merq",
|
"column_break_merq",
|
||||||
"start_time",
|
"start_time",
|
||||||
"end_time",
|
"end_time",
|
||||||
"duration"
|
"duration",
|
||||||
|
"milestone"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -69,12 +70,17 @@
|
|||||||
"fieldname": "day",
|
"fieldname": "day",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"label": "Day"
|
"label": "Day"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "milestone",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Milestone"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-10-03 17:40:31.530181",
|
"modified": "2023-10-20 11:58:01.782921",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Batch Timetable",
|
"name": "LMS Batch Timetable",
|
||||||
|
|||||||
@@ -10,6 +10,14 @@ frappe.ui.form.on("LMS Certificate", {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
frm.set_query("template", function (doc) {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
doc_type: "LMS Certificate",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
},
|
},
|
||||||
refresh: (frm) => {
|
refresh: (frm) => {
|
||||||
if (frm.doc.name)
|
if (frm.doc.name)
|
||||||
|
|||||||
@@ -8,11 +8,12 @@
|
|||||||
"course",
|
"course",
|
||||||
"member",
|
"member",
|
||||||
"member_name",
|
"member_name",
|
||||||
"published",
|
"template",
|
||||||
"column_break_3",
|
"column_break_3",
|
||||||
"issue_date",
|
"issue_date",
|
||||||
"expiry_date",
|
"expiry_date",
|
||||||
"batch_name"
|
"batch_name",
|
||||||
|
"published"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -67,11 +68,18 @@
|
|||||||
"fieldname": "published",
|
"fieldname": "published",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Publish on Participant Page"
|
"label": "Publish on Participant Page"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "template",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Template",
|
||||||
|
"options": "Print Format",
|
||||||
|
"reqd": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-09-13 11:03:23.479255",
|
"modified": "2023-10-25 12:20:56.091979",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Certificate",
|
"name": "LMS Certificate",
|
||||||
|
|||||||
@@ -6,12 +6,42 @@ from frappe import _
|
|||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import add_years, nowdate
|
from frappe.utils import add_years, nowdate
|
||||||
from lms.lms.utils import is_certified
|
from lms.lms.utils import is_certified
|
||||||
|
from frappe.email.doctype.email_template.email_template import get_email_template
|
||||||
|
|
||||||
|
|
||||||
class LMSCertificate(Document):
|
class LMSCertificate(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
self.validate_duplicate_certificate()
|
self.validate_duplicate_certificate()
|
||||||
|
|
||||||
|
def after_insert(self):
|
||||||
|
if not frappe.flags.in_test:
|
||||||
|
self.send_mail()
|
||||||
|
|
||||||
|
def send_mail(self):
|
||||||
|
subject = _("Congratulations on getting certified!")
|
||||||
|
template = "certification"
|
||||||
|
custom_template = frappe.db.get_single_value("LMS Settings", "certification_template")
|
||||||
|
|
||||||
|
args = {
|
||||||
|
"student_name": self.member_name,
|
||||||
|
"course_name": self.course,
|
||||||
|
"course_title": frappe.db.get_value("LMS Course", self.course, "title"),
|
||||||
|
"certificate_name": self.name,
|
||||||
|
}
|
||||||
|
|
||||||
|
if custom_template:
|
||||||
|
email_template = get_email_template(custom_template, args)
|
||||||
|
subject = email_template.get("subject")
|
||||||
|
content = email_template.get("message")
|
||||||
|
frappe.sendmail(
|
||||||
|
recipients=self.member,
|
||||||
|
subject=subject,
|
||||||
|
template=template if not custom_template else None,
|
||||||
|
content=content if custom_template else None,
|
||||||
|
args=args,
|
||||||
|
header=[subject, "green"],
|
||||||
|
)
|
||||||
|
|
||||||
def validate_duplicate_certificate(self):
|
def validate_duplicate_certificate(self):
|
||||||
certificates = frappe.get_all(
|
certificates = frappe.get_all(
|
||||||
"LMS Certificate",
|
"LMS Certificate",
|
||||||
@@ -48,6 +78,15 @@ def create_certificate(course):
|
|||||||
if expires_after_yrs:
|
if expires_after_yrs:
|
||||||
expiry_date = add_years(nowdate(), expires_after_yrs)
|
expiry_date = add_years(nowdate(), expires_after_yrs)
|
||||||
|
|
||||||
|
default_certificate_template = frappe.db.get_value(
|
||||||
|
"Property Setter",
|
||||||
|
{
|
||||||
|
"doc_type": "LMS Certificate",
|
||||||
|
"property": "default_print_format",
|
||||||
|
},
|
||||||
|
"value",
|
||||||
|
)
|
||||||
|
|
||||||
certificate = frappe.get_doc(
|
certificate = frappe.get_doc(
|
||||||
{
|
{
|
||||||
"doctype": "LMS Certificate",
|
"doctype": "LMS Certificate",
|
||||||
@@ -55,6 +94,7 @@ def create_certificate(course):
|
|||||||
"course": course,
|
"course": course,
|
||||||
"issue_date": nowdate(),
|
"issue_date": nowdate(),
|
||||||
"expiry_date": expiry_date,
|
"expiry_date": expiry_date,
|
||||||
|
"template": default_certificate_template,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
certificate.save(ignore_permissions=True)
|
certificate.save(ignore_permissions=True)
|
||||||
|
|||||||
@@ -47,12 +47,13 @@
|
|||||||
"fieldtype": "Rating",
|
"fieldtype": "Rating",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Rating",
|
"label": "Rating",
|
||||||
"reqd": 1
|
"mandatory_depends_on": "eval:doc.status != 'Pending' && doc.status != 'In Progress'"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "summary",
|
"fieldname": "summary",
|
||||||
"fieldtype": "Small Text",
|
"fieldtype": "Small Text",
|
||||||
"label": "Summary"
|
"label": "Summary",
|
||||||
|
"mandatory_depends_on": "eval:doc.status != 'Pending' && doc.status != 'In Progress'"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "date",
|
"fieldname": "date",
|
||||||
@@ -106,7 +107,7 @@
|
|||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-09-26 19:44:43.594892",
|
"modified": "2023-12-18 20:03:27.040073",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Certificate Evaluation",
|
"name": "LMS Certificate Evaluation",
|
||||||
|
|||||||
@@ -2,13 +2,19 @@
|
|||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
from lms.lms.utils import has_course_moderator_role
|
from lms.lms.utils import has_course_moderator_role
|
||||||
|
|
||||||
|
|
||||||
class LMSCertificateEvaluation(Document):
|
class LMSCertificateEvaluation(Document):
|
||||||
pass
|
def validate(self):
|
||||||
|
self.validate_rating()
|
||||||
|
|
||||||
|
def validate_rating(self):
|
||||||
|
if self.status not in ["Pending", "In Progress"] and self.rating == 0:
|
||||||
|
frappe.throw(_("Rating cannot be 0"))
|
||||||
|
|
||||||
|
|
||||||
def has_website_permission(doc, ptype, user, verbose=False):
|
def has_website_permission(doc, ptype, user, verbose=False):
|
||||||
|
|||||||
@@ -103,13 +103,13 @@
|
|||||||
"fieldname": "batch_name",
|
"fieldname": "batch_name",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Batch Name",
|
"label": "Batch",
|
||||||
"options": "LMS Batch"
|
"options": "LMS Batch"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-08-23 14:50:37.618352",
|
"modified": "2023-11-29 15:00:30.617298",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Certificate Request",
|
"name": "LMS Certificate Request",
|
||||||
|
|||||||
@@ -11,7 +11,20 @@ from lms.lms.utils import get_evaluator
|
|||||||
|
|
||||||
class LMSCertificateRequest(Document):
|
class LMSCertificateRequest(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
|
self.validate_slot()
|
||||||
self.validate_if_existing_requests()
|
self.validate_if_existing_requests()
|
||||||
|
self.validate_evaluation_end_date()
|
||||||
|
|
||||||
|
def validate_slot(self):
|
||||||
|
if frappe.db.exists(
|
||||||
|
"LMS Certificate Request",
|
||||||
|
{
|
||||||
|
"evaluator": self.evaluator,
|
||||||
|
"date": self.date,
|
||||||
|
"start_time": self.start_time,
|
||||||
|
},
|
||||||
|
):
|
||||||
|
frappe.throw(_("The slot is already booked by another participant."))
|
||||||
|
|
||||||
def validate_if_existing_requests(self):
|
def validate_if_existing_requests(self):
|
||||||
existing_requests = frappe.get_all(
|
existing_requests = frappe.get_all(
|
||||||
@@ -32,6 +45,20 @@ class LMSCertificateRequest(Document):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def validate_evaluation_end_date(self):
|
||||||
|
if self.batch_name:
|
||||||
|
evaluation_end_date = frappe.db.get_value(
|
||||||
|
"LMS Batch", self.batch_name, "evaluation_end_date"
|
||||||
|
)
|
||||||
|
|
||||||
|
if evaluation_end_date:
|
||||||
|
if getdate(self.date) > getdate(evaluation_end_date):
|
||||||
|
frappe.throw(
|
||||||
|
_("You cannot schedule evaluations after {0}.").format(
|
||||||
|
format_date(evaluation_end_date, "medium")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def schedule_evals():
|
def schedule_evals():
|
||||||
if frappe.db.get_single_value("LMS Settings", "send_calendar_invite_for_evaluations"):
|
if frappe.db.get_single_value("LMS Settings", "send_calendar_invite_for_evaluations"):
|
||||||
@@ -104,7 +131,9 @@ def update_meeting_details(eval, event, calendar):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def create_certificate_request(course, date, day, start_time, end_time, batch=None):
|
def create_certificate_request(
|
||||||
|
course, date, day, start_time, end_time, batch_name=None
|
||||||
|
):
|
||||||
is_member = frappe.db.exists(
|
is_member = frappe.db.exists(
|
||||||
{"doctype": "LMS Enrollment", "course": course, "member": frappe.session.user}
|
{"doctype": "LMS Enrollment", "course": course, "member": frappe.session.user}
|
||||||
)
|
)
|
||||||
@@ -115,13 +144,13 @@ def create_certificate_request(course, date, day, start_time, end_time, batch=No
|
|||||||
eval.update(
|
eval.update(
|
||||||
{
|
{
|
||||||
"course": course,
|
"course": course,
|
||||||
"evaluator": get_evaluator(course, batch),
|
"evaluator": get_evaluator(course, batch_name),
|
||||||
"member": frappe.session.user,
|
"member": frappe.session.user,
|
||||||
"date": date,
|
"date": date,
|
||||||
"day": day,
|
"day": day,
|
||||||
"start_time": start_time,
|
"start_time": start_time,
|
||||||
"end_time": end_time,
|
"end_time": end_time,
|
||||||
"batch": batch,
|
"batch_name": batch_name,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
eval.save(ignore_permissions=True)
|
eval.save(ignore_permissions=True)
|
||||||
|
|||||||
@@ -34,8 +34,10 @@
|
|||||||
"related_courses",
|
"related_courses",
|
||||||
"pricing_section",
|
"pricing_section",
|
||||||
"paid_course",
|
"paid_course",
|
||||||
"currency",
|
"column_break_acoj",
|
||||||
"course_price",
|
"course_price",
|
||||||
|
"currency",
|
||||||
|
"amount_usd",
|
||||||
"certification_section",
|
"certification_section",
|
||||||
"enable_certification",
|
"enable_certification",
|
||||||
"expiry",
|
"expiry",
|
||||||
@@ -222,12 +224,22 @@
|
|||||||
"fieldname": "course_price",
|
"fieldname": "course_price",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Course Price",
|
"label": "Course Price",
|
||||||
"option": "currency",
|
|
||||||
"mandatory_depends_on": "paid_course"
|
"mandatory_depends_on": "paid_course"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_rxww",
|
"fieldname": "column_break_rxww",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_acoj",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "paid_course",
|
||||||
|
"description": "If you set an amount here, then the USD equivalent setting will not get applied.",
|
||||||
|
"fieldname": "amount_usd",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Amount (USD)"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"is_published_field": "published",
|
"is_published_field": "published",
|
||||||
@@ -254,7 +266,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"make_attachments_public": 1,
|
"make_attachments_public": 1,
|
||||||
"modified": "2023-08-28 11:09:11.945066",
|
"modified": "2023-12-21 12:27:32.559901",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Course",
|
"name": "LMS Course",
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
// Copyright (c) 2023, Frappe and contributors
|
// Copyright (c) 2023, Frappe and contributors
|
||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
// frappe.ui.form.on("LMS Payment", {
|
frappe.ui.form.on("LMS Payment", {
|
||||||
// refresh(frm) {
|
onload(frm) {
|
||||||
|
frm.set_query("member", function (doc) {
|
||||||
// },
|
return {
|
||||||
// });
|
filters: {
|
||||||
|
ignore_user_type: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -8,8 +8,11 @@
|
|||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
|
"payment_for_document_type",
|
||||||
"member",
|
"member",
|
||||||
|
"source",
|
||||||
"column_break_rqkd",
|
"column_break_rqkd",
|
||||||
|
"payment_for_document",
|
||||||
"billing_name",
|
"billing_name",
|
||||||
"payment_received",
|
"payment_received",
|
||||||
"payment_details_section",
|
"payment_details_section",
|
||||||
@@ -115,11 +118,29 @@
|
|||||||
"fieldname": "amount_with_gst",
|
"fieldname": "amount_with_gst",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Amount with GST"
|
"label": "Amount with GST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "payment_for_document_type",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Payment for Document Type",
|
||||||
|
"options": "\nLMS Course\nLMS Batch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "payment_for_document",
|
||||||
|
"fieldtype": "Dynamic Link",
|
||||||
|
"label": "Payment for Document",
|
||||||
|
"options": "payment_for_document_type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "source",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Source",
|
||||||
|
"options": "LMS Source"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-09-12 10:40:22.721371",
|
"modified": "2023-10-26 16:54:12.408274",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Payment",
|
"name": "LMS Payment",
|
||||||
|
|||||||
8
lms/lms/doctype/lms_question/lms_question.js
Normal file
8
lms/lms/doctype/lms_question/lms_question.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// Copyright (c) 2023, Frappe and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
// frappe.ui.form.on("LMS Question", {
|
||||||
|
// refresh(frm) {
|
||||||
|
|
||||||
|
// },
|
||||||
|
// });
|
||||||
245
lms/lms/doctype/lms_question/lms_question.json
Normal file
245
lms/lms/doctype/lms_question/lms_question.json
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"autoname": "format:QTS-{YYYY}-{#####}",
|
||||||
|
"creation": "2023-10-10 10:24:14.035772",
|
||||||
|
"default_view": "List",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"question",
|
||||||
|
"type",
|
||||||
|
"multiple",
|
||||||
|
"section_break_ytxi",
|
||||||
|
"option_1",
|
||||||
|
"is_correct_1",
|
||||||
|
"column_break_fpvl",
|
||||||
|
"explanation_1",
|
||||||
|
"section_break_eiaa",
|
||||||
|
"option_2",
|
||||||
|
"is_correct_2",
|
||||||
|
"column_break_akwy",
|
||||||
|
"explanation_2",
|
||||||
|
"section_break_cwqv",
|
||||||
|
"option_3",
|
||||||
|
"is_correct_3",
|
||||||
|
"column_break_atpl",
|
||||||
|
"explanation_3",
|
||||||
|
"section_break_yqel",
|
||||||
|
"option_4",
|
||||||
|
"is_correct_4",
|
||||||
|
"column_break_lknb",
|
||||||
|
"explanation_4",
|
||||||
|
"section_break_hkfe",
|
||||||
|
"possibility_1",
|
||||||
|
"possibility_3",
|
||||||
|
"column_break_wpjr",
|
||||||
|
"possibility_2",
|
||||||
|
"possibility_4"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "question",
|
||||||
|
"fieldtype": "Text Editor",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Question"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "type",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Type",
|
||||||
|
"options": "Choices\nUser Input"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:doc.type == \"Choices\";",
|
||||||
|
"fieldname": "section_break_ytxi",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "option_1",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Option 1",
|
||||||
|
"mandatory_depends_on": "eval: doc.type == 'Choices'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "is_correct_1",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Is Correct"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_fpvl",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "explanation_1",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Explanation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:doc.type == \"Choices\";",
|
||||||
|
"fieldname": "section_break_eiaa",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "option_2",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Option 2",
|
||||||
|
"mandatory_depends_on": "eval: doc.type == 'Choices'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "is_correct_2",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Is Correct"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_akwy",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "explanation_2",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Explanation "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval: doc.type == 'Choices'",
|
||||||
|
"fieldname": "section_break_cwqv",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "option_3",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Option 3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "is_correct_3",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Is Correct"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_atpl",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "explanation_3",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Explanation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval: doc.type == 'Choices'",
|
||||||
|
"fieldname": "section_break_yqel",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "option_4",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Option 4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "is_correct_4",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Is Correct"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_lknb",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "explanation_4",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Explanation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "multiple",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Multiple Correct Answers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval: doc.type == 'User Input'",
|
||||||
|
"fieldname": "section_break_hkfe",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_wpjr",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "possibility_1",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Possible Answer 1",
|
||||||
|
"mandatory_depends_on": "eval: doc.type == 'User Input'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "possibility_3",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Possible Answer 3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "possibility_2",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Possible Answer 2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "possibility_4",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Possible Answer 4"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2023-10-18 21:58:42.653317",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "LMS",
|
||||||
|
"name": "LMS Question",
|
||||||
|
"naming_rule": "Expression",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Moderator",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Course Creator",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
|
"title_field": "question"
|
||||||
|
}
|
||||||
92
lms/lms/doctype/lms_question/lms_question.py
Normal file
92
lms/lms/doctype/lms_question/lms_question.py
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# Copyright (c) 2023, Frappe and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
from frappe.model.document import Document
|
||||||
|
from lms.lms.utils import has_course_instructor_role, has_course_moderator_role
|
||||||
|
|
||||||
|
|
||||||
|
class LMSQuestion(Document):
|
||||||
|
def validate(self):
|
||||||
|
validate_correct_answers(self)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_correct_answers(question):
|
||||||
|
if question.type == "Choices":
|
||||||
|
validate_duplicate_options(question)
|
||||||
|
validate_correct_options(question)
|
||||||
|
else:
|
||||||
|
validate_possible_answer(question)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_duplicate_options(question):
|
||||||
|
options = []
|
||||||
|
|
||||||
|
for num in range(1, 5):
|
||||||
|
if question.get(f"option_{num}"):
|
||||||
|
options.append(question.get(f"option_{num}"))
|
||||||
|
|
||||||
|
if len(set(options)) != len(options):
|
||||||
|
frappe.throw(_("Duplicate options found for this question."))
|
||||||
|
|
||||||
|
|
||||||
|
def validate_correct_options(question):
|
||||||
|
correct_options = get_correct_options(question)
|
||||||
|
|
||||||
|
if len(correct_options) > 1:
|
||||||
|
question.multiple = 1
|
||||||
|
|
||||||
|
if not len(correct_options):
|
||||||
|
frappe.throw(_("At least one option must be correct for this question."))
|
||||||
|
|
||||||
|
|
||||||
|
def validate_possible_answer(question):
|
||||||
|
possible_answers = []
|
||||||
|
possible_answers_fields = [
|
||||||
|
"possibility_1",
|
||||||
|
"possibility_2",
|
||||||
|
"possibility_3",
|
||||||
|
"possibility_4",
|
||||||
|
]
|
||||||
|
|
||||||
|
for field in possible_answers_fields:
|
||||||
|
if question.get(field):
|
||||||
|
possible_answers.append(field)
|
||||||
|
|
||||||
|
if not len(possible_answers):
|
||||||
|
frappe.throw(
|
||||||
|
_("Add at least one possible answer for this question: {0}").format(
|
||||||
|
frappe.bold(question.question)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_correct_options(question):
|
||||||
|
correct_options = []
|
||||||
|
correct_option_fields = [
|
||||||
|
"is_correct_1",
|
||||||
|
"is_correct_2",
|
||||||
|
"is_correct_3",
|
||||||
|
"is_correct_4",
|
||||||
|
]
|
||||||
|
for field in correct_option_fields:
|
||||||
|
if question.get(field) == 1:
|
||||||
|
correct_options.append(field)
|
||||||
|
|
||||||
|
return correct_options
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_question_details(question):
|
||||||
|
if not has_course_instructor_role() or not has_course_moderator_role():
|
||||||
|
return
|
||||||
|
|
||||||
|
fields = ["question", "type", "name"]
|
||||||
|
for i in range(1, 5):
|
||||||
|
fields.append(f"option_{i}")
|
||||||
|
fields.append(f"is_correct_{i}")
|
||||||
|
fields.append(f"explanation_{i}")
|
||||||
|
fields.append(f"possibility_{i}")
|
||||||
|
|
||||||
|
return frappe.db.get_value("LMS Question", question, fields, as_dict=1)
|
||||||
9
lms/lms/doctype/lms_question/test_lms_question.py
Normal file
9
lms/lms/doctype/lms_question/test_lms_question.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Copyright (c) 2023, Frappe and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.tests.utils import FrappeTestCase
|
||||||
|
|
||||||
|
|
||||||
|
class TestLMSQuestion(FrappeTestCase):
|
||||||
|
pass
|
||||||
@@ -5,3 +5,13 @@ frappe.ui.form.on("LMS Quiz", {
|
|||||||
// refresh: function(frm) {
|
// refresh: function(frm) {
|
||||||
// }
|
// }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
frappe.ui.form.on("LMS Quiz Question", {
|
||||||
|
marks: function (frm) {
|
||||||
|
total_marks = 0;
|
||||||
|
frm.doc.questions.forEach((question) => {
|
||||||
|
total_marks += question.marks;
|
||||||
|
});
|
||||||
|
frm.doc.total_marks = total_marks;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -12,6 +12,10 @@
|
|||||||
"column_break_gaac",
|
"column_break_gaac",
|
||||||
"max_attempts",
|
"max_attempts",
|
||||||
"show_submission_history",
|
"show_submission_history",
|
||||||
|
"section_break_hsiv",
|
||||||
|
"passing_percentage",
|
||||||
|
"column_break_rocd",
|
||||||
|
"total_marks",
|
||||||
"section_break_sbjx",
|
"section_break_sbjx",
|
||||||
"questions",
|
"questions",
|
||||||
"section_break_3",
|
"section_break_3",
|
||||||
@@ -43,7 +47,7 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "1",
|
"default": "0",
|
||||||
"fieldname": "max_attempts",
|
"fieldname": "max_attempts",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"label": "Max Attempts"
|
"label": "Max Attempts"
|
||||||
@@ -90,11 +94,35 @@
|
|||||||
"fieldname": "show_submission_history",
|
"fieldname": "show_submission_history",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Show Submission History"
|
"label": "Show Submission History"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_hsiv",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "passing_percentage",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": "Passing Percentage",
|
||||||
|
"non_negative": 1,
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_rocd",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "total_marks",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": "Total Marks",
|
||||||
|
"non_negative": 1,
|
||||||
|
"read_only": 1,
|
||||||
|
"reqd": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-07-04 15:26:24.457745",
|
"modified": "2023-11-07 10:11:49.126789",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Quiz",
|
"name": "LMS Quiz",
|
||||||
@@ -123,6 +151,18 @@
|
|||||||
"role": "Moderator",
|
"role": "Moderator",
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Course Creator",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"show_title_field_in_link": 1,
|
"show_title_field_in_link": 1,
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import json
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import cstr
|
from frappe.utils import cstr, comma_and
|
||||||
|
from lms.lms.doctype.lms_question.lms_question import validate_correct_answers
|
||||||
from lms.lms.utils import (
|
from lms.lms.utils import (
|
||||||
generate_slug,
|
generate_slug,
|
||||||
has_course_moderator_role,
|
has_course_moderator_role,
|
||||||
@@ -14,13 +15,22 @@ from lms.lms.utils import (
|
|||||||
|
|
||||||
|
|
||||||
class LMSQuiz(Document):
|
class LMSQuiz(Document):
|
||||||
|
def validate(self):
|
||||||
|
self.validate_duplicate_questions()
|
||||||
|
self.total_marks = set_total_marks(self.name, self.questions)
|
||||||
|
|
||||||
|
def validate_duplicate_questions(self):
|
||||||
|
questions = [row.question for row in self.questions]
|
||||||
|
rows = [i + 1 for i, x in enumerate(questions) if questions.count(x) > 1]
|
||||||
|
if len(rows):
|
||||||
|
frappe.throw(
|
||||||
|
_("Rows {0} have the duplicate questions.").format(frappe.bold(comma_and(rows)))
|
||||||
|
)
|
||||||
|
|
||||||
def autoname(self):
|
def autoname(self):
|
||||||
if not self.name:
|
if not self.name:
|
||||||
self.name = generate_slug(self.title, "LMS Quiz")
|
self.name = generate_slug(self.title, "LMS Quiz")
|
||||||
|
|
||||||
def validate(self):
|
|
||||||
validate_correct_answers(self.questions)
|
|
||||||
|
|
||||||
def get_last_submission_details(self):
|
def get_last_submission_details(self):
|
||||||
"""Returns the latest submission for this user."""
|
"""Returns the latest submission for this user."""
|
||||||
user = frappe.session.user
|
user = frappe.session.user
|
||||||
@@ -39,76 +49,11 @@ class LMSQuiz(Document):
|
|||||||
return result[0]
|
return result[0]
|
||||||
|
|
||||||
|
|
||||||
def get_correct_options(question):
|
def set_total_marks(quiz, questions):
|
||||||
correct_option_fields = [
|
marks = 0
|
||||||
"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 validate_correct_answers(questions):
|
|
||||||
for question in questions:
|
for question in questions:
|
||||||
if question.type == "Choices":
|
marks += question.get("marks")
|
||||||
validate_duplicate_options(question)
|
return marks
|
||||||
validate_correct_options(question)
|
|
||||||
else:
|
|
||||||
validate_possible_answer(question)
|
|
||||||
|
|
||||||
|
|
||||||
def validate_duplicate_options(question):
|
|
||||||
options = []
|
|
||||||
|
|
||||||
for num in range(1, 5):
|
|
||||||
if question.get(f"option_{num}"):
|
|
||||||
options.append(question.get(f"option_{num}"))
|
|
||||||
|
|
||||||
if len(set(options)) != len(options):
|
|
||||||
frappe.throw(
|
|
||||||
_("Duplicate options found for this question: {0}").format(
|
|
||||||
frappe.bold(question.question)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def validate_correct_options(question):
|
|
||||||
correct_options = get_correct_options(question)
|
|
||||||
|
|
||||||
if len(correct_options) > 1:
|
|
||||||
question.multiple = 1
|
|
||||||
|
|
||||||
if not len(correct_options):
|
|
||||||
frappe.throw(
|
|
||||||
_("At least one option must be correct for this question: {0}").format(
|
|
||||||
frappe.bold(question.question)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def validate_possible_answer(question):
|
|
||||||
possible_answers_fields = [
|
|
||||||
"possibility_1",
|
|
||||||
"possibility_2",
|
|
||||||
"possibility_3",
|
|
||||||
"possibility_4",
|
|
||||||
]
|
|
||||||
possible_answers = list(filter(lambda x: question.get(x), possible_answers_fields))
|
|
||||||
|
|
||||||
if not len(possible_answers):
|
|
||||||
frappe.throw(
|
|
||||||
_("Add at least one possible answer for this question: {0}").format(
|
|
||||||
frappe.bold(question.question)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def update_lesson_info(doc, method):
|
|
||||||
if doc.quiz_id:
|
|
||||||
frappe.db.set_value(
|
|
||||||
"LMS Quiz", doc.quiz_id, {"lesson": doc.name, "course": doc.course}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@@ -118,45 +63,73 @@ def quiz_summary(quiz, results):
|
|||||||
|
|
||||||
for result in results:
|
for result in results:
|
||||||
correct = result["is_correct"][0]
|
correct = result["is_correct"][0]
|
||||||
result["question"] = frappe.db.get_value(
|
|
||||||
"LMS Quiz Question",
|
|
||||||
{"parent": quiz, "idx": result["question_index"] + 1},
|
|
||||||
["question"],
|
|
||||||
)
|
|
||||||
|
|
||||||
for point in result["is_correct"]:
|
for point in result["is_correct"]:
|
||||||
correct = correct and point
|
correct = correct and point
|
||||||
|
|
||||||
result["is_correct"] = correct
|
result["is_correct"] = correct
|
||||||
score += correct
|
|
||||||
|
question_details = frappe.db.get_value(
|
||||||
|
"LMS Quiz Question",
|
||||||
|
{"parent": quiz, "idx": result["question_index"]},
|
||||||
|
["question", "marks"],
|
||||||
|
as_dict=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
result["question_name"] = question_details.question
|
||||||
|
result["question"] = frappe.db.get_value(
|
||||||
|
"LMS Question", question_details.question, "question"
|
||||||
|
)
|
||||||
|
marks = question_details.marks if correct else 0
|
||||||
|
|
||||||
|
result["marks"] = marks
|
||||||
|
score += marks
|
||||||
|
|
||||||
del result["question_index"]
|
del result["question_index"]
|
||||||
|
|
||||||
|
quiz_details = frappe.db.get_value(
|
||||||
|
"LMS Quiz", quiz, ["total_marks", "passing_percentage"], as_dict=1
|
||||||
|
)
|
||||||
|
score_out_of = quiz_details.total_marks
|
||||||
|
percentage = (score / score_out_of) * 100
|
||||||
|
|
||||||
submission = frappe.get_doc(
|
submission = frappe.get_doc(
|
||||||
{
|
{
|
||||||
"doctype": "LMS Quiz Submission",
|
"doctype": "LMS Quiz Submission",
|
||||||
"quiz": quiz,
|
"quiz": quiz,
|
||||||
"result": results,
|
"result": results,
|
||||||
"score": score,
|
"score": score,
|
||||||
|
"score_out_of": score_out_of,
|
||||||
"member": frappe.session.user,
|
"member": frappe.session.user,
|
||||||
|
"percentage": percentage,
|
||||||
|
"passing_percentage": quiz_details.passing_percentage,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
submission.save(ignore_permissions=True)
|
submission.save(ignore_permissions=True)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"score": score,
|
"score": score,
|
||||||
|
"score_out_of": score_out_of,
|
||||||
"submission": submission.name,
|
"submission": submission.name,
|
||||||
|
"pass": percentage == quiz_details.passing_percentage,
|
||||||
|
"percentage": percentage,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def save_quiz(
|
def save_quiz(
|
||||||
quiz_title, max_attempts=1, quiz=None, show_answers=1, show_submission_history=0
|
quiz_title,
|
||||||
|
passing_percentage,
|
||||||
|
questions,
|
||||||
|
max_attempts=0,
|
||||||
|
quiz=None,
|
||||||
|
show_answers=1,
|
||||||
|
show_submission_history=0,
|
||||||
):
|
):
|
||||||
if not has_course_moderator_role() or not has_course_instructor_role():
|
if not has_course_moderator_role() or not has_course_instructor_role():
|
||||||
return
|
return
|
||||||
|
|
||||||
values = {
|
values = {
|
||||||
"title": quiz_title,
|
"title": quiz_title,
|
||||||
|
"passing_percentage": passing_percentage,
|
||||||
"max_attempts": max_attempts,
|
"max_attempts": max_attempts,
|
||||||
"show_answers": show_answers,
|
"show_answers": show_answers,
|
||||||
"show_submission_history": show_submission_history,
|
"show_submission_history": show_submission_history,
|
||||||
@@ -164,38 +137,74 @@ def save_quiz(
|
|||||||
|
|
||||||
if quiz:
|
if quiz:
|
||||||
frappe.db.set_value("LMS Quiz", quiz, values)
|
frappe.db.set_value("LMS Quiz", quiz, values)
|
||||||
|
update_questions(quiz, questions)
|
||||||
return quiz
|
return quiz
|
||||||
else:
|
else:
|
||||||
doc = frappe.new_doc("LMS Quiz")
|
doc = frappe.new_doc("LMS Quiz")
|
||||||
doc.update(values)
|
doc.update(values)
|
||||||
doc.save(ignore_permissions=True)
|
doc.save()
|
||||||
|
update_questions(doc.name, questions)
|
||||||
return doc.name
|
return doc.name
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
def update_questions(quiz, questions):
|
||||||
def save_question(quiz, values, index):
|
questions = json.loads(questions)
|
||||||
values = frappe._dict(json.loads(values))
|
|
||||||
validate_correct_answers([values])
|
|
||||||
|
|
||||||
if values.get("name"):
|
delete_questions(quiz, questions)
|
||||||
doc = frappe.get_doc("LMS Quiz Question", values.get("name"))
|
add_questions(quiz, questions)
|
||||||
else:
|
frappe.db.set_value("LMS Quiz", quiz, "total_marks", set_total_marks(quiz, questions))
|
||||||
doc = frappe.new_doc("LMS Quiz Question")
|
|
||||||
|
|
||||||
doc.update(
|
|
||||||
|
def delete_questions(quiz, questions):
|
||||||
|
existing_questions = frappe.get_all(
|
||||||
|
"LMS Quiz Question",
|
||||||
{
|
{
|
||||||
"question": values["question"],
|
"parent": quiz,
|
||||||
"type": values["type"],
|
},
|
||||||
}
|
pluck="name",
|
||||||
)
|
)
|
||||||
|
|
||||||
if not values.get("name"):
|
current_questions = [question.get("question_name") for question in questions]
|
||||||
|
|
||||||
|
for question in existing_questions:
|
||||||
|
if question not in current_questions:
|
||||||
|
frappe.db.delete("LMS Quiz Question", question)
|
||||||
|
|
||||||
|
|
||||||
|
def add_questions(quiz, questions):
|
||||||
|
for index, question in enumerate(questions):
|
||||||
|
question = frappe._dict(question)
|
||||||
|
if question.question_name:
|
||||||
|
doc = frappe.get_doc("LMS Quiz Question", question.question_name)
|
||||||
|
else:
|
||||||
|
doc = frappe.new_doc("LMS Quiz Question")
|
||||||
doc.update(
|
doc.update(
|
||||||
{
|
{
|
||||||
"parent": quiz,
|
"parent": quiz,
|
||||||
"parenttype": "LMS Quiz",
|
"parenttype": "LMS Quiz",
|
||||||
"parentfield": "questions",
|
"parentfield": "questions",
|
||||||
"idx": index,
|
"idx": index + 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
doc.update({"question": question.question, "marks": question.marks})
|
||||||
|
|
||||||
|
doc.save()
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def save_question(quiz, values, index):
|
||||||
|
values = frappe._dict(json.loads(values))
|
||||||
|
|
||||||
|
if values.get("name"):
|
||||||
|
doc = frappe.get_doc("LMS Question", values.get("name"))
|
||||||
|
else:
|
||||||
|
doc = frappe.new_doc("LMS Question")
|
||||||
|
|
||||||
|
doc.update(
|
||||||
|
{
|
||||||
|
"question": values.question,
|
||||||
|
"type": values["type"],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -222,9 +231,8 @@ def save_question(quiz, values, index):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
doc.save(ignore_permissions=True)
|
doc.save()
|
||||||
|
return doc.name
|
||||||
return quiz
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@@ -257,13 +265,13 @@ def check_choice_answers(question, answers):
|
|||||||
fields.append(f"option_{cstr(num)}")
|
fields.append(f"option_{cstr(num)}")
|
||||||
fields.append(f"is_correct_{cstr(num)}")
|
fields.append(f"is_correct_{cstr(num)}")
|
||||||
|
|
||||||
question_details = frappe.db.get_value(
|
question_details = frappe.db.get_value("LMS Question", question, fields, as_dict=1)
|
||||||
"LMS Quiz Question", question, fields, as_dict=1
|
|
||||||
)
|
|
||||||
|
|
||||||
for num in range(1, 5):
|
for num in range(1, 5):
|
||||||
if question_details[f"option_{num}"] in answers:
|
if question_details[f"option_{num}"] in answers:
|
||||||
is_correct.append(question_details[f"is_correct_{num}"])
|
is_correct.append(question_details[f"is_correct_{num}"])
|
||||||
|
elif question_details[f"is_correct_{num}"]:
|
||||||
|
is_correct.append(2)
|
||||||
else:
|
else:
|
||||||
is_correct.append(0)
|
is_correct.append(0)
|
||||||
|
|
||||||
@@ -275,9 +283,7 @@ def check_input_answers(question, answer):
|
|||||||
for num in range(1, 5):
|
for num in range(1, 5):
|
||||||
fields.append(f"possibility_{cstr(num)}")
|
fields.append(f"possibility_{cstr(num)}")
|
||||||
|
|
||||||
question_details = frappe.db.get_value(
|
question_details = frappe.db.get_value("LMS Question", question, fields, as_dict=1)
|
||||||
"LMS Quiz Question", question, fields, as_dict=1
|
|
||||||
)
|
|
||||||
for num in range(1, 5):
|
for num in range(1, 5):
|
||||||
current_possibility = question_details[f"possibility_{num}"]
|
current_possibility = question_details[f"possibility_{num}"]
|
||||||
if current_possibility and current_possibility.lower() == answer.lower():
|
if current_possibility and current_possibility.lower() == answer.lower():
|
||||||
|
|||||||
@@ -10,51 +10,36 @@ import frappe
|
|||||||
class TestLMSQuiz(unittest.TestCase):
|
class TestLMSQuiz(unittest.TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls) -> None:
|
def setUpClass(cls) -> None:
|
||||||
frappe.get_doc({"doctype": "LMS Quiz", "title": "Test Quiz"}).save(
|
frappe.get_doc(
|
||||||
ignore_permissions=True
|
{"doctype": "LMS Quiz", "title": "Test Quiz", "passing_percentage": 90}
|
||||||
)
|
).save(ignore_permissions=True)
|
||||||
|
|
||||||
def test_with_multiple_options(self):
|
def test_with_multiple_options(self):
|
||||||
quiz = frappe.get_doc("LMS Quiz", "test-quiz")
|
question = frappe.new_doc("LMS Question")
|
||||||
quiz.append(
|
question.question = "Question Multiple"
|
||||||
"questions",
|
question.type = "Choices"
|
||||||
{
|
question.option_1 = "Option 1"
|
||||||
"question": "Question Multiple",
|
question.is_correct_1 = 1
|
||||||
"type": "Choices",
|
question.option_2 = "Option 2"
|
||||||
"option_1": "Option 1",
|
question.is_correct_2 = 1
|
||||||
"is_correct_1": 1,
|
question.save()
|
||||||
"option_2": "Option 2",
|
self.assertTrue(question.multiple)
|
||||||
"is_correct_2": 1,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
quiz.save()
|
|
||||||
self.assertTrue(quiz.questions[0].multiple)
|
|
||||||
|
|
||||||
def test_with_no_correct_option(self):
|
def test_with_no_correct_option(self):
|
||||||
quiz = frappe.get_doc("LMS Quiz", "test-quiz")
|
question = frappe.new_doc("LMS Question")
|
||||||
quiz.append(
|
question.question = "Question Multiple"
|
||||||
"questions",
|
question.type = "Choices"
|
||||||
{
|
question.option_1 = "Option 1"
|
||||||
"question": "Question no correct option",
|
question.option_2 = "Option 2"
|
||||||
"type": "Choices",
|
self.assertRaises(frappe.ValidationError, question.save)
|
||||||
"option_1": "Option 1",
|
|
||||||
"option_2": "Option 2",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertRaises(frappe.ValidationError, quiz.save)
|
|
||||||
|
|
||||||
def test_with_no_possible_answers(self):
|
def test_with_no_possible_answers(self):
|
||||||
quiz = frappe.get_doc("LMS Quiz", "test-quiz")
|
question = frappe.new_doc("LMS Question")
|
||||||
quiz.append(
|
question.question = "Question Multiple"
|
||||||
"questions",
|
question.type = "User Input"
|
||||||
{
|
self.assertRaises(frappe.ValidationError, question.save)
|
||||||
"question": "Question Possible Answers",
|
|
||||||
"type": "User Input",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertRaises(frappe.ValidationError, quiz.save)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def tearDownClass(cls) -> None:
|
def tearDownClass(cls) -> None:
|
||||||
frappe.db.delete("LMS Quiz", "test-quiz")
|
frappe.db.delete("LMS Quiz", "test-quiz")
|
||||||
frappe.db.delete("LMS Quiz Question", {"parent": "test-quiz"})
|
frappe.db.delete("LMS Question")
|
||||||
|
|||||||
@@ -6,208 +6,31 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"question",
|
"question",
|
||||||
"type",
|
"marks"
|
||||||
"options_section",
|
|
||||||
"option_1",
|
|
||||||
"is_correct_1",
|
|
||||||
"column_break_5",
|
|
||||||
"explanation_1",
|
|
||||||
"section_break_5",
|
|
||||||
"option_2",
|
|
||||||
"is_correct_2",
|
|
||||||
"column_break_10",
|
|
||||||
"explanation_2",
|
|
||||||
"column_break_4",
|
|
||||||
"option_3",
|
|
||||||
"is_correct_3",
|
|
||||||
"column_break_15",
|
|
||||||
"explanation_3",
|
|
||||||
"section_break_11",
|
|
||||||
"option_4",
|
|
||||||
"is_correct_4",
|
|
||||||
"column_break_20",
|
|
||||||
"explanation_4",
|
|
||||||
"section_break_mnhr",
|
|
||||||
"possibility_1",
|
|
||||||
"possibility_3",
|
|
||||||
"column_break_vnaj",
|
|
||||||
"possibility_2",
|
|
||||||
"possibility_4",
|
|
||||||
"section_break_c1lf",
|
|
||||||
"multiple"
|
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldname": "question",
|
"fieldname": "question",
|
||||||
"fieldtype": "Text Editor",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Question",
|
"label": "Question",
|
||||||
|
"options": "LMS Question",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "option_1",
|
"default": "1",
|
||||||
"fieldtype": "Small Text",
|
"fieldname": "marks",
|
||||||
"label": "Option 1",
|
"fieldtype": "Int",
|
||||||
"mandatory_depends_on": "eval: doc.type == 'Choices'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "option_2",
|
|
||||||
"fieldtype": "Small Text",
|
|
||||||
"label": "Option 2",
|
|
||||||
"mandatory_depends_on": "eval: doc.type == 'Choices'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "option_3",
|
|
||||||
"fieldtype": "Small Text",
|
|
||||||
"label": "Option 3"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "option_4",
|
|
||||||
"fieldtype": "Small Text",
|
|
||||||
"label": "Option 4"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"depends_on": "option_1",
|
|
||||||
"fieldname": "is_correct_1",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Is Correct"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"depends_on": "option_2",
|
|
||||||
"fieldname": "is_correct_2",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Is Correct"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"depends_on": "option_3",
|
|
||||||
"fieldname": "is_correct_3",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Is Correct"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"depends_on": "option_4",
|
|
||||||
"fieldname": "is_correct_4",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Is Correct"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fieldname": "multiple",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"hidden": 1,
|
|
||||||
"label": "Multiple Correct Answers",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "eval: doc.type == 'Choices'",
|
|
||||||
"fieldname": "options_section",
|
|
||||||
"fieldtype": "Section Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "eval: doc.type == 'Choices'",
|
|
||||||
"fieldname": "column_break_4",
|
|
||||||
"fieldtype": "Section Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "eval: doc.type == 'Choices'",
|
|
||||||
"fieldname": "section_break_5",
|
|
||||||
"fieldtype": "Section Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "eval: doc.type == 'Choices'",
|
|
||||||
"fieldname": "section_break_11",
|
|
||||||
"fieldtype": "Section Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "option_1",
|
|
||||||
"fieldname": "explanation_1",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"label": "Explanation"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "option_2",
|
|
||||||
"fieldname": "explanation_2",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"label": "Explanation"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "option_3",
|
|
||||||
"fieldname": "explanation_3",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"label": "Explanation"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "option_4",
|
|
||||||
"fieldname": "explanation_4",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"label": "Explanation"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_5",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_10",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_15",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_20",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "type",
|
|
||||||
"fieldtype": "Select",
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Type",
|
"label": "Marks",
|
||||||
"options": "Choices\nUser Input"
|
"non_negative": 1,
|
||||||
},
|
"reqd": 1
|
||||||
{
|
|
||||||
"depends_on": "eval: doc.type == 'User Input'",
|
|
||||||
"fieldname": "section_break_mnhr",
|
|
||||||
"fieldtype": "Section Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "possibility_1",
|
|
||||||
"fieldtype": "Small Text",
|
|
||||||
"label": "Possible Answer 1",
|
|
||||||
"mandatory_depends_on": "eval: doc.type == 'User Input'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "possibility_2",
|
|
||||||
"fieldtype": "Small Text",
|
|
||||||
"label": "Possible Answer 2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "possibility_3",
|
|
||||||
"fieldtype": "Small Text",
|
|
||||||
"label": "Possible Answer 3"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "possibility_4",
|
|
||||||
"fieldtype": "Small Text",
|
|
||||||
"label": "Possible Answer 4"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "section_break_c1lf",
|
|
||||||
"fieldtype": "Section Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_vnaj",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-07-04 16:43:49.837134",
|
"modified": "2023-10-16 19:51:03.893144",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Quiz Question",
|
"name": "LMS Quiz Question",
|
||||||
|
|||||||
@@ -6,7 +6,11 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"question",
|
"question",
|
||||||
|
"section_break_fztv",
|
||||||
|
"question_name",
|
||||||
"answer",
|
"answer",
|
||||||
|
"column_break_flus",
|
||||||
|
"marks",
|
||||||
"is_correct"
|
"is_correct"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
@@ -31,12 +35,33 @@
|
|||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Is Correct",
|
"label": "Is Correct",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_fztv",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "question_name",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Question Name",
|
||||||
|
"options": "LMS Question"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_flus",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "marks",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Marks",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-11-24 11:15:45.931119",
|
"modified": "2023-10-17 11:55:25.641214",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Quiz Result",
|
"name": "LMS Quiz Result",
|
||||||
|
|||||||
@@ -6,11 +6,16 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"quiz",
|
"quiz",
|
||||||
"score",
|
|
||||||
"course",
|
"course",
|
||||||
"column_break_3",
|
"column_break_3",
|
||||||
"member",
|
"member",
|
||||||
"member_name",
|
"member_name",
|
||||||
|
"section_break_dkpn",
|
||||||
|
"score",
|
||||||
|
"score_out_of",
|
||||||
|
"column_break_gkip",
|
||||||
|
"percentage",
|
||||||
|
"passing_percentage",
|
||||||
"section_break_6",
|
"section_break_6",
|
||||||
"result"
|
"result"
|
||||||
],
|
],
|
||||||
@@ -31,9 +36,11 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "score",
|
"fieldname": "score",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Int",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Score"
|
"label": "Score",
|
||||||
|
"read_only": 1,
|
||||||
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "member",
|
"fieldname": "member",
|
||||||
@@ -65,12 +72,45 @@
|
|||||||
"label": "Course",
|
"label": "Course",
|
||||||
"options": "LMS Course",
|
"options": "LMS Course",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "quiz.total_marks",
|
||||||
|
"fieldname": "score_out_of",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": "Score Out Of",
|
||||||
|
"non_negative": 1,
|
||||||
|
"read_only": 1,
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_dkpn",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_gkip",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "percentage",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": "Percentage",
|
||||||
|
"non_negative": 1,
|
||||||
|
"read_only": 1,
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "passing_percentage",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": "Passing Percentage",
|
||||||
|
"non_negative": 1,
|
||||||
|
"read_only": 1,
|
||||||
|
"reqd": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"in_create": 1,
|
"in_create": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-11-15 15:27:07.770945",
|
"modified": "2023-10-17 13:07:27.979975",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Quiz Submission",
|
"name": "LMS Quiz Submission",
|
||||||
|
|||||||
@@ -6,4 +6,10 @@ from frappe.model.document import Document
|
|||||||
|
|
||||||
|
|
||||||
class LMSQuizSubmission(Document):
|
class LMSQuizSubmission(Document):
|
||||||
pass
|
def before_insert(self):
|
||||||
|
if not self.percentage:
|
||||||
|
self.set_percentage()
|
||||||
|
|
||||||
|
def set_percentage(self):
|
||||||
|
if self.score and self.score_out_of:
|
||||||
|
self.percentage = (self.score / self.score_out_of) * 100
|
||||||
|
|||||||
@@ -16,18 +16,16 @@
|
|||||||
"portal_course_creation",
|
"portal_course_creation",
|
||||||
"section_break_szgq",
|
"section_break_szgq",
|
||||||
"send_calendar_invite_for_evaluations",
|
"send_calendar_invite_for_evaluations",
|
||||||
"batch_confirmation_template",
|
"show_day_view",
|
||||||
"column_break_2",
|
|
||||||
"allow_student_progress",
|
"allow_student_progress",
|
||||||
"payment_section",
|
"column_break_2",
|
||||||
"razorpay_key",
|
"show_dashboard",
|
||||||
"razorpay_secret",
|
"show_courses",
|
||||||
"apply_gst",
|
"show_students",
|
||||||
"column_break_cfcv",
|
"show_assessments",
|
||||||
"default_currency",
|
"show_live_class",
|
||||||
"show_usd_equivalent",
|
"show_discussions",
|
||||||
"apply_rounding",
|
"show_emails",
|
||||||
"exception_country",
|
|
||||||
"signup_settings_tab",
|
"signup_settings_tab",
|
||||||
"signup_settings_section",
|
"signup_settings_section",
|
||||||
"terms_of_use",
|
"terms_of_use",
|
||||||
@@ -42,7 +40,22 @@
|
|||||||
"mentor_request_tab",
|
"mentor_request_tab",
|
||||||
"mentor_request_section",
|
"mentor_request_section",
|
||||||
"mentor_request_creation",
|
"mentor_request_creation",
|
||||||
"mentor_request_status_update"
|
"mentor_request_status_update",
|
||||||
|
"payment_settings_tab",
|
||||||
|
"payment_section",
|
||||||
|
"razorpay_key",
|
||||||
|
"razorpay_secret",
|
||||||
|
"apply_gst",
|
||||||
|
"column_break_cfcv",
|
||||||
|
"default_currency",
|
||||||
|
"show_usd_equivalent",
|
||||||
|
"apply_rounding",
|
||||||
|
"exception_country",
|
||||||
|
"email_templates_tab",
|
||||||
|
"certification_template",
|
||||||
|
"batch_confirmation_template",
|
||||||
|
"column_break_uwsp",
|
||||||
|
"assignment_submission_template"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -71,7 +84,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_2",
|
"fieldname": "column_break_2",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break",
|
||||||
|
"label": "Show Tab in Batch"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "search_placeholder",
|
"fieldname": "search_placeholder",
|
||||||
@@ -199,8 +213,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "payment_section",
|
"fieldname": "payment_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break"
|
||||||
"label": "Payment"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "default_currency",
|
"fieldname": "default_currency",
|
||||||
@@ -261,12 +274,86 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Batch Confirmation Template",
|
"label": "Batch Confirmation Template",
|
||||||
"options": "Email Template"
|
"options": "Email Template"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "1",
|
||||||
|
"fieldname": "show_courses",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Courses"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "1",
|
||||||
|
"fieldname": "show_students",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Students"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "1",
|
||||||
|
"fieldname": "show_assessments",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Assessments"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "1",
|
||||||
|
"fieldname": "show_live_class",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Live Class"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "1",
|
||||||
|
"fieldname": "show_discussions",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Discussions"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "1",
|
||||||
|
"fieldname": "show_emails",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Emails"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "payment_settings_tab",
|
||||||
|
"fieldtype": "Tab Break",
|
||||||
|
"label": "Payment Settings"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "1",
|
||||||
|
"fieldname": "show_dashboard",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Dashboard"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "certification_template",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Certificate Email Template",
|
||||||
|
"options": "Email Template"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "email_templates_tab",
|
||||||
|
"fieldtype": "Tab Break",
|
||||||
|
"label": "Email Templates"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "assignment_submission_template",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Assignment Submission Template",
|
||||||
|
"options": "Email Template"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_uwsp",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "show_day_view",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Show Day View in Timetable"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-10-09 17:27:28.615355",
|
"modified": "2023-12-12 10:32:13.638368",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Settings",
|
"name": "LMS Settings",
|
||||||
|
|||||||
8
lms/lms/doctype/lms_source/lms_source.js
Normal file
8
lms/lms/doctype/lms_source/lms_source.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// Copyright (c) 2023, Frappe and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
// frappe.ui.form.on("LMS Source", {
|
||||||
|
// refresh(frm) {
|
||||||
|
|
||||||
|
// },
|
||||||
|
// });
|
||||||
69
lms/lms/doctype/lms_source/lms_source.json
Normal file
69
lms/lms/doctype/lms_source/lms_source.json
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"autoname": "field:source",
|
||||||
|
"creation": "2023-10-26 16:28:53.932278",
|
||||||
|
"default_view": "List",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"source"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "source",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Source",
|
||||||
|
"unique": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2023-10-26 17:25:09.144367",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "LMS",
|
||||||
|
"name": "LMS Source",
|
||||||
|
"naming_rule": "By fieldname",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Moderator",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "LMS Student",
|
||||||
|
"select": 1,
|
||||||
|
"share": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
|
"title_field": "source"
|
||||||
|
}
|
||||||
9
lms/lms/doctype/lms_source/lms_source.py
Normal file
9
lms/lms/doctype/lms_source/lms_source.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Copyright (c) 2023, Frappe and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class LMSSource(Document):
|
||||||
|
pass
|
||||||
9
lms/lms/doctype/lms_source/test_lms_source.py
Normal file
9
lms/lms/doctype/lms_source/test_lms_source.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Copyright (c) 2023, Frappe and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.tests.utils import FrappeTestCase
|
||||||
|
|
||||||
|
|
||||||
|
class TestLMSSource(FrappeTestCase):
|
||||||
|
pass
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
{
|
|
||||||
"attach_print": 0,
|
|
||||||
"channel": "Email",
|
|
||||||
"creation": "2023-03-27 16:34:03.505647",
|
|
||||||
"days_in_advance": 0,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Notification",
|
|
||||||
"document_type": "LMS Assignment Submission",
|
|
||||||
"enabled": 1,
|
|
||||||
"event": "New",
|
|
||||||
"idx": 0,
|
|
||||||
"is_standard": 1,
|
|
||||||
"message": "<h3> {{ _(\"Assignment Submission\") }}\n\n{% set title = frappe.db.get_value(\"Course Lesson\", doc.lesson, \"title\") %}\n\n<p> {{ _(\"{0} has submitted their assignment for the lesson {1}\").format(doc.member_name, title) }} </p>\n\n <p> {{ _(\" Please evaluate and grade the assignment. \") }} </p>",
|
|
||||||
"modified": "2023-03-27 16:46:44.564007",
|
|
||||||
"modified_by": "Administrator",
|
|
||||||
"module": "LMS",
|
|
||||||
"name": "Assignment Submission Notification",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"recipients": [
|
|
||||||
{
|
|
||||||
"receiver_by_document_field": "evaluator"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"send_system_notification": 0,
|
|
||||||
"send_to_all_assignees": 0,
|
|
||||||
"subject": "Assignment Submission"
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<div style="background-color: #f4f5f6; padding: 1rem;">
|
|
||||||
<div style="background-color: #ffffff; width: 75%; margin: 0 auto; padding: 1rem;">
|
|
||||||
<h3> {{ _("Assignment Submission") }} </h3>
|
|
||||||
{% set title = frappe.db.get_value("Course Lesson", doc.lesson, "title") %}
|
|
||||||
<br>
|
|
||||||
<p> {{ _("{0} has submitted their assignment for the lesson {1}").format(frappe.bold(doc.member_name), frappe.bold(title)) }}
|
|
||||||
</p>
|
|
||||||
<p> {{ _(" Please evaluate and grade the assignment.") }} </p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import frappe
|
|
||||||
|
|
||||||
|
|
||||||
def get_context(context):
|
|
||||||
# do your magic here
|
|
||||||
pass
|
|
||||||
@@ -11,7 +11,8 @@
|
|||||||
"idx": 0,
|
"idx": 0,
|
||||||
"is_standard": 1,
|
"is_standard": 1,
|
||||||
"message": "{% set title = frappe.db.get_value(\"LMS Course\", doc.course, \"title\") %}\n\n<p> {{ _(\"Hey {0}\").format(doc.member_name) }} </p>\n<p> {{ _('Your evaluation for the course {0} has been scheduled on {1} at {2}.').format(title, frappe.utils.format_date(doc.date, \"medium\"), frappe.utils.format_time(doc.start_time, \"short\")) }}</p>\n<p> {{ _(\"Please prepare well and be on time for the evaluations.\") }} </p>\n",
|
"message": "{% set title = frappe.db.get_value(\"LMS Course\", doc.course, \"title\") %}\n\n<p> {{ _(\"Hey {0}\").format(doc.member_name) }} </p>\n<p> {{ _('Your evaluation for the course {0} has been scheduled on {1} at {2}.').format(title, frappe.utils.format_date(doc.date, \"medium\"), frappe.utils.format_time(doc.start_time, \"short\")) }}</p>\n<p> {{ _(\"Please prepare well and be on time for the evaluations.\") }} </p>\n",
|
||||||
"modified": "2023-02-28 19:53:47.716135",
|
"message_type": "HTML",
|
||||||
|
"modified": "2023-11-29 17:34:54.514031",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "Certificate Request Creation",
|
"name": "Certificate Request Creation",
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<p> {{ _("Please prepare well and be on time for the evaluations.") }} </p>
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{% set title = frappe.db.get_value("LMS Course", doc.course, "title") %}
|
{% set title = frappe.db.get_value("LMS Course", doc.course, "title") %}
|
||||||
|
|
||||||
<p> {{ _("Hey {0}").format(doc.member_name) }} </p>
|
<p> {{ _('Your evaluation for the course ${0} has been scheduled on ${1} at ${2}.').format(title, frappe.utils.format_date(doc.date, "medium"), frappe.utils.format_time(doc.start_time, "short")) }}</p>
|
||||||
<p> {{ _('Your evaluation for the course {0} has been scheduled on {1} at {2}.').format(title, frappe.utils.format_date(doc.date, "medium"), frappe.utils.format_time(doc.start_time, "short")) }}</p>
|
|
||||||
<p> {{ _("Please prepare well and be on time for the evaluations.") }} </p>
|
<p> {{ _("Please prepare well and be on time for the evaluations.") }} </p>
|
||||||
|
|||||||
@@ -11,8 +11,9 @@
|
|||||||
"event": "Days Before",
|
"event": "Days Before",
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"is_standard": 1,
|
"is_standard": 1,
|
||||||
"message": "{% set title = frappe.db.get_value(\"LMS Course\", doc.course, \"title\") %}\n<p> {{ _('Your evaluation for the course ${0} has been scheduled on ${1} at ${2}.').format(title, frappe.utils.format_date(doc.date, \"medium\"), frappe.utils.format_time(doc.start_time, \"short\")) }}</p>\n<p> {{ _(\"Please prepare well and be on time for the evaluations.\") }} </p>\n",
|
"message": "{% set title = frappe.db.get_value(\"LMS Course\", doc.course, \"title\") %}\n\n<p> {{ _('Your evaluation for the course {0} has been scheduled on {1} at {2}.').format(title, frappe.utils.format_date(doc.date, \"medium\"), frappe.utils.format_time(doc.start_time, \"short\")) }}</p>\n\n<p> {{ _(\"Please prepare well and be on time for the evaluations.\") }} </p>\n",
|
||||||
"modified": "2022-06-03 11:51:02.681803",
|
"message_type": "HTML",
|
||||||
|
"modified": "2023-11-29 17:26:53.355501",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "Certificate Request Reminder",
|
"name": "Certificate Request Reminder",
|
||||||
@@ -20,6 +21,9 @@
|
|||||||
"recipients": [
|
"recipients": [
|
||||||
{
|
{
|
||||||
"receiver_by_document_field": "member"
|
"receiver_by_document_field": "member"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"receiver_by_document_field": "evaluator"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"send_system_notification": 0,
|
"send_system_notification": 0,
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
{% set title = frappe.db.get_value("LMS Course", doc.course, "title") %}
|
|
||||||
<p> {{ _('Your evaluation for the course ${0} has been scheduled on ${1} at ${2}.').format(title, frappe.utils.format_date(doc.date, "medium"), frappe.utils.format_time(doc.start_time, "short")) }}</p>
|
|
||||||
<p> {{ _("Please prepare well and be on time for the evaluations.") }} </p>
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
"absolute_value": 0,
|
"absolute_value": 0,
|
||||||
"align_labels_right": 0,
|
"align_labels_right": 0,
|
||||||
"creation": "2023-08-09 17:02:21.430320",
|
"creation": "2023-08-09 17:02:21.430320",
|
||||||
"css": ".outer-border {\n font-family: \"Inter\" sans-serif;\n font-size: 16px;\n border-radius: 0.5rem;\n border: 1px solid #E2E6E9;\n padding: 1rem;\n}\n\n.inner-border {\n border: 10px solid #0089FF;\n border-radius: 8px;\n text-align: center;\n padding: 6rem 4rem;\n background-color: #FFFFFF;\n}\n\n.certificate-logo {\n height: 1.5rem;\n margin-bottom: 4rem;\n}\n\n.certificate-name {\n font-size: 2rem;\n font-weight: 500;\n color: #192734;\n margin-bottom: 0.5rem;\n}\n\n.certificate-footer {\n margin: 4rem auto 0;\n width: 70%;\n text-align: center;\n}\n\n.certificate-footer-item {\n color: #192734;\n}\n\n.cursive-font {\n font-family: cursive;\n font-weight: 600;\n}\n\n.certificate-divider {\n margin: 0.5rem 0;\n}\n\n.certificate-expiry {\n margin-left: 2rem;\n}",
|
"css": ".outer-border {\n font-family: \"Inter\" sans-serif;\n font-size: 16px;\n border-radius: 0.5rem;\n border: 1px solid #E2E6E9;\n padding: 1rem;\n}\n\n.inner-border {\n border: 8px solid #0089FF;\n border-radius: 8px;\n text-align: center;\n padding: 6rem 4rem;\n background-color: #FFFFFF;\n}\n\n.certificate-logo {\n height: 1.5rem;\n margin-bottom: 4rem;\n}\n\n.certificate-name {\n font-size: 2rem;\n font-weight: 500;\n color: #192734;\n margin-bottom: 0.5rem;\n}\n\n.certificate-footer {\n margin: 4rem auto 0;\n width: 70%;\n text-align: center;\n}\n\n.certificate-footer-item {\n color: #192734;\n}\n\n.cursive-font {\n font-family: cursive;\n font-weight: 600;\n}\n\n.certificate-divider {\n margin: 0.5rem 0;\n}\n\n.certificate-expiry {\n margin-left: 2rem;\n}",
|
||||||
"custom_format": 1,
|
"custom_format": 1,
|
||||||
"disabled": 0,
|
"disabled": 0,
|
||||||
"doc_type": "LMS Certificate",
|
"doc_type": "LMS Certificate",
|
||||||
@@ -10,19 +10,20 @@
|
|||||||
"doctype": "Print Format",
|
"doctype": "Print Format",
|
||||||
"font_size": 14,
|
"font_size": 14,
|
||||||
"format_data": "{\"header\":\"<div class=\\\"document-header\\\">\\n\\t<h3>LMS Certificate</h3>\\n\\t<p>{{ doc.name }}</p>\\n</div>\",\"sections\":[{\"label\":\"\",\"columns\":[{\"label\":\"\",\"fields\":[{\"label\":\"Course\",\"fieldname\":\"course\",\"fieldtype\":\"Link\",\"options\":\"LMS Course\"},{\"label\":\"Member\",\"fieldname\":\"member\",\"fieldtype\":\"Link\",\"options\":\"User\"},{\"label\":\"Member Name\",\"fieldname\":\"member_name\",\"fieldtype\":\"Data\"},{\"label\":\"Evaluator\",\"fieldname\":\"evaluator\",\"fieldtype\":\"Data\",\"options\":\"\"}]},{\"label\":\"\",\"fields\":[{\"label\":\"Issue Date\",\"fieldname\":\"issue_date\",\"fieldtype\":\"Date\"},{\"label\":\"Expiry Date\",\"fieldname\":\"expiry_date\",\"fieldtype\":\"Date\"},{\"label\":\"Version\",\"fieldname\":\"version\",\"fieldtype\":\"Select\",\"options\":\"V13\\nV14\"},{\"label\":\"Module Names for Certificate\",\"fieldname\":\"module_names_for_certificate\",\"fieldtype\":\"Data\"}]}],\"has_fields\":true}]}",
|
"format_data": "{\"header\":\"<div class=\\\"document-header\\\">\\n\\t<h3>LMS Certificate</h3>\\n\\t<p>{{ doc.name }}</p>\\n</div>\",\"sections\":[{\"label\":\"\",\"columns\":[{\"label\":\"\",\"fields\":[{\"label\":\"Course\",\"fieldname\":\"course\",\"fieldtype\":\"Link\",\"options\":\"LMS Course\"},{\"label\":\"Member\",\"fieldname\":\"member\",\"fieldtype\":\"Link\",\"options\":\"User\"},{\"label\":\"Member Name\",\"fieldname\":\"member_name\",\"fieldtype\":\"Data\"},{\"label\":\"Evaluator\",\"fieldname\":\"evaluator\",\"fieldtype\":\"Data\",\"options\":\"\"}]},{\"label\":\"\",\"fields\":[{\"label\":\"Issue Date\",\"fieldname\":\"issue_date\",\"fieldtype\":\"Date\"},{\"label\":\"Expiry Date\",\"fieldname\":\"expiry_date\",\"fieldtype\":\"Date\"},{\"label\":\"Version\",\"fieldname\":\"version\",\"fieldtype\":\"Select\",\"options\":\"V13\\nV14\"},{\"label\":\"Module Names for Certificate\",\"fieldname\":\"module_names_for_certificate\",\"fieldtype\":\"Data\"}]}],\"has_fields\":true}]}",
|
||||||
"html": "{% set certificate = frappe.db.get_value(\"LMS Certificate\", doc.name, [\"name\", \"member\", \"issue_date\", \"expiry_date\", \"course\"], as_dict=True) %}\n{% set member = frappe.db.get_value(\"User\", doc.member, [\"full_name\"], as_dict=True) %}\n{% set course = frappe.db.get_value(\"LMS Course\", doc.course, [\"title\", \"name\", \"image\"], as_dict=True) %}\n{% set logo = frappe.db.get_single_value(\"Website Settings\", \"banner_image\") %}\n{% set instructors = frappe.get_all(\"Course Instructor\", {\"parent\": doc.course}, pluck=\"instructor\", order_by=\"idx\") %}\n\n<meta name=\"pdfkit-orientation\" content=\"Landscape\">\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500&display=swap\" rel=\"stylesheet\">\n\n<div class=\"outer-border\">\n <div class=\"inner-border\">\n \n {% if logo %}\n <img src=\"{{ logo }}\" class=\"certificate-logo\">\n {% endif %}\n <div>\n {{ _(\"This certifies that\") }}\n </div>\n \n <div class=\"certificate-name\" style=\"\">\n {{ member.full_name }}\n </div>\n <div>\n {{ _(\"has successfully completed the course on\") }}\n <b> {{ course.title }} </b>\n on {{ frappe.utils.format_date(certificate.issue_date, \"medium\") }}.\n </div>\n \n <table class=\"certificate-footer\">\n <tr>\n {% if instructors %}\n <td>\n <div class=\"certificate-footer-item cursive-font\">\n {% for i in instructors %}\n \t\t\t\t\t{{ frappe.db.get_value(\"User\", i, \"full_name\") }}\n \t\t\t\t\t{% if not loop.last %}\n \t\t\t\t\t,\n \t\t\t\t\t{% endif %}\n \t\t\t\t\t{% endfor %}\n </div>\n <hr class=\"certificate-divider\">\n <div class=\"text-center\"> {{ _(\"Course Instructor\") }} </div>\n </td>\n {% endif %}\n \n {% if certificate.expiry_date %}\n <td style=\"width: 30%\"></td>\n \n <td class=\"certificate-expiry\">\n <div class=\"certificate-footer-item\">\n {{ frappe.utils.format_date(certificate.expiry_date, \"medium\") }}\n </div>\n <hr class=\"certificate-divider\">\n <div class=\"text-center\"> {{ _(\"Expiry Date\") }} </div>\n </td>\n {% endif %}\n </tr>\n </table>\n </div>\n </div>",
|
"html": "{% set certificate = frappe.db.get_value(\"LMS Certificate\", doc.name, [\"name\", \"member\", \"issue_date\", \"expiry_date\", \"course\"], as_dict=True) %}\n{% set member = frappe.db.get_value(\"User\", doc.member, [\"full_name\"], as_dict=True) %}\n{% set course = frappe.db.get_value(\"LMS Course\", doc.course, [\"title\", \"name\", \"image\"], as_dict=True) %}\n{% set logo = frappe.db.get_single_value(\"Website Settings\", \"banner_image\") %}\n{% set instructors = frappe.get_all(\"Course Instructor\", {\"parent\": doc.course}, pluck=\"instructor\", order_by=\"idx\") %}\n\n<meta name=\"pdfkit-orientation\" content=\"Landscape\">\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500&display=swap\" rel=\"stylesheet\">\n\n<div>\n <div class=\"inner-border\">\n \n {% if logo %}\n <img src=\"{{ logo }}\" class=\"certificate-logo\">\n {% endif %}\n <div>\n {{ _(\"This certifies that\") }}\n </div>\n \n <div class=\"certificate-name\" style=\"\">\n {{ member.full_name }}\n </div>\n <div>\n {{ _(\"has successfully completed the course on\") }}\n <b> {{ course.title }} </b>\n on {{ frappe.utils.format_date(certificate.issue_date, \"medium\") }}.\n </div>\n \n <table class=\"certificate-footer\">\n <tr>\n {% if instructors %}\n <td>\n <div class=\"certificate-footer-item cursive-font\">\n {% for i in instructors %}\n \t\t\t\t\t{{ frappe.db.get_value(\"User\", i, \"full_name\") }}\n \t\t\t\t\t{% if not loop.last %}\n \t\t\t\t\t,\n \t\t\t\t\t{% endif %}\n \t\t\t\t\t{% endfor %}\n </div>\n <hr class=\"certificate-divider\">\n <div class=\"text-center\"> {{ _(\"Course Instructor\") }} </div>\n </td>\n {% endif %}\n \n {% if certificate.expiry_date %}\n <td style=\"width: 30%\"></td>\n \n <td class=\"certificate-expiry\">\n <div class=\"certificate-footer-item\">\n {{ frappe.utils.format_date(certificate.expiry_date, \"medium\") }}\n </div>\n <hr class=\"certificate-divider\">\n <div class=\"text-center\"> {{ _(\"Expiry Date\") }} </div>\n </td>\n {% endif %}\n </tr>\n </table>\n </div>\n </div>",
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"line_breaks": 0,
|
"line_breaks": 0,
|
||||||
"margin_bottom": 0.0,
|
"margin_bottom": 0.0,
|
||||||
"margin_left": 0.0,
|
"margin_left": 0.0,
|
||||||
"margin_right": 0.0,
|
"margin_right": 0.0,
|
||||||
"margin_top": 0.0,
|
"margin_top": 0.0,
|
||||||
"modified": "2023-08-09 17:02:21.430320",
|
"modified": "2023-11-01 18:22:56.715846",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "Certificate",
|
"name": "Certificate",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"page_number": "Hide",
|
"page_number": "Hide",
|
||||||
|
"print_designer": 0,
|
||||||
"print_format_builder": 0,
|
"print_format_builder": 0,
|
||||||
"print_format_builder_beta": 1,
|
"print_format_builder_beta": 1,
|
||||||
"print_format_type": "Jinja",
|
"print_format_type": "Jinja",
|
||||||
|
|||||||
135
lms/lms/utils.py
135
lms/lms/utils.py
@@ -4,10 +4,16 @@ import frappe
|
|||||||
import json
|
import json
|
||||||
import razorpay
|
import razorpay
|
||||||
import requests
|
import requests
|
||||||
import base64
|
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.desk.doctype.dashboard_chart.dashboard_chart import get_result
|
from frappe.desk.doctype.dashboard_chart.dashboard_chart import get_result
|
||||||
from frappe.desk.doctype.notification_log.notification_log import make_notification_logs
|
from frappe.desk.doctype.notification_log.notification_log import (
|
||||||
|
make_notification_logs,
|
||||||
|
enqueue_create_notification,
|
||||||
|
get_title,
|
||||||
|
)
|
||||||
|
from frappe.utils import get_fullname
|
||||||
|
from frappe.desk.search import get_user_groups
|
||||||
|
from frappe.desk.notifications import extract_mentions
|
||||||
from frappe.utils import (
|
from frappe.utils import (
|
||||||
add_months,
|
add_months,
|
||||||
cint,
|
cint,
|
||||||
@@ -150,7 +156,7 @@ def get_lesson_details(chapter):
|
|||||||
],
|
],
|
||||||
as_dict=True,
|
as_dict=True,
|
||||||
)
|
)
|
||||||
lesson_details.number = flt(f"{chapter.idx}.{row.idx}")
|
lesson_details.number = f"{chapter.idx}.{row.idx}"
|
||||||
lesson_details.icon = get_lesson_icon(lesson_details.body)
|
lesson_details.icon = get_lesson_icon(lesson_details.body)
|
||||||
lessons.append(lesson_details)
|
lessons.append(lesson_details)
|
||||||
return lessons
|
return lessons
|
||||||
@@ -549,6 +555,9 @@ def can_create_courses(course, member=None):
|
|||||||
if portal_course_creation == "Anyone" and member in instructors:
|
if portal_course_creation == "Anyone" and member in instructors:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
if not course and has_course_instructor_role(member):
|
||||||
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@@ -604,17 +613,20 @@ def validate_image(path):
|
|||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
def create_notification_log(doc, method):
|
def handle_notifications(doc, method):
|
||||||
topic = frappe.db.get_value(
|
topic = frappe.db.get_value(
|
||||||
"Discussion Topic",
|
"Discussion Topic",
|
||||||
doc.topic,
|
doc.topic,
|
||||||
["reference_doctype", "reference_docname", "owner", "title"],
|
["reference_doctype", "reference_docname", "owner", "title"],
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
)
|
)
|
||||||
|
if topic.reference_doctype not in ["Course Lesson", "LMS Batch"]:
|
||||||
if topic.reference_doctype != "Course Lesson":
|
|
||||||
return
|
return
|
||||||
|
create_notification_log(doc, topic)
|
||||||
|
notify_mentions(doc, topic)
|
||||||
|
|
||||||
|
|
||||||
|
def create_notification_log(doc, topic):
|
||||||
course = frappe.db.get_value("Course Lesson", topic.reference_docname, "course")
|
course = frappe.db.get_value("Course Lesson", topic.reference_docname, "course")
|
||||||
instructors = frappe.db.get_all(
|
instructors = frappe.db.get_all(
|
||||||
"Course Instructor", {"parent": course}, pluck="instructor"
|
"Course Instructor", {"parent": course}, pluck="instructor"
|
||||||
@@ -641,6 +653,47 @@ def create_notification_log(doc, method):
|
|||||||
make_notification_logs(notification, users)
|
make_notification_logs(notification, users)
|
||||||
|
|
||||||
|
|
||||||
|
def notify_mentions(doc, topic):
|
||||||
|
mentions = extract_mentions(doc.reply)
|
||||||
|
if not mentions:
|
||||||
|
return
|
||||||
|
|
||||||
|
sender_fullname = get_fullname(doc.owner)
|
||||||
|
recipients = [
|
||||||
|
frappe.db.get_value(
|
||||||
|
"User",
|
||||||
|
{"enabled": 1, "name": name},
|
||||||
|
"email",
|
||||||
|
)
|
||||||
|
for name in mentions
|
||||||
|
]
|
||||||
|
subject = _("{0} mentioned you in a comment").format(sender_fullname)
|
||||||
|
template = "mention_template"
|
||||||
|
|
||||||
|
if topic.reference_doctype == "LMS Batch":
|
||||||
|
link = f"/batches/{topic.reference_docname}#discussions"
|
||||||
|
if topic.reference_doctype == "Course Lesson":
|
||||||
|
course = frappe.db.get_value("Course Lesson", topic.reference_docname, "course")
|
||||||
|
lesson_index = get_lesson_index(topic.reference_docname)
|
||||||
|
link = get_lesson_url(course, lesson_index)
|
||||||
|
|
||||||
|
args = {
|
||||||
|
"sender": sender_fullname,
|
||||||
|
"content": doc.reply,
|
||||||
|
"link": link,
|
||||||
|
}
|
||||||
|
|
||||||
|
for recipient in recipients:
|
||||||
|
frappe.sendmail(
|
||||||
|
recipients=recipient,
|
||||||
|
subject=subject,
|
||||||
|
template=template,
|
||||||
|
args=args,
|
||||||
|
header=[subject, "green"],
|
||||||
|
retry=3,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_lesson_count(course):
|
def get_lesson_count(course):
|
||||||
lesson_count = 0
|
lesson_count = 0
|
||||||
chapters = frappe.get_all("Chapter Reference", {"parent": course}, ["chapter"])
|
chapters = frappe.get_all("Chapter Reference", {"parent": course}, ["chapter"])
|
||||||
@@ -774,6 +827,17 @@ def get_telemetry_boot_info():
|
|||||||
|
|
||||||
|
|
||||||
def is_onboarding_complete():
|
def is_onboarding_complete():
|
||||||
|
onboarding_status = frappe.db.get_single_value(
|
||||||
|
"LMS Settings", "is_onboarding_complete"
|
||||||
|
)
|
||||||
|
if onboarding_status:
|
||||||
|
return {
|
||||||
|
"is_onboarded": onboarding_status,
|
||||||
|
"course_created": True,
|
||||||
|
"chapter_created": True,
|
||||||
|
"lesson_created": True,
|
||||||
|
"first_course": None,
|
||||||
|
}
|
||||||
course_created = frappe.db.a_row_exists("LMS Course")
|
course_created = frappe.db.a_row_exists("LMS Course")
|
||||||
chapter_created = frappe.db.a_row_exists("Course Chapter")
|
chapter_created = frappe.db.a_row_exists("Course Chapter")
|
||||||
lesson_created = frappe.db.a_row_exists("Course Lesson")
|
lesson_created = frappe.db.a_row_exists("Course Lesson")
|
||||||
@@ -782,7 +846,7 @@ def is_onboarding_complete():
|
|||||||
frappe.db.set_single_value("LMS Settings", "is_onboarding_complete", 1)
|
frappe.db.set_single_value("LMS Settings", "is_onboarding_complete", 1)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"is_onboarded": frappe.db.get_single_value("LMS Settings", "is_onboarding_complete"),
|
"is_onboarded": onboarding_status,
|
||||||
"course_created": course_created,
|
"course_created": course_created,
|
||||||
"chapter_created": chapter_created,
|
"chapter_created": chapter_created,
|
||||||
"lesson_created": lesson_created,
|
"lesson_created": lesson_created,
|
||||||
@@ -853,8 +917,9 @@ def get_payment_options(doctype, docname, phone, country):
|
|||||||
|
|
||||||
validate_phone_number(phone, True)
|
validate_phone_number(phone, True)
|
||||||
details = get_details(doctype, docname)
|
details = get_details(doctype, docname)
|
||||||
|
|
||||||
details.amount, details.currency = check_multicurrency(
|
details.amount, details.currency = check_multicurrency(
|
||||||
details.amount, details.currency, country
|
details.amount, details.currency, country, details.amount_usd
|
||||||
)
|
)
|
||||||
if details.currency == "INR":
|
if details.currency == "INR":
|
||||||
details.amount, details.gst_applied = apply_gst(details.amount, country)
|
details.amount, details.gst_applied = apply_gst(details.amount, country)
|
||||||
@@ -867,7 +932,7 @@ def get_payment_options(doctype, docname, phone, country):
|
|||||||
"name": frappe.db.get_single_value("Website Settings", "app_name"),
|
"name": frappe.db.get_single_value("Website Settings", "app_name"),
|
||||||
"description": _("Payment for {0} course").format(details["title"]),
|
"description": _("Payment for {0} course").format(details["title"]),
|
||||||
"order_id": order["id"],
|
"order_id": order["id"],
|
||||||
"amount": order["amount"] * 100,
|
"amount": cint(order["amount"]) * 100,
|
||||||
"currency": order["currency"],
|
"currency": order["currency"],
|
||||||
"prefill": {
|
"prefill": {
|
||||||
"name": frappe.db.get_value("User", frappe.session.user, "full_name"),
|
"name": frappe.db.get_value("User", frappe.session.user, "full_name"),
|
||||||
@@ -878,16 +943,21 @@ def get_payment_options(doctype, docname, phone, country):
|
|||||||
return options
|
return options
|
||||||
|
|
||||||
|
|
||||||
def check_multicurrency(amount, currency, country=None):
|
def check_multicurrency(amount, currency, country=None, amount_usd=None):
|
||||||
show_usd_equivalent = frappe.db.get_single_value("LMS Settings", "show_usd_equivalent")
|
show_usd_equivalent = frappe.db.get_single_value("LMS Settings", "show_usd_equivalent")
|
||||||
exception_country = frappe.get_all(
|
exception_country = frappe.get_all(
|
||||||
"Payment Country", filters={"parent": "LMS Settings"}, pluck="country"
|
"Payment Country", filters={"parent": "LMS Settings"}, pluck="country"
|
||||||
)
|
)
|
||||||
apply_rounding = frappe.db.get_single_value("LMS Settings", "apply_rounding")
|
country = (
|
||||||
country = country or frappe.db.get_value(
|
country
|
||||||
"Address", {"email_id": frappe.session.user}, "country"
|
or frappe.db.get_value("Address", {"email_id": frappe.session.user}, "country")
|
||||||
|
or frappe.db.get_value("User", frappe.session.user, "country")
|
||||||
|
or get_country_code()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if amount_usd and country and country not in exception_country:
|
||||||
|
return amount_usd, "USD"
|
||||||
|
|
||||||
if not show_usd_equivalent or currency == "USD":
|
if not show_usd_equivalent or currency == "USD":
|
||||||
return amount, currency
|
return amount, currency
|
||||||
|
|
||||||
@@ -898,8 +968,9 @@ def check_multicurrency(amount, currency, country=None):
|
|||||||
amount = amount * exchange_rate
|
amount = amount * exchange_rate
|
||||||
currency = "USD"
|
currency = "USD"
|
||||||
|
|
||||||
|
apply_rounding = frappe.db.get_single_value("LMS Settings", "apply_rounding")
|
||||||
if apply_rounding and amount % 100 != 0:
|
if apply_rounding and amount % 100 != 0:
|
||||||
amount = ceil(amount + 100 - amount % 100)
|
amount = amount + 100 - amount % 100
|
||||||
|
|
||||||
return amount, currency
|
return amount, currency
|
||||||
|
|
||||||
@@ -923,7 +994,7 @@ def get_details(doctype, docname):
|
|||||||
details = frappe.db.get_value(
|
details = frappe.db.get_value(
|
||||||
"LMS Course",
|
"LMS Course",
|
||||||
docname,
|
docname,
|
||||||
["name", "title", "paid_course", "currency", "course_price as amount"],
|
["name", "title", "paid_course", "currency", "course_price as amount", "amount_usd"],
|
||||||
as_dict=True,
|
as_dict=True,
|
||||||
)
|
)
|
||||||
if not details.paid_course:
|
if not details.paid_course:
|
||||||
@@ -932,7 +1003,7 @@ def get_details(doctype, docname):
|
|||||||
details = frappe.db.get_value(
|
details = frappe.db.get_value(
|
||||||
"LMS Batch",
|
"LMS Batch",
|
||||||
docname,
|
docname,
|
||||||
["name", "title", "paid_batch", "currency", "amount"],
|
["name", "title", "paid_batch", "currency", "amount", "amount_usd"],
|
||||||
as_dict=True,
|
as_dict=True,
|
||||||
)
|
)
|
||||||
if not details.paid_batch:
|
if not details.paid_batch:
|
||||||
@@ -981,13 +1052,15 @@ def create_order(client, amount, currency):
|
|||||||
try:
|
try:
|
||||||
return client.order.create(
|
return client.order.create(
|
||||||
{
|
{
|
||||||
"amount": amount * 100,
|
"amount": cint(amount) * 100,
|
||||||
"currency": currency,
|
"currency": currency,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Error during payment: {0}. Please contact the Administrator.").format(e)
|
_(
|
||||||
|
"Error during payment: {0} Please contact the Administrator. Amount {1} Currency {2} Formatted {3}"
|
||||||
|
).format(e, amount, currency, cint(amount))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -1029,19 +1102,23 @@ def record_payment(address, response, client, doctype, docname):
|
|||||||
"amount_with_gst": payment_details["amount_with_gst"],
|
"amount_with_gst": payment_details["amount_with_gst"],
|
||||||
"gstin": address.gstin,
|
"gstin": address.gstin,
|
||||||
"pan": address.pan,
|
"pan": address.pan,
|
||||||
|
"source": address.source,
|
||||||
|
"payment_for_document_type": doctype,
|
||||||
|
"payment_for_document": docname,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
payment_doc.save(ignore_permissions=True)
|
payment_doc.save(ignore_permissions=True)
|
||||||
return payment_doc.name
|
return payment_doc
|
||||||
|
|
||||||
|
|
||||||
def get_payment_details(doctype, docname, address):
|
def get_payment_details(doctype, docname, address):
|
||||||
amount_field = "course_price" if doctype == "LMS Course" else "amount"
|
amount_field = "course_price" if doctype == "LMS Course" else "amount"
|
||||||
amount = frappe.db.get_value(doctype, docname, amount_field)
|
amount = frappe.db.get_value(doctype, docname, amount_field)
|
||||||
currency = frappe.db.get_value(doctype, docname, "currency")
|
currency = frappe.db.get_value(doctype, docname, "currency")
|
||||||
|
amount_usd = frappe.db.get_value(doctype, docname, "amount_usd")
|
||||||
amount_with_gst = 0
|
amount_with_gst = 0
|
||||||
|
|
||||||
amount, currency = check_multicurrency(amount, currency)
|
amount, currency = check_multicurrency(amount, currency, None, amount_usd)
|
||||||
if currency == "INR" and address.country == "India":
|
if currency == "INR" and address.country == "India":
|
||||||
amount_with_gst, gst_applied = apply_gst(amount, address.country)
|
amount_with_gst, gst_applied = apply_gst(amount, address.country)
|
||||||
|
|
||||||
@@ -1055,7 +1132,7 @@ def get_payment_details(doctype, docname, address):
|
|||||||
def create_membership(course, payment):
|
def create_membership(course, payment):
|
||||||
membership = frappe.new_doc("LMS Enrollment")
|
membership = frappe.new_doc("LMS Enrollment")
|
||||||
membership.update(
|
membership.update(
|
||||||
{"member": frappe.session.user, "course": course, "payment": payment}
|
{"member": frappe.session.user, "course": course, "payment": payment.name}
|
||||||
)
|
)
|
||||||
membership.save(ignore_permissions=True)
|
membership.save(ignore_permissions=True)
|
||||||
return f"/courses/{course}/learn/1.1"
|
return f"/courses/{course}/learn/1.1"
|
||||||
@@ -1066,7 +1143,8 @@ def add_student_to_batch(batchname, payment):
|
|||||||
student.update(
|
student.update(
|
||||||
{
|
{
|
||||||
"student": frappe.session.user,
|
"student": frappe.session.user,
|
||||||
"payment": payment,
|
"payment": payment.name,
|
||||||
|
"source": payment.source,
|
||||||
"parent": batchname,
|
"parent": batchname,
|
||||||
"parenttype": "LMS Batch",
|
"parenttype": "LMS Batch",
|
||||||
"parentfield": "students",
|
"parentfield": "students",
|
||||||
@@ -1089,3 +1167,16 @@ def change_currency(amount, currency, country=None):
|
|||||||
amount = cint(amount)
|
amount = cint(amount)
|
||||||
amount, currency = check_multicurrency(amount, currency, country)
|
amount, currency = check_multicurrency(amount, currency, country)
|
||||||
return fmt_money(amount, 0, currency)
|
return fmt_money(amount, 0, currency)
|
||||||
|
|
||||||
|
|
||||||
|
def get_country_code():
|
||||||
|
ip = frappe.local.request_ip
|
||||||
|
res = requests.get(f"http://ip-api.com/json/{ip}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = res.json()
|
||||||
|
if data.get("status") != "fail":
|
||||||
|
return frappe.db.get_value("Country", {"code": data.get("countryCode")}, "name")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
|||||||
@@ -29,10 +29,6 @@
|
|||||||
<button class="btn btn-primary btn-sm notify-me pull-right" data-course="{{course.name | urlencode}}">
|
<button class="btn btn-primary btn-sm notify-me pull-right" data-course="{{course.name | urlencode}}">
|
||||||
{{ _("Notify me when available") }}
|
{{ _("Notify me when available") }}
|
||||||
</button>
|
</button>
|
||||||
{% elif show_start_learing_cta(course, membership) %}
|
|
||||||
<button class="btn btn-primary btn-sm enroll-in-course pull-right" data-course="{{ course.name | urlencode}}">
|
|
||||||
{{ _("Start Learning") }}
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from frappe import _
|
|||||||
from frappe.core.doctype.user.user import User
|
from frappe.core.doctype.user.user import User
|
||||||
from frappe.utils import cint, escape_html, random_string
|
from frappe.utils import cint, escape_html, random_string
|
||||||
from frappe.website.utils import is_signup_disabled
|
from frappe.website.utils import is_signup_disabled
|
||||||
from lms.lms.utils import get_average_rating
|
from lms.lms.utils import get_average_rating, get_country_code
|
||||||
from frappe.website.utils import cleanup_page_name
|
from frappe.website.utils import cleanup_page_name
|
||||||
from frappe.model.naming import append_number_if_name_exists
|
from frappe.model.naming import append_number_if_name_exists
|
||||||
from lms.widgets import Widgets
|
from lms.widgets import Widgets
|
||||||
@@ -260,19 +260,6 @@ def set_country_from_ip(login_manager=None, user=None):
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
def get_country_code():
|
|
||||||
ip = frappe.local.request_ip
|
|
||||||
res = requests.get(f"http://ip-api.com/json/{ip}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
data = res.json()
|
|
||||||
if data.get("status") != "fail":
|
|
||||||
return frappe.db.get_value("Country", {"code": data.get("countryCode")}, "name")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
def on_session_creation(login_manager):
|
def on_session_creation(login_manager):
|
||||||
if frappe.db.get_single_value(
|
if frappe.db.get_single_value(
|
||||||
"System Settings", "setup_complete"
|
"System Settings", "setup_complete"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
[pre_model_sync]
|
||||||
community.patches.set_email_preferences
|
community.patches.set_email_preferences
|
||||||
community.patches.change_name_for_community_members
|
community.patches.change_name_for_community_members
|
||||||
community.patches.save_abbr_for_community_members
|
community.patches.save_abbr_for_community_members
|
||||||
@@ -72,3 +73,13 @@ lms.patches.v1_0.publish_certificates
|
|||||||
lms.patches.v1_0.change_naming_for_batch_course #14-09-2023
|
lms.patches.v1_0.change_naming_for_batch_course #14-09-2023
|
||||||
execute:frappe.permissions.reset_perms("LMS Enrollment")
|
execute:frappe.permissions.reset_perms("LMS Enrollment")
|
||||||
lms.patches.v1_0.create_student_role
|
lms.patches.v1_0.create_student_role
|
||||||
|
lms.patches.v1_0.mark_confirmation_for_batch_students
|
||||||
|
lms.patches.v1_0.create_quiz_questions
|
||||||
|
lms.patches.v1_0.add_default_marks #16-10-2023
|
||||||
|
lms.patches.v1_0.add_certificate_template #26-10-2023
|
||||||
|
lms.patches.v1_0.create_batch_source
|
||||||
|
|
||||||
|
[post_model_sync]
|
||||||
|
lms.patches.v1_0.batch_tabs_settings
|
||||||
|
execute:frappe.delete_doc("Notification", "Assignment Submission Notification")
|
||||||
|
lms.patches.v1_0.change_jobs_url #17-01-2024
|
||||||
20
lms/patches/v1_0/add_certificate_template.py
Normal file
20
lms/patches/v1_0/add_certificate_template.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
frappe.reload_doc("lms", "doctype", "lms_certificate")
|
||||||
|
default_certificate_template = frappe.db.get_value(
|
||||||
|
"Property Setter",
|
||||||
|
{
|
||||||
|
"doc_type": "LMS Certificate",
|
||||||
|
"property": "default_print_format",
|
||||||
|
},
|
||||||
|
"value",
|
||||||
|
)
|
||||||
|
|
||||||
|
if frappe.db.exists("Print Format", default_certificate_template):
|
||||||
|
certificates = frappe.get_all("LMS Certificate", pluck="name")
|
||||||
|
for certificate in certificates:
|
||||||
|
frappe.db.set_value(
|
||||||
|
"LMS Certificate", certificate, "template", default_certificate_template
|
||||||
|
)
|
||||||
18
lms/patches/v1_0/add_default_marks.py
Normal file
18
lms/patches/v1_0/add_default_marks.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
frappe.reload_doc("lms", "doctype", "lms_quiz_question")
|
||||||
|
frappe.reload_doc("lms", "doctype", "lms_quiz")
|
||||||
|
questions = frappe.get_all("LMS Quiz Question", pluck="name")
|
||||||
|
|
||||||
|
for question in questions:
|
||||||
|
frappe.db.set_value("LMS Quiz Question", question, "marks", 1)
|
||||||
|
|
||||||
|
quizzes = frappe.get_all("LMS Quiz", pluck="name")
|
||||||
|
|
||||||
|
for quiz in quizzes:
|
||||||
|
questions_count = frappe.db.count("LMS Quiz Question", {"parent": quiz})
|
||||||
|
frappe.db.set_value(
|
||||||
|
"LMS Quiz", quiz, {"total_marks": questions_count, "passing_percentage": 100}
|
||||||
|
)
|
||||||
16
lms/patches/v1_0/batch_tabs_settings.py
Normal file
16
lms/patches/v1_0/batch_tabs_settings.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
fields = [
|
||||||
|
"show_dashboard",
|
||||||
|
"show_courses",
|
||||||
|
"show_students",
|
||||||
|
"show_emails",
|
||||||
|
"show_assessments",
|
||||||
|
"show_discussions",
|
||||||
|
"show_live_class",
|
||||||
|
]
|
||||||
|
|
||||||
|
for field in fields:
|
||||||
|
frappe.db.set_single_value("LMS Settings", field, 1)
|
||||||
15
lms/patches/v1_0/change_jobs_url.py
Normal file
15
lms/patches/v1_0/change_jobs_url.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
jobs_link = frappe.db.exists(
|
||||||
|
"Top Bar Item",
|
||||||
|
{
|
||||||
|
"label": "Jobs",
|
||||||
|
"url": "/jobs",
|
||||||
|
"parent_label": "Explore",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if jobs_link:
|
||||||
|
frappe.db.set_value("Top Bar Item", jobs_link, "url", "/job-openings")
|
||||||
7
lms/patches/v1_0/create_batch_source.py
Normal file
7
lms/patches/v1_0/create_batch_source.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import frappe
|
||||||
|
from lms.install import create_batch_source
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
frappe.reload_doc("lms", "doctype", "lms_source")
|
||||||
|
create_batch_source()
|
||||||
43
lms/patches/v1_0/create_quiz_questions.py
Normal file
43
lms/patches/v1_0/create_quiz_questions.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
frappe.reload_doc("lms", "doctype", "lms_question")
|
||||||
|
|
||||||
|
fields = ["name", "question", "type", "multiple"]
|
||||||
|
for num in range(1, 5):
|
||||||
|
fields.append(f"option_{num}")
|
||||||
|
fields.append(f"is_correct_{num}")
|
||||||
|
fields.append(f"explanation_{num}")
|
||||||
|
fields.append(f"possibility_{num}")
|
||||||
|
|
||||||
|
questions = frappe.get_all(
|
||||||
|
"LMS Quiz Question",
|
||||||
|
fields=fields,
|
||||||
|
)
|
||||||
|
|
||||||
|
for question in questions:
|
||||||
|
print(question.name)
|
||||||
|
doc = frappe.new_doc("LMS Question")
|
||||||
|
doc.update(
|
||||||
|
{
|
||||||
|
"question": question.question,
|
||||||
|
"type": question.type,
|
||||||
|
"multiple": question.multiple,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
for num in range(1, 5):
|
||||||
|
if question.get(f"option_{num}"):
|
||||||
|
doc.update(
|
||||||
|
{
|
||||||
|
f"option_{num}": question[f"option_{num}"],
|
||||||
|
f"is_correct_{num}": question[f"is_correct_{num}"],
|
||||||
|
f"explanation_{num}": question[f"explanation_{num}"],
|
||||||
|
f"possibility_{num}": question[f"possibility_{num}"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
doc.save()
|
||||||
|
print(doc.name)
|
||||||
|
frappe.db.set_value("LMS Quiz Question", question.name, "question", doc.name)
|
||||||
9
lms/patches/v1_0/mark_confirmation_for_batch_students.py
Normal file
9
lms/patches/v1_0/mark_confirmation_for_batch_students.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
frappe.reload_doc("lms", "doctype", "batch_student")
|
||||||
|
students = frappe.get_all("Batch Student", pluck="name")
|
||||||
|
|
||||||
|
for student in students:
|
||||||
|
frappe.db.set_value("Batch Student", student, "confirmation_email_sent", 1)
|
||||||
@@ -109,7 +109,39 @@ def quiz_renderer(quiz_name):
|
|||||||
)
|
)
|
||||||
+"</div>"
|
+"</div>"
|
||||||
|
|
||||||
quiz = frappe.get_doc("LMS Quiz", quiz_name)
|
quiz = frappe.db.get_value(
|
||||||
|
"LMS Quiz",
|
||||||
|
quiz_name,
|
||||||
|
[
|
||||||
|
"name",
|
||||||
|
"title",
|
||||||
|
"max_attempts",
|
||||||
|
"show_answers",
|
||||||
|
"show_submission_history",
|
||||||
|
"passing_percentage",
|
||||||
|
],
|
||||||
|
as_dict=True,
|
||||||
|
)
|
||||||
|
quiz.questions = []
|
||||||
|
fields = ["name", "question", "type", "multiple"]
|
||||||
|
for num in range(1, 5):
|
||||||
|
fields.append(f"option_{num}")
|
||||||
|
fields.append(f"is_correct_{num}")
|
||||||
|
fields.append(f"explanation_{num}")
|
||||||
|
fields.append(f"possibility_{num}")
|
||||||
|
|
||||||
|
questions = frappe.get_all(
|
||||||
|
"LMS Quiz Question",
|
||||||
|
filters={"parent": quiz.name},
|
||||||
|
fields=["question", "marks"],
|
||||||
|
order_by="idx",
|
||||||
|
)
|
||||||
|
|
||||||
|
for question in questions:
|
||||||
|
details = frappe.db.get_value("LMS Question", question.question, fields, as_dict=1)
|
||||||
|
details["marks"] = question.marks
|
||||||
|
quiz.questions.append(details)
|
||||||
|
|
||||||
no_of_attempts = frappe.db.count(
|
no_of_attempts = frappe.db.count(
|
||||||
"LMS Quiz Submission", {"owner": frappe.session.user, "quiz": quiz_name}
|
"LMS Quiz Submission", {"owner": frappe.session.user, "quiz": quiz_name}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -791,6 +791,7 @@ input[type=checkbox] {
|
|||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
color: var(--gray-900);
|
color: var(--gray-900);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.course-details-outline {
|
.course-details-outline {
|
||||||
@@ -2386,6 +2387,7 @@ select {
|
|||||||
border: 1px solid var(--gray-200) !important;
|
border: 1px solid var(--gray-200) !important;
|
||||||
border-radius: var(--border-radius-md) !important;
|
border-radius: var(--border-radius-md) !important;
|
||||||
background-color: var(--gray-100) !important;
|
background-color: var(--gray-100) !important;
|
||||||
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toastui-calendar-panel .toastui-calendar-day-names.toastui-calendar-week {
|
.toastui-calendar-panel .toastui-calendar-day-names.toastui-calendar-week {
|
||||||
@@ -2441,13 +2443,23 @@ select {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.calendar-legends {
|
.calendar-legends {
|
||||||
display: flex;
|
display: grid;
|
||||||
align-items: center;
|
grid-template-columns: repeat(4, 1fr);
|
||||||
justify-content: space-between;
|
width: 75%;
|
||||||
width: 50%;
|
|
||||||
margin: 0 auto 1rem;
|
margin: 0 auto 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.calendar-legends {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.batch-details {
|
.batch-details {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
margin: 2rem 0;
|
margin: 2rem 0;
|
||||||
@@ -2474,3 +2486,15 @@ select {
|
|||||||
.modal-body .ql-container {
|
.modal-body .ql-container {
|
||||||
max-height: unset !important;
|
max-height: unset !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.questions-table .row-index {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-color {
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toastui-calendar-weekday-event-block {
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
BIN
lms/public/images/lms-logo.png
Normal file
BIN
lms/public/images/lms-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
@@ -267,15 +267,6 @@ const open_batch_dialog = () => {
|
|||||||
fieldname: "published",
|
fieldname: "published",
|
||||||
default: batch_info && batch_info.published,
|
default: batch_info && batch_info.published,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
fieldtype: "Column Break",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldtype: "Int",
|
|
||||||
label: __("Seat Count"),
|
|
||||||
fieldname: "seat_count",
|
|
||||||
default: batch_info && batch_info.seat_count,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
fieldtype: "Section Break",
|
fieldtype: "Section Break",
|
||||||
},
|
},
|
||||||
@@ -293,13 +284,6 @@ const open_batch_dialog = () => {
|
|||||||
reqd: 1,
|
reqd: 1,
|
||||||
default: batch_info && batch_info.end_date,
|
default: batch_info && batch_info.end_date,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
fieldtype: "Select",
|
|
||||||
label: __("Medium"),
|
|
||||||
fieldname: "medium",
|
|
||||||
options: ["Online", "Offline"],
|
|
||||||
default: (batch_info && batch_info.medium) || "Online",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
fieldtype: "Column Break",
|
fieldtype: "Column Break",
|
||||||
},
|
},
|
||||||
@@ -308,12 +292,24 @@ const open_batch_dialog = () => {
|
|||||||
label: __("Start Time"),
|
label: __("Start Time"),
|
||||||
fieldname: "start_time",
|
fieldname: "start_time",
|
||||||
default: batch_info && batch_info.start_time,
|
default: batch_info && batch_info.start_time,
|
||||||
|
reqd: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldtype: "Time",
|
fieldtype: "Time",
|
||||||
label: __("End Time"),
|
label: __("End Time"),
|
||||||
fieldname: "end_time",
|
fieldname: "end_time",
|
||||||
default: batch_info && batch_info.end_time,
|
default: batch_info && batch_info.end_time,
|
||||||
|
reqd: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldtype: "Section Break",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldtype: "Select",
|
||||||
|
label: __("Medium"),
|
||||||
|
fieldname: "medium",
|
||||||
|
options: ["Online", "Offline"],
|
||||||
|
default: (batch_info && batch_info.medium) || "Online",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldtype: "Link",
|
fieldtype: "Link",
|
||||||
@@ -323,6 +319,21 @@ const open_batch_dialog = () => {
|
|||||||
only_select: 1,
|
only_select: 1,
|
||||||
default: batch_info && batch_info.category,
|
default: batch_info && batch_info.category,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
fieldtype: "Column Break",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldtype: "Int",
|
||||||
|
label: __("Seat Count"),
|
||||||
|
fieldname: "seat_count",
|
||||||
|
default: batch_info && batch_info.seat_count,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldtype: "Date",
|
||||||
|
label: __("Evaluation End Date"),
|
||||||
|
fieldname: "evaluation_end_date",
|
||||||
|
default: batch_info && batch_info.evaluation_end_date,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
fieldtype: "Section Break",
|
fieldtype: "Section Break",
|
||||||
},
|
},
|
||||||
@@ -381,6 +392,15 @@ const open_batch_dialog = () => {
|
|||||||
depends_on: "paid_batch",
|
depends_on: "paid_batch",
|
||||||
only_select: 1,
|
only_select: 1,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
fieldtype: "Currency",
|
||||||
|
label: __("Amount (USD)"),
|
||||||
|
fieldname: "amount_usd",
|
||||||
|
depends_on: "paid_batch",
|
||||||
|
description: __(
|
||||||
|
"If you set an amount here, then the USD equivalent setting will not get applied."
|
||||||
|
),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
primary_action_label: __("Save"),
|
primary_action_label: __("Save"),
|
||||||
primary_action: (values) => {
|
primary_action: (values) => {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
{% set certificates = get_certificates(user) %}
|
|
||||||
|
|
||||||
{% if certificates | length %}
|
{% if certificates | length %}
|
||||||
<div class="cards-parent">
|
<div class="cards-parent">
|
||||||
{% for certificate in certificates %}
|
{% for certificate in certificates %}
|
||||||
|
|||||||
10
lms/templates/emails/assignment_submission.html
Normal file
10
lms/templates/emails/assignment_submission.html
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<p>
|
||||||
|
{{ _("{0} has submitted the assignment {1}").format(frappe.bold(member_name), frappe.bold(assignment_title)) }}
|
||||||
|
</p>
|
||||||
|
<br>
|
||||||
|
<p> {{ _(" Please evaluate and grade it.") }} </p>
|
||||||
|
<br>`
|
||||||
|
<a href="/assignment-submission/{{ assignment_name }}/{{ submission_name }}">
|
||||||
|
{{ _("Open Assignment") }}
|
||||||
|
</a>
|
||||||
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<br>
|
<br>
|
||||||
<p>
|
<p>
|
||||||
{{ _("I am pleased to inform you that your enrollment for the upcoming training batch has been successfully processed. Congratulations!") }}
|
{{ _("We are pleased to inform you that you have been enrolled in our upcoming batch. Congratulations!") }}
|
||||||
</p>
|
</p>
|
||||||
<br>
|
<br>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
21
lms/templates/emails/certification.html
Normal file
21
lms/templates/emails/certification.html
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<p>
|
||||||
|
{{ _("Dear ") }} {{ student_name }},
|
||||||
|
</p>
|
||||||
|
<br>
|
||||||
|
<p>
|
||||||
|
{{ _("I am delighted to inform you that you have successfully earned your certification for the {0} course. Congratulations!").format(frappe.bold(course_title)) }}
|
||||||
|
</p>
|
||||||
|
<br>
|
||||||
|
<p>
|
||||||
|
{{ _("With this certification, you can now showcase your updated skills and share your achievement with your colleagues and on LinkedIn. To access your certificate, please click on the link provided below.") }}
|
||||||
|
</p>
|
||||||
|
<br>
|
||||||
|
<a href="/courses/{{ course_name }}/{{certificate_name}}">{{ _("Certificate Link") }}</a>
|
||||||
|
<br>
|
||||||
|
<p>
|
||||||
|
{{ _("Once again, congratulations on this significant accomplishment.")}}
|
||||||
|
</p>
|
||||||
|
<br>
|
||||||
|
<p>
|
||||||
|
{{ _("Best Regards") }}
|
||||||
|
</p>
|
||||||
11
lms/templates/emails/mention_template.html
Normal file
11
lms/templates/emails/mention_template.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<p>
|
||||||
|
{{ _("{0} mentioned you in a comment in your batch.").format(sender) }}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<blockquote>
|
||||||
|
{{ content | markdown }}
|
||||||
|
</blockquote>
|
||||||
|
</p>
|
||||||
|
<div class="more-info">
|
||||||
|
<a href="{{ link }}">{{ _("Check Discussion") }}</a>
|
||||||
|
</div>
|
||||||
@@ -6,6 +6,12 @@
|
|||||||
{{ _("This quiz consists of {0} questions.").format(quiz.questions | length) }}
|
{{ _("This quiz consists of {0} questions.").format(quiz.questions | length) }}
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
{% if quiz.passing_percentage %}
|
||||||
|
<li>
|
||||||
|
{{ _("You will have to get {0}% correct answers in order to pass the quiz.").format(quiz.passing_percentage) }}
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if quiz.max_attempts %}
|
{% if quiz.max_attempts %}
|
||||||
{% set suffix = "times" if quiz.max_attempts > 1 else "time" %}
|
{% set suffix = "times" if quiz.max_attempts > 1 else "time" %}
|
||||||
<li>
|
<li>
|
||||||
@@ -20,7 +26,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
<div id="start-banner" class="common-card-style column-card align-items-center">
|
<div id="start-banner" class="common-card-style column-card align-items-center">
|
||||||
|
|
||||||
<div class="text-center my-10">
|
<div class="text-center my-10">
|
||||||
@@ -50,8 +55,12 @@
|
|||||||
<div class="question hide" data-name="{{ question.name }}" data-type="{{ question.type }}"
|
<div class="question hide" data-name="{{ question.name }}" data-type="{{ question.type }}"
|
||||||
data-multi="{{ question.multiple }}" data-qt-index="{{ loop.index }}">
|
data-multi="{{ question.multiple }}" data-qt-index="{{ loop.index }}">
|
||||||
<div>
|
<div>
|
||||||
|
<div class="pull-right font-weight-bold">
|
||||||
|
{{ question.marks }} {{ _("Marks") }}
|
||||||
|
</div>
|
||||||
<div class="question-number">
|
<div class="question-number">
|
||||||
{{ _("Question ") }}{{ loop.index }}: {{ instruction }}</div>
|
{{ _("Question ") }}{{ loop.index }}: {{ instruction }}
|
||||||
|
</div>
|
||||||
<div class="question-text">
|
<div class="question-text">
|
||||||
{{ question.question }}
|
{{ question.question }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ frappe.ready(() => {
|
|||||||
this.answer = [];
|
this.answer = [];
|
||||||
this.is_correct = [];
|
this.is_correct = [];
|
||||||
this.show_answers = $("#quiz-title").data("show-answers");
|
this.show_answers = $("#quiz-title").data("show-answers");
|
||||||
|
this.current_index = 0;
|
||||||
localStorage.removeItem($("#quiz-title").data("name"));
|
localStorage.removeItem($("#quiz-title").data("name"));
|
||||||
|
|
||||||
$(".btn-start-quiz").click((e) => {
|
$(".btn-start-quiz").click((e) => {
|
||||||
@@ -37,7 +38,6 @@ frappe.ready(() => {
|
|||||||
$("#next").click((e) => {
|
$("#next").click((e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!this.show_answers) check_answer();
|
if (!this.show_answers) check_answer();
|
||||||
|
|
||||||
mark_active_question(e);
|
mark_active_question(e);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ frappe.ready(() => {
|
|||||||
|
|
||||||
const mark_active_question = (e = undefined) => {
|
const mark_active_question = (e = undefined) => {
|
||||||
let total_questions = $(".question").length;
|
let total_questions = $(".question").length;
|
||||||
let current_index = $(".active-question").attr("data-qt-index") || 0;
|
let current_index = this.current_index;
|
||||||
let next_index = parseInt(current_index) + 1;
|
let next_index = parseInt(current_index) + 1;
|
||||||
|
|
||||||
if (this.show_answers) {
|
if (this.show_answers) {
|
||||||
@@ -120,7 +120,6 @@ const enable_check = (e) => {
|
|||||||
const quiz_summary = (e = undefined) => {
|
const quiz_summary = (e = undefined) => {
|
||||||
e && e.preventDefault();
|
e && e.preventDefault();
|
||||||
let quiz_name = $("#quiz-title").data("name");
|
let quiz_name = $("#quiz-title").data("name");
|
||||||
let total_questions = $(".question").length;
|
|
||||||
let self = this;
|
let self = this;
|
||||||
|
|
||||||
frappe.call({
|
frappe.call({
|
||||||
@@ -135,14 +134,20 @@ const quiz_summary = (e = undefined) => {
|
|||||||
$(".quiz-footer span").addClass("hide");
|
$(".quiz-footer span").addClass("hide");
|
||||||
$("#quiz-form").prepend(
|
$("#quiz-form").prepend(
|
||||||
`<div class="summary bold-heading text-center">
|
`<div class="summary bold-heading text-center">
|
||||||
|
${__("You got")} ${Math.ceil(data.message.percentage)}% ${__("correct answers")}
|
||||||
|
</div>
|
||||||
|
<div class="summary bold-heading text-center mt-2">
|
||||||
${__("Your score is")} ${data.message.score}
|
${__("Your score is")} ${data.message.score}
|
||||||
${__("out of")} ${total_questions}
|
${__("out of")} ${data.message.score_out_of}
|
||||||
</div>`
|
</div>`
|
||||||
);
|
);
|
||||||
$("#try-again").attr("data-submission", data.message.submission);
|
$("#try-again").attr("data-submission", data.message.submission);
|
||||||
$("#try-again").removeClass("hide");
|
$("#try-again").removeClass("hide");
|
||||||
self.quiz_submitted = true;
|
self.quiz_submitted = true;
|
||||||
if (this.hasOwnProperty("marked_as_complete")) {
|
if (
|
||||||
|
this.hasOwnProperty("marked_as_complete") &&
|
||||||
|
data.message.pass
|
||||||
|
) {
|
||||||
mark_progress();
|
mark_progress();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -165,7 +170,7 @@ const check_answer = (e = undefined) => {
|
|||||||
e && e.preventDefault();
|
e && e.preventDefault();
|
||||||
let answer = $(".active-question textarea");
|
let answer = $(".active-question textarea");
|
||||||
let total_questions = $(".question").length;
|
let total_questions = $(".question").length;
|
||||||
let current_index = $(".active-question").attr("data-qt-index");
|
let current_index = this.current_index;
|
||||||
|
|
||||||
if (answer.length && !answer.val().trim()) {
|
if (answer.length && !answer.val().trim()) {
|
||||||
frappe.throw(__("Please enter your answer"));
|
frappe.throw(__("Please enter your answer"));
|
||||||
@@ -177,12 +182,13 @@ const check_answer = (e = undefined) => {
|
|||||||
$(".explanation").removeClass("hide");
|
$(".explanation").removeClass("hide");
|
||||||
$("#check").addClass("hide");
|
$("#check").addClass("hide");
|
||||||
|
|
||||||
if (current_index == total_questions) {
|
if (current_index == total_questions - 1) {
|
||||||
$("#summary").removeClass("hide");
|
$("#summary").removeClass("hide");
|
||||||
} else if (this.show_answers) {
|
} else if (this.show_answers) {
|
||||||
$("#next").removeClass("hide");
|
$("#next").removeClass("hide");
|
||||||
}
|
}
|
||||||
parse_options();
|
parse_options();
|
||||||
|
this.current_index += 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
const parse_options = () => {
|
const parse_options = () => {
|
||||||
@@ -233,7 +239,9 @@ const parse_choices = (element, is_correct) => {
|
|||||||
? add_icon(elem, "check")
|
? add_icon(elem, "check")
|
||||||
: add_icon(elem, "wrong");
|
: add_icon(elem, "wrong");
|
||||||
} else {
|
} else {
|
||||||
add_icon(elem, "minus-circle");
|
if (this.show_answers && is_correct[i] == 2)
|
||||||
|
add_icon(elem, "minus-circle-green");
|
||||||
|
else add_icon(elem, "minus-circle");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -272,12 +280,10 @@ const add_icon = (element, icon) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const add_to_local_storage = () => {
|
const add_to_local_storage = () => {
|
||||||
let current_index = $(".active-question").attr("data-qt-index");
|
|
||||||
let quiz_name = $("#quiz-title").data("name");
|
let quiz_name = $("#quiz-title").data("name");
|
||||||
let quiz_stored = JSON.parse(localStorage.getItem(quiz_name));
|
let quiz_stored = JSON.parse(localStorage.getItem(quiz_name));
|
||||||
|
|
||||||
let quiz_obj = {
|
let quiz_obj = {
|
||||||
question_index: current_index - 1,
|
question_index: this.current_index,
|
||||||
answer: self.answer.join(),
|
answer: self.answer.join(),
|
||||||
is_correct: self.is_correct,
|
is_correct: self.is_correct,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,8 +6,4 @@
|
|||||||
<a class="btn btn-secondary btn-sm" href="/login?redirect-to=/courses/{{ course.name }}">
|
<a class="btn btn-secondary btn-sm" href="/login?redirect-to=/courses/{{ course.name }}">
|
||||||
{{ _("Write a review") }}
|
{{ _("Write a review") }}
|
||||||
</a>
|
</a>
|
||||||
{% elif show_start_learing_cta(course, membership) %}
|
|
||||||
<div class="btn btn-secondary btn-sm enroll-in-course" data-course="{{ course.name | urlencode }}">
|
|
||||||
{{ _("Start Learning") }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ def get_context(context):
|
|||||||
context.no_cache = 1
|
context.no_cache = 1
|
||||||
|
|
||||||
if frappe.session.user == "Guest":
|
if frappe.session.user == "Guest":
|
||||||
raise frappe.PermissionError(_("You don't have permission to access this page."))
|
raise frappe.PermissionError(_("Please login to submit the assignment."))
|
||||||
|
|
||||||
context.is_moderator = has_course_moderator_role()
|
context.is_moderator = has_course_moderator_role()
|
||||||
submission = frappe.form_dict["submission"]
|
submission = frappe.form_dict["submission"]
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
{{ _("Title") }}
|
{{ _("Title") }}
|
||||||
</div>
|
</div>
|
||||||
<div class="">
|
<div class="">
|
||||||
<input id="lesson-title" type="text" class="field-input" data-index="{{ lesson_index }}" data-chapter="{{ chapter }}" data-course="{{ course.name }}" {% if lesson.name %} data-lesson="{{ lesson.name }}" value="{{ lesson.title }}" {% endif %}>
|
<input id="lesson-title" type="text" class="field-input" data-index="{{ lesson_index }}" data-chapter="{{ chapter | urlencode }}" data-course="{{ course.name }}" {% if lesson.name %} data-lesson="{{ lesson.name }}" value="{{ lesson.title }}" {% endif %}>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -127,7 +127,7 @@
|
|||||||
{%- block script %}
|
{%- block script %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@latest"></script>
|
<script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@latest"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/paragraph@latest"></script>
|
<script src="https://cdn.jsdelivr.net/npm/@editorjs/paragraph@2.10.0"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/header@latest"></script>
|
<script src="https://cdn.jsdelivr.net/npm/@editorjs/header@latest"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/embed@latest"></script>
|
<script src="https://cdn.jsdelivr.net/npm/@editorjs/embed@latest"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -49,9 +49,9 @@ const get_tools = () => {
|
|||||||
vimeo: true,
|
vimeo: true,
|
||||||
codepen: true,
|
codepen: true,
|
||||||
slides: {
|
slides: {
|
||||||
regex: /https:\/\/docs\.google\.com\/presentation\/d\/e\/([A-Za-z0-9_-]+)\/pub/,
|
regex: /https:\/\/docs\.google\.com\/presentation\/d\/([A-Za-z0-9_-]+)\/pub/,
|
||||||
embedUrl:
|
embedUrl:
|
||||||
"https://docs.google.com/presentation/d/e/<%= remote_id %>/embed",
|
"https://docs.google.com/presentation/d/<%= remote_id %>/embed",
|
||||||
html: "<iframe width='100%' height='300' frameborder='0' allowfullscreen='true'></iframe>",
|
html: "<iframe width='100%' height='300' frameborder='0' allowfullscreen='true'></iframe>",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -234,7 +234,7 @@ const save = () => {
|
|||||||
args: {
|
args: {
|
||||||
title: $("#lesson-title").val(),
|
title: $("#lesson-title").val(),
|
||||||
body: this.lesson_content_data,
|
body: this.lesson_content_data,
|
||||||
chapter: $("#lesson-title").data("chapter"),
|
chapter: decodeURIComponent($("#lesson-title").data("chapter")),
|
||||||
preview: $("#preview").prop("checked") ? 1 : 0,
|
preview: $("#preview").prop("checked") ? 1 : 0,
|
||||||
idx: $("#lesson-title").data("index"),
|
idx: $("#lesson-title").data("index"),
|
||||||
lesson: lesson ? lesson : "",
|
lesson: lesson ? lesson : "",
|
||||||
@@ -429,9 +429,9 @@ class Quiz {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render_quiz(quiz) {
|
render_quiz(quiz) {
|
||||||
return `<div class="common-card-style p-2 my-2 bold-heading">
|
return `<a class="common-card-style p-20 my-2 justify-center bold-heading" target="_blank" href=/quizzes/${quiz}>
|
||||||
Quiz: ${quiz}
|
Quiz: ${quiz}
|
||||||
</div>`;
|
</a>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
validate(savedData) {
|
validate(savedData) {
|
||||||
|
|||||||
@@ -107,10 +107,13 @@ def get_page_extensions(context):
|
|||||||
|
|
||||||
|
|
||||||
def get_neighbours(current, lessons):
|
def get_neighbours(current, lessons):
|
||||||
current = flt(current)
|
numbers = [lesson.number for lesson in lessons]
|
||||||
numbers = sorted(lesson.number for lesson in lessons)
|
tuples_list = [tuple(int(x) for x in s.split(".")) for s in numbers]
|
||||||
index = numbers.index(current)
|
sorted_tuples = sorted(tuples_list)
|
||||||
|
sorted_numbers = [".".join(str(num) for num in t) for t in sorted_tuples]
|
||||||
|
index = sorted_numbers.index(current)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"prev": numbers[index - 1] if index - 1 >= 0 else None,
|
"prev": sorted_numbers[index - 1] if index - 1 >= 0 else None,
|
||||||
"next": numbers[index + 1] if index + 1 < len(numbers) else None,
|
"next": sorted_numbers[index + 1] if index + 1 < len(sorted_numbers) else None,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,25 +16,9 @@
|
|||||||
{% macro QuizForm(quiz) %}
|
{% macro QuizForm(quiz) %}
|
||||||
<div id="quiz-form" {% if quiz.name %} data-name="{{ quiz.name }}" data-index="{{ quiz.questions | length }}" {% endif %}>
|
<div id="quiz-form" {% if quiz.name %} data-name="{{ quiz.name }}" data-index="{{ quiz.questions | length }}" {% endif %}>
|
||||||
{{ QuizDetails(quiz) }}
|
{{ QuizDetails(quiz) }}
|
||||||
{% if quiz.questions %}
|
|
||||||
<div class="field-group">
|
<div class="field-group">
|
||||||
<div class="field-label mb-1">
|
<div class="questions-table"></div>
|
||||||
{{ _("Questions") }}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="common-card-style column-card px-3 py-0">
|
|
||||||
{% for question in quiz.questions %}
|
|
||||||
{{ Question(question, loop.index) }}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-secondary btn-sm btn-add-question mt-4">
|
|
||||||
{{ _("Add Question") }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if quiz.name and not quiz.questions | length %}
|
|
||||||
{{ EmptyState() }}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
@@ -59,11 +43,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="align-self-center">
|
<div class="align-self-center">
|
||||||
{% if quiz.name %}
|
|
||||||
<button class="btn btn-secondary btn-sm btn-add-question mr-2">
|
|
||||||
{{ _("Add Question") }}
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
<button class="btn btn-primary btn-sm btn-save-quiz">
|
<button class="btn btn-primary btn-sm btn-save-quiz">
|
||||||
{{ _("Save") }}
|
{{ _("Save") }}
|
||||||
</button>
|
</button>
|
||||||
@@ -98,18 +77,30 @@
|
|||||||
{{ _("Enter the maximum number of times a user can attempt this quiz") }}
|
{{ _("Enter the maximum number of times a user can attempt this quiz") }}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{% set max_attempts = quiz.max_attempts if quiz.name else 1 %}
|
{% set max_attempts = quiz.max_attempts if quiz.name else 0 %}
|
||||||
<input type="number" class="field-input" id="max-attempts" value="{{ max_attempts }}">
|
<input type="number" class="field-input" id="max-attempts" value="{{ max_attempts }}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="field-group">
|
||||||
|
<div class="field-label reqd">
|
||||||
|
{{ _("Passing Percentage") }}
|
||||||
|
</div>
|
||||||
|
<div class="field-description">
|
||||||
|
{{ _("Minimum percentage required to pass this quiz.") }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="number" class="field-input" id="passing-percentage" value="{{ quiz.passing_percentage }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="field-group vertically-center">
|
<div class="field-group vertically-center">
|
||||||
{% set show_answers = quiz.show_answers or not quiz.name %}
|
{% set show_answers = quiz.show_answers or not quiz.name %}
|
||||||
<label for="show-answers" class="vertically-center mb-0">
|
<label for="show-answers" class="vertically-center mb-0">
|
||||||
<input type="checkbox" id="show-answers" {% if show_answers %} checked {% endif %}>
|
<input type="checkbox" id="show-answers" {% if show_answers %} checked {% endif %}>
|
||||||
{{ _("Show Answers") }}
|
{{ _("Show Answers") }}
|
||||||
</label>
|
</label>
|
||||||
<label for="upcoming" class="vertically-center mb-0 ml-20">
|
<label for="show-submission-history" class="vertically-center mb-0 ml-20">
|
||||||
<input type="checkbox" id="show-submission-history" {% if quiz.show_submission_history %} checked {% endif %}>
|
<input type="checkbox" id="show-submission-history" {% if quiz.show_submission_history %} checked {% endif %}>
|
||||||
{{ _("Show Submission History") }}
|
{{ _("Show Submission History") }}
|
||||||
</label>
|
</label>
|
||||||
@@ -151,5 +142,9 @@
|
|||||||
|
|
||||||
{%- block script %}
|
{%- block script %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
{{ include_script('controls.bundle.js') }}
|
{% if has_course_instructor_role() or has_course_moderator_role() %}
|
||||||
|
<script>
|
||||||
|
const quiz_questions = {{ quiz.questions or [] }}
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -1,17 +1,21 @@
|
|||||||
frappe.ready(() => {
|
frappe.ready(() => {
|
||||||
$(".btn-save-quiz").click((e) => {
|
if ($(".questions-table").length) {
|
||||||
save_quiz({
|
frappe.require("controls.bundle.js", () => {
|
||||||
quiz_title: $("#quiz-title").val(),
|
create_questions_table();
|
||||||
max_attempts: $("#max-attempts").val(),
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(".btn-save-quiz").click((e) => {
|
||||||
|
save_quiz();
|
||||||
});
|
});
|
||||||
|
|
||||||
$(".question-row").click((e) => {
|
$(".question-row").click((e) => {
|
||||||
edit_question(e);
|
edit_question(e);
|
||||||
});
|
});
|
||||||
|
|
||||||
$(".btn-add-question").click((e) => {
|
$(document).on("click", ".questions-table .link-btn", (e) => {
|
||||||
show_question_modal();
|
e.preventDefault();
|
||||||
|
fetch_question_data(e);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -31,6 +35,8 @@ const show_question_modal = (values = {}) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const get_question_fields = (values = {}) => {
|
const get_question_fields = (values = {}) => {
|
||||||
|
if (!values.question) values = {};
|
||||||
|
|
||||||
let dialog_fields = [
|
let dialog_fields = [
|
||||||
{
|
{
|
||||||
fieldtype: "Text Editor",
|
fieldtype: "Text Editor",
|
||||||
@@ -66,6 +72,7 @@ const get_question_fields = (values = {}) => {
|
|||||||
if (num <= 2) option.mandatory_depends_on = "eval:doc.type=='Choices'";
|
if (num <= 2) option.mandatory_depends_on = "eval:doc.type=='Choices'";
|
||||||
|
|
||||||
dialog_fields.push(option);
|
dialog_fields.push(option);
|
||||||
|
console.log(dialog_fields);
|
||||||
|
|
||||||
dialog_fields.push({
|
dialog_fields.push({
|
||||||
fieldtype: "Data",
|
fieldtype: "Data",
|
||||||
@@ -120,12 +127,16 @@ const edit_question = (e) => {
|
|||||||
|
|
||||||
const save_quiz = (values) => {
|
const save_quiz = (values) => {
|
||||||
validate_mandatory();
|
validate_mandatory();
|
||||||
|
validate_questions();
|
||||||
|
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "lms.lms.doctype.lms_quiz.lms_quiz.save_quiz",
|
method: "lms.lms.doctype.lms_quiz.lms_quiz.save_quiz",
|
||||||
args: {
|
args: {
|
||||||
quiz_title: values.quiz_title,
|
quiz_title: $("#quiz-title").val(),
|
||||||
max_attempts: values.max_attempts,
|
max_attempts: $("#max-attempts").val(),
|
||||||
|
passing_percentage: $("#passing-percentage").val(),
|
||||||
quiz: $("#quiz-form").data("name") || "",
|
quiz: $("#quiz-form").data("name") || "",
|
||||||
|
questions: this.table.get_value("questions"),
|
||||||
show_answers: $("#show-answers").is(":checked") ? 1 : 0,
|
show_answers: $("#show-answers").is(":checked") ? 1 : 0,
|
||||||
show_submission_history: $("#show-submission-history").is(
|
show_submission_history: $("#show-submission-history").is(
|
||||||
":checked"
|
":checked"
|
||||||
@@ -146,13 +157,45 @@ const save_quiz = (values) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const validate_mandatory = () => {
|
const validate_mandatory = () => {
|
||||||
if (!$("#quiz-title").val()) {
|
let fields = ["#quiz-title", "#passing-percentage"];
|
||||||
|
fields.forEach((field, idx) => {
|
||||||
|
if (!$(field).val()) {
|
||||||
let error = $("p")
|
let error = $("p")
|
||||||
.addClass("error-message")
|
.addClass("error-message")
|
||||||
.text(__("Please enter a Quiz Title"));
|
.text(__("Please enter a value"));
|
||||||
$(error).insertAfter("#quiz-title");
|
$(error).insertAfter(field);
|
||||||
$("#quiz-title").focus();
|
scroll_to_element($(field));
|
||||||
throw "Title is mandatory";
|
throw "This field is mandatory";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const validate_questions = () => {
|
||||||
|
let questions = this.table.get_value("questions");
|
||||||
|
|
||||||
|
if (!questions.length) {
|
||||||
|
frappe.throw(__("Please add a question."));
|
||||||
|
}
|
||||||
|
|
||||||
|
questions.forEach((question, index) => {
|
||||||
|
if (!question.question) {
|
||||||
|
frappe.throw(__("Please add question in row") + " " + (index + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!question.marks) {
|
||||||
|
frappe.throw(__("Please add marks in row") + " " + (index + 1));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const scroll_to_element = (element) => {
|
||||||
|
if ($(element).length) {
|
||||||
|
$([document.documentElement, document.body]).animate(
|
||||||
|
{
|
||||||
|
scrollTop: $(element).offset().top - 100,
|
||||||
|
},
|
||||||
|
1000
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -167,6 +210,7 @@ const save_question = (values) => {
|
|||||||
callback: (data) => {
|
callback: (data) => {
|
||||||
if (data.message) this.question_dialog.hide();
|
if (data.message) this.question_dialog.hide();
|
||||||
|
|
||||||
|
if (values.name) {
|
||||||
frappe.show_alert({
|
frappe.show_alert({
|
||||||
message: __("Saved"),
|
message: __("Saved"),
|
||||||
indicator: "green",
|
indicator: "green",
|
||||||
@@ -174,6 +218,90 @@ const save_question = (values) => {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
let details = {
|
||||||
|
question: data.message,
|
||||||
|
};
|
||||||
|
index = this.table.get_value("questions").length;
|
||||||
|
add_question_row(details, index);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const create_questions_table = () => {
|
||||||
|
this.table = new frappe.ui.FieldGroup({
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
fieldname: "questions",
|
||||||
|
fieldtype: "Table",
|
||||||
|
in_place_edit: 1,
|
||||||
|
label: __("Questions"),
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
fieldname: "question",
|
||||||
|
fieldtype: "Link",
|
||||||
|
label: __("Question"),
|
||||||
|
options: "LMS Question",
|
||||||
|
in_list_view: 1,
|
||||||
|
only_select: 1,
|
||||||
|
reqd: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "marks",
|
||||||
|
fieldtype: "Int",
|
||||||
|
label: __("Marks"),
|
||||||
|
in_list_view: 1,
|
||||||
|
reqd: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "question_name",
|
||||||
|
fieldname: "Link",
|
||||||
|
options: "LMS Quiz Question",
|
||||||
|
label: __("Question Name"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
body: $(".questions-table").get(0),
|
||||||
|
});
|
||||||
|
this.table.make();
|
||||||
|
$(".questions-table .form-section:last").removeClass("empty-section");
|
||||||
|
$(".questions-table .frappe-control").removeClass("hide-control");
|
||||||
|
$(".questions-table .form-column").addClass("p-0");
|
||||||
|
|
||||||
|
quiz_questions.forEach((question, idx) => {
|
||||||
|
add_question_row(question, idx);
|
||||||
|
});
|
||||||
|
this.table.fields_dict["questions"].grid.add_custom_button(
|
||||||
|
"New Question",
|
||||||
|
show_question_modal,
|
||||||
|
"bottom"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const add_question_row = (question, idx) => {
|
||||||
|
this.table.fields_dict["questions"].grid.add_new_row();
|
||||||
|
this.table.get_value("questions")[idx] = {
|
||||||
|
question: question.question,
|
||||||
|
marks: question.marks,
|
||||||
|
};
|
||||||
|
this.table.refresh();
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetch_question_data = (e) => {
|
||||||
|
let question_name = $(e.currentTarget)
|
||||||
|
.find(".btn-open")
|
||||||
|
.attr("href")
|
||||||
|
.split("/")[3];
|
||||||
|
|
||||||
|
frappe.call({
|
||||||
|
method: "lms.lms.doctype.lms_question.lms_question.get_question_details",
|
||||||
|
args: {
|
||||||
|
question: question_name,
|
||||||
|
},
|
||||||
|
callback: (data) => {
|
||||||
|
show_question_modal(data.message);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,14 +18,22 @@ def get_context(context):
|
|||||||
if quizname == "new-quiz":
|
if quizname == "new-quiz":
|
||||||
context.quiz = frappe._dict()
|
context.quiz = frappe._dict()
|
||||||
else:
|
else:
|
||||||
fields_arr = ["name", "question", "type"]
|
|
||||||
|
|
||||||
context.quiz = frappe.db.get_value(
|
context.quiz = frappe.db.get_value(
|
||||||
"LMS Quiz",
|
"LMS Quiz",
|
||||||
quizname,
|
quizname,
|
||||||
["title", "name", "max_attempts", "show_answers", "show_submission_history"],
|
[
|
||||||
|
"title",
|
||||||
|
"name",
|
||||||
|
"max_attempts",
|
||||||
|
"passing_percentage",
|
||||||
|
"show_answers",
|
||||||
|
"show_submission_history",
|
||||||
|
],
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fields_arr = ["name", "question", "marks"]
|
||||||
context.quiz.questions = frappe.get_all(
|
context.quiz.questions = frappe.get_all(
|
||||||
"LMS Quiz Question", {"parent": quizname}, fields_arr, order_by="idx"
|
"LMS Quiz Question", {"parent": quizname}, fields_arr, order_by="idx"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -49,11 +49,14 @@
|
|||||||
<use href="#icon-calendar"></use>
|
<use href="#icon-calendar"></use>
|
||||||
</svg>
|
</svg>
|
||||||
<span>
|
<span>
|
||||||
{{ frappe.utils.format_date(batch_info.start_date, "long") }} -
|
{{ frappe.utils.format_date(batch_info.start_date, "long") }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
{% if batch_info.start_date != batch_info.end_date %}
|
||||||
<span>
|
<span>
|
||||||
{{ frappe.utils.format_date(batch_info.end_date, "long") }}
|
- {{ frappe.utils.format_date(batch_info.end_date, "long") }}
|
||||||
</span>
|
</span>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span class="seperator"></span>
|
<span class="seperator"></span>
|
||||||
@@ -75,14 +78,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if is_moderator %}
|
|
||||||
<div class="mt-4">
|
|
||||||
<button class="btn btn-secondary btn-sm btn-email">
|
|
||||||
{{ _("Email to Students") }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if batch_info.custom_component %}
|
{% if batch_info.custom_component %}
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
{{ batch_info.custom_component }}
|
{{ batch_info.custom_component }}
|
||||||
@@ -96,8 +91,7 @@
|
|||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
|
|
||||||
<ul class="nav lms-nav" id="batches-tab">
|
<ul class="nav lms-nav" id="batches-tab">
|
||||||
|
{% if settings.show_dashboard and is_student %}
|
||||||
{% if is_student %}
|
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {% if is_student %} active {% endif %}" data-toggle="tab" href="#dashboard">
|
<a class="nav-link {% if is_student %} active {% endif %}" data-toggle="tab" href="#dashboard">
|
||||||
{{ _("Dashboard") }}
|
{{ _("Dashboard") }}
|
||||||
@@ -105,6 +99,7 @@
|
|||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if settings.show_courses %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {% if not is_student %} active {% endif %}" data-toggle="tab" href="#courses">
|
<a class="nav-link {% if not is_student %} active {% endif %}" data-toggle="tab" href="#courses">
|
||||||
{{ _("Courses") }}
|
{{ _("Courses") }}
|
||||||
@@ -113,6 +108,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if show_timetable %}
|
{% if show_timetable %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
@@ -123,6 +119,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if is_moderator %}
|
{% if is_moderator %}
|
||||||
|
{% if settings.show_students %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" data-toggle="tab" href="#students">
|
<a class="nav-link" data-toggle="tab" href="#students">
|
||||||
{{ _("Students") }}
|
{{ _("Students") }}
|
||||||
@@ -131,7 +128,9 @@
|
|||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if settings.show_assessments %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" data-toggle="tab" href="#assessments">
|
<a class="nav-link" data-toggle="tab" href="#assessments">
|
||||||
{{ _("Assessments") }}
|
{{ _("Assessments") }}
|
||||||
@@ -142,13 +141,28 @@
|
|||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if settings.show_emails %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" data-toggle="tab" href="#emails">
|
||||||
|
{{ _("Emails") }}
|
||||||
|
<span class="course-list-count">
|
||||||
|
{{ batch_emails | length }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if batch_students | length and (is_moderator or is_student) %}
|
{% if batch_students | length and (is_moderator or is_student) %}
|
||||||
|
{% if settings.show_discussions %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" data-toggle="tab" href="#discussions">
|
<a class="nav-link" data-toggle="tab" href="#discussions">
|
||||||
{{ _("Discussions") }}
|
{{ _("Discussions") }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if settings.show_live_class %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" data-toggle="tab" href="#live-class">
|
<a class="nav-link" data-toggle="tab" href="#live-class">
|
||||||
{{ _("Live Class") }}
|
{{ _("Live Class") }}
|
||||||
@@ -158,6 +172,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if custom_tabs_header %}
|
{% if custom_tabs_header %}
|
||||||
{% include custom_tabs_header %}
|
{% include custom_tabs_header %}
|
||||||
@@ -168,15 +183,17 @@
|
|||||||
|
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
|
|
||||||
{% if is_student %}
|
{% if settings.show_dashboard and is_student %}
|
||||||
<div class="tab-pane {% if is_student %} active {% endif %}" id="dashboard" role="tabpanel" aria-labelledby="dashboard">
|
<div class="tab-pane {% if is_student %} active {% endif %}" id="dashboard" role="tabpanel" aria-labelledby="dashboard">
|
||||||
{{ Dashboard(batch_info, batch_courses, current_student) }}
|
{{ Dashboard(batch_info, batch_courses, current_student) }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if settings.show_courses %}
|
||||||
<div class="tab-pane {% if not is_student %} active {% endif %}" id="courses" role="tabpanel" aria-labelledby="courses">
|
<div class="tab-pane {% if not is_student %} active {% endif %}" id="courses" role="tabpanel" aria-labelledby="courses">
|
||||||
{{ CoursesSection(batch_info, batch_courses) }}
|
{{ CoursesSection(batch_info, batch_courses) }}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if show_timetable %}
|
{% if show_timetable %}
|
||||||
<div class="tab-pane" id="timetable" role="tabpanel" aria-labelledby="timetable">
|
<div class="tab-pane" id="timetable" role="tabpanel" aria-labelledby="timetable">
|
||||||
@@ -185,24 +202,38 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if is_moderator %}
|
{% if is_moderator %}
|
||||||
|
{% if settings.show_students %}
|
||||||
<div class="tab-pane" id="students" role="tabpanel" aria-labelledby="students">
|
<div class="tab-pane" id="students" role="tabpanel" aria-labelledby="students">
|
||||||
{{ StudentsSection(batch_info, batch_students) }}
|
{{ StudentsSection(batch_info, batch_students) }}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if settings.show_assessments %}
|
||||||
<div class="tab-pane" id="assessments" role="tabpanel" aria-labelledby="assessments">
|
<div class="tab-pane" id="assessments" role="tabpanel" aria-labelledby="assessments">
|
||||||
{{ AssessmentsSection(batch_info) }}
|
{{ AssessmentsSection(batch_info) }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if settings.show_emails %}
|
||||||
|
<div class="tab-pane" id="emails" role="tabpanel" aria-labelledby="emails">
|
||||||
|
{{ EmailsSection() }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if batch_students | length and (is_moderator or is_student or is_evaluator) %}
|
{% if batch_students | length and (is_moderator or is_student or is_evaluator) %}
|
||||||
|
{% if settings.show_discussions %}
|
||||||
<div class="tab-pane" id="discussions" role="tabpanel" aria-labelledby="discussions">
|
<div class="tab-pane" id="discussions" role="tabpanel" aria-labelledby="discussions">
|
||||||
{{ Discussions(batch_info) }}
|
{{ Discussions(batch_info) }}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if settings.show_live_class %}
|
||||||
<div class="tab-pane" id="live-class" role="tabpanel" aria-labelledby="live-class">
|
<div class="tab-pane" id="live-class" role="tabpanel" aria-labelledby="live-class">
|
||||||
{{ LiveClassSection(batch_info, live_classes) }}
|
{{ LiveClassSection(batch_info, live_classes) }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if custom_tabs_content %}
|
{% if custom_tabs_content %}
|
||||||
{% include custom_tabs_content %}
|
{% include custom_tabs_content %}
|
||||||
@@ -376,6 +407,41 @@
|
|||||||
</article>
|
</article>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
|
{% macro EmailsSection() %}
|
||||||
|
<div class="my-4">
|
||||||
|
<button class="btn btn-secondary btn-sm btn-email">
|
||||||
|
{{ _("Email to Students") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{% for email in batch_emails %}
|
||||||
|
<div class="frappe-card mb-5">
|
||||||
|
<div class="flex justify-between m-1">
|
||||||
|
<span class="text-color flex">
|
||||||
|
<span class="margin-right">
|
||||||
|
{% set member = frappe.db.get_value("User", email.sender, ["full_name", "username", "name", "user_image"], as_dict=1) %}
|
||||||
|
{{ widgets.Avatar(member=member, avatar_class="avatar-small") }}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{{ member.full_name }}
|
||||||
|
<div class="text-muted">
|
||||||
|
<span class="frappe-timestamp" data-timestamp="{{ email.communication_date }}" title="{{ communication_date }}">
|
||||||
|
{{ frappe.utils.pretty_date(email.communication_date) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="ml-10">
|
||||||
|
{{ email.content }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
{% macro AssessmentList(assessments) %}
|
{% macro AssessmentList(assessments) %}
|
||||||
{% if assessments | length %}
|
{% if assessments | length %}
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
@@ -575,7 +641,10 @@
|
|||||||
frappe.boot.single_types = []
|
frappe.boot.single_types = []
|
||||||
let courses = {{ course_list | json }};
|
let courses = {{ course_list | json }};
|
||||||
const legends = {{ legends | json }};
|
const legends = {{ legends | json }};
|
||||||
const allow_future = {{ batch_info.allow_future }}
|
const allow_future = {{ batch_info.allow_future }};
|
||||||
|
const is_student = "{{ is_student or '' }}";
|
||||||
|
const evaluation_end_date = "{{ batch_info.evaluation_end_date if batch_info.evaluation_end_date else '' }}"
|
||||||
|
const show_day_view = {{ settings.show_day_view }};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://uicdn.toast.com/calendar/latest/toastui-calendar.min.css" />
|
<link rel="stylesheet" href="https://uicdn.toast.com/calendar/latest/toastui-calendar.min.css" />
|
||||||
|
|||||||
@@ -517,6 +517,10 @@ const open_evaluation_form = (e) => {
|
|||||||
},
|
},
|
||||||
filter_description: " ",
|
filter_description: " ",
|
||||||
only_select: 1,
|
only_select: 1,
|
||||||
|
change: () => {
|
||||||
|
this.eval_form.set_value("date", "");
|
||||||
|
$("[data-fieldname='slots']").html("");
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldtype: "Date",
|
fieldtype: "Date",
|
||||||
@@ -526,8 +530,11 @@ const open_evaluation_form = (e) => {
|
|||||||
min_date: new Date(
|
min_date: new Date(
|
||||||
frappe.datetime.add_days(frappe.datetime.get_today(), 1)
|
frappe.datetime.add_days(frappe.datetime.get_today(), 1)
|
||||||
),
|
),
|
||||||
|
max_date: evaluation_end_date
|
||||||
|
? new Date(evaluation_end_date)
|
||||||
|
: "",
|
||||||
change: () => {
|
change: () => {
|
||||||
get_slots();
|
if (this.eval_form.get_value("date")) get_slots();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -552,7 +559,7 @@ const get_slots = () => {
|
|||||||
args: {
|
args: {
|
||||||
course: this.eval_form.get_value("course"),
|
course: this.eval_form.get_value("course"),
|
||||||
date: this.eval_form.get_value("date"),
|
date: this.eval_form.get_value("date"),
|
||||||
batch_name: $(".class-details").data("batch"),
|
batch: $(".class-details").data("batch"),
|
||||||
},
|
},
|
||||||
callback: (r) => {
|
callback: (r) => {
|
||||||
if (r.message) {
|
if (r.message) {
|
||||||
@@ -653,7 +660,8 @@ const setup_calendar = (events) => {
|
|||||||
const options = get_calendar_options(element, calendar_id);
|
const options = get_calendar_options(element, calendar_id);
|
||||||
const calendar = new Calendar(container, options);
|
const calendar = new Calendar(container, options);
|
||||||
this.calendar_ = calendar;
|
this.calendar_ = calendar;
|
||||||
create_events(calendar, events);
|
|
||||||
|
create_events(calendar, events, calendar_id);
|
||||||
add_links_to_events(calendar, events);
|
add_links_to_events(calendar, events);
|
||||||
scroll_to_date(calendar, events);
|
scroll_to_date(calendar, events);
|
||||||
set_calendar_range(calendar, events);
|
set_calendar_range(calendar, events);
|
||||||
@@ -664,7 +672,7 @@ const get_calendar_options = (element, calendar_id) => {
|
|||||||
const end_time = element.data("end");
|
const end_time = element.data("end");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
defaultView: "week",
|
defaultView: $(window).width() < 768 || show_day_view ? "day" : "week",
|
||||||
usageStatistics: false,
|
usageStatistics: false,
|
||||||
week: {
|
week: {
|
||||||
narrowWeekend: true,
|
narrowWeekend: true,
|
||||||
@@ -684,13 +692,35 @@ const get_calendar_options = (element, calendar_id) => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
template: {
|
template: {
|
||||||
time: function (event) {
|
allday: function (event) {
|
||||||
return `<div class="calendar-event-time">
|
let hide = event.raw.completed ? "" : "hide";
|
||||||
<div> ${frappe.datetime.get_time(event.start.d.d)} -
|
return `<div class="calendar-event-time" title="${
|
||||||
${frappe.datetime.get_time(event.end.d.d)} </div>
|
event.title
|
||||||
|
} - ${frappe.datetime.get_time(
|
||||||
|
event.start.d.d
|
||||||
|
)} - ${frappe.datetime.get_time(event.end.d.d)}">
|
||||||
|
<img class='icon icon-sm pull-right ${hide}' src="/assets/lms/icons/check.svg">
|
||||||
<div class="calendar-event-title"> ${event.title} </div>
|
<div class="calendar-event-title"> ${event.title} </div>
|
||||||
</div>`;
|
</div>`;
|
||||||
},
|
},
|
||||||
|
time: function (event) {
|
||||||
|
let hide = event.raw.completed ? "" : "hide";
|
||||||
|
return `<div class="calendar-event-time" title="${
|
||||||
|
event.title
|
||||||
|
} - ${frappe.datetime.get_time(
|
||||||
|
event.start.d.d
|
||||||
|
)} - ${frappe.datetime.get_time(event.end.d.d)}">
|
||||||
|
<img class='icon icon-sm pull-right ${hide}' src="/assets/lms/icons/check.svg">
|
||||||
|
<div>
|
||||||
|
<span class="calendar-event-title"> ${event.title} </span>
|
||||||
|
<span>
|
||||||
|
${frappe.datetime.get_time(event.start.d.d)} - ${frappe.datetime.get_time(
|
||||||
|
event.end.d.d
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -703,9 +733,10 @@ const create_events = (calendar, events, calendar_id) => {
|
|||||||
id: `event${idx}`,
|
id: `event${idx}`,
|
||||||
calendarId: calendar_id,
|
calendarId: calendar_id,
|
||||||
title: event.title,
|
title: event.title,
|
||||||
start: `${event.date}T${event.start_time}`,
|
start: `${event.date}T${format_time(event.start_time)}`,
|
||||||
end: `${event.date}T${event.end_time}`,
|
end: `${event.date}T${format_time(event.end_time)}`,
|
||||||
isAllday: event.start_time ? false : true,
|
isAllday: event.start_time ? false : true,
|
||||||
|
category: event.start_time ? "time" : "allday",
|
||||||
borderColor: clr,
|
borderColor: clr,
|
||||||
backgroundColor: "var(--fg-color)",
|
backgroundColor: "var(--fg-color)",
|
||||||
customStyle: {
|
customStyle: {
|
||||||
@@ -716,6 +747,11 @@ const create_events = (calendar, events, calendar_id) => {
|
|||||||
},
|
},
|
||||||
raw: {
|
raw: {
|
||||||
url: event.url,
|
url: event.url,
|
||||||
|
milestone: event.milestone,
|
||||||
|
name: event.name,
|
||||||
|
idx: event.idx,
|
||||||
|
parent: event.parent,
|
||||||
|
completed: event.completed,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -723,28 +759,76 @@ const create_events = (calendar, events, calendar_id) => {
|
|||||||
calendar.createEvents(calendar_events);
|
calendar.createEvents(calendar_events);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const format_time = (time) => {
|
||||||
|
if (!time) return "00:00:00";
|
||||||
|
let time_arr = time.split(":");
|
||||||
|
if (time_arr[0] < 10) time_arr[0] = "0" + time_arr[0];
|
||||||
|
return time_arr.join(":");
|
||||||
|
};
|
||||||
|
|
||||||
const add_links_to_events = (calendar) => {
|
const add_links_to_events = (calendar) => {
|
||||||
calendar.on("clickEvent", ({ event }) => {
|
calendar.on("clickEvent", ({ event }) => {
|
||||||
let event_date = event.start.d.d;
|
let event_date = event.start.d.d;
|
||||||
event_date = moment(event_date).format("YYYY-MM-DD");
|
event_date = moment(event_date).format("YYYY-MM-DD");
|
||||||
|
|
||||||
let current_date = moment().format("YYYY-MM-DD");
|
let current_date = moment().format("YYYY-MM-DD");
|
||||||
if (allow_future || moment(event_date).isSameOrBefore(current_date)) {
|
|
||||||
window.open(event.raw.url, "_blank");
|
if (
|
||||||
}
|
is_student &&
|
||||||
|
!moment(event_date).isSameOrBefore(current_date) &&
|
||||||
|
!allow_future
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (is_student && event.raw.milestone) {
|
||||||
|
frappe.call({
|
||||||
|
method: "lms.lms.doctype.lms_batch.lms_batch.is_milestone_complete",
|
||||||
|
args: {
|
||||||
|
idx: event.raw.idx,
|
||||||
|
batch: event.raw.parent,
|
||||||
|
},
|
||||||
|
callback: (data) => {
|
||||||
|
if (data.message) window.open(event.raw.url, "_blank");
|
||||||
|
else
|
||||||
|
frappe.show_alert({
|
||||||
|
message:
|
||||||
|
"Please complete all previous activities to proceed.",
|
||||||
|
indicator: "red",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else window.open(event.raw.url, "_blank");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const scroll_to_date = (calendar, events) => {
|
const scroll_to_date = (calendar, events) => {
|
||||||
if (
|
if (
|
||||||
new Date() < new Date(events[0].date) ||
|
new Date() < new Date(events[0].date) ||
|
||||||
new Date() > new Date(events.slice(-1).date)
|
new Date() > new Date(events.slice(-1)[0].date)
|
||||||
) {
|
) {
|
||||||
calendar.setDate(new Date(events[0].date));
|
calendar.setDate(new Date(events[0].date));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const set_calendar_range = (calendar, events) => {
|
const set_calendar_range = (calendar, events) => {
|
||||||
|
let day_view = $(window).width() < 768 || show_day_view ? true : false;
|
||||||
|
if (day_view) {
|
||||||
|
let calendar_date = moment(calendar.getDate().d.d).format(
|
||||||
|
"DD MMMM YYYY"
|
||||||
|
);
|
||||||
|
$(".calendar-range").text(`${calendar_date}`);
|
||||||
|
|
||||||
|
if (moment(calendar_date).isSameOrBefore(moment(events[0].date)))
|
||||||
|
$("#prev-week").hide();
|
||||||
|
else $("#prev-week").show();
|
||||||
|
|
||||||
|
if (
|
||||||
|
moment(calendar_date).isSameOrAfter(
|
||||||
|
moment(events.slice(-1)[0].date)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
$("#next-week").hide();
|
||||||
|
else $("#next-week").show();
|
||||||
|
} else {
|
||||||
let week_start = moment(calendar.getDateRangeStart().d.d);
|
let week_start = moment(calendar.getDateRangeStart().d.d);
|
||||||
let week_end = moment(calendar.getDateRangeEnd().d.d);
|
let week_end = moment(calendar.getDateRangeEnd().d.d);
|
||||||
|
|
||||||
@@ -754,16 +838,13 @@ const set_calendar_range = (calendar, events) => {
|
|||||||
).format("DD MMMM YYYY")}`
|
).format("DD MMMM YYYY")}`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (week_start.diff(moment(events[0].date), "days") <= 0) {
|
if (week_start.diff(moment(events[0].date), "days") <= 0)
|
||||||
$("#prev-week").hide();
|
$("#prev-week").hide();
|
||||||
} else {
|
else $("#prev-week").show();
|
||||||
$("#prev-week").show();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (week_end.diff(moment(events.slice(-1)[0].date), "days") > 0) {
|
if (week_end.diff(moment(events.slice(-1)[0].date), "days") > 0)
|
||||||
$("#next-week").hide();
|
$("#next-week").hide();
|
||||||
} else {
|
else $("#next-week").show();
|
||||||
$("#next-week").show();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -784,6 +865,12 @@ const email_to_students = () => {
|
|||||||
label: __("Subject"),
|
label: __("Subject"),
|
||||||
reqd: 1,
|
reqd: 1,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
fieldtype: "Data",
|
||||||
|
fieldname: "reply_to",
|
||||||
|
label: __("Reply To"),
|
||||||
|
reqd: 0,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
fieldtype: "Text Editor",
|
fieldtype: "Text Editor",
|
||||||
fieldname: "message",
|
fieldname: "message",
|
||||||
@@ -802,11 +889,33 @@ const email_to_students = () => {
|
|||||||
|
|
||||||
const send_email = (values) => {
|
const send_email = (values) => {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "lms.lms.doctype.lms_batch.lms_batch.send_email_to_students",
|
method: "frappe.client.get_list",
|
||||||
args: {
|
args: {
|
||||||
batch: $(".class-details").data("batch"),
|
doctype: "Batch Student",
|
||||||
|
parent: "LMS Batch",
|
||||||
|
fields: ["student"],
|
||||||
|
filters: {
|
||||||
|
parent: $(".class-details").data("batch"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
callback: (data) => {
|
||||||
|
send_email_to_students(data.message, values);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const send_email_to_students = (students, values) => {
|
||||||
|
students = students.map((row) => row.student);
|
||||||
|
frappe.call({
|
||||||
|
method: "frappe.core.doctype.communication.email.make",
|
||||||
|
args: {
|
||||||
|
recipients: students.join(", "),
|
||||||
|
cc: values.reply_to,
|
||||||
subject: values.subject,
|
subject: values.subject,
|
||||||
message: values.message,
|
content: values.message,
|
||||||
|
doctype: "LMS Batch",
|
||||||
|
name: $(".class-details").data("batch"),
|
||||||
|
send_email: 1,
|
||||||
},
|
},
|
||||||
callback: (r) => {
|
callback: (r) => {
|
||||||
this.email_dialog.hide();
|
this.email_dialog.hide();
|
||||||
@@ -814,6 +923,9 @@ const send_email = (values) => {
|
|||||||
message: __("Email sent successfully"),
|
message: __("Email sent successfully"),
|
||||||
indicator: "green",
|
indicator: "green",
|
||||||
});
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 2000);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ def get_context(context):
|
|||||||
"batch_details",
|
"batch_details",
|
||||||
"published",
|
"published",
|
||||||
"allow_future",
|
"allow_future",
|
||||||
|
"evaluation_end_date",
|
||||||
|
"meta_image",
|
||||||
],
|
],
|
||||||
as_dict=True,
|
as_dict=True,
|
||||||
)
|
)
|
||||||
@@ -71,6 +73,13 @@ def get_context(context):
|
|||||||
)
|
)
|
||||||
context.course_name_list = [course.course for course in context.batch_courses]
|
context.course_name_list = [course.course for course in context.batch_courses]
|
||||||
context.assessments = get_assessments(batch_name)
|
context.assessments = get_assessments(batch_name)
|
||||||
|
context.batch_emails = frappe.get_all(
|
||||||
|
"Communication",
|
||||||
|
filters={"reference_doctype": "LMS Batch", "reference_name": batch_name},
|
||||||
|
fields=["subject", "content", "recipients", "cc", "communication_date", "sender"],
|
||||||
|
order_by="communication_date desc",
|
||||||
|
)
|
||||||
|
|
||||||
context.batch_students = get_class_student_details(
|
context.batch_students = get_class_student_details(
|
||||||
batch_students, batch_courses, context.assessments
|
batch_students, batch_courses, context.assessments
|
||||||
)
|
)
|
||||||
@@ -98,9 +107,9 @@ def get_context(context):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
context.legends = get_legends(batch_name)
|
context.legends = get_legends(batch_name)
|
||||||
|
context.settings = frappe.get_single("LMS Settings")
|
||||||
|
|
||||||
custom_tabs = frappe.get_hooks("lms_batch_tabs")
|
custom_tabs = frappe.get_hooks("lms_batch_tabs")
|
||||||
|
|
||||||
if custom_tabs:
|
if custom_tabs:
|
||||||
context.custom_tabs_header = custom_tabs.get("header_html")[0]
|
context.custom_tabs_header = custom_tabs.get("header_html")[0]
|
||||||
context.custom_tabs_content = custom_tabs.get("content_html")[0]
|
context.custom_tabs_content = custom_tabs.get("content_html")[0]
|
||||||
@@ -148,7 +157,6 @@ def get_class_course_details(batch_courses):
|
|||||||
"image",
|
"image",
|
||||||
"upcoming",
|
"upcoming",
|
||||||
"short_introduction",
|
"short_introduction",
|
||||||
"image",
|
|
||||||
"paid_course",
|
"paid_course",
|
||||||
"course_price",
|
"course_price",
|
||||||
"enable_certification",
|
"enable_certification",
|
||||||
|
|||||||
@@ -1,67 +1,48 @@
|
|||||||
{% extends "lms/templates/lms_base.html" %}
|
{% extends "lms/templates/lms_base.html" %} {% block title %} {{
|
||||||
{% block title %}
|
_(batch_info.title) }} {% endblock %} {% block page_content %}
|
||||||
{{ _(batch_info.title) }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
|
|
||||||
{% block page_content %}
|
|
||||||
<div class="common-page-style lms-page-style">
|
<div class="common-page-style lms-page-style">
|
||||||
{{ BatchHeader(batch_info) }}
|
{{ BatchHeader(batch_info) }}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
{{ BatchOverlay(batch_info, courses, students) }}
|
{{ BatchOverlay(batch_info, courses, students) }}
|
||||||
<div class="pt-10">
|
<div class="pt-10">
|
||||||
{{ BatchDetails(batch_info) }}
|
{{ BatchDetails(batch_info) }} {{ CourseList(courses) }}
|
||||||
{{ CourseList(courses) }}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ BatchDetailsRaw() }}
|
{{ BatchDetailsRaw() }}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %} {% macro BatchHeader(batch_info) %}
|
||||||
|
|
||||||
{% macro BatchHeader(batch_info) %}
|
|
||||||
<div class="course-head-container">
|
<div class="course-head-container">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="course-card-wide">
|
<div class="course-card-wide">
|
||||||
{{ BreadCrumb(batch_info) }}
|
{{ BreadCrumb(batch_info) }} {{ BatchHeaderDetails(batch_info,
|
||||||
{{ BatchHeaderDetails(batch_info, courses, students) }}
|
courses, students) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %} {% macro BreadCrumb(batch_info) %}
|
||||||
|
|
||||||
{% macro BreadCrumb(batch_info) %}
|
|
||||||
<article class="mb-8">
|
<article class="mb-8">
|
||||||
<a class="dark-links" href="/batches">
|
<a class="dark-links" href="/batches"> {{ _("All Batches") }} </a>
|
||||||
{{ _("All Batches") }}
|
<img class="" src="/assets/lms/icons/chevron-right.svg" />
|
||||||
</a>
|
<span class="breadcrumb-destination"> {{ _("Batch Details") }} </span>
|
||||||
<img class="" src="/assets/lms/icons/chevron-right.svg">
|
|
||||||
<span class="breadcrumb-destination">
|
|
||||||
{{ _("Batch Details") }}
|
|
||||||
</span>
|
|
||||||
</article>
|
</article>
|
||||||
{% endmacro %}
|
{% endmacro %} {% macro BatchHeaderDetails(batch_info, courses, students) %}
|
||||||
|
|
||||||
{% macro BatchHeaderDetails(batch_info, courses, students) %}
|
|
||||||
<div class="class-details" data-batch="{{ batch_info.name }}">
|
<div class="class-details" data-batch="{{ batch_info.name }}">
|
||||||
|
<div class="page-title">{{ batch_info.title }}</div>
|
||||||
|
|
||||||
<div class="page-title">
|
<div class="">{{ batch_info.description }}</div>
|
||||||
{{ batch_info.title }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="">
|
|
||||||
{{ batch_info.description }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-8">
|
<div class="mt-8">
|
||||||
<svg class="icon icon-sm">
|
<svg class="icon icon-sm">
|
||||||
<use href="#icon-calendar"></use>
|
<use href="#icon-calendar"></use>
|
||||||
</svg>
|
</svg>
|
||||||
<span>
|
<span>
|
||||||
{{ frappe.utils.format_date(batch_info.start_date, "long") }} -
|
{{ frappe.utils.format_date(batch_info.start_date, "long") }}
|
||||||
</span>
|
</span>
|
||||||
|
{% if batch_info.start_date != batch_info.end_date %}
|
||||||
<span>
|
<span>
|
||||||
{{ frappe.utils.format_date(batch_info.end_date, "long") }}
|
- {{ frappe.utils.format_date(batch_info.end_date, "long") }}
|
||||||
</span>
|
</span>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if batch_info.start_time and batch_info.end_time %}
|
{% if batch_info.start_time and batch_info.end_time %}
|
||||||
@@ -78,15 +59,10 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %} {% macro BatchOverlay(batch_info, courses, students) %}
|
||||||
|
|
||||||
{% macro BatchOverlay(batch_info, courses, students) %}
|
|
||||||
<div class="course-overlay-card class-overlay">
|
<div class="course-overlay-card class-overlay">
|
||||||
|
|
||||||
<div class="course-overlay-content">
|
<div class="course-overlay-content">
|
||||||
|
{% if batch_info.seat_count %} {% if seats_left %}
|
||||||
{% if batch_info.seat_count %}
|
|
||||||
{% if seats_left %}
|
|
||||||
<div class="indicator-pill green pull-right">
|
<div class="indicator-pill green pull-right">
|
||||||
{{ _("Seats Available") }}: {{ seats_left }}
|
{{ _("Seats Available") }}: {{ seats_left }}
|
||||||
</div>
|
</div>
|
||||||
@@ -94,12 +70,10 @@
|
|||||||
<div class="indicator-pill red pull-right">
|
<div class="indicator-pill red pull-right">
|
||||||
{{ _("No seats left") }}
|
{{ _("No seats left") }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %} {% endif %} {% if batch_info.paid_batch %}
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if batch_info.paid_batch %}
|
|
||||||
<div class="bold-heading">
|
<div class="bold-heading">
|
||||||
{{ frappe.utils.fmt_money(batch_info.amount, 0, batch_info.currency) }}
|
{{ frappe.utils.fmt_money(batch_info.amount, 0, batch_info.currency)
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@@ -115,11 +89,13 @@
|
|||||||
<use href="#icon-calendar"></use>
|
<use href="#icon-calendar"></use>
|
||||||
</svg>
|
</svg>
|
||||||
<span>
|
<span>
|
||||||
{{ frappe.utils.format_date(batch_info.start_date, "long") }} -
|
{{ frappe.utils.format_date(batch_info.start_date, "long") }}
|
||||||
</span>
|
</span>
|
||||||
|
{% if batch_info.start_date != batch_info.end_date %}
|
||||||
<span>
|
<span>
|
||||||
{{ frappe.utils.format_date(batch_info.end_date, "long") }}
|
- {{ frappe.utils.format_date(batch_info.end_date, "long") }}
|
||||||
</span>
|
</span>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if batch_info.start_time and batch_info.end_time %}
|
{% if batch_info.start_time and batch_info.end_time %}
|
||||||
@@ -128,7 +104,8 @@
|
|||||||
<use href="#icon-clock"></use>
|
<use href="#icon-clock"></use>
|
||||||
</svg>
|
</svg>
|
||||||
<span>
|
<span>
|
||||||
{{ frappe.utils.format_time(batch_info.start_time, "hh:mm a") }} -
|
{{ frappe.utils.format_time(batch_info.start_time, "hh:mm a") }}
|
||||||
|
-
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
{{ frappe.utils.format_time(batch_info.end_time, "hh:mm a") }}
|
{{ frappe.utils.format_time(batch_info.end_time, "hh:mm a") }}
|
||||||
@@ -138,14 +115,25 @@
|
|||||||
|
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
{% if is_moderator or is_evaluator %}
|
{% if is_moderator or is_evaluator %}
|
||||||
<a class="btn btn-primary wide-button" href="/batches/{{ batch_info.name }}">
|
<a
|
||||||
|
class="btn btn-primary wide-button"
|
||||||
|
href="/batches/{{ batch_info.name }}"
|
||||||
|
>
|
||||||
{{ _("Manage Batch") }}
|
{{ _("Manage Batch") }}
|
||||||
</a>
|
</a>
|
||||||
{% elif batch_info.paid_batch %}
|
{% elif batch_info.paid_batch and batch_info.start_date >
|
||||||
<a class="btn btn-primary wide-button {% if batch_info.seat_count and not seats_left %} hide {% endif %}"
|
frappe.utils.getdate() %}
|
||||||
href="/billing/batch/{{ batch_info.name }}">
|
<a
|
||||||
|
class="btn btn-primary wide-button {% if batch_info.seat_count and not seats_left %} hide {% endif %}"
|
||||||
|
href="/billing/batch/{{ batch_info.name }}"
|
||||||
|
>
|
||||||
{{ _("Register Now") }}
|
{{ _("Register Now") }}
|
||||||
</a>
|
</a>
|
||||||
|
{% elif batch_info.allow_self_enrollment and batch_info.seat_count
|
||||||
|
and seats_left and batch_info.start_date > frappe.utils.getdate() %}
|
||||||
|
<button class="btn btn-primary wide-button enroll-batch">
|
||||||
|
{{ _("Enroll Now") }}
|
||||||
|
</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
{{ _("To join this batch, please contact the Administrator.") }}
|
{{ _("To join this batch, please contact the Administrator.") }}
|
||||||
@@ -161,24 +149,13 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %} {% macro BatchDetails(batch_info) %}
|
||||||
|
<div class="batch-details">{{ batch_info.batch_details }}</div>
|
||||||
|
{% endmacro %} {% macro CourseList(courses) %} {% if courses | length or
|
||||||
{% macro BatchDetails(batch_info) %}
|
is_moderator %}
|
||||||
<div class="batch-details">
|
|
||||||
{{ batch_info.batch_details }}
|
|
||||||
</div>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
|
|
||||||
{% macro CourseList(courses) %}
|
|
||||||
{% if courses | length or is_moderator %}
|
|
||||||
<div class="batch-course-list">
|
<div class="batch-course-list">
|
||||||
|
<div class="align-center flex">
|
||||||
<div class="flex align-center">
|
<div class="page-title">{{ _("Courses") }}</div>
|
||||||
<div class="page-title">
|
|
||||||
{{ _("Courses") }}
|
|
||||||
</div>
|
|
||||||
{% if is_moderator %}
|
{% if is_moderator %}
|
||||||
<button class="btn btn-default btn-sm btn-add-course ml-4">
|
<button class="btn btn-default btn-sm btn-add-course ml-4">
|
||||||
{{ _("Add Course") }}
|
{{ _("Add Course") }}
|
||||||
@@ -192,47 +169,46 @@
|
|||||||
<div class="h-100">
|
<div class="h-100">
|
||||||
{% if is_moderator %}
|
{% if is_moderator %}
|
||||||
<div class="card-buttons">
|
<div class="card-buttons">
|
||||||
<button class="btn icon-btn btn-default btn-edit-course"
|
<button
|
||||||
data-name="{{ course.batch_course }}" data-course="{{ course.name }}"
|
class="btn icon-btn btn-default btn-edit-course"
|
||||||
{% if course.evaluator %} data-evaluator="{{ course.evaluator }}" {% endif %}>
|
data-name="{{ course.batch_course }}"
|
||||||
|
data-course="{{ course.name }}"
|
||||||
|
{%
|
||||||
|
if
|
||||||
|
course.evaluator
|
||||||
|
%}
|
||||||
|
data-evaluator="{{ course.evaluator }}"
|
||||||
|
{%
|
||||||
|
endif
|
||||||
|
%}
|
||||||
|
>
|
||||||
<svg class="icon icon-sm">
|
<svg class="icon icon-sm">
|
||||||
<use href="#icon-edit"></use>
|
<use href="#icon-edit"></use>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn icon-btn btn-default btn-remove-course ml-2" data-course="{{ course.name }}">
|
<button
|
||||||
|
class="btn icon-btn btn-default btn-remove-course ml-2"
|
||||||
|
data-course="{{ course.name }}"
|
||||||
|
>
|
||||||
<svg class="icon icon-sm">
|
<svg class="icon icon-sm">
|
||||||
<use href="#icon-delete"></use>
|
<use href="#icon-delete"></use>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %} {{ widgets.CourseCard(course=course, read_only=False) }}
|
||||||
{{ widgets.CourseCard(course=course, read_only=False) }}
|
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="">
|
<div class="">{{ _("No courses") }}</div>
|
||||||
{{ _("No courses") }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %} {% endmacro %} {% macro BatchDetailsRaw() %} {% if
|
||||||
{% endmacro %}
|
batch_info.batch_details_raw %}
|
||||||
|
<div class="mt-10 pt-10">{{ batch_info.batch_details_raw }}</div>
|
||||||
|
{% endif %} {% endmacro %} {%- block script %} {{ super() }} {% if is_moderator
|
||||||
{% macro BatchDetailsRaw() %}
|
%}
|
||||||
{% if batch_info.batch_details_raw %}
|
|
||||||
<div class="mt-10 pt-10">
|
|
||||||
{{ batch_info.batch_details_raw }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{%- block script %}
|
|
||||||
{{ super() }}
|
|
||||||
{% if is_moderator %}
|
|
||||||
<script>
|
<script>
|
||||||
let batch_info = {{ batch_info | json }};
|
let batch_info = {{ batch_info | json }};
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %} {% endblock %}
|
||||||
{% endblock %}
|
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ frappe.ready(() => {
|
|||||||
$(".btn-remove-course").click((e) => {
|
$(".btn-remove-course").click((e) => {
|
||||||
remove_course(e);
|
remove_course(e);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(".enroll-batch").click((e) => {
|
||||||
|
enroll_batch(e);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const show_course_modal = (e) => {
|
const show_course_modal = (e) => {
|
||||||
@@ -54,6 +58,30 @@ const show_course_modal = (e) => {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const enroll_batch = (e) => {
|
||||||
|
let batch_name = $(".class-details").data("batch");
|
||||||
|
if (frappe.session.user == "Guest") {
|
||||||
|
window.location.href =
|
||||||
|
"/login?redirect-to=/batches/details/" + batch_name;
|
||||||
|
}
|
||||||
|
frappe.call({
|
||||||
|
method: "lms.lms.doctype.batch_student.batch_student.enroll_batch",
|
||||||
|
args: {
|
||||||
|
batch_name: batch_name,
|
||||||
|
},
|
||||||
|
callback(r) {
|
||||||
|
frappe.show_alert(
|
||||||
|
{
|
||||||
|
message: __("Successfully Enrolled"),
|
||||||
|
indicator: "green",
|
||||||
|
},
|
||||||
|
2000
|
||||||
|
);
|
||||||
|
window.location.href = `/batches/${batch_name}`;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const add_course = (values, course_name) => {
|
const add_course = (values, course_name) => {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "lms.lms.doctype.lms_batch.lms_batch.add_course",
|
method: "lms.lms.doctype.lms_batch.lms_batch.add_course",
|
||||||
|
|||||||
@@ -33,13 +33,19 @@ def get_context(context):
|
|||||||
"published",
|
"published",
|
||||||
"meta_image",
|
"meta_image",
|
||||||
"batch_details_raw",
|
"batch_details_raw",
|
||||||
|
"evaluation_end_date",
|
||||||
|
"amount_usd",
|
||||||
|
"allow_self_enrollment",
|
||||||
],
|
],
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
if context.batch_info.amount and context.batch_info.currency:
|
if context.batch_info.amount and context.batch_info.currency:
|
||||||
amount, currency = check_multicurrency(
|
amount, currency = check_multicurrency(
|
||||||
context.batch_info.amount, context.batch_info.currency
|
context.batch_info.amount,
|
||||||
|
context.batch_info.currency,
|
||||||
|
None,
|
||||||
|
context.batch_info.amount_usd,
|
||||||
)
|
)
|
||||||
context.batch_info.amount = amount
|
context.batch_info.amount = amount
|
||||||
context.batch_info.currency = currency
|
context.batch_info.currency = currency
|
||||||
|
|||||||
@@ -140,10 +140,24 @@
|
|||||||
<use href="#icon-calendar"></use>
|
<use href="#icon-calendar"></use>
|
||||||
</svg>
|
</svg>
|
||||||
<span>
|
<span>
|
||||||
{{ frappe.utils.format_date(batch.start_date, "medium") }} -
|
{{ frappe.utils.format_date(batch.start_date, "medium") }}
|
||||||
|
</span>
|
||||||
|
{% if batch.start_date != batch.end_date %}
|
||||||
|
<span>
|
||||||
|
- {{ frappe.utils.format_date(batch.end_date, "long") }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-2">
|
||||||
|
<svg class="icon icon-sm">
|
||||||
|
<use href="#icon-clock"></use>
|
||||||
|
</svg>
|
||||||
|
<span>
|
||||||
|
{{ frappe.utils.format_time(batch.start_time, "HH:mm a") }} -
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
{{ frappe.utils.format_date(batch.end_date, "medium") }}
|
{{ frappe.utils.format_time(batch.end_time, "HH:mm a") }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import getdate
|
from frappe.utils import getdate, get_time_str, nowtime
|
||||||
from lms.lms.utils import (
|
from lms.lms.utils import (
|
||||||
has_course_moderator_role,
|
has_course_moderator_role,
|
||||||
has_course_evaluator_role,
|
has_course_evaluator_role,
|
||||||
@@ -19,11 +19,14 @@ def get_context(context):
|
|||||||
"description",
|
"description",
|
||||||
"start_date",
|
"start_date",
|
||||||
"end_date",
|
"end_date",
|
||||||
|
"start_time",
|
||||||
|
"end_time",
|
||||||
"paid_batch",
|
"paid_batch",
|
||||||
"amount",
|
"amount",
|
||||||
"currency",
|
"currency",
|
||||||
"seat_count",
|
"seat_count",
|
||||||
"published",
|
"published",
|
||||||
|
"amount_usd",
|
||||||
],
|
],
|
||||||
order_by="start_date",
|
order_by="start_date",
|
||||||
)
|
)
|
||||||
@@ -34,7 +37,9 @@ def get_context(context):
|
|||||||
batch.course_count = frappe.db.count("Batch Course", {"parent": batch.name})
|
batch.course_count = frappe.db.count("Batch Course", {"parent": batch.name})
|
||||||
|
|
||||||
if batch.amount and batch.currency:
|
if batch.amount and batch.currency:
|
||||||
amount, currency = check_multicurrency(batch.amount, batch.currency)
|
amount, currency = check_multicurrency(
|
||||||
|
batch.amount, batch.currency, None, batch.amount_usd
|
||||||
|
)
|
||||||
batch.amount = amount
|
batch.amount = amount
|
||||||
batch.currency = currency
|
batch.currency = currency
|
||||||
|
|
||||||
@@ -43,12 +48,16 @@ def get_context(context):
|
|||||||
)
|
)
|
||||||
if not batch.published:
|
if not batch.published:
|
||||||
private_batches.append(batch)
|
private_batches.append(batch)
|
||||||
elif getdate(batch.start_date) <= getdate():
|
elif getdate(batch.start_date) < getdate():
|
||||||
|
past_batches.append(batch)
|
||||||
|
elif (
|
||||||
|
getdate(batch.start_date) == getdate() and get_time_str(batch.start_time) < nowtime()
|
||||||
|
):
|
||||||
past_batches.append(batch)
|
past_batches.append(batch)
|
||||||
else:
|
else:
|
||||||
upcoming_batches.append(batch)
|
upcoming_batches.append(batch)
|
||||||
|
|
||||||
context.past_batches = sorted(past_batches, key=lambda d: d.start_date)
|
context.past_batches = sorted(past_batches, key=lambda d: d.start_date, reverse=True)
|
||||||
context.upcoming_batches = sorted(upcoming_batches, key=lambda d: d.start_date)
|
context.upcoming_batches = sorted(upcoming_batches, key=lambda d: d.start_date)
|
||||||
context.private_batches = sorted(private_batches, key=lambda d: d.start_date)
|
context.private_batches = sorted(private_batches, key=lambda d: d.start_date)
|
||||||
|
|
||||||
@@ -83,5 +92,6 @@ def get_context(context):
|
|||||||
batchinfo.seats_left = batchinfo.seat_count - batchinfo.student_count
|
batchinfo.seats_left = batchinfo.seat_count - batchinfo.student_count
|
||||||
|
|
||||||
my_batches_info.append(batchinfo)
|
my_batches_info.append(batchinfo)
|
||||||
|
my_batches_info = sorted(my_batches_info, key=lambda d: d.start_date, reverse=True)
|
||||||
|
|
||||||
context.my_batches = my_batches_info
|
context.my_batches = my_batches_info
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="field-label">
|
<div class="field-label">
|
||||||
{{ _("Total Price: ") }}
|
{{ _("Total Price: ") }}
|
||||||
<span class="total-price">{{ frappe.utils.fmt_money(amount, 2, currency) }}</span>
|
<span class="total-price">{{ frappe.utils.fmt_money(amount_with_gst, 2, currency) if gst_applied else frappe.utils.fmt_money(amount, 2, currency) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if gst_applied %}
|
{% if gst_applied %}
|
||||||
|
|||||||
@@ -40,15 +40,15 @@ const setup_billing = () => {
|
|||||||
reqd: 1,
|
reqd: 1,
|
||||||
default: address && address.city,
|
default: address && address.city,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
fieldtype: "Column Break",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
fieldtype: "Data",
|
fieldtype: "Data",
|
||||||
label: __("State/Province"),
|
label: __("State/Province"),
|
||||||
fieldname: "state",
|
fieldname: "state",
|
||||||
default: address && address.state,
|
default: address && address.state,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
fieldtype: "Column Break",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
fieldtype: "Link",
|
fieldtype: "Link",
|
||||||
label: __("Country"),
|
label: __("Country"),
|
||||||
@@ -75,6 +75,14 @@ const setup_billing = () => {
|
|||||||
reqd: 1,
|
reqd: 1,
|
||||||
default: address && address.phone,
|
default: address && address.phone,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
fieldtype: "Link",
|
||||||
|
label: __("Where did you hear about this?"),
|
||||||
|
fieldname: "source",
|
||||||
|
options: "LMS Source",
|
||||||
|
only_select: 1,
|
||||||
|
reqd: 1,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
fieldtype: "Section Break",
|
fieldtype: "Section Break",
|
||||||
label: __("GST Details"),
|
label: __("GST Details"),
|
||||||
@@ -106,6 +114,7 @@ const setup_billing = () => {
|
|||||||
|
|
||||||
const generate_payment_link = (e) => {
|
const generate_payment_link = (e) => {
|
||||||
let new_address = this.billing.get_values();
|
let new_address = this.billing.get_values();
|
||||||
|
validate_address(new_address);
|
||||||
let doctype = $(e.currentTarget).attr("data-doctype");
|
let doctype = $(e.currentTarget).attr("data-doctype");
|
||||||
let docname = decodeURIComponent($(e.currentTarget).attr("data-name"));
|
let docname = decodeURIComponent($(e.currentTarget).attr("data-name"));
|
||||||
|
|
||||||
@@ -174,8 +183,10 @@ const change_currency = () => {
|
|||||||
if (current_price != data.message) {
|
if (current_price != data.message) {
|
||||||
update_price(data.message);
|
update_price(data.message);
|
||||||
}
|
}
|
||||||
if (!data.message.includes("INR")) {
|
if (data.message.includes("INR")) {
|
||||||
$("#gst-message").addClass("hide");
|
$("#gst-message").removeClass("hide").addClass("show");
|
||||||
|
} else {
|
||||||
|
$("#gst-message").removeClass("show").addClass("hide");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -188,3 +199,48 @@ const update_price = (price) => {
|
|||||||
indicator: "yellow",
|
indicator: "yellow",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const validate_address = (billing_address) => {
|
||||||
|
if (billing_address.country == "India" && !billing_address.state)
|
||||||
|
frappe.throw(__("State is mandatory."));
|
||||||
|
|
||||||
|
const states = [
|
||||||
|
"Andhra Pradesh",
|
||||||
|
"Arunachal Pradesh",
|
||||||
|
"Assam",
|
||||||
|
"Bihar",
|
||||||
|
"Chhattisgarh",
|
||||||
|
"Goa",
|
||||||
|
"Gujarat",
|
||||||
|
"Haryana",
|
||||||
|
"Himachal Pradesh",
|
||||||
|
"Jharkhand",
|
||||||
|
"Karnataka",
|
||||||
|
"Kerala",
|
||||||
|
"Madhya Pradesh",
|
||||||
|
"Maharashtra",
|
||||||
|
"Manipur",
|
||||||
|
"Meghalaya",
|
||||||
|
"Mizoram",
|
||||||
|
"Nagaland",
|
||||||
|
"Odisha",
|
||||||
|
"Punjab",
|
||||||
|
"Rajasthan",
|
||||||
|
"Sikkim",
|
||||||
|
"Tamil Nadu",
|
||||||
|
"Telangana",
|
||||||
|
"Tripura",
|
||||||
|
"Uttar Pradesh",
|
||||||
|
"Uttarakhand",
|
||||||
|
"West Bengal",
|
||||||
|
];
|
||||||
|
if (
|
||||||
|
billing_address.country == "India" &&
|
||||||
|
!states.includes(billing_address.state)
|
||||||
|
)
|
||||||
|
frappe.throw(
|
||||||
|
__(
|
||||||
|
"Please enter a valid state with correct spelling and the first letter capitalized."
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -15,20 +15,22 @@ def get_context(context):
|
|||||||
validate_access(doctype, docname, module)
|
validate_access(doctype, docname, module)
|
||||||
get_billing_details(context)
|
get_billing_details(context)
|
||||||
|
|
||||||
|
context.original_currency = context.currency
|
||||||
|
context.original_amount = (
|
||||||
|
(context.amount * 1.18) if context.original_currency == "INR" else context.amount
|
||||||
|
)
|
||||||
|
|
||||||
context.exception_country = frappe.get_all(
|
context.exception_country = frappe.get_all(
|
||||||
"Payment Country", filters={"parent": "LMS Settings"}, pluck="country"
|
"Payment Country", filters={"parent": "LMS Settings"}, pluck="country"
|
||||||
)
|
)
|
||||||
|
|
||||||
context.amount, context.currency = check_multicurrency(
|
context.amount, context.currency = check_multicurrency(
|
||||||
context.amount, context.currency
|
context.amount, context.currency, None, context.amount_usd
|
||||||
)
|
)
|
||||||
|
|
||||||
context.address = get_address()
|
context.address = get_address()
|
||||||
if context.currency == "INR":
|
if context.currency == "INR":
|
||||||
context.amount, context.gst_applied = apply_gst(context.amount, None)
|
context.amount_with_gst, context.gst_applied = apply_gst(context.amount, None)
|
||||||
|
|
||||||
context.original_amount = context.amount
|
|
||||||
context.original_currency = context.currency
|
|
||||||
|
|
||||||
|
|
||||||
def validate_access(doctype, docname, module):
|
def validate_access(doctype, docname, module):
|
||||||
@@ -61,7 +63,7 @@ def get_billing_details(context):
|
|||||||
details = frappe.db.get_value(
|
details = frappe.db.get_value(
|
||||||
"LMS Course",
|
"LMS Course",
|
||||||
context.docname,
|
context.docname,
|
||||||
["title", "name", "paid_course", "course_price as amount", "currency"],
|
["title", "name", "paid_course", "course_price as amount", "currency", "amount_usd"],
|
||||||
as_dict=True,
|
as_dict=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -72,7 +74,7 @@ def get_billing_details(context):
|
|||||||
details = frappe.db.get_value(
|
details = frappe.db.get_value(
|
||||||
"LMS Batch",
|
"LMS Batch",
|
||||||
context.docname,
|
context.docname,
|
||||||
["title", "name", "paid_batch", "amount", "currency"],
|
["title", "name", "paid_batch", "amount", "currency", "amount_usd"],
|
||||||
as_dict=True,
|
as_dict=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -84,6 +86,7 @@ def get_billing_details(context):
|
|||||||
context.title = details.title
|
context.title = details.title
|
||||||
context.amount = details.amount
|
context.amount = details.amount
|
||||||
context.currency = details.currency
|
context.currency = details.currency
|
||||||
|
context.amount_usd = details.amount_usd
|
||||||
|
|
||||||
|
|
||||||
def get_address():
|
def get_address():
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user