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">
|
||||
<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>
|
||||
<p align="center">Easy to use, open source, learning management system.</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,
|
||||
},
|
||||
e2e: {
|
||||
baseUrl: "http://dd1:8000",
|
||||
baseUrl: "http://pyp:8000",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
$ git clone https://github.com/frappe/lms.git
|
||||
|
||||
$ cd lms
|
||||
|
||||
$ cd docker
|
||||
```
|
||||
|
||||
**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",
|
||||
"length": 0,
|
||||
"mandatory_depends_on": null,
|
||||
"modified": "2022-04-19 13:02:18.219508",
|
||||
"modified": "2022-04-19 13:02:18.219510",
|
||||
"module": "LMS",
|
||||
"name": "User-user_category",
|
||||
"no_copy": 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,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
|
||||
12
lms/hooks.py
12
lms/hooks.py
@@ -97,8 +97,7 @@ override_doctype_class = {
|
||||
# Hook on document methods and events
|
||||
|
||||
doc_events = {
|
||||
"Discussion Reply": {"after_insert": "lms.lms.utils.create_notification_log"},
|
||||
"Course Lesson": {"on_update": "lms.lms.doctype.lms_quiz.lms_quiz.update_lesson_info"},
|
||||
"Discussion Reply": {"after_insert": "lms.lms.utils.handle_notifications"},
|
||||
}
|
||||
|
||||
# Scheduled Tasks
|
||||
@@ -119,9 +118,9 @@ fixtures = ["Custom Field", "Function", "Industry"]
|
||||
# Overriding Methods
|
||||
# ------------------------------
|
||||
#
|
||||
# override_whitelisted_methods = {
|
||||
# "frappe.desk.doctype.event.event.get_events": "lms.event.get_events"
|
||||
# }
|
||||
override_whitelisted_methods = {
|
||||
# "frappe.desk.search.get_names_for_mentions": "lms.lms.utils.get_names_for_mentions",
|
||||
}
|
||||
#
|
||||
# each overriding function accepts a `data` argument;
|
||||
# generated from the base implementation of the doctype dashboard,
|
||||
@@ -174,7 +173,8 @@ website_route_rules = [
|
||||
"to_route": "cohorts/join",
|
||||
},
|
||||
{"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>",
|
||||
"to_route": "/batches/progress",
|
||||
|
||||
@@ -4,11 +4,11 @@ from frappe.desk.page.setup_wizard.setup_wizard import add_all_roles_to
|
||||
|
||||
def after_install():
|
||||
add_pages_to_nav()
|
||||
create_batch_source()
|
||||
|
||||
|
||||
def after_sync():
|
||||
create_lms_roles()
|
||||
set_default_home()
|
||||
set_default_certificate_print_format()
|
||||
add_all_roles_to("Administrator")
|
||||
|
||||
@@ -19,7 +19,7 @@ def add_pages_to_nav():
|
||||
{"label": "Courses", "url": "/courses", "parent": "Explore", "idx": 2},
|
||||
{"label": "Batches", "url": "/batches", "parent": "Explore", "idx": 3},
|
||||
{"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},
|
||||
]
|
||||
|
||||
@@ -64,10 +64,6 @@ def delete_lms_roles():
|
||||
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():
|
||||
if not frappe.db.exists("Role", "Course Creator"):
|
||||
role = frappe.get_doc(
|
||||
@@ -182,3 +178,20 @@ def delete_custom_fields():
|
||||
|
||||
for field in fields:
|
||||
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", {
|
||||
refresh: (frm) => {
|
||||
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.web_form.after_save = () => {
|
||||
setTimeout(() => {
|
||||
window.location.href = `/jobs`;
|
||||
window.location.href = `/job-openings`;
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"list_columns": [],
|
||||
"login_required": 1,
|
||||
"max_attachment_size": 0,
|
||||
"modified": "2022-09-15 17:22:43.957184",
|
||||
"modified": "2022-09-15 17:22:43.957185",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Job",
|
||||
"name": "job-opportunity",
|
||||
@@ -32,7 +32,7 @@
|
||||
"show_list": 1,
|
||||
"show_sidebar": 0,
|
||||
"success_message": "",
|
||||
"success_url": "/jobs",
|
||||
"success_url": "/job-openings",
|
||||
"title": "Job Opportunity",
|
||||
"web_form_fields": [
|
||||
{
|
||||
|
||||
@@ -9,11 +9,12 @@
|
||||
"field_order": [
|
||||
"student_details_section",
|
||||
"student",
|
||||
"payment",
|
||||
"confirmation_email_sent",
|
||||
"column_break_oduu",
|
||||
"student_name",
|
||||
"username"
|
||||
"username",
|
||||
"column_break_oduu",
|
||||
"payment",
|
||||
"source",
|
||||
"confirmation_email_sent"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -59,12 +60,18 @@
|
||||
"fieldname": "confirmation_email_sent",
|
||||
"fieldtype": "Check",
|
||||
"label": "Confirmation Email Sent"
|
||||
},
|
||||
{
|
||||
"fieldname": "source",
|
||||
"fieldtype": "Link",
|
||||
"label": "Source",
|
||||
"options": "LMS Source"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-10-09 17:09:50.481794",
|
||||
"modified": "2023-10-26 16:52:04.266693",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Batch Student",
|
||||
|
||||
@@ -1,9 +1,23 @@
|
||||
# Copyright (c) 2022, Frappe and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class BatchStudent(Document):
|
||||
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"]
|
||||
|
||||
for quiz in quizzes:
|
||||
passing_percentage = frappe.db.get_value("LMS Quiz", quiz, "passing_percentage")
|
||||
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
|
||||
|
||||
|
||||
@@ -11,7 +11,11 @@ from frappe.utils.password import get_decrypted_password
|
||||
|
||||
class InviteRequest(Document):
|
||||
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()
|
||||
|
||||
def create_user(self, password):
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
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
|
||||
from frappe import _
|
||||
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):
|
||||
def validate(self):
|
||||
self.validate_duplicates()
|
||||
|
||||
def after_insert(self):
|
||||
if not frappe.flags.in_test:
|
||||
self.send_mail()
|
||||
|
||||
def validate_duplicates(self):
|
||||
if frappe.db.exists(
|
||||
"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()
|
||||
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) {
|
||||
set_timetable(frm);
|
||||
},
|
||||
|
||||
refresh: (frm) => {
|
||||
frm.add_web_link(`/batches/details/${frm.doc.name}`, "See on website");
|
||||
},
|
||||
});
|
||||
|
||||
const set_timetable = (frm) => {
|
||||
@@ -52,6 +60,7 @@ const set_timetable = (frm) => {
|
||||
"start_time",
|
||||
"end_time",
|
||||
"duration",
|
||||
"milestone",
|
||||
],
|
||||
filters: {
|
||||
parent: frm.doc.timetable_template,
|
||||
@@ -82,6 +91,7 @@ const add_timetable_rows = (frm, timetable) => {
|
||||
.format("HH:mm")
|
||||
: null;
|
||||
child.duration = row.duration;
|
||||
child.milestone = row.milestone;
|
||||
});
|
||||
frm.refresh_field("timetable");
|
||||
|
||||
@@ -121,3 +131,37 @@ const add_legend_rows = (frm, legends) => {
|
||||
frm.refresh_field("timetable_legends");
|
||||
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",
|
||||
"end_time",
|
||||
"published",
|
||||
"allow_self_enrollment",
|
||||
"section_break_rgfj",
|
||||
"medium",
|
||||
"category",
|
||||
"column_break_flwy",
|
||||
"seat_count",
|
||||
"evaluation_end_date",
|
||||
"section_break_6",
|
||||
"description",
|
||||
"batch_details_raw",
|
||||
@@ -45,6 +47,7 @@
|
||||
"column_break_iens",
|
||||
"amount",
|
||||
"currency",
|
||||
"amount_usd",
|
||||
"customisations_tab",
|
||||
"section_break_ubxi",
|
||||
"custom_component",
|
||||
@@ -120,12 +123,14 @@
|
||||
{
|
||||
"fieldname": "start_time",
|
||||
"fieldtype": "Time",
|
||||
"label": "Start Time"
|
||||
"label": "Start Time",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "end_time",
|
||||
"fieldtype": "Time",
|
||||
"label": "End Time"
|
||||
"label": "End Time",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "assessment_tab",
|
||||
@@ -277,11 +282,29 @@
|
||||
"fieldname": "allow_future",
|
||||
"fieldtype": "Check",
|
||||
"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,
|
||||
"links": [],
|
||||
"modified": "2023-10-12 12:53:37.351989",
|
||||
"modified": "2024-01-22 10:42:42.872995",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Batch",
|
||||
|
||||
@@ -12,9 +12,7 @@ from frappe.utils import (
|
||||
cint,
|
||||
format_date,
|
||||
format_datetime,
|
||||
add_to_date,
|
||||
getdate,
|
||||
get_datetime,
|
||||
get_time,
|
||||
)
|
||||
from lms.lms.utils import get_lessons, get_lesson_index, get_lesson_url
|
||||
from lms.www.utils import get_quiz_details, get_assignment_details
|
||||
@@ -31,6 +29,7 @@ class LMSBatch(Document):
|
||||
self.validate_membership()
|
||||
self.validate_timetable()
|
||||
self.send_confirmation_mail()
|
||||
self.validate_evaluation_end_date()
|
||||
|
||||
def validate_duplicate_students(self):
|
||||
students = [row.student for row in self.students]
|
||||
@@ -66,11 +65,14 @@ class LMSBatch(Document):
|
||||
|
||||
def send_confirmation_mail(self):
|
||||
for student in self.students:
|
||||
|
||||
if not student.confirmation_email_sent:
|
||||
self.send_mail(student)
|
||||
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):
|
||||
subject = _("Enrollment Confirmation for the Next Training Batch")
|
||||
template = "batch_confirmation"
|
||||
@@ -119,23 +121,27 @@ class LMSBatch(Document):
|
||||
def validate_timetable(self):
|
||||
for schedule in self.timetable:
|
||||
if schedule.start_time and schedule.end_time:
|
||||
if (
|
||||
schedule.start_time > schedule.end_time or schedule.start_time == schedule.end_time
|
||||
):
|
||||
if get_time(schedule.start_time) > get_time(schedule.end_time) or get_time(
|
||||
schedule.start_time
|
||||
) == get_time(schedule.end_time):
|
||||
frappe.throw(
|
||||
_("Row #{0} Start time cannot be greater than or equal to end time.").format(
|
||||
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(
|
||||
_("Row #{0} Start time cannot be outside the batch duration.").format(
|
||||
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(
|
||||
_("Row #{0} End time cannot be outside the batch duration.").format(schedule.idx)
|
||||
)
|
||||
@@ -250,8 +256,10 @@ def create_batch(
|
||||
paid_batch=0,
|
||||
amount=0,
|
||||
currency=None,
|
||||
amount_usd=0,
|
||||
name=None,
|
||||
published=0,
|
||||
evaluation_end_date=None,
|
||||
):
|
||||
frappe.only_for("Moderator")
|
||||
if name:
|
||||
@@ -267,7 +275,7 @@ def create_batch(
|
||||
"description": description,
|
||||
"batch_details": batch_details,
|
||||
"batch_details_raw": batch_details_raw,
|
||||
"image": meta_image,
|
||||
"meta_image": meta_image,
|
||||
"seat_count": seat_count,
|
||||
"start_time": start_time,
|
||||
"end_time": end_time,
|
||||
@@ -276,7 +284,9 @@ def create_batch(
|
||||
"paid_batch": paid_batch,
|
||||
"amount": amount,
|
||||
"currency": currency,
|
||||
"amount_usd": amount_usd,
|
||||
"published": published,
|
||||
"evaluation_end_date": evaluation_end_date,
|
||||
}
|
||||
)
|
||||
doc.save()
|
||||
@@ -325,7 +335,17 @@ def get_batch_timetable(batch):
|
||||
timetable = frappe.get_all(
|
||||
"LMS Batch Timetable",
|
||||
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",
|
||||
)
|
||||
|
||||
@@ -362,20 +382,26 @@ def get_timetable_details(timetable):
|
||||
assessment = frappe._dict({"assessment_name": entry.reference_docname})
|
||||
|
||||
if entry.reference_doctype == "Course Lesson":
|
||||
entry.icon = "icon-list"
|
||||
course = frappe.db.get_value(
|
||||
entry.reference_doctype, entry.reference_docname, "course"
|
||||
)
|
||||
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":
|
||||
entry.icon = "icon-quiz"
|
||||
entry.url = "/quizzes"
|
||||
details = get_quiz_details(assessment, frappe.session.user)
|
||||
entry.update(details)
|
||||
|
||||
elif entry.reference_doctype == "LMS Assignment":
|
||||
entry.icon = "icon-quiz"
|
||||
details = get_assignment_details(assessment, frappe.session.user)
|
||||
entry.update(details)
|
||||
|
||||
@@ -384,12 +410,37 @@ def get_timetable_details(timetable):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def send_email_to_students(batch, subject, reply_to, message):
|
||||
frappe.only_for("Moderator")
|
||||
students = frappe.get_all("Batch Student", {"parent": batch}, pluck="student")
|
||||
frappe.sendmail(
|
||||
recipients=students,
|
||||
subject=subject,
|
||||
reply_to=reply_to,
|
||||
message=message
|
||||
def is_milestone_complete(idx, batch):
|
||||
previous_rows = frappe.get_all(
|
||||
"LMS Batch Timetable",
|
||||
filters={"parent": batch, "idx": ["<", cint(idx)]},
|
||||
fields=["reference_doctype", "reference_docname", "idx"],
|
||||
order_by="idx",
|
||||
)
|
||||
|
||||
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",
|
||||
"start_time",
|
||||
"end_time",
|
||||
"duration"
|
||||
"duration",
|
||||
"milestone"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -69,12 +70,17 @@
|
||||
"fieldname": "day",
|
||||
"fieldtype": "Int",
|
||||
"label": "Day"
|
||||
},
|
||||
{
|
||||
"fieldname": "milestone",
|
||||
"fieldtype": "Check",
|
||||
"label": "Milestone"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-10-03 17:40:31.530181",
|
||||
"modified": "2023-10-20 11:58:01.782921",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"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) => {
|
||||
if (frm.doc.name)
|
||||
|
||||
@@ -8,11 +8,12 @@
|
||||
"course",
|
||||
"member",
|
||||
"member_name",
|
||||
"published",
|
||||
"template",
|
||||
"column_break_3",
|
||||
"issue_date",
|
||||
"expiry_date",
|
||||
"batch_name"
|
||||
"batch_name",
|
||||
"published"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -67,11 +68,18 @@
|
||||
"fieldname": "published",
|
||||
"fieldtype": "Check",
|
||||
"label": "Publish on Participant Page"
|
||||
},
|
||||
{
|
||||
"fieldname": "template",
|
||||
"fieldtype": "Link",
|
||||
"label": "Template",
|
||||
"options": "Print Format",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-09-13 11:03:23.479255",
|
||||
"modified": "2023-10-25 12:20:56.091979",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Certificate",
|
||||
|
||||
@@ -6,12 +6,42 @@ from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import add_years, nowdate
|
||||
from lms.lms.utils import is_certified
|
||||
from frappe.email.doctype.email_template.email_template import get_email_template
|
||||
|
||||
|
||||
class LMSCertificate(Document):
|
||||
def validate(self):
|
||||
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):
|
||||
certificates = frappe.get_all(
|
||||
"LMS Certificate",
|
||||
@@ -48,6 +78,15 @@ def create_certificate(course):
|
||||
if 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(
|
||||
{
|
||||
"doctype": "LMS Certificate",
|
||||
@@ -55,6 +94,7 @@ def create_certificate(course):
|
||||
"course": course,
|
||||
"issue_date": nowdate(),
|
||||
"expiry_date": expiry_date,
|
||||
"template": default_certificate_template,
|
||||
}
|
||||
)
|
||||
certificate.save(ignore_permissions=True)
|
||||
|
||||
@@ -47,12 +47,13 @@
|
||||
"fieldtype": "Rating",
|
||||
"in_list_view": 1,
|
||||
"label": "Rating",
|
||||
"reqd": 1
|
||||
"mandatory_depends_on": "eval:doc.status != 'Pending' && doc.status != 'In Progress'"
|
||||
},
|
||||
{
|
||||
"fieldname": "summary",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Summary"
|
||||
"label": "Summary",
|
||||
"mandatory_depends_on": "eval:doc.status != 'Pending' && doc.status != 'In Progress'"
|
||||
},
|
||||
{
|
||||
"fieldname": "date",
|
||||
@@ -106,7 +107,7 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-09-26 19:44:43.594892",
|
||||
"modified": "2023-12-18 20:03:27.040073",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Certificate Evaluation",
|
||||
|
||||
@@ -2,13 +2,19 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from lms.lms.utils import has_course_moderator_role
|
||||
|
||||
|
||||
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):
|
||||
|
||||
@@ -103,13 +103,13 @@
|
||||
"fieldname": "batch_name",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Batch Name",
|
||||
"label": "Batch",
|
||||
"options": "LMS Batch"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-23 14:50:37.618352",
|
||||
"modified": "2023-11-29 15:00:30.617298",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Certificate Request",
|
||||
|
||||
@@ -11,7 +11,20 @@ from lms.lms.utils import get_evaluator
|
||||
|
||||
class LMSCertificateRequest(Document):
|
||||
def validate(self):
|
||||
self.validate_slot()
|
||||
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):
|
||||
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():
|
||||
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()
|
||||
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(
|
||||
{"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(
|
||||
{
|
||||
"course": course,
|
||||
"evaluator": get_evaluator(course, batch),
|
||||
"evaluator": get_evaluator(course, batch_name),
|
||||
"member": frappe.session.user,
|
||||
"date": date,
|
||||
"day": day,
|
||||
"start_time": start_time,
|
||||
"end_time": end_time,
|
||||
"batch": batch,
|
||||
"batch_name": batch_name,
|
||||
}
|
||||
)
|
||||
eval.save(ignore_permissions=True)
|
||||
|
||||
@@ -34,8 +34,10 @@
|
||||
"related_courses",
|
||||
"pricing_section",
|
||||
"paid_course",
|
||||
"currency",
|
||||
"column_break_acoj",
|
||||
"course_price",
|
||||
"currency",
|
||||
"amount_usd",
|
||||
"certification_section",
|
||||
"enable_certification",
|
||||
"expiry",
|
||||
@@ -222,12 +224,22 @@
|
||||
"fieldname": "course_price",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Course Price",
|
||||
"option": "currency",
|
||||
"mandatory_depends_on": "paid_course"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_rxww",
|
||||
"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",
|
||||
@@ -254,7 +266,7 @@
|
||||
}
|
||||
],
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2023-08-28 11:09:11.945066",
|
||||
"modified": "2023-12-21 12:27:32.559901",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Course",
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
// Copyright (c) 2023, Frappe and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("LMS Payment", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
frappe.ui.form.on("LMS Payment", {
|
||||
onload(frm) {
|
||||
frm.set_query("member", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
ignore_user_type: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -8,8 +8,11 @@
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"payment_for_document_type",
|
||||
"member",
|
||||
"source",
|
||||
"column_break_rqkd",
|
||||
"payment_for_document",
|
||||
"billing_name",
|
||||
"payment_received",
|
||||
"payment_details_section",
|
||||
@@ -115,11 +118,29 @@
|
||||
"fieldname": "amount_with_gst",
|
||||
"fieldtype": "Currency",
|
||||
"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,
|
||||
"links": [],
|
||||
"modified": "2023-09-12 10:40:22.721371",
|
||||
"modified": "2023-10-26 16:54:12.408274",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"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) {
|
||||
// }
|
||||
});
|
||||
|
||||
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",
|
||||
"max_attempts",
|
||||
"show_submission_history",
|
||||
"section_break_hsiv",
|
||||
"passing_percentage",
|
||||
"column_break_rocd",
|
||||
"total_marks",
|
||||
"section_break_sbjx",
|
||||
"questions",
|
||||
"section_break_3",
|
||||
@@ -43,7 +47,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"default": "0",
|
||||
"fieldname": "max_attempts",
|
||||
"fieldtype": "Int",
|
||||
"label": "Max Attempts"
|
||||
@@ -90,11 +94,35 @@
|
||||
"fieldname": "show_submission_history",
|
||||
"fieldtype": "Check",
|
||||
"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,
|
||||
"links": [],
|
||||
"modified": "2023-07-04 15:26:24.457745",
|
||||
"modified": "2023-11-07 10:11:49.126789",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Quiz",
|
||||
@@ -123,6 +151,18 @@
|
||||
"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
|
||||
}
|
||||
],
|
||||
"show_title_field_in_link": 1,
|
||||
|
||||
@@ -5,7 +5,8 @@ import json
|
||||
import frappe
|
||||
from frappe import _
|
||||
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 (
|
||||
generate_slug,
|
||||
has_course_moderator_role,
|
||||
@@ -14,13 +15,22 @@ from lms.lms.utils import (
|
||||
|
||||
|
||||
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):
|
||||
if not self.name:
|
||||
self.name = generate_slug(self.title, "LMS Quiz")
|
||||
|
||||
def validate(self):
|
||||
validate_correct_answers(self.questions)
|
||||
|
||||
def get_last_submission_details(self):
|
||||
"""Returns the latest submission for this user."""
|
||||
user = frappe.session.user
|
||||
@@ -39,76 +49,11 @@ class LMSQuiz(Document):
|
||||
return result[0]
|
||||
|
||||
|
||||
def get_correct_options(question):
|
||||
correct_option_fields = [
|
||||
"is_correct_1",
|
||||
"is_correct_2",
|
||||
"is_correct_3",
|
||||
"is_correct_4",
|
||||
]
|
||||
return list(filter(lambda x: question.get(x) == 1, correct_option_fields))
|
||||
|
||||
|
||||
def validate_correct_answers(questions):
|
||||
def set_total_marks(quiz, questions):
|
||||
marks = 0
|
||||
for question in questions:
|
||||
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: {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}
|
||||
)
|
||||
marks += question.get("marks")
|
||||
return marks
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -118,45 +63,73 @@ def quiz_summary(quiz, results):
|
||||
|
||||
for result in results:
|
||||
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"]:
|
||||
correct = correct and point
|
||||
|
||||
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"]
|
||||
|
||||
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(
|
||||
{
|
||||
"doctype": "LMS Quiz Submission",
|
||||
"quiz": quiz,
|
||||
"result": results,
|
||||
"score": score,
|
||||
"score_out_of": score_out_of,
|
||||
"member": frappe.session.user,
|
||||
"percentage": percentage,
|
||||
"passing_percentage": quiz_details.passing_percentage,
|
||||
}
|
||||
)
|
||||
submission.save(ignore_permissions=True)
|
||||
|
||||
return {
|
||||
"score": score,
|
||||
"score_out_of": score_out_of,
|
||||
"submission": submission.name,
|
||||
"pass": percentage == quiz_details.passing_percentage,
|
||||
"percentage": percentage,
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
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():
|
||||
return
|
||||
|
||||
values = {
|
||||
"title": quiz_title,
|
||||
"passing_percentage": passing_percentage,
|
||||
"max_attempts": max_attempts,
|
||||
"show_answers": show_answers,
|
||||
"show_submission_history": show_submission_history,
|
||||
@@ -164,41 +137,77 @@ def save_quiz(
|
||||
|
||||
if quiz:
|
||||
frappe.db.set_value("LMS Quiz", quiz, values)
|
||||
update_questions(quiz, questions)
|
||||
return quiz
|
||||
else:
|
||||
doc = frappe.new_doc("LMS Quiz")
|
||||
doc.update(values)
|
||||
doc.save(ignore_permissions=True)
|
||||
doc.save()
|
||||
update_questions(doc.name, questions)
|
||||
return doc.name
|
||||
|
||||
|
||||
def update_questions(quiz, questions):
|
||||
questions = json.loads(questions)
|
||||
|
||||
delete_questions(quiz, questions)
|
||||
add_questions(quiz, questions)
|
||||
frappe.db.set_value("LMS Quiz", quiz, "total_marks", set_total_marks(quiz, questions))
|
||||
|
||||
|
||||
def delete_questions(quiz, questions):
|
||||
existing_questions = frappe.get_all(
|
||||
"LMS Quiz Question",
|
||||
{
|
||||
"parent": quiz,
|
||||
},
|
||||
pluck="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(
|
||||
{
|
||||
"parent": quiz,
|
||||
"parenttype": "LMS Quiz",
|
||||
"parentfield": "questions",
|
||||
"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))
|
||||
validate_correct_answers([values])
|
||||
|
||||
if values.get("name"):
|
||||
doc = frappe.get_doc("LMS Quiz Question", values.get("name"))
|
||||
doc = frappe.get_doc("LMS Question", values.get("name"))
|
||||
else:
|
||||
doc = frappe.new_doc("LMS Quiz Question")
|
||||
doc = frappe.new_doc("LMS Question")
|
||||
|
||||
doc.update(
|
||||
{
|
||||
"question": values["question"],
|
||||
"question": values.question,
|
||||
"type": values["type"],
|
||||
}
|
||||
)
|
||||
|
||||
if not values.get("name"):
|
||||
doc.update(
|
||||
{
|
||||
"parent": quiz,
|
||||
"parenttype": "LMS Quiz",
|
||||
"parentfield": "questions",
|
||||
"idx": index,
|
||||
}
|
||||
)
|
||||
|
||||
for num in range(1, 5):
|
||||
if values.get(f"option_{num}"):
|
||||
doc.update(
|
||||
@@ -222,9 +231,8 @@ def save_question(quiz, values, index):
|
||||
}
|
||||
)
|
||||
|
||||
doc.save(ignore_permissions=True)
|
||||
|
||||
return quiz
|
||||
doc.save()
|
||||
return doc.name
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -257,13 +265,13 @@ def check_choice_answers(question, answers):
|
||||
fields.append(f"option_{cstr(num)}")
|
||||
fields.append(f"is_correct_{cstr(num)}")
|
||||
|
||||
question_details = frappe.db.get_value(
|
||||
"LMS Quiz Question", question, fields, as_dict=1
|
||||
)
|
||||
question_details = frappe.db.get_value("LMS Question", question, fields, as_dict=1)
|
||||
|
||||
for num in range(1, 5):
|
||||
if question_details[f"option_{num}"] in answers:
|
||||
is_correct.append(question_details[f"is_correct_{num}"])
|
||||
elif question_details[f"is_correct_{num}"]:
|
||||
is_correct.append(2)
|
||||
else:
|
||||
is_correct.append(0)
|
||||
|
||||
@@ -275,9 +283,7 @@ def check_input_answers(question, answer):
|
||||
for num in range(1, 5):
|
||||
fields.append(f"possibility_{cstr(num)}")
|
||||
|
||||
question_details = frappe.db.get_value(
|
||||
"LMS Quiz Question", question, fields, as_dict=1
|
||||
)
|
||||
question_details = frappe.db.get_value("LMS Question", question, fields, as_dict=1)
|
||||
for num in range(1, 5):
|
||||
current_possibility = question_details[f"possibility_{num}"]
|
||||
if current_possibility and current_possibility.lower() == answer.lower():
|
||||
|
||||
@@ -10,51 +10,36 @@ import frappe
|
||||
class TestLMSQuiz(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
frappe.get_doc({"doctype": "LMS Quiz", "title": "Test Quiz"}).save(
|
||||
ignore_permissions=True
|
||||
)
|
||||
frappe.get_doc(
|
||||
{"doctype": "LMS Quiz", "title": "Test Quiz", "passing_percentage": 90}
|
||||
).save(ignore_permissions=True)
|
||||
|
||||
def test_with_multiple_options(self):
|
||||
quiz = frappe.get_doc("LMS Quiz", "test-quiz")
|
||||
quiz.append(
|
||||
"questions",
|
||||
{
|
||||
"question": "Question Multiple",
|
||||
"type": "Choices",
|
||||
"option_1": "Option 1",
|
||||
"is_correct_1": 1,
|
||||
"option_2": "Option 2",
|
||||
"is_correct_2": 1,
|
||||
},
|
||||
)
|
||||
quiz.save()
|
||||
self.assertTrue(quiz.questions[0].multiple)
|
||||
question = frappe.new_doc("LMS Question")
|
||||
question.question = "Question Multiple"
|
||||
question.type = "Choices"
|
||||
question.option_1 = "Option 1"
|
||||
question.is_correct_1 = 1
|
||||
question.option_2 = "Option 2"
|
||||
question.is_correct_2 = 1
|
||||
question.save()
|
||||
self.assertTrue(question.multiple)
|
||||
|
||||
def test_with_no_correct_option(self):
|
||||
quiz = frappe.get_doc("LMS Quiz", "test-quiz")
|
||||
quiz.append(
|
||||
"questions",
|
||||
{
|
||||
"question": "Question no correct option",
|
||||
"type": "Choices",
|
||||
"option_1": "Option 1",
|
||||
"option_2": "Option 2",
|
||||
},
|
||||
)
|
||||
self.assertRaises(frappe.ValidationError, quiz.save)
|
||||
question = frappe.new_doc("LMS Question")
|
||||
question.question = "Question Multiple"
|
||||
question.type = "Choices"
|
||||
question.option_1 = "Option 1"
|
||||
question.option_2 = "Option 2"
|
||||
self.assertRaises(frappe.ValidationError, question.save)
|
||||
|
||||
def test_with_no_possible_answers(self):
|
||||
quiz = frappe.get_doc("LMS Quiz", "test-quiz")
|
||||
quiz.append(
|
||||
"questions",
|
||||
{
|
||||
"question": "Question Possible Answers",
|
||||
"type": "User Input",
|
||||
},
|
||||
)
|
||||
self.assertRaises(frappe.ValidationError, quiz.save)
|
||||
question = frappe.new_doc("LMS Question")
|
||||
question.question = "Question Multiple"
|
||||
question.type = "User Input"
|
||||
self.assertRaises(frappe.ValidationError, question.save)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls) -> None:
|
||||
frappe.db.delete("LMS Quiz", "test-quiz")
|
||||
frappe.db.delete("LMS Quiz Question", {"parent": "test-quiz"})
|
||||
frappe.db.delete("LMS Question")
|
||||
|
||||
@@ -6,208 +6,31 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"question",
|
||||
"type",
|
||||
"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"
|
||||
"marks"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "question",
|
||||
"fieldtype": "Text Editor",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Question",
|
||||
"options": "LMS Question",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "option_1",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Option 1",
|
||||
"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",
|
||||
"default": "1",
|
||||
"fieldname": "marks",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Type",
|
||||
"options": "Choices\nUser Input"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
"label": "Marks",
|
||||
"non_negative": 1,
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-07-04 16:43:49.837134",
|
||||
"modified": "2023-10-16 19:51:03.893144",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Quiz Question",
|
||||
|
||||
@@ -6,7 +6,11 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"question",
|
||||
"section_break_fztv",
|
||||
"question_name",
|
||||
"answer",
|
||||
"column_break_flus",
|
||||
"marks",
|
||||
"is_correct"
|
||||
],
|
||||
"fields": [
|
||||
@@ -31,12 +35,33 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Is Correct",
|
||||
"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,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-11-24 11:15:45.931119",
|
||||
"modified": "2023-10-17 11:55:25.641214",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Quiz Result",
|
||||
|
||||
@@ -6,11 +6,16 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"quiz",
|
||||
"score",
|
||||
"course",
|
||||
"column_break_3",
|
||||
"member",
|
||||
"member_name",
|
||||
"section_break_dkpn",
|
||||
"score",
|
||||
"score_out_of",
|
||||
"column_break_gkip",
|
||||
"percentage",
|
||||
"passing_percentage",
|
||||
"section_break_6",
|
||||
"result"
|
||||
],
|
||||
@@ -31,9 +36,11 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "score",
|
||||
"fieldtype": "Data",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Score"
|
||||
"label": "Score",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "member",
|
||||
@@ -65,12 +72,45 @@
|
||||
"label": "Course",
|
||||
"options": "LMS Course",
|
||||
"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,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2022-11-15 15:27:07.770945",
|
||||
"modified": "2023-10-17 13:07:27.979975",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Quiz Submission",
|
||||
|
||||
@@ -6,4 +6,10 @@ from frappe.model.document import 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",
|
||||
"section_break_szgq",
|
||||
"send_calendar_invite_for_evaluations",
|
||||
"batch_confirmation_template",
|
||||
"column_break_2",
|
||||
"show_day_view",
|
||||
"allow_student_progress",
|
||||
"payment_section",
|
||||
"razorpay_key",
|
||||
"razorpay_secret",
|
||||
"apply_gst",
|
||||
"column_break_cfcv",
|
||||
"default_currency",
|
||||
"show_usd_equivalent",
|
||||
"apply_rounding",
|
||||
"exception_country",
|
||||
"column_break_2",
|
||||
"show_dashboard",
|
||||
"show_courses",
|
||||
"show_students",
|
||||
"show_assessments",
|
||||
"show_live_class",
|
||||
"show_discussions",
|
||||
"show_emails",
|
||||
"signup_settings_tab",
|
||||
"signup_settings_section",
|
||||
"terms_of_use",
|
||||
@@ -42,7 +40,22 @@
|
||||
"mentor_request_tab",
|
||||
"mentor_request_section",
|
||||
"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": [
|
||||
{
|
||||
@@ -71,7 +84,8 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_2",
|
||||
"fieldtype": "Column Break"
|
||||
"fieldtype": "Column Break",
|
||||
"label": "Show Tab in Batch"
|
||||
},
|
||||
{
|
||||
"fieldname": "search_placeholder",
|
||||
@@ -199,8 +213,7 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Payment"
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "default_currency",
|
||||
@@ -261,12 +274,86 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Batch Confirmation 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,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-10-09 17:27:28.615355",
|
||||
"modified": "2023-12-12 10:32:13.638368",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"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,
|
||||
"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",
|
||||
"modified": "2023-02-28 19:53:47.716135",
|
||||
"message_type": "HTML",
|
||||
"modified": "2023-11-29 17:34:54.514031",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"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") %}
|
||||
|
||||
<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>
|
||||
|
||||
@@ -11,8 +11,9 @@
|
||||
"event": "Days Before",
|
||||
"idx": 0,
|
||||
"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",
|
||||
"modified": "2022-06-03 11:51:02.681803",
|
||||
"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",
|
||||
"message_type": "HTML",
|
||||
"modified": "2023-11-29 17:26:53.355501",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Certificate Request Reminder",
|
||||
@@ -20,6 +21,9 @@
|
||||
"recipients": [
|
||||
{
|
||||
"receiver_by_document_field": "member"
|
||||
},
|
||||
{
|
||||
"receiver_by_document_field": "evaluator"
|
||||
}
|
||||
],
|
||||
"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,
|
||||
"align_labels_right": 0,
|
||||
"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,
|
||||
"disabled": 0,
|
||||
"doc_type": "LMS Certificate",
|
||||
@@ -10,19 +10,20 @@
|
||||
"doctype": "Print Format",
|
||||
"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}]}",
|
||||
"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,
|
||||
"line_breaks": 0,
|
||||
"margin_bottom": 0.0,
|
||||
"margin_left": 0.0,
|
||||
"margin_right": 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",
|
||||
"module": "LMS",
|
||||
"name": "Certificate",
|
||||
"owner": "Administrator",
|
||||
"page_number": "Hide",
|
||||
"print_designer": 0,
|
||||
"print_format_builder": 0,
|
||||
"print_format_builder_beta": 1,
|
||||
"print_format_type": "Jinja",
|
||||
|
||||
135
lms/lms/utils.py
135
lms/lms/utils.py
@@ -4,10 +4,16 @@ import frappe
|
||||
import json
|
||||
import razorpay
|
||||
import requests
|
||||
import base64
|
||||
from frappe import _
|
||||
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 (
|
||||
add_months,
|
||||
cint,
|
||||
@@ -150,7 +156,7 @@ def get_lesson_details(chapter):
|
||||
],
|
||||
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)
|
||||
lessons.append(lesson_details)
|
||||
return lessons
|
||||
@@ -549,6 +555,9 @@ def can_create_courses(course, member=None):
|
||||
if portal_course_creation == "Anyone" and member in instructors:
|
||||
return True
|
||||
|
||||
if not course and has_course_instructor_role(member):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@@ -604,17 +613,20 @@ def validate_image(path):
|
||||
return path
|
||||
|
||||
|
||||
def create_notification_log(doc, method):
|
||||
def handle_notifications(doc, method):
|
||||
topic = frappe.db.get_value(
|
||||
"Discussion Topic",
|
||||
doc.topic,
|
||||
["reference_doctype", "reference_docname", "owner", "title"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if topic.reference_doctype != "Course Lesson":
|
||||
if topic.reference_doctype not in ["Course Lesson", "LMS Batch"]:
|
||||
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")
|
||||
instructors = frappe.db.get_all(
|
||||
"Course Instructor", {"parent": course}, pluck="instructor"
|
||||
@@ -641,6 +653,47 @@ def create_notification_log(doc, method):
|
||||
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):
|
||||
lesson_count = 0
|
||||
chapters = frappe.get_all("Chapter Reference", {"parent": course}, ["chapter"])
|
||||
@@ -774,6 +827,17 @@ def get_telemetry_boot_info():
|
||||
|
||||
|
||||
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")
|
||||
chapter_created = frappe.db.a_row_exists("Course Chapter")
|
||||
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)
|
||||
|
||||
return {
|
||||
"is_onboarded": frappe.db.get_single_value("LMS Settings", "is_onboarding_complete"),
|
||||
"is_onboarded": onboarding_status,
|
||||
"course_created": course_created,
|
||||
"chapter_created": chapter_created,
|
||||
"lesson_created": lesson_created,
|
||||
@@ -853,8 +917,9 @@ def get_payment_options(doctype, docname, phone, country):
|
||||
|
||||
validate_phone_number(phone, True)
|
||||
details = get_details(doctype, docname)
|
||||
|
||||
details.amount, details.currency = check_multicurrency(
|
||||
details.amount, details.currency, country
|
||||
details.amount, details.currency, country, details.amount_usd
|
||||
)
|
||||
if details.currency == "INR":
|
||||
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"),
|
||||
"description": _("Payment for {0} course").format(details["title"]),
|
||||
"order_id": order["id"],
|
||||
"amount": order["amount"] * 100,
|
||||
"amount": cint(order["amount"]) * 100,
|
||||
"currency": order["currency"],
|
||||
"prefill": {
|
||||
"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
|
||||
|
||||
|
||||
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")
|
||||
exception_country = frappe.get_all(
|
||||
"Payment Country", filters={"parent": "LMS Settings"}, pluck="country"
|
||||
)
|
||||
apply_rounding = frappe.db.get_single_value("LMS Settings", "apply_rounding")
|
||||
country = country or frappe.db.get_value(
|
||||
"Address", {"email_id": frappe.session.user}, "country"
|
||||
country = (
|
||||
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":
|
||||
return amount, currency
|
||||
|
||||
@@ -898,8 +968,9 @@ def check_multicurrency(amount, currency, country=None):
|
||||
amount = amount * exchange_rate
|
||||
currency = "USD"
|
||||
|
||||
apply_rounding = frappe.db.get_single_value("LMS Settings", "apply_rounding")
|
||||
if apply_rounding and amount % 100 != 0:
|
||||
amount = ceil(amount + 100 - amount % 100)
|
||||
amount = amount + 100 - amount % 100
|
||||
|
||||
return amount, currency
|
||||
|
||||
@@ -923,7 +994,7 @@ def get_details(doctype, docname):
|
||||
details = frappe.db.get_value(
|
||||
"LMS Course",
|
||||
docname,
|
||||
["name", "title", "paid_course", "currency", "course_price as amount"],
|
||||
["name", "title", "paid_course", "currency", "course_price as amount", "amount_usd"],
|
||||
as_dict=True,
|
||||
)
|
||||
if not details.paid_course:
|
||||
@@ -932,7 +1003,7 @@ def get_details(doctype, docname):
|
||||
details = frappe.db.get_value(
|
||||
"LMS Batch",
|
||||
docname,
|
||||
["name", "title", "paid_batch", "currency", "amount"],
|
||||
["name", "title", "paid_batch", "currency", "amount", "amount_usd"],
|
||||
as_dict=True,
|
||||
)
|
||||
if not details.paid_batch:
|
||||
@@ -981,13 +1052,15 @@ def create_order(client, amount, currency):
|
||||
try:
|
||||
return client.order.create(
|
||||
{
|
||||
"amount": amount * 100,
|
||||
"amount": cint(amount) * 100,
|
||||
"currency": currency,
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
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"],
|
||||
"gstin": address.gstin,
|
||||
"pan": address.pan,
|
||||
"source": address.source,
|
||||
"payment_for_document_type": doctype,
|
||||
"payment_for_document": docname,
|
||||
}
|
||||
)
|
||||
payment_doc.save(ignore_permissions=True)
|
||||
return payment_doc.name
|
||||
return payment_doc
|
||||
|
||||
|
||||
def get_payment_details(doctype, docname, address):
|
||||
amount_field = "course_price" if doctype == "LMS Course" else "amount"
|
||||
amount = frappe.db.get_value(doctype, docname, amount_field)
|
||||
currency = frappe.db.get_value(doctype, docname, "currency")
|
||||
amount_usd = frappe.db.get_value(doctype, docname, "amount_usd")
|
||||
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":
|
||||
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):
|
||||
membership = frappe.new_doc("LMS Enrollment")
|
||||
membership.update(
|
||||
{"member": frappe.session.user, "course": course, "payment": payment}
|
||||
{"member": frappe.session.user, "course": course, "payment": payment.name}
|
||||
)
|
||||
membership.save(ignore_permissions=True)
|
||||
return f"/courses/{course}/learn/1.1"
|
||||
@@ -1066,7 +1143,8 @@ def add_student_to_batch(batchname, payment):
|
||||
student.update(
|
||||
{
|
||||
"student": frappe.session.user,
|
||||
"payment": payment,
|
||||
"payment": payment.name,
|
||||
"source": payment.source,
|
||||
"parent": batchname,
|
||||
"parenttype": "LMS Batch",
|
||||
"parentfield": "students",
|
||||
@@ -1089,3 +1167,16 @@ def change_currency(amount, currency, country=None):
|
||||
amount = cint(amount)
|
||||
amount, currency = check_multicurrency(amount, currency, country)
|
||||
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}}">
|
||||
{{ _("Notify me when available") }}
|
||||
</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 %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -5,7 +5,7 @@ from frappe import _
|
||||
from frappe.core.doctype.user.user import User
|
||||
from frappe.utils import cint, escape_html, random_string
|
||||
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.model.naming import append_number_if_name_exists
|
||||
from lms.widgets import Widgets
|
||||
@@ -260,19 +260,6 @@ def set_country_from_ip(login_manager=None, user=None):
|
||||
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):
|
||||
if frappe.db.get_single_value(
|
||||
"System Settings", "setup_complete"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
[pre_model_sync]
|
||||
community.patches.set_email_preferences
|
||||
community.patches.change_name_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
|
||||
execute:frappe.permissions.reset_perms("LMS Enrollment")
|
||||
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>"
|
||||
|
||||
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(
|
||||
"LMS Quiz Submission", {"owner": frappe.session.user, "quiz": quiz_name}
|
||||
)
|
||||
|
||||
@@ -785,12 +785,13 @@ input[type=checkbox] {
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: var(--text-base);
|
||||
line-height: 20px;
|
||||
color: var(--gray-900);
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: var(--text-base);
|
||||
line-height: 20px;
|
||||
color: var(--gray-900);
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.course-details-outline {
|
||||
@@ -2386,6 +2387,7 @@ select {
|
||||
border: 1px solid var(--gray-200) !important;
|
||||
border-radius: var(--border-radius-md) !important;
|
||||
background-color: var(--gray-100) !important;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.toastui-calendar-panel .toastui-calendar-day-names.toastui-calendar-week {
|
||||
@@ -2441,13 +2443,23 @@ select {
|
||||
}
|
||||
|
||||
.calendar-legends {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 50%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
width: 75%;
|
||||
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 {
|
||||
width: 50%;
|
||||
margin: 2rem 0;
|
||||
@@ -2474,3 +2486,15 @@ select {
|
||||
.modal-body .ql-container {
|
||||
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",
|
||||
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",
|
||||
},
|
||||
@@ -293,13 +284,6 @@ const open_batch_dialog = () => {
|
||||
reqd: 1,
|
||||
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",
|
||||
},
|
||||
@@ -308,12 +292,24 @@ const open_batch_dialog = () => {
|
||||
label: __("Start Time"),
|
||||
fieldname: "start_time",
|
||||
default: batch_info && batch_info.start_time,
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldtype: "Time",
|
||||
label: __("End Time"),
|
||||
fieldname: "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",
|
||||
@@ -323,6 +319,21 @@ const open_batch_dialog = () => {
|
||||
only_select: 1,
|
||||
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",
|
||||
},
|
||||
@@ -381,6 +392,15 @@ const open_batch_dialog = () => {
|
||||
depends_on: "paid_batch",
|
||||
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: (values) => {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
{% set certificates = get_certificates(user) %}
|
||||
|
||||
{% if certificates | length %}
|
||||
<div class="cards-parent">
|
||||
{% 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>
|
||||
<br>
|
||||
<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>
|
||||
<br>
|
||||
<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) }}
|
||||
</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 %}
|
||||
{% set suffix = "times" if quiz.max_attempts > 1 else "time" %}
|
||||
<li>
|
||||
@@ -18,8 +24,7 @@
|
||||
{{ _("The quiz has a time limit. For each question you will be given {0} seconds.").format(quiz.time) }}
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
</ul>
|
||||
|
||||
<div id="start-banner" class="common-card-style column-card align-items-center">
|
||||
|
||||
@@ -50,8 +55,12 @@
|
||||
<div class="question hide" data-name="{{ question.name }}" data-type="{{ question.type }}"
|
||||
data-multi="{{ question.multiple }}" data-qt-index="{{ loop.index }}">
|
||||
<div>
|
||||
<div class="pull-right font-weight-bold">
|
||||
{{ question.marks }} {{ _("Marks") }}
|
||||
</div>
|
||||
<div class="question-number">
|
||||
{{ _("Question ") }}{{ loop.index }}: {{ instruction }}</div>
|
||||
{{ _("Question ") }}{{ loop.index }}: {{ instruction }}
|
||||
</div>
|
||||
<div class="question-text">
|
||||
{{ question.question }}
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ frappe.ready(() => {
|
||||
this.answer = [];
|
||||
this.is_correct = [];
|
||||
this.show_answers = $("#quiz-title").data("show-answers");
|
||||
this.current_index = 0;
|
||||
localStorage.removeItem($("#quiz-title").data("name"));
|
||||
|
||||
$(".btn-start-quiz").click((e) => {
|
||||
@@ -37,7 +38,6 @@ frappe.ready(() => {
|
||||
$("#next").click((e) => {
|
||||
e.preventDefault();
|
||||
if (!this.show_answers) check_answer();
|
||||
|
||||
mark_active_question(e);
|
||||
});
|
||||
|
||||
@@ -48,7 +48,7 @@ frappe.ready(() => {
|
||||
|
||||
const mark_active_question = (e = undefined) => {
|
||||
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;
|
||||
|
||||
if (this.show_answers) {
|
||||
@@ -120,7 +120,6 @@ const enable_check = (e) => {
|
||||
const quiz_summary = (e = undefined) => {
|
||||
e && e.preventDefault();
|
||||
let quiz_name = $("#quiz-title").data("name");
|
||||
let total_questions = $(".question").length;
|
||||
let self = this;
|
||||
|
||||
frappe.call({
|
||||
@@ -135,14 +134,20 @@ const quiz_summary = (e = undefined) => {
|
||||
$(".quiz-footer span").addClass("hide");
|
||||
$("#quiz-form").prepend(
|
||||
`<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}
|
||||
${__("out of")} ${total_questions}
|
||||
${__("out of")} ${data.message.score_out_of}
|
||||
</div>`
|
||||
);
|
||||
$("#try-again").attr("data-submission", data.message.submission);
|
||||
$("#try-again").removeClass("hide");
|
||||
self.quiz_submitted = true;
|
||||
if (this.hasOwnProperty("marked_as_complete")) {
|
||||
if (
|
||||
this.hasOwnProperty("marked_as_complete") &&
|
||||
data.message.pass
|
||||
) {
|
||||
mark_progress();
|
||||
}
|
||||
},
|
||||
@@ -165,7 +170,7 @@ const check_answer = (e = undefined) => {
|
||||
e && e.preventDefault();
|
||||
let answer = $(".active-question textarea");
|
||||
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()) {
|
||||
frappe.throw(__("Please enter your answer"));
|
||||
@@ -177,12 +182,13 @@ const check_answer = (e = undefined) => {
|
||||
$(".explanation").removeClass("hide");
|
||||
$("#check").addClass("hide");
|
||||
|
||||
if (current_index == total_questions) {
|
||||
if (current_index == total_questions - 1) {
|
||||
$("#summary").removeClass("hide");
|
||||
} else if (this.show_answers) {
|
||||
$("#next").removeClass("hide");
|
||||
}
|
||||
parse_options();
|
||||
this.current_index += 1;
|
||||
};
|
||||
|
||||
const parse_options = () => {
|
||||
@@ -233,7 +239,9 @@ const parse_choices = (element, is_correct) => {
|
||||
? add_icon(elem, "check")
|
||||
: add_icon(elem, "wrong");
|
||||
} 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 = () => {
|
||||
let current_index = $(".active-question").attr("data-qt-index");
|
||||
let quiz_name = $("#quiz-title").data("name");
|
||||
let quiz_stored = JSON.parse(localStorage.getItem(quiz_name));
|
||||
|
||||
let quiz_obj = {
|
||||
question_index: current_index - 1,
|
||||
question_index: this.current_index,
|
||||
answer: self.answer.join(),
|
||||
is_correct: self.is_correct,
|
||||
};
|
||||
|
||||
@@ -6,8 +6,4 @@
|
||||
<a class="btn btn-secondary btn-sm" href="/login?redirect-to=/courses/{{ course.name }}">
|
||||
{{ _("Write a review") }}
|
||||
</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 %}
|
||||
|
||||
@@ -7,7 +7,7 @@ def get_context(context):
|
||||
context.no_cache = 1
|
||||
|
||||
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()
|
||||
submission = frappe.form_dict["submission"]
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
{{ _("Title") }}
|
||||
</div>
|
||||
<div class="">
|
||||
<input id="lesson-title" type="text" class="field-input" data-index="{{ lesson_index }}" data-chapter="{{ chapter }}" data-course="{{ course.name }}" {% if lesson.name %} data-lesson="{{ lesson.name }}" value="{{ lesson.title }}" {% endif %}>
|
||||
<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>
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
{%- block script %}
|
||||
{{ super() }}
|
||||
<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/embed@latest"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -49,9 +49,9 @@ const get_tools = () => {
|
||||
vimeo: true,
|
||||
codepen: true,
|
||||
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:
|
||||
"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>",
|
||||
},
|
||||
},
|
||||
@@ -234,7 +234,7 @@ const save = () => {
|
||||
args: {
|
||||
title: $("#lesson-title").val(),
|
||||
body: this.lesson_content_data,
|
||||
chapter: $("#lesson-title").data("chapter"),
|
||||
chapter: decodeURIComponent($("#lesson-title").data("chapter")),
|
||||
preview: $("#preview").prop("checked") ? 1 : 0,
|
||||
idx: $("#lesson-title").data("index"),
|
||||
lesson: lesson ? lesson : "",
|
||||
@@ -429,9 +429,9 @@ class 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}
|
||||
</div>`;
|
||||
</a>`;
|
||||
}
|
||||
|
||||
validate(savedData) {
|
||||
|
||||
@@ -107,10 +107,13 @@ def get_page_extensions(context):
|
||||
|
||||
|
||||
def get_neighbours(current, lessons):
|
||||
current = flt(current)
|
||||
numbers = sorted(lesson.number for lesson in lessons)
|
||||
index = numbers.index(current)
|
||||
numbers = [lesson.number for lesson in lessons]
|
||||
tuples_list = [tuple(int(x) for x in s.split(".")) for s in numbers]
|
||||
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 {
|
||||
"prev": numbers[index - 1] if index - 1 >= 0 else None,
|
||||
"next": numbers[index + 1] if index + 1 < len(numbers) else None,
|
||||
"prev": sorted_numbers[index - 1] if index - 1 >= 0 else None,
|
||||
"next": sorted_numbers[index + 1] if index + 1 < len(sorted_numbers) else None,
|
||||
}
|
||||
|
||||
@@ -16,25 +16,9 @@
|
||||
{% macro QuizForm(quiz) %}
|
||||
<div id="quiz-form" {% if quiz.name %} data-name="{{ quiz.name }}" data-index="{{ quiz.questions | length }}" {% endif %}>
|
||||
{{ QuizDetails(quiz) }}
|
||||
{% if quiz.questions %}
|
||||
<div class="field-group">
|
||||
<div class="field-label mb-1">
|
||||
{{ _("Questions") }}
|
||||
</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 class="field-group">
|
||||
<div class="questions-table"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
@@ -59,11 +43,6 @@
|
||||
</div>
|
||||
|
||||
<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">
|
||||
{{ _("Save") }}
|
||||
</button>
|
||||
@@ -98,18 +77,30 @@
|
||||
{{ _("Enter the maximum number of times a user can attempt this quiz") }}
|
||||
</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 }}">
|
||||
</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">
|
||||
{% set show_answers = quiz.show_answers or not quiz.name %}
|
||||
<label for="show-answers" class="vertically-center mb-0">
|
||||
<input type="checkbox" id="show-answers" {% if show_answers %} checked {% endif %}>
|
||||
{{ _("Show Answers") }}
|
||||
</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 %}>
|
||||
{{ _("Show Submission History") }}
|
||||
</label>
|
||||
@@ -151,5 +142,9 @@
|
||||
|
||||
{%- block script %}
|
||||
{{ 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 %}
|
||||
@@ -1,17 +1,21 @@
|
||||
frappe.ready(() => {
|
||||
$(".btn-save-quiz").click((e) => {
|
||||
save_quiz({
|
||||
quiz_title: $("#quiz-title").val(),
|
||||
max_attempts: $("#max-attempts").val(),
|
||||
if ($(".questions-table").length) {
|
||||
frappe.require("controls.bundle.js", () => {
|
||||
create_questions_table();
|
||||
});
|
||||
}
|
||||
|
||||
$(".btn-save-quiz").click((e) => {
|
||||
save_quiz();
|
||||
});
|
||||
|
||||
$(".question-row").click((e) => {
|
||||
edit_question(e);
|
||||
});
|
||||
|
||||
$(".btn-add-question").click((e) => {
|
||||
show_question_modal();
|
||||
$(document).on("click", ".questions-table .link-btn", (e) => {
|
||||
e.preventDefault();
|
||||
fetch_question_data(e);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,6 +35,8 @@ const show_question_modal = (values = {}) => {
|
||||
};
|
||||
|
||||
const get_question_fields = (values = {}) => {
|
||||
if (!values.question) values = {};
|
||||
|
||||
let dialog_fields = [
|
||||
{
|
||||
fieldtype: "Text Editor",
|
||||
@@ -66,6 +72,7 @@ const get_question_fields = (values = {}) => {
|
||||
if (num <= 2) option.mandatory_depends_on = "eval:doc.type=='Choices'";
|
||||
|
||||
dialog_fields.push(option);
|
||||
console.log(dialog_fields);
|
||||
|
||||
dialog_fields.push({
|
||||
fieldtype: "Data",
|
||||
@@ -120,12 +127,16 @@ const edit_question = (e) => {
|
||||
|
||||
const save_quiz = (values) => {
|
||||
validate_mandatory();
|
||||
validate_questions();
|
||||
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_quiz.lms_quiz.save_quiz",
|
||||
args: {
|
||||
quiz_title: values.quiz_title,
|
||||
max_attempts: values.max_attempts,
|
||||
quiz_title: $("#quiz-title").val(),
|
||||
max_attempts: $("#max-attempts").val(),
|
||||
passing_percentage: $("#passing-percentage").val(),
|
||||
quiz: $("#quiz-form").data("name") || "",
|
||||
questions: this.table.get_value("questions"),
|
||||
show_answers: $("#show-answers").is(":checked") ? 1 : 0,
|
||||
show_submission_history: $("#show-submission-history").is(
|
||||
":checked"
|
||||
@@ -146,13 +157,45 @@ const save_quiz = (values) => {
|
||||
};
|
||||
|
||||
const validate_mandatory = () => {
|
||||
if (!$("#quiz-title").val()) {
|
||||
let error = $("p")
|
||||
.addClass("error-message")
|
||||
.text(__("Please enter a Quiz Title"));
|
||||
$(error).insertAfter("#quiz-title");
|
||||
$("#quiz-title").focus();
|
||||
throw "Title is mandatory";
|
||||
let fields = ["#quiz-title", "#passing-percentage"];
|
||||
fields.forEach((field, idx) => {
|
||||
if (!$(field).val()) {
|
||||
let error = $("p")
|
||||
.addClass("error-message")
|
||||
.text(__("Please enter a value"));
|
||||
$(error).insertAfter(field);
|
||||
scroll_to_element($(field));
|
||||
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,13 +210,98 @@ const save_question = (values) => {
|
||||
callback: (data) => {
|
||||
if (data.message) this.question_dialog.hide();
|
||||
|
||||
frappe.show_alert({
|
||||
message: __("Saved"),
|
||||
indicator: "green",
|
||||
});
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
if (values.name) {
|
||||
frappe.show_alert({
|
||||
message: __("Saved"),
|
||||
indicator: "green",
|
||||
});
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 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":
|
||||
context.quiz = frappe._dict()
|
||||
else:
|
||||
fields_arr = ["name", "question", "type"]
|
||||
|
||||
context.quiz = frappe.db.get_value(
|
||||
"LMS Quiz",
|
||||
quizname,
|
||||
["title", "name", "max_attempts", "show_answers", "show_submission_history"],
|
||||
[
|
||||
"title",
|
||||
"name",
|
||||
"max_attempts",
|
||||
"passing_percentage",
|
||||
"show_answers",
|
||||
"show_submission_history",
|
||||
],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
fields_arr = ["name", "question", "marks"]
|
||||
context.quiz.questions = frappe.get_all(
|
||||
"LMS Quiz Question", {"parent": quizname}, fields_arr, order_by="idx"
|
||||
)
|
||||
|
||||
@@ -49,11 +49,14 @@
|
||||
<use href="#icon-calendar"></use>
|
||||
</svg>
|
||||
<span>
|
||||
{{ frappe.utils.format_date(batch_info.start_date, "long") }} -
|
||||
{{ frappe.utils.format_date(batch_info.start_date, "long") }}
|
||||
</span>
|
||||
|
||||
{% if batch_info.start_date != batch_info.end_date %}
|
||||
<span>
|
||||
{{ frappe.utils.format_date(batch_info.end_date, "long") }}
|
||||
- {{ frappe.utils.format_date(batch_info.end_date, "long") }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<span class="seperator"></span>
|
||||
@@ -75,14 +78,6 @@
|
||||
</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 %}
|
||||
<div class="mt-4">
|
||||
{{ batch_info.custom_component }}
|
||||
@@ -96,8 +91,7 @@
|
||||
<div class="mt-4">
|
||||
|
||||
<ul class="nav lms-nav" id="batches-tab">
|
||||
|
||||
{% if is_student %}
|
||||
{% if settings.show_dashboard and is_student %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if is_student %} active {% endif %}" data-toggle="tab" href="#dashboard">
|
||||
{{ _("Dashboard") }}
|
||||
@@ -105,6 +99,7 @@
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if settings.show_courses %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if not is_student %} active {% endif %}" data-toggle="tab" href="#courses">
|
||||
{{ _("Courses") }}
|
||||
@@ -113,6 +108,7 @@
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if show_timetable %}
|
||||
<li class="nav-item">
|
||||
@@ -123,40 +119,59 @@
|
||||
{% endif %}
|
||||
|
||||
{% if is_moderator %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#students">
|
||||
{{ _("Students") }}
|
||||
<span class="course-list-count">
|
||||
{{ batch_students | length }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{% if settings.show_students %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#students">
|
||||
{{ _("Students") }}
|
||||
<span class="course-list-count">
|
||||
{{ batch_students | length }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#assessments">
|
||||
{{ _("Assessments") }}
|
||||
<span class="course-list-count">
|
||||
{{ assessments | length }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{% if settings.show_assessments %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#assessments">
|
||||
{{ _("Assessments") }}
|
||||
<span class="course-list-count">
|
||||
{{ assessments | length }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{% 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) %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#discussions">
|
||||
{{ _("Discussions") }}
|
||||
</a>
|
||||
</li>
|
||||
{% if settings.show_discussions %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#discussions">
|
||||
{{ _("Discussions") }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#live-class">
|
||||
{{ _("Live Class") }}
|
||||
<span class="course-list-count">
|
||||
{{ live_classes | length }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{% if settings.show_live_class %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#live-class">
|
||||
{{ _("Live Class") }}
|
||||
<span class="course-list-count">
|
||||
{{ live_classes | length }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if custom_tabs_header %}
|
||||
@@ -168,15 +183,17 @@
|
||||
|
||||
<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">
|
||||
{{ Dashboard(batch_info, batch_courses, current_student) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if settings.show_courses %}
|
||||
<div class="tab-pane {% if not is_student %} active {% endif %}" id="courses" role="tabpanel" aria-labelledby="courses">
|
||||
{{ CoursesSection(batch_info, batch_courses) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if show_timetable %}
|
||||
<div class="tab-pane" id="timetable" role="tabpanel" aria-labelledby="timetable">
|
||||
@@ -185,23 +202,37 @@
|
||||
{% endif %}
|
||||
|
||||
{% if is_moderator %}
|
||||
<div class="tab-pane" id="students" role="tabpanel" aria-labelledby="students">
|
||||
{{ StudentsSection(batch_info, batch_students) }}
|
||||
</div>
|
||||
{% if settings.show_students %}
|
||||
<div class="tab-pane" id="students" role="tabpanel" aria-labelledby="students">
|
||||
{{ StudentsSection(batch_info, batch_students) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="tab-pane" id="assessments" role="tabpanel" aria-labelledby="assessments">
|
||||
{{ AssessmentsSection(batch_info) }}
|
||||
</div>
|
||||
{% if settings.show_assessments %}
|
||||
<div class="tab-pane" id="assessments" role="tabpanel" aria-labelledby="assessments">
|
||||
{{ AssessmentsSection(batch_info) }}
|
||||
</div>
|
||||
{% 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) %}
|
||||
<div class="tab-pane" id="discussions" role="tabpanel" aria-labelledby="discussions">
|
||||
{{ Discussions(batch_info) }}
|
||||
</div>
|
||||
{% if settings.show_discussions %}
|
||||
<div class="tab-pane" id="discussions" role="tabpanel" aria-labelledby="discussions">
|
||||
{{ Discussions(batch_info) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="tab-pane" id="live-class" role="tabpanel" aria-labelledby="live-class">
|
||||
{{ LiveClassSection(batch_info, live_classes) }}
|
||||
</div>
|
||||
{% if settings.show_live_class %}
|
||||
<div class="tab-pane" id="live-class" role="tabpanel" aria-labelledby="live-class">
|
||||
{{ LiveClassSection(batch_info, live_classes) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if custom_tabs_content %}
|
||||
@@ -376,6 +407,41 @@
|
||||
</article>
|
||||
{% 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) %}
|
||||
{% if assessments | length %}
|
||||
<div class="form-grid">
|
||||
@@ -575,7 +641,10 @@
|
||||
frappe.boot.single_types = []
|
||||
let courses = {{ course_list | 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>
|
||||
|
||||
<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: " ",
|
||||
only_select: 1,
|
||||
change: () => {
|
||||
this.eval_form.set_value("date", "");
|
||||
$("[data-fieldname='slots']").html("");
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldtype: "Date",
|
||||
@@ -526,8 +530,11 @@ const open_evaluation_form = (e) => {
|
||||
min_date: new Date(
|
||||
frappe.datetime.add_days(frappe.datetime.get_today(), 1)
|
||||
),
|
||||
max_date: evaluation_end_date
|
||||
? new Date(evaluation_end_date)
|
||||
: "",
|
||||
change: () => {
|
||||
get_slots();
|
||||
if (this.eval_form.get_value("date")) get_slots();
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -552,7 +559,7 @@ const get_slots = () => {
|
||||
args: {
|
||||
course: this.eval_form.get_value("course"),
|
||||
date: this.eval_form.get_value("date"),
|
||||
batch_name: $(".class-details").data("batch"),
|
||||
batch: $(".class-details").data("batch"),
|
||||
},
|
||||
callback: (r) => {
|
||||
if (r.message) {
|
||||
@@ -653,7 +660,8 @@ const setup_calendar = (events) => {
|
||||
const options = get_calendar_options(element, calendar_id);
|
||||
const calendar = new Calendar(container, options);
|
||||
this.calendar_ = calendar;
|
||||
create_events(calendar, events);
|
||||
|
||||
create_events(calendar, events, calendar_id);
|
||||
add_links_to_events(calendar, events);
|
||||
scroll_to_date(calendar, events);
|
||||
set_calendar_range(calendar, events);
|
||||
@@ -664,7 +672,7 @@ const get_calendar_options = (element, calendar_id) => {
|
||||
const end_time = element.data("end");
|
||||
|
||||
return {
|
||||
defaultView: "week",
|
||||
defaultView: $(window).width() < 768 || show_day_view ? "day" : "week",
|
||||
usageStatistics: false,
|
||||
week: {
|
||||
narrowWeekend: true,
|
||||
@@ -684,13 +692,35 @@ const get_calendar_options = (element, calendar_id) => {
|
||||
},
|
||||
],
|
||||
template: {
|
||||
time: function (event) {
|
||||
return `<div class="calendar-event-time">
|
||||
<div> ${frappe.datetime.get_time(event.start.d.d)} -
|
||||
${frappe.datetime.get_time(event.end.d.d)} </div>
|
||||
allday: 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 class="calendar-event-title"> ${event.title} </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}`,
|
||||
calendarId: calendar_id,
|
||||
title: event.title,
|
||||
start: `${event.date}T${event.start_time}`,
|
||||
end: `${event.date}T${event.end_time}`,
|
||||
start: `${event.date}T${format_time(event.start_time)}`,
|
||||
end: `${event.date}T${format_time(event.end_time)}`,
|
||||
isAllday: event.start_time ? false : true,
|
||||
category: event.start_time ? "time" : "allday",
|
||||
borderColor: clr,
|
||||
backgroundColor: "var(--fg-color)",
|
||||
customStyle: {
|
||||
@@ -716,6 +747,11 @@ const create_events = (calendar, events, calendar_id) => {
|
||||
},
|
||||
raw: {
|
||||
url: event.url,
|
||||
milestone: event.milestone,
|
||||
name: event.name,
|
||||
idx: event.idx,
|
||||
parent: event.parent,
|
||||
completed: event.completed,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -723,47 +759,92 @@ const create_events = (calendar, events, calendar_id) => {
|
||||
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) => {
|
||||
calendar.on("clickEvent", ({ event }) => {
|
||||
let event_date = event.start.d.d;
|
||||
event_date = moment(event_date).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) => {
|
||||
if (
|
||||
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));
|
||||
}
|
||||
};
|
||||
|
||||
const set_calendar_range = (calendar, events) => {
|
||||
let week_start = moment(calendar.getDateRangeStart().d.d);
|
||||
let week_end = moment(calendar.getDateRangeEnd().d.d);
|
||||
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}`);
|
||||
|
||||
$(".calendar-range").text(
|
||||
`${moment(week_start).format("DD MMMM YYYY")} - ${moment(
|
||||
week_end
|
||||
).format("DD MMMM YYYY")}`
|
||||
);
|
||||
if (moment(calendar_date).isSameOrBefore(moment(events[0].date)))
|
||||
$("#prev-week").hide();
|
||||
else $("#prev-week").show();
|
||||
|
||||
if (week_start.diff(moment(events[0].date), "days") <= 0) {
|
||||
$("#prev-week").hide();
|
||||
if (
|
||||
moment(calendar_date).isSameOrAfter(
|
||||
moment(events.slice(-1)[0].date)
|
||||
)
|
||||
)
|
||||
$("#next-week").hide();
|
||||
else $("#next-week").show();
|
||||
} else {
|
||||
$("#prev-week").show();
|
||||
}
|
||||
let week_start = moment(calendar.getDateRangeStart().d.d);
|
||||
let week_end = moment(calendar.getDateRangeEnd().d.d);
|
||||
|
||||
if (week_end.diff(moment(events.slice(-1)[0].date), "days") > 0) {
|
||||
$("#next-week").hide();
|
||||
} else {
|
||||
$("#next-week").show();
|
||||
$(".calendar-range").text(
|
||||
`${moment(week_start).format("DD MMMM YYYY")} - ${moment(
|
||||
week_end
|
||||
).format("DD MMMM YYYY")}`
|
||||
);
|
||||
|
||||
if (week_start.diff(moment(events[0].date), "days") <= 0)
|
||||
$("#prev-week").hide();
|
||||
else $("#prev-week").show();
|
||||
|
||||
if (week_end.diff(moment(events.slice(-1)[0].date), "days") > 0)
|
||||
$("#next-week").hide();
|
||||
else $("#next-week").show();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -808,12 +889,33 @@ const email_to_students = () => {
|
||||
|
||||
const send_email = (values) => {
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_batch.lms_batch.send_email_to_students",
|
||||
method: "frappe.client.get_list",
|
||||
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,
|
||||
reply_to: values.reply_to,
|
||||
message: values.message,
|
||||
content: values.message,
|
||||
doctype: "LMS Batch",
|
||||
name: $(".class-details").data("batch"),
|
||||
send_email: 1,
|
||||
},
|
||||
callback: (r) => {
|
||||
this.email_dialog.hide();
|
||||
@@ -821,6 +923,9 @@ const send_email = (values) => {
|
||||
message: __("Email sent successfully"),
|
||||
indicator: "green",
|
||||
});
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -43,6 +43,8 @@ def get_context(context):
|
||||
"batch_details",
|
||||
"published",
|
||||
"allow_future",
|
||||
"evaluation_end_date",
|
||||
"meta_image",
|
||||
],
|
||||
as_dict=True,
|
||||
)
|
||||
@@ -71,6 +73,13 @@ def get_context(context):
|
||||
)
|
||||
context.course_name_list = [course.course for course in context.batch_courses]
|
||||
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(
|
||||
batch_students, batch_courses, context.assessments
|
||||
)
|
||||
@@ -98,9 +107,9 @@ def get_context(context):
|
||||
},
|
||||
)
|
||||
context.legends = get_legends(batch_name)
|
||||
context.settings = frappe.get_single("LMS Settings")
|
||||
|
||||
custom_tabs = frappe.get_hooks("lms_batch_tabs")
|
||||
|
||||
if custom_tabs:
|
||||
context.custom_tabs_header = custom_tabs.get("header_html")[0]
|
||||
context.custom_tabs_content = custom_tabs.get("content_html")[0]
|
||||
@@ -148,7 +157,6 @@ def get_class_course_details(batch_courses):
|
||||
"image",
|
||||
"upcoming",
|
||||
"short_introduction",
|
||||
"image",
|
||||
"paid_course",
|
||||
"course_price",
|
||||
"enable_certification",
|
||||
|
||||
@@ -1,238 +1,214 @@
|
||||
{% extends "lms/templates/lms_base.html" %}
|
||||
{% block title %}
|
||||
{{ _(batch_info.title) }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block page_content %}
|
||||
{% extends "lms/templates/lms_base.html" %} {% block title %} {{
|
||||
_(batch_info.title) }} {% endblock %} {% block page_content %}
|
||||
<div class="common-page-style lms-page-style">
|
||||
{{ BatchHeader(batch_info) }}
|
||||
{{ BatchHeader(batch_info) }}
|
||||
<div class="container">
|
||||
{{ BatchOverlay(batch_info, courses, students) }}
|
||||
<div class="pt-10">
|
||||
{{ BatchDetails(batch_info) }}
|
||||
{{ CourseList(courses) }}
|
||||
</div>
|
||||
{{ BatchOverlay(batch_info, courses, students) }}
|
||||
<div class="pt-10">
|
||||
{{ BatchDetails(batch_info) }} {{ CourseList(courses) }}
|
||||
</div>
|
||||
</div>
|
||||
{{ BatchDetailsRaw() }}
|
||||
{{ BatchDetailsRaw() }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% macro BatchHeader(batch_info) %}
|
||||
{% endblock %} {% macro BatchHeader(batch_info) %}
|
||||
<div class="course-head-container">
|
||||
<div class="container">
|
||||
<div class="course-card-wide">
|
||||
{{ BreadCrumb(batch_info) }}
|
||||
{{ BatchHeaderDetails(batch_info, courses, students) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="course-card-wide">
|
||||
{{ BreadCrumb(batch_info) }} {{ BatchHeaderDetails(batch_info,
|
||||
courses, students) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro BreadCrumb(batch_info) %}
|
||||
{% endmacro %} {% macro BreadCrumb(batch_info) %}
|
||||
<article class="mb-8">
|
||||
<a class="dark-links" href="/batches">
|
||||
{{ _("All Batches") }}
|
||||
</a>
|
||||
<img class="" src="/assets/lms/icons/chevron-right.svg">
|
||||
<span class="breadcrumb-destination">
|
||||
{{ _("Batch Details") }}
|
||||
</span>
|
||||
<a class="dark-links" href="/batches"> {{ _("All Batches") }} </a>
|
||||
<img class="" src="/assets/lms/icons/chevron-right.svg" />
|
||||
<span class="breadcrumb-destination"> {{ _("Batch Details") }} </span>
|
||||
</article>
|
||||
{% endmacro %}
|
||||
{% endmacro %} {% macro BatchHeaderDetails(batch_info, courses, students) %}
|
||||
<div class="class-details" data-batch="{{ batch_info.name }}">
|
||||
<div class="page-title">{{ batch_info.title }}</div>
|
||||
|
||||
{% macro BatchHeaderDetails(batch_info, courses, students) %}
|
||||
<div class="class-details" data-batch="{{ batch_info.name }}">
|
||||
<div class="">{{ batch_info.description }}</div>
|
||||
|
||||
<div class="page-title">
|
||||
{{ batch_info.title }}
|
||||
</div>
|
||||
<div class="mt-8">
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-calendar"></use>
|
||||
</svg>
|
||||
<span>
|
||||
{{ frappe.utils.format_date(batch_info.start_date, "long") }}
|
||||
</span>
|
||||
{% if batch_info.start_date != batch_info.end_date %}
|
||||
<span>
|
||||
- {{ frappe.utils.format_date(batch_info.end_date, "long") }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="">
|
||||
{{ batch_info.description }}
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-calendar"></use>
|
||||
</svg>
|
||||
<span>
|
||||
{{ frappe.utils.format_date(batch_info.start_date, "long") }} -
|
||||
</span>
|
||||
<span>
|
||||
{{ frappe.utils.format_date(batch_info.end_date, "long") }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{% if batch_info.start_time and batch_info.end_time %}
|
||||
<div class="mt-1">
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-clock"></use>
|
||||
</svg>
|
||||
<span>
|
||||
{{ frappe.utils.format_time(batch_info.start_time, "hh:mm a") }} -
|
||||
</span>
|
||||
<span>
|
||||
{{ frappe.utils.format_time(batch_info.end_time, "hh:mm a") }}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro BatchOverlay(batch_info, courses, students) %}
|
||||
{% if batch_info.start_time and batch_info.end_time %}
|
||||
<div class="mt-1">
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-clock"></use>
|
||||
</svg>
|
||||
<span>
|
||||
{{ frappe.utils.format_time(batch_info.start_time, "hh:mm a") }} -
|
||||
</span>
|
||||
<span>
|
||||
{{ frappe.utils.format_time(batch_info.end_time, "hh:mm a") }}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %} {% macro BatchOverlay(batch_info, courses, students) %}
|
||||
<div class="course-overlay-card class-overlay">
|
||||
<div class="course-overlay-content">
|
||||
{% if batch_info.seat_count %} {% if seats_left %}
|
||||
<div class="indicator-pill green pull-right">
|
||||
{{ _("Seats Available") }}: {{ seats_left }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="indicator-pill red pull-right">
|
||||
{{ _("No seats left") }}
|
||||
</div>
|
||||
{% endif %} {% endif %} {% if batch_info.paid_batch %}
|
||||
<div class="bold-heading">
|
||||
{{ frappe.utils.fmt_money(batch_info.amount, 0, batch_info.currency)
|
||||
}}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="course-overlay-content">
|
||||
<div class="vertically-center mt-2">
|
||||
<svg class="icon icon-md mr-1">
|
||||
<use href="#icon-education"></use>
|
||||
</svg>
|
||||
{{ courses | length }} {{ _("Courses") }}
|
||||
</div>
|
||||
|
||||
{% if batch_info.seat_count %}
|
||||
{% if seats_left %}
|
||||
<div class="indicator-pill green pull-right">
|
||||
{{ _("Seats Available") }}: {{ seats_left }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="indicator-pill red pull-right">
|
||||
{{ _("No seats left") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div class="mt-2">
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-calendar"></use>
|
||||
</svg>
|
||||
<span>
|
||||
{{ frappe.utils.format_date(batch_info.start_date, "long") }}
|
||||
</span>
|
||||
{% if batch_info.start_date != batch_info.end_date %}
|
||||
<span>
|
||||
- {{ frappe.utils.format_date(batch_info.end_date, "long") }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if batch_info.paid_batch %}
|
||||
<div class="bold-heading">
|
||||
{{ frappe.utils.fmt_money(batch_info.amount, 0, batch_info.currency) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if batch_info.start_time and batch_info.end_time %}
|
||||
<div class="mt-2">
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-clock"></use>
|
||||
</svg>
|
||||
<span>
|
||||
{{ frappe.utils.format_time(batch_info.start_time, "hh:mm a") }}
|
||||
-
|
||||
</span>
|
||||
<span>
|
||||
{{ frappe.utils.format_time(batch_info.end_time, "hh:mm a") }}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="vertically-center mt-2">
|
||||
<svg class="icon icon-md mr-1">
|
||||
<use href="#icon-education"></use>
|
||||
</svg>
|
||||
{{ courses | length }} {{ _("Courses") }}
|
||||
</div>
|
||||
|
||||
<div class="mt-2">
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-calendar"></use>
|
||||
</svg>
|
||||
<span>
|
||||
{{ frappe.utils.format_date(batch_info.start_date, "long") }} -
|
||||
</span>
|
||||
<span>
|
||||
{{ frappe.utils.format_date(batch_info.end_date, "long") }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{% if batch_info.start_time and batch_info.end_time %}
|
||||
<div class="mt-2">
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-clock"></use>
|
||||
</svg>
|
||||
<span>
|
||||
{{ frappe.utils.format_time(batch_info.start_time, "hh:mm a") }} -
|
||||
</span>
|
||||
<span>
|
||||
{{ frappe.utils.format_time(batch_info.end_time, "hh:mm a") }}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-2">
|
||||
{% if is_moderator or is_evaluator %}
|
||||
<a class="btn btn-primary wide-button" href="/batches/{{ batch_info.name }}">
|
||||
{{ _("Manage Batch") }}
|
||||
</a>
|
||||
{% elif batch_info.paid_batch %}
|
||||
<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") }}
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
{{ _("To join this batch, please contact the Administrator.") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if is_moderator %}
|
||||
<div class="mt-2">
|
||||
<div class="btn btn-secondary wide-button" id="create-batch">
|
||||
{{ _("Edit") }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
{% if is_moderator or is_evaluator %}
|
||||
<a
|
||||
class="btn btn-primary wide-button"
|
||||
href="/batches/{{ batch_info.name }}"
|
||||
>
|
||||
{{ _("Manage Batch") }}
|
||||
</a>
|
||||
{% elif batch_info.paid_batch and batch_info.start_date >
|
||||
frappe.utils.getdate() %}
|
||||
<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") }}
|
||||
</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 %}
|
||||
<div class="alert alert-info">
|
||||
{{ _("To join this batch, please contact the Administrator.") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if is_moderator %}
|
||||
<div class="mt-2">
|
||||
<div class="btn btn-secondary wide-button" id="create-batch">
|
||||
{{ _("Edit") }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro BatchDetails(batch_info) %}
|
||||
<div class="batch-details">
|
||||
{{ batch_info.batch_details }}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro CourseList(courses) %}
|
||||
{% if courses | length or is_moderator %}
|
||||
{% endmacro %} {% macro BatchDetails(batch_info) %}
|
||||
<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="align-center flex">
|
||||
<div class="page-title">{{ _("Courses") }}</div>
|
||||
{% if is_moderator %}
|
||||
<button class="btn btn-default btn-sm btn-add-course ml-4">
|
||||
{{ _("Add Course") }}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="flex align-center">
|
||||
<div class="page-title">
|
||||
{{ _("Courses") }}
|
||||
</div>
|
||||
{% if is_moderator %}
|
||||
<button class="btn btn-default btn-sm btn-add-course ml-4">
|
||||
{{ _("Add Course") }}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if courses | length %}
|
||||
{% if courses | length %}
|
||||
<div class="cards-parent mt-2">
|
||||
{% for course in courses %}
|
||||
<div class="h-100">
|
||||
{% if is_moderator %}
|
||||
<div class="card-buttons">
|
||||
<button class="btn icon-btn btn-default btn-edit-course"
|
||||
data-name="{{ course.batch_course }}" data-course="{{ course.name }}"
|
||||
{% if course.evaluator %} data-evaluator="{{ course.evaluator }}" {% endif %}>
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-edit"></use>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn icon-btn btn-default btn-remove-course ml-2" data-course="{{ course.name }}">
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-delete"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{{ widgets.CourseCard(course=course, read_only=False) }}
|
||||
{% if is_moderator %}
|
||||
<div class="card-buttons">
|
||||
<button
|
||||
class="btn icon-btn btn-default btn-edit-course"
|
||||
data-name="{{ course.batch_course }}"
|
||||
data-course="{{ course.name }}"
|
||||
{%
|
||||
if
|
||||
course.evaluator
|
||||
%}
|
||||
data-evaluator="{{ course.evaluator }}"
|
||||
{%
|
||||
endif
|
||||
%}
|
||||
>
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-edit"></use>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="btn icon-btn btn-default btn-remove-course ml-2"
|
||||
data-course="{{ course.name }}"
|
||||
>
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-delete"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %} {{ widgets.CourseCard(course=course, read_only=False) }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="">
|
||||
{{ _("No courses") }}
|
||||
</div>
|
||||
<div class="">{{ _("No courses") }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% 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>
|
||||
let batch_info = {{ batch_info | json }};
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% endif %} {% endmacro %} {% 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>
|
||||
let batch_info = {{ batch_info | json }};
|
||||
</script>
|
||||
{% endif %} {% endblock %}
|
||||
|
||||
@@ -12,6 +12,10 @@ frappe.ready(() => {
|
||||
$(".btn-remove-course").click((e) => {
|
||||
remove_course(e);
|
||||
});
|
||||
|
||||
$(".enroll-batch").click((e) => {
|
||||
enroll_batch(e);
|
||||
});
|
||||
});
|
||||
|
||||
const show_course_modal = (e) => {
|
||||
@@ -54,6 +58,30 @@ const show_course_modal = (e) => {
|
||||
}, 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) => {
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_batch.lms_batch.add_course",
|
||||
|
||||
@@ -33,13 +33,19 @@ def get_context(context):
|
||||
"published",
|
||||
"meta_image",
|
||||
"batch_details_raw",
|
||||
"evaluation_end_date",
|
||||
"amount_usd",
|
||||
"allow_self_enrollment",
|
||||
],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if context.batch_info.amount and context.batch_info.currency:
|
||||
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.currency = currency
|
||||
|
||||
@@ -140,10 +140,24 @@
|
||||
<use href="#icon-calendar"></use>
|
||||
</svg>
|
||||
<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>
|
||||
{{ frappe.utils.format_date(batch.end_date, "medium") }}
|
||||
{{ frappe.utils.format_time(batch.end_time, "HH:mm a") }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import frappe
|
||||
from frappe.utils import getdate
|
||||
from frappe.utils import getdate, get_time_str, nowtime
|
||||
from lms.lms.utils import (
|
||||
has_course_moderator_role,
|
||||
has_course_evaluator_role,
|
||||
@@ -19,11 +19,14 @@ def get_context(context):
|
||||
"description",
|
||||
"start_date",
|
||||
"end_date",
|
||||
"start_time",
|
||||
"end_time",
|
||||
"paid_batch",
|
||||
"amount",
|
||||
"currency",
|
||||
"seat_count",
|
||||
"published",
|
||||
"amount_usd",
|
||||
],
|
||||
order_by="start_date",
|
||||
)
|
||||
@@ -34,7 +37,9 @@ def get_context(context):
|
||||
batch.course_count = frappe.db.count("Batch Course", {"parent": batch.name})
|
||||
|
||||
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.currency = currency
|
||||
|
||||
@@ -43,12 +48,16 @@ def get_context(context):
|
||||
)
|
||||
if not batch.published:
|
||||
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)
|
||||
else:
|
||||
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.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
|
||||
|
||||
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
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<div class="flex">
|
||||
<div class="field-label">
|
||||
{{ _("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>
|
||||
{% if gst_applied %}
|
||||
|
||||
@@ -40,15 +40,15 @@ const setup_billing = () => {
|
||||
reqd: 1,
|
||||
default: address && address.city,
|
||||
},
|
||||
{
|
||||
fieldtype: "Column Break",
|
||||
},
|
||||
{
|
||||
fieldtype: "Data",
|
||||
label: __("State/Province"),
|
||||
fieldname: "state",
|
||||
default: address && address.state,
|
||||
},
|
||||
{
|
||||
fieldtype: "Column Break",
|
||||
},
|
||||
{
|
||||
fieldtype: "Link",
|
||||
label: __("Country"),
|
||||
@@ -75,6 +75,14 @@ const setup_billing = () => {
|
||||
reqd: 1,
|
||||
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",
|
||||
label: __("GST Details"),
|
||||
@@ -106,6 +114,7 @@ const setup_billing = () => {
|
||||
|
||||
const generate_payment_link = (e) => {
|
||||
let new_address = this.billing.get_values();
|
||||
validate_address(new_address);
|
||||
let doctype = $(e.currentTarget).attr("data-doctype");
|
||||
let docname = decodeURIComponent($(e.currentTarget).attr("data-name"));
|
||||
|
||||
@@ -174,8 +183,10 @@ const change_currency = () => {
|
||||
if (current_price != data.message) {
|
||||
update_price(data.message);
|
||||
}
|
||||
if (!data.message.includes("INR")) {
|
||||
$("#gst-message").addClass("hide");
|
||||
if (data.message.includes("INR")) {
|
||||
$("#gst-message").removeClass("hide").addClass("show");
|
||||
} else {
|
||||
$("#gst-message").removeClass("show").addClass("hide");
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -188,3 +199,48 @@ const update_price = (price) => {
|
||||
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)
|
||||
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(
|
||||
"Payment Country", filters={"parent": "LMS Settings"}, pluck="country"
|
||||
)
|
||||
|
||||
context.amount, context.currency = check_multicurrency(
|
||||
context.amount, context.currency
|
||||
context.amount, context.currency, None, context.amount_usd
|
||||
)
|
||||
|
||||
context.address = get_address()
|
||||
if context.currency == "INR":
|
||||
context.amount, context.gst_applied = apply_gst(context.amount, None)
|
||||
|
||||
context.original_amount = context.amount
|
||||
context.original_currency = context.currency
|
||||
context.amount_with_gst, context.gst_applied = apply_gst(context.amount, None)
|
||||
|
||||
|
||||
def validate_access(doctype, docname, module):
|
||||
@@ -61,7 +63,7 @@ def get_billing_details(context):
|
||||
details = frappe.db.get_value(
|
||||
"LMS Course",
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -72,7 +74,7 @@ def get_billing_details(context):
|
||||
details = frappe.db.get_value(
|
||||
"LMS Batch",
|
||||
context.docname,
|
||||
["title", "name", "paid_batch", "amount", "currency"],
|
||||
["title", "name", "paid_batch", "amount", "currency", "amount_usd"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
@@ -84,6 +86,7 @@ def get_billing_details(context):
|
||||
context.title = details.title
|
||||
context.amount = details.amount
|
||||
context.currency = details.currency
|
||||
context.amount_usd = details.amount_usd
|
||||
|
||||
|
||||
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