Merge branch 'develop' into qmsystem

This commit is contained in:
Himanshu 2019-06-06 16:42:53 +05:30 committed by GitHub
commit b57b4a7199
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
66 changed files with 1822 additions and 4150 deletions

View File

@ -14,7 +14,7 @@ data = {
'Student Attendance Tool',
'Student Applicant'
],
'default_portal_role': 'LMS User',
'default_portal_role': 'Student',
'restricted_roles': [
'Student',
'Instructor',

View File

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

View File

@ -35,7 +35,7 @@ class CourseEnrollment(Document):
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 @@ class CourseEnrollment(Document):
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 @@ class CourseEnrollment(Document):
"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 @@ class CourseEnrollment(Document):
"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)
if activity:
return activity[0].name
else:
return None

View File

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

View File

@ -96,29 +96,6 @@ class ProgramEnrollment(Document):
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'):

View File

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

View File

@ -12,6 +12,7 @@ class Question(Document):
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 @@ class Question(Document):
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]

View File

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

View File

@ -7,51 +7,47 @@ import frappe
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:

View File

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

View File

@ -54,7 +54,7 @@ class Student(Document):
'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()

View File

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

View File

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

View File

@ -9,7 +9,8 @@ from erpnext.setup.utils import insert_record
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 @@ def create_academic_sessions():
{"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()

View File

@ -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 @@ def validate_duplicate_student(students):
# 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 @@ def get_current_student():
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 @@ def create_student_from_current_user():
"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_content_completion(content_name, content_type, enrollment_name):
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:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 &mdash; </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>

View File

@ -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()">&times;</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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

View File

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

View File

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

106
erpnext/www/lms/course.html Normal file
View File

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

19
erpnext/www/lms/course.py Normal file
View File

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

View File

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

16
erpnext/www/lms/index.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

41
erpnext/www/lms/topic.py Normal file
View File

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

View File

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