Merge branch 'main' of https://github.com/frappe/lms into lms-frappe-ui
This commit is contained in:
2
.github/helper/install.sh
vendored
2
.github/helper/install.sh
vendored
@@ -43,4 +43,4 @@ build_pid=$!
|
||||
bench --site lms.test reinstall --yes
|
||||
bench --site lms.test install-app lms
|
||||
|
||||
wait $build_pid
|
||||
wait $build_pid
|
||||
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -77,5 +77,4 @@ jobs:
|
||||
run: bench --site frappe.local build
|
||||
- name: run tests
|
||||
working-directory: /home/runner/frappe-bench
|
||||
run: bench --site frappe.local run-tests --app lms
|
||||
|
||||
run: bench --site frappe.local run-tests --app lms
|
||||
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",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -33,19 +33,17 @@ describe("Course Creation", () => {
|
||||
cy.get("#lesson-title").type("Test Lesson");
|
||||
|
||||
// Content
|
||||
cy.get(".ce-block").click().type("{enter}");
|
||||
cy.get(".ce-toolbar__plus").click();
|
||||
cy.get('[data-item-name="youtube"]').click();
|
||||
cy.get(".collapse-section.collapsed:first").click();
|
||||
cy.get("#lesson-content .ce-block")
|
||||
.click()
|
||||
.type(
|
||||
"This is an extremely big paragraph that is meant to test the UI. This is a very long paragraph. It contains more than once sentence. Its meant to be this long as this is a UI test. Its unbearably long and I'm not sure why I'm typing this much. I'm just going to keep typing until I feel like its long enough. I think its long enough now. I'm going to stop typing now. {enter}"
|
||||
);
|
||||
cy.get("#lesson-content .ce-toolbar__plus").click();
|
||||
cy.get('#lesson-content [data-item-name="youtube"]').click();
|
||||
cy.get('input[data-fieldname="youtube"]').type("GoDtyItReto");
|
||||
cy.button("Insert").click();
|
||||
cy.wait(1000);
|
||||
|
||||
cy.get(".ce-block:last").click().type("{enter}");
|
||||
cy.get(".ce-block:last")
|
||||
.click()
|
||||
.type(
|
||||
"This is an extremely big paragraph that is meant to test the UI. This is a very long paragraph. It contains more than once sentence. Its meant to be this long as this is a UI test. Its unbearably long and I'm not sure why I'm typing this much. I'm just going to keep typing until I feel like its long enough. I think its long enough now. I'm going to stop typing now."
|
||||
);
|
||||
cy.button("Save").click();
|
||||
|
||||
// View Course
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -98,7 +98,6 @@ override_doctype_class = {
|
||||
|
||||
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"},
|
||||
}
|
||||
|
||||
# Scheduled Tasks
|
||||
@@ -304,6 +303,9 @@ lms_markdown_macro_renderers = {
|
||||
"YouTubeVideo": "lms.plugins.youtube_video_renderer",
|
||||
"Video": "lms.plugins.video_renderer",
|
||||
"Assignment": "lms.plugins.assignment_renderer",
|
||||
"Embed": "lms.plugins.embed_renderer",
|
||||
"Audio": "lms.plugins.audio_renderer",
|
||||
"PDF": "lms.plugins.pdf_renderer",
|
||||
}
|
||||
|
||||
# page_renderer to manage profile pages
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -54,6 +54,7 @@ def create_lms_roles():
|
||||
create_course_creator_role()
|
||||
create_moderator_role()
|
||||
create_evaluator_role()
|
||||
create_lms_student_role()
|
||||
|
||||
|
||||
def delete_lms_roles():
|
||||
@@ -63,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(
|
||||
@@ -106,6 +103,19 @@ def create_evaluator_role():
|
||||
role.save()
|
||||
|
||||
|
||||
def create_lms_student_role():
|
||||
if not frappe.db.exists("Role", "LMS Student"):
|
||||
role = frappe.new_doc("Role")
|
||||
role.update(
|
||||
{
|
||||
"role_name": "LMS Student",
|
||||
"home_page": "",
|
||||
"desk_access": 0,
|
||||
}
|
||||
)
|
||||
role.save()
|
||||
|
||||
|
||||
def set_default_certificate_print_format():
|
||||
filters = {
|
||||
"doc_type": "LMS Certificate",
|
||||
@@ -168,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()
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2022-09-15 17:22:21.662675",
|
||||
"modified": "2023-09-29 17:03:30.825021",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Job",
|
||||
"name": "Job Opportunity",
|
||||
@@ -144,7 +144,7 @@
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "All",
|
||||
"role": "LMS Student",
|
||||
"select": 1,
|
||||
"share": 1,
|
||||
"write": 1
|
||||
|
||||
@@ -40,7 +40,7 @@ def save_current_lesson(course_name, lesson_name):
|
||||
return
|
||||
doc = frappe.get_doc("LMS Enrollment", name)
|
||||
doc.current_lesson = lesson_name
|
||||
doc.save(ignore_permissions=True)
|
||||
doc.save()
|
||||
return {"current_lesson": doc.current_lesson}
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ def join_cohort(course, cohort, subgroup, invite_code):
|
||||
return {"ok": True, "status": "record found"}
|
||||
else:
|
||||
doc = frappe.get_doc(data)
|
||||
doc.insert(ignore_permissions=True)
|
||||
doc.insert()
|
||||
return {"ok": True, "status": "record created"}
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ def approve_cohort_join_request(join_request):
|
||||
return {"ok": False, "error": "Permission Deined"}
|
||||
|
||||
r.status = "Accepted"
|
||||
r.save(ignore_permissions=True)
|
||||
r.save()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ def reject_cohort_join_request(join_request):
|
||||
return {"ok": False, "error": "Permission Deined"}
|
||||
|
||||
r.status = "Rejected"
|
||||
r.save(ignore_permissions=True)
|
||||
r.save()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ def undo_reject_cohort_join_request(join_request):
|
||||
return {"ok": False, "error": "Permission Deined"}
|
||||
|
||||
r.status = "Pending"
|
||||
r.save(ignore_permissions=True)
|
||||
r.save()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
|
||||
@@ -9,10 +9,12 @@
|
||||
"field_order": [
|
||||
"student_details_section",
|
||||
"student",
|
||||
"payment",
|
||||
"column_break_oduu",
|
||||
"student_name",
|
||||
"username"
|
||||
"username",
|
||||
"column_break_oduu",
|
||||
"payment",
|
||||
"source",
|
||||
"confirmation_email_sent"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -52,12 +54,24 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Payment",
|
||||
"options": "LMS Payment"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"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-08-24 17:48:53.045539",
|
||||
"modified": "2023-10-26 16:52:04.266693",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Batch Student",
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-12-16 15:06:03.985221",
|
||||
"modified": "2023-09-29 17:08:18.950560",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Cohort Join Request",
|
||||
@@ -68,9 +68,21 @@
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "LMS Student",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -59,7 +59,7 @@
|
||||
"link_fieldname": "chapter"
|
||||
}
|
||||
],
|
||||
"modified": "2022-03-14 17:57:00.707416",
|
||||
"modified": "2023-09-29 17:03:58.013819",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Course Chapter",
|
||||
@@ -86,7 +86,7 @@
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "All",
|
||||
"role": "LMS Student",
|
||||
"select": 1,
|
||||
"share": 1,
|
||||
"write": 1
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"file_type",
|
||||
"section_break_11",
|
||||
"body",
|
||||
"instructor_notes",
|
||||
"help_section",
|
||||
"help"
|
||||
],
|
||||
@@ -131,11 +132,16 @@
|
||||
{
|
||||
"fieldname": "column_break_15",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "instructor_notes",
|
||||
"fieldtype": "Markdown Editor",
|
||||
"label": "Instructor Notes"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-05-02 12:42:16.926753",
|
||||
"modified": "2023-09-29 17:04:19.252897",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Course Lesson",
|
||||
@@ -163,7 +169,7 @@
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "All",
|
||||
"role": "LMS Student",
|
||||
"select": 1,
|
||||
"share": 1,
|
||||
"write": 1
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-12-21 09:34:35.018280",
|
||||
"modified": "2023-09-29 17:04:58.167481",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Function",
|
||||
@@ -44,11 +44,12 @@
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "All",
|
||||
"role": "LMS Student",
|
||||
"select": 1,
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -19,7 +19,7 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-12-21 09:35:20.443192",
|
||||
"modified": "2023-09-29 17:05:27.231982",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Industry",
|
||||
@@ -44,11 +44,12 @@
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "All",
|
||||
"role": "LMS Student",
|
||||
"select": 1,
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -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):
|
||||
|
||||
@@ -9,10 +9,12 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"title",
|
||||
"grade_assignment",
|
||||
"question",
|
||||
"column_break_hmwv",
|
||||
"type",
|
||||
"section_break_lwvt",
|
||||
"question"
|
||||
"show_answer",
|
||||
"answer"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -26,7 +28,7 @@
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Type",
|
||||
"options": "Document\nPDF\nURL\nImage"
|
||||
"options": "Document\nPDF\nURL\nImage\nText"
|
||||
},
|
||||
{
|
||||
"fieldname": "title",
|
||||
@@ -40,13 +42,29 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_lwvt",
|
||||
"fieldtype": "Section Break"
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.type == \"Text\"",
|
||||
"fieldname": "show_answer",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Answer"
|
||||
},
|
||||
{
|
||||
"depends_on": "show_answer",
|
||||
"fieldname": "answer",
|
||||
"fieldtype": "Text Editor",
|
||||
"label": "Answer"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "eval:doc.type == \"Text\"",
|
||||
"fieldname": "grade_assignment",
|
||||
"fieldtype": "Check",
|
||||
"label": "Grade Assignment"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-06-26 18:09:29.809564",
|
||||
"modified": "2023-10-06 12:08:46.898950",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Assignment",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from lms.lms.utils import can_create_courses
|
||||
from lms.lms.utils import has_course_moderator_role, has_course_instructor_role
|
||||
|
||||
|
||||
class LMSAssignment(Document):
|
||||
@@ -12,7 +12,7 @@ class LMSAssignment(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def save_assignment(assignment, title, type, question):
|
||||
if not can_create_courses():
|
||||
if not has_course_moderator_role() or not has_course_instructor_role():
|
||||
return
|
||||
|
||||
if assignment:
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -9,29 +9,29 @@
|
||||
"field_order": [
|
||||
"assignment",
|
||||
"assignment_title",
|
||||
"question",
|
||||
"type",
|
||||
"column_break_3",
|
||||
"member",
|
||||
"member_name",
|
||||
"type",
|
||||
"section_break_dlzh",
|
||||
"question",
|
||||
"column_break_zvis",
|
||||
"assignment_attachment",
|
||||
"answer",
|
||||
"section_break_rqal",
|
||||
"status",
|
||||
"evaluator",
|
||||
"column_break_esgd",
|
||||
"comments",
|
||||
"section_break_cwaw",
|
||||
"lesson",
|
||||
"course",
|
||||
"column_break_ygdu",
|
||||
"evaluator"
|
||||
"column_break_ygdu"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "lesson",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Lesson",
|
||||
"options": "Course Lesson"
|
||||
},
|
||||
@@ -78,7 +78,7 @@
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"label": "Status",
|
||||
"options": "Pass\nFail\nNot Graded"
|
||||
"options": "Pass\nFail\nNot Graded\nNot Applicable"
|
||||
},
|
||||
{
|
||||
"fieldname": "comments",
|
||||
@@ -94,7 +94,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.type != \"URL\";",
|
||||
"depends_on": "eval:!([\"URL\", \"Text\"]).includes(doc.type);",
|
||||
"fieldname": "assignment_attachment",
|
||||
"fieldtype": "Attach",
|
||||
"label": "Assignment Attachment",
|
||||
@@ -104,8 +104,9 @@
|
||||
"fetch_from": "assignment.type",
|
||||
"fieldname": "type",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Type",
|
||||
"options": "Document\nPDF\nURL\nImage"
|
||||
"options": "Document\nPDF\nURL\nImage\nText"
|
||||
},
|
||||
{
|
||||
"fetch_from": "assignment.question",
|
||||
@@ -137,17 +138,25 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.type == \"URL\";",
|
||||
"depends_on": "eval:([\"URL\", \"Text\"]).includes(doc.type);",
|
||||
"fieldname": "answer",
|
||||
"fieldtype": "Long Text",
|
||||
"fieldtype": "Text Editor",
|
||||
"label": "Answer",
|
||||
"mandatory_depends_on": "eval:doc.type == \"URL\";"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_dlzh",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_zvis",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2023-08-30 12:09:03.332820",
|
||||
"modified": "2023-10-06 15:14:55.984714",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Assignment Submission",
|
||||
@@ -181,6 +190,10 @@
|
||||
{
|
||||
"color": "Red",
|
||||
"title": "Fail"
|
||||
},
|
||||
{
|
||||
"color": "Blue",
|
||||
"title": "Not Applicable"
|
||||
}
|
||||
],
|
||||
"title_field": "assignment_title"
|
||||
|
||||
@@ -5,12 +5,17 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import validate_url
|
||||
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,35 @@ 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")
|
||||
|
||||
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(
|
||||
@@ -37,9 +71,12 @@ def upload_assignment(
|
||||
if frappe.session.user == "Guest":
|
||||
return
|
||||
|
||||
assignment_type = frappe.db.get_value("LMS Assignment", assignment, "type")
|
||||
assignment_details = frappe.db.get_value(
|
||||
"LMS Assignment", assignment, ["type", "grade_assignment"], as_dict=1
|
||||
)
|
||||
assignment_type = assignment_details.type
|
||||
|
||||
if assignment_type == "URL" and not answer:
|
||||
if assignment_type in ["URL", "Text"] and not answer:
|
||||
frappe.throw(_("Please enter the URL for assignment submission."))
|
||||
|
||||
if assignment_type == "File" and not assignment_attachment:
|
||||
@@ -64,7 +101,9 @@ def upload_assignment(
|
||||
doc.update(
|
||||
{
|
||||
"assignment_attachment": assignment_attachment,
|
||||
"status": status,
|
||||
"status": "Not Applicable"
|
||||
if assignment_type == "Text" and not assignment_details.grade_assignment
|
||||
else status,
|
||||
"comments": comments,
|
||||
"answer": answer,
|
||||
}
|
||||
|
||||
@@ -10,25 +10,116 @@ frappe.ui.form.on("LMS Batch", {
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
fetch_lessons: (frm) => {
|
||||
frm.clear_table("scheduled_flow");
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_batch.lms_batch.fetch_lessons",
|
||||
args: {
|
||||
courses: frm.doc.courses,
|
||||
},
|
||||
callback: (r) => {
|
||||
if (r.message) {
|
||||
r.message.forEach((lesson) => {
|
||||
let row = frm.add_child("scheduled_flow");
|
||||
row.lesson = lesson.name;
|
||||
row.lesson_title = lesson.title;
|
||||
});
|
||||
frm.refresh_field("scheduled_flow");
|
||||
}
|
||||
},
|
||||
frm.set_query("reference_doctype", "timetable", function () {
|
||||
let doctypes = ["Course Lesson", "LMS Quiz", "LMS Assignment"];
|
||||
return {
|
||||
filters: {
|
||||
name: ["in", doctypes],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("reference_doctype", "timetable_legends", function () {
|
||||
let doctypes = ["Course Lesson", "LMS Quiz", "LMS Assignment"];
|
||||
return {
|
||||
filters: {
|
||||
name: ["in", doctypes],
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
timetable_template: function (frm) {
|
||||
set_timetable(frm);
|
||||
},
|
||||
});
|
||||
|
||||
const set_timetable = (frm) => {
|
||||
if (frm.doc.timetable_template) {
|
||||
frm.clear_table("timetable");
|
||||
frm.refresh_fields();
|
||||
|
||||
frappe.call({
|
||||
method: "frappe.client.get_list",
|
||||
args: {
|
||||
doctype: "LMS Batch Timetable",
|
||||
parent: "LMS Timetable Template",
|
||||
fields: [
|
||||
"reference_doctype",
|
||||
"reference_docname",
|
||||
"day",
|
||||
"start_time",
|
||||
"end_time",
|
||||
"duration",
|
||||
"milestone",
|
||||
],
|
||||
filters: {
|
||||
parent: frm.doc.timetable_template,
|
||||
parenttype: "LMS Timetable Template",
|
||||
},
|
||||
order_by: "idx",
|
||||
},
|
||||
callback: (data) => {
|
||||
add_timetable_rows(frm, data.message);
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const add_timetable_rows = (frm, timetable) => {
|
||||
timetable.forEach((row) => {
|
||||
let child = frm.add_child("timetable");
|
||||
child.reference_doctype = row.reference_doctype;
|
||||
child.reference_docname = row.reference_docname;
|
||||
child.date = frappe.datetime.add_days(frm.doc.start_date, row.day - 1);
|
||||
child.start_time = row.start_time;
|
||||
child.end_time = row.end_time
|
||||
? row.end_time
|
||||
: row.duration
|
||||
? moment
|
||||
.utc(row.start_time, "HH:mm")
|
||||
.add(row.duration, "hour")
|
||||
.format("HH:mm")
|
||||
: null;
|
||||
child.duration = row.duration;
|
||||
child.milestone = row.milestone;
|
||||
});
|
||||
frm.refresh_field("timetable");
|
||||
|
||||
set_legends(frm);
|
||||
};
|
||||
|
||||
const set_legends = (frm) => {
|
||||
if (frm.doc.timetable_template) {
|
||||
frm.clear_table("timetable_legends");
|
||||
frm.refresh_fields();
|
||||
frappe.call({
|
||||
method: "frappe.client.get_list",
|
||||
args: {
|
||||
doctype: "LMS Timetable Legend",
|
||||
parent: "LMS Timetable Template",
|
||||
fields: ["reference_doctype", "label", "color"],
|
||||
filters: {
|
||||
parent: frm.doc.timetable_template,
|
||||
parenttype: "LMS Timetable Template",
|
||||
},
|
||||
order_by: "idx",
|
||||
},
|
||||
callback: (data) => {
|
||||
add_legend_rows(frm, data.message);
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const add_legend_rows = (frm, legends) => {
|
||||
legends.forEach((row) => {
|
||||
let child = frm.add_child("timetable_legends");
|
||||
child.reference_doctype = row.reference_doctype;
|
||||
child.label = row.label;
|
||||
child.color = row.color;
|
||||
});
|
||||
frm.refresh_field("timetable_legends");
|
||||
frm.save();
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"column_break_4",
|
||||
"start_time",
|
||||
"end_time",
|
||||
"published",
|
||||
"section_break_rgfj",
|
||||
"medium",
|
||||
"category",
|
||||
@@ -21,21 +22,34 @@
|
||||
"seat_count",
|
||||
"section_break_6",
|
||||
"description",
|
||||
"batch_details_raw",
|
||||
"column_break_hlqw",
|
||||
"batch_details",
|
||||
"meta_image",
|
||||
"section_break_jgji",
|
||||
"students",
|
||||
"courses",
|
||||
"assessment_tab",
|
||||
"assessment",
|
||||
"schedule_tab",
|
||||
"timetable_template",
|
||||
"column_break_anya",
|
||||
"show_live_class",
|
||||
"allow_future",
|
||||
"section_break_ontp",
|
||||
"timetable",
|
||||
"timetable_legends",
|
||||
"pricing_tab",
|
||||
"section_break_gsac",
|
||||
"paid_batch",
|
||||
"column_break_iens",
|
||||
"amount",
|
||||
"currency",
|
||||
"customisations_tab",
|
||||
"section_break_ubxi",
|
||||
"custom_component",
|
||||
"assessment_tab",
|
||||
"assessment",
|
||||
"schedule_tab",
|
||||
"fetch_lessons",
|
||||
"scheduled_flow"
|
||||
"column_break_pxgb",
|
||||
"custom_script"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -86,10 +100,9 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"description": "The HTML code entered here will be displayed on the batch details page.",
|
||||
"fieldname": "custom_component",
|
||||
"fieldtype": "Code",
|
||||
"label": "Custom Component",
|
||||
"label": "Custom HTML",
|
||||
"options": "HTML"
|
||||
},
|
||||
{
|
||||
@@ -142,33 +155,23 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "category",
|
||||
"fieldtype": "Autocomplete",
|
||||
"label": "Category"
|
||||
},
|
||||
{
|
||||
"fieldname": "scheduled_flow",
|
||||
"fieldtype": "Table",
|
||||
"label": "Scheduled Flow",
|
||||
"options": "Scheduled Flow"
|
||||
"fieldtype": "Link",
|
||||
"label": "Category",
|
||||
"options": "LMS Category"
|
||||
},
|
||||
{
|
||||
"description": "These customisations will work on the main batch page.",
|
||||
"fieldname": "section_break_ubxi",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "fetch_lessons",
|
||||
"fieldtype": "Button",
|
||||
"label": "Fetch Lessons"
|
||||
},
|
||||
{
|
||||
"fieldname": "schedule_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Schedule"
|
||||
"label": "Timetable"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_gsac",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Pricing"
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_iens",
|
||||
@@ -192,11 +195,93 @@
|
||||
"fieldtype": "Text Editor",
|
||||
"label": "Batch Details",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "published",
|
||||
"fieldtype": "Check",
|
||||
"label": "Published"
|
||||
},
|
||||
{
|
||||
"fieldname": "timetable",
|
||||
"fieldtype": "Table",
|
||||
"label": "Timetable",
|
||||
"options": "LMS Batch Timetable"
|
||||
},
|
||||
{
|
||||
"fieldname": "timetable_template",
|
||||
"fieldtype": "Link",
|
||||
"label": "Timetable Template",
|
||||
"options": "LMS Timetable Template"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_anya",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "show_live_class",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show live class"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_ontp",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "batch_details_raw",
|
||||
"fieldtype": "HTML Editor",
|
||||
"label": "Batch Details Raw"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_hlqw",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_jgji",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "meta_image",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "Meta Image"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_pxgb",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "customisations_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Customisations"
|
||||
},
|
||||
{
|
||||
"fieldname": "pricing_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Pricing"
|
||||
},
|
||||
{
|
||||
"fieldname": "custom_script",
|
||||
"fieldtype": "Code",
|
||||
"label": "Custom Script (JavaScript)",
|
||||
"options": "Javascript"
|
||||
},
|
||||
{
|
||||
"fieldname": "timetable_legends",
|
||||
"fieldtype": "Table",
|
||||
"label": "Timetable Legends",
|
||||
"options": "LMS Timetable Legend"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "allow_future",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow accessing future dates"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-23 17:35:42.750754",
|
||||
"modified": "2023-10-12 12:53:37.351989",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Batch",
|
||||
|
||||
@@ -6,9 +6,17 @@ import requests
|
||||
import base64
|
||||
import json
|
||||
from frappe import _
|
||||
from datetime import timedelta
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, format_date, format_datetime
|
||||
from lms.lms.utils import get_lessons
|
||||
from frappe.utils import (
|
||||
cint,
|
||||
format_date,
|
||||
format_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
|
||||
from frappe.email.doctype.email_template.email_template import get_email_template
|
||||
|
||||
|
||||
class LMSBatch(Document):
|
||||
@@ -19,7 +27,8 @@ class LMSBatch(Document):
|
||||
self.validate_duplicate_students()
|
||||
self.validate_duplicate_assessments()
|
||||
self.validate_membership()
|
||||
self.validate_schedule()
|
||||
self.validate_timetable()
|
||||
self.send_confirmation_mail()
|
||||
|
||||
def validate_duplicate_students(self):
|
||||
students = [row.student for row in self.students]
|
||||
@@ -53,6 +62,43 @@ 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 send_mail(self, student):
|
||||
subject = _("Enrollment Confirmation for the Next Training Batch")
|
||||
template = "batch_confirmation"
|
||||
custom_template = frappe.db.get_single_value(
|
||||
"LMS Settings", "batch_confirmation_template"
|
||||
)
|
||||
|
||||
args = {
|
||||
"student_name": student.student_name,
|
||||
"start_time": self.start_time,
|
||||
"start_date": self.start_date,
|
||||
"medium": self.medium,
|
||||
"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=student.student,
|
||||
subject=subject,
|
||||
template=template if not custom_template else None,
|
||||
content=content if custom_template else None,
|
||||
args=args,
|
||||
header=[subject, "green"],
|
||||
retry=3,
|
||||
)
|
||||
|
||||
def validate_membership(self):
|
||||
for course in self.courses:
|
||||
for student in self.students:
|
||||
@@ -68,26 +114,30 @@ class LMSBatch(Document):
|
||||
if cint(self.seat_count) < len(self.students):
|
||||
frappe.throw(_("There are no seats available in this batch."))
|
||||
|
||||
def validate_schedule(self):
|
||||
for schedule in self.scheduled_flow:
|
||||
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)
|
||||
)
|
||||
@@ -192,6 +242,8 @@ def create_batch(
|
||||
end_date,
|
||||
description=None,
|
||||
batch_details=None,
|
||||
batch_details_raw=None,
|
||||
meta_image=None,
|
||||
seat_count=0,
|
||||
start_time=None,
|
||||
end_time=None,
|
||||
@@ -201,6 +253,7 @@ def create_batch(
|
||||
amount=0,
|
||||
currency=None,
|
||||
name=None,
|
||||
published=0,
|
||||
):
|
||||
frappe.only_for("Moderator")
|
||||
if name:
|
||||
@@ -215,6 +268,8 @@ def create_batch(
|
||||
"end_date": end_date,
|
||||
"description": description,
|
||||
"batch_details": batch_details,
|
||||
"batch_details_raw": batch_details_raw,
|
||||
"image": meta_image,
|
||||
"seat_count": seat_count,
|
||||
"start_time": start_time,
|
||||
"end_time": end_time,
|
||||
@@ -223,6 +278,7 @@ def create_batch(
|
||||
"paid_batch": paid_batch,
|
||||
"amount": amount,
|
||||
"currency": currency,
|
||||
"published": published,
|
||||
}
|
||||
)
|
||||
doc.save()
|
||||
@@ -243,6 +299,10 @@ def fetch_lessons(courses):
|
||||
@frappe.whitelist()
|
||||
def add_course(course, parent, name=None, evaluator=None):
|
||||
frappe.only_for("Moderator")
|
||||
|
||||
if frappe.db.exists("Batch Course", {"course": course, "parent": parent}):
|
||||
frappe.throw(_("Course already added to the batch."))
|
||||
|
||||
if name:
|
||||
doc = frappe.get_doc("Batch Course", name)
|
||||
else:
|
||||
@@ -260,3 +320,119 @@ def add_course(course, parent, name=None, evaluator=None):
|
||||
doc.save()
|
||||
|
||||
return doc.name
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
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",
|
||||
"milestone",
|
||||
"name",
|
||||
"idx",
|
||||
"parent",
|
||||
],
|
||||
order_by="date",
|
||||
)
|
||||
|
||||
show_live_class = frappe.db.get_value("LMS Batch", batch, "show_live_class")
|
||||
if show_live_class:
|
||||
live_classes = get_live_classes(batch)
|
||||
timetable.extend(live_classes)
|
||||
|
||||
timetable = get_timetable_details(timetable)
|
||||
return timetable
|
||||
|
||||
|
||||
def get_live_classes(batch):
|
||||
live_classes = frappe.get_all(
|
||||
"LMS Live Class",
|
||||
{"batch_name": batch},
|
||||
["name", "title", "date", "time as start_time", "duration", "join_url as url"],
|
||||
order_by="date",
|
||||
)
|
||||
for class_ in live_classes:
|
||||
class_.end_time = class_.start_time + timedelta(minutes=class_.duration)
|
||||
class_.reference_doctype = "LMS Live Class"
|
||||
class_.reference_docname = class_.name
|
||||
class_.icon = "icon-call"
|
||||
|
||||
return live_classes
|
||||
|
||||
|
||||
def get_timetable_details(timetable):
|
||||
for entry in timetable:
|
||||
entry.title = frappe.db.get_value(
|
||||
entry.reference_doctype, entry.reference_docname, "title"
|
||||
)
|
||||
assessment = frappe._dict({"assessment_name": entry.reference_docname})
|
||||
|
||||
if entry.reference_doctype == "Course Lesson":
|
||||
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.url = "/quizzes"
|
||||
details = get_quiz_details(assessment, frappe.session.user)
|
||||
entry.update(details)
|
||||
|
||||
elif entry.reference_doctype == "LMS Assignment":
|
||||
details = get_assignment_details(assessment, frappe.session.user)
|
||||
entry.update(details)
|
||||
|
||||
timetable = sorted(timetable, key=lambda k: k["date"])
|
||||
return timetable
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
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
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2023, Frappe and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("LMS Batch Timetable", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
93
lms/lms/doctype/lms_batch_timetable/lms_batch_timetable.json
Normal file
93
lms/lms/doctype/lms_batch_timetable/lms_batch_timetable.json
Normal file
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "hash",
|
||||
"creation": "2023-09-14 12:44:51.098956",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"column_break_htdc",
|
||||
"reference_doctype",
|
||||
"reference_docname",
|
||||
"date",
|
||||
"day",
|
||||
"column_break_merq",
|
||||
"start_time",
|
||||
"end_time",
|
||||
"duration",
|
||||
"milestone"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "reference_doctype",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Reference DocType",
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_docname",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Reference DocName",
|
||||
"options": "reference_doctype"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_merq",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.parenttype == \"LMS Batch\";",
|
||||
"fieldname": "date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "start_time",
|
||||
"fieldtype": "Time",
|
||||
"in_list_view": 1,
|
||||
"label": "Start Time"
|
||||
},
|
||||
{
|
||||
"fieldname": "duration",
|
||||
"fieldtype": "Data",
|
||||
"label": "Duration"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_htdc",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "end_time",
|
||||
"fieldtype": "Time",
|
||||
"in_list_view": 1,
|
||||
"label": "End Time"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.parenttype == \"LMS Timetable Template\";",
|
||||
"fieldname": "day",
|
||||
"fieldtype": "Int",
|
||||
"label": "Day"
|
||||
},
|
||||
{
|
||||
"fieldname": "milestone",
|
||||
"fieldtype": "Check",
|
||||
"label": "Milestone"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-10-20 11:58:01.782921",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Batch Timetable",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -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 LMSBatchTimetable(Document):
|
||||
pass
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2023, Frappe and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestLMSBatchTimetable(FrappeTestCase):
|
||||
pass
|
||||
@@ -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,10 +8,12 @@
|
||||
"course",
|
||||
"member",
|
||||
"member_name",
|
||||
"template",
|
||||
"column_break_3",
|
||||
"issue_date",
|
||||
"expiry_date",
|
||||
"batch_name"
|
||||
"batch_name",
|
||||
"published"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -60,11 +62,24 @@
|
||||
"in_standard_filter": 1,
|
||||
"label": "Batch",
|
||||
"options": "LMS Batch"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"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-08-29 15:23:08.637215",
|
||||
"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)
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"label": "Status",
|
||||
"options": "Pass\nFail",
|
||||
"options": "Pending\nIn Progress\nPass\nFail",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -106,7 +106,7 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-23 14:51:21.947169",
|
||||
"modified": "2023-09-26 19:44:43.594892",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Certificate Evaluation",
|
||||
@@ -139,6 +139,23 @@
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"states": [
|
||||
{
|
||||
"color": "Green",
|
||||
"title": "Pass"
|
||||
},
|
||||
{
|
||||
"color": "Red",
|
||||
"title": "Fail"
|
||||
},
|
||||
{
|
||||
"color": "Blue",
|
||||
"title": "Pending"
|
||||
},
|
||||
{
|
||||
"color": "Orange",
|
||||
"title": "In Progress"
|
||||
}
|
||||
],
|
||||
"title_field": "member_name"
|
||||
}
|
||||
@@ -20,7 +20,6 @@ class LMSCourse(Document):
|
||||
self.image = validate_image(self.image)
|
||||
|
||||
def validate_instructors(self):
|
||||
print(self.is_new(), not self.instructors)
|
||||
if self.is_new() and not self.instructors:
|
||||
frappe.get_doc(
|
||||
{
|
||||
@@ -217,7 +216,7 @@ def save_course(
|
||||
course_price=None,
|
||||
currency=None,
|
||||
):
|
||||
if not can_create_courses():
|
||||
if not can_create_courses(course):
|
||||
return
|
||||
|
||||
if course:
|
||||
@@ -281,6 +280,7 @@ def save_lesson(
|
||||
preview,
|
||||
idx,
|
||||
lesson,
|
||||
instructor_notes=None,
|
||||
youtube=None,
|
||||
quiz_id=None,
|
||||
question=None,
|
||||
@@ -296,6 +296,7 @@ def save_lesson(
|
||||
"chapter": chapter,
|
||||
"title": title,
|
||||
"body": body,
|
||||
"instructor_notes": instructor_notes,
|
||||
"include_in_preview": preview,
|
||||
"youtube": youtube,
|
||||
"quiz_id": quiz_id,
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-24 17:52:35.487141",
|
||||
"modified": "2023-10-02 12:41:25.139734",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Enrollment",
|
||||
@@ -140,6 +140,31 @@
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "LMS Student",
|
||||
"select": 1,
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Moderator",
|
||||
"select": 1,
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
|
||||
@@ -10,16 +10,16 @@
|
||||
"title",
|
||||
"host",
|
||||
"batch_name",
|
||||
"password",
|
||||
"auto_recording",
|
||||
"column_break_astv",
|
||||
"description",
|
||||
"section_break_glxh",
|
||||
"date",
|
||||
"timezone",
|
||||
"column_break_spvt",
|
||||
"time",
|
||||
"duration",
|
||||
"section_break_glxh",
|
||||
"description",
|
||||
"column_break_spvt",
|
||||
"timezone",
|
||||
"password",
|
||||
"auto_recording",
|
||||
"section_break_yrpq",
|
||||
"start_url",
|
||||
"column_break_yokr",
|
||||
@@ -126,7 +126,7 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-03-14 18:44:48.813103",
|
||||
"modified": "2023-09-20 11:29:20.899897",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Live Class",
|
||||
@@ -157,8 +157,10 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"show_title_field_in_link": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -8,13 +8,17 @@
|
||||
"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",
|
||||
"amount",
|
||||
"currency",
|
||||
"amount",
|
||||
"amount_with_gst",
|
||||
"column_break_yxpl",
|
||||
"order_id",
|
||||
"payment_id",
|
||||
@@ -39,7 +43,8 @@
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Amount"
|
||||
"label": "Amount",
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "currency",
|
||||
@@ -107,11 +112,35 @@
|
||||
"label": "Member",
|
||||
"options": "User",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.currency == \"INR\";",
|
||||
"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-08-24 22:08:12.294960",
|
||||
"modified": "2023-10-26 16:54:12.408274",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Payment",
|
||||
|
||||
0
lms/lms/doctype/lms_question/__init__.py
Normal file
0
lms/lms/doctype/lms_question/__init__.py
Normal file
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,18 +5,32 @@ import json
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cstr
|
||||
from lms.lms.utils import generate_slug, has_course_moderator_role, can_create_courses
|
||||
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,
|
||||
has_course_instructor_role,
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
@@ -35,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()
|
||||
@@ -114,45 +63,72 @@ 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"] + 1},
|
||||
["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,
|
||||
}
|
||||
|
||||
|
||||
@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 can_create_courses():
|
||||
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,
|
||||
@@ -160,41 +136,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(
|
||||
@@ -218,9 +230,8 @@ def save_question(quiz, values, index):
|
||||
}
|
||||
)
|
||||
|
||||
doc.save(ignore_permissions=True)
|
||||
|
||||
return quiz
|
||||
doc.save()
|
||||
return doc.name
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -253,9 +264,7 @@ 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:
|
||||
@@ -271,9 +280,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,14 +16,15 @@
|
||||
"portal_course_creation",
|
||||
"section_break_szgq",
|
||||
"send_calendar_invite_for_evaluations",
|
||||
"column_break_2",
|
||||
"allow_student_progress",
|
||||
"payment_section",
|
||||
"razorpay_key",
|
||||
"default_currency",
|
||||
"column_break_cfcv",
|
||||
"razorpay_secret",
|
||||
"apply_gst",
|
||||
"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",
|
||||
@@ -38,7 +39,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": [
|
||||
{
|
||||
@@ -67,7 +83,8 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_2",
|
||||
"fieldtype": "Column Break"
|
||||
"fieldtype": "Column Break",
|
||||
"label": "Show Tab in Batch"
|
||||
},
|
||||
{
|
||||
"fieldname": "search_placeholder",
|
||||
@@ -173,7 +190,7 @@
|
||||
{
|
||||
"fieldname": "section_break_szgq",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Class Settings"
|
||||
"label": "Batch Settings"
|
||||
},
|
||||
{
|
||||
"fieldname": "signup_settings_tab",
|
||||
@@ -183,6 +200,7 @@
|
||||
{
|
||||
"fieldname": "mentor_request_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"hidden": 1,
|
||||
"label": "Mentor Request"
|
||||
},
|
||||
{
|
||||
@@ -194,8 +212,7 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Payment"
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "default_currency",
|
||||
@@ -231,12 +248,105 @@
|
||||
"fieldname": "apply_gst",
|
||||
"fieldtype": "Check",
|
||||
"label": "Apply GST for India"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "show_usd_equivalent",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show USD Equivalent"
|
||||
},
|
||||
{
|
||||
"depends_on": "show_usd_equivalent",
|
||||
"fieldname": "exception_country",
|
||||
"fieldtype": "Table MultiSelect",
|
||||
"label": "Maintain Original Currency",
|
||||
"options": "Payment Country"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "apply_rounding",
|
||||
"fieldtype": "Check",
|
||||
"label": "Apply Rounding on Equivalent"
|
||||
},
|
||||
{
|
||||
"fieldname": "batch_confirmation_template",
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-29 09:54:48.030823",
|
||||
"modified": "2023-11-07 11:23:14.257687",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Settings",
|
||||
|
||||
0
lms/lms/doctype/lms_source/__init__.py
Normal file
0
lms/lms/doctype/lms_source/__init__.py
Normal file
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
|
||||
0
lms/lms/doctype/lms_timetable_legend/__init__.py
Normal file
0
lms/lms/doctype/lms_timetable_legend/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2023, Frappe and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("LMS Timetable Legend", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "hash",
|
||||
"creation": "2023-10-11 16:36:45.079267",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"reference_doctype",
|
||||
"label",
|
||||
"color"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "reference_doctype",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Reference DocType",
|
||||
"options": "DocType",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "color",
|
||||
"fieldtype": "Color",
|
||||
"in_list_view": 1,
|
||||
"label": "Color",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "label",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Label",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-10-11 17:15:37.039139",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Timetable Legend",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -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 LMSTimetableLegend(Document):
|
||||
pass
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2023, Frappe and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestLMSTimetableLegend(FrappeTestCase):
|
||||
pass
|
||||
0
lms/lms/doctype/lms_timetable_template/__init__.py
Normal file
0
lms/lms/doctype/lms_timetable_template/__init__.py
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) 2023, Frappe and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("LMS Timetable Template", {
|
||||
refresh(frm) {
|
||||
frm.set_query("reference_doctype", "timetable", function () {
|
||||
let doctypes = ["Course Lesson", "LMS Quiz", "LMS Assignment"];
|
||||
return {
|
||||
filters: {
|
||||
name: ["in", doctypes],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("reference_doctype", "timetable_legends", function () {
|
||||
let doctypes = [
|
||||
"Course Lesson",
|
||||
"LMS Quiz",
|
||||
"LMS Assignment",
|
||||
"LMS Live Class",
|
||||
];
|
||||
return {
|
||||
filters: {
|
||||
name: ["in", doctypes],
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "hash",
|
||||
"creation": "2023-09-18 14:16:16.964077",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"title",
|
||||
"timetable",
|
||||
"timetable_legends"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Title"
|
||||
},
|
||||
{
|
||||
"fieldname": "timetable",
|
||||
"fieldtype": "Table",
|
||||
"label": "Timetable",
|
||||
"options": "LMS Batch Timetable"
|
||||
},
|
||||
{
|
||||
"fieldname": "timetable_legends",
|
||||
"fieldtype": "Table",
|
||||
"label": "Timetable Legends",
|
||||
"options": "LMS Timetable Legend"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-10-11 17:09:05.096243",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Timetable Template",
|
||||
"naming_rule": "Random",
|
||||
"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
|
||||
}
|
||||
],
|
||||
"show_title_field_in_link": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title"
|
||||
}
|
||||
@@ -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 LMSTimetableTemplate(Document):
|
||||
pass
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2023, Frappe and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestLMSTimetableTemplate(FrappeTestCase):
|
||||
pass
|
||||
0
lms/lms/doctype/payment_country/__init__.py
Normal file
0
lms/lms/doctype/payment_country/__init__.py
Normal file
8
lms/lms/doctype/payment_country/payment_country.js
Normal file
8
lms/lms/doctype/payment_country/payment_country.js
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2023, Frappe and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Payment Country", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
33
lms/lms/doctype/payment_country/payment_country.json
Normal file
33
lms/lms/doctype/payment_country/payment_country.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2023-09-11 11:53:16.253740",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"country"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "country",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Country",
|
||||
"options": "Country"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-09-11 12:04:56.048632",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Payment Country",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
9
lms/lms/doctype/payment_country/payment_country.py
Normal file
9
lms/lms/doctype/payment_country/payment_country.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 PaymentCountry(Document):
|
||||
pass
|
||||
9
lms/lms/doctype/payment_country/test_payment_country.py
Normal file
9
lms/lms/doctype/payment_country/test_payment_country.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 TestPaymentCountry(FrappeTestCase):
|
||||
pass
|
||||
@@ -19,7 +19,7 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-12-21 09:35:44.265910",
|
||||
"modified": "2023-09-29 17:05:50.502696",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "User Skill",
|
||||
@@ -44,11 +44,12 @@
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "All",
|
||||
"role": "LMS Student",
|
||||
"select": 1,
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -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
|
||||
@@ -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",
|
||||
|
||||
@@ -72,4 +72,4 @@ def create_evaluation(user, course, date, rating, status):
|
||||
"status": status,
|
||||
}
|
||||
)
|
||||
evaluation.save(ignore_permissions=True)
|
||||
evaluation.save()
|
||||
|
||||
137
lms/lms/utils.py
137
lms/lms/utils.py
@@ -3,6 +3,7 @@ import string
|
||||
import frappe
|
||||
import json
|
||||
import razorpay
|
||||
import requests
|
||||
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
|
||||
@@ -16,6 +17,7 @@ from frappe.utils import (
|
||||
get_datetime,
|
||||
getdate,
|
||||
validate_phone_number,
|
||||
ceil,
|
||||
)
|
||||
from frappe.utils.dateutils import get_period
|
||||
from lms.lms.md import find_macros, markdown_to_html
|
||||
@@ -143,12 +145,12 @@ def get_lesson_details(chapter):
|
||||
"quiz_id",
|
||||
"question",
|
||||
"file_type",
|
||||
"instructor_notes",
|
||||
],
|
||||
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
|
||||
|
||||
@@ -518,21 +520,35 @@ def has_course_instructor_role(member=None):
|
||||
)
|
||||
|
||||
|
||||
def can_create_courses(member=None):
|
||||
def can_create_courses(course, member=None):
|
||||
if not member:
|
||||
member = frappe.session.user
|
||||
|
||||
instructors = frappe.get_all(
|
||||
"Course Instructor",
|
||||
{
|
||||
"parent": course,
|
||||
},
|
||||
pluck="instructor",
|
||||
)
|
||||
|
||||
if frappe.session.user == "Guest":
|
||||
return False
|
||||
|
||||
if has_course_instructor_role(member) or has_course_moderator_role(member):
|
||||
if has_course_moderator_role(member):
|
||||
return True
|
||||
|
||||
if has_course_instructor_role(member) and member in instructors:
|
||||
return True
|
||||
|
||||
portal_course_creation = frappe.db.get_single_value(
|
||||
"LMS Settings", "portal_course_creation"
|
||||
)
|
||||
|
||||
return portal_course_creation == "Anyone"
|
||||
if portal_course_creation == "Anyone" and member in instructors:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def has_course_moderator_role(member=None):
|
||||
@@ -582,7 +598,7 @@ def validate_image(path):
|
||||
if path and "/private" in path:
|
||||
file = frappe.get_doc("File", {"file_url": path})
|
||||
file.is_private = 0
|
||||
file.save(ignore_permissions=True)
|
||||
file.save()
|
||||
return file.file_url
|
||||
return path
|
||||
|
||||
@@ -724,7 +740,7 @@ def get_chart_data(chart_name, timespan, timegrain, from_date, to_date):
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def get_course_completion_data():
|
||||
all_membership = frappe.db.count("LMS Enrollment")
|
||||
completed = frappe.db.count("LMS Enrollment", {"progress": ["like", "%100%"]})
|
||||
@@ -830,19 +846,23 @@ def get_upcoming_evals(student, courses):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_payment_options(doctype, docname, phone):
|
||||
def get_payment_options(doctype, docname, phone, country):
|
||||
if not frappe.db.exists(doctype, docname):
|
||||
frappe.throw(_("Invalid document provided."))
|
||||
|
||||
validate_phone_number(phone, True)
|
||||
details = get_details(doctype, docname)
|
||||
details.amount, details.currency = check_multicurrency(
|
||||
details.amount, details.currency, country
|
||||
)
|
||||
if details.currency == "INR":
|
||||
details.amount, details.gst_applied = apply_gst(details.amount, country)
|
||||
|
||||
razorpay_key = frappe.db.get_single_value("LMS Settings", "razorpay_key")
|
||||
client = get_client()
|
||||
order = create_order(client, details.amount, details.currency)
|
||||
|
||||
options = {
|
||||
"key_id": razorpay_key,
|
||||
"key_id": frappe.db.get_single_value("LMS Settings", "razorpay_key"),
|
||||
"name": frappe.db.get_single_value("Website Settings", "app_name"),
|
||||
"description": _("Payment for {0} course").format(details["title"]),
|
||||
"order_id": order["id"],
|
||||
@@ -857,6 +877,46 @@ def get_payment_options(doctype, docname, phone):
|
||||
return options
|
||||
|
||||
|
||||
def check_multicurrency(amount, currency, country=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"
|
||||
)
|
||||
|
||||
if not show_usd_equivalent or currency == "USD":
|
||||
return amount, currency
|
||||
|
||||
if not country or (exception_country and country in exception_country):
|
||||
return amount, currency
|
||||
|
||||
exchange_rate = get_current_exchange_rate(currency, "USD")
|
||||
amount = amount * exchange_rate
|
||||
currency = "USD"
|
||||
|
||||
if apply_rounding and amount % 100 != 0:
|
||||
amount = amount + 100 - amount % 100
|
||||
|
||||
return amount, currency
|
||||
|
||||
|
||||
def apply_gst(amount, country=None):
|
||||
gst_applied = False
|
||||
apply_gst = frappe.db.get_single_value("LMS Settings", "apply_gst")
|
||||
|
||||
if not country:
|
||||
country = frappe.db.get_value("User", frappe.session.user, "country")
|
||||
|
||||
if apply_gst and country == "India":
|
||||
gst_applied = True
|
||||
amount = amount * 1.18
|
||||
|
||||
return amount, gst_applied
|
||||
|
||||
|
||||
def get_details(doctype, docname):
|
||||
if doctype == "LMS Course":
|
||||
details = frappe.db.get_value(
|
||||
@@ -881,7 +941,15 @@ def get_details(doctype, docname):
|
||||
|
||||
|
||||
def save_address(address):
|
||||
address.update(
|
||||
filters = {"email_id": frappe.session.user}
|
||||
exists = frappe.db.exists("Address", filters)
|
||||
if exists:
|
||||
address_doc = frappe.get_last_doc("Address", filters=filters)
|
||||
else:
|
||||
address_doc = frappe.new_doc("Address")
|
||||
|
||||
address_doc.update(address)
|
||||
address_doc.update(
|
||||
{
|
||||
"address_title": frappe.db.get_value("User", frappe.session.user, "full_name"),
|
||||
"address_type": "Billing",
|
||||
@@ -889,15 +957,14 @@ def save_address(address):
|
||||
"email_id": frappe.session.user,
|
||||
}
|
||||
)
|
||||
doc = frappe.new_doc("Address")
|
||||
doc.update(address)
|
||||
doc.save(ignore_permissions=True)
|
||||
return doc.name
|
||||
address_doc.save(ignore_permissions=True)
|
||||
return address_doc.name
|
||||
|
||||
|
||||
def get_client():
|
||||
razorpay_key = frappe.db.get_single_value("LMS Settings", "razorpay_key")
|
||||
razorpay_secret = frappe.db.get_single_value("LMS Settings", "razorpay_secret")
|
||||
settings = frappe.get_single("LMS Settings")
|
||||
razorpay_key = settings.razorpay_key
|
||||
razorpay_secret = settings.get_password("razorpay_secret", raise_exception=True)
|
||||
|
||||
if not razorpay_key and not razorpay_secret:
|
||||
frappe.throw(
|
||||
@@ -946,7 +1013,7 @@ def record_payment(address, response, client, doctype, docname):
|
||||
address = frappe._dict(json.loads(address))
|
||||
address_name = save_address(address)
|
||||
|
||||
payment_details = get_payment_details(doctype, docname)
|
||||
payment_details = get_payment_details(doctype, docname, address)
|
||||
payment_doc = frappe.new_doc("LMS Payment")
|
||||
payment_doc.update(
|
||||
{
|
||||
@@ -958,29 +1025,39 @@ def record_payment(address, response, client, doctype, docname):
|
||||
"payment_id": response["razorpay_payment_id"],
|
||||
"amount": payment_details["amount"],
|
||||
"currency": payment_details["currency"],
|
||||
"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):
|
||||
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_with_gst = 0
|
||||
|
||||
amount, currency = check_multicurrency(amount, currency)
|
||||
if currency == "INR" and address.country == "India":
|
||||
amount_with_gst, gst_applied = apply_gst(amount, address.country)
|
||||
|
||||
return {
|
||||
"amount": amount,
|
||||
"currency": currency,
|
||||
"amount_with_gst": amount_with_gst,
|
||||
}
|
||||
|
||||
|
||||
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"
|
||||
@@ -991,7 +1068,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",
|
||||
@@ -999,3 +1077,18 @@ def add_student_to_batch(batchname, payment):
|
||||
)
|
||||
student.save(ignore_permissions=True)
|
||||
return f"/batches/{batchname}"
|
||||
|
||||
|
||||
def get_current_exchange_rate(source, target="USD"):
|
||||
url = f"https://api.frankfurter.app/latest?from={source}&to={target}"
|
||||
|
||||
response = requests.request("GET", url)
|
||||
details = response.json()
|
||||
return details["rates"][target]
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def change_currency(amount, currency, country=None):
|
||||
amount = cint(amount)
|
||||
amount, currency = check_multicurrency(amount, currency, country)
|
||||
return fmt_money(amount, 0, currency)
|
||||
|
||||
@@ -235,13 +235,15 @@ def sign_up(email, full_name, verify_terms, user_category):
|
||||
user.flags.ignore_permissions = True
|
||||
user.flags.ignore_password_policy = True
|
||||
user.insert()
|
||||
set_country_from_ip(None, user.name)
|
||||
|
||||
# set default signup role as per Portal Settings
|
||||
default_role = frappe.db.get_value("Portal Settings", None, "default_role")
|
||||
if default_role:
|
||||
user.add_roles(default_role)
|
||||
|
||||
user.add_roles("LMS Student")
|
||||
set_country_from_ip(None, user.name)
|
||||
|
||||
if user.flags.email_sent:
|
||||
return 1, _("Please check your email for verification")
|
||||
else:
|
||||
|
||||
@@ -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
|
||||
@@ -66,4 +67,18 @@ lms.patches.v1_0.revert_class_registration #18-08-2023
|
||||
lms.patches.v1_0.rename_lms_batch_doctype
|
||||
lms.patches.v1_0.rename_lms_batch_membership_doctype
|
||||
lms.patches.v1_0.rename_lms_class_to_lms_batch
|
||||
lms.patches.v1_0.rename_classes_in_navbar
|
||||
lms.patches.v1_0.rename_classes_in_navbar
|
||||
lms.patches.v1_0.publish_batches
|
||||
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")
|
||||
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)
|
||||
6
lms/patches/v1_0/change_naming_for_batch_course.py
Normal file
6
lms/patches/v1_0/change_naming_for_batch_course.py
Normal file
@@ -0,0 +1,6 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
frappe.db.create_sequence("Batch Course", check_not_exists=True)
|
||||
frappe.db.set_next_sequence_val("Batch Course", 500, is_val_used=False)
|
||||
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)
|
||||
22
lms/patches/v1_0/create_student_role.py
Normal file
22
lms/patches/v1_0/create_student_role.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import frappe
|
||||
from lms.install import create_lms_student_role
|
||||
|
||||
|
||||
def execute():
|
||||
create_lms_student_role()
|
||||
|
||||
users = frappe.get_all(
|
||||
"User", filters={"user_type": "Website User", "enabled": 1}, pluck="name"
|
||||
)
|
||||
|
||||
for user in users:
|
||||
filters = {
|
||||
"parent": user,
|
||||
"parenttype": "User",
|
||||
"parentfield": "roles",
|
||||
"role": "LMS Student",
|
||||
}
|
||||
if not frappe.db.exists("Has Role", filters):
|
||||
doc = frappe.new_doc("Has Role")
|
||||
doc.update(filters)
|
||||
doc.save()
|
||||
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)
|
||||
9
lms/patches/v1_0/publish_batches.py
Normal file
9
lms/patches/v1_0/publish_batches.py
Normal file
@@ -0,0 +1,9 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc("lms", "doctype", "lms_batch")
|
||||
batches = frappe.get_all("LMS Batch", pluck="name")
|
||||
|
||||
for batch in batches:
|
||||
frappe.db.set_value("LMS Batch", batch, "Published", 1)
|
||||
9
lms/patches/v1_0/publish_certificates.py
Normal file
9
lms/patches/v1_0/publish_certificates.py
Normal file
@@ -0,0 +1,9 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc("lms", "doctype", "lms_certificate")
|
||||
certificates = frappe.get_all("LMS Certificate", pluck="name")
|
||||
|
||||
for certificate in certificates:
|
||||
frappe.db.set_value("LMS Certificate", certificate, "published", 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}
|
||||
)
|
||||
@@ -155,10 +187,38 @@ def youtube_video_renderer(video_id):
|
||||
"""
|
||||
|
||||
|
||||
def embed_renderer(details):
|
||||
type = details.split("|||")[0]
|
||||
src = details.split("|||")[1]
|
||||
width = "100%"
|
||||
height = "400"
|
||||
|
||||
if type == "pdf":
|
||||
width = "75%"
|
||||
height = "600"
|
||||
|
||||
return f"""
|
||||
<iframe width={width} height={height}
|
||||
src={src}
|
||||
title="Embedded Content"
|
||||
frameborder="0"
|
||||
style="border-radius: var(--border-radius-lg)"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen>
|
||||
</iframe>
|
||||
"""
|
||||
|
||||
|
||||
def video_renderer(src):
|
||||
return (
|
||||
f"<video controls width='100%'><source src={quote(src)} type='video/mp4'></video>"
|
||||
)
|
||||
return f"<video controls width='100%' controls controlsList='nodownload'><source src={quote(src)} type='video/mp4'></video>"
|
||||
|
||||
|
||||
def audio_renderer(src):
|
||||
return f"<audio width='100%' controls controlsList='nodownload'><source src={quote(src)} type='audio/mp3'></audio>"
|
||||
|
||||
|
||||
def pdf_renderer(src):
|
||||
return f"<iframe src='{quote(src)}#toolbar=0' width='100%' height='700px'></iframe>"
|
||||
|
||||
|
||||
def assignment_renderer(detail):
|
||||
|
||||
@@ -160,6 +160,10 @@ textarea.field-input {
|
||||
position: unset;
|
||||
}
|
||||
|
||||
.codex-editor--narrow .ce-toolbar__actions {
|
||||
right: 100%;
|
||||
}
|
||||
|
||||
.lesson-editor {
|
||||
border: 1px solid var(--gray-300);
|
||||
border-radius: var(--border-radius-md);
|
||||
@@ -781,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 {
|
||||
@@ -2053,8 +2058,8 @@ select {
|
||||
}
|
||||
|
||||
.onboarding-parent {
|
||||
background-color: var(--primary-light);
|
||||
padding: 2rem 0;
|
||||
background-color: var(--gray-100);
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.onboarding-steps {
|
||||
@@ -2339,4 +2344,142 @@ select {
|
||||
|
||||
.batch-course-list .cards-parent {
|
||||
row-gap: 3rem
|
||||
}
|
||||
|
||||
.embed-tool__caption {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.card-buttons {
|
||||
display: flex;
|
||||
position: relative;
|
||||
top: 10%;
|
||||
left: 80%;
|
||||
z-index: 10;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.toastui-calendar-milestone {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.toastui-calendar-task {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.toastui-calendar-panel-resizer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.toastui-calendar-day-name__date {
|
||||
font-size: var(--text-base) !important;
|
||||
}
|
||||
|
||||
.toastui-calendar-day-name__name {
|
||||
font-size: var(--text-base) !important;
|
||||
}
|
||||
|
||||
.toastui-calendar-day-view-day-names, .toastui-calendar-week-view-day-names {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
.toastui-calendar-layout {
|
||||
border: 1px solid var(--gray-200) !important;
|
||||
border-radius: var(--border-radius-md) !important;
|
||||
background-color: var(--gray-100) !important;
|
||||
}
|
||||
|
||||
.toastui-calendar-panel .toastui-calendar-day-names.toastui-calendar-week {
|
||||
border-top: none !important;
|
||||
}
|
||||
|
||||
.toastui-calendar-panel.toastui-calendar-time {
|
||||
height: 80% !important;
|
||||
}
|
||||
|
||||
.toastui-calendar-panel.toastui-calendar-week-view-day-names {
|
||||
background-color: var(--gray-50) !important;
|
||||
}
|
||||
|
||||
.toastui-calendar-allday {
|
||||
border-bottom: 1px solid var(--gray-200) !important;
|
||||
}
|
||||
|
||||
.calendar-navigation {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.calendar-range {
|
||||
margin: 0 2rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.calendar-event-title {
|
||||
font-size: var(--text-md);
|
||||
font-weight: 500;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
.legend-color {
|
||||
width: 50px;
|
||||
height: 20px;
|
||||
border-radius: var(--border-radius-sm);
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.legend-text {
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.calendar-legends {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 50%;
|
||||
margin: 0 auto 1rem;
|
||||
}
|
||||
|
||||
.batch-details {
|
||||
width: 50%;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
.batch-details {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-section {
|
||||
font-size: var(--text-lg);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.collapse-section.collapsed .icon {
|
||||
transition: all 0.5s;
|
||||
-webkit-transform: rotate(180deg);
|
||||
-moz-transform: rotate(180deg);
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.modal-body .ql-container {
|
||||
max-height: unset !important;
|
||||
}
|
||||
|
||||
.questions-table .row-index {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.text-color {
|
||||
color: var(--text-color);
|
||||
}
|
||||
@@ -261,6 +261,24 @@ const open_batch_dialog = () => {
|
||||
reqd: 1,
|
||||
default: batch_info && batch_info.title,
|
||||
},
|
||||
{
|
||||
fieldtype: "Check",
|
||||
label: __("Published"),
|
||||
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",
|
||||
},
|
||||
{
|
||||
fieldtype: "Date",
|
||||
label: __("Start Date"),
|
||||
@@ -297,17 +315,12 @@ const open_batch_dialog = () => {
|
||||
fieldname: "end_time",
|
||||
default: batch_info && batch_info.end_time,
|
||||
},
|
||||
{
|
||||
fieldtype: "Int",
|
||||
label: __("Seat Count"),
|
||||
fieldname: "seat_count",
|
||||
default: batch_info && batch_info.seat_count,
|
||||
},
|
||||
{
|
||||
fieldtype: "Link",
|
||||
label: __("Category"),
|
||||
fieldname: "category",
|
||||
options: "LMS Category",
|
||||
only_select: 1,
|
||||
default: batch_info && batch_info.category,
|
||||
},
|
||||
{
|
||||
@@ -327,6 +340,18 @@ const open_batch_dialog = () => {
|
||||
default: batch_info && batch_info.batch_details,
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldtype: "HTML Editor",
|
||||
label: __("Batch Details Raw"),
|
||||
fieldname: "batch_details_raw",
|
||||
default: batch_info && batch_info.batch_details_raw,
|
||||
},
|
||||
{
|
||||
fieldtype: "Attach Image",
|
||||
label: __("Meta Image"),
|
||||
fieldname: "meta_image",
|
||||
default: batch_info && batch_info.meta_image,
|
||||
},
|
||||
{
|
||||
fieldtype: "Section Break",
|
||||
label: __("Pricing"),
|
||||
|
||||
@@ -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>
|
||||
|
||||
38
lms/templates/emails/batch_confirmation.html
Normal file
38
lms/templates/emails/batch_confirmation.html
Normal file
@@ -0,0 +1,38 @@
|
||||
<p>
|
||||
{{ _("Dear ") }} {{ student_name }},
|
||||
</p>
|
||||
<br>
|
||||
<p>
|
||||
{{ _("I am pleased to inform you that your enrollment for the upcoming training batch has been successfully processed. Congratulations!") }}
|
||||
</p>
|
||||
<br>
|
||||
<p>
|
||||
<b>
|
||||
{{ _("Important Details:") }}
|
||||
</b>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<b>{{ _("Batch Start Date:") }}</b> {{ frappe.utils.format_date(start_date, "medium") }}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<b>{{ _("Medium:") }}</b> {{ medium }}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<b>{{ _("Timings:") }}</b> {{ frappe.utils.format_time(start_time, "hh:mm a") }}
|
||||
</p>
|
||||
<br>
|
||||
<p>
|
||||
{{ _("Visit the following link to view your ") }}
|
||||
<a href="/batches/{{ name }}">{{ _("Batch Details") }}</a>
|
||||
</p>
|
||||
<p>
|
||||
{{ _("If you have any questions or require assistance, feel free to contact us.") }}
|
||||
</p>
|
||||
<br>
|
||||
<p>
|
||||
{{ _("Best Regards") }}
|
||||
</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>
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="onboarding-parent">
|
||||
<div class="container">
|
||||
<div class="onboarding-skip">{{ _("Skip") }}</div>
|
||||
<div class="course-home-headings mt-0">
|
||||
<div class="page-title">
|
||||
{{ _("Get Started") }}
|
||||
</div>
|
||||
<div class="onboarding-subtitle">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user