Merge branch 'main' into 644
This commit is contained in:
@@ -98,7 +98,6 @@ override_doctype_class = {
|
|||||||
|
|
||||||
doc_events = {
|
doc_events = {
|
||||||
"Discussion Reply": {"after_insert": "lms.lms.utils.create_notification_log"},
|
"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
|
# Scheduled Tasks
|
||||||
|
|||||||
@@ -99,8 +99,14 @@ def save_progress(lesson, course, status):
|
|||||||
quizzes = [value for name, value in macros if name == "Quiz"]
|
quizzes = [value for name, value in macros if name == "Quiz"]
|
||||||
|
|
||||||
for quiz in quizzes:
|
for quiz in quizzes:
|
||||||
|
passing_percentage = frappe.db.get_value("LMS Quiz", quiz, "passing_percentage")
|
||||||
if not frappe.db.exists(
|
if not frappe.db.exists(
|
||||||
"LMS Quiz Submission", {"quiz": quiz, "owner": frappe.session.user}
|
"LMS Quiz Submission",
|
||||||
|
{
|
||||||
|
"quiz": quiz,
|
||||||
|
"owner": frappe.session.user,
|
||||||
|
"percentage": [">=", passing_percentage],
|
||||||
|
},
|
||||||
):
|
):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ const set_timetable = (frm) => {
|
|||||||
"start_time",
|
"start_time",
|
||||||
"end_time",
|
"end_time",
|
||||||
"duration",
|
"duration",
|
||||||
|
"milestone",
|
||||||
],
|
],
|
||||||
filters: {
|
filters: {
|
||||||
parent: frm.doc.timetable_template,
|
parent: frm.doc.timetable_template,
|
||||||
@@ -82,6 +83,7 @@ const add_timetable_rows = (frm, timetable) => {
|
|||||||
.format("HH:mm")
|
.format("HH:mm")
|
||||||
: null;
|
: null;
|
||||||
child.duration = row.duration;
|
child.duration = row.duration;
|
||||||
|
child.milestone = row.milestone;
|
||||||
});
|
});
|
||||||
frm.refresh_field("timetable");
|
frm.refresh_field("timetable");
|
||||||
|
|
||||||
|
|||||||
@@ -12,9 +12,6 @@ from frappe.utils import (
|
|||||||
cint,
|
cint,
|
||||||
format_date,
|
format_date,
|
||||||
format_datetime,
|
format_datetime,
|
||||||
add_to_date,
|
|
||||||
getdate,
|
|
||||||
get_datetime,
|
|
||||||
)
|
)
|
||||||
from lms.lms.utils import get_lessons, get_lesson_index, get_lesson_url
|
from lms.lms.utils import get_lessons, get_lesson_index, get_lesson_url
|
||||||
from lms.www.utils import get_quiz_details, get_assignment_details
|
from lms.www.utils import get_quiz_details, get_assignment_details
|
||||||
@@ -325,7 +322,17 @@ def get_batch_timetable(batch):
|
|||||||
timetable = frappe.get_all(
|
timetable = frappe.get_all(
|
||||||
"LMS Batch Timetable",
|
"LMS Batch Timetable",
|
||||||
filters={"parent": batch},
|
filters={"parent": batch},
|
||||||
fields=["reference_doctype", "reference_docname", "date", "start_time", "end_time"],
|
fields=[
|
||||||
|
"reference_doctype",
|
||||||
|
"reference_docname",
|
||||||
|
"date",
|
||||||
|
"start_time",
|
||||||
|
"end_time",
|
||||||
|
"milestone",
|
||||||
|
"name",
|
||||||
|
"idx",
|
||||||
|
"parent",
|
||||||
|
],
|
||||||
order_by="date",
|
order_by="date",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -362,20 +369,26 @@ def get_timetable_details(timetable):
|
|||||||
assessment = frappe._dict({"assessment_name": entry.reference_docname})
|
assessment = frappe._dict({"assessment_name": entry.reference_docname})
|
||||||
|
|
||||||
if entry.reference_doctype == "Course Lesson":
|
if entry.reference_doctype == "Course Lesson":
|
||||||
entry.icon = "icon-list"
|
|
||||||
course = frappe.db.get_value(
|
course = frappe.db.get_value(
|
||||||
entry.reference_doctype, entry.reference_docname, "course"
|
entry.reference_doctype, entry.reference_docname, "course"
|
||||||
)
|
)
|
||||||
entry.url = get_lesson_url(course, get_lesson_index(entry.reference_docname))
|
entry.url = get_lesson_url(course, get_lesson_index(entry.reference_docname))
|
||||||
|
|
||||||
|
entry.completed = (
|
||||||
|
True
|
||||||
|
if frappe.db.exists(
|
||||||
|
"LMS Course Progress",
|
||||||
|
{"lesson": entry.reference_docname, "member": frappe.session.user},
|
||||||
|
)
|
||||||
|
else False
|
||||||
|
)
|
||||||
|
|
||||||
elif entry.reference_doctype == "LMS Quiz":
|
elif entry.reference_doctype == "LMS Quiz":
|
||||||
entry.icon = "icon-quiz"
|
|
||||||
entry.url = "/quizzes"
|
entry.url = "/quizzes"
|
||||||
details = get_quiz_details(assessment, frappe.session.user)
|
details = get_quiz_details(assessment, frappe.session.user)
|
||||||
entry.update(details)
|
entry.update(details)
|
||||||
|
|
||||||
elif entry.reference_doctype == "LMS Assignment":
|
elif entry.reference_doctype == "LMS Assignment":
|
||||||
entry.icon = "icon-quiz"
|
|
||||||
details = get_assignment_details(assessment, frappe.session.user)
|
details = get_assignment_details(assessment, frappe.session.user)
|
||||||
entry.update(details)
|
entry.update(details)
|
||||||
|
|
||||||
@@ -384,11 +397,37 @@ def get_timetable_details(timetable):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def send_email_to_students(batch, subject, message):
|
def is_milestone_complete(idx, batch):
|
||||||
frappe.only_for("Moderator")
|
previous_rows = frappe.get_all(
|
||||||
students = frappe.get_all("Batch Student", {"parent": batch}, pluck="student")
|
"LMS Batch Timetable",
|
||||||
frappe.sendmail(
|
filters={"parent": batch, "idx": ["<", cint(idx)]},
|
||||||
recipients=students,
|
fields=["reference_doctype", "reference_docname", "idx"],
|
||||||
subject=subject,
|
order_by="idx",
|
||||||
message=message,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
for row in previous_rows:
|
||||||
|
if row.reference_doctype == "Course Lesson":
|
||||||
|
if not frappe.db.exists(
|
||||||
|
"LMS Course Progress",
|
||||||
|
{"member": frappe.session.user, "lesson": row.reference_docname},
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if row.reference_doctype == "LMS Quiz":
|
||||||
|
passing_percentage = frappe.db.get_value(
|
||||||
|
row.reference_doctype, row.reference_docname, "passing_percentage"
|
||||||
|
)
|
||||||
|
if not frappe.db.exists(
|
||||||
|
"LMS Quiz Submission",
|
||||||
|
{"quiz": row.reference_docname, "member": frappe.session.user},
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if row.reference_doctype == "LMS Assignment":
|
||||||
|
if not frappe.db.exists(
|
||||||
|
"LMS Assignment Submission",
|
||||||
|
{"assignment": row.reference_docname, "member": frappe.session.user},
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|||||||
@@ -16,7 +16,8 @@
|
|||||||
"column_break_merq",
|
"column_break_merq",
|
||||||
"start_time",
|
"start_time",
|
||||||
"end_time",
|
"end_time",
|
||||||
"duration"
|
"duration",
|
||||||
|
"milestone"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -69,12 +70,17 @@
|
|||||||
"fieldname": "day",
|
"fieldname": "day",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"label": "Day"
|
"label": "Day"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "milestone",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Milestone"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-10-03 17:40:31.530181",
|
"modified": "2023-10-20 11:58:01.782921",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Batch Timetable",
|
"name": "LMS Batch Timetable",
|
||||||
|
|||||||
@@ -10,6 +10,14 @@ frappe.ui.form.on("LMS Certificate", {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
frm.set_query("template", function (doc) {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
doc_type: "LMS Certificate",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
},
|
},
|
||||||
refresh: (frm) => {
|
refresh: (frm) => {
|
||||||
if (frm.doc.name)
|
if (frm.doc.name)
|
||||||
|
|||||||
@@ -8,11 +8,12 @@
|
|||||||
"course",
|
"course",
|
||||||
"member",
|
"member",
|
||||||
"member_name",
|
"member_name",
|
||||||
"published",
|
"template",
|
||||||
"column_break_3",
|
"column_break_3",
|
||||||
"issue_date",
|
"issue_date",
|
||||||
"expiry_date",
|
"expiry_date",
|
||||||
"batch_name"
|
"batch_name",
|
||||||
|
"published"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -67,11 +68,18 @@
|
|||||||
"fieldname": "published",
|
"fieldname": "published",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Publish on Participant Page"
|
"label": "Publish on Participant Page"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "template",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Template",
|
||||||
|
"options": "Print Format",
|
||||||
|
"reqd": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-09-13 11:03:23.479255",
|
"modified": "2023-10-25 12:20:56.091979",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Certificate",
|
"name": "LMS Certificate",
|
||||||
|
|||||||
@@ -48,6 +48,15 @@ def create_certificate(course):
|
|||||||
if expires_after_yrs:
|
if expires_after_yrs:
|
||||||
expiry_date = add_years(nowdate(), expires_after_yrs)
|
expiry_date = add_years(nowdate(), expires_after_yrs)
|
||||||
|
|
||||||
|
default_certificate_template = frappe.db.get_value(
|
||||||
|
"Property Setter",
|
||||||
|
{
|
||||||
|
"doc_type": "LMS Certificate",
|
||||||
|
"property": "default_print_format",
|
||||||
|
},
|
||||||
|
"value",
|
||||||
|
)
|
||||||
|
|
||||||
certificate = frappe.get_doc(
|
certificate = frappe.get_doc(
|
||||||
{
|
{
|
||||||
"doctype": "LMS Certificate",
|
"doctype": "LMS Certificate",
|
||||||
@@ -55,6 +64,7 @@ def create_certificate(course):
|
|||||||
"course": course,
|
"course": course,
|
||||||
"issue_date": nowdate(),
|
"issue_date": nowdate(),
|
||||||
"expiry_date": expiry_date,
|
"expiry_date": expiry_date,
|
||||||
|
"template": default_certificate_template,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
certificate.save(ignore_permissions=True)
|
certificate.save(ignore_permissions=True)
|
||||||
|
|||||||
@@ -8,8 +8,10 @@
|
|||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
|
"payment_for_document_type",
|
||||||
"member",
|
"member",
|
||||||
"column_break_rqkd",
|
"column_break_rqkd",
|
||||||
|
"payment_for_document",
|
||||||
"billing_name",
|
"billing_name",
|
||||||
"payment_received",
|
"payment_received",
|
||||||
"payment_details_section",
|
"payment_details_section",
|
||||||
@@ -115,11 +117,23 @@
|
|||||||
"fieldname": "amount_with_gst",
|
"fieldname": "amount_with_gst",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Amount with GST"
|
"label": "Amount with GST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "payment_for_document_type",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Payment for Document Type",
|
||||||
|
"options": "\nLMS Course\nLMS Batch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "payment_for_document",
|
||||||
|
"fieldtype": "Dynamic Link",
|
||||||
|
"label": "Payment for Document",
|
||||||
|
"options": "payment_for_document_type"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-09-12 10:40:22.721371",
|
"modified": "2023-10-17 23:17:50.334975",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Payment",
|
"name": "LMS Payment",
|
||||||
|
|||||||
0
lms/lms/doctype/lms_question/__init__.py
Normal file
0
lms/lms/doctype/lms_question/__init__.py
Normal file
8
lms/lms/doctype/lms_question/lms_question.js
Normal file
8
lms/lms/doctype/lms_question/lms_question.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// Copyright (c) 2023, Frappe and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
// frappe.ui.form.on("LMS Question", {
|
||||||
|
// refresh(frm) {
|
||||||
|
|
||||||
|
// },
|
||||||
|
// });
|
||||||
245
lms/lms/doctype/lms_question/lms_question.json
Normal file
245
lms/lms/doctype/lms_question/lms_question.json
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"autoname": "format:QTS-{YYYY}-{#####}",
|
||||||
|
"creation": "2023-10-10 10:24:14.035772",
|
||||||
|
"default_view": "List",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"question",
|
||||||
|
"type",
|
||||||
|
"multiple",
|
||||||
|
"section_break_ytxi",
|
||||||
|
"option_1",
|
||||||
|
"is_correct_1",
|
||||||
|
"column_break_fpvl",
|
||||||
|
"explanation_1",
|
||||||
|
"section_break_eiaa",
|
||||||
|
"option_2",
|
||||||
|
"is_correct_2",
|
||||||
|
"column_break_akwy",
|
||||||
|
"explanation_2",
|
||||||
|
"section_break_cwqv",
|
||||||
|
"option_3",
|
||||||
|
"is_correct_3",
|
||||||
|
"column_break_atpl",
|
||||||
|
"explanation_3",
|
||||||
|
"section_break_yqel",
|
||||||
|
"option_4",
|
||||||
|
"is_correct_4",
|
||||||
|
"column_break_lknb",
|
||||||
|
"explanation_4",
|
||||||
|
"section_break_hkfe",
|
||||||
|
"possibility_1",
|
||||||
|
"possibility_3",
|
||||||
|
"column_break_wpjr",
|
||||||
|
"possibility_2",
|
||||||
|
"possibility_4"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "question",
|
||||||
|
"fieldtype": "Text Editor",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Question"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "type",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Type",
|
||||||
|
"options": "Choices\nUser Input"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:doc.type == \"Choices\";",
|
||||||
|
"fieldname": "section_break_ytxi",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "option_1",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Option 1",
|
||||||
|
"mandatory_depends_on": "eval: doc.type == 'Choices'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "is_correct_1",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Is Correct"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_fpvl",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "explanation_1",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Explanation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:doc.type == \"Choices\";",
|
||||||
|
"fieldname": "section_break_eiaa",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "option_2",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Option 2",
|
||||||
|
"mandatory_depends_on": "eval: doc.type == 'Choices'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "is_correct_2",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Is Correct"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_akwy",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "explanation_2",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Explanation "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval: doc.type == 'Choices'",
|
||||||
|
"fieldname": "section_break_cwqv",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "option_3",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Option 3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "is_correct_3",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Is Correct"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_atpl",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "explanation_3",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Explanation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval: doc.type == 'Choices'",
|
||||||
|
"fieldname": "section_break_yqel",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "option_4",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Option 4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "is_correct_4",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Is Correct"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_lknb",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "explanation_4",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Explanation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "multiple",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Multiple Correct Answers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval: doc.type == 'User Input'",
|
||||||
|
"fieldname": "section_break_hkfe",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_wpjr",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "possibility_1",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Possible Answer 1",
|
||||||
|
"mandatory_depends_on": "eval: doc.type == 'User Input'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "possibility_3",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Possible Answer 3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "possibility_2",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Possible Answer 2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "possibility_4",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Possible Answer 4"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2023-10-18 21:58:42.653317",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "LMS",
|
||||||
|
"name": "LMS Question",
|
||||||
|
"naming_rule": "Expression",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Moderator",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Course Creator",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
|
"title_field": "question"
|
||||||
|
}
|
||||||
92
lms/lms/doctype/lms_question/lms_question.py
Normal file
92
lms/lms/doctype/lms_question/lms_question.py
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# Copyright (c) 2023, Frappe and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
from frappe.model.document import Document
|
||||||
|
from lms.lms.utils import has_course_instructor_role, has_course_moderator_role
|
||||||
|
|
||||||
|
|
||||||
|
class LMSQuestion(Document):
|
||||||
|
def validate(self):
|
||||||
|
validate_correct_answers(self)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_correct_answers(question):
|
||||||
|
if question.type == "Choices":
|
||||||
|
validate_duplicate_options(question)
|
||||||
|
validate_correct_options(question)
|
||||||
|
else:
|
||||||
|
validate_possible_answer(question)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_duplicate_options(question):
|
||||||
|
options = []
|
||||||
|
|
||||||
|
for num in range(1, 5):
|
||||||
|
if question.get(f"option_{num}"):
|
||||||
|
options.append(question.get(f"option_{num}"))
|
||||||
|
|
||||||
|
if len(set(options)) != len(options):
|
||||||
|
frappe.throw(_("Duplicate options found for this question."))
|
||||||
|
|
||||||
|
|
||||||
|
def validate_correct_options(question):
|
||||||
|
correct_options = get_correct_options(question)
|
||||||
|
|
||||||
|
if len(correct_options) > 1:
|
||||||
|
question.multiple = 1
|
||||||
|
|
||||||
|
if not len(correct_options):
|
||||||
|
frappe.throw(_("At least one option must be correct for this question."))
|
||||||
|
|
||||||
|
|
||||||
|
def validate_possible_answer(question):
|
||||||
|
possible_answers = []
|
||||||
|
possible_answers_fields = [
|
||||||
|
"possibility_1",
|
||||||
|
"possibility_2",
|
||||||
|
"possibility_3",
|
||||||
|
"possibility_4",
|
||||||
|
]
|
||||||
|
|
||||||
|
for field in possible_answers_fields:
|
||||||
|
if question.get(field):
|
||||||
|
possible_answers.append(field)
|
||||||
|
|
||||||
|
if not len(possible_answers):
|
||||||
|
frappe.throw(
|
||||||
|
_("Add at least one possible answer for this question: {0}").format(
|
||||||
|
frappe.bold(question.question)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_correct_options(question):
|
||||||
|
correct_options = []
|
||||||
|
correct_option_fields = [
|
||||||
|
"is_correct_1",
|
||||||
|
"is_correct_2",
|
||||||
|
"is_correct_3",
|
||||||
|
"is_correct_4",
|
||||||
|
]
|
||||||
|
for field in correct_option_fields:
|
||||||
|
if question.get(field) == 1:
|
||||||
|
correct_options.append(field)
|
||||||
|
|
||||||
|
return correct_options
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_question_details(question):
|
||||||
|
if not has_course_instructor_role() or not has_course_moderator_role():
|
||||||
|
return
|
||||||
|
|
||||||
|
fields = ["question", "type", "name"]
|
||||||
|
for i in range(1, 5):
|
||||||
|
fields.append(f"option_{i}")
|
||||||
|
fields.append(f"is_correct_{i}")
|
||||||
|
fields.append(f"explanation_{i}")
|
||||||
|
fields.append(f"possibility_{i}")
|
||||||
|
|
||||||
|
return frappe.db.get_value("LMS Question", question, fields, as_dict=1)
|
||||||
9
lms/lms/doctype/lms_question/test_lms_question.py
Normal file
9
lms/lms/doctype/lms_question/test_lms_question.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Copyright (c) 2023, Frappe and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.tests.utils import FrappeTestCase
|
||||||
|
|
||||||
|
|
||||||
|
class TestLMSQuestion(FrappeTestCase):
|
||||||
|
pass
|
||||||
@@ -5,3 +5,13 @@ frappe.ui.form.on("LMS Quiz", {
|
|||||||
// refresh: function(frm) {
|
// refresh: function(frm) {
|
||||||
// }
|
// }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
frappe.ui.form.on("LMS Quiz Question", {
|
||||||
|
marks: function (frm) {
|
||||||
|
total_marks = 0;
|
||||||
|
frm.doc.questions.forEach((question) => {
|
||||||
|
total_marks += question.marks;
|
||||||
|
});
|
||||||
|
frm.doc.total_marks = total_marks;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -12,6 +12,10 @@
|
|||||||
"column_break_gaac",
|
"column_break_gaac",
|
||||||
"max_attempts",
|
"max_attempts",
|
||||||
"show_submission_history",
|
"show_submission_history",
|
||||||
|
"section_break_hsiv",
|
||||||
|
"passing_percentage",
|
||||||
|
"column_break_rocd",
|
||||||
|
"total_marks",
|
||||||
"section_break_sbjx",
|
"section_break_sbjx",
|
||||||
"questions",
|
"questions",
|
||||||
"section_break_3",
|
"section_break_3",
|
||||||
@@ -43,7 +47,7 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "1",
|
"default": "0",
|
||||||
"fieldname": "max_attempts",
|
"fieldname": "max_attempts",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"label": "Max Attempts"
|
"label": "Max Attempts"
|
||||||
@@ -90,11 +94,34 @@
|
|||||||
"fieldname": "show_submission_history",
|
"fieldname": "show_submission_history",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Show Submission History"
|
"label": "Show Submission History"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_hsiv",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "passing_percentage",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": "Passing Percentage",
|
||||||
|
"non_negative": 1,
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_rocd",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "total_marks",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": "Total Marks",
|
||||||
|
"non_negative": 1,
|
||||||
|
"read_only": 1,
|
||||||
|
"reqd": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-07-04 15:26:24.457745",
|
"modified": "2023-10-18 22:50:58.252350",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Quiz",
|
"name": "LMS Quiz",
|
||||||
@@ -123,6 +150,18 @@
|
|||||||
"role": "Moderator",
|
"role": "Moderator",
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Course Creator",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"show_title_field_in_link": 1,
|
"show_title_field_in_link": 1,
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import json
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import cstr
|
from frappe.utils import cstr, comma_and
|
||||||
|
from lms.lms.doctype.lms_question.lms_question import validate_correct_answers
|
||||||
from lms.lms.utils import (
|
from lms.lms.utils import (
|
||||||
generate_slug,
|
generate_slug,
|
||||||
has_course_moderator_role,
|
has_course_moderator_role,
|
||||||
@@ -14,13 +15,22 @@ from lms.lms.utils import (
|
|||||||
|
|
||||||
|
|
||||||
class LMSQuiz(Document):
|
class LMSQuiz(Document):
|
||||||
|
def validate(self):
|
||||||
|
self.validate_duplicate_questions()
|
||||||
|
self.total_marks = set_total_marks(self.name, self.questions)
|
||||||
|
|
||||||
|
def validate_duplicate_questions(self):
|
||||||
|
questions = [row.question for row in self.questions]
|
||||||
|
rows = [i + 1 for i, x in enumerate(questions) if questions.count(x) > 1]
|
||||||
|
if len(rows):
|
||||||
|
frappe.throw(
|
||||||
|
_("Rows {0} have the duplicate questions.").format(frappe.bold(comma_and(rows)))
|
||||||
|
)
|
||||||
|
|
||||||
def autoname(self):
|
def autoname(self):
|
||||||
if not self.name:
|
if not self.name:
|
||||||
self.name = generate_slug(self.title, "LMS Quiz")
|
self.name = generate_slug(self.title, "LMS Quiz")
|
||||||
|
|
||||||
def validate(self):
|
|
||||||
validate_correct_answers(self.questions)
|
|
||||||
|
|
||||||
def get_last_submission_details(self):
|
def get_last_submission_details(self):
|
||||||
"""Returns the latest submission for this user."""
|
"""Returns the latest submission for this user."""
|
||||||
user = frappe.session.user
|
user = frappe.session.user
|
||||||
@@ -39,76 +49,11 @@ class LMSQuiz(Document):
|
|||||||
return result[0]
|
return result[0]
|
||||||
|
|
||||||
|
|
||||||
def get_correct_options(question):
|
def set_total_marks(quiz, questions):
|
||||||
correct_option_fields = [
|
marks = 0
|
||||||
"is_correct_1",
|
|
||||||
"is_correct_2",
|
|
||||||
"is_correct_3",
|
|
||||||
"is_correct_4",
|
|
||||||
]
|
|
||||||
return list(filter(lambda x: question.get(x) == 1, correct_option_fields))
|
|
||||||
|
|
||||||
|
|
||||||
def validate_correct_answers(questions):
|
|
||||||
for question in questions:
|
for question in questions:
|
||||||
if question.type == "Choices":
|
marks += question.get("marks")
|
||||||
validate_duplicate_options(question)
|
return marks
|
||||||
validate_correct_options(question)
|
|
||||||
else:
|
|
||||||
validate_possible_answer(question)
|
|
||||||
|
|
||||||
|
|
||||||
def validate_duplicate_options(question):
|
|
||||||
options = []
|
|
||||||
|
|
||||||
for num in range(1, 5):
|
|
||||||
if question.get(f"option_{num}"):
|
|
||||||
options.append(question.get(f"option_{num}"))
|
|
||||||
|
|
||||||
if len(set(options)) != len(options):
|
|
||||||
frappe.throw(
|
|
||||||
_("Duplicate options found for this question: {0}").format(
|
|
||||||
frappe.bold(question.question)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def validate_correct_options(question):
|
|
||||||
correct_options = get_correct_options(question)
|
|
||||||
|
|
||||||
if len(correct_options) > 1:
|
|
||||||
question.multiple = 1
|
|
||||||
|
|
||||||
if not len(correct_options):
|
|
||||||
frappe.throw(
|
|
||||||
_("At least one option must be correct for this question: {0}").format(
|
|
||||||
frappe.bold(question.question)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def validate_possible_answer(question):
|
|
||||||
possible_answers_fields = [
|
|
||||||
"possibility_1",
|
|
||||||
"possibility_2",
|
|
||||||
"possibility_3",
|
|
||||||
"possibility_4",
|
|
||||||
]
|
|
||||||
possible_answers = list(filter(lambda x: question.get(x), possible_answers_fields))
|
|
||||||
|
|
||||||
if not len(possible_answers):
|
|
||||||
frappe.throw(
|
|
||||||
_("Add at least one possible answer for this question: {0}").format(
|
|
||||||
frappe.bold(question.question)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def update_lesson_info(doc, method):
|
|
||||||
if doc.quiz_id:
|
|
||||||
frappe.db.set_value(
|
|
||||||
"LMS Quiz", doc.quiz_id, {"lesson": doc.name, "course": doc.course}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@@ -118,45 +63,72 @@ def quiz_summary(quiz, results):
|
|||||||
|
|
||||||
for result in results:
|
for result in results:
|
||||||
correct = result["is_correct"][0]
|
correct = result["is_correct"][0]
|
||||||
result["question"] = frappe.db.get_value(
|
|
||||||
"LMS Quiz Question",
|
|
||||||
{"parent": quiz, "idx": result["question_index"] + 1},
|
|
||||||
["question"],
|
|
||||||
)
|
|
||||||
|
|
||||||
for point in result["is_correct"]:
|
for point in result["is_correct"]:
|
||||||
correct = correct and point
|
correct = correct and point
|
||||||
|
|
||||||
result["is_correct"] = correct
|
result["is_correct"] = correct
|
||||||
score += correct
|
|
||||||
|
question_details = frappe.db.get_value(
|
||||||
|
"LMS Quiz Question",
|
||||||
|
{"parent": quiz, "idx": result["question_index"] + 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"]
|
del result["question_index"]
|
||||||
|
|
||||||
|
quiz_details = frappe.db.get_value(
|
||||||
|
"LMS Quiz", quiz, ["total_marks", "passing_percentage"], as_dict=1
|
||||||
|
)
|
||||||
|
score_out_of = quiz_details.total_marks
|
||||||
|
percentage = (score / score_out_of) * 100
|
||||||
|
|
||||||
submission = frappe.get_doc(
|
submission = frappe.get_doc(
|
||||||
{
|
{
|
||||||
"doctype": "LMS Quiz Submission",
|
"doctype": "LMS Quiz Submission",
|
||||||
"quiz": quiz,
|
"quiz": quiz,
|
||||||
"result": results,
|
"result": results,
|
||||||
"score": score,
|
"score": score,
|
||||||
|
"score_out_of": score_out_of,
|
||||||
"member": frappe.session.user,
|
"member": frappe.session.user,
|
||||||
|
"percentage": percentage,
|
||||||
|
"passing_percentage": quiz_details.passing_percentage,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
submission.save(ignore_permissions=True)
|
submission.save(ignore_permissions=True)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"score": score,
|
"score": score,
|
||||||
|
"score_out_of": score_out_of,
|
||||||
"submission": submission.name,
|
"submission": submission.name,
|
||||||
|
"pass": percentage == quiz_details.passing_percentage,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def save_quiz(
|
def save_quiz(
|
||||||
quiz_title, max_attempts=1, quiz=None, show_answers=1, show_submission_history=0
|
quiz_title,
|
||||||
|
passing_percentage,
|
||||||
|
questions,
|
||||||
|
max_attempts=0,
|
||||||
|
quiz=None,
|
||||||
|
show_answers=1,
|
||||||
|
show_submission_history=0,
|
||||||
):
|
):
|
||||||
if not has_course_moderator_role() or not has_course_instructor_role():
|
if not has_course_moderator_role() or not has_course_instructor_role():
|
||||||
return
|
return
|
||||||
|
|
||||||
values = {
|
values = {
|
||||||
"title": quiz_title,
|
"title": quiz_title,
|
||||||
|
"passing_percentage": passing_percentage,
|
||||||
"max_attempts": max_attempts,
|
"max_attempts": max_attempts,
|
||||||
"show_answers": show_answers,
|
"show_answers": show_answers,
|
||||||
"show_submission_history": show_submission_history,
|
"show_submission_history": show_submission_history,
|
||||||
@@ -164,41 +136,77 @@ def save_quiz(
|
|||||||
|
|
||||||
if quiz:
|
if quiz:
|
||||||
frappe.db.set_value("LMS Quiz", quiz, values)
|
frappe.db.set_value("LMS Quiz", quiz, values)
|
||||||
|
update_questions(quiz, questions)
|
||||||
return quiz
|
return quiz
|
||||||
else:
|
else:
|
||||||
doc = frappe.new_doc("LMS Quiz")
|
doc = frappe.new_doc("LMS Quiz")
|
||||||
doc.update(values)
|
doc.update(values)
|
||||||
doc.save(ignore_permissions=True)
|
doc.save()
|
||||||
|
update_questions(doc.name, questions)
|
||||||
return doc.name
|
return doc.name
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
@frappe.whitelist()
|
||||||
def save_question(quiz, values, index):
|
def save_question(quiz, values, index):
|
||||||
values = frappe._dict(json.loads(values))
|
values = frappe._dict(json.loads(values))
|
||||||
validate_correct_answers([values])
|
|
||||||
|
|
||||||
if values.get("name"):
|
if values.get("name"):
|
||||||
doc = frappe.get_doc("LMS Quiz Question", values.get("name"))
|
doc = frappe.get_doc("LMS Question", values.get("name"))
|
||||||
else:
|
else:
|
||||||
doc = frappe.new_doc("LMS Quiz Question")
|
doc = frappe.new_doc("LMS Question")
|
||||||
|
|
||||||
doc.update(
|
doc.update(
|
||||||
{
|
{
|
||||||
"question": values["question"],
|
"question": values.question,
|
||||||
"type": values["type"],
|
"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):
|
for num in range(1, 5):
|
||||||
if values.get(f"option_{num}"):
|
if values.get(f"option_{num}"):
|
||||||
doc.update(
|
doc.update(
|
||||||
@@ -222,9 +230,8 @@ def save_question(quiz, values, index):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
doc.save(ignore_permissions=True)
|
doc.save()
|
||||||
|
return doc.name
|
||||||
return quiz
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@@ -257,9 +264,7 @@ def check_choice_answers(question, answers):
|
|||||||
fields.append(f"option_{cstr(num)}")
|
fields.append(f"option_{cstr(num)}")
|
||||||
fields.append(f"is_correct_{cstr(num)}")
|
fields.append(f"is_correct_{cstr(num)}")
|
||||||
|
|
||||||
question_details = frappe.db.get_value(
|
question_details = frappe.db.get_value("LMS Question", question, fields, as_dict=1)
|
||||||
"LMS Quiz Question", question, fields, as_dict=1
|
|
||||||
)
|
|
||||||
|
|
||||||
for num in range(1, 5):
|
for num in range(1, 5):
|
||||||
if question_details[f"option_{num}"] in answers:
|
if question_details[f"option_{num}"] in answers:
|
||||||
@@ -275,9 +280,7 @@ def check_input_answers(question, answer):
|
|||||||
for num in range(1, 5):
|
for num in range(1, 5):
|
||||||
fields.append(f"possibility_{cstr(num)}")
|
fields.append(f"possibility_{cstr(num)}")
|
||||||
|
|
||||||
question_details = frappe.db.get_value(
|
question_details = frappe.db.get_value("LMS Question", question, fields, as_dict=1)
|
||||||
"LMS Quiz Question", question, fields, as_dict=1
|
|
||||||
)
|
|
||||||
for num in range(1, 5):
|
for num in range(1, 5):
|
||||||
current_possibility = question_details[f"possibility_{num}"]
|
current_possibility = question_details[f"possibility_{num}"]
|
||||||
if current_possibility and current_possibility.lower() == answer.lower():
|
if current_possibility and current_possibility.lower() == answer.lower():
|
||||||
|
|||||||
@@ -10,51 +10,36 @@ import frappe
|
|||||||
class TestLMSQuiz(unittest.TestCase):
|
class TestLMSQuiz(unittest.TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls) -> None:
|
def setUpClass(cls) -> None:
|
||||||
frappe.get_doc({"doctype": "LMS Quiz", "title": "Test Quiz"}).save(
|
frappe.get_doc(
|
||||||
ignore_permissions=True
|
{"doctype": "LMS Quiz", "title": "Test Quiz", "passing_percentage": 90}
|
||||||
)
|
).save(ignore_permissions=True)
|
||||||
|
|
||||||
def test_with_multiple_options(self):
|
def test_with_multiple_options(self):
|
||||||
quiz = frappe.get_doc("LMS Quiz", "test-quiz")
|
question = frappe.new_doc("LMS Question")
|
||||||
quiz.append(
|
question.question = "Question Multiple"
|
||||||
"questions",
|
question.type = "Choices"
|
||||||
{
|
question.option_1 = "Option 1"
|
||||||
"question": "Question Multiple",
|
question.is_correct_1 = 1
|
||||||
"type": "Choices",
|
question.option_2 = "Option 2"
|
||||||
"option_1": "Option 1",
|
question.is_correct_2 = 1
|
||||||
"is_correct_1": 1,
|
question.save()
|
||||||
"option_2": "Option 2",
|
self.assertTrue(question.multiple)
|
||||||
"is_correct_2": 1,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
quiz.save()
|
|
||||||
self.assertTrue(quiz.questions[0].multiple)
|
|
||||||
|
|
||||||
def test_with_no_correct_option(self):
|
def test_with_no_correct_option(self):
|
||||||
quiz = frappe.get_doc("LMS Quiz", "test-quiz")
|
question = frappe.new_doc("LMS Question")
|
||||||
quiz.append(
|
question.question = "Question Multiple"
|
||||||
"questions",
|
question.type = "Choices"
|
||||||
{
|
question.option_1 = "Option 1"
|
||||||
"question": "Question no correct option",
|
question.option_2 = "Option 2"
|
||||||
"type": "Choices",
|
self.assertRaises(frappe.ValidationError, question.save)
|
||||||
"option_1": "Option 1",
|
|
||||||
"option_2": "Option 2",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertRaises(frappe.ValidationError, quiz.save)
|
|
||||||
|
|
||||||
def test_with_no_possible_answers(self):
|
def test_with_no_possible_answers(self):
|
||||||
quiz = frappe.get_doc("LMS Quiz", "test-quiz")
|
question = frappe.new_doc("LMS Question")
|
||||||
quiz.append(
|
question.question = "Question Multiple"
|
||||||
"questions",
|
question.type = "User Input"
|
||||||
{
|
self.assertRaises(frappe.ValidationError, question.save)
|
||||||
"question": "Question Possible Answers",
|
|
||||||
"type": "User Input",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertRaises(frappe.ValidationError, quiz.save)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def tearDownClass(cls) -> None:
|
def tearDownClass(cls) -> None:
|
||||||
frappe.db.delete("LMS Quiz", "test-quiz")
|
frappe.db.delete("LMS Quiz", "test-quiz")
|
||||||
frappe.db.delete("LMS Quiz Question", {"parent": "test-quiz"})
|
frappe.db.delete("LMS Question")
|
||||||
|
|||||||
@@ -6,208 +6,31 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"question",
|
"question",
|
||||||
"type",
|
"marks"
|
||||||
"options_section",
|
|
||||||
"option_1",
|
|
||||||
"is_correct_1",
|
|
||||||
"column_break_5",
|
|
||||||
"explanation_1",
|
|
||||||
"section_break_5",
|
|
||||||
"option_2",
|
|
||||||
"is_correct_2",
|
|
||||||
"column_break_10",
|
|
||||||
"explanation_2",
|
|
||||||
"column_break_4",
|
|
||||||
"option_3",
|
|
||||||
"is_correct_3",
|
|
||||||
"column_break_15",
|
|
||||||
"explanation_3",
|
|
||||||
"section_break_11",
|
|
||||||
"option_4",
|
|
||||||
"is_correct_4",
|
|
||||||
"column_break_20",
|
|
||||||
"explanation_4",
|
|
||||||
"section_break_mnhr",
|
|
||||||
"possibility_1",
|
|
||||||
"possibility_3",
|
|
||||||
"column_break_vnaj",
|
|
||||||
"possibility_2",
|
|
||||||
"possibility_4",
|
|
||||||
"section_break_c1lf",
|
|
||||||
"multiple"
|
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldname": "question",
|
"fieldname": "question",
|
||||||
"fieldtype": "Text Editor",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Question",
|
"label": "Question",
|
||||||
|
"options": "LMS Question",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "option_1",
|
"default": "1",
|
||||||
"fieldtype": "Small Text",
|
"fieldname": "marks",
|
||||||
"label": "Option 1",
|
"fieldtype": "Int",
|
||||||
"mandatory_depends_on": "eval: doc.type == 'Choices'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "option_2",
|
|
||||||
"fieldtype": "Small Text",
|
|
||||||
"label": "Option 2",
|
|
||||||
"mandatory_depends_on": "eval: doc.type == 'Choices'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "option_3",
|
|
||||||
"fieldtype": "Small Text",
|
|
||||||
"label": "Option 3"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "option_4",
|
|
||||||
"fieldtype": "Small Text",
|
|
||||||
"label": "Option 4"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"depends_on": "option_1",
|
|
||||||
"fieldname": "is_correct_1",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Is Correct"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"depends_on": "option_2",
|
|
||||||
"fieldname": "is_correct_2",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Is Correct"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"depends_on": "option_3",
|
|
||||||
"fieldname": "is_correct_3",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Is Correct"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"depends_on": "option_4",
|
|
||||||
"fieldname": "is_correct_4",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Is Correct"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fieldname": "multiple",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"hidden": 1,
|
|
||||||
"label": "Multiple Correct Answers",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "eval: doc.type == 'Choices'",
|
|
||||||
"fieldname": "options_section",
|
|
||||||
"fieldtype": "Section Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "eval: doc.type == 'Choices'",
|
|
||||||
"fieldname": "column_break_4",
|
|
||||||
"fieldtype": "Section Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "eval: doc.type == 'Choices'",
|
|
||||||
"fieldname": "section_break_5",
|
|
||||||
"fieldtype": "Section Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "eval: doc.type == 'Choices'",
|
|
||||||
"fieldname": "section_break_11",
|
|
||||||
"fieldtype": "Section Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "option_1",
|
|
||||||
"fieldname": "explanation_1",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"label": "Explanation"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "option_2",
|
|
||||||
"fieldname": "explanation_2",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"label": "Explanation"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "option_3",
|
|
||||||
"fieldname": "explanation_3",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"label": "Explanation"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "option_4",
|
|
||||||
"fieldname": "explanation_4",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"label": "Explanation"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_5",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_10",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_15",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_20",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "type",
|
|
||||||
"fieldtype": "Select",
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Type",
|
"label": "Marks",
|
||||||
"options": "Choices\nUser Input"
|
"non_negative": 1,
|
||||||
},
|
"reqd": 1
|
||||||
{
|
|
||||||
"depends_on": "eval: doc.type == 'User Input'",
|
|
||||||
"fieldname": "section_break_mnhr",
|
|
||||||
"fieldtype": "Section Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "possibility_1",
|
|
||||||
"fieldtype": "Small Text",
|
|
||||||
"label": "Possible Answer 1",
|
|
||||||
"mandatory_depends_on": "eval: doc.type == 'User Input'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "possibility_2",
|
|
||||||
"fieldtype": "Small Text",
|
|
||||||
"label": "Possible Answer 2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "possibility_3",
|
|
||||||
"fieldtype": "Small Text",
|
|
||||||
"label": "Possible Answer 3"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "possibility_4",
|
|
||||||
"fieldtype": "Small Text",
|
|
||||||
"label": "Possible Answer 4"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "section_break_c1lf",
|
|
||||||
"fieldtype": "Section Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_vnaj",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-07-04 16:43:49.837134",
|
"modified": "2023-10-16 19:51:03.893144",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Quiz Question",
|
"name": "LMS Quiz Question",
|
||||||
|
|||||||
@@ -6,7 +6,11 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"question",
|
"question",
|
||||||
|
"section_break_fztv",
|
||||||
|
"question_name",
|
||||||
"answer",
|
"answer",
|
||||||
|
"column_break_flus",
|
||||||
|
"marks",
|
||||||
"is_correct"
|
"is_correct"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
@@ -31,12 +35,33 @@
|
|||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Is Correct",
|
"label": "Is Correct",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_fztv",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "question_name",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Question Name",
|
||||||
|
"options": "LMS Question"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_flus",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "marks",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Marks",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-11-24 11:15:45.931119",
|
"modified": "2023-10-17 11:55:25.641214",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Quiz Result",
|
"name": "LMS Quiz Result",
|
||||||
|
|||||||
@@ -6,11 +6,16 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"quiz",
|
"quiz",
|
||||||
"score",
|
|
||||||
"course",
|
"course",
|
||||||
"column_break_3",
|
"column_break_3",
|
||||||
"member",
|
"member",
|
||||||
"member_name",
|
"member_name",
|
||||||
|
"section_break_dkpn",
|
||||||
|
"score",
|
||||||
|
"score_out_of",
|
||||||
|
"column_break_gkip",
|
||||||
|
"percentage",
|
||||||
|
"passing_percentage",
|
||||||
"section_break_6",
|
"section_break_6",
|
||||||
"result"
|
"result"
|
||||||
],
|
],
|
||||||
@@ -31,9 +36,11 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "score",
|
"fieldname": "score",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Int",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Score"
|
"label": "Score",
|
||||||
|
"read_only": 1,
|
||||||
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "member",
|
"fieldname": "member",
|
||||||
@@ -65,12 +72,45 @@
|
|||||||
"label": "Course",
|
"label": "Course",
|
||||||
"options": "LMS Course",
|
"options": "LMS Course",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "quiz.total_marks",
|
||||||
|
"fieldname": "score_out_of",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": "Score Out Of",
|
||||||
|
"non_negative": 1,
|
||||||
|
"read_only": 1,
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_dkpn",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_gkip",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "percentage",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": "Percentage",
|
||||||
|
"non_negative": 1,
|
||||||
|
"read_only": 1,
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "passing_percentage",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": "Passing Percentage",
|
||||||
|
"non_negative": 1,
|
||||||
|
"read_only": 1,
|
||||||
|
"reqd": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"in_create": 1,
|
"in_create": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-11-15 15:27:07.770945",
|
"modified": "2023-10-17 13:07:27.979975",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Quiz Submission",
|
"name": "LMS Quiz Submission",
|
||||||
|
|||||||
@@ -6,4 +6,10 @@ from frappe.model.document import Document
|
|||||||
|
|
||||||
|
|
||||||
class LMSQuizSubmission(Document):
|
class LMSQuizSubmission(Document):
|
||||||
pass
|
def before_insert(self):
|
||||||
|
if not self.percentage:
|
||||||
|
self.set_percentage()
|
||||||
|
|
||||||
|
def set_percentage(self):
|
||||||
|
if self.score and self.score_out_of:
|
||||||
|
self.percentage = (self.score / self.score_out_of) * 100
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import frappe
|
|||||||
import json
|
import json
|
||||||
import razorpay
|
import razorpay
|
||||||
import requests
|
import requests
|
||||||
import base64
|
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.desk.doctype.dashboard_chart.dashboard_chart import get_result
|
from frappe.desk.doctype.dashboard_chart.dashboard_chart import get_result
|
||||||
from frappe.desk.doctype.notification_log.notification_log import make_notification_logs
|
from frappe.desk.doctype.notification_log.notification_log import make_notification_logs
|
||||||
@@ -1029,6 +1028,8 @@ def record_payment(address, response, client, doctype, docname):
|
|||||||
"amount_with_gst": payment_details["amount_with_gst"],
|
"amount_with_gst": payment_details["amount_with_gst"],
|
||||||
"gstin": address.gstin,
|
"gstin": address.gstin,
|
||||||
"pan": address.pan,
|
"pan": address.pan,
|
||||||
|
"payment_for_document_type": doctype,
|
||||||
|
"payment_for_document": docname,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
payment_doc.save(ignore_permissions=True)
|
payment_doc.save(ignore_permissions=True)
|
||||||
|
|||||||
@@ -71,4 +71,8 @@ lms.patches.v1_0.publish_batches
|
|||||||
lms.patches.v1_0.publish_certificates
|
lms.patches.v1_0.publish_certificates
|
||||||
lms.patches.v1_0.change_naming_for_batch_course #14-09-2023
|
lms.patches.v1_0.change_naming_for_batch_course #14-09-2023
|
||||||
execute:frappe.permissions.reset_perms("LMS Enrollment")
|
execute:frappe.permissions.reset_perms("LMS Enrollment")
|
||||||
lms.patches.v1_0.create_student_role
|
lms.patches.v1_0.create_student_role
|
||||||
|
lms.patches.v1_0.mark_confirmation_for_batch_students
|
||||||
|
lms.patches.v1_0.create_quiz_questions
|
||||||
|
lms.patches.v1_0.add_default_marks #16-10-2023
|
||||||
|
lms.patches.v1_0.add_certificate_template #26-10-2023
|
||||||
20
lms/patches/v1_0/add_certificate_template.py
Normal file
20
lms/patches/v1_0/add_certificate_template.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
frappe.reload_doc("lms", "doctype", "lms_certificate")
|
||||||
|
default_certificate_template = frappe.db.get_value(
|
||||||
|
"Property Setter",
|
||||||
|
{
|
||||||
|
"doc_type": "LMS Certificate",
|
||||||
|
"property": "default_print_format",
|
||||||
|
},
|
||||||
|
"value",
|
||||||
|
)
|
||||||
|
|
||||||
|
if frappe.db.exists("Print Format", default_certificate_template):
|
||||||
|
certificates = frappe.get_all("LMS Certificate", pluck="name")
|
||||||
|
for certificate in certificates:
|
||||||
|
frappe.db.set_value(
|
||||||
|
"LMS Certificate", certificate, "template", default_certificate_template
|
||||||
|
)
|
||||||
18
lms/patches/v1_0/add_default_marks.py
Normal file
18
lms/patches/v1_0/add_default_marks.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
frappe.reload_doc("lms", "doctype", "lms_quiz_question")
|
||||||
|
frappe.reload_doc("lms", "doctype", "lms_quiz")
|
||||||
|
questions = frappe.get_all("LMS Quiz Question", pluck="name")
|
||||||
|
|
||||||
|
for question in questions:
|
||||||
|
frappe.db.set_value("LMS Quiz Question", question, "marks", 1)
|
||||||
|
|
||||||
|
quizzes = frappe.get_all("LMS Quiz", pluck="name")
|
||||||
|
|
||||||
|
for quiz in quizzes:
|
||||||
|
questions_count = frappe.db.count("LMS Quiz Question", {"parent": quiz})
|
||||||
|
frappe.db.set_value(
|
||||||
|
"LMS Quiz", quiz, {"total_marks": questions_count, "passing_percentage": 100}
|
||||||
|
)
|
||||||
43
lms/patches/v1_0/create_quiz_questions.py
Normal file
43
lms/patches/v1_0/create_quiz_questions.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
frappe.reload_doc("lms", "doctype", "lms_question")
|
||||||
|
|
||||||
|
fields = ["name", "question", "type", "multiple"]
|
||||||
|
for num in range(1, 5):
|
||||||
|
fields.append(f"option_{num}")
|
||||||
|
fields.append(f"is_correct_{num}")
|
||||||
|
fields.append(f"explanation_{num}")
|
||||||
|
fields.append(f"possibility_{num}")
|
||||||
|
|
||||||
|
questions = frappe.get_all(
|
||||||
|
"LMS Quiz Question",
|
||||||
|
fields=fields,
|
||||||
|
)
|
||||||
|
|
||||||
|
for question in questions:
|
||||||
|
print(question.name)
|
||||||
|
doc = frappe.new_doc("LMS Question")
|
||||||
|
doc.update(
|
||||||
|
{
|
||||||
|
"question": question.question,
|
||||||
|
"type": question.type,
|
||||||
|
"multiple": question.multiple,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
for num in range(1, 5):
|
||||||
|
if question.get(f"option_{num}"):
|
||||||
|
doc.update(
|
||||||
|
{
|
||||||
|
f"option_{num}": question[f"option_{num}"],
|
||||||
|
f"is_correct_{num}": question[f"is_correct_{num}"],
|
||||||
|
f"explanation_{num}": question[f"explanation_{num}"],
|
||||||
|
f"possibility_{num}": question[f"possibility_{num}"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
doc.save()
|
||||||
|
print(doc.name)
|
||||||
|
frappe.db.set_value("LMS Quiz Question", question.name, "question", doc.name)
|
||||||
9
lms/patches/v1_0/mark_confirmation_for_batch_students.py
Normal file
9
lms/patches/v1_0/mark_confirmation_for_batch_students.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
frappe.reload_doc("lms", "doctype", "batch_student")
|
||||||
|
students = frappe.get_all("Batch Student", pluck="name")
|
||||||
|
|
||||||
|
for student in students:
|
||||||
|
frappe.db.set_value("Batch Student", student, "confirmation_email_sent", 1)
|
||||||
@@ -109,7 +109,39 @@ def quiz_renderer(quiz_name):
|
|||||||
)
|
)
|
||||||
+"</div>"
|
+"</div>"
|
||||||
|
|
||||||
quiz = frappe.get_doc("LMS Quiz", quiz_name)
|
quiz = frappe.db.get_value(
|
||||||
|
"LMS Quiz",
|
||||||
|
quiz_name,
|
||||||
|
[
|
||||||
|
"name",
|
||||||
|
"title",
|
||||||
|
"max_attempts",
|
||||||
|
"show_answers",
|
||||||
|
"show_submission_history",
|
||||||
|
"passing_percentage",
|
||||||
|
],
|
||||||
|
as_dict=True,
|
||||||
|
)
|
||||||
|
quiz.questions = []
|
||||||
|
fields = ["name", "question", "type", "multiple"]
|
||||||
|
for num in range(1, 5):
|
||||||
|
fields.append(f"option_{num}")
|
||||||
|
fields.append(f"is_correct_{num}")
|
||||||
|
fields.append(f"explanation_{num}")
|
||||||
|
fields.append(f"possibility_{num}")
|
||||||
|
|
||||||
|
questions = frappe.get_all(
|
||||||
|
"LMS Quiz Question",
|
||||||
|
filters={"parent": quiz.name},
|
||||||
|
fields=["question", "marks"],
|
||||||
|
order_by="idx",
|
||||||
|
)
|
||||||
|
|
||||||
|
for question in questions:
|
||||||
|
details = frappe.db.get_value("LMS Question", question.question, fields, as_dict=1)
|
||||||
|
details["marks"] = question.marks
|
||||||
|
quiz.questions.append(details)
|
||||||
|
|
||||||
no_of_attempts = frappe.db.count(
|
no_of_attempts = frappe.db.count(
|
||||||
"LMS Quiz Submission", {"owner": frappe.session.user, "quiz": quiz_name}
|
"LMS Quiz Submission", {"owner": frappe.session.user, "quiz": quiz_name}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -785,12 +785,13 @@ input[type=checkbox] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumb {
|
.breadcrumb {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: var(--text-base);
|
font-size: var(--text-base);
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
color: var(--gray-900);
|
color: var(--gray-900);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.course-details-outline {
|
.course-details-outline {
|
||||||
@@ -2473,4 +2474,12 @@ select {
|
|||||||
|
|
||||||
.modal-body .ql-container {
|
.modal-body .ql-container {
|
||||||
max-height: unset !important;
|
max-height: unset !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.questions-table .row-index {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-color {
|
||||||
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,15 @@
|
|||||||
{{ _("This quiz consists of {0} questions.").format(quiz.questions | length) }}
|
{{ _("This quiz consists of {0} questions.").format(quiz.questions | length) }}
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
{% if quiz.passing_percentage %}
|
||||||
|
<li>
|
||||||
|
{{ _("You will have to get {0}% correct answers in order to pass the quiz.").format(quiz.passing_percentage) }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{{ _("Without passing the quiz you won't be able to complete the lesson.") }}
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if quiz.max_attempts %}
|
{% if quiz.max_attempts %}
|
||||||
{% set suffix = "times" if quiz.max_attempts > 1 else "time" %}
|
{% set suffix = "times" if quiz.max_attempts > 1 else "time" %}
|
||||||
<li>
|
<li>
|
||||||
@@ -18,8 +27,7 @@
|
|||||||
{{ _("The quiz has a time limit. For each question you will be given {0} seconds.").format(quiz.time) }}
|
{{ _("The quiz has a time limit. For each question you will be given {0} seconds.").format(quiz.time) }}
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
<div id="start-banner" class="common-card-style column-card align-items-center">
|
<div id="start-banner" class="common-card-style column-card align-items-center">
|
||||||
|
|
||||||
@@ -50,8 +58,12 @@
|
|||||||
<div class="question hide" data-name="{{ question.name }}" data-type="{{ question.type }}"
|
<div class="question hide" data-name="{{ question.name }}" data-type="{{ question.type }}"
|
||||||
data-multi="{{ question.multiple }}" data-qt-index="{{ loop.index }}">
|
data-multi="{{ question.multiple }}" data-qt-index="{{ loop.index }}">
|
||||||
<div>
|
<div>
|
||||||
|
<div class="pull-right font-weight-bold">
|
||||||
|
{{ question.marks }} {{ _("Marks") }}
|
||||||
|
</div>
|
||||||
<div class="question-number">
|
<div class="question-number">
|
||||||
{{ _("Question ") }}{{ loop.index }}: {{ instruction }}</div>
|
{{ _("Question ") }}{{ loop.index }}: {{ instruction }}
|
||||||
|
</div>
|
||||||
<div class="question-text">
|
<div class="question-text">
|
||||||
{{ question.question }}
|
{{ question.question }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -120,7 +120,6 @@ const enable_check = (e) => {
|
|||||||
const quiz_summary = (e = undefined) => {
|
const quiz_summary = (e = undefined) => {
|
||||||
e && e.preventDefault();
|
e && e.preventDefault();
|
||||||
let quiz_name = $("#quiz-title").data("name");
|
let quiz_name = $("#quiz-title").data("name");
|
||||||
let total_questions = $(".question").length;
|
|
||||||
let self = this;
|
let self = this;
|
||||||
|
|
||||||
frappe.call({
|
frappe.call({
|
||||||
@@ -136,13 +135,16 @@ const quiz_summary = (e = undefined) => {
|
|||||||
$("#quiz-form").prepend(
|
$("#quiz-form").prepend(
|
||||||
`<div class="summary bold-heading text-center">
|
`<div class="summary bold-heading text-center">
|
||||||
${__("Your score is")} ${data.message.score}
|
${__("Your score is")} ${data.message.score}
|
||||||
${__("out of")} ${total_questions}
|
${__("out of")} ${data.message.score_out_of}
|
||||||
</div>`
|
</div>`
|
||||||
);
|
);
|
||||||
$("#try-again").attr("data-submission", data.message.submission);
|
$("#try-again").attr("data-submission", data.message.submission);
|
||||||
$("#try-again").removeClass("hide");
|
$("#try-again").removeClass("hide");
|
||||||
self.quiz_submitted = true;
|
self.quiz_submitted = true;
|
||||||
if (this.hasOwnProperty("marked_as_complete")) {
|
if (
|
||||||
|
this.hasOwnProperty("marked_as_complete") &&
|
||||||
|
data.message.pass
|
||||||
|
) {
|
||||||
mark_progress();
|
mark_progress();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ def get_context(context):
|
|||||||
context.no_cache = 1
|
context.no_cache = 1
|
||||||
|
|
||||||
if frappe.session.user == "Guest":
|
if frappe.session.user == "Guest":
|
||||||
raise frappe.PermissionError(_("You don't have permission to access this page."))
|
raise frappe.PermissionError(_("Please login to submit the assignment."))
|
||||||
|
|
||||||
context.is_moderator = has_course_moderator_role()
|
context.is_moderator = has_course_moderator_role()
|
||||||
submission = frappe.form_dict["submission"]
|
submission = frappe.form_dict["submission"]
|
||||||
|
|||||||
@@ -127,7 +127,7 @@
|
|||||||
{%- block script %}
|
{%- block script %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@latest"></script>
|
<script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@latest"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/paragraph@latest"></script>
|
<script src="https://cdn.jsdelivr.net/npm/@editorjs/paragraph@2.10.0"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/header@latest"></script>
|
<script src="https://cdn.jsdelivr.net/npm/@editorjs/header@latest"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/embed@latest"></script>
|
<script src="https://cdn.jsdelivr.net/npm/@editorjs/embed@latest"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -429,9 +429,9 @@ class Quiz {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render_quiz(quiz) {
|
render_quiz(quiz) {
|
||||||
return `<div class="common-card-style p-2 my-2 bold-heading">
|
return `<a class="common-card-style p-20 my-2 justify-center bold-heading" target="_blank" href=/quizzes/${quiz}>
|
||||||
Quiz: ${quiz}
|
Quiz: ${quiz}
|
||||||
</div>`;
|
</a>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
validate(savedData) {
|
validate(savedData) {
|
||||||
|
|||||||
@@ -16,25 +16,9 @@
|
|||||||
{% macro QuizForm(quiz) %}
|
{% macro QuizForm(quiz) %}
|
||||||
<div id="quiz-form" {% if quiz.name %} data-name="{{ quiz.name }}" data-index="{{ quiz.questions | length }}" {% endif %}>
|
<div id="quiz-form" {% if quiz.name %} data-name="{{ quiz.name }}" data-index="{{ quiz.questions | length }}" {% endif %}>
|
||||||
{{ QuizDetails(quiz) }}
|
{{ QuizDetails(quiz) }}
|
||||||
{% if quiz.questions %}
|
<div class="field-group">
|
||||||
<div class="field-group">
|
<div class="questions-table"></div>
|
||||||
<div class="field-label mb-1">
|
</div>
|
||||||
{{ _("Questions") }}
|
|
||||||
</div>
|
|
||||||
<div class="common-card-style column-card px-3 py-0">
|
|
||||||
{% for question in quiz.questions %}
|
|
||||||
{{ Question(question, loop.index) }}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-secondary btn-sm btn-add-question mt-4">
|
|
||||||
{{ _("Add Question") }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if quiz.name and not quiz.questions | length %}
|
|
||||||
{{ EmptyState() }}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
@@ -59,11 +43,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="align-self-center">
|
<div class="align-self-center">
|
||||||
{% if quiz.name %}
|
|
||||||
<button class="btn btn-secondary btn-sm btn-add-question mr-2">
|
|
||||||
{{ _("Add Question") }}
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
<button class="btn btn-primary btn-sm btn-save-quiz">
|
<button class="btn btn-primary btn-sm btn-save-quiz">
|
||||||
{{ _("Save") }}
|
{{ _("Save") }}
|
||||||
</button>
|
</button>
|
||||||
@@ -98,18 +77,30 @@
|
|||||||
{{ _("Enter the maximum number of times a user can attempt this quiz") }}
|
{{ _("Enter the maximum number of times a user can attempt this quiz") }}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{% set max_attempts = quiz.max_attempts if quiz.name else 1 %}
|
{% set max_attempts = quiz.max_attempts if quiz.name else 0 %}
|
||||||
<input type="number" class="field-input" id="max-attempts" value="{{ max_attempts }}">
|
<input type="number" class="field-input" id="max-attempts" value="{{ max_attempts }}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="field-group">
|
||||||
|
<div class="field-label reqd">
|
||||||
|
{{ _("Passing Percentage") }}
|
||||||
|
</div>
|
||||||
|
<div class="field-description">
|
||||||
|
{{ _("Minimum percentage required to pass this quiz.") }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="number" class="field-input" id="passing-percentage" value="{{ quiz.passing_percentage }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="field-group vertically-center">
|
<div class="field-group vertically-center">
|
||||||
{% set show_answers = quiz.show_answers or not quiz.name %}
|
{% set show_answers = quiz.show_answers or not quiz.name %}
|
||||||
<label for="show-answers" class="vertically-center mb-0">
|
<label for="show-answers" class="vertically-center mb-0">
|
||||||
<input type="checkbox" id="show-answers" {% if show_answers %} checked {% endif %}>
|
<input type="checkbox" id="show-answers" {% if show_answers %} checked {% endif %}>
|
||||||
{{ _("Show Answers") }}
|
{{ _("Show Answers") }}
|
||||||
</label>
|
</label>
|
||||||
<label for="upcoming" class="vertically-center mb-0 ml-20">
|
<label for="show-submission-history" class="vertically-center mb-0 ml-20">
|
||||||
<input type="checkbox" id="show-submission-history" {% if quiz.show_submission_history %} checked {% endif %}>
|
<input type="checkbox" id="show-submission-history" {% if quiz.show_submission_history %} checked {% endif %}>
|
||||||
{{ _("Show Submission History") }}
|
{{ _("Show Submission History") }}
|
||||||
</label>
|
</label>
|
||||||
@@ -151,5 +142,9 @@
|
|||||||
|
|
||||||
{%- block script %}
|
{%- block script %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
{{ include_script('controls.bundle.js') }}
|
{% if has_course_instructor_role() or has_course_moderator_role() %}
|
||||||
|
<script>
|
||||||
|
const quiz_questions = {{ quiz.questions or [] }}
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -1,17 +1,21 @@
|
|||||||
frappe.ready(() => {
|
frappe.ready(() => {
|
||||||
$(".btn-save-quiz").click((e) => {
|
if ($(".questions-table").length) {
|
||||||
save_quiz({
|
frappe.require("controls.bundle.js", () => {
|
||||||
quiz_title: $("#quiz-title").val(),
|
create_questions_table();
|
||||||
max_attempts: $("#max-attempts").val(),
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(".btn-save-quiz").click((e) => {
|
||||||
|
save_quiz();
|
||||||
});
|
});
|
||||||
|
|
||||||
$(".question-row").click((e) => {
|
$(".question-row").click((e) => {
|
||||||
edit_question(e);
|
edit_question(e);
|
||||||
});
|
});
|
||||||
|
|
||||||
$(".btn-add-question").click((e) => {
|
$(document).on("click", ".questions-table .link-btn", (e) => {
|
||||||
show_question_modal();
|
e.preventDefault();
|
||||||
|
fetch_question_data(e);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -31,6 +35,8 @@ const show_question_modal = (values = {}) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const get_question_fields = (values = {}) => {
|
const get_question_fields = (values = {}) => {
|
||||||
|
if (!values.question) values = {};
|
||||||
|
|
||||||
let dialog_fields = [
|
let dialog_fields = [
|
||||||
{
|
{
|
||||||
fieldtype: "Text Editor",
|
fieldtype: "Text Editor",
|
||||||
@@ -66,6 +72,7 @@ const get_question_fields = (values = {}) => {
|
|||||||
if (num <= 2) option.mandatory_depends_on = "eval:doc.type=='Choices'";
|
if (num <= 2) option.mandatory_depends_on = "eval:doc.type=='Choices'";
|
||||||
|
|
||||||
dialog_fields.push(option);
|
dialog_fields.push(option);
|
||||||
|
console.log(dialog_fields);
|
||||||
|
|
||||||
dialog_fields.push({
|
dialog_fields.push({
|
||||||
fieldtype: "Data",
|
fieldtype: "Data",
|
||||||
@@ -120,12 +127,16 @@ const edit_question = (e) => {
|
|||||||
|
|
||||||
const save_quiz = (values) => {
|
const save_quiz = (values) => {
|
||||||
validate_mandatory();
|
validate_mandatory();
|
||||||
|
validate_questions();
|
||||||
|
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "lms.lms.doctype.lms_quiz.lms_quiz.save_quiz",
|
method: "lms.lms.doctype.lms_quiz.lms_quiz.save_quiz",
|
||||||
args: {
|
args: {
|
||||||
quiz_title: values.quiz_title,
|
quiz_title: $("#quiz-title").val(),
|
||||||
max_attempts: values.max_attempts,
|
max_attempts: $("#max-attempts").val(),
|
||||||
|
passing_percentage: $("#passing-percentage").val(),
|
||||||
quiz: $("#quiz-form").data("name") || "",
|
quiz: $("#quiz-form").data("name") || "",
|
||||||
|
questions: this.table.get_value("questions"),
|
||||||
show_answers: $("#show-answers").is(":checked") ? 1 : 0,
|
show_answers: $("#show-answers").is(":checked") ? 1 : 0,
|
||||||
show_submission_history: $("#show-submission-history").is(
|
show_submission_history: $("#show-submission-history").is(
|
||||||
":checked"
|
":checked"
|
||||||
@@ -146,13 +157,45 @@ const save_quiz = (values) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const validate_mandatory = () => {
|
const validate_mandatory = () => {
|
||||||
if (!$("#quiz-title").val()) {
|
let fields = ["#quiz-title", "#passing-percentage"];
|
||||||
let error = $("p")
|
fields.forEach((field, idx) => {
|
||||||
.addClass("error-message")
|
if (!$(field).val()) {
|
||||||
.text(__("Please enter a Quiz Title"));
|
let error = $("p")
|
||||||
$(error).insertAfter("#quiz-title");
|
.addClass("error-message")
|
||||||
$("#quiz-title").focus();
|
.text(__("Please enter a value"));
|
||||||
throw "Title is mandatory";
|
$(error).insertAfter(field);
|
||||||
|
scroll_to_element($(field));
|
||||||
|
throw "This field is mandatory";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const validate_questions = () => {
|
||||||
|
let questions = this.table.get_value("questions");
|
||||||
|
|
||||||
|
if (!questions.length) {
|
||||||
|
frappe.throw(__("Please add a question."));
|
||||||
|
}
|
||||||
|
|
||||||
|
questions.forEach((question, index) => {
|
||||||
|
if (!question.question) {
|
||||||
|
frappe.throw(__("Please add question in row") + " " + (index + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!question.marks) {
|
||||||
|
frappe.throw(__("Please add marks in row") + " " + (index + 1));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const scroll_to_element = (element) => {
|
||||||
|
if ($(element).length) {
|
||||||
|
$([document.documentElement, document.body]).animate(
|
||||||
|
{
|
||||||
|
scrollTop: $(element).offset().top - 100,
|
||||||
|
},
|
||||||
|
1000
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -167,13 +210,98 @@ const save_question = (values) => {
|
|||||||
callback: (data) => {
|
callback: (data) => {
|
||||||
if (data.message) this.question_dialog.hide();
|
if (data.message) this.question_dialog.hide();
|
||||||
|
|
||||||
frappe.show_alert({
|
if (values.name) {
|
||||||
message: __("Saved"),
|
frappe.show_alert({
|
||||||
indicator: "green",
|
message: __("Saved"),
|
||||||
});
|
indicator: "green",
|
||||||
setTimeout(() => {
|
});
|
||||||
window.location.reload();
|
setTimeout(() => {
|
||||||
}, 1000);
|
window.location.reload();
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
let details = {
|
||||||
|
question: data.message,
|
||||||
|
};
|
||||||
|
index = this.table.get_value("questions").length;
|
||||||
|
add_question_row(details, index);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const create_questions_table = () => {
|
||||||
|
this.table = new frappe.ui.FieldGroup({
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
fieldname: "questions",
|
||||||
|
fieldtype: "Table",
|
||||||
|
in_place_edit: 1,
|
||||||
|
label: __("Questions"),
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
fieldname: "question",
|
||||||
|
fieldtype: "Link",
|
||||||
|
label: __("Question"),
|
||||||
|
options: "LMS Question",
|
||||||
|
in_list_view: 1,
|
||||||
|
only_select: 1,
|
||||||
|
reqd: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "marks",
|
||||||
|
fieldtype: "Int",
|
||||||
|
label: __("Marks"),
|
||||||
|
in_list_view: 1,
|
||||||
|
reqd: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "question_name",
|
||||||
|
fieldname: "Link",
|
||||||
|
options: "LMS Quiz Question",
|
||||||
|
label: __("Question Name"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
body: $(".questions-table").get(0),
|
||||||
|
});
|
||||||
|
this.table.make();
|
||||||
|
$(".questions-table .form-section:last").removeClass("empty-section");
|
||||||
|
$(".questions-table .frappe-control").removeClass("hide-control");
|
||||||
|
$(".questions-table .form-column").addClass("p-0");
|
||||||
|
|
||||||
|
quiz_questions.forEach((question, idx) => {
|
||||||
|
add_question_row(question, idx);
|
||||||
|
});
|
||||||
|
this.table.fields_dict["questions"].grid.add_custom_button(
|
||||||
|
"New Question",
|
||||||
|
show_question_modal,
|
||||||
|
"bottom"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const add_question_row = (question, idx) => {
|
||||||
|
this.table.fields_dict["questions"].grid.add_new_row();
|
||||||
|
this.table.get_value("questions")[idx] = {
|
||||||
|
question: question.question,
|
||||||
|
marks: question.marks,
|
||||||
|
};
|
||||||
|
this.table.refresh();
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetch_question_data = (e) => {
|
||||||
|
let question_name = $(e.currentTarget)
|
||||||
|
.find(".btn-open")
|
||||||
|
.attr("href")
|
||||||
|
.split("/")[3];
|
||||||
|
|
||||||
|
frappe.call({
|
||||||
|
method: "lms.lms.doctype.lms_question.lms_question.get_question_details",
|
||||||
|
args: {
|
||||||
|
question: question_name,
|
||||||
|
},
|
||||||
|
callback: (data) => {
|
||||||
|
show_question_modal(data.message);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,14 +18,22 @@ def get_context(context):
|
|||||||
if quizname == "new-quiz":
|
if quizname == "new-quiz":
|
||||||
context.quiz = frappe._dict()
|
context.quiz = frappe._dict()
|
||||||
else:
|
else:
|
||||||
fields_arr = ["name", "question", "type"]
|
|
||||||
|
|
||||||
context.quiz = frappe.db.get_value(
|
context.quiz = frappe.db.get_value(
|
||||||
"LMS Quiz",
|
"LMS Quiz",
|
||||||
quizname,
|
quizname,
|
||||||
["title", "name", "max_attempts", "show_answers", "show_submission_history"],
|
[
|
||||||
|
"title",
|
||||||
|
"name",
|
||||||
|
"max_attempts",
|
||||||
|
"passing_percentage",
|
||||||
|
"show_answers",
|
||||||
|
"show_submission_history",
|
||||||
|
],
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fields_arr = ["name", "question", "marks"]
|
||||||
context.quiz.questions = frappe.get_all(
|
context.quiz.questions = frappe.get_all(
|
||||||
"LMS Quiz Question", {"parent": quizname}, fields_arr, order_by="idx"
|
"LMS Quiz Question", {"parent": quizname}, fields_arr, order_by="idx"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -75,14 +75,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if is_moderator %}
|
|
||||||
<div class="mt-4">
|
|
||||||
<button class="btn btn-secondary btn-sm btn-email">
|
|
||||||
{{ _("Email to Students") }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if batch_info.custom_component %}
|
{% if batch_info.custom_component %}
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
{{ batch_info.custom_component }}
|
{{ batch_info.custom_component }}
|
||||||
@@ -140,6 +132,15 @@
|
|||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" data-toggle="tab" href="#emails">
|
||||||
|
{{ _("Emails") }}
|
||||||
|
<span class="course-list-count">
|
||||||
|
{{ batch_emails | length }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if batch_students | length and (is_moderator or is_student) %}
|
{% if batch_students | length and (is_moderator or is_student) %}
|
||||||
@@ -192,6 +193,10 @@
|
|||||||
<div class="tab-pane" id="assessments" role="tabpanel" aria-labelledby="assessments">
|
<div class="tab-pane" id="assessments" role="tabpanel" aria-labelledby="assessments">
|
||||||
{{ AssessmentsSection(batch_info) }}
|
{{ AssessmentsSection(batch_info) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-pane" id="emails" role="tabpanel" aria-labelledby="emails">
|
||||||
|
{{ EmailsSection() }}
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if batch_students | length and (is_moderator or is_student or is_evaluator) %}
|
{% if batch_students | length and (is_moderator or is_student or is_evaluator) %}
|
||||||
@@ -376,6 +381,41 @@
|
|||||||
</article>
|
</article>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
|
{% macro EmailsSection() %}
|
||||||
|
<div class="my-4">
|
||||||
|
<button class="btn btn-secondary btn-sm btn-email">
|
||||||
|
{{ _("Email to Students") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{% for email in batch_emails %}
|
||||||
|
<div class="frappe-card mb-5">
|
||||||
|
<div class="flex justify-between m-1">
|
||||||
|
<span class="text-color flex">
|
||||||
|
<span class="margin-right">
|
||||||
|
{% set member = frappe.db.get_value("User", email.sender, ["full_name", "username", "name", "user_image"], as_dict=1) %}
|
||||||
|
{{ widgets.Avatar(member=member, avatar_class="avatar-small") }}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{{ member.full_name }}
|
||||||
|
<div class="text-muted">
|
||||||
|
<span class="frappe-timestamp" data-timestamp="{{ email.communication_date }}" title="{{ communication_date }}">
|
||||||
|
{{ frappe.utils.pretty_date(email.communication_date) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="ml-10">
|
||||||
|
{{ email.content }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
{% macro AssessmentList(assessments) %}
|
{% macro AssessmentList(assessments) %}
|
||||||
{% if assessments | length %}
|
{% if assessments | length %}
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
|
|||||||
@@ -653,6 +653,7 @@ const setup_calendar = (events) => {
|
|||||||
const options = get_calendar_options(element, calendar_id);
|
const options = get_calendar_options(element, calendar_id);
|
||||||
const calendar = new Calendar(container, options);
|
const calendar = new Calendar(container, options);
|
||||||
this.calendar_ = calendar;
|
this.calendar_ = calendar;
|
||||||
|
|
||||||
create_events(calendar, events);
|
create_events(calendar, events);
|
||||||
add_links_to_events(calendar, events);
|
add_links_to_events(calendar, events);
|
||||||
scroll_to_date(calendar, events);
|
scroll_to_date(calendar, events);
|
||||||
@@ -685,7 +686,9 @@ const get_calendar_options = (element, calendar_id) => {
|
|||||||
],
|
],
|
||||||
template: {
|
template: {
|
||||||
time: function (event) {
|
time: function (event) {
|
||||||
|
let hide = event.raw.completed ? "" : "hide";
|
||||||
return `<div class="calendar-event-time">
|
return `<div class="calendar-event-time">
|
||||||
|
<img class='icon icon-sm pull-right ${hide}' src="/assets/lms/icons/check.svg">
|
||||||
<div> ${frappe.datetime.get_time(event.start.d.d)} -
|
<div> ${frappe.datetime.get_time(event.start.d.d)} -
|
||||||
${frappe.datetime.get_time(event.end.d.d)} </div>
|
${frappe.datetime.get_time(event.end.d.d)} </div>
|
||||||
<div class="calendar-event-title"> ${event.title} </div>
|
<div class="calendar-event-title"> ${event.title} </div>
|
||||||
@@ -716,6 +719,11 @@ const create_events = (calendar, events, calendar_id) => {
|
|||||||
},
|
},
|
||||||
raw: {
|
raw: {
|
||||||
url: event.url,
|
url: event.url,
|
||||||
|
milestone: event.milestone,
|
||||||
|
name: event.name,
|
||||||
|
idx: event.idx,
|
||||||
|
parent: event.parent,
|
||||||
|
completed: event.completed,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -729,9 +737,28 @@ const add_links_to_events = (calendar) => {
|
|||||||
event_date = moment(event_date).format("YYYY-MM-DD");
|
event_date = moment(event_date).format("YYYY-MM-DD");
|
||||||
|
|
||||||
let current_date = moment().format("YYYY-MM-DD");
|
let current_date = moment().format("YYYY-MM-DD");
|
||||||
if (allow_future || moment(event_date).isSameOrBefore(current_date)) {
|
|
||||||
window.open(event.raw.url, "_blank");
|
if (!moment(event_date).isSameOrBefore(current_date) && !allow_future)
|
||||||
}
|
return;
|
||||||
|
|
||||||
|
if (event.raw.milestone) {
|
||||||
|
frappe.call({
|
||||||
|
method: "lms.lms.doctype.lms_batch.lms_batch.is_milestone_complete",
|
||||||
|
args: {
|
||||||
|
idx: event.raw.idx,
|
||||||
|
batch: event.raw.parent,
|
||||||
|
},
|
||||||
|
callback: (data) => {
|
||||||
|
if (data.message) window.open(event.raw.url, "_blank");
|
||||||
|
else
|
||||||
|
frappe.show_alert({
|
||||||
|
message:
|
||||||
|
"Please complete all previous activities to proceed.",
|
||||||
|
indicator: "red",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else window.open(event.raw.url, "_blank");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -754,17 +781,13 @@ const set_calendar_range = (calendar, events) => {
|
|||||||
).format("DD MMMM YYYY")}`
|
).format("DD MMMM YYYY")}`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (week_start.diff(moment(events[0].date), "days") <= 0) {
|
if (week_start.diff(moment(events[0].date), "days") <= 0)
|
||||||
$("#prev-week").hide();
|
$("#prev-week").hide();
|
||||||
} else {
|
else $("#prev-week").show();
|
||||||
$("#prev-week").show();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (week_end.diff(moment(events.slice(-1)[0].date), "days") > 0) {
|
if (week_end.diff(moment(events.slice(-1)[0].date), "days") > 0)
|
||||||
$("#next-week").hide();
|
$("#next-week").hide();
|
||||||
} else {
|
else $("#next-week").show();
|
||||||
$("#next-week").show();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const get_background_color = (doctype) => {
|
const get_background_color = (doctype) => {
|
||||||
@@ -784,6 +807,12 @@ const email_to_students = () => {
|
|||||||
label: __("Subject"),
|
label: __("Subject"),
|
||||||
reqd: 1,
|
reqd: 1,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
fieldtype: "Data",
|
||||||
|
fieldname: "reply_to",
|
||||||
|
label: __("Reply To"),
|
||||||
|
reqd: 0,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
fieldtype: "Text Editor",
|
fieldtype: "Text Editor",
|
||||||
fieldname: "message",
|
fieldname: "message",
|
||||||
@@ -802,11 +831,33 @@ const email_to_students = () => {
|
|||||||
|
|
||||||
const send_email = (values) => {
|
const send_email = (values) => {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "lms.lms.doctype.lms_batch.lms_batch.send_email_to_students",
|
method: "frappe.client.get_list",
|
||||||
args: {
|
args: {
|
||||||
batch: $(".class-details").data("batch"),
|
doctype: "Batch Student",
|
||||||
|
parent: "LMS Batch",
|
||||||
|
fields: ["student"],
|
||||||
|
filters: {
|
||||||
|
parent: $(".class-details").data("batch"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
callback: (data) => {
|
||||||
|
send_email_to_students(data.message, values);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const send_email_to_students = (students, values) => {
|
||||||
|
students = students.map((row) => row.student);
|
||||||
|
frappe.call({
|
||||||
|
method: "frappe.core.doctype.communication.email.make",
|
||||||
|
args: {
|
||||||
|
recipients: students.join(", "),
|
||||||
|
cc: values.reply_to,
|
||||||
subject: values.subject,
|
subject: values.subject,
|
||||||
message: values.message,
|
content: values.message,
|
||||||
|
doctype: "LMS Batch",
|
||||||
|
name: $(".class-details").data("batch"),
|
||||||
|
send_email: 1,
|
||||||
},
|
},
|
||||||
callback: (r) => {
|
callback: (r) => {
|
||||||
this.email_dialog.hide();
|
this.email_dialog.hide();
|
||||||
@@ -814,6 +865,9 @@ const send_email = (values) => {
|
|||||||
message: __("Email sent successfully"),
|
message: __("Email sent successfully"),
|
||||||
indicator: "green",
|
indicator: "green",
|
||||||
});
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 2000);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -71,6 +71,13 @@ def get_context(context):
|
|||||||
)
|
)
|
||||||
context.course_name_list = [course.course for course in context.batch_courses]
|
context.course_name_list = [course.course for course in context.batch_courses]
|
||||||
context.assessments = get_assessments(batch_name)
|
context.assessments = get_assessments(batch_name)
|
||||||
|
context.batch_emails = frappe.get_all(
|
||||||
|
"Communication",
|
||||||
|
filters={"reference_doctype": "LMS Batch", "reference_name": batch_name},
|
||||||
|
fields=["subject", "content", "recipients", "cc", "communication_date", "sender"],
|
||||||
|
order_by="communication_date desc",
|
||||||
|
)
|
||||||
|
|
||||||
context.batch_students = get_class_student_details(
|
context.batch_students = get_class_student_details(
|
||||||
batch_students, batch_courses, context.assessments
|
batch_students, batch_courses, context.assessments
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -19,9 +19,9 @@
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
{% if doc.member == frappe.session.user %}
|
{% if doc.member == frappe.session.user or is_moderator %}
|
||||||
<div class="">
|
<div class="">
|
||||||
<a class="btn btn-default btn-sm" target="_blank" href="/api/method/frappe.utils.print_format.download_pdf?doctype=LMS%20Certificate&name={{ doc.name }}&_lang=en">
|
<a class="btn btn-default btn-sm" target="_blank" href="/api/method/frappe.utils.print_format.download_pdf?doctype=LMS%20Certificate&name={{ doc.name }}&format={{ print_format }}&_lang=en">
|
||||||
{{ _("Download") }}
|
{{ _("Download") }}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import frappe
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.utils.jinja import render_template
|
from frappe.utils.jinja import render_template
|
||||||
from frappe.utils import get_url
|
from frappe.utils import get_url
|
||||||
|
from lms.lms.utils import has_course_moderator_role
|
||||||
|
|
||||||
|
|
||||||
def get_context(context):
|
def get_context(context):
|
||||||
@@ -16,7 +17,7 @@ def get_context(context):
|
|||||||
context.doc = frappe.db.get_value(
|
context.doc = frappe.db.get_value(
|
||||||
"LMS Certificate",
|
"LMS Certificate",
|
||||||
certificate_name,
|
certificate_name,
|
||||||
["name", "member", "issue_date", "expiry_date", "course"],
|
["name", "member", "issue_date", "expiry_date", "course", "template"],
|
||||||
as_dict=True,
|
as_dict=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -30,9 +31,14 @@ def get_context(context):
|
|||||||
"User", context.doc.member, ["full_name", "username"], as_dict=True
|
"User", context.doc.member, ["full_name", "username"], as_dict=True
|
||||||
)
|
)
|
||||||
context.url = f"{get_url()}/courses/{context.course.name}/{context.doc.name}"
|
context.url = f"{get_url()}/courses/{context.course.name}/{context.doc.name}"
|
||||||
|
context.is_moderator = has_course_moderator_role()
|
||||||
|
|
||||||
print_format = get_print_format()
|
if context.doc.template:
|
||||||
|
print_format = context.doc.template
|
||||||
|
else:
|
||||||
|
print_format = get_print_format()
|
||||||
|
|
||||||
|
context.print_format = print_format
|
||||||
template = frappe.db.get_value(
|
template = frappe.db.get_value(
|
||||||
"Print Format", print_format, ["html", "css"], as_dict=True
|
"Print Format", print_format, ["html", "css"], as_dict=True
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ def get_assignment_details(assessment, member):
|
|||||||
"assignment": assessment.assessment_name,
|
"assignment": assessment.assessment_name,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
assessment.completed = False
|
||||||
if existing_submission:
|
if existing_submission:
|
||||||
assessment.submission = frappe.db.get_value(
|
assessment.submission = frappe.db.get_value(
|
||||||
"LMS Assignment Submission",
|
"LMS Assignment Submission",
|
||||||
@@ -101,6 +101,7 @@ def get_assignment_details(assessment, member):
|
|||||||
["name", "status", "comments"],
|
["name", "status", "comments"],
|
||||||
as_dict=True,
|
as_dict=True,
|
||||||
)
|
)
|
||||||
|
assessment.completed = True
|
||||||
|
|
||||||
assessment.edit_url = f"/assignments/{assessment.assessment_name}"
|
assessment.edit_url = f"/assignments/{assessment.assessment_name}"
|
||||||
submission_name = existing_submission if existing_submission else "new-submission"
|
submission_name = existing_submission if existing_submission else "new-submission"
|
||||||
@@ -112,7 +113,10 @@ def get_assignment_details(assessment, member):
|
|||||||
|
|
||||||
|
|
||||||
def get_quiz_details(assessment, member):
|
def get_quiz_details(assessment, member):
|
||||||
assessment.title = frappe.db.get_value("LMS Quiz", assessment.assessment_name, "title")
|
assessment_details = frappe.db.get_value(
|
||||||
|
"LMS Quiz", assessment.assessment_name, ["title", "passing_percentage"], as_dict=1
|
||||||
|
)
|
||||||
|
assessment.title = assessment_details.title
|
||||||
|
|
||||||
existing_submission = frappe.get_all(
|
existing_submission = frappe.get_all(
|
||||||
"LMS Quiz Submission",
|
"LMS Quiz Submission",
|
||||||
@@ -120,13 +124,17 @@ def get_quiz_details(assessment, member):
|
|||||||
"member": member,
|
"member": member,
|
||||||
"quiz": assessment.assessment_name,
|
"quiz": assessment.assessment_name,
|
||||||
},
|
},
|
||||||
["name", "score"],
|
["name", "score", "percentage"],
|
||||||
order_by="creation desc",
|
order_by="percentage desc",
|
||||||
)
|
)
|
||||||
|
|
||||||
if len(existing_submission):
|
if len(existing_submission):
|
||||||
assessment.submission = existing_submission[0]
|
assessment.submission = existing_submission[0]
|
||||||
|
|
||||||
|
assessment.completed = False
|
||||||
|
if assessment.submission:
|
||||||
|
assessment.completed = True
|
||||||
|
|
||||||
assessment.edit_url = f"/quizzes/{assessment.assessment_name}"
|
assessment.edit_url = f"/quizzes/{assessment.assessment_name}"
|
||||||
submission_name = (
|
submission_name = (
|
||||||
existing_submission[0].name if len(existing_submission) else "new-submission"
|
existing_submission[0].name if len(existing_submission) else "new-submission"
|
||||||
|
|||||||
Reference in New Issue
Block a user