Merge branch 'develop' into qmsystem
diff --git a/erpnext/domains/education.py b/erpnext/domains/education.py
index 55e4eed..bbaa6e5 100644
--- a/erpnext/domains/education.py
+++ b/erpnext/domains/education.py
@@ -14,7 +14,7 @@
'Student Attendance Tool',
'Student Applicant'
],
- 'default_portal_role': 'LMS User',
+ 'default_portal_role': 'Student',
'restricted_roles': [
'Student',
'Instructor',
diff --git a/erpnext/education/doctype/course/course.json b/erpnext/education/doctype/course/course.json
index 072e8b4..7d8b073 100644
--- a/erpnext/education/doctype/course/course.json
+++ b/erpnext/education/doctype/course/course.json
@@ -1,514 +1,135 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 1,
- "allow_rename": 1,
- "autoname": "field:course_code",
- "beta": 0,
- "creation": "2015-09-07 12:39:55.181893",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 0,
- "engine": "InnoDB",
+ "allow_import": 1,
+ "allow_rename": 1,
+ "autoname": "field:course_code",
+ "creation": "2015-09-07 12:39:55.181893",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "course_name",
+ "department",
+ "parent_course",
+ "column_break_3",
+ "course_code",
+ "course_abbreviation",
+ "section_break_6",
+ "topics",
+ "description",
+ "hero_image",
+ "assessment",
+ "default_grading_scale",
+ "assessment_criteria"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "course_name",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Course Name",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "course_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Course Name",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "department",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 1,
- "label": "Department",
- "length": 0,
- "no_copy": 0,
- "options": "Department",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "department",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Department",
+ "options": "Department"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "parent_course",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Parent Course (Leave blank, if this isn't part of Parent Course)",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "parent_course",
+ "fieldtype": "Data",
+ "label": "Parent Course (Leave blank, if this isn't part of Parent Course)"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "column_break_3",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "course_code",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Course Code",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
+ "fieldname": "course_code",
+ "fieldtype": "Data",
+ "label": "Course Code",
+ "reqd": 1,
"unique": 1
- },
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "course_abbreviation",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Course Abbreviation",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "course_abbreviation",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Course Abbreviation"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "section_break_6",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "section_break_6",
+ "fieldtype": "Section Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "topics",
- "fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Topics",
- "length": 0,
- "no_copy": 0,
- "options": "Course Topic",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "topics",
+ "fieldtype": "Table",
+ "label": "Topics",
+ "options": "Course Topic"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "course_intro",
- "fieldtype": "Small Text",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Course Intro",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "hero_image",
+ "fieldtype": "Attach Image",
+ "label": "Hero Image"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "hero_image",
- "fieldtype": "Attach Image",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Hero Image",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "assessment",
+ "fieldtype": "Section Break",
+ "label": "Assessment"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "assessment",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Assessment",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "default_grading_scale",
+ "fieldtype": "Link",
+ "label": "Default Grading Scale",
+ "options": "Grading Scale"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "default_grading_scale",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Default Grading Scale",
- "length": 0,
- "no_copy": 0,
- "options": "Grading Scale",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "assessment_criteria",
+ "fieldtype": "Table",
+ "label": "Assessment Criteria",
+ "options": "Course Assessment Criteria"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "assessment_criteria",
- "fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Assessment Criteria",
- "length": 0,
- "no_copy": 0,
- "options": "Course Assessment Criteria",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldname": "description",
+ "fieldtype": "Small Text",
+ "label": "Description"
}
- ],
- "has_web_view": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "menu_index": 0,
- "modified": "2019-04-09 11:35:27.354877",
- "modified_by": "Administrator",
- "module": "Education",
- "name": "Course",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "modified": "2019-06-05 18:39:11.870605",
+ "modified_by": "Administrator",
+ "module": "Education",
+ "name": "Course",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Academics User",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Academics User",
+ "share": 1,
"write": 1
- },
+ },
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Instructor",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Instructor",
+ "share": 1,
"write": 1
}
- ],
- "quick_entry": 0,
- "read_only": 0,
- "restrict_to_domain": "Education",
- "search_fields": "course_name",
- "show_name_in_global_search": 1,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 0,
- "track_seen": 0,
- "track_views": 0
+ ],
+ "restrict_to_domain": "Education",
+ "search_fields": "course_name",
+ "show_name_in_global_search": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC"
}
\ No newline at end of file
diff --git a/erpnext/education/doctype/course_enrollment/course_enrollment.py b/erpnext/education/doctype/course_enrollment/course_enrollment.py
index 6f2bb0d..b082be2 100644
--- a/erpnext/education/doctype/course_enrollment/course_enrollment.py
+++ b/erpnext/education/doctype/course_enrollment/course_enrollment.py
@@ -35,7 +35,7 @@
if enrollment:
frappe.throw(_("Student is already enrolled."))
- def add_quiz_activity(self, quiz_name, quiz_response,answers, score, status):
+ def add_quiz_activity(self, quiz_name, quiz_response, answers, score, status):
result = {k: ('Correct' if v else 'Wrong') for k,v in answers.items()}
result_data = []
for key in answers:
@@ -43,7 +43,9 @@
item['question'] = key
item['quiz_result'] = result[key]
try:
- if isinstance(quiz_response[key], list):
+ if not quiz_response[key]:
+ item['selected_option'] = "Unattempted"
+ elif isinstance(quiz_response[key], list):
item['selected_option'] = ', '.join(frappe.get_value('Options', res, 'option') for res in quiz_response[key])
else:
item['selected_option'] = frappe.get_value('Options', quiz_response[key], 'option')
@@ -59,11 +61,12 @@
"result": result_data,
"score": score,
"status": status
- }).insert()
+ }).insert(ignore_permissions = True)
def add_activity(self, content_type, content):
- if check_activity_exists(self.name, content_type, content):
- pass
+ activity = check_activity_exists(self.name, content_type, content)
+ if activity:
+ return activity
else:
activity = frappe.get_doc({
"doctype": "Course Activity",
@@ -71,9 +74,14 @@
"content_type": content_type,
"content": content,
"activity_date": frappe.utils.datetime.datetime.now()
- })
- activity.insert()
+ })
+
+ activity.insert(ignore_permissions=True)
+ return activity.name
def check_activity_exists(enrollment, content_type, content):
activity = frappe.get_all("Course Activity", filters={'enrollment': enrollment, 'content_type': content_type, 'content': content})
- return bool(activity)
\ No newline at end of file
+ if activity:
+ return activity[0].name
+ else:
+ return None
\ No newline at end of file
diff --git a/erpnext/education/doctype/program/program.json b/erpnext/education/doctype/program/program.json
index cb8d778..a0a2aa2 100644
--- a/erpnext/education/doctype/program/program.json
+++ b/erpnext/education/doctype/program/program.json
@@ -1,627 +1,181 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:program_code",
- "beta": 0,
"creation": "2015-09-07 12:54:03.609282",
- "custom": 0,
- "docstatus": 0,
"doctype": "DocType",
- "document_type": "",
- "editable_grid": 0,
"engine": "InnoDB",
+ "field_order": [
+ "program_name",
+ "department",
+ "column_break_3",
+ "program_code",
+ "program_abbreviation",
+ "section_break_5",
+ "courses",
+ "section_break_9",
+ "description",
+ "intro_video",
+ "hero_image",
+ "column_break_11",
+ "is_published",
+ "is_featured",
+ "allow_self_enroll"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "program_name",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
"label": "Program Name",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "reqd": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "department",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Department",
- "length": 0,
- "no_copy": 0,
- "options": "Department",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "Department"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "column_break_3",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Column Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "program_code",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Program Code",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
"reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
"unique": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "program_abbreviation",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Program Abbreviation",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Program Abbreviation"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "",
- "fetch_if_empty": 0,
"fieldname": "section_break_5",
"fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Portal Settings",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Portal Settings"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "courses",
"fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Courses",
- "length": 0,
- "no_copy": 0,
- "options": "Program Course",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "Program Course"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "section_break_9",
"fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "LMS Settings"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "description",
"fieldtype": "Small Text",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Description",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Description"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "intro_video",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Intro Video",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Intro Video"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "hero_image",
"fieldtype": "Attach Image",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Hero Image",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Hero Image"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "column_break_11",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Column Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "0",
- "fetch_if_empty": 0,
"fieldname": "is_published",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Is Published",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Is Published"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "0",
- "fetch_if_empty": 0,
+ "depends_on": "eval: doc.is_published == 1",
"fieldname": "is_featured",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Is Featured",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Is Featured"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval: doc.is_published == 1",
+ "description": "Allow students to enroll themselves from the portal",
+ "fieldname": "allow_self_enroll",
+ "fieldtype": "Check",
+ "label": "Allow Self Enroll"
}
],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "menu_index": 0,
- "modified": "2019-03-18 15:26:56.737903",
+ "modified": "2019-06-05 17:47:26.877296",
"modified_by": "Administrator",
"module": "Education",
"name": "Program",
- "name_case": "",
"owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Academics User",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- },
- {
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Instructor",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- },
- {
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Guest",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 0
- },
- {
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "LMS User",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 0
- },
- {
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Student",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 0
- },
- {
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
- "set_user_permissions": 0,
"share": 1,
- "submit": 0,
"write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Academics User",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Instructor",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Guest",
+ "share": 1
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Student",
+ "share": 1
}
],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
"restrict_to_domain": "Education",
- "route": "",
"search_fields": "program_name",
"show_name_in_global_search": 1,
"sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 0,
- "track_seen": 0,
- "track_views": 0
+ "sort_order": "DESC"
}
\ No newline at end of file
diff --git a/erpnext/education/doctype/program_enrollment/program_enrollment.py b/erpnext/education/doctype/program_enrollment/program_enrollment.py
index 22cca86..d232e47 100644
--- a/erpnext/education/doctype/program_enrollment/program_enrollment.py
+++ b/erpnext/education/doctype/program_enrollment/program_enrollment.py
@@ -96,29 +96,6 @@
quiz_progress.program = self.program
return quiz_progress
- def get_program_progress(self):
- import math
- program = frappe.get_doc("Program", self.program)
- program_progress = {}
- progress = []
- for course in program.get_all_children():
- course_progress = lms.get_student_course_details(course.course, self.program)
- is_complete = False
- if course_progress['flag'] == "Completed":
- is_complete = True
- progress.append({'course_name': course.course_name, 'name': course.course, 'is_complete': is_complete})
-
- program_progress['progress'] = progress
- program_progress['name'] = self.program
- program_progress['program'] = frappe.get_value("Program", self.program, 'program_name')
-
- try:
- program_progress['percentage'] = math.ceil((sum([item['is_complete'] for item in progress] * 100)/len(progress)))
- except ZeroDivisionError:
- program_progress['percentage'] = 0
-
- return program_progress
-
@frappe.whitelist()
def get_program_courses(doctype, txt, searchfield, start, page_len, filters):
if filters.get('program'):
diff --git a/erpnext/education/doctype/question/question.json b/erpnext/education/doctype/question/question.json
index 14a9f3c..b3a161d 100644
--- a/erpnext/education/doctype/question/question.json
+++ b/erpnext/education/doctype/question/question.json
@@ -1,167 +1,80 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 1,
- "allow_rename": 0,
- "autoname": "format:QUESTION-{#####}",
- "beta": 0,
- "creation": "2018-10-01 15:58:00.696815",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "allow_import": 1,
+ "autoname": "format:QUESTION-{#####}",
+ "creation": "2018-10-01 15:58:00.696815",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "question",
+ "options",
+ "question_type"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "question",
- "fieldtype": "Small Text",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Question",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "question",
+ "fieldtype": "Small Text",
+ "in_list_view": 1,
+ "label": "Question",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "description": "",
- "fetch_if_empty": 0,
- "fieldname": "options",
- "fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Options",
- "length": 0,
- "no_copy": 0,
- "options": "Options",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldname": "options",
+ "fieldtype": "Table",
+ "label": "Options",
+ "options": "Options",
+ "reqd": 1
+ },
+ {
+ "fieldname": "question_type",
+ "fieldtype": "Select",
+ "in_standard_filter": 1,
+ "label": "Type",
+ "options": "\nSingle Correct Answer\nMultiple Correct Answer",
+ "read_only": 1
}
- ],
- "has_web_view": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2019-04-22 14:02:08.140652",
- "modified_by": "Administrator",
- "module": "Education",
- "name": "Question",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "modified": "2019-05-30 18:39:21.880974",
+ "modified_by": "Administrator",
+ "module": "Education",
+ "name": "Question",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Academics User",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Academics User",
+ "share": 1,
"write": 1
- },
+ },
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Instructor",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Instructor",
+ "share": 1,
"write": 1
- },
+ },
{
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "LMS User",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 0
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "LMS User",
+ "share": 1
}
- ],
- "quick_entry": 1,
- "read_only": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 0,
- "track_seen": 0,
- "track_views": 0
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC"
}
\ No newline at end of file
diff --git a/erpnext/education/doctype/question/question.py b/erpnext/education/doctype/question/question.py
index 8cd2398..b822108 100644
--- a/erpnext/education/doctype/question/question.py
+++ b/erpnext/education/doctype/question/question.py
@@ -12,6 +12,7 @@
def validate(self):
self.check_at_least_one_option()
self.check_minimum_one_correct_answer()
+ self.set_question_type()
def check_at_least_one_option(self):
if len(self.options) <= 1:
@@ -26,6 +27,13 @@
else:
frappe.throw(_("A qustion must have at least one correct options"))
+ def set_question_type(self):
+ correct_options = [option for option in self.options if option.is_correct]
+ if len(correct_options) > 1:
+ self.question_type = "Multiple Correct Answer"
+ else:
+ self.question_type = "Single Correct Answer"
+
def get_answer(self):
options = self.options
answers = [item.name for item in options if item.is_correct == True]
diff --git a/erpnext/education/doctype/quiz/quiz.json b/erpnext/education/doctype/quiz/quiz.json
index f91bc0f..b4903fc 100644
--- a/erpnext/education/doctype/quiz/quiz.json
+++ b/erpnext/education/doctype/quiz/quiz.json
@@ -1,299 +1,105 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
"autoname": "field:title",
- "beta": 0,
"creation": "2018-10-17 05:52:50.149904",
- "custom": 0,
- "docstatus": 0,
"doctype": "DocType",
- "document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
+ "field_order": [
+ "title",
+ "question",
+ "quiz_configuration_section",
+ "passing_score",
+ "max_attempts",
+ "grading_basis"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "title",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Title",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
"reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
"unique": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "question",
"fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Question",
- "length": 0,
- "no_copy": 0,
"options": "Quiz Question",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "reqd": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "quiz_configuration_section",
"fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Quiz Configuration",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Quiz Configuration"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
+ "default": "75",
+ "description": "Score out of 100",
"fieldname": "passing_score",
"fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
"label": "Passing Score",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "reqd": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "1",
"description": "Enter 0 to waive limit",
"fieldname": "max_attempts",
"fieldtype": "Int",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
"label": "Max Attempts",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "reqd": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "Last Highest Score",
+ "default": "Latest Highest Score",
"fieldname": "grading_basis",
"fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Grading Basis",
- "length": 0,
- "no_copy": 0,
- "options": "\nLast Attempt\nLast Highest Score",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "Latest Highest Score\nLatest Attempt"
}
],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-11-25 19:07:36.190116",
+ "modified": "2019-05-30 18:50:54.218571",
"modified_by": "Administrator",
"module": "Education",
"name": "Quiz",
- "name_case": "",
"owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Academics User",
- "set_user_permissions": 0,
"share": 1,
- "submit": 0,
"write": 1
},
{
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
"email": 1,
"export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "LMS User",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 0
+ "share": 1
},
{
- "amend": 0,
- "cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Instructor",
- "set_user_permissions": 0,
"share": 1,
- "submit": 0,
"write": 1
}
],
"quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/education/doctype/quiz/quiz.py b/erpnext/education/doctype/quiz/quiz.py
index 6da50a6..8e54745 100644
--- a/erpnext/education/doctype/quiz/quiz.py
+++ b/erpnext/education/doctype/quiz/quiz.py
@@ -7,51 +7,47 @@
from frappe.model.document import Document
class Quiz(Document):
+ def validate(self):
+ if self.passing_score > 100:
+ frappe.throw("Passing Score value should be between 0 and 100")
- def validate_quiz_attempts(self, enrollment, quiz_name):
- if self.max_attempts > 0:
- try:
- if len(frappe.get_all("Quiz Activity", {'enrollment': enrollment.name, 'quiz': quiz_name})) >= self.max_attempts:
- frappe.throw('Maximum attempts reached!')
- except Exception as e:
- pass
+ def allowed_attempt(self, enrollment, quiz_name):
+ if self.max_attempts == 0:
+ return True
+
+ try:
+ if len(frappe.get_all("Quiz Activity", {'enrollment': enrollment.name, 'quiz': quiz_name})) >= self.max_attempts:
+ frappe.msgprint("Maximum attempts for this quiz reached!")
+ return False
+ else:
+ return True
+ except Exception as e:
+ return False
def evaluate(self, response_dict, quiz_name):
- # self.validate_quiz_attempts(enrollment, quiz_name)
questions = [frappe.get_doc('Question', question.question_link) for question in self.question]
answers = {q.name:q.get_answer() for q in questions}
- correct_answers = {}
+ result = {}
for key in answers:
try:
if isinstance(response_dict[key], list):
- result = compare_list_elementwise(response_dict[key], answers[key])
+ is_correct = compare_list_elementwise(response_dict[key], answers[key])
else:
- result = (response_dict[key] == answers[key])
- except:
- result = False
- correct_answers[key] = result
- score = (sum(correct_answers.values()) * 100 ) / len(answers)
+ is_correct = (response_dict[key] == answers[key])
+ except Exception as e:
+ is_correct = False
+ result[key] = is_correct
+ score = (sum(result.values()) * 100 ) / len(answers)
if score >= self.passing_score:
status = "Pass"
else:
status = "Fail"
- return correct_answers, score, status
+ return result, score, status
def get_questions(self):
- quiz_question = self.get_all_children()
- if quiz_question:
- questions = [frappe.get_doc('Question', question.question_link).as_dict() for question in quiz_question]
- for question in questions:
- correct_options = [option.is_correct for option in question.options]
- if sum(correct_options) > 1:
- question['type'] = "MultipleChoice"
- else:
- question['type'] = "SingleChoice"
- return questions
- else:
- return None
+ return [frappe.get_doc('Question', question.question_link) for question in self.question]
def compare_list_elementwise(*args):
try:
diff --git a/erpnext/education/doctype/quiz_result/quiz_result.json b/erpnext/education/doctype/quiz_result/quiz_result.json
index 86505ac..67c7e2d 100644
--- a/erpnext/education/doctype/quiz_result/quiz_result.json
+++ b/erpnext/education/doctype/quiz_result/quiz_result.json
@@ -1,145 +1,52 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2018-10-15 15:52:25.766374",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "creation": "2018-10-15 15:52:25.766374",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "question",
+ "selected_option",
+ "quiz_result"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "question",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Question",
- "length": 0,
- "no_copy": 0,
- "options": "Question",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 1,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "question",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Question",
+ "options": "Question",
+ "read_only": 1,
+ "reqd": 1,
+ "set_only_once": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "selected_option",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Selected Option",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 1,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "selected_option",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Selected Option",
+ "read_only": 1,
+ "set_only_once": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "quiz_result",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Result",
- "length": 0,
- "no_copy": 0,
- "options": "\nCorrect\nWrong",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 1,
- "translatable": 0,
- "unique": 0
+ "fieldname": "quiz_result",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Result",
+ "options": "\nCorrect\nWrong",
+ "read_only": 1,
+ "reqd": 1,
+ "set_only_once": 1
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "modified": "2019-03-27 17:58:54.388848",
- "modified_by": "Administrator",
- "module": "Education",
- "name": "Quiz Result",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
+ ],
+ "istable": 1,
+ "modified": "2019-06-03 12:52:32.267392",
+ "modified_by": "Administrator",
+ "module": "Education",
+ "name": "Quiz Result",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/education/doctype/student/student.py b/erpnext/education/doctype/student/student.py
index 529f78d..da25880 100644
--- a/erpnext/education/doctype/student/student.py
+++ b/erpnext/education/doctype/student/student.py
@@ -54,7 +54,7 @@
'send_welcome_email': 1,
'user_type': 'Website User'
})
- student_user.add_roles("Student", "LMS User")
+ student_user.add_roles("Student")
student_user.save()
update_password_link = student_user.reset_password()
diff --git a/erpnext/education/doctype/topic/topic.json b/erpnext/education/doctype/topic/topic.json
index f47b10d..6e748fd 100644
--- a/erpnext/education/doctype/topic/topic.json
+++ b/erpnext/education/doctype/topic/topic.json
@@ -1,297 +1,104 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "field:topic_code",
- "beta": 0,
- "creation": "2018-12-12 11:37:39.917760",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "autoname": "field:topic_code",
+ "creation": "2018-12-12 11:37:39.917760",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "topic_name",
+ "column_break_2",
+ "topic_code",
+ "section_break_4",
+ "topic_content",
+ "description",
+ "hero_image"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "topic_name",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Name",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "topic_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Name",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "column_break_2",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "column_break_2",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "topic_code",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Code",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
+ "fieldname": "topic_code",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Code",
+ "reqd": 1,
"unique": 1
- },
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "section_break_4",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "section_break_4",
+ "fieldtype": "Section Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "topic_content",
- "fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Topic Content",
- "length": 0,
- "no_copy": 0,
- "options": "Topic Content",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "topic_content",
+ "fieldtype": "Table",
+ "label": "Topic Content",
+ "options": "Topic Content"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "hero_image",
- "fieldtype": "Attach Image",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Hero Image",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldname": "hero_image",
+ "fieldtype": "Attach Image",
+ "label": "Hero Image"
+ },
+ {
+ "fieldname": "description",
+ "fieldtype": "Small Text",
+ "label": "Description"
}
- ],
- "has_web_view": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "menu_index": 0,
- "modified": "2019-04-09 11:35:34.137040",
- "modified_by": "Administrator",
- "module": "Education",
- "name": "Topic",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "modified": "2019-06-05 18:38:44.029711",
+ "modified_by": "Administrator",
+ "module": "Education",
+ "name": "Topic",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
"write": 1
- },
+ },
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Administrator",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Administrator",
+ "share": 1,
"write": 1
- },
+ },
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Instructor",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Instructor",
+ "share": 1,
"write": 1
}
- ],
- "quick_entry": 1,
- "read_only": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/education/doctype/video/video.json b/erpnext/education/doctype/video/video.json
index cc8f718..3d11bd2 100644
--- a/erpnext/education/doctype/video/video.json
+++ b/erpnext/education/doctype/video/video.json
@@ -1,262 +1,102 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
"allow_import": 1,
- "allow_rename": 0,
"autoname": "field:title",
- "beta": 0,
"creation": "2018-10-17 05:47:13.087395",
- "custom": 0,
- "docstatus": 0,
"doctype": "DocType",
- "document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
+ "field_order": [
+ "title",
+ "description",
+ "duration",
+ "provider",
+ "url",
+ "publish_date"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "title",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
+ "in_list_view": 1,
"label": "Title",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
+ "reqd": 1,
"unique": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "description",
"fieldtype": "Text Editor",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
+ "in_list_view": 1,
"label": "Description",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "reqd": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "duration",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Duration",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Duration"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "url",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
+ "in_list_view": 1,
"label": "URL",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "reqd": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "publish_date",
"fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Publish Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Publish Date"
+ },
+ {
+ "fieldname": "provider",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Provider",
+ "options": "YouTube\nVimeo",
+ "reqd": 1
}
],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-11-25 19:07:17.134288",
+ "modified": "2019-05-20 15:11:53.075093",
"modified_by": "Administrator",
"module": "Education",
"name": "Video",
- "name_case": "",
"owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Academics User",
- "set_user_permissions": 0,
"share": 1,
- "submit": 0,
"write": 1
},
{
- "amend": 0,
- "cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Instructor",
- "set_user_permissions": 0,
"share": 1,
- "submit": 0,
"write": 1
},
{
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
"email": 1,
"export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "LMS User",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 0
+ "share": 1
}
],
"quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/education/setup.py b/erpnext/education/setup.py
index ed1d69e..5c40928 100644
--- a/erpnext/education/setup.py
+++ b/erpnext/education/setup.py
@@ -9,7 +9,8 @@
def setup_education():
- if frappe.db.exists('Academic Year', '2015-16'):
+ disable_desk_access_for_student_role()
+ if frappe.db.exists("Academic Year", "2015-16"):
# already setup
return
create_academic_sessions()
@@ -26,3 +27,22 @@
{"doctype": "Academic Term", "academic_year": "2017-18", "term_name": "Semester 2"}
]
insert_record(data)
+
+def disable_desk_access_for_student_role():
+ try:
+ student_role = frappe.get_doc("Role", "Student")
+ except frappe.DoesNotExistError:
+ create_student_role()
+ return
+
+ student_role.desk_access = 0
+ student_role.save()
+
+def create_student_role():
+ student_role = frappe.get_doc({
+ "doctype": "Role",
+ "role_name": "Student",
+ "desk_access": 0,
+ "restrict_to_domain": "Education"
+ })
+ student_role.insert()
diff --git a/erpnext/education/utils.py b/erpnext/education/utils.py
index bf766ad..0e02712 100644
--- a/erpnext/education/utils.py
+++ b/erpnext/education/utils.py
@@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
-# For lice
from __future__ import unicode_literals, division
import frappe
@@ -57,9 +56,10 @@
# LMS Utils
def get_current_student():
- """
- Returns student user name, example EDU-STU-2018-00001 (Based on the naming series).
- Takes email from from frappe.session.user
+ """Returns current student from frappe.session.user
+
+ Returns:
+ object: Student Document
"""
email = frappe.session.user
if email in ('Administrator', 'Guest'):
@@ -70,44 +70,266 @@
except (IndexError, frappe.DoesNotExistError):
return None
-def check_super_access():
+def get_portal_programs():
+ """Returns a list of all program to be displayed on the portal
+ Programs are returned based on the following logic
+ is_published and (student_is_enrolled or student_can_self_enroll)
+
+ Returns:
+ list of dictionary: List of all programs and to be displayed on the portal along with access rights
+ """
+ published_programs = frappe.get_all("Program", filters={"is_published": True})
+ if not published_programs:
+ return None
+
+ program_list = [frappe.get_doc("Program", program) for program in published_programs]
+ portal_programs = [{'program': program, 'has_access': allowed_program_access(program.name)} for program in program_list if allowed_program_access(program.name) or program.allow_self_enroll]
+
+ return portal_programs
+
+def allowed_program_access(program, student=None):
+ """Returns enrollment status for current student
+
+ Args:
+ program (string): Name of the program
+ student (object): instance of Student document
+
+ Returns:
+ bool: Is current user enrolled or not
+ """
+ if has_super_access():
+ return True
+ if not student:
+ student = get_current_student()
+ if student and get_enrollment('program', program, student.name):
+ return True
+ else:
+ return False
+
+def get_enrollment(master, document, student):
+ """Gets enrollment for course or program
+
+ Args:
+ master (string): can either be program or course
+ document (string): program or course name
+ student (string): Student ID
+
+ Returns:
+ string: Enrollment Name if exists else returns empty string
+ """
+ if master == 'program':
+ enrollments = frappe.get_all("Program Enrollment", filters={'student':student, 'program': document, 'docstatus': 1})
+ if master == 'course':
+ enrollments = frappe.get_all("Course Enrollment", filters={'student':student, 'course': document})
+
+ if enrollments:
+ return enrollments[0].name
+ else:
+ return None
+
+@frappe.whitelist()
+def enroll_in_program(program_name, student=None):
+ """Enroll student in program
+
+ Args:
+ program_name (string): Name of the program to be enrolled into
+ student (string, optional): name of student who has to be enrolled, if not
+ provided, a student will be created from the current user
+
+ Returns:
+ string: name of the program enrollment document
+ """
+ if has_super_access():
+ return
+
+ if not student == None:
+ student = frappe.get_doc("Student", student)
+ else:
+ # Check if self enrollment in allowed
+ program = frappe.get_doc('Program', program_name)
+ if not program.allow_self_enroll:
+ return frappe.throw("You are not allowed to enroll for this course")
+
+ student = get_current_student()
+ if not student:
+ student = create_student_from_current_user()
+
+ # Check if student is already enrolled in program
+ enrollment = get_enrollment('program', program_name, student.name)
+ if enrollment:
+ return enrollment
+
+ # Check if self enrollment in allowed
+ program = frappe.get_doc('Program', program_name)
+ if not program.allow_self_enroll:
+ return frappe.throw("You are not allowed to enroll for this course")
+
+ # Enroll in program
+ program_enrollment = student.enroll_in_program(program_name)
+ return program_enrollment.name
+
+def has_super_access():
+ """Check if user has a role that allows full access to LMS
+
+ Returns:
+ bool: true if user has access to all lms content
+ """
current_user = frappe.get_doc('User', frappe.session.user)
roles = set([role.role for role in current_user.roles])
return bool(roles & {'Administrator', 'Instructor', 'Education Manager', 'System Manager', 'Academic User'})
-def get_program_enrollment(program_name):
- """
- Function to get program enrollments for a particular student for a program
- """
+@frappe.whitelist()
+def add_activity(course, content_type, content, program):
+ if has_super_access():
+ return None
+
student = get_current_student()
if not student:
- return None
+ return frappe.throw("Student with email {0} does not exist".format(frappe.session.user), frappe.DoesNotExistError)
+
+ enrollment = get_or_create_course_enrollment(course, program)
+ if content_type == 'Quiz':
+ return
else:
- enrollment = frappe.get_all("Program Enrollment", filters={'student':student.name, 'program': program_name})
- if enrollment:
- return enrollment[0].name
+ return enrollment.add_activity(content_type, content)
+
+@frappe.whitelist()
+def evaluate_quiz(quiz_response, quiz_name, course, program):
+ import json
+
+ student = get_current_student()
+
+ quiz_response = json.loads(quiz_response)
+ quiz = frappe.get_doc("Quiz", quiz_name)
+ result, score, status = quiz.evaluate(quiz_response, quiz_name)
+
+ if has_super_access():
+ return {'result': result, 'score': score, 'status': status}
+
+ if student:
+ enrollment = get_or_create_course_enrollment(course, program)
+ if quiz.allowed_attempt(enrollment, quiz_name):
+ enrollment.add_quiz_activity(quiz_name, quiz_response, result, score, status)
+ return {'result': result, 'score': score, 'status': status}
else:
return None
-def get_program_and_enrollment_status(program_name):
- program = frappe.get_doc('Program', program_name)
- is_enrolled = bool(get_program_enrollment(program_name)) or check_super_access()
- return {'program': program, 'is_enrolled': is_enrolled}
+@frappe.whitelist()
+def get_quiz(quiz_name, course):
+ try:
+ quiz = frappe.get_doc("Quiz", quiz_name)
+ questions = quiz.get_questions()
+ except:
+ frappe.throw("Quiz {0} does not exist".format(quiz_name))
+ return None
-def get_course_enrollment(course_name):
+ questions = [{
+ 'name': question.name,
+ 'question': question.question,
+ 'type': question.question_type,
+ 'options': [{'name': option.name, 'option': option.option}
+ for option in question.options],
+ } for question in questions]
+
+ if has_super_access():
+ return {'questions': questions, 'activity': None}
+
+ student = get_current_student()
+ course_enrollment = get_enrollment("course", course, student.name)
+ status, score, result = check_quiz_completion(quiz, course_enrollment)
+ return {'questions': questions, 'activity': {'is_complete': status, 'score': score, 'result': result}}
+
+def get_topic_progress(topic, course_name, program):
+ """
+ Return the porgress of a course in a program as well as the content to continue from.
+ :param topic_name:
+ :param course_name:
+ """
student = get_current_student()
if not student:
return None
- enrollment_name = frappe.get_all("Course Enrollment", filters={'student': student.name, 'course':course_name})
- try:
- name = enrollment_name[0].name
- enrollment = frappe.get_doc("Course Enrollment", name)
- return enrollment
- except:
+ course_enrollment = get_or_create_course_enrollment(course_name, program)
+ progress = student.get_topic_progress(course_enrollment.name, topic)
+ if not progress:
return None
+ count = sum([activity['is_complete'] for activity in progress])
+ if count == 0:
+ return {'completed': False, 'started': False}
+ elif count == len(progress):
+ return {'completed': True, 'started': True}
+ elif count < len(progress):
+ return {'completed': False, 'started': True}
+
+def get_course_progress(course, program):
+ """
+ Return the porgress of a course in a program as well as the content to continue from.
+ :param topic_name:
+ :param course_name:
+ """
+ course_progress = []
+ for course_topic in course.topics:
+ topic = frappe.get_doc("Topic", course_topic.topic)
+ progress = get_topic_progress(topic, course.name, program)
+ if progress:
+ course_progress.append(progress)
+ if course_progress:
+ number_of_completed_topics = sum([activity['completed'] for activity in course_progress])
+ total_topics = len(course_progress)
+ if total_topics == 1:
+ return course_progress[0]
+ if number_of_completed_topics == 0:
+ return {'completed': False, 'started': False}
+ if number_of_completed_topics == total_topics:
+ return {'completed': True, 'started': True}
+ if number_of_completed_topics < total_topics:
+ return {'completed': False, 'started': True}
+
+ return None
+
+def get_program_progress(program):
+ program_progress = []
+ if not program.courses:
+ return None
+ for program_course in program.courses:
+ course = frappe.get_doc("Course", program_course.course)
+ progress = get_course_progress(course, program.name)
+ if progress:
+ progress['name'] = course.name
+ progress['course'] = course.course_name
+ program_progress.append(progress)
+
+ if program_progress:
+ return program_progress
+
+ return None
+
+def get_program_completion(program):
+ topics = frappe.db.sql("""select `tabcourse topic`.topic, `tabcourse topic`.parent
+ from `tabcourse topic`,
+ `tabprogram course`
+ where `tabcourse topic`.parent = `tabprogram course`.course
+ and `tabprogram course`.parent = %s""", program.name)
+
+ progress = []
+ for topic in topics:
+ topic_doc = frappe.get_doc('Topic', topic[0])
+ topic_progress = get_topic_progress(topic_doc, topic[1], program.name)
+ if topic_progress:
+ progress.append(topic_progress)
+
+ if progress:
+ number_of_completed_topics = sum([activity['completed'] for activity in progress if activity])
+ total_topics = len(progress)
+ try:
+ return int((float(number_of_completed_topics)/total_topics)*100)
+ except ZeroDivisionError:
+ return 0
+
+ return 0
def create_student_from_current_user():
user = frappe.get_doc("User", frappe.session.user)
+
student = frappe.get_doc({
"doctype": "Student",
"first_name": user.first_name,
@@ -115,12 +337,21 @@
"student_email_id": user.email,
"user": frappe.session.user
})
+
student.save(ignore_permissions=True)
return student
-def enroll_in_course(course_name, program_name):
+def get_or_create_course_enrollment(course, program):
student = get_current_student()
- return student.enroll_in_course(course_name=course_name, program_enrollment=get_program_enrollment(program_name))
+ course_enrollment = get_enrollment("course", course, student.name)
+ if not course_enrollment:
+ program_enrollment = get_enrollment('program', program, student.name)
+ if not program_enrollment:
+ frappe.throw("You are not enrolled in program {0}".format(program))
+ return
+ return student.enroll_in_course(course_name=course, program_enrollment=get_enrollment('program', program, student.name))
+ else:
+ return frappe.get_doc('Course Enrollment', course_enrollment)
def check_content_completion(content_name, content_type, enrollment_name):
activity = frappe.get_all("Course Activity", filters={'enrollment': enrollment_name, 'content_type': content_type, 'content': content_name})
@@ -131,7 +362,7 @@
def check_quiz_completion(quiz, enrollment_name):
attempts = frappe.get_all("Quiz Activity", filters={'enrollment': enrollment_name, 'quiz': quiz.name}, fields=["name", "activity_date", "score", "status"])
- status = False if quiz.max_attempts == 0 else bool(len(attempts) == quiz.max_attempts)
+ status = False if quiz.max_attempts == 0 else bool(len(attempts) >= quiz.max_attempts)
score = None
result = None
if attempts:
diff --git a/erpnext/public/build.json b/erpnext/public/build.json
index bb19803..be7189b 100644
--- a/erpnext/public/build.json
+++ b/erpnext/public/build.json
@@ -55,8 +55,5 @@
"stock/dashboard/item_dashboard.html",
"stock/dashboard/item_dashboard_list.html",
"stock/dashboard/item_dashboard.js"
- ],
- "js/lms.min.js": [
- "public/js/education/lms/lms.js"
]
}
diff --git a/erpnext/public/js/education/lms/call.js b/erpnext/public/js/education/lms/call.js
deleted file mode 100644
index e35acbd..0000000
--- a/erpnext/public/js/education/lms/call.js
+++ /dev/null
@@ -1,15 +0,0 @@
-frappe.ready(() => {
- frappe.provide('lms');
-
- lms.call = (method, args) => {
- const method_path = 'erpnext.www.lms.' + method;
- return new Promise((resolve, reject) => {
- return frappe.call({
- method: method_path,
- args,
- })
- .then(r => resolve(r.message))
- .fail(reject);
- });
- };
-});
\ No newline at end of file
diff --git a/erpnext/public/js/education/lms/components/Article.vue b/erpnext/public/js/education/lms/components/Article.vue
deleted file mode 100644
index eab1424..0000000
--- a/erpnext/public/js/education/lms/components/Article.vue
+++ /dev/null
@@ -1,44 +0,0 @@
-<template>
-<div>
- <ContentTitle :title="contentData.title" :author="contentData.author" :publishDate="contentData.publish_date">
- <slot></slot>
- </ContentTitle>
- <section class="article-content-section">
- <div>
- <div class="content" v-html="contentData.content"></div>
- <div class="text-right">
- </div>
- <div class="mt-3 text-right">
- <a class="text-muted" href="/report"><i class="octicon octicon-issue-opened" title="Report"></i> Report a
- Mistake</a>
- </div>
- </div>
- </section>
-</div>
-</template>
-<script>
-import ContentTitle from './ContentTitle.vue'
-export default {
- props: ['content', 'type'],
- name: 'Article',
- data() {
- return {
- contentData: ''
- }
- },
- mounted() {
- this.getContent().then(data => this.contentData = data);
- },
- methods: {
- getContent() {
- return lms.call('get_content', {
- content_type: this.type,
- content: this.content
- })
- }
- },
- components: {
- ContentTitle
- }
-};
-</script>
diff --git a/erpnext/public/js/education/lms/components/Breadcrumb.vue b/erpnext/public/js/education/lms/components/Breadcrumb.vue
deleted file mode 100644
index 1b617a3..0000000
--- a/erpnext/public/js/education/lms/components/Breadcrumb.vue
+++ /dev/null
@@ -1,56 +0,0 @@
-<template>
- <div>
- <nav aria-label="breadcrumb">
- <ol class="breadcrumb">
- <li v-for="(route, index) in routeData" class="breadcrumb-item active" aria-current="page">
- <router-link v-if="index != routeData.length - 1" :to="route.route">
- {{ route.label }}
- </router-link>
- <span v-else>{{ route.label }}</span>
- </li>
- </ol>
- </nav>
- </div>
-</template>
-<script type="text/javascript">
- export default {
- name: "Breadcrumb",
- data() {
- return {
- routeName: this.$route.name,
- routeParams: this.$route.params,
- routeData: [{
- label: "All Programs",
- route: "/List/Program"
- }]
- }
- },
- mounted() {
- this.buildBreadcrumb()
- },
- methods: {
- buildBreadcrumb() {
- if(this.routeName == 'program') {
- return
- }
- if(this.routeName == 'course') {
- let routeObject = {
- label: this.routeParams.program_name,
- route: `/Program/${this.routeParams.program_name}`
- }
- this.routeData.push(routeObject)
- }
- if(this.routeName == 'content') {
- this.routeData.push({
- label: this.routeParams.program_name,
- route: `/Program/${this.routeParams.program_name}`
- })
- this.routeData.push({
- label: this.routeParams.course_name,
- route: `/Program/${this.routeParams.program_name}/${this.routeParams.course_name}`
- })
- }
- }
- }
- };
-</script>
\ No newline at end of file
diff --git a/erpnext/public/js/education/lms/components/Button.vue b/erpnext/public/js/education/lms/components/Button.vue
deleted file mode 100644
index 4d8df4b..0000000
--- a/erpnext/public/js/education/lms/components/Button.vue
+++ /dev/null
@@ -1,25 +0,0 @@
-<template>
- <button :class="classList" v-on="$listeners" v-bind="$attrs" @click="goToRoute">
- <slot></slot>
- </button>
-</template>
-<script>
-export default {
- name: 'AButton',
- props: ['type', 'size', 'route'],
- computed: {
- classList() {
- return [
- 'btn',
- 'btn-' + this.type,
- 'btn-' + this.size
- ]
- }
- },
- methods: {
- goToRoute() {
- this.$router.push(this.route);
- }
- }
-}
-</script>
diff --git a/erpnext/public/js/education/lms/components/CardList.vue b/erpnext/public/js/education/lms/components/CardList.vue
deleted file mode 100644
index 10f6af0..0000000
--- a/erpnext/public/js/education/lms/components/CardList.vue
+++ /dev/null
@@ -1,28 +0,0 @@
-<template>
- <div class="featured-products-section py-3">
- <h5 class='featured-heading' v-html="title"></h5>
- <div class="featured-products row">
- <!-- <p class='lead text-center' v-html="description"></p> -->
- <slot name="card-list-slot"></slot>
- </div>
- <div class='mt-4 text-center'>
- <slot name="list-bottom"></slot>
- </div>
- </div>
-</template>
-<script>
-export default {
- props:['title', 'description'],
- name: "CardList",
-};
-</script>
-<style scoped>
-
-.featured-heading {
- text-transform: uppercase;
- letter-spacing: 0.5px;
- font-size: 12px;
- font-weight: 500;
-}
-
-</style>
diff --git a/erpnext/public/js/education/lms/components/ContentNavigation.vue b/erpnext/public/js/education/lms/components/ContentNavigation.vue
deleted file mode 100644
index a07c0f8..0000000
--- a/erpnext/public/js/education/lms/components/ContentNavigation.vue
+++ /dev/null
@@ -1,40 +0,0 @@
-<template>
- <div class="nav-buttons">
- <button class='btn btn-outline-secondary' @click="$router.go(-1)">Back</button>
- <button v-if="nextContent" class='btn btn-primary' @click="goNext()">Next</button>
- <button v-else class='btn btn-primary' @click="finish()">Finish Topic</button>
- </div>
-</template>
-
-<script>
-export default {
- props: ['nextContent', 'nextContentType'],
- name: 'ContentNavigation',
- methods: {
- addActivity() {
- if(this.$route.params.type != "Quiz"){
- console.log("Adding Activity")
- lms.call("add_activity",
- {
- course: this.$route.params.course_name,
- content_type: this.$route.params.type,
- content: this.$route.params.content,
- }
- )
- }
- },
- goNext() {
- this.addActivity()
- this.$router.push({ name: 'content', params: { course: this.$route.params.course_name, type:this.nextContentType, content:this.nextContent }})
- },
- finish() {
- this.addActivity()
- this.$router.push({ name: 'course', params: { program_name: this.$route.params.program_name, course_name: this.$route.params.course_name}})
- lms.trigger('course-completed', course_name);
- }
- }
-};
-</script>
-
-<style lang="css" scoped>
-</style>
diff --git a/erpnext/public/js/education/lms/components/ContentTitle.vue b/erpnext/public/js/education/lms/components/ContentTitle.vue
deleted file mode 100644
index a488ab8..0000000
--- a/erpnext/public/js/education/lms/components/ContentTitle.vue
+++ /dev/null
@@ -1,29 +0,0 @@
-<template>
- <section class='article-top-section video-section-bg'>
- <div>
- <div class="row">
- <div class="col-md-8">
- <h2>{{ title }}</h2>
- <span v-if="typeof author !== 'undefined' || author !== null" class="text-muted">
- <span v-if="publishDate">Published on {{ publishDate }}</span>
- <span v-if="author">— {{ author }}</span>
- </span>
- </div>
- <div class="col-md-4 text-right">
- <slot></slot>
- </div>
- </div>
- <hr>
- </div>
- </section>
-</template>
-
-<script>
-export default {
- props: ['title', 'publishDate', 'author'],
- name: 'ContentTitle',
-};
-</script>
-
-<style lang="css" scoped>
-</style>
diff --git a/erpnext/public/js/education/lms/components/CourseCard.vue b/erpnext/public/js/education/lms/components/CourseCard.vue
deleted file mode 100644
index 48a9f59..0000000
--- a/erpnext/public/js/education/lms/components/CourseCard.vue
+++ /dev/null
@@ -1,87 +0,0 @@
-<template>
- <div class="py-3 col-md-4 col-sm-12">
- <div class="card h-100">
- <div class="card-hero-img" v-if="course.hero_image" v-bind:style="{ 'background-image': 'url(' + image + ')' }"></div>
- <div v-else class="card-image-wrapper">
- <div class="image-body">{{ course.course_name }}</div>
- </div>
- <div class='card-body'>
- <h5 class="card-title">{{ course.course_name }}</h5>
- <span class="course-list text-muted" id="getting-started">
- {{ course.course_intro.substring(0,120) }}
- </span>
- </div>
- <div class='p-3' style="display: flex; justify-content: space-between;">
- <div>
- <span v-if="complete"><i class="mr-2 text-success fa fa-check-circle" aria-hidden="true"></i>Course Complete</span>
- </div>
- <div class='text-right'>
- <a-button
- :type="'primary'"
- size="sm"
- :route="courseRoute"
- >
- {{ buttonName }}
- </a-button>
- </div>
- </div>
- </div>
- </div>
-</template>
-
-<script>
-import AButton from './Button.vue';
-
-export default {
- props: ['course', 'program_name'],
- name: "CourseCard",
- components: {
- AButton
- },
- data() {
- return {
- courseDetails: {},
- }
- },
- mounted() {
- if(lms.store.checkLogin()) this.getCourseDetails().then(data => this.courseDetails = data)
- },
- computed: {
- courseRoute() {
- return `${this.program_name}/${this.course.name}`
- },
- complete() {
- if(lms.store.checkProgramEnrollment(this.program_name)){
- if (this.courseDetails.flag === "Completed" ) {
- return true
- }
- else {
- return false
- }
- }
- else {
- return false
- }
- },
- isLogin() {
- return lms.store.checkLogin()
- },
- buttonName() {
- if(lms.store.checkProgramEnrollment(this.program_name)){
- return "Start Course"
- }
- else {
- return "Explore"
- }
- }
- },
- methods: {
- getCourseDetails() {
- return lms.call('get_student_course_details', {
- course_name: this.course.name,
- program_name: this.program_name
- })
- },
- }
-};
-</script>
\ No newline at end of file
diff --git a/erpnext/public/js/education/lms/components/Navbar.vue b/erpnext/public/js/education/lms/components/Navbar.vue
deleted file mode 100644
index f3f3ce4..0000000
--- a/erpnext/public/js/education/lms/components/Navbar.vue
+++ /dev/null
@@ -1,85 +0,0 @@
-<template>
-<nav class="navbar navbar-light bg-white navbar-expand-lg sticky-top shadow-sm">
- <div class="container">
- <a class="navbar-brand" href="/lms">
- <span>{{ portal.title }}</span>
- </a>
- <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
- <span class="navbar-toggler-icon"></span>
- </button>
-
- <div class="collapse navbar-collapse" id="navbarSupportedContent">
- <ul class="navbar-nav mr-auto">
-
- <li class="nav-item">
- <a class="nav-link" href="lms#/List/Program">
- All Programs
- </a>
- </li>
-
- <li class="nav-item">
- <a class="nav-link" href="/lms#/Profile">
- Profile
- </a>
- </li>
- </ul>
- <ul class="navbar-nav ml-auto">
- <!-- post login tools -->
- <li v-if="isLogin" class="nav-item dropdown logged-in" id="website-post-login" data-label="website-post-login">
- <a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
- <span class="user-image-wrapper">
- <span class="avatar avatar-small" :title="fullName">
- <span class="avatar-frame" :style="avatarStyle" :title="fullName"></span>
- </span>
- </span>
- <span class="full-name">{{ fullName }}</span>
- <b class="caret"></b>
- </a>
- <ul class="dropdown-menu dropdown-menu-right" role="menu">
- <a class="dropdown-item" href="/me" rel="nofollow"> My Account </a>
- <a class="dropdown-item" href="/?cmd=web_logout" rel="nofollow"> Logout </a>
- </ul>
- </li>
-
- <li v-else class="nav-item">
- <a class="nav-link btn-login-area" href="/login">Login</a>
- </li>
- </ul>
- </div>
- </div>
-</nav>
-</template>
-<script>
-export default {
- name: "Home",
- data() {
- return{
- portal: {},
- avatar: frappe.user_image,
- fullName: frappe.full_name,
- isLogin: frappe.is_user_logged_in()
- }
- },
- mounted() {
- this.getPortalDetails().then(data => this.portal = data);
- },
- methods: {
- getPortalDetails() {
- return lms.call("get_portal_details")
- }
- },
- computed: {
- avatarStyle() {
- return `background-image: url("${this.avatar}")`
- },
- // isLogin() {
- // return frappe.is_user_logged_in()
- // },
- }
-};
-</script>
-<style scoped>
-a {
- text-decoration: none;
-}
-</style>
\ No newline at end of file
diff --git a/erpnext/public/js/education/lms/components/ProfileInfo.vue b/erpnext/public/js/education/lms/components/ProfileInfo.vue
deleted file mode 100644
index 5bad713..0000000
--- a/erpnext/public/js/education/lms/components/ProfileInfo.vue
+++ /dev/null
@@ -1,83 +0,0 @@
-<template>
- <div class="py-5">
- <div class="row">
- <div class="col-sm-12">
- <div>
- <h3>{{ fullName }}</h3>
- <ul>
- <li class="row">
- <div class="col-md-3 col-sm-4 pr-0 text-muted">Email:</div>
- <div class="col-md-9 col-sm-8">{{ email }}</div>
- </li>
- <li v-if="joiningDate" class="row">
- <div class="col-md-3 col-sm-4 pr-0 text-muted">Date of Joining:</div>
- <div class="col-md-9 col-sm-8">{{ joiningDate }}</div>
- </li>
- <li class="row">
- <div class="col-md-3 col-sm-4 pr-0 text-muted">Programs Enrolled:</div>
- <div class="col-md-9 col-sm-8">
- <ul v-if="enrolledPrograms">
- <li v-for="program in enrolledPrograms" :key="program">{{ program }}</li>
- </ul>
- <span v-else>None</span>
- </div>
- </li>
- </ul>
- </div>
- <a href="/update-profile" class="edit-button text-muted">Edit Profile</a>
- </div>
- </div>
- <div ></div>
- </div>
-</template>
-<script>
-
-export default {
- props: ['enrolledPrograms'],
- name: "ProfileInfo",
- data() {
- return {
- avatar: frappe.user_image,
- fullName: frappe.full_name,
- abbr: frappe.get_abbr(frappe.get_cookie("full_name")),
- email: frappe.session.user,
- joiningDate: ''
- }
- },
- mounted(){
- this.getJoiningDate().then(data => {
- if(data) {
- this.joiningDate = lms.moment(String(data)).format('D MMMM YYYY')
- }
- })
- },
- computed: {
- avatarStyle() {
- return `background-image: url("${this.avatar}")`
- },
- },
- methods: {
- getJoiningDate() {
- return lms.call("get_joining_date")
- }
- }
-};
-</script>
-<style scoped>
- .edit-button {
- position: absolute;
- top: 0;
- right: 0;
- }
-
- .standard-image {
- font-size: 72px;
- border-radius: 6px;
- }
-
- ul {
- list-style-type: none;
- padding: 0;
- margin: 0
- }
-</style>
\ No newline at end of file
diff --git a/erpnext/public/js/education/lms/components/ProgramCard.vue b/erpnext/public/js/education/lms/components/ProgramCard.vue
deleted file mode 100644
index 15a9fcd..0000000
--- a/erpnext/public/js/education/lms/components/ProgramCard.vue
+++ /dev/null
@@ -1,82 +0,0 @@
-<template>
-<div class='py-3 col-md-4 col-sm-12'>
- <div class="card h-100">
- <router-link :to="'/Program/' + program.name">
- <div class="card-hero-img" v-if="program.hero_image" v-bind:style="{ 'background-image': 'url(' + image + ')' }"></div>
- <div v-else class="card-image-wrapper text-center">
- <div class="image-body">{{ program.program_name }}</div>
- </div>
- <div class='card-body'>
- <h5 class='card-title'>{{ program.program_name }}</h5>
- <div class="text-muted">{{ program.description.substring(0,120) }}...</div>
- </div>
- </router-link>
- <div class='text-right p-3'>
- <button v-if="program.intro_video" class='btn btn-light btn-sm' data-toggle="modal" data-target="#videoModal">Watch Intro</button>
- <a-button v-if="enrolled" type="dark" size="sm" :route="programPageRoute">
- {{ buttonName }}
- </a-button>
- <button v-else-if="isLogin" class='btn btn-dark btn-sm' @click="enroll()">{{ enrollButton }}</button>
- <a v-else class='btn btn-secondary btn-sm' href="/login#signup">Sign Up</a>
- </div>
- <VideoModal v-if="program.intro_video" :title="program.program_name" :video="program.intro_video"/>
- </div>
-</div>
-</template>
-<script>
-import AButton from './Button.vue';
-import VideoModal from './VideoModal.vue';
-export default {
- props: ['program', 'enrolled'],
- name: "ProgramCard",
- data() {
- return {
- isLogin: frappe.is_user_logged_in(),
- enrollButton: 'Enroll Now',
- programRoute: { name: 'program', params: { program_name: this.program.name }},
- image: "'" + this.program.hero_image + "'"
- };
- },
- methods: {
- enroll() {
- this.enrollButton = 'Enrolling...'
- lms.call('enroll_in_program', {
- program_name: this.program.name,
- }).then(data => {
- lms.store.updateEnrolledPrograms()
- this.$router.push(this.programRoute)
- })
- }
- },
- computed: {
- buttonName() {
- if(this.enrolled){
- return "Start Program"
- }
- else {
- return "Enroll"
- }
- },
- programPageRoute() {
- return this.programRoute
- },
- isEnrolled() {
- return lms.store.enrolledPrograms.includes(this.program.name)
- }
- },
- components: {
- AButton,
- VideoModal
- }
-};
-</script>
-
-<style lang="css" scoped>
- a {
- text-decoration: none;
- color: black;
- }
- a.btn-secondary {
- color: white !important;
- }
-</style>
\ No newline at end of file
diff --git a/erpnext/public/js/education/lms/components/ProgressCard.vue b/erpnext/public/js/education/lms/components/ProgressCard.vue
deleted file mode 100644
index 66b61f6..0000000
--- a/erpnext/public/js/education/lms/components/ProgressCard.vue
+++ /dev/null
@@ -1,89 +0,0 @@
-<template>
- <div class='py-3 col-md-4 col-sm-12'>
- <div class="card h-100">
- <div class='card-body'>
- <router-link :to="'/Program/' + programData.name">
- <h5 class='card-title'>{{ programData.program }}</h5>
- </router-link>
- <span class="course-list text-muted" id="getting-started">
- Courses
- <ul class="mb-0 mt-1 list-unstyled" style="padding-left: 1.5em;">
- <li v-for="item in programData.progress" :key="item.name">
- <span v-if="item.is_complete"><i class="text-success fa fa-check-circle" aria-hidden="true"></i></span>
- <span v-else><i class="text-secondary fa fa-circle-o" aria-hidden="true"></i></span>
- {{ item.course_name }}
- </li>
- </ul>
- </span>
- </div>
- <div class='p-3' style="display: flex; justify-content: space-between;">
- <div></div>
- <div class='text-right'>
- <a-button
- :type="buttonType"
- size="sm btn-block"
- :route="programRoute"
- >
- {{ buttonName }}
- </a-button>
- </div>
- </div>
- </div>
- </div>
-</template>
-<script>
-import AButton from './Button.vue';
-export default {
- props: ['program'],
- name: "ProgressCard",
- data() {
- return {
- programData: {}
- };
- },
- mounted() {
- this.getProgramProgress().then(data => this.programData = data)
- },
- methods: {
- getProgramProgress() {
- return lms.call('get_program_progress', {
- program_name: this.program
- })
- },
- },
- computed: {
- programRoute() {
- return {name: 'program', params: {program_name: this.program}}
- },
- buttonType() {
- if (this.programData.percentage == 100 ){
- return "success"
- }
- else if (this.programData.percentage == "0" ) {
- return "secondary"
- }
- else {
- return "info"
- }
- },
- buttonName() {
- if (this.programData.percentage == 100 ){
- return "Program Complete"
- }
- else {
- return `${this.programData.percentage}% Completed`
- }
- }
- },
- components: {
- AButton
- },
-};
-</script>
-<style scoped>
-
- a {
- text-decoration: none;
- color: black;
- }
-</style>
diff --git a/erpnext/public/js/education/lms/components/Quiz.vue b/erpnext/public/js/education/lms/components/Quiz.vue
deleted file mode 100644
index 0a6199a..0000000
--- a/erpnext/public/js/education/lms/components/Quiz.vue
+++ /dev/null
@@ -1,119 +0,0 @@
-<template>
- <section class="quiz-section">
- <div>
- <div class="row">
- <div class="col-md-8">
- <h2>{{ content }}</h2>
- </div>
- </div>
- <div class="content">
- <hr>
- <div id="quiz" :name="content">
- <div id="quiz-body">
- <component v-for="question in quizData" :key="question.name" v-bind:is="question.type" :question="question" @updateResponse="updateResponse" :isDisabled="isDisabled"></component>
- </div>
- <div class="mt-3">
- <div>
- <div v-if="isDisabled || submitted" id="post-quiz-actions" class="row">
- <div class="col-md-8 text-left">
- <span v-html="message"></span>
- </div>
- <div class="col-md-4 text-right">
- <slot></slot>
- </div>
- </div>
- <div v-else id="quiz-actions" class="text-right">
- <button class='btn btn-outline-secondary' type="reset" :disabled="isDisabled">Reset</button>
- <button class='btn btn-primary' @click="submitQuiz" type="button" :disabled="isDisabled">Submit</button>
- </div>
- </div>
- </div>
- </div>
- </div>
- <div class="mt-3 text-right">
- <a class="text-muted" href="/report"><i class="octicon octicon-issue-opened" title="Report"></i> Report a
- Mistake</a>
- </div>
- </div>
-</section>
-</template>
-
-<script>
-import QuizSingleChoice from "./Quiz/QuizSingleChoice.vue"
-import QuizMultipleChoice from "./Quiz/QuizMultipleChoice.vue"
-
-export default {
- props: ['content', 'type'],
- name: 'Quiz',
- data() {
- return {
- quizData: '',
- quizResponse: {},
- score: '',
- submitted: false,
- isDisabled: false,
- quizStatus: {},
- }
- },
- mounted() {
- this.getQuizWithoutAnswers().then(data => {
- this.quizData = data.quizData
- this.quizStatus = data.status
- this.isDisabled = data.status.is_complete
- });
- },
- components: {
- 'SingleChoice': QuizSingleChoice,
- 'MultipleChoice': QuizMultipleChoice
- },
- methods: {
- getQuizWithoutAnswers() {
- return lms.call("get_quiz_without_answers",
- {
- quiz_name: this.content,
- course_name: this.$route.params.course_name
- }
- )
- },
- updateResponse(res) {
- this.quizResponse[res.question] = res.option
- },
- submitQuiz() {
- lms.call("evaluate_quiz",
- {
- quiz_response: this.quizResponse,
- quiz_name: this.content,
- course: this.$route.params.course_name
- }
- ).then(data => {
- this.score = data
- this.submitted = true
- this.quizResponse = null
- });
- }
- },
- computed: {
- currentComponent: function() {
- if(this.quizData.type === "MultipleChoice") {
- return 'QuizMultipleChoice'
- }
- else {
- return 'QuizSingleChoice'
- }
- },
- message: function() {
- if(this.submitted) {
- return '<h3>Your Score: <span id="result">'+ this.score +'</span></h3>'
- }
- let message = '<h4>You have exhausted all attempts for this quiz.</h4>'
- if(this.quizStatus.result == 'Pass') {
- message = "<h4>You have successfully completed this quiz.</h4>Score: " + this.quizStatus.score
- }
- return message
- }
- },
-};
-</script>
-
-<style lang="css" scoped>
-</style>
diff --git a/erpnext/public/js/education/lms/components/Quiz/QuizMultipleChoice.vue b/erpnext/public/js/education/lms/components/Quiz/QuizMultipleChoice.vue
deleted file mode 100644
index 338b1ac..0000000
--- a/erpnext/public/js/education/lms/components/Quiz/QuizMultipleChoice.vue
+++ /dev/null
@@ -1,34 +0,0 @@
-<template>
- <div class="question mt-4">
- <h5>{{ question.question }}</h5>
- <div class="options ml-2">
- <div v-for="option in question.options" :key="option.name" class="form-check pb-1">
- <input v-model="checked" class="form-check-input" type="checkbox" :name="question.name" :id="option.name" :value="option.name" @change="emitResponse(question.name, option.name)" :disabled="isDisabled">
- <label class="form-check-label" :for="option.name">
- {{ option.option }}
- </label>
- </div>
- </div>
-</div>
-</template>
-
-<script>
-export default {
- props: ['question', 'isDisabled'],
- name: 'QuizSingleChoice',
- data() {
- return {
- checked: []
- }
- },
- methods: {
- emitResponse(q, o) {
- console.log(this.checked)
- this.$emit('updateResponse', {'question':q , 'option': this.checked, 'type': this.question.type})
- }
- }
-};
-</script>
-
-<style lang="css" scoped>
-</style>
diff --git a/erpnext/public/js/education/lms/components/Quiz/QuizSingleChoice.vue b/erpnext/public/js/education/lms/components/Quiz/QuizSingleChoice.vue
deleted file mode 100644
index 235cbce..0000000
--- a/erpnext/public/js/education/lms/components/Quiz/QuizSingleChoice.vue
+++ /dev/null
@@ -1,28 +0,0 @@
-<template>
- <div class="question mt-4">
- <h5>{{ question.question }}</h5>
- <div class="options ml-2">
- <div v-for="option in question.options" :key="option.name" class="form-check pb-1">
- <input class="form-check-input" type="radio" :name="question.name" :id="option.name" :value="option.name" @change="emitResponse(question.name, option.name)" :disabled="isDisabled">
- <label class="form-check-label" :for="option.name">
- {{ option.option }}
- </label>
- </div>
- </div>
-</div>
-</template>
-
-<script>
-export default {
- props: ['question', 'isDisabled'],
- name: 'QuizSingleChoice',
- methods: {
- emitResponse(q, o) {
- this.$emit('updateResponse', {'question':q , 'option': o, 'type': this.question.type})
- }
- }
-};
-</script>
-
-<style lang="css" scoped>
-</style>
diff --git a/erpnext/public/js/education/lms/components/ScoreCard.vue b/erpnext/public/js/education/lms/components/ScoreCard.vue
deleted file mode 100644
index 80b12cb..0000000
--- a/erpnext/public/js/education/lms/components/ScoreCard.vue
+++ /dev/null
@@ -1,60 +0,0 @@
-<template>
- <div v-if="quizData" class='py-3 col-md-4 col-sm-12'>
- <div class="card h-100">
- <div class='card-body'>
- <h5 class='card-title'>{{ quizData.program }}</h5>
- <div v-for="attempt in quizData.quiz_attempt" :key="attempt.content" class="course-list" id="getting-started">
- <div>
- {{ attempt.content }}
- <ul v-if="attempt.is_complete">
- <li><span class="text-muted">Score: </span>{{ attempt.score }}</li>
- <li><span class="text-muted">Status: </span>{{attempt.result }}</li>
- </ul>
- <span v-else>- Unattempted</span>
- </div>
- </div>
- </div>
- <div class='p-3' style="display: flex; justify-content: space-between;">
- <div></div>
- <div class='text-right'>
- <a-button
- :type="'primary'"
- size="sm btn-block"
- :route="programRoute"
- >
- Go To Program
- </a-button>
- </div>
- </div>
- </div>
- </div>
-</template>
-<script>
-import AButton from './Button.vue';
-export default {
- props: ['program'],
- name: "ScoreCard",
- data() {
- return {
- quizData: {}
- };
- },
- mounted() {
- this.getQuizProgress().then(data => this.quizData = data)
- },
- methods: {
- getQuizProgress() {
- return lms.call('get_quiz_progress_of_program', {
- program_name: this.program
- })
- },
- programRoute() {
- return {name: 'program', params: {program_name: this.program}}
- },
- },
- components: {
- AButton
- },
-};
-</script>
-
diff --git a/erpnext/public/js/education/lms/components/TopSection.vue b/erpnext/public/js/education/lms/components/TopSection.vue
deleted file mode 100644
index c27d003..0000000
--- a/erpnext/public/js/education/lms/components/TopSection.vue
+++ /dev/null
@@ -1,27 +0,0 @@
-<template>
-<div class="hero">
- <h1 class="text-center" v-html="title"></h1>
- <p class='text-center' v-html="description"></p>
- <p class="text-center padding">
- <slot></slot>
- </p>
-</div>
-</template>
-<script>
-
-export default {
- props: ['title', 'description'],
- name: "TopSection",
-};
-</script>
-<style scoped>
- .hero {
- padding-top: 50px;
- padding-bottom: 100px;
- }
-
- .hero h1 {
- font-size: 40px;
- font-weight: 200;
- }
-</style>
diff --git a/erpnext/public/js/education/lms/components/TopSectionButton.vue b/erpnext/public/js/education/lms/components/TopSectionButton.vue
deleted file mode 100644
index 0fa49d4..0000000
--- a/erpnext/public/js/education/lms/components/TopSectionButton.vue
+++ /dev/null
@@ -1,49 +0,0 @@
-<template>
- <button v-if="isLoggedIn" class='btn btn-primary btn-md' @click="primaryAction()">{{ buttonName }}</button>
- <a v-else class='btn btn-primary btn-md' href="/login#signup">{{ buttonName }}</a>
-</template>
-<script>
-export default {
- name: "TopSectionButton",
- data() {
- return {
- buttonName: '',
- isLoggedIn: lms.store.checkLogin(),
- nextContent: '',
- nextContentType: '',
- nextCourse: '',
- link: '',
- }
- },
- mounted() {
- this.computeButtons()
- },
- methods: {
- computeButtons(){
- if(this.isLoggedIn){
- this.buttonName = 'Explore Programs'
- }
- else{
- this.buttonName = 'Sign Up'
- }
- },
- primaryAction() {
- if(this.$route.name == 'home'){
- this.$router.push('List/Program');
- }
- else if(this.$route.name == 'program' && lms.store.enrolledPrograms.includes(this.$route.params.program_name)){
- this.$router.push({ name: 'content', params: { program_name: this.$route.params.program_name, course: this.nextCourse, type: this.nextContentType, content: this.nextContent}})
- }
- else {
- lms.call("enroll_in_program",
- {
- program_name: this.$route.params.program_name,
- student_email_id: frappe.session.user
- }
- )
- lms.store.updateEnrolledPrograms()
- }
- },
- }
-};
-</script>
\ No newline at end of file
diff --git a/erpnext/public/js/education/lms/components/TopicCard.vue b/erpnext/public/js/education/lms/components/TopicCard.vue
deleted file mode 100644
index 4cb8e85..0000000
--- a/erpnext/public/js/education/lms/components/TopicCard.vue
+++ /dev/null
@@ -1,112 +0,0 @@
-
-<template>
- <div class="py-3 col-md-4 col-sm-12">
- <div class="card h-100">
- <div class="card-hero-img" v-if="topic.hero_image" v-bind:style="{ 'background-image': 'url(' + image + ')' }"></div>
- <div v-else class="card-image-wrapper">
- <div class="image-body">{{ topic.topic_name }}</div>
- </div>
- <div class='card-body'>
- <h5 class="card-title">{{ topic.topic_name }}</h5>
- <span class="course-list text-muted" id="getting-started">
- Content
- <ul class="mb-0 mt-1" style="padding-left: 1.5em;">
- <li v-for="content in topic.topic_content" :key="content.name">
- <router-link v-if="isLogin" tag="a" :class="'text-muted'" :to="{name: 'content', params:{program_name: program_name, topic:topic.name, course_name: course_name, type:content.content_type, content: content.content} }">
- {{ content.content }}
- </router-link>
- <div v-else><span style="padding-right: 0.4em"></span>{{ content.content }}</div>
- </li>
- </ul>
- </span>
- </div>
- <div v-if="isLogin" class='p-3' style="display: flex; justify-content: space-between;">
- <div>
- <span v-if="complete"><i class="mr-2 text-success fa fa-check-circle" aria-hidden="true"></i>Course Complete</span>
- </div>
- <div class='text-right'>
- <a-button
- :type="'primary'"
- size="sm"
- :route="firstContentRoute"
- >
- {{ buttonName }}
- </a-button>
- </div>
- </div>
- </div>
- </div>
-</template>
-
-<script>
-import AButton from './Button.vue';
-
-export default {
- props: ['topic', 'course_name', 'program_name'],
- name: "TopicCard",
- data() {
- return {
- topicDetails: {}
- }
- },
- mounted() {
- if(lms.store.checkLogin()) this.gettopicDetails().then(data => this.topicDetails = data)
- },
- components: {
- AButton
- },
- computed: {
- firstContentRoute() {
- if(lms.store.checkLogin()){
- return `/Program/${this.program_name}/${this.course_name}/${this.topic.name}/${this.topicDetails.content_type}/${this.topicDetails.content}`
- }
- else {
- return {}
- }
- },
- complete() {
- if(lms.store.checkProgramEnrollment(this.program_name)){
- if (this.topicDetails.flag === "Completed" ) {
- return true
- }
- else {
- return false
- }
- }
- else {
- return false
- }
- },
- isLogin() {
- // return lms.store.checkProgramEnrollment(this.program_name)
- return lms.store.checkLogin()
- },
- buttonName() {
- if(lms.store.checkProgramEnrollment(this.program_name)){
- if (this.topicDetails.flag == 'Continue'){
- return 'Continue'
- }
- else {
- return 'Start Topic'
- }
- }
- else {
- return "Explore"
- }
- }
- },
- methods: {
- iconClass(content_type) {
- if(content_type == 'Video') return 'fa fa-play'
- if(content_type == 'Article') return 'fa fa-file-text-o'
- if(content_type == 'Quiz') return 'fa fa-question-circle-o'
- },
- gettopicDetails() {
- return lms.call('get_student_topic_details', {
- topic_name: this.topic.name,
- course_name: this.course_name,
- })
- },
- }
-};
-</script>
\ No newline at end of file
diff --git a/erpnext/public/js/education/lms/components/Video.vue b/erpnext/public/js/education/lms/components/Video.vue
deleted file mode 100644
index 50b4dd4..0000000
--- a/erpnext/public/js/education/lms/components/Video.vue
+++ /dev/null
@@ -1,63 +0,0 @@
-<template>
-<div>
- <div class='mt-2'>
- <div>
- <div class="mt-3 row">
- <div class="col-md-8">
- <h2>{{ contentData.name }}</h2>
- <span class="text-muted">
- <i class="octicon octicon-clock" title="Duration"></i> <span v-if="contentData.duration"> {{ contentData.duration }} Mins — </span><span v-if="contentData.publish_date"> Published on {{ contentData.publish_date }}. </span>
- </span>
- </div>
- <div class="col-md-4 text-right">
- <slot></slot>
- </div>
- </div>
- <youtube-player :url="contentData.url" class="mt-3"/>
- <hr>
- </div>
-</div>
-<div class="video-description-section">
- <div>
- <div class="content" v-html="contentData.description">
- </div>
- <div class="text-right hidden">
- <a class='btn btn-outline-secondary' href="/classrooms/module">Previous</a>
- <a class='btn btn-primary' href="/classrooms/module">Next</a>
- </div>
- <div class="mt-3 text-right">
- <a class="text-muted" href="/report"><i class="octicon octicon-issue-opened" title="Report"></i> Report a
- Mistake</a>
- </div>
- </div>
-</div>
-</div>
-</template>
-<script>
-import YoutubePlayer from './YoutubePlayer.vue'
-
-export default {
- props: ['content', 'type'],
- name: 'Video',
- data() {
- return {
- contentData: '',
- }
- },
- components: {
- YoutubePlayer
- },
- mounted() {
- this.getContent()
- .then(data => this.contentData = data)
- },
- methods: {
- getContent() {
- return lms.call('get_content', {
- content_type: this.type,
- content: this.content
- })
- }
- }
-};
-</script>
\ No newline at end of file
diff --git a/erpnext/public/js/education/lms/components/VideoModal.vue b/erpnext/public/js/education/lms/components/VideoModal.vue
deleted file mode 100644
index 71227ad..0000000
--- a/erpnext/public/js/education/lms/components/VideoModal.vue
+++ /dev/null
@@ -1,35 +0,0 @@
-<template>
- <div class="modal" id="videoModal" tabindex="-1" role="dialog">
- <div class="modal-dialog" role="document">
- <div class="modal-content">
- <div class="modal-header">
- <h5 class="modal-title">{{ title }}</h5>
- <button type="button" class="close" data-dismiss="modal" aria-label="Close">
- <span id="close_modal" aria-hidden="true" @click="stopVideo()">×</span>
- </button>
- </div>
- <div class="modal-body">
- <youtube-player :url="video"/>
- </div>
- </div>
- </div>
- </div>
-</template>
-<script type="text/javascript">
-import YoutubePlayer from './YoutubePlayer.vue'
-
-export default {
- name: 'VideoModal',
- props: ['title', 'video'],
- components: {
- YoutubePlayer
- },
- methods: {
- stopVideo() {
- $('.yvideo').each(function() {
- this.contentWindow.postMessage('{"event":"command","func":"stopVideo","args":""}', '*')
- });
- }
- }
-};
-</script>
\ No newline at end of file
diff --git a/erpnext/public/js/education/lms/components/YoutubePlayer.vue b/erpnext/public/js/education/lms/components/YoutubePlayer.vue
deleted file mode 100644
index 9377b57..0000000
--- a/erpnext/public/js/education/lms/components/YoutubePlayer.vue
+++ /dev/null
@@ -1,36 +0,0 @@
-<template>
- <div class="embed-responsive embed-responsive-16by9">
- <iframe class="embed-responsive-item yvideo" :src="'https://www.youtube.com/embed/' + videoID + '?version=3&enablejsapi=1'" allowfullscreen></iframe>
- </div>
-</template>
-<script type="text/javascript">
- export default {
- name: 'YoutubePlayer',
- props: ['url'],
- data() {
- return {
- videoID: ''
- }
- },
- watch: {
- url() {
- this.videoID = this.getVideoID(this.url)
- }
- },
- methods: {
- getVideoID(link) {
- if (!Array.prototype.last){
- Array.prototype.last = function(){
- return this[this.length - 1];
- };
- };
- if (link.includes('v=')){
- return link.split('v=')[1].split('&')[0]
- }
- else if (link.includes('youtu.be')) {
- return link.split('/').last().split('?')[0]
- }
- }
- }
- };
-</script>
\ No newline at end of file
diff --git a/erpnext/public/js/education/lms/lms.js b/erpnext/public/js/education/lms/lms.js
deleted file mode 100644
index 4665b14..0000000
--- a/erpnext/public/js/education/lms/lms.js
+++ /dev/null
@@ -1,81 +0,0 @@
-import Vue from 'vue/dist/vue.js';
-import VueRouter from 'vue-router/dist/vue-router.js';
-import moment from 'moment/min/moment.min.js';
-
-import lmsRoot from "./lmsRoot.vue";
-import routes from './routes';
-import './call';
-
-Vue.use(VueRouter);
-
-var store = {
- enrolledPrograms: [],
- enrolledCourses: []
-};
-
-// let profile_page = `<a class="dropdown-item" href="/lms#/Profile" rel="nofollow"> LMS Profile </a>`
-// document.querySelector('#website-post-login > ul').innerHTML += profile_page
-
-frappe.ready(() => {
- frappe.provide('lms');
-
- lms.moment = moment;
-
- lms.store = new Vue({
- data: store,
- methods: {
- updateEnrolledPrograms() {
- if(this.checkLogin()) {
- lms.call("get_program_enrollments").then(data => {
- this.enrolledPrograms = data;
- });
- }
- },
- updateEnrolledCourses() {
- if(this.checkLogin()) {
- lms.call("get_all_course_enrollments").then(data => {
- this.enrolledCourses = data;
- });
- }
- },
- checkLogin() {
- return frappe.is_user_logged_in();
- },
- updateState() {
- this.checkLogin();
- this.updateEnrolledPrograms();
- this.updateEnrolledCourses();
- },
- checkProgramEnrollment(programName) {
- if(this.checkLogin()){
- if(this.enrolledPrograms) {
- if(this.enrolledPrograms.includes(programName)) {
- return true;
- }
- else {
- return false;
- }
- }
- else {
- return false;
- }
- }
- else {
- return false;
- }
- }
- }
- });
- lms.view = new Vue({
- el: "#lms-app",
- router: new VueRouter({ routes }),
- template: "<lms-root/>",
- components: { lmsRoot },
- mounted() {
- lms.store.updateState();
- }
- });
- lms.view.$router.afterEach((to, from) => {
- window.scrollTo(0,0);
- });
-});
\ No newline at end of file
diff --git a/erpnext/public/js/education/lms/lmsRoot.vue b/erpnext/public/js/education/lms/lmsRoot.vue
deleted file mode 100644
index d359265..0000000
--- a/erpnext/public/js/education/lms/lmsRoot.vue
+++ /dev/null
@@ -1,45 +0,0 @@
-<template>
- <div id="lms-root">
- <navbar></navbar>
- <main class="container my-5">
- <div class="page_content">
- <router-view :key="$route.fullPath"></router-view>
- </div>
- </main>
- </div>
-</template>
-<script>
-import Navbar from "./components/Navbar.vue"
-export default {
- name: "lmsRoot",
- components: {
- Navbar
- }
-};
-</script>
-<style>
- div.card-hero-img {
- height: 220px;
- background-size: cover;
- background-repeat: no-repeat;
- background-position: center;
- background-color: rgb(250, 251, 252);
- }
-
- .card-image-wrapper {
- display: flex;
- overflow: hidden;
- height: 220px;
- background-color: rgb(250, 251, 252);
- justify-content: center;
- }
-
- .image-body {
- align-self: center;
- color: #d1d8dd;
- font-size: 24px;
- font-weight: 600;
- line-height: 1;
- padding: 20px;
- }
-</style>
diff --git a/erpnext/public/js/education/lms/pages/ContentPage.vue b/erpnext/public/js/education/lms/pages/ContentPage.vue
deleted file mode 100644
index 224ee03..0000000
--- a/erpnext/public/js/education/lms/pages/ContentPage.vue
+++ /dev/null
@@ -1,84 +0,0 @@
-<template>
- <div>
- <component v-bind:is="currentComponent" :content="content" :type="type">
- <ContentNavigation :nextContent="nextContent" :nextContentType="nextContentType"/>
- </component>
- </div>
-</template>
-<script>
-import Article from "../components/Article.vue"
-import Quiz from "../components/Quiz.vue"
-import Video from "../components/Video.vue"
-import ContentNavigation from "../components/ContentNavigation.vue"
-
-export default {
- props:['program_name', 'course_name', 'topic', 'type', 'content'],
- name: "ContentPage",
- data() {
- return{
- nextContent: '',
- nextContentType: '',
- }
- },
- computed: {
- currentComponent: function() {
- if(this.type === "Article") {
- return 'Article'
- }
- else if(this.type === "Quiz") {
- return 'Quiz'
- }
- else if(this.type === "Video") {
- return 'Video'
- }
- },
- },
- mounted() {
- this.getNextContent().then(data => {
- this.nextContent = data.content,
- this.nextContentType = data.content_type
- });
- },
- methods: {
- getNextContent(){
- return lms.call("get_next_content",
- {
- current_content: this.content,
- current_content_type: this.type,
- topic: this.topic,
- }
- );
- }
- },
- components: {
- Article,
- Video,
- Quiz,
- ContentNavigation
- }
-};
-</script>
-
-<style>
-.footer-message {
- display: none;
-}
-
-.video-description-section {
- padding-top: 0em !important;
-}
-
-.article-top-section {
- padding-top: 0.5em !important;
- padding-bottom: 0rem !important;
-}
-
-.article-content-section {
- padding-top: 0em !important;
-}
-
-.quiz-section {
- padding-top: 0.5em !important;
- padding-bottom: 0rem !important;
-}
-</style>
\ No newline at end of file
diff --git a/erpnext/public/js/education/lms/pages/CoursePage.vue b/erpnext/public/js/education/lms/pages/CoursePage.vue
deleted file mode 100644
index dc3d130..0000000
--- a/erpnext/public/js/education/lms/pages/CoursePage.vue
+++ /dev/null
@@ -1,49 +0,0 @@
-<template>
-<div>
- <breadcrumb></breadcrumb>
- <TopSection v-bind:title="course.course_name" v-bind:description="course.course_intro">
- </TopSection>
- <CardList :title="'Topics'" :description="''">
- <TopicCard slot="card-list-slot" v-for="topic in topicData" :topic="topic" :course_name="course_name" :program_name="program_name" :key="topic.name"/>
- </CardList>
-</div>
-</template>
-<script>
-import TopSection from "../components/TopSection.vue"
-import CardList from "../components/CardList.vue"
-import TopicCard from "../components/TopicCard.vue"
-import Breadcrumb from "../components/Breadcrumb.vue"
-
-export default {
- props: ['program_name','course_name'],
- name: "CoursePage",
- components: {
- TopSection,
- CardList,
- TopicCard,
- Breadcrumb
- },
- data() {
- return {
- course: {},
- topicData: [],
- }
- },
- mounted() {
- this.getCourseDetails().then(data => this.course = data);
- this.getTopics().then(data => this.topicData = data);
- },
- methods: {
- getCourseDetails() {
- return lms.call('get_course_details', {
- course_name: this.course_name
- });
- },
- getTopics() {
- return lms.call('get_topics', {
- course_name: this.course_name
- })
- }
- }
-};
-</script>
\ No newline at end of file
diff --git a/erpnext/public/js/education/lms/pages/Home.vue b/erpnext/public/js/education/lms/pages/Home.vue
deleted file mode 100644
index 6554a76..0000000
--- a/erpnext/public/js/education/lms/pages/Home.vue
+++ /dev/null
@@ -1,48 +0,0 @@
-<template>
-<div>
- <TopSection :title="portal.title" :description="portal.description">
- <TopSectionButton/>
- </TopSection>
- <CardList :title="'Featured Programs'" :description="'Master ERPNext'">
- <ProgramCard slot="card-list-slot" v-for="item in featuredPrograms" :key="item.program.name" :program="item.program" :enrolled="item.is_enrolled"/>
- <AButton slot="list-bottom" :type="'primary'" :size="'md'" :route="'List/Program'">View All</AButton>
- </CardList>
-</div>
-</template>
-<script>
-import Button from '../components/Button.vue';
-import TopSection from "../components/TopSection.vue"
-import CardList from "../components/CardList.vue"
-import ProgramCard from "../components/ProgramCard.vue"
-import TopSectionButton from "../components/TopSectionButton.vue"
-
-export default {
- name: "Home",
- data() {
- return{
- portal: {},
- featuredPrograms: {},
- // enrolledPrograms: new Set()
- }
- },
- components: {
- AButton: Button,
- TopSection,
- CardList,
- ProgramCard,
- TopSectionButton
- },
- mounted() {
- this.getPortalDetails().then(data => this.portal = data);
- this.getFeaturedPrograms().then(data => this.featuredPrograms = data);
- },
- methods: {
- getPortalDetails() {
- return lms.call("get_portal_details")
- },
- getFeaturedPrograms() {
- return lms.call("get_featured_programs")
- }
- }
-};
-</script>
\ No newline at end of file
diff --git a/erpnext/public/js/education/lms/pages/ListPage.vue b/erpnext/public/js/education/lms/pages/ListPage.vue
deleted file mode 100644
index cf5cecc..0000000
--- a/erpnext/public/js/education/lms/pages/ListPage.vue
+++ /dev/null
@@ -1,53 +0,0 @@
-<template>
- <div>
- <TopSection :title="'Programs at ' + portal.title" :description="portal.description">
- <AButton v-if="isLogin" :type="'primary'" :size="'lg'" :route="{ name: 'signup'}">Sign Up</AButton>
- </TopSection>
- <CardList :title="'All Programs'" :description="''">
- <ProgramCard slot="card-list-slot" v-for="item in masterData" :key="item.program.name" :program="item.program" :enrolled="item.is_enrolled"/>
- </CardList>
- </div>
-</template>
-<script>
-import ProgramCard from '../components/ProgramCard.vue';
-import CourseCard from "../components/CourseCard.vue"
-import Button from '../components/Button.vue';
-import TopSection from "../components/TopSection.vue"
-import CardList from "../components/CardList.vue"
-
-
-export default {
- props: ['master'],
- name: "ListPage",
- components: {
- AButton: Button,
- CourseCard,
- ProgramCard,
- CardList,
- TopSection
- },
- data() {
- return {
- portal: {},
- masterData: {}
- }
- },
- mounted() {
- this.getPortalDetails().then(data => this.portal = data);
- this.getMaster().then(data => this.masterData = data);
- },
- methods: {
- getPortalDetails() {
- return lms.call("get_portal_details")
- },
- getMaster() {
- return lms.call("get_all_programs")
- }
- },
- computed: {
- isLogin() {
- return !lms.store.checkLogin()
- }
- }
-};
-</script>
\ No newline at end of file
diff --git a/erpnext/public/js/education/lms/pages/ProfilePage.vue b/erpnext/public/js/education/lms/pages/ProfilePage.vue
deleted file mode 100644
index beff5eb..0000000
--- a/erpnext/public/js/education/lms/pages/ProfilePage.vue
+++ /dev/null
@@ -1,50 +0,0 @@
-<template>
-<div>
- <ProfileInfo :enrolledPrograms="enrolledPrograms"></ProfileInfo>
- <div v-if="enrolledPrograms">
- <CardList :title="'Your Progress'" :description="''">
- <ProgressCard slot="card-list-slot" v-for="program in enrolledPrograms" :program="program" :key="program"/>
- </CardList>
- <CardList :title="''" :description="''">
- <ScoreCard slot="card-list-slot" v-for="program in enrolledPrograms" :program="program" :key="program"/>
- </CardList>
- </div>
- <div v-else>
- You haven't enrolled in any programs yet.
- </div>
-
-</div>
-</template>
-<script>
-import Button from '../components/Button.vue';
-import TopSection from "../components/TopSection.vue"
-import CardList from "../components/CardList.vue"
-import ProgressCard from "../components/ProgressCard.vue"
-import ProfileInfo from "../components/ProfileInfo.vue"
-import ScoreCard from "../components/ScoreCard.vue"
-
-export default {
- name: "ProfilePage",
- components: {
- AButton: Button,
- TopSection,
- CardList,
- ProfileInfo,
- ProgressCard,
- ScoreCard
- },
- data() {
- return {
- enrolledPrograms: {},
- }
- },
- mounted() {
- this.getEnrolledPrograms().then(data => this.enrolledPrograms = data);
- },
- methods: {
- getEnrolledPrograms() {
- return lms.call("get_program_enrollments")
- }
- }
-};
-</script>
\ No newline at end of file
diff --git a/erpnext/public/js/education/lms/pages/ProgramPage.vue b/erpnext/public/js/education/lms/pages/ProgramPage.vue
deleted file mode 100644
index 415c861..0000000
--- a/erpnext/public/js/education/lms/pages/ProgramPage.vue
+++ /dev/null
@@ -1,49 +0,0 @@
-<template>
-<div>
- <breadcrumb></breadcrumb>
- <TopSection v-bind:title="program.program_name" v-bind:description="program.description">
- </TopSection>
- <CardList :title="'Courses'" :description="''">
- <CourseCard slot="card-list-slot" v-for="course in courseData" :course="course" :program_name="program_name" :key="course.name"/>
- </CardList>
-</div>
-</template>
-<script>
-import TopSection from "../components/TopSection.vue"
-import CardList from "../components/CardList.vue"
-import CourseCard from "../components/CourseCard.vue"
-import Breadcrumb from "../components/Breadcrumb.vue"
-
-export default {
- props: ['program_name'],
- name: "ProgramPage",
- components: {
- TopSection,
- CardList,
- CourseCard,
- Breadcrumb
- },
- data() {
- return {
- program: {},
- courseData: [],
- }
- },
- mounted() {
- this.getProgramDetails().then(data => this.program = data);
- this.getCourses().then(data => this.courseData = data);
- },
- methods: {
- getProgramDetails() {
- return lms.call('get_program', {
- program_name: this.program_name
- });
- },
- getCourses() {
- return lms.call('get_courses', {
- program_name: this.program_name
- })
- }
- }
-};
-</script>
\ No newline at end of file
diff --git a/erpnext/public/js/education/lms/quiz.js b/erpnext/public/js/education/lms/quiz.js
new file mode 100644
index 0000000..1b520eb
--- /dev/null
+++ b/erpnext/public/js/education/lms/quiz.js
@@ -0,0 +1,186 @@
+class Quiz {
+ constructor(wrapper, options) {
+ this.wrapper = wrapper;
+ Object.assign(this, options);
+ this.questions = []
+ this.refresh();
+ }
+
+ refresh() {
+ this.get_quiz();
+ }
+
+ get_quiz() {
+ frappe.call('erpnext.education.utils.get_quiz', {
+ quiz_name: this.name,
+ course: this.course
+ }).then(res => {
+ this.make(res.message)
+ });
+ }
+
+ make(data) {
+ data.questions.forEach(question_data => {
+ let question_wrapper = document.createElement('div');
+ let question = new Question({
+ wrapper: question_wrapper,
+ ...question_data
+ });
+ this.questions.push(question)
+ this.wrapper.appendChild(question_wrapper);
+ })
+ if (data.activity.is_complete) {
+ this.disable()
+ let indicator = 'red'
+ let message = 'Your are not allowed to attempt the quiz again.'
+ if (data.activity.result == 'Pass') {
+ indicator = 'green'
+ message = 'You have already cleared the quiz.'
+ }
+
+ this.set_quiz_footer(message, indicator, data.activity.score)
+ }
+ else {
+ this.make_actions();
+ }
+ }
+
+ make_actions() {
+ const button = document.createElement("button");
+ button.classList.add("btn", "btn-primary", "mt-5", "mr-2");
+
+ button.id = 'submit-button';
+ button.innerText = 'Submit';
+ button.onclick = () => this.submit();
+ this.submit_btn = button
+ this.wrapper.appendChild(button);
+ }
+
+ submit() {
+ this.submit_btn.innerText = 'Evaluating..'
+ this.submit_btn.disabled = true
+ this.disable()
+ frappe.call('erpnext.education.utils.evaluate_quiz', {
+ quiz_name: this.name,
+ quiz_response: this.get_selected(),
+ course: this.course,
+ program: this.program
+ }).then(res => {
+ this.submit_btn.remove()
+ if (!res.message) {
+ frappe.throw("Something went wrong while evaluating the quiz.")
+ }
+
+ let indicator = 'red'
+ let message = 'Fail'
+ if (res.message.status == 'Pass') {
+ indicator = 'green'
+ message = 'Congratulations, you cleared the quiz.'
+ }
+
+ this.set_quiz_footer(message, indicator, res.message.score)
+ });
+ }
+
+ set_quiz_footer(message, indicator, score) {
+ const div = document.createElement("div");
+ div.classList.add("mt-5");
+ div.innerHTML = `<div class="row">
+ <div class="col-md-8">
+ <h4>${message}</h4>
+ <h5 class="text-muted"><span class="indicator ${indicator}">Score: ${score}/100</span></h5>
+ </div>
+ <div class="col-md-4">
+ <a href="${this.next_url}" class="btn btn-primary pull-right">${this.quiz_exit_button}</a>
+ </div>
+ </div>`
+
+ this.wrapper.appendChild(div)
+ }
+
+ disable() {
+ this.questions.forEach(que => que.disable())
+ }
+
+ get_selected() {
+ let que = {}
+ this.questions.forEach(question => {
+ que[question.name] = question.get_selected()
+ })
+ return que
+ }
+}
+
+class Question {
+ constructor(opts) {
+ Object.assign(this, opts);
+ this.make();
+ }
+
+ make() {
+ this.make_question()
+ this.make_options()
+ }
+
+ get_selected() {
+ let selected = this.options.filter(opt => opt.input.checked)
+ if (this.type == 'Single Correct Answer') {
+ if (selected[0]) return selected[0].name
+ }
+ if (this.type == 'Multiple Correct Answer') {
+ return selected.map(opt => opt.name)
+ }
+ return null
+ }
+
+ disable() {
+ let selected = this.options.forEach(opt => opt.input.disabled = true)
+ }
+
+ make_question() {
+ let question_wrapper = document.createElement('h5');
+ question_wrapper.classList.add('mt-3');
+ question_wrapper.innerText = this.question;
+ this.wrapper.appendChild(question_wrapper);
+ }
+
+ make_options() {
+ let make_input = (name, value) => {
+ let input = document.createElement('input');
+ input.id = name;
+ input.name = this.name;
+ input.value = value;
+ input.type = 'radio';
+ if (this.type == 'Multiple Correct Answer')
+ input.type = 'checkbox';
+ input.classList.add('form-check-input');
+ return input;
+ }
+
+ let make_label = function(name, value) {
+ let label = document.createElement('label');
+ label.classList.add('form-check-label');
+ label.htmlFor = name;
+ label.innerText = value;
+ return label
+ }
+
+ let make_option = function (wrapper, option) {
+ let option_div = document.createElement('div')
+ option_div.classList.add('form-check', 'pb-1')
+ let input = make_input(option.name, option.option);
+ let label = make_label(option.name, option.option);
+ option_div.appendChild(input)
+ option_div.appendChild(label)
+ wrapper.appendChild(option_div)
+ return {input: input, ...option}
+ }
+
+ let options_wrapper = document.createElement('div')
+ options_wrapper.classList.add('ml-2')
+ let option_list = []
+ this.options.forEach(opt => option_list.push(make_option(options_wrapper, opt)))
+ this.options = option_list
+ this.wrapper.appendChild(options_wrapper)
+ }
+}
\ No newline at end of file
diff --git a/erpnext/public/js/education/lms/routes.js b/erpnext/public/js/education/lms/routes.js
deleted file mode 100644
index 483f222..0000000
--- a/erpnext/public/js/education/lms/routes.js
+++ /dev/null
@@ -1,92 +0,0 @@
-import Home from "./pages/Home.vue";
-import ProgramPage from "./pages/ProgramPage.vue";
-import CoursePage from "./pages/CoursePage.vue";
-import ContentPage from "./pages/ContentPage.vue";
-import ListPage from "./pages/ListPage.vue";
-import ProfilePage from "./pages/ProfilePage.vue";
-
-const routes = [{
- name: 'home',
- path: '',
- component: Home
-},
-{
- name: 'program',
- path: '/Program/:program_name',
- component: ProgramPage,
- props: true
-},
-{
- name: 'course',
- path: '/Program/:program_name/:course_name/',
- component: CoursePage,
- props: true,
-},
-{
- name: 'content',
- path: '/Program/:program_name/:course_name/:topic/:type/:content',
- component: ContentPage,
- props: true,
- beforeRouteUpdate (to, from, next) {
- if (lms.store.checkProgramEnrollment(to.params.program_name)) {
- next();
- } else {
- next({
- name: 'program',
- params: {
- program_name: to.params.program_name
- }
- });
- }
- }
-},
-{
- name: 'list',
- path: '/List/:master',
- component: ListPage,
- props: true
-},
-{
- name: 'signup',
- path: '/Signup',
- beforeEnter(to, from, next) {
- window.location = window.location.origin.toString() + '/login#signup';
- },
- component: Home,
- props: true
-},
-{
- name: 'login',
- path: '/Login',
- beforeEnter(to, from, next) {
- window.location = window.location.origin.toString() + '/login#login';
- },
- component: Home,
- props: true
-},
-{
- name: 'logout',
- path: '/Logout',
- beforeEnter(to, from, next) {
- window.location = window.location.origin.toString() + '/?cmd=web_logout';
- },
- component: Home,
- props: true
-},
-{
- name: 'profile',
- path: '/Profile',
- component: ProfilePage,
- props: true,
- beforeEnter: (to, from, next) => {
- if (!lms.store.checkLogin()) {
- next({
- name: 'home'
- });
- } else {
- next();
- }
- }
-}];
-
-export default routes;
\ No newline at end of file
diff --git a/erpnext/www/lms.html b/erpnext/www/lms.html
deleted file mode 100644
index aa76ca0..0000000
--- a/erpnext/www/lms.html
+++ /dev/null
@@ -1,39 +0,0 @@
-{% extends "templates/web.html" %}
-
-{% block title %}{{ heading or "LMS"}}{% endblock %}
-
-{% block navbar %}{% endblock %}
-
-{% block content %}
-{% if lms_enabled %}
-<div id="lms-app"></div>
-<script type="text/javascript" src="/assets/js/lms.min.js"></script>
-{% else %}
-<style>
-.hero-and-content {
- background-color: #f5f7fa;
-}
-header, footer {
- display: none;
-}
-html, body {
- background-color: #f5f7fa;
-}
-{% include "templates/styles/card_style.css" %}
-</style>
-
-<div class='page-card'>
- <div class='page-card-head'>
- <span class='indicator darkgrey'>{{_("Page Missing or Moved")}}</span>
- </div>
- <p>{{_("The page you are looking for is missing. This could be because it is moved or there is a typo in the link.")}}</p>
- <div><a href='/' class='btn btn-primary btn-sm'>{{ _("Home") }}</a></div>
-</div>
-<p class='text-muted text-center small' style='margin-top: -20px;'>{{ _("Error Code: {0}").format('404') }}</p>
-<style>
-.hero-and-content {
- background-color: #f5f7fa;
-}
-</style>
-{% endif %}
-{% endblock %}
\ No newline at end of file
diff --git a/erpnext/www/lms.py b/erpnext/www/lms.py
deleted file mode 100644
index 7561d73..0000000
--- a/erpnext/www/lms.py
+++ /dev/null
@@ -1,242 +0,0 @@
-from __future__ import unicode_literals
-import erpnext.education.utils as utils
-import frappe
-from frappe import _
-
-# LMS Utils to Update State for Vue Store
-@frappe.whitelist()
-def get_program_enrollments():
- student = utils.get_current_student()
- if student == None:
- return None
- return student.get_program_enrollments()
-
-@frappe.whitelist()
-def get_all_course_enrollments():
- student = utils.get_current_student()
- if student == None:
- return None
- return student.get_all_course_enrollments()
-
-# Vue Client Functions
-@frappe.whitelist(allow_guest=True)
-def get_portal_details():
- """
- Returns portal details from Education Settings Doctype. This contains the Title and Description for LMS amoung other things.
- """
- from erpnext import get_default_company
-
- settings = frappe.get_doc("Education Settings")
- title = settings.portal_title or get_default_company()
- description = settings.description
- return dict(title=title, description=description)
-
-@frappe.whitelist(allow_guest=True)
-def get_featured_programs():
- featured_program_names = frappe.get_all("Program", filters={"is_published": True, "is_featured": True})
- if featured_program_names:
- featured_list = [utils.get_program_and_enrollment_status(program['name']) for program in featured_program_names]
- return featured_list
- else:
- return get_all_programs()[:2]
-
-@frappe.whitelist(allow_guest=True)
-def get_all_programs():
- program_names = frappe.get_all("Program", filters={"is_published": True})
- if program_names:
- program_list = [utils.get_program_and_enrollment_status(program['name']) for program in program_names]
- return program_list
-
-@frappe.whitelist(allow_guest=True)
-def get_program(program_name):
- try:
- return frappe.get_doc('Program', program_name)
- except frappe.DoesNotExistError:
- frappe.throw(_("Program {0} does not exist.".format(program_name)))
-
-# Functions to get program & course details
-@frappe.whitelist(allow_guest=True)
-def get_courses(program_name):
- program = frappe.get_doc('Program', program_name)
- courses = program.get_course_list()
- return courses
-
-@frappe.whitelist()
-def get_next_content(current_content, current_content_type, topic):
- if frappe.session.user == "Guest":
- return None
- topic = frappe.get_doc("Topic", topic)
- content_list = [{'content_type':item.doctype, 'content':item.name} for item in topic.get_contents()]
- current_index = content_list.index({'content': current_content, 'content_type': current_content_type})
- try:
- return content_list[current_index + 1]
- except IndexError:
- return None
-
-def get_quiz_with_answers(quiz_name):
- try:
- quiz = frappe.get_doc("Quiz", quiz_name).get_questions()
- quiz_output = [{'name':question.name, 'question':question.question, 'options':[{'name': option.name, 'option':option.option, 'is_correct':option.is_correct} for option in question.options]} for question in quiz]
- return quiz_output
- except:
- frappe.throw("Quiz {0} does not exist".format(quiz_name))
- return None
-
-@frappe.whitelist()
-def get_quiz_without_answers(quiz_name, course_name):
- try:
- quiz = frappe.get_doc("Quiz", quiz_name)
- questions = quiz.get_questions()
- except:
- frappe.throw("Quiz {0} does not exist".format(quiz_name))
- return None
-
- if utils.check_super_access():
- quiz_output = [{'name':question.name, 'question':question.question, 'type': question.type, 'options':[{'name': option.name, 'option':option.option} for option in question.options]} for question in questions]
- return { 'quizData': quiz_output, 'status': None}
-
- enrollment = utils.get_course_enrollment(course_name).name
- quiz_progress = {}
- quiz_progress['is_complete'], quiz_progress['score'], quiz_progress['result'] = utils.check_quiz_completion(quiz, enrollment)
- quiz_output = [{'name':question.name, 'question':question.question, 'type': question.type, 'options':[{'name': option.name, 'option':option.option} for option in question.options]} for question in questions]
- return { 'quizData': quiz_output, 'status': quiz_progress}
-
-@frappe.whitelist()
-def evaluate_quiz(course, quiz_response, quiz_name):
- """LMS Function: Evaluates a simple multiple choice quiz.
- :param course: name of the course
- :param quiz_response: contains user selected choices for a quiz in the form of a string formatted as a dictionary. The function uses `json.loads()` to convert it to a python dictionary.
- :param quiz_name: Name of the quiz attempted
- """
- import json
- quiz_response = json.loads(quiz_response)
- quiz = frappe.get_doc("Quiz", quiz_name)
- answers, score, status = quiz.evaluate(quiz_response, quiz_name)
- print(answers)
-
- course_enrollment = utils.get_course_enrollment(course)
- if course_enrollment:
- course_enrollment.add_quiz_activity(quiz_name, quiz_response, answers, score, status)
-
- return score
-
-@frappe.whitelist()
-def enroll_in_program(program_name):
- student = utils.get_current_student()
- if not student:
- student = utils.create_student_from_current_user()
- program_enrollment = student.enroll_in_program(program_name)
- return program_name
-
-# Academdy Activity
-@frappe.whitelist()
-def add_activity(course, content_type, content):
- if not utils.get_current_student():
- return
- enrollment = utils.get_course_enrollment(course)
- enrollment.add_activity(content_type, content)
-
-@frappe.whitelist()
-def get_student_course_details(course_name, program_name):
- """
- Return the porgress of a course in a program as well as the content to continue from.
- :param course_name:
- :param program_name:
- """
- student = utils.get_current_student()
- if not student:
- return {'flag':'Start Course' }
-
- course_enrollment = utils.get_course_enrollment(course_name)
- program_enrollment = utils.get_program_enrollment(program_name)
-
- if not program_enrollment:
- return None
-
- if not course_enrollment:
- course_enrollment = utils.enroll_in_course(course_name, program_name)
-
- progress = course_enrollment.get_progress(student)
- count = sum([activity['is_complete'] for activity in progress])
- if count == 0:
- return {'flag':'Start Course'}
- elif count == len(progress):
- return {'flag':'Completed'}
- elif count < len(progress):
- next_item = next(item for item in progress if item['is_complete']==False)
- return {'flag':'Continue'}
-
-@frappe.whitelist()
-def get_student_topic_details(topic_name, course_name):
- """
- Return the porgress of a course in a program as well as the content to continue from.
- :param topic_name:
- :param course_name:
- """
- topic = frappe.get_doc("Topic", topic_name)
- student = utils.get_current_student()
- if not student:
- topic_content = topic.get_all_children()
- if topic_content:
- return {'flag':'Start Course', 'content_type': topic_content[0].content_type, 'content': topic_content[0].content}
- else:
- return None
- course_enrollment = utils.get_course_enrollment(course_name)
- progress = student.get_topic_progress(course_enrollment.name, topic)
- if not progress:
- return { 'flag':'Start Topic', 'content_type': None, 'content': None }
- count = sum([activity['is_complete'] for activity in progress])
- if count == 0:
- return {'flag':'Start Topic', 'content_type': progress[0]['content_type'], 'content': progress[0]['content']}
- elif count == len(progress):
- return {'flag':'Completed', 'content_type': progress[0]['content_type'], 'content': progress[0]['content']}
- elif count < len(progress):
- next_item = next(item for item in progress if item['is_complete']==False)
- return {'flag':'Continue', 'content_type': next_item['content_type'], 'content': next_item['content']}
-
-@frappe.whitelist()
-def get_program_progress(program_name):
- program_enrollment = frappe.get_doc("Program Enrollment", utils.get_program_enrollment(program_name))
- if not program_enrollment:
- return None
- else:
- return program_enrollment.get_program_progress()
-
-@frappe.whitelist()
-def get_joining_date():
- student = utils.get_current_student()
- if student:
- return student.joining_date
-
-@frappe.whitelist()
-def get_quiz_progress_of_program(program_name):
- program_enrollment = frappe.get_doc("Program Enrollment", utils.get_program_enrollment(program_name))
- if not program_enrollment:
- return None
- else:
- return program_enrollment.get_quiz_progress()
-
-
-@frappe.whitelist(allow_guest=True)
-def get_course_details(course_name):
- try:
- course = frappe.get_doc('Course', course_name)
- return course
- except:
- return None
-
-# Functions to get program & course details
-@frappe.whitelist(allow_guest=True)
-def get_topics(course_name):
- try:
- course = frappe.get_doc('Course', course_name)
- return course.get_topics()
- except frappe.DoesNotExistError:
- frappe.throw(_("Course {0} does not exist.".format(course_name)))
-
-@frappe.whitelist()
-def get_content(content_type, content):
- try:
- return frappe.get_doc(content_type, content)
- except frappe.DoesNotExistError:
- frappe.throw(_("{0} {1} does not exist.".format(content_type, content)))
\ No newline at end of file
diff --git a/erpnext/www/lms/__init__.py b/erpnext/www/lms/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/www/lms/__init__.py
diff --git a/erpnext/www/lms/content.html b/erpnext/www/lms/content.html
new file mode 100644
index 0000000..9b8c45c
--- /dev/null
+++ b/erpnext/www/lms/content.html
@@ -0,0 +1,208 @@
+{% extends "templates/base.html" %}
+{% block title %}{{ content.name or 'Content Page' }}{% endblock %}
+
+{% block head_include %}
+ <style>
+ .lms-content {
+ line-height: 1.8em;
+ }
+
+ .lms-content h1 {
+ margin-top: 1em;
+ }
+
+ .lms-content h2 {
+ margin-top: 1em;
+ }
+
+ .lms-content h3 {
+ margin-top: 0.8em;
+ }
+
+ .lms-content h4 {
+ margin-top: 0.6em;
+ }
+
+ section {
+ padding: 5rem 0 5rem 0;
+ }
+ .plyr--video .plyr__control.plyr__tab-focus,
+ .plyr--video .plyr__control:hover,
+ .plyr--video .plyr__control[aria-expanded='true'] {
+ background: #5e64ff !important;
+ }
+
+ .plyr__control--overlaid:focus,
+ .plyr__control--overlaid:hover {
+ background: #5e64ff !important;
+ }
+
+ .plyr__menu__container .plyr__control[role=menuitemradio][aria-checked=true]::before {
+ background: #5e64ff !important;
+ }
+
+ .plyr__menu__container
+ .plyr__control[role='menuitemradio'][aria-checked='true']::before {
+ background: #5e64ff;
+ }
+ .plyr--full-ui input[type='range'] {
+ color: #5e64ff !important;
+ }
+
+ .plyr__control--overlaid {
+ background: rgba(94, 100, 255, 0.8) !important;
+ }
+ </style>
+ <link rel="stylesheet" href="https://cdn.plyr.io/3.5.3/plyr.css" />
+{% endblock %}
+
+{% macro title() %}
+ <div class="mb-3">
+ <a href="#" class="text-muted">
+ Back to Course
+ </a>
+ </div>
+ <div>
+ <h1>{{ content.name }} <span class="small text-muted">({{ position + 1 }}/{{length}})</span></h1>
+ </div>
+{% endmacro %}
+
+{% macro navigation() %}
+ {% if previous %}
+ <a href="/lms/content?program={{ program }}&course={{ course }}&topic={{ topic }}&type={{ previous.content_type }}&content={{ previous.content }}" class='btn text-muted' style="box-shadow: none;">Previous</a>
+ {% else %}
+ <a href="/lms/course?name={{ course }}&program={{ program }}" class='btn text-muted' style="box-shadow: none;">Back to Course</a>
+ {% endif %}
+
+ {% if next %}
+ <button id="nextButton" onclick="handle('/lms/content?program={{ program }}&course={{ course }}&topic={{ topic }}&type={{ next.content_type }}&content={{ next.content }}')" class='btn btn-primary' disabled="true">Next</button>
+ {% else %}
+ <button id="nextButton" onclick="handle('/lms/course?name={{ course }}&program={{ program }}')" class='btn btn-primary' disabled="true">Finish Topic</button>
+ {% endif %}
+{% endmacro %}
+
+{% macro video() %}
+<div class="mb-5">
+ {{ title() }}
+ <div class="text-muted">
+ {% if content.duration %}
+ {{ content.duration }} Mins
+ {% endif %}
+
+ {% if content.publish_date and content.duration%}
+ -
+ {% endif %}
+
+ {% if content.publish_date %}
+ Published on {{ content.publish_date.strftime('%d, %b %Y') }}
+ {% endif %}
+ </div>
+</div>
+<div id="player" data-plyr-provider="{{ content.provider|lower }}" data-plyr-embed-id="{{ content.url }}"></div>
+<div class="my-5 lms-content">
+ {{ content.description }}
+</div>
+{% endmacro %}
+
+{% macro article() %}
+<div class="mb-5">
+ {{ title() }}
+ <div class="text-muted">
+ {% if content.author or content.publish_date %}
+ Published
+ {% endif %}
+ {% if content.author %}
+ by {{ content.author }}
+ {% endif %}
+ {% if content.publish_date %}
+ on {{ content.publish_date.strftime('%d, %b %Y') }}
+ {% endif %}
+ </div>
+</div>
+<div class="lms-content">
+ {{ content.content }}
+</div>
+{% endmacro %}
+
+{% macro quiz() %}
+<div class="mb-5">
+ {{ title() }}
+</div>
+<div id="quiz-wrapper">
+</div>
+{% endmacro %}
+
+{% block content %}
+<section class="section">
+ <div>
+ <div class='container pb-5'>
+ {% if content_type=='Video' %}
+ {{ video() }}
+ {% elif content_type=='Article'%}
+ {{ article() }}
+ {% elif content_type=='Quiz' %}
+ {{ quiz() }}
+ {% endif %}
+ <div class="pull-right" {{ 'hidden' if content_type=='Quiz'}}>
+ {{ navigation() }}
+ </div>
+ </div>
+ </div>
+</section>
+{% endblock %}
+
+{% block script %}
+ {% if content_type=='Video' %}
+ <script src="https://cdn.plyr.io/3.5.3/plyr.js"></script>
+ {% elif content_type == 'Quiz' %}
+ <script src='/assets/erpnext/js/education/lms/quiz.js'></script>
+ {% endif %}
+ <script>
+ {% if content_type == 'Video' %}
+ const player = new Plyr('#player');
+ {% elif content_type == 'Quiz' %}
+ {% if next %}
+ const quiz_exit_button = 'Next'
+ const next_url = '/lms/content?program={{ program }}&course={{ course }}&topic={{ topic }}&type={{ next.content_type }}&content={{ next.content }}'
+ {% else %}
+ const quiz_exit_button = 'Finish Course'
+ const next_url = '/lms/course?name={{ course }}&program={{ program }}'
+ {% endif %}
+ frappe.ready(() => {
+ const quiz = new Quiz(document.getElementById('quiz-wrapper'), {
+ name: '{{ content.name }}',
+ course: '{{ course }}',
+ program: '{{ program }}',
+ quiz_exit_button: quiz_exit_button,
+ next_url: next_url
+ })
+ window.quiz = quiz;
+ })
+ {% endif %}
+
+ {% if content_type != 'Quiz' %}
+
+ frappe.ready(() => {
+ next = document.getElementById('nextButton')
+ next.disabled = false;
+ })
+
+
+ function handle(url) {
+ opts = {
+ method: "erpnext.education.utils.add_activity",
+ args: {
+ course: "{{ course }}",
+ content_type: "{{ content_type }}",
+ content: "{{ content.name }}",
+ program: "{{ program }}"
+ }
+ }
+ frappe.call(opts).then(res => {
+ window.location.href = url;
+ })
+ }
+
+ {% endif %}
+ </script>
+{% endblock %}
\ No newline at end of file
diff --git a/erpnext/www/lms/content.py b/erpnext/www/lms/content.py
new file mode 100644
index 0000000..3ab7a9f
--- /dev/null
+++ b/erpnext/www/lms/content.py
@@ -0,0 +1,68 @@
+from __future__ import unicode_literals
+import erpnext.education.utils as utils
+import frappe
+
+no_cache = 1
+
+def get_context(context):
+ # Load Query Parameters
+ try:
+ program = frappe.form_dict['program']
+ content = frappe.form_dict['content']
+ content_type = frappe.form_dict['type']
+ course = frappe.form_dict['course']
+ topic = frappe.form_dict['topic']
+ except KeyError:
+ frappe.local.flags.redirect_location = '/lms'
+ raise frappe.Redirect
+
+
+ # Check if user has access to the content
+ has_program_access = utils.allowed_program_access(program)
+ has_content_access = allowed_content_access(program, content, content_type)
+
+ if frappe.session.user == "Guest" or not has_program_access or not has_content_access:
+ frappe.local.flags.redirect_location = '/lms'
+ raise frappe.Redirect
+
+
+ # Set context for content to be displayer
+ context.content = frappe.get_doc(content_type, content).as_dict()
+ context.content_type = content_type
+ context.program = program
+ context.course = course
+ context.topic = topic
+
+ topic = frappe.get_doc("Topic", topic)
+ content_list = [{'content_type':item.doctype, 'content':item.name} for item in topic.get_contents()]
+
+ # Set context for progress numbers
+ context.position = content_list.index({'content': content, 'content_type': content_type})
+ context.length = len(content_list)
+
+ # Set context for navigation
+ context.previous = get_previous_content(content_list, context.position)
+ context.next = get_next_content(content_list, context.position)
+
+def get_next_content(content_list, current_index):
+ try:
+ return content_list[current_index + 1]
+ except IndexError:
+ return None
+
+def get_previous_content(content_list, current_index):
+ if current_index == 0:
+ return None
+ else:
+ return content_list[current_index - 1]
+
+def allowed_content_access(program, content, content_type):
+ contents_of_program = frappe.db.sql("""select `tabtopic content`.content, `tabtopic content`.content_type
+ from `tabcourse topic`,
+ `tabprogram course`,
+ `tabtopic content`
+ where `tabcourse topic`.parent = `tabprogram course`.course
+ and `tabtopic content`.parent = `tabcourse topic`.topic
+ and `tabprogram course`.parent = %(program)s""", {'program': program})
+
+ return (content, content_type) in contents_of_program
\ No newline at end of file
diff --git a/erpnext/www/lms/course.html b/erpnext/www/lms/course.html
new file mode 100644
index 0000000..0bfd059
--- /dev/null
+++ b/erpnext/www/lms/course.html
@@ -0,0 +1,106 @@
+{% extends "templates/base.html" %}
+{% block title %}{{ course.course_name }}{% endblock %}
+{% from "www/lms/macros/hero.html" import hero %}
+{% from "www/lms/macros/card.html" import null_card %}
+
+{% block head_include %}
+ <style>
+ div.card-hero-img {
+ height: 220px;
+ background-size: cover;
+ background-repeat: no-repeat;
+ background-position: center;
+ background-color: rgb(250, 251, 252);
+ }
+
+ .card-image-wrapper {
+ display: flex;
+ overflow: hidden;
+ height: 220px;
+ background-color: rgb(250, 251, 252);
+ justify-content: center;
+ }
+
+ .image-body {
+ align-self: center;
+ color: #d1d8dd;
+ font-size: 24px;
+ font-weight: 600;
+ line-height: 1;
+ padding: 20px;
+ }
+ section {
+ padding: 5rem 0 5rem 0;
+ }
+ </style>
+{% endblock %}
+
+
+{% macro card(topic) %}
+<div class="col-sm-4 mb-4 text-left">
+ <div class="card h-100">
+ {% if has_access %}
+ <a href="/lms/topic?program={{ program }}&course={{ course.name }}&topic={{ topic.name }}" class="no-decoration no-underline">
+ {% else %}
+ <div>
+ {% endif %}
+ {% if topic.hero_image %}
+ <div class="card-hero-img" style="background-image: url({{ topic.hero_image }})"></div>
+ {% else %}
+ <div class="card-image-wrapper text-center">
+ <div class="image-body"><i class="fa fa-picture-o" aria-hidden="true"></i></div>
+ </div>
+ {% endif %}
+ <div class='card-body'>
+ <h5 class='card-title'>{{ topic.topic_name }}</h5>
+ <div>
+ <ol class="list-unstyled">
+ {% for content in topic.topic_content %}
+ <li>
+ {% if has_access %}
+ <a class="text-muted" href="/lms/content?program={{ program }}&course={{ course.name }}&topic={{ topic.name }}&type={{ content.content_type }}&content={{ content.content }}">
+ {{ content.content }}
+ </a>
+ {% else %}
+ <span class="text-muted">{{ content.content }}</span>
+ {% endif %}
+ </li>
+ {% endfor %}
+ </ol>
+ </div>
+ </div>
+ {% if has_access %}
+ <div class='card-footer'>
+ {% if progress[topic.name].completed %}
+ <span class="indicator green">Completed</span>
+ {% elif progress[topic.name].started %}
+ <span class="indicator orange">In Progress</span>
+ {% else %}
+ <span class="indicator blue">Start</span>
+ {% endif %}
+ </div>
+ </a>
+ {% else %}
+ </div>
+ {% endif %}
+ </div>
+</div>
+{% endmacro %}
+
+{% block content %}
+<section class="section">
+ {{ hero(course.course_name, course.description, has_access, {'name': 'Program', 'url': '/lms/program?program=' + program }) }}
+ <div class='container'>
+ <div class="row mt-5">
+ {% for topic in topics %}
+ {{ card(topic) }}
+ {% endfor %}
+ {% if topics %}
+ {% for n in range(3 - ((topics|length)%3)) %}
+ {{ null_card() }}
+ {% endfor %}
+ {% endif %}
+ </div>
+ </div>
+</section>
+{% endblock %}
\ No newline at end of file
diff --git a/erpnext/www/lms/course.py b/erpnext/www/lms/course.py
new file mode 100644
index 0000000..e7ed2e3
--- /dev/null
+++ b/erpnext/www/lms/course.py
@@ -0,0 +1,19 @@
+from __future__ import unicode_literals
+import erpnext.education.utils as utils
+import frappe
+
+no_cache = 1
+
+def get_context(context):
+ context.education_settings = frappe.get_single("Education Settings")
+ course = frappe.get_doc('Course', frappe.form_dict['name'])
+ context.program = frappe.form_dict['program']
+ context.course = course
+
+ context.topics = course.get_topics()
+ context.has_access = utils.allowed_program_access(context.program)
+ context.progress = get_topic_progress(context.topics, course, context.program)
+
+def get_topic_progress(topics, course, program):
+ progress = {topic.name: utils.get_topic_progress(topic, course.name, program) for topic in topics}
+ return progress
diff --git a/erpnext/www/lms/index.html b/erpnext/www/lms/index.html
new file mode 100644
index 0000000..7ea39d8
--- /dev/null
+++ b/erpnext/www/lms/index.html
@@ -0,0 +1,65 @@
+{% extends "templates/base.html" %}
+{% block title %}{{ education_settings.portal_title }}{% endblock %}
+{% from "www/lms/macros/card.html" import program_card %}
+{% from "www/lms/macros/card.html" import null_card %}
+
+{% block head_include %}
+ <meta name="description" content="{{ education_settings.description }}" />
+ <meta name="keywords" content="ERP Software, Cloud ERP, Open Source ERP, Accounting Software, Online ERP, Online Accounting, ERP for small business" />
+ <style>
+ div.card-hero-img {
+ height: 220px;
+ background-size: cover;
+ background-repeat: no-repeat;
+ background-position: center;
+ background-color: rgb(250, 251, 252);
+ }
+
+ .card-image-wrapper {
+ display: flex;
+ overflow: hidden;
+ height: 220px;
+ background-color: rgb(250, 251, 252);
+ justify-content: center;
+ }
+
+ .image-body {
+ align-self: center;
+ color: #d1d8dd;
+ font-size: 24px;
+ font-weight: 600;
+ line-height: 1;
+ padding: 20px;
+ }
+
+ section {
+ padding: 5rem 0 5rem 0;
+ }
+ </style>
+{% endblock %}
+
+{% block content %}
+<section class="top-section" style="padding: 6rem 0rem;">
+ <div class='container pb-5'>
+ <h1>{{ education_settings.portal_title }}</h1>
+ <p class='lead'>{{ education_settings.description }}</p>
+ <p class="mt-4">
+ {% if frappe.session.user == 'Guest' %}
+ <a class="btn btn-primary btn-lg" href="'/login#signup'}}">Start Learning</a>
+ {% endif %}
+ </p>
+ </div>
+ <div class='container'>
+ <div class="row mt-5">
+ {% for program in featured_programs %}
+ {{ program_card(program.program, program.has_access) }}
+ {% endfor %}
+ {% if featured_programs %}
+ {% for n in range(3 - ((featured_programs|length)%3)) %}
+ {{ null_card() }}
+ {% endfor %}
+ {% endif %}
+ </div>
+ </div>
+</section>
+{% endblock %}
\ No newline at end of file
diff --git a/erpnext/www/lms/index.py b/erpnext/www/lms/index.py
new file mode 100644
index 0000000..00f66e7
--- /dev/null
+++ b/erpnext/www/lms/index.py
@@ -0,0 +1,16 @@
+from __future__ import unicode_literals
+import erpnext.education.utils as utils
+import frappe
+
+no_cache = 1
+
+def get_context(context):
+ context.education_settings = frappe.get_single("Education Settings")
+ if not context.education_settings.enable_lms:
+ frappe.local.flags.redirect_location = '/'
+ raise frappe.Redirect
+ context.featured_programs = get_featured_programs()
+
+
+def get_featured_programs():
+ return utils.get_portal_programs()
\ No newline at end of file
diff --git a/erpnext/www/lms/macros/card.html b/erpnext/www/lms/macros/card.html
new file mode 100644
index 0000000..f227355
--- /dev/null
+++ b/erpnext/www/lms/macros/card.html
@@ -0,0 +1,34 @@
+{% macro program_card(program, has_access) %}
+<div class="col-sm-4 mb-4 text-left">
+ <a href="/lms/program?program={{ program.name }}" class="no-decoration no-underline">
+ <div class="card h-100">
+ {% if program.hero_image %}
+ <div class="card-hero-img" style="background-image: url({{ program.hero_image }})"></div>
+ {% else %}
+ <div class="card-image-wrapper text-center">
+ <div class="image-body"><i class="fa fa-picture-o" aria-hidden="true"></i></div>
+ </div>
+ {% endif %}
+ <div class='card-body'>
+ <h5 class='card-title'>{{ program.program_name }}</h5>
+ <div class="text-muted">{{ program.description[:110] + '...' if program.description else '' }}</div>
+ </div>
+ {% if has_access or program.intro_video%}
+ <div class='card-footer'>
+ {% if has_access %} <span class="indicator green">Enrolled</span>
+ {% elif program.intro_video %} <span><a href="{{ program.intro_video }}" target="blank">Watch Intro</a></span>
+ {% endif %}
+ </div>
+ {% endif %}
+ </div>
+ </a>
+</div>
+{% endmacro %}
+
+
+{% macro null_card() %}
+<div class="col-sm-4 mb-4 text-left">
+ <div class="h-100" style="border: 1px solid rgba(209,216,221,0.5);border-radius: 0.25rem;background-color: rgb(250, 251, 252);">
+ </div>
+</div>
+{% endmacro %}
\ No newline at end of file
diff --git a/erpnext/www/lms/macros/hero.html b/erpnext/www/lms/macros/hero.html
new file mode 100644
index 0000000..66bb861
--- /dev/null
+++ b/erpnext/www/lms/macros/hero.html
@@ -0,0 +1,55 @@
+{% macro hero(title, description, has_access, back) %}
+ <div class='container pb-5'>
+ <div class="mb-3">
+ <a href="{{ back.url }}" class="text-muted">
+ Back to {{ back.name }}
+ </a>
+ </div>
+ <h1>{{ title }}</h1>
+ <p class='lead' style="max-width: 100%;">{{ description or ''}}</p>
+ <p class="mt-4">
+ {% if frappe.session.user == 'Guest' %}
+ <a id="signup" class="btn btn-primary btn-lg" href="/login#signup">Sign Up</a>
+ {% elif not has_access %}
+ <button id="enroll" class="btn btn-primary btn-lg" onclick="enroll()" disabled>Enroll</button>
+ {% endif %}
+ </p>
+ </div>
+
+{% block script %}
+<script type="text/javascript">
+ frappe.ready(() => {
+ btn = document.getElementById('enroll');
+ if (btn) btn.disabled = false;
+ })
+
+ function enroll() {
+ let params = frappe.utils.get_query_params()
+
+ let btn = document.getElementById('enroll');
+ btn.disbaled = true;
+ btn.innerText = 'Enrolling...'
+
+ let opts = {
+ method: 'erpnext.education.utils.enroll_in_program',
+ args: {
+ program_name: params.program
+ }
+ }
+
+ frappe.call(opts).then(res => {
+ let success_dialog = new frappe.ui.Dialog({
+ title: __('Success'),
+ secondary_action: function() {
+ window.location.reload()
+ }
+ })
+ success_dialog.set_message('You have successfully enrolled for the program ');
+ success_dialog.$message.show()
+ success_dialog.show();
+ btn.disbaled = false;
+ })
+ }
+</script>
+{% endblock %}
+{% endmacro %}
\ No newline at end of file
diff --git a/erpnext/www/lms/profile.html b/erpnext/www/lms/profile.html
new file mode 100644
index 0000000..9508dae
--- /dev/null
+++ b/erpnext/www/lms/profile.html
@@ -0,0 +1,64 @@
+{% extends "templates/base.html" %}
+{% block title %}Profile{% endblock %}
+{% from "www/lms/macros/hero.html" import hero %}
+
+{% block head_include %}
+ <style>
+ section {
+ padding: 5rem 0 5rem 0;
+ }
+ </style>
+{% endblock %}
+
+{% macro card(program) %}
+<div class="col-sm-4 mb-4 text-left">
+ <a href="/lms/program?program={{ program.name }}" class="no-decoration no-underline">
+ <div class="card h-100">
+ <div class='card-body'>
+ <h5 class='card-title'>{{ program.program }}</h5>
+ <ul class="list-unstyled text-muted">
+ {% for course in program.progress %}
+ <li>
+ {% if course.completed %} <span class="indicator green">
+ {% elif course.started %} <span class="indicator orange">
+ {% else %} <span class="indicator blue">
+ {% endif %}
+ <a class="text-muted" href="/lms/course?name={{ course.name }}&program={{ program.name }}">{{ course.course }}</a>
+ </span>
+ </li>
+ {% endfor %}
+ </ul>
+ </div>
+ <div class='card-footer'>
+ <span class="small">{{ program.completion }}% Complete</span>
+ </div>
+ </div>
+ </a>
+</div>
+{% endmacro %}
+
+{% block content %}
+<section class="section">
+ <div class='container pb-5'>
+ <div class="mb-3 row">
+ <div class="col-md-7">
+ <a href="/lms" class="text-muted">
+ Back to Home
+ </a>
+ </div>
+ <div class="col-md-5 text-right">
+ <a href="/update-profile?name={{ frappe.session.user }}" target="blank" class="mt-0 text-muted">Edit Profile</a>
+ </div>
+ </div>
+ <h1>{{ student.first_name }} {{ student.last_name or '' }}</h1>
+ <p class="lead" style="max-width: 100%;">{{ student.name }}</p>
+ </div>
+ <div class='container'>
+ <div class="row mt-5">
+ {% for program in progress %}
+ {{ card(program) }}
+ {% endfor %}
+ </div>
+ </div>
+</section>
+{% endblock %}
\ No newline at end of file
diff --git a/erpnext/www/lms/profile.py b/erpnext/www/lms/profile.py
new file mode 100644
index 0000000..4788ea6
--- /dev/null
+++ b/erpnext/www/lms/profile.py
@@ -0,0 +1,26 @@
+from __future__ import unicode_literals
+import erpnext.education.utils as utils
+import frappe
+
+no_cache = 1
+
+def get_context(context):
+ if frappe.session.user == "Guest":
+ frappe.local.flags.redirect_location = '/lms'
+ raise frappe.Redirect
+
+ context.student = utils.get_current_student()
+ if not context.student:
+ context.student = frappe.get_doc('User', frappe.session.user)
+ context.progress = get_program_progress(context.student.name)
+
+def get_program_progress(student):
+ enrolled_programs = frappe.get_all("Program Enrollment", filters={'student':student}, fields=['program'])
+ student_progress = []
+ for list_item in enrolled_programs:
+ program = frappe.get_doc("Program", list_item.program)
+ progress = utils.get_program_progress(program)
+ completion = utils.get_program_completion(program)
+ student_progress.append({'program': program.program_name, 'name': program.name, 'progress':progress, 'completion': completion})
+
+ return student_progress
\ No newline at end of file
diff --git a/erpnext/www/lms/program.html b/erpnext/www/lms/program.html
new file mode 100644
index 0000000..d122778
--- /dev/null
+++ b/erpnext/www/lms/program.html
@@ -0,0 +1,87 @@
+{% extends "templates/base.html" %}
+{% block title %}{{ program.program_name }}{% endblock %}
+{% from "www/lms/macros/hero.html" import hero %}
+{% from "www/lms/macros/card.html" import null_card %}
+
+{% block head_include %}
+ <style>
+ div.card-hero-img {
+ height: 220px;
+ background-size: cover;
+ background-repeat: no-repeat;
+ background-position: center;
+ background-color: rgb(250, 251, 252);
+ }
+
+ .card-image-wrapper {
+ display: flex;
+ overflow: hidden;
+ height: 220px;
+ background-color: rgb(250, 251, 252);
+ justify-content: center;
+ }
+
+ .image-body {
+ align-self: center;
+ color: #d1d8dd;
+ font-size: 24px;
+ font-weight: 600;
+ line-height: 1;
+ padding: 20px;
+ }
+
+ section {
+ padding: 5rem 0 5rem 0;
+ }
+ </style>
+{% endblock %}
+
+
+{% macro card(course) %}
+<div class="col-sm-4 mb-4 text-left">
+ <a href="/lms/course?name={{ course.name }}&program={{ program.name }}" class="no-decoration no-underline">
+ <div class="card h-100">
+ {% if course.hero_image %}
+ <div class="card-hero-img" style="background-image: url({{ course.hero_image }})"></div>
+ {% else %}
+ <div class="card-image-wrapper text-center">
+ <div class="image-body"><i class="fa fa-picture-o" aria-hidden="true"></i></div>
+ </div>
+ {% endif %}
+ <div class='card-body'>
+ <h5 class='card-title'>{{ course.course_name }}</h5>
+ <div class="text-muted">{{ course.description[:110] + '...' if course.description else '' }}</div>
+ </div>
+ {% if has_access and progress[course.name] %}
+ <div class='card-footer'>
+ {% if progress[course.name].completed %}
+ <span class="indicator green">Completed</span>
+ {% elif progress[course.name].started %}
+ <span class="indicator orange">In Progress</span>
+ {% else %}
+ <span class="indicator blue">Start</span>
+ {% endif %}
+ </div>
+ {% endif %}
+ </div>
+ </a>
+</div>
+{% endmacro %}
+
+{% block content %}
+<section class="section">
+ {{ hero(program.program_name, program.description, has_access, {'name': 'Home', 'url': '/lms'}) }}
+ <div class='container'>
+ <div class="row mt-5">
+ {% for course in courses %}
+ {{ card(course) }}
+ {% endfor %}
+ {% if courses %}
+ {% for n in range(3 - ((courses|length)%3)) %}
+ {{ null_card() }}
+ {% endfor %}
+ {% endif %}
+ </div>
+ </div>
+</section>
+{% endblock %}
\ No newline at end of file
diff --git a/erpnext/www/lms/program.py b/erpnext/www/lms/program.py
new file mode 100644
index 0000000..1fcb3d3
--- /dev/null
+++ b/erpnext/www/lms/program.py
@@ -0,0 +1,23 @@
+from __future__ import unicode_literals
+import erpnext.education.utils as utils
+import frappe
+from frappe import _
+
+no_cache = 1
+
+def get_context(context):
+ context.education_settings = frappe.get_single("Education Settings")
+ context.program = get_program(frappe.form_dict['program'])
+ context.courses = [frappe.get_doc("Course", course.course) for course in context.program.courses]
+ context.has_access = utils.allowed_program_access(frappe.form_dict['program'])
+ context.progress = get_course_progress(context.courses, context.program)
+
+def get_program(program_name):
+ try:
+ return frappe.get_doc('Program', program_name)
+ except frappe.DoesNotExistError:
+ frappe.throw(_("Program {0} does not exist.".format(program_name)))
+
+def get_course_progress(courses, program):
+ progress = {course.name: utils.get_course_progress(course, program) for course in courses}
+ return progress
\ No newline at end of file
diff --git a/erpnext/www/lms/topic.html b/erpnext/www/lms/topic.html
new file mode 100644
index 0000000..3a77776
--- /dev/null
+++ b/erpnext/www/lms/topic.html
@@ -0,0 +1,58 @@
+{% extends "templates/base.html" %}
+{% block title %}Topic Title{% endblock %}
+{% from "www/lms/macros/hero.html" import hero %}
+{% from "www/lms/macros/card.html" import null_card %}
+
+{% block head_include %}
+ <style>
+ section {
+ padding: 5rem 0 5rem 0;
+ }
+ </style>
+{% endblock %}
+
+
+{% macro card(content, index, length) %}
+<div class="col-sm-{{ 12 if length%3 == 1 and index == 1 else 6 if length%3 == 2 and index in [1,2] else 4}} mb-4 text-left">
+ <a href="/lms/content?program={{ program }}&course={{ course }}&topic={{ topic.name }}&type={{ content.content_type }}&content={{ content.content.name }}" class="no-decoration no-underline">
+ <div class="card h-100">
+ <div class='card-body'>
+ <div>{{ content.content_type or '' }}</div>
+ <h5 class='card-title'>{{ content.content.name }}</h5>
+ </div>
+ {% if has_access %}
+ <div class='card-footer'>
+ {% if content.content_type == 'Quiz' %}
+ {% if content.result == 'Fail' %} <span class="indicator red">Fail <span class="text-muted">({{ content.score }}/100)</span></span>
+ {% elif content.result == 'Pass' %} <span class="indicator green">Pass <span class="text-muted">({{ content.score }}/100)</span>
+ {% else %} <span class="indicator blue">Start</span>
+ {% endif %}
+ {% else %}
+ {% if content.completed %} <span class="indicator green">Completed</span>
+ {% else %} <span class="indicator blue">Start</span>
+ {% endif %}
+ {% endif %}
+ </div>
+ {% endif %}
+ </div>
+ </a>
+</div>
+{% endmacro %}
+
+{% block content %}
+<section class="section">
+ {{ hero(topic.topic_name, topic.description, has_access, {'name': 'Course', 'url': '/lms/course?name=' + course +'&program=' + program}) }}
+ <div class='container'>
+ <div class="row mt-5">
+ {% for content in contents %}
+ {{ card(content, loop.index, topic.contents|length) }}
+ {% endfor %}
+ {% if contents %}
+ {% for n in range(3 - ((contents|length)%3)) %}
+ {{ null_card() }}
+ {% endfor %}
+ {% endif %}
+ </div>
+ </div>
+</section>
+{% endblock %}
\ No newline at end of file
diff --git a/erpnext/www/lms/topic.py b/erpnext/www/lms/topic.py
new file mode 100644
index 0000000..0af0778
--- /dev/null
+++ b/erpnext/www/lms/topic.py
@@ -0,0 +1,41 @@
+from __future__ import unicode_literals
+import erpnext.education.utils as utils
+import frappe
+
+no_cache = 1
+
+def get_context(context):
+ course = frappe.form_dict['course']
+ program = frappe.form_dict['program']
+ topic = frappe.form_dict['topic']
+
+ context.program = program
+ context.course = course
+ context.topic = frappe.get_doc("Topic", topic)
+ context.contents = get_contents(context.topic, course, program)
+ context.has_access = utils.allowed_program_access(program)
+
+def get_contents(topic, course, program):
+ student = utils.get_current_student()
+ if student:
+ course_enrollment = utils.get_or_create_course_enrollment(course, program)
+ contents = topic.get_contents()
+ progress = []
+ if contents:
+ for content in contents:
+ if content.doctype in ('Article', 'Video'):
+ if student:
+ status = utils.check_content_completion(content.name, content.doctype, course_enrollment.name)
+ else:
+ status = True
+ progress.append({'content': content, 'content_type': content.doctype, 'completed': status})
+ elif content.doctype == 'Quiz':
+ if student:
+ status, score, result = utils.check_quiz_completion(content, course_enrollment.name)
+ else:
+ status = False
+ score = None
+ result = None
+ progress.append({'content': content, 'content_type': content.doctype, 'completed': status, 'score': score, 'result': result})
+
+ return progress
\ No newline at end of file
diff --git a/erpnext/www/test_lms.py b/erpnext/www/test_lms.py
deleted file mode 100644
index e63f4c9..0000000
--- a/erpnext/www/test_lms.py
+++ /dev/null
@@ -1,11 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2018, Frappe Technologies and Contributors
-# See license.txt
-from __future__ import unicode_literals
-from erpnext.education.doctype.program.test_program import make_program_and_linked_courses
-
-import frappe
-import unittest
-
-class TestLms(unittest.TestCase):
- pass
\ No newline at end of file