Merge branch 'main' of https://github.com/frappe/lms into lms-frappe-ui

This commit is contained in:
Jannat Patel
2023-11-09 16:16:08 +05:30
143 changed files with 4222 additions and 1175 deletions

View File

@@ -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

View File

@@ -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
View 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.

View File

@@ -13,6 +13,6 @@ module.exports = defineConfig({
openMode: 0,
},
e2e: {
baseUrl: "http://dd1:8000",
baseUrl: "http://pyp:8000",
},
});

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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}

View File

@@ -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",

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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": []
}

View File

@@ -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": []
}

View File

@@ -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):

View File

@@ -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",

View File

@@ -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:

View File

@@ -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,
},
};
});
},
});

View File

@@ -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"

View File

@@ -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,
}

View File

@@ -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();
};

View File

@@ -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",

View File

@@ -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

View File

@@ -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) {
// },
// });

View 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": []
}

View 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 LMSBatchTimetable(Document):
pass

View File

@@ -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

View File

@@ -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)

View File

@@ -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",

View File

@@ -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)

View File

@@ -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"
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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",

View File

View 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) {
// },
// });

View 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"
}

View 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)

View 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

View File

@@ -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;
},
});

View File

@@ -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,

View File

@@ -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():

View File

@@ -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")

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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

View File

@@ -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",

View File

View 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) {
// },
// });

View 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"
}

View 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

View 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

View 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) {
// },
// });

View File

@@ -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": []
}

View 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 LMSTimetableLegend(Document):
pass

View File

@@ -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

View 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],
},
};
});
},
});

View File

@@ -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"
}

View 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 LMSTimetableTemplate(Document):
pass

View File

@@ -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

View 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) {
// },
// });

View 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": []
}

View 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

View 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

View File

@@ -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": []
}

View File

@@ -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"
}

View File

@@ -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>

View File

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

View File

@@ -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",

View File

@@ -72,4 +72,4 @@ def create_evaluation(user, course, date, rating, status):
"status": status,
}
)
evaluation.save(ignore_permissions=True)
evaluation.save()

View File

@@ -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)

View File

@@ -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:

View File

@@ -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")

View 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
)

View 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}
)

View 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)

View 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)

View 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()

View 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)

View 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()

View 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)

View 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)

View 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)

View File

@@ -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):

View File

@@ -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);
}

View File

@@ -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"),

View File

@@ -1,5 +1,3 @@
{% set certificates = get_certificates(user) %}
{% if certificates | length %}
<div class="cards-parent">
{% for certificate in certificates %}

View 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>

View 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>

View 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>

View File

@@ -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