Compare commits
79 Commits
lesson-mar
...
cleanup
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f74c74d50 | ||
|
|
d2f435016c | ||
|
|
389b35802b | ||
|
|
a9192a74f9 | ||
|
|
1366c7cf75 | ||
|
|
eaa9e8e3ea | ||
|
|
5ecae0df61 | ||
|
|
4891be1d8c | ||
|
|
ec852fc255 | ||
|
|
47f2d3cb7b | ||
|
|
37820c1e19 | ||
|
|
230fab3bb2 | ||
|
|
d292d2d093 | ||
|
|
51d5db01e9 | ||
|
|
0fd760df81 | ||
|
|
71d0a89968 | ||
|
|
d939a63412 | ||
|
|
daaa2d2fe2 | ||
|
|
6dd7cb19df | ||
|
|
b1de2481a8 | ||
|
|
27c01b3b0c | ||
|
|
b7aa9aff51 | ||
|
|
524a041fb9 | ||
|
|
9de0203914 | ||
|
|
0ed5309b97 | ||
|
|
68fd32d536 | ||
|
|
5ea3b25d21 | ||
|
|
2c24412633 | ||
|
|
1b8a45ba4a | ||
|
|
3dd4adbc1f | ||
|
|
0c52c9c4bc | ||
|
|
9caf44cdbd | ||
|
|
45d88bdc08 | ||
|
|
94b3ccd3d9 | ||
|
|
ee8273fd30 | ||
|
|
60c1449f40 | ||
|
|
67708325ae | ||
|
|
3e99577401 | ||
|
|
5e916dc2c8 | ||
|
|
0c64d46e99 | ||
|
|
3aa974f8bd | ||
|
|
621d01d502 | ||
|
|
aa20136223 | ||
|
|
9bc5408a44 | ||
|
|
5a7afb3092 | ||
|
|
f8948ac2ef | ||
|
|
8b1576a028 | ||
|
|
56d8a72a7d | ||
|
|
f6c11ce52f | ||
|
|
0284c9305c | ||
|
|
d785fb7562 | ||
|
|
9f50af4ebd | ||
|
|
4c3645f0d4 | ||
|
|
20b3ae7d76 | ||
|
|
f303be4db5 | ||
|
|
fc1c393f15 | ||
|
|
5d96bf544d | ||
|
|
5abfa35095 | ||
|
|
6c751cdf39 | ||
|
|
2c570ea214 | ||
|
|
ecfcc8a2f7 | ||
|
|
3384f974e5 | ||
|
|
eb435261fe | ||
|
|
dc7eabefb9 | ||
|
|
fed4b5568b | ||
|
|
aa77c60abd | ||
|
|
9c1506d3c8 | ||
|
|
e94c3f27ab | ||
|
|
5fa8bdd40c | ||
|
|
17f03aeee7 | ||
|
|
7840512a13 | ||
|
|
526ded784b | ||
|
|
6b5ddcd54a | ||
|
|
c42247db42 | ||
|
|
8f8d4901ff | ||
|
|
f5f3c808d4 | ||
|
|
1e3152e303 | ||
|
|
344661cf83 | ||
|
|
1cb81de5c0 |
3
.gitignore
vendored
@@ -5,3 +5,6 @@
|
|||||||
tags
|
tags
|
||||||
community/docs/current
|
community/docs/current
|
||||||
community/public/dist
|
community/public/dist
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|||||||
373
community/fixtures/custom_field.json
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
|
"allow_on_submit": 0,
|
||||||
|
"bold": 0,
|
||||||
|
"collapsible": 0,
|
||||||
|
"collapsible_depends_on": null,
|
||||||
|
"columns": 0,
|
||||||
|
"default": null,
|
||||||
|
"depends_on": null,
|
||||||
|
"description": null,
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Custom Field",
|
||||||
|
"dt": "User",
|
||||||
|
"fetch_from": null,
|
||||||
|
"fetch_if_empty": 0,
|
||||||
|
"fieldname": "linkedin",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 0,
|
||||||
|
"hide_border": 0,
|
||||||
|
"hide_days": 0,
|
||||||
|
"hide_seconds": 0,
|
||||||
|
"ignore_user_permissions": 0,
|
||||||
|
"ignore_xss_filter": 0,
|
||||||
|
"in_global_search": 0,
|
||||||
|
"in_list_view": 0,
|
||||||
|
"in_preview": 0,
|
||||||
|
"in_standard_filter": 0,
|
||||||
|
"insert_after": "mobile_no",
|
||||||
|
"label": "LinkedIn ID",
|
||||||
|
"length": 0,
|
||||||
|
"mandatory_depends_on": null,
|
||||||
|
"modified": "2021-06-30 14:46:55.834145",
|
||||||
|
"name": "User-linkedin",
|
||||||
|
"no_copy": 0,
|
||||||
|
"non_negative": 0,
|
||||||
|
"options": null,
|
||||||
|
"parent": null,
|
||||||
|
"parentfield": null,
|
||||||
|
"parenttype": null,
|
||||||
|
"permlevel": 0,
|
||||||
|
"precision": "",
|
||||||
|
"print_hide": 0,
|
||||||
|
"print_hide_if_no_value": 0,
|
||||||
|
"print_width": null,
|
||||||
|
"read_only": 0,
|
||||||
|
"read_only_depends_on": null,
|
||||||
|
"report_hide": 0,
|
||||||
|
"reqd": 0,
|
||||||
|
"search_index": 0,
|
||||||
|
"translatable": 1,
|
||||||
|
"unique": 0,
|
||||||
|
"width": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
|
"allow_on_submit": 0,
|
||||||
|
"bold": 0,
|
||||||
|
"collapsible": 0,
|
||||||
|
"collapsible_depends_on": null,
|
||||||
|
"columns": 0,
|
||||||
|
"default": null,
|
||||||
|
"depends_on": null,
|
||||||
|
"description": null,
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Custom Field",
|
||||||
|
"dt": "User",
|
||||||
|
"fetch_from": null,
|
||||||
|
"fetch_if_empty": 0,
|
||||||
|
"fieldname": "github",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 0,
|
||||||
|
"hide_border": 0,
|
||||||
|
"hide_days": 0,
|
||||||
|
"hide_seconds": 0,
|
||||||
|
"ignore_user_permissions": 0,
|
||||||
|
"ignore_xss_filter": 0,
|
||||||
|
"in_global_search": 0,
|
||||||
|
"in_list_view": 0,
|
||||||
|
"in_preview": 0,
|
||||||
|
"in_standard_filter": 0,
|
||||||
|
"insert_after": "linkedin",
|
||||||
|
"label": "Github ID",
|
||||||
|
"length": 0,
|
||||||
|
"mandatory_depends_on": null,
|
||||||
|
"modified": "2021-06-30 14:46:55.834145",
|
||||||
|
"name": "User-github",
|
||||||
|
"no_copy": 0,
|
||||||
|
"non_negative": 0,
|
||||||
|
"options": null,
|
||||||
|
"parent": null,
|
||||||
|
"parentfield": null,
|
||||||
|
"parenttype": null,
|
||||||
|
"permlevel": 0,
|
||||||
|
"precision": "",
|
||||||
|
"print_hide": 0,
|
||||||
|
"print_hide_if_no_value": 0,
|
||||||
|
"print_width": null,
|
||||||
|
"read_only": 0,
|
||||||
|
"read_only_depends_on": null,
|
||||||
|
"report_hide": 0,
|
||||||
|
"reqd": 0,
|
||||||
|
"search_index": 0,
|
||||||
|
"translatable": 1,
|
||||||
|
"unique": 0,
|
||||||
|
"width": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
|
"allow_on_submit": 0,
|
||||||
|
"bold": 0,
|
||||||
|
"collapsible": 0,
|
||||||
|
"collapsible_depends_on": null,
|
||||||
|
"columns": 0,
|
||||||
|
"default": null,
|
||||||
|
"depends_on": null,
|
||||||
|
"description": null,
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Custom Field",
|
||||||
|
"dt": "User",
|
||||||
|
"fetch_from": null,
|
||||||
|
"fetch_if_empty": 0,
|
||||||
|
"fieldname": "medium",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 0,
|
||||||
|
"hide_border": 0,
|
||||||
|
"hide_days": 0,
|
||||||
|
"hide_seconds": 0,
|
||||||
|
"ignore_user_permissions": 0,
|
||||||
|
"ignore_xss_filter": 0,
|
||||||
|
"in_global_search": 0,
|
||||||
|
"in_list_view": 0,
|
||||||
|
"in_preview": 0,
|
||||||
|
"in_standard_filter": 0,
|
||||||
|
"insert_after": "github",
|
||||||
|
"label": "Medium ID",
|
||||||
|
"length": 0,
|
||||||
|
"mandatory_depends_on": null,
|
||||||
|
"modified": "2021-06-30 14:46:55.834145",
|
||||||
|
"name": "User-medium",
|
||||||
|
"no_copy": 0,
|
||||||
|
"non_negative": 0,
|
||||||
|
"options": null,
|
||||||
|
"parent": null,
|
||||||
|
"parentfield": null,
|
||||||
|
"parenttype": null,
|
||||||
|
"permlevel": 0,
|
||||||
|
"precision": "",
|
||||||
|
"print_hide": 0,
|
||||||
|
"print_hide_if_no_value": 0,
|
||||||
|
"print_width": null,
|
||||||
|
"read_only": 0,
|
||||||
|
"read_only_depends_on": null,
|
||||||
|
"report_hide": 0,
|
||||||
|
"reqd": 0,
|
||||||
|
"search_index": 0,
|
||||||
|
"translatable": 1,
|
||||||
|
"unique": 0,
|
||||||
|
"width": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
|
"allow_on_submit": 0,
|
||||||
|
"bold": 0,
|
||||||
|
"collapsible": 0,
|
||||||
|
"collapsible_depends_on": null,
|
||||||
|
"columns": 0,
|
||||||
|
"default": null,
|
||||||
|
"depends_on": null,
|
||||||
|
"description": null,
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Custom Field",
|
||||||
|
"dt": "User",
|
||||||
|
"fetch_from": null,
|
||||||
|
"fetch_if_empty": 0,
|
||||||
|
"fieldname": "city",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 0,
|
||||||
|
"hide_border": 0,
|
||||||
|
"hide_days": 0,
|
||||||
|
"hide_seconds": 0,
|
||||||
|
"ignore_user_permissions": 0,
|
||||||
|
"ignore_xss_filter": 0,
|
||||||
|
"in_global_search": 0,
|
||||||
|
"in_list_view": 0,
|
||||||
|
"in_preview": 0,
|
||||||
|
"in_standard_filter": 0,
|
||||||
|
"insert_after": "mute_sounds",
|
||||||
|
"label": "City",
|
||||||
|
"length": 0,
|
||||||
|
"mandatory_depends_on": null,
|
||||||
|
"modified": "2021-06-30 14:46:55.834145",
|
||||||
|
"name": "User-city",
|
||||||
|
"no_copy": 0,
|
||||||
|
"non_negative": 0,
|
||||||
|
"options": null,
|
||||||
|
"parent": null,
|
||||||
|
"parentfield": null,
|
||||||
|
"parenttype": null,
|
||||||
|
"permlevel": 0,
|
||||||
|
"precision": "",
|
||||||
|
"print_hide": 0,
|
||||||
|
"print_hide_if_no_value": 0,
|
||||||
|
"print_width": null,
|
||||||
|
"read_only": 0,
|
||||||
|
"read_only_depends_on": null,
|
||||||
|
"report_hide": 0,
|
||||||
|
"reqd": 0,
|
||||||
|
"search_index": 0,
|
||||||
|
"translatable": 1,
|
||||||
|
"unique": 0,
|
||||||
|
"width": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
|
"allow_on_submit": 0,
|
||||||
|
"bold": 0,
|
||||||
|
"collapsible": 0,
|
||||||
|
"collapsible_depends_on": null,
|
||||||
|
"columns": 0,
|
||||||
|
"default": null,
|
||||||
|
"depends_on": null,
|
||||||
|
"description": null,
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Custom Field",
|
||||||
|
"dt": "User",
|
||||||
|
"fetch_from": null,
|
||||||
|
"fetch_if_empty": 0,
|
||||||
|
"fieldname": "college",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 0,
|
||||||
|
"hide_border": 0,
|
||||||
|
"hide_days": 0,
|
||||||
|
"hide_seconds": 0,
|
||||||
|
"ignore_user_permissions": 0,
|
||||||
|
"ignore_xss_filter": 0,
|
||||||
|
"in_global_search": 0,
|
||||||
|
"in_list_view": 0,
|
||||||
|
"in_preview": 0,
|
||||||
|
"in_standard_filter": 0,
|
||||||
|
"insert_after": "city",
|
||||||
|
"label": "College Name",
|
||||||
|
"length": 0,
|
||||||
|
"mandatory_depends_on": null,
|
||||||
|
"modified": "2021-06-30 14:46:55.834145",
|
||||||
|
"name": "User-college",
|
||||||
|
"no_copy": 0,
|
||||||
|
"non_negative": 0,
|
||||||
|
"options": null,
|
||||||
|
"parent": null,
|
||||||
|
"parentfield": null,
|
||||||
|
"parenttype": null,
|
||||||
|
"permlevel": 0,
|
||||||
|
"precision": "",
|
||||||
|
"print_hide": 0,
|
||||||
|
"print_hide_if_no_value": 0,
|
||||||
|
"print_width": null,
|
||||||
|
"read_only": 0,
|
||||||
|
"read_only_depends_on": null,
|
||||||
|
"report_hide": 0,
|
||||||
|
"reqd": 0,
|
||||||
|
"search_index": 0,
|
||||||
|
"translatable": 1,
|
||||||
|
"unique": 0,
|
||||||
|
"width": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
|
"allow_on_submit": 0,
|
||||||
|
"bold": 0,
|
||||||
|
"collapsible": 0,
|
||||||
|
"collapsible_depends_on": null,
|
||||||
|
"columns": 0,
|
||||||
|
"default": null,
|
||||||
|
"depends_on": null,
|
||||||
|
"description": null,
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Custom Field",
|
||||||
|
"dt": "User",
|
||||||
|
"fetch_from": null,
|
||||||
|
"fetch_if_empty": 0,
|
||||||
|
"fieldname": "branch",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 0,
|
||||||
|
"hide_border": 0,
|
||||||
|
"hide_days": 0,
|
||||||
|
"hide_seconds": 0,
|
||||||
|
"ignore_user_permissions": 0,
|
||||||
|
"ignore_xss_filter": 0,
|
||||||
|
"in_global_search": 0,
|
||||||
|
"in_list_view": 0,
|
||||||
|
"in_preview": 0,
|
||||||
|
"in_standard_filter": 0,
|
||||||
|
"insert_after": "college",
|
||||||
|
"label": "Branch",
|
||||||
|
"length": 0,
|
||||||
|
"mandatory_depends_on": null,
|
||||||
|
"modified": "2021-06-30 14:46:55.834145",
|
||||||
|
"name": "User-branch",
|
||||||
|
"no_copy": 0,
|
||||||
|
"non_negative": 0,
|
||||||
|
"options": null,
|
||||||
|
"parent": null,
|
||||||
|
"parentfield": null,
|
||||||
|
"parenttype": null,
|
||||||
|
"permlevel": 0,
|
||||||
|
"precision": "",
|
||||||
|
"print_hide": 0,
|
||||||
|
"print_hide_if_no_value": 0,
|
||||||
|
"print_width": null,
|
||||||
|
"read_only": 0,
|
||||||
|
"read_only_depends_on": null,
|
||||||
|
"report_hide": 0,
|
||||||
|
"reqd": 0,
|
||||||
|
"search_index": 0,
|
||||||
|
"translatable": 1,
|
||||||
|
"unique": 0,
|
||||||
|
"width": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
|
"allow_on_submit": 0,
|
||||||
|
"bold": 0,
|
||||||
|
"collapsible": 0,
|
||||||
|
"collapsible_depends_on": null,
|
||||||
|
"columns": 0,
|
||||||
|
"default": null,
|
||||||
|
"depends_on": null,
|
||||||
|
"description": null,
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Custom Field",
|
||||||
|
"dt": "User",
|
||||||
|
"fetch_from": null,
|
||||||
|
"fetch_if_empty": 0,
|
||||||
|
"fieldname": "profession",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 0,
|
||||||
|
"hide_border": 0,
|
||||||
|
"hide_days": 0,
|
||||||
|
"hide_seconds": 0,
|
||||||
|
"ignore_user_permissions": 0,
|
||||||
|
"ignore_xss_filter": 0,
|
||||||
|
"in_global_search": 0,
|
||||||
|
"in_list_view": 0,
|
||||||
|
"in_preview": 0,
|
||||||
|
"in_standard_filter": 0,
|
||||||
|
"insert_after": "medium",
|
||||||
|
"label": "Profession",
|
||||||
|
"length": 0,
|
||||||
|
"mandatory_depends_on": null,
|
||||||
|
"modified": "2021-06-30 14:46:55.834145",
|
||||||
|
"name": "User-profession",
|
||||||
|
"no_copy": 0,
|
||||||
|
"non_negative": 0,
|
||||||
|
"options": null,
|
||||||
|
"parent": null,
|
||||||
|
"parentfield": null,
|
||||||
|
"parenttype": null,
|
||||||
|
"permlevel": 0,
|
||||||
|
"precision": "",
|
||||||
|
"print_hide": 0,
|
||||||
|
"print_hide_if_no_value": 0,
|
||||||
|
"print_width": null,
|
||||||
|
"read_only": 0,
|
||||||
|
"read_only_depends_on": null,
|
||||||
|
"report_hide": 0,
|
||||||
|
"reqd": 0,
|
||||||
|
"search_index": 0,
|
||||||
|
"translatable": 1,
|
||||||
|
"unique": 0,
|
||||||
|
"width": null
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -3,8 +3,9 @@
|
|||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
# import frappe
|
import frappe
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
from frappe import _
|
||||||
|
|
||||||
class CommunityProjectMember(Document):
|
class CommunityProjectMember(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
|
|||||||
@@ -104,6 +104,8 @@ doc_events = {
|
|||||||
# ]
|
# ]
|
||||||
#}
|
#}
|
||||||
|
|
||||||
|
fixtures = ["Custom Field"]
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
# -------
|
# -------
|
||||||
|
|
||||||
@@ -136,15 +138,15 @@ primary_rules = [
|
|||||||
{"from_route": "/hackathons/<hackathon>/<project>", "to_route": "hackathons/project"},
|
{"from_route": "/hackathons/<hackathon>/<project>", "to_route": "hackathons/project"},
|
||||||
{"from_route": "/dashboard", "to_route": ""},
|
{"from_route": "/dashboard", "to_route": ""},
|
||||||
{"from_route": "/add-a-new-batch", "to_route": "add-a-new-batch"},
|
{"from_route": "/add-a-new-batch", "to_route": "add-a-new-batch"},
|
||||||
{"from_route": "/courses/<course>/<batch>/home", "to_route": "batch/home"},
|
{"from_route": "/courses/<course>/home", "to_route": "batch/home"},
|
||||||
{"from_route": "/courses/<course>/<batch>/learn", "to_route": "batch/learn"},
|
{"from_route": "/courses/<course>/learn", "to_route": "batch/learn"},
|
||||||
{"from_route": "/courses/<course>/<batch>/learn/<int:chapter>.<int:lesson>", "to_route": "batch/learn"},
|
{"from_route": "/courses/<course>/learn/<int:chapter>.<int:lesson>", "to_route": "batch/learn"},
|
||||||
{"from_route": "/courses/<course>/<batch>/schedule", "to_route": "batch/schedule"},
|
{"from_route": "/courses/<course>/schedule", "to_route": "batch/schedule"},
|
||||||
{"from_route": "/courses/<course>/<batch>/members", "to_route": "batch/members"},
|
{"from_route": "/courses/<course>/members", "to_route": "batch/members"},
|
||||||
{"from_route": "/courses/<course>/<batch>/discuss", "to_route": "batch/discuss"},
|
{"from_route": "/courses/<course>/discuss", "to_route": "batch/discuss"},
|
||||||
{"from_route": "/courses/<course>/<batch>/about", "to_route": "batch/about"},
|
{"from_route": "/courses/<course>/about", "to_route": "batch/about"},
|
||||||
{"from_route": "/courses/<course>/<batch>/progress", "to_route": "batch/progress"},
|
{"from_route": "/courses/<course>/progress", "to_route": "batch/progress"},
|
||||||
{"from_route": "/courses/<course>/<batch>/join", "to_route": "batch/join"}
|
{"from_route": "/courses/<course>/join", "to_route": "batch/join"}
|
||||||
]
|
]
|
||||||
|
|
||||||
# Any frappe default URL is blocked by profile-rules, add it here to unblock it
|
# Any frappe default URL is blocked by profile-rules, add it here to unblock it
|
||||||
@@ -161,10 +163,12 @@ whitelist = [
|
|||||||
"/socket.io",
|
"/socket.io",
|
||||||
"/hackathons",
|
"/hackathons",
|
||||||
"/dashboard",
|
"/dashboard",
|
||||||
"/join-request"
|
"/join-request",
|
||||||
"/add-a-new-batch",
|
"/add-a-new-batch",
|
||||||
"/new-sign-up",
|
"/new-sign-up",
|
||||||
"/message"
|
"/message",
|
||||||
|
"/about",
|
||||||
|
"/edit-profile"
|
||||||
]
|
]
|
||||||
whitelist_rules = [{"from_route": p, "to_route": p[1:]} for p in whitelist]
|
whitelist_rules = [{"from_route": p, "to_route": p[1:]} for p in whitelist]
|
||||||
|
|
||||||
@@ -175,6 +179,10 @@ profile_rules = [
|
|||||||
|
|
||||||
website_route_rules = primary_rules + whitelist_rules + profile_rules
|
website_route_rules = primary_rules + whitelist_rules + profile_rules
|
||||||
|
|
||||||
|
website_redirects = [
|
||||||
|
{"source": "/update-profile", "target": "/edit-profile"},
|
||||||
|
]
|
||||||
|
|
||||||
update_website_context = 'community.widgets.update_website_context'
|
update_website_context = 'community.widgets.update_website_context'
|
||||||
|
|
||||||
## Specify the additional tabs to be included in the user profile page.
|
## Specify the additional tabs to be included in the user profile page.
|
||||||
@@ -186,5 +194,13 @@ update_website_context = 'community.widgets.update_website_context'
|
|||||||
## subclass of community.community.plugins.PageExtension
|
## subclass of community.community.plugins.PageExtension
|
||||||
# community_lesson_page_extension = None
|
# community_lesson_page_extension = None
|
||||||
|
|
||||||
|
community_lesson_page_extensions = [
|
||||||
|
"community.plugins.LiveCodeExtension"
|
||||||
|
]
|
||||||
|
|
||||||
## Markdown Macros for Lessons
|
## Markdown Macros for Lessons
|
||||||
# community_markdown_macro_renderers = {"Exercise": "myapp.mymodule.plugins.render_exercise"}
|
community_markdown_macro_renderers = {
|
||||||
|
"Exercise": "community.plugins.exercise_renderer",
|
||||||
|
"Quiz": "community.plugins.quiz_renderer",
|
||||||
|
"YouTubeVideo": "community.plugins.youtube_video_renderer",
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,13 +15,6 @@ def autosave_section(section, code):
|
|||||||
doc.insert()
|
doc.insert()
|
||||||
return {"name": doc.name}
|
return {"name": doc.name}
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def get_section(name):
|
|
||||||
"""Saves the code edited in one of the sections.
|
|
||||||
"""
|
|
||||||
doc = frappe.get_doc("LMS Section", name)
|
|
||||||
return doc and doc.as_dict()
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def submit_solution(exercise, code):
|
def submit_solution(exercise, code):
|
||||||
"""Submits a solution.
|
"""Submits a solution.
|
||||||
@@ -36,13 +29,13 @@ def submit_solution(exercise, code):
|
|||||||
return {"name": doc.name, "creation": doc.creation}
|
return {"name": doc.name, "creation": doc.creation}
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def save_current_lesson(batch_name, lesson_name):
|
def save_current_lesson(course_name, lesson_name):
|
||||||
"""Saves the current lesson for a student/mentor.
|
"""Saves the current lesson for a student/mentor.
|
||||||
"""
|
"""
|
||||||
name = frappe.get_value(
|
name = frappe.get_value(
|
||||||
doctype="LMS Batch Membership",
|
doctype="LMS Batch Membership",
|
||||||
filters={
|
filters={
|
||||||
"batch": batch_name,
|
"course": course_name,
|
||||||
"member": frappe.session.user
|
"member": frappe.session.user
|
||||||
},
|
},
|
||||||
fieldname="name")
|
fieldname="name")
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
from ...utils import slugify
|
||||||
|
|
||||||
class Chapter(Document):
|
class Chapter(Document):
|
||||||
def get_lessons(self):
|
def get_lessons(self):
|
||||||
@@ -13,3 +14,6 @@ class Chapter(Document):
|
|||||||
fields='name',
|
fields='name',
|
||||||
order_by="index_")
|
order_by="index_")
|
||||||
return [frappe.get_doc('Lesson', row['name']) for row in rows]
|
return [frappe.get_doc('Lesson', row['name']) for row in rows]
|
||||||
|
|
||||||
|
def get_slugified_chapter_title(self):
|
||||||
|
return slugify(self.title)
|
||||||
|
|||||||
@@ -3,12 +3,8 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from ..lms_sketch.livecode import livecode_to_svg
|
|
||||||
|
|
||||||
class Exercise(Document):
|
class Exercise(Document):
|
||||||
def before_save(self):
|
|
||||||
self.image = livecode_to_svg(None, self.answer)
|
|
||||||
|
|
||||||
def get_user_submission(self):
|
def get_user_submission(self):
|
||||||
"""Returns the latest submission for this user.
|
"""Returns the latest submission for this user.
|
||||||
"""
|
"""
|
||||||
@@ -42,8 +38,6 @@ class Exercise(Document):
|
|||||||
course = frappe.get_doc("LMS Course", self.course)
|
course = frappe.get_doc("LMS Course", self.course)
|
||||||
batch = course.get_student_batch(user)
|
batch = course.get_student_batch(user)
|
||||||
|
|
||||||
image = livecode_to_svg(None, code)
|
|
||||||
|
|
||||||
doc = frappe.get_doc(
|
doc = frappe.get_doc(
|
||||||
doctype="Exercise Submission",
|
doctype="Exercise Submission",
|
||||||
exercise=self.name,
|
exercise=self.name,
|
||||||
@@ -51,7 +45,6 @@ class Exercise(Document):
|
|||||||
course=self.course,
|
course=self.course,
|
||||||
lesson=self.lesson,
|
lesson=self.lesson,
|
||||||
batch=batch and batch.name,
|
batch=batch and batch.name,
|
||||||
image=image,
|
|
||||||
solution=code)
|
solution=code)
|
||||||
doc.insert(ignore_permissions=True)
|
doc.insert(ignore_permissions=True)
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,17 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"exercise",
|
"exercise",
|
||||||
"solution",
|
"status",
|
||||||
|
"batch",
|
||||||
|
"column_break_4",
|
||||||
"exercise_title",
|
"exercise_title",
|
||||||
"course",
|
"course",
|
||||||
"batch",
|
|
||||||
"lesson",
|
"lesson",
|
||||||
"image"
|
"section_break_8",
|
||||||
|
"solution",
|
||||||
|
"image",
|
||||||
|
"test_results",
|
||||||
|
"comments"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -21,12 +26,6 @@
|
|||||||
"label": "Exercise",
|
"label": "Exercise",
|
||||||
"options": "Exercise"
|
"options": "Exercise"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "solution",
|
|
||||||
"fieldtype": "Code",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Solution"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fetch_from": "exercise.title",
|
"fetch_from": "exercise.title",
|
||||||
"fieldname": "exercise_title",
|
"fieldname": "exercise_title",
|
||||||
@@ -61,11 +60,41 @@
|
|||||||
"fieldtype": "Code",
|
"fieldtype": "Code",
|
||||||
"label": "Image",
|
"label": "Image",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "status",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Status",
|
||||||
|
"options": "Correct\nIncorrect"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "test_results",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Test Results"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "comments",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Comments"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "solution",
|
||||||
|
"fieldtype": "Code",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Solution"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_4",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_8",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-05-21 11:28:45.833018",
|
"modified": "2021-06-24 16:22:50.570845",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "Exercise Submission",
|
"name": "Exercise Submission",
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ def create_invite_request(invite_email):
|
|||||||
|
|
||||||
frappe.get_doc({
|
frappe.get_doc({
|
||||||
"doctype": "Invite Request",
|
"doctype": "Invite Request",
|
||||||
"invite_email": invite_email
|
"invite_email": invite_email,
|
||||||
|
"status": "Approved"
|
||||||
}).save(ignore_permissions=True)
|
}).save(ignore_permissions=True)
|
||||||
return "OK"
|
return "OK"
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class TestInviteRequest(unittest.TestCase):
|
|||||||
filters={"invite_email": "test_invite@example.com"},
|
filters={"invite_email": "test_invite@example.com"},
|
||||||
fieldname=["invite_email", "status", "signup_email"],
|
fieldname=["invite_email", "status", "signup_email"],
|
||||||
as_dict=True)
|
as_dict=True)
|
||||||
self.assertEqual(invite.status, "Pending")
|
self.assertEqual(invite.status, "Approved")
|
||||||
self.assertEqual(invite.signup_email, None)
|
self.assertEqual(invite.signup_email, None)
|
||||||
|
|
||||||
def test_create_invite_request_update(self):
|
def test_create_invite_request_update(self):
|
||||||
|
|||||||
@@ -2,7 +2,47 @@
|
|||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
frappe.ui.form.on('Lesson', {
|
frappe.ui.form.on('Lesson', {
|
||||||
// refresh: function(frm) {
|
setup: function (frm) {
|
||||||
|
frm.trigger('setup_help');
|
||||||
|
},
|
||||||
|
setup_help(frm) {
|
||||||
|
frm.get_field('help').html(`
|
||||||
|
<p>You can add some more additional content to the lesson using a special syntax. The table below mentions all types of dynamic content that you can add to the lessons and the syntax for the same.</p>
|
||||||
|
<div class="row font-weight-bold mb-3">
|
||||||
|
<div class="col-sm-4">
|
||||||
|
Content Type
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
Syntax
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
// }
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-4">
|
||||||
|
YouTube Video
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
{{ YouTubeVideo("unique_embed_id") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-4">
|
||||||
|
Exercise
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
{{ Exercise("exercise_name") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-4">
|
||||||
|
Quiz
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
{{ Quiz("lms_quiz_name") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,12 +7,15 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"chapter",
|
"chapter",
|
||||||
"lesson_type",
|
"include_in_preview",
|
||||||
|
"column_break_4",
|
||||||
"title",
|
"title",
|
||||||
"index_",
|
"index_",
|
||||||
"index_label",
|
"index_label",
|
||||||
|
"section_break_6",
|
||||||
"body",
|
"body",
|
||||||
"sections"
|
"help_section",
|
||||||
|
"help"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -22,14 +25,6 @@
|
|||||||
"label": "Chapter",
|
"label": "Chapter",
|
||||||
"options": "Chapter"
|
"options": "Chapter"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"default": "Video",
|
|
||||||
"fieldname": "lesson_type",
|
|
||||||
"fieldtype": "Select",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Lesson Type",
|
|
||||||
"options": "Video\nText\nQuiz"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "title",
|
"fieldname": "title",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
@@ -40,6 +35,7 @@
|
|||||||
"default": "1",
|
"default": "1",
|
||||||
"fieldname": "index_",
|
"fieldname": "index_",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
|
"in_list_view": 1,
|
||||||
"label": "Index"
|
"label": "Index"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -47,23 +43,39 @@
|
|||||||
"fieldtype": "Markdown Editor",
|
"fieldtype": "Markdown Editor",
|
||||||
"label": "Body"
|
"label": "Body"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "sections",
|
|
||||||
"fieldtype": "Table",
|
|
||||||
"label": "Sections",
|
|
||||||
"options": "LMS Section"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "index_label",
|
"fieldname": "index_label",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Index Label",
|
"label": "Index Label",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "include_in_preview",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Include In Preview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_6",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_4",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "help_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Help"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "help",
|
||||||
|
"fieldtype": "HTML"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-06-01 05:30:48.127494",
|
"modified": "2021-06-29 13:34:49.077363",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "Lesson",
|
"name": "Lesson",
|
||||||
|
|||||||
@@ -5,32 +5,39 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from ...section_parser import SectionParser
|
|
||||||
from ...md import markdown_to_html, find_macros
|
from ...md import markdown_to_html, find_macros
|
||||||
|
|
||||||
class Lesson(Document):
|
class Lesson(Document):
|
||||||
def before_save(self):
|
def before_save(self):
|
||||||
macros = find_macros(self.body)
|
dynamic_documents = ["Exercise", "Quiz"]
|
||||||
exercises = [value for name, value in macros if name == "Exercise"]
|
for section in dynamic_documents:
|
||||||
|
self.update_lesson_name_in_document(section)
|
||||||
|
|
||||||
|
def update_lesson_name_in_document(self, section):
|
||||||
|
doctype_map= {
|
||||||
|
"Exercise": "Exercise",
|
||||||
|
"Quiz": "LMS Quiz"
|
||||||
|
}
|
||||||
|
macros = find_macros(self.body)
|
||||||
|
documents = [value for name, value in macros if name == section]
|
||||||
index = 1
|
index = 1
|
||||||
for name in exercises:
|
for name in documents:
|
||||||
e = frappe.get_doc("Exercise", name)
|
e = frappe.get_doc(doctype_map[section], name)
|
||||||
e.lesson = self.name
|
e.lesson = self.name
|
||||||
e.index_ = index
|
e.index_ = index
|
||||||
e.save()
|
e.save()
|
||||||
index += 1
|
index += 1
|
||||||
self.update_orphan_exercises(exercises)
|
self.update_orphan_documents(doctype_map[section], documents)
|
||||||
|
|
||||||
def update_orphan_exercises(self, active_exercises):
|
def update_orphan_documents(self, doctype, documents):
|
||||||
"""Updates the exercises that were previously part of this lesson,
|
"""Updates the documents that were previously part of this lesson,
|
||||||
but not any more.
|
but not any more.
|
||||||
"""
|
"""
|
||||||
linked_exercises = {row['name'] for row in frappe.get_all('Exercise', {"lesson": self.name})}
|
linked_documents = {row['name'] for row in frappe.get_all(doctype, {"lesson": self.name})}
|
||||||
active_exercises = set(active_exercises)
|
active_documents = set(documents)
|
||||||
orphan_exercises = linked_exercises - active_exercises
|
orphan_documents = linked_documents - active_documents
|
||||||
for name in orphan_exercises:
|
for name in orphan_documents:
|
||||||
ex = frappe.get_doc("Exercise", name)
|
ex = frappe.get_doc(doctype, name)
|
||||||
ex.lesson = None
|
ex.lesson = None
|
||||||
ex.index_ = 0
|
ex.index_ = 0
|
||||||
ex.index_label = ""
|
ex.index_label = ""
|
||||||
@@ -39,9 +46,6 @@ class Lesson(Document):
|
|||||||
def render_html(self):
|
def render_html(self):
|
||||||
return markdown_to_html(self.body)
|
return markdown_to_html(self.body)
|
||||||
|
|
||||||
def get_sections(self):
|
|
||||||
return sorted(self.get('sections'), key=lambda s: s.index)
|
|
||||||
|
|
||||||
def get_exercises(self):
|
def get_exercises(self):
|
||||||
if not self.body:
|
if not self.body:
|
||||||
return []
|
return []
|
||||||
@@ -50,30 +54,6 @@ class Lesson(Document):
|
|||||||
exercises = [value for name, value in macros if name == "Exercise"]
|
exercises = [value for name, value in macros if name == "Exercise"]
|
||||||
return [frappe.get_doc("Exercise", name) for name in exercises]
|
return [frappe.get_doc("Exercise", name) for name in exercises]
|
||||||
|
|
||||||
def make_lms_section(self, index, section):
|
|
||||||
s = frappe.new_doc('LMS Section', parent_doc=self, parentfield='sections')
|
|
||||||
s.type = section.type
|
|
||||||
s.id = section.id
|
|
||||||
s.label = section.label
|
|
||||||
s.contents = section.contents
|
|
||||||
s.index = index
|
|
||||||
return s
|
|
||||||
|
|
||||||
def get_next(self):
|
|
||||||
"""Returns the number for the next lesson.
|
|
||||||
|
|
||||||
The return value would be like 1.2, 2.1 etc.
|
|
||||||
It will be None if there is no next lesson.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def get_prev(self):
|
|
||||||
"""Returns the number for the prev lesson.
|
|
||||||
|
|
||||||
The return value would be like 1.2, 2.1 etc.
|
|
||||||
It will be None if there is no next lesson.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_progress(self):
|
def get_progress(self):
|
||||||
return frappe.db.get_value("LMS Course Progress", {"lesson": self.name, "owner": frappe.session.user}, "status")
|
return frappe.db.get_value("LMS Course Progress", {"lesson": self.name, "owner": frappe.session.user}, "status")
|
||||||
|
|
||||||
@@ -83,36 +63,35 @@ class Lesson(Document):
|
|||||||
return
|
return
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def save_progress(lesson, batch):
|
def save_progress(lesson, course, status):
|
||||||
if not frappe.db.exists("LMS Batch Membership",
|
if not frappe.db.exists("LMS Batch Membership",
|
||||||
{
|
{
|
||||||
"member": frappe.session.user,
|
"member": frappe.session.user,
|
||||||
"batch": batch
|
"course": course
|
||||||
}):
|
}):
|
||||||
return
|
return
|
||||||
|
|
||||||
if frappe.db.exists("LMS Course Progress",
|
if frappe.db.exists("LMS Course Progress",
|
||||||
{
|
{
|
||||||
"lesson": lesson,
|
"lesson": lesson,
|
||||||
"owner": frappe.session.user
|
"owner": frappe.session.user,
|
||||||
|
"course": course
|
||||||
}):
|
}):
|
||||||
return
|
doc = frappe.get_doc("LMS Course Progress",
|
||||||
|
{
|
||||||
lesson_details = frappe.get_doc("Lesson", lesson)
|
"lesson": lesson,
|
||||||
dynamic_content = frappe.db.count("LMS Section",
|
"owner": frappe.session.user,
|
||||||
filters={
|
"course": course
|
||||||
"type": ["not in", ["example", "text"]],
|
|
||||||
"parent": lesson_details.name
|
|
||||||
})
|
})
|
||||||
|
doc.status = status
|
||||||
status = "Complete"
|
doc.save(ignore_permissions=True)
|
||||||
if dynamic_content:
|
else:
|
||||||
status = "Partially Complete"
|
|
||||||
|
|
||||||
frappe.get_doc({
|
frappe.get_doc({
|
||||||
"doctype": "LMS Course Progress",
|
"doctype": "LMS Course Progress",
|
||||||
"lesson": lesson_details.name,
|
"lesson": lesson,
|
||||||
"status": status
|
"status": status,
|
||||||
}).save(ignore_permissions=True)
|
}).save(ignore_permissions=True)
|
||||||
|
return "OK"
|
||||||
|
|
||||||
def update_progress(lesson):
|
def update_progress(lesson):
|
||||||
user = frappe.session.user
|
user = frappe.session.user
|
||||||
@@ -121,17 +100,33 @@ def update_progress(lesson):
|
|||||||
if frappe.db.exists("LMS Course Progress", {"lesson": lesson, "owner": user}):
|
if frappe.db.exists("LMS Course Progress", {"lesson": lesson, "owner": user}):
|
||||||
course_progress = frappe.get_doc("LMS Course Progress", {"lesson": lesson, "owner": user})
|
course_progress = frappe.get_doc("LMS Course Progress", {"lesson": lesson, "owner": user})
|
||||||
course_progress.status = "Complete"
|
course_progress.status = "Complete"
|
||||||
course_progress.save()
|
course_progress.save(ignore_permissions=True)
|
||||||
|
|
||||||
def all_dynamic_content_submitted(lesson, user):
|
def all_dynamic_content_submitted(lesson, user):
|
||||||
exercise_names = frappe.get_list("Exercise", {"lesson": lesson}, ["name"], pluck="name")
|
all_exercises_submitted = check_all_exercise_submission(lesson, user)
|
||||||
all_exercises_submitted = False
|
all_quiz_submitted = check_all_quiz_submitted(lesson, user)
|
||||||
print(exercise_names)
|
return all_exercises_submitted and all_quiz_submitted
|
||||||
|
|
||||||
|
def check_all_exercise_submission(lesson, user):
|
||||||
|
exercise_names = frappe.get_list("Exercise", {"lesson": lesson}, pluck="name", ignore_permissions=True)
|
||||||
|
if not len(exercise_names):
|
||||||
|
return True
|
||||||
query = {
|
query = {
|
||||||
"exercise": ["in", exercise_names],
|
"exercise": ["in", exercise_names],
|
||||||
"owner": user
|
"owner": user
|
||||||
}
|
}
|
||||||
if frappe.db.count("Exercise Submission", query) == len(exercise_names):
|
if frappe.db.count("Exercise Submission", query) == len(exercise_names):
|
||||||
all_exercises_submitted = True
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
return all_exercises_submitted
|
def check_all_quiz_submitted(lesson, user):
|
||||||
|
quizzes = frappe.get_list("LMS Quiz", {"lesson": lesson}, pluck="name", ignore_permissions=True)
|
||||||
|
if not len(quizzes):
|
||||||
|
return True
|
||||||
|
query = {
|
||||||
|
"quiz": ["in", quizzes],
|
||||||
|
"owner": user
|
||||||
|
}
|
||||||
|
if frappe.db.count("LMS Quiz Submission", query) == len(quizzes):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|||||||
@@ -33,8 +33,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "title",
|
"fieldname": "title",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Title",
|
"label": "Title"
|
||||||
"unique": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "description",
|
"fieldname": "description",
|
||||||
@@ -120,7 +119,7 @@
|
|||||||
"link_fieldname": "batch"
|
"link_fieldname": "batch"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2021-05-26 16:43:57.399747",
|
"modified": "2021-06-16 10:51:05.403726",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Batch",
|
"name": "LMS Batch",
|
||||||
|
|||||||
@@ -19,15 +19,7 @@ class LMSBatch(Document):
|
|||||||
frappe.throw(_("You are not a mentor of the course {0}").format(course.title))
|
frappe.throw(_("You are not a mentor of the course {0}").format(course.title))
|
||||||
|
|
||||||
def after_insert(self):
|
def after_insert(self):
|
||||||
create_membership(batch=self.name, member_type="Mentor")
|
create_membership(batch=self.name, course=self.course, member_type="Mentor")
|
||||||
|
|
||||||
def get_mentors(self):
|
|
||||||
memberships = frappe.get_all(
|
|
||||||
"LMS Batch Membership",
|
|
||||||
{"batch": self.name, "member_type": "Mentor"},
|
|
||||||
["member"])
|
|
||||||
member_names = [m['member'] for m in memberships]
|
|
||||||
return find_all("User", name=["IN", member_names])
|
|
||||||
|
|
||||||
def is_member(self, email, member_type=None):
|
def is_member(self, email, member_type=None):
|
||||||
"""Checks if a person is part of a batch.
|
"""Checks if a person is part of a batch.
|
||||||
@@ -43,16 +35,6 @@ class LMSBatch(Document):
|
|||||||
filters['member_type'] = member_type
|
filters['member_type'] = member_type
|
||||||
return frappe.db.exists("LMS Batch Membership", filters)
|
return frappe.db.exists("LMS Batch Membership", filters)
|
||||||
|
|
||||||
def get_students(self):
|
|
||||||
"""Returns (email, full_name, username) of all the students of this batch as a list of dict.
|
|
||||||
"""
|
|
||||||
memberships = frappe.get_all(
|
|
||||||
"LMS Batch Membership",
|
|
||||||
{"batch": self.name, "member_type": "Student"},
|
|
||||||
["member"])
|
|
||||||
member_names = [m['member'] for m in memberships]
|
|
||||||
return find_all("User", name=["IN", member_names])
|
|
||||||
|
|
||||||
def get_messages(self):
|
def get_messages(self):
|
||||||
messages = frappe.get_all("LMS Message", {"batch": self.name}, ["*"], order_by="creation")
|
messages = frappe.get_all("LMS Message", {"batch": self.name}, ["*"], order_by="creation")
|
||||||
for message in messages:
|
for message in messages:
|
||||||
@@ -80,11 +62,6 @@ class LMSBatch(Document):
|
|||||||
membership = self.get_membership(user)
|
membership = self.get_membership(user)
|
||||||
return membership and membership.current_lesson
|
return membership and membership.current_lesson
|
||||||
|
|
||||||
def get_learn_url(self, lesson_number):
|
|
||||||
if not lesson_number:
|
|
||||||
return
|
|
||||||
return f"/courses/{self.course}/{self.name}/learn/{lesson_number}"
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def save_message(message, batch):
|
def save_message(message, batch):
|
||||||
doc = frappe.get_doc({
|
doc = frappe.get_doc({
|
||||||
@@ -94,3 +71,38 @@ def save_message(message, batch):
|
|||||||
"message": message
|
"message": message
|
||||||
})
|
})
|
||||||
doc.save(ignore_permissions=True)
|
doc.save(ignore_permissions=True)
|
||||||
|
|
||||||
|
def switch_batch(course_name, email, batch_name):
|
||||||
|
"""Switches the user from the current batch of the course to a new batch.
|
||||||
|
"""
|
||||||
|
membership = frappe.get_last_doc(
|
||||||
|
"LMS Batch Membership",
|
||||||
|
filters={"course": course_name, "member": email})
|
||||||
|
|
||||||
|
batch = frappe.get_doc("LMS Batch", batch_name)
|
||||||
|
if not batch:
|
||||||
|
raise ValueError(f"Invalid Batch: {batch_name}")
|
||||||
|
|
||||||
|
if batch.course != course_name:
|
||||||
|
raise ValueError("Can not switch batches across courses")
|
||||||
|
|
||||||
|
if batch.is_member(email):
|
||||||
|
print(f"{email} is already a member of {batch.title}")
|
||||||
|
return
|
||||||
|
|
||||||
|
old_batch = frappe.get_doc("LMS Batch", membership.batch)
|
||||||
|
|
||||||
|
print("updating membership", membership.name)
|
||||||
|
membership.batch = batch_name
|
||||||
|
membership.save()
|
||||||
|
|
||||||
|
# update exercise submissions
|
||||||
|
filters = {
|
||||||
|
"owner": email,
|
||||||
|
"batch": old_batch.name
|
||||||
|
}
|
||||||
|
for name in frappe.db.get_all("Exercise Submission", filters=filters, pluck='name'):
|
||||||
|
doc = frappe.get_doc("Exercise Submission", name)
|
||||||
|
print("updating exercise submission", name)
|
||||||
|
doc.batch = batch_name
|
||||||
|
doc.save()
|
||||||
|
|||||||
@@ -10,5 +10,5 @@ frappe.ui.form.on('LMS Batch Membership', {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -65,8 +65,7 @@
|
|||||||
"fieldname": "course",
|
"fieldname": "course",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Course",
|
"label": "Course"
|
||||||
"read_only": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "current_lesson",
|
"fieldname": "current_lesson",
|
||||||
@@ -84,7 +83,7 @@
|
|||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-05-24 12:40:57.125694",
|
"modified": "2021-07-06 20:50:46.885325",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Batch Membership",
|
"name": "LMS Batch Membership",
|
||||||
|
|||||||
@@ -14,42 +14,65 @@ class LMSBatchMembership(Document):
|
|||||||
self.validate_membership_in_different_batch_same_course()
|
self.validate_membership_in_different_batch_same_course()
|
||||||
|
|
||||||
def validate_membership_in_same_batch(self):
|
def validate_membership_in_same_batch(self):
|
||||||
previous_membership = frappe.db.get_value("LMS Batch Membership",
|
|
||||||
filters={
|
filters={
|
||||||
"member": self.member,
|
"member": self.member,
|
||||||
"batch": self.batch,
|
"course": self.course,
|
||||||
"name": ["!=", self.name]
|
"name": ["!=", self.name]
|
||||||
},
|
}
|
||||||
|
if self.batch:
|
||||||
|
filters["batch"] = self.batch
|
||||||
|
previous_membership = frappe.db.get_value("LMS Batch Membership",
|
||||||
|
filters,
|
||||||
fieldname=["member_type","member"],
|
fieldname=["member_type","member"],
|
||||||
as_dict=1)
|
as_dict=1)
|
||||||
|
|
||||||
if previous_membership:
|
if previous_membership:
|
||||||
member_name = frappe.db.get_value("User", self.member, "full_name")
|
member_name = frappe.db.get_value("User", self.member, "full_name")
|
||||||
frappe.throw(_("{0} is already a {1} of {2}").format(member_name, previous_membership.member_type, self.batch))
|
course_title = frappe.db.get_value("LMS Course", self.course, "title")
|
||||||
|
frappe.throw(_("{0} is already a {1} of the course {2}").format(member_name, previous_membership.member_type, course_title))
|
||||||
|
|
||||||
def validate_membership_in_different_batch_same_course(self):
|
def validate_membership_in_different_batch_same_course(self):
|
||||||
|
"""Ensures that a studnet is only part of one batch.
|
||||||
|
"""
|
||||||
|
# nothing to worry if the member is not a student
|
||||||
|
if self.member_type != "Student":
|
||||||
|
return
|
||||||
|
|
||||||
course = frappe.db.get_value("LMS Batch", self.batch, "course")
|
course = frappe.db.get_value("LMS Batch", self.batch, "course")
|
||||||
previous_membership = frappe.get_all("LMS Batch Membership",
|
memberships = frappe.get_all(
|
||||||
|
"LMS Batch Membership",
|
||||||
filters={
|
filters={
|
||||||
"member": self.member,
|
"member": self.member,
|
||||||
"name": ["!=", self.name]
|
"name": ["!=", self.name],
|
||||||
|
"member_type": "Student",
|
||||||
|
"course": self.course
|
||||||
},
|
},
|
||||||
fields=["batch", "member_type", "name"]
|
fields=["batch", "member_type", "name"]
|
||||||
)
|
)
|
||||||
|
|
||||||
for membership in previous_membership:
|
if memberships:
|
||||||
batch_course = frappe.db.get_value("LMS Batch", membership.batch, "course")
|
membership = memberships[0]
|
||||||
if batch_course == course and (membership.member_type == "Student" or self.member_type == "Student"):
|
|
||||||
member_name = frappe.db.get_value("User", self.member, "full_name")
|
member_name = frappe.db.get_value("User", self.member, "full_name")
|
||||||
frappe.throw(_("{0} is already a {1} of {2} course through {3} batch").format(member_name, membership.member_type, course, membership.batch))
|
frappe.throw(_("{0} is already a Student of {1} course through {2} batch").format(member_name, course, membership.batch))
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def create_membership(batch, member=None, member_type="Student", role="Member"):
|
def create_membership(course, batch=None, member=None, member_type="Student", role="Member"):
|
||||||
frappe.get_doc({
|
frappe.get_doc({
|
||||||
"doctype": "LMS Batch Membership",
|
"doctype": "LMS Batch Membership",
|
||||||
"batch": batch,
|
"batch": batch,
|
||||||
|
"course": course,
|
||||||
"role": role,
|
"role": role,
|
||||||
"member_type": member_type,
|
"member_type": member_type,
|
||||||
"member": member or frappe.session.user
|
"member": member or frappe.session.user
|
||||||
}).save(ignore_permissions=True)
|
}).save(ignore_permissions=True)
|
||||||
return "OK"
|
return "OK"
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def update_current_membership(batch, course, member):
|
||||||
|
all_memberships = frappe.get_all("LMS Batch Membership", {"member": member, "course": course})
|
||||||
|
for membership in all_memberships:
|
||||||
|
frappe.db.set_value("LMS Batch Membership", membership.name, "is_current", 0)
|
||||||
|
|
||||||
|
current_membership = frappe.get_all("LMS Batch Membership", {"batch": batch, "member": member})
|
||||||
|
if len(current_membership):
|
||||||
|
frappe.db.set_value("LMS Batch Membership", current_membership[0].name, "is_current", 1)
|
||||||
|
|||||||
@@ -21,11 +21,14 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"title",
|
"title",
|
||||||
"is_published",
|
|
||||||
"column_break_3",
|
|
||||||
"short_code",
|
"short_code",
|
||||||
"video_link",
|
"video_link",
|
||||||
|
"column_break_3",
|
||||||
|
"is_published",
|
||||||
|
"disable_self_learning",
|
||||||
|
"image",
|
||||||
"section_break_5",
|
"section_break_5",
|
||||||
|
"tags",
|
||||||
"short_introduction",
|
"short_introduction",
|
||||||
"description"
|
"description"
|
||||||
],
|
],
|
||||||
@@ -73,6 +76,22 @@
|
|||||||
"fieldtype": "Small Text",
|
"fieldtype": "Small Text",
|
||||||
"label": "Short Introduction",
|
"label": "Short Introduction",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "disable_self_learning",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Disable Self Learning"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "image",
|
||||||
|
"fieldtype": "Attach Image",
|
||||||
|
"label": "Preview Image"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "tags",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Tags"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
@@ -99,7 +118,7 @@
|
|||||||
"link_fieldname": "course"
|
"link_fieldname": "course"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2021-06-01 04:36:45.696776",
|
"modified": "2021-07-09 15:05:05.372430",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Course",
|
"name": "LMS Course",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from frappe.model.document import Document
|
|||||||
import json
|
import json
|
||||||
from ...utils import slugify
|
from ...utils import slugify
|
||||||
from community.query import find, find_all
|
from community.query import find, find_all
|
||||||
|
from frappe.utils import flt, cint
|
||||||
|
|
||||||
class LMSCourse(Document):
|
class LMSCourse(Document):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -81,8 +82,8 @@ class LMSCourse(Document):
|
|||||||
"""
|
"""
|
||||||
if not email:
|
if not email:
|
||||||
return False
|
return False
|
||||||
return frappe.db.exists({
|
return frappe.db.count("LMS Course Mentor Mapping",
|
||||||
"doctype": "LMS Course Mentor Mapping",
|
{
|
||||||
"course": self.name,
|
"course": self.name,
|
||||||
"mentor": email
|
"mentor": email
|
||||||
})
|
})
|
||||||
@@ -112,7 +113,29 @@ class LMSCourse(Document):
|
|||||||
"""Returns all chapters of this course.
|
"""Returns all chapters of this course.
|
||||||
"""
|
"""
|
||||||
# TODO: chapters should have a way to specify the order
|
# TODO: chapters should have a way to specify the order
|
||||||
return find_all("Chapter", course=self.name, order_by="creation")
|
return find_all("Chapter", course=self.name, order_by="index_")
|
||||||
|
|
||||||
|
def get_lessons(self):
|
||||||
|
""" Returns all lessons of this course """
|
||||||
|
lessons = []
|
||||||
|
chapters = self.get_chapters()
|
||||||
|
for chapter in chapters:
|
||||||
|
lessons.append(frappe.get_all("Lesson", {"chapter": chapter.name}))
|
||||||
|
return lessons
|
||||||
|
|
||||||
|
def get_course_progress(self):
|
||||||
|
""" Returns the course progress of the session user """
|
||||||
|
lesson_count = len(self.get_lessons())
|
||||||
|
completed_lessons = frappe.db.count("LMS Course Progress",
|
||||||
|
{
|
||||||
|
"course": self.name,
|
||||||
|
"owner": frappe.session.user,
|
||||||
|
"status": "Complete"
|
||||||
|
})
|
||||||
|
precision = cint(frappe.db.get_default("float_precision")) or 3
|
||||||
|
if not lesson_count:
|
||||||
|
return 0
|
||||||
|
return flt(((completed_lessons/lesson_count) * 100), precision)
|
||||||
|
|
||||||
def get_batch(self, batch_name):
|
def get_batch(self, batch_name):
|
||||||
return find("LMS Batch", name=batch_name, course=self.name)
|
return find("LMS Batch", name=batch_name, course=self.name)
|
||||||
@@ -186,10 +209,111 @@ class LMSCourse(Document):
|
|||||||
exercise.save()
|
exercise.save()
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
|
def get_learn_url(self, lesson_number):
|
||||||
|
if not lesson_number:
|
||||||
|
return
|
||||||
|
return f"/courses/{self.name}/learn/{lesson_number}"
|
||||||
|
|
||||||
|
def get_membership(self, member, batch=None):
|
||||||
|
filters = {
|
||||||
|
"member": member,
|
||||||
|
"course": self.name
|
||||||
|
}
|
||||||
|
if batch:
|
||||||
|
filters["batch"] = batch
|
||||||
|
|
||||||
|
membership = frappe.db.get_value("LMS Batch Membership",
|
||||||
|
filters,
|
||||||
|
["name", "batch", "current_lesson", "member_type"],
|
||||||
|
as_dict=True)
|
||||||
|
|
||||||
|
if membership and membership.batch:
|
||||||
|
membership.batch_title = frappe.db.get_value("LMS Batch", membership.batch, "title")
|
||||||
|
return membership
|
||||||
|
|
||||||
|
def get_all_memberships(self, member):
|
||||||
|
all_memberships = frappe.get_all("LMS Batch Membership", {"member": member, "course": self.name}, ["batch"])
|
||||||
|
for membership in all_memberships:
|
||||||
|
membership.batch_title = frappe.db.get_value("LMS Batch", membership.batch, "title")
|
||||||
|
return all_memberships
|
||||||
|
|
||||||
|
def get_mentors(self, batch=None):
|
||||||
|
filters = {
|
||||||
|
"course": self.name,
|
||||||
|
"member_type": "Mentor"
|
||||||
|
}
|
||||||
|
if batch:
|
||||||
|
filters["batch"] = batch
|
||||||
|
|
||||||
|
memberships = frappe.get_all(
|
||||||
|
"LMS Batch Membership",
|
||||||
|
filters,
|
||||||
|
["member"])
|
||||||
|
member_names = [m['member'] for m in memberships]
|
||||||
|
return find_all("User", name=["IN", member_names])
|
||||||
|
|
||||||
|
def get_students(self, batch=None):
|
||||||
|
"""Returns (email, full_name, username) of all the students of this batch as a list of dict.
|
||||||
|
"""
|
||||||
|
filters = {
|
||||||
|
"course": self.name,
|
||||||
|
"member_type": "Student"
|
||||||
|
}
|
||||||
|
if batch:
|
||||||
|
filters["batch"] = batch
|
||||||
|
memberships = frappe.get_all(
|
||||||
|
"LMS Batch Membership",
|
||||||
|
filters,
|
||||||
|
["member"])
|
||||||
|
member_names = [m['member'] for m in memberships]
|
||||||
|
return find_all("User", name=["IN", member_names])
|
||||||
|
|
||||||
|
def get_tags(self):
|
||||||
|
return self.tags.split(",") if self.tags else []
|
||||||
|
|
||||||
|
def get_reviews(self):
|
||||||
|
reviews = frappe.get_all("LMS Course Review",
|
||||||
|
{
|
||||||
|
"course": self.name
|
||||||
|
},
|
||||||
|
["review", "rating", "owner"],
|
||||||
|
order_by= "creation desc")
|
||||||
|
|
||||||
|
for review in reviews:
|
||||||
|
review.owner_details = frappe.get_doc("User", review.owner)
|
||||||
|
|
||||||
|
return reviews
|
||||||
|
|
||||||
|
def is_eligible_to_review(self, membership):
|
||||||
|
""" Checks if user is eligible to review the course """
|
||||||
|
if not membership:
|
||||||
|
return False
|
||||||
|
if frappe.db.count("LMS Course Review",
|
||||||
|
{
|
||||||
|
"course": self.name,
|
||||||
|
"owner": frappe.session.user
|
||||||
|
}):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_average_rating(self):
|
||||||
|
ratings = [review.rating for review in self.get_reviews()]
|
||||||
|
if not len(ratings):
|
||||||
|
return None
|
||||||
|
return sum(ratings)/len(ratings)
|
||||||
|
|
||||||
def get_outline(self):
|
def get_outline(self):
|
||||||
return CourseOutline(self)
|
return CourseOutline(self)
|
||||||
|
|
||||||
|
def get_progress(self, lesson):
|
||||||
|
return frappe.db.get_value("LMS Course Progress",
|
||||||
|
{
|
||||||
|
"course": self.name,
|
||||||
|
"owner": frappe.session.user,
|
||||||
|
"lesson": lesson
|
||||||
|
},
|
||||||
|
["status"])
|
||||||
|
|
||||||
class CourseOutline:
|
class CourseOutline:
|
||||||
def __init__(self, course):
|
def __init__(self, course):
|
||||||
self.course = course
|
self.course = course
|
||||||
@@ -197,6 +321,7 @@ class CourseOutline:
|
|||||||
self.lessons = self.get_lessons()
|
self.lessons = self.get_lessons()
|
||||||
|
|
||||||
def get_next(self, current):
|
def get_next(self, current):
|
||||||
|
current = flt(current)
|
||||||
numbers = sorted(lesson['number'] for lesson in self.lessons)
|
numbers = sorted(lesson['number'] for lesson in self.lessons)
|
||||||
try:
|
try:
|
||||||
index = numbers.index(current)
|
index = numbers.index(current)
|
||||||
@@ -205,6 +330,7 @@ class CourseOutline:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def get_prev(self, current):
|
def get_prev(self, current):
|
||||||
|
current = flt(current)
|
||||||
numbers = sorted(lesson['number'] for lesson in self.lessons)
|
numbers = sorted(lesson['number'] for lesson in self.lessons)
|
||||||
try:
|
try:
|
||||||
index = numbers.index(current)
|
index = numbers.index(current)
|
||||||
@@ -228,7 +354,7 @@ class CourseOutline:
|
|||||||
|
|
||||||
chapter_numbers = {c['name']: c['index_'] for c in self.chapters}
|
chapter_numbers = {c['name']: c['index_'] for c in self.chapters}
|
||||||
for lesson in lessons:
|
for lesson in lessons:
|
||||||
lesson['number'] = "{}.{}".format(chapter_numbers[lesson['chapter']], lesson['index_'])
|
lesson['number'] = flt("{}.{}".format(chapter_numbers[lesson['chapter']], lesson['index_']))
|
||||||
return lessons
|
return lessons
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ class TestLMSCourse(unittest.TestCase):
|
|||||||
course = self.new_course("Test Course")
|
course = self.new_course("Test Course")
|
||||||
assert course.title == "Test Course"
|
assert course.title == "Test Course"
|
||||||
assert course.name == "test-course"
|
assert course.name == "test-course"
|
||||||
assert course.get_mentors() == []
|
|
||||||
|
|
||||||
def test_find_all(self):
|
def test_find_all(self):
|
||||||
courses = LMSCourse.find_all()
|
courses = LMSCourse.find_all()
|
||||||
|
|||||||
@@ -2,7 +2,13 @@
|
|||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
frappe.ui.form.on('LMS Course Mentor Mapping', {
|
frappe.ui.form.on('LMS Course Mentor Mapping', {
|
||||||
// refresh: function(frm) {
|
onload: function(frm) {
|
||||||
|
frm.set_query('mentor', function(doc) {
|
||||||
// }
|
return {
|
||||||
|
filters: {
|
||||||
|
"ignore_user_type": 1,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2021, FOSS United and contributors
|
// Copyright (c) 2021, FOSS United and contributors
|
||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
frappe.ui.form.on('Code Revision', {
|
frappe.ui.form.on('LMS Course Review', {
|
||||||
// refresh: function(frm) {
|
// refresh: function(frm) {
|
||||||
|
|
||||||
// }
|
// }
|
||||||
@@ -1,41 +1,41 @@
|
|||||||
{
|
{
|
||||||
"actions": [],
|
"actions": [],
|
||||||
"creation": "2021-04-07 00:26:28.806520",
|
"creation": "2021-06-28 13:36:36.146718",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"section",
|
"review",
|
||||||
"code",
|
"rating",
|
||||||
"author"
|
"course"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldname": "section",
|
"fieldname": "review",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Small Text",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Section",
|
"label": "Review"
|
||||||
"options": "LMS Section"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "code",
|
"fieldname": "rating",
|
||||||
"fieldtype": "Code",
|
"fieldtype": "Rating",
|
||||||
"label": "Code"
|
"in_list_view": 1,
|
||||||
|
"label": "Rating"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "author",
|
"fieldname": "course",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Author",
|
"label": "Course",
|
||||||
"options": "User"
|
"options": "LMS Course"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-04-14 11:26:19.628317",
|
"modified": "2021-07-05 14:57:03.841430",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "Code Revision",
|
"name": "LMS Course Review",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
@@ -53,6 +53,5 @@
|
|||||||
],
|
],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"title_field": "section",
|
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
18
community/lms/doctype/lms_course_review/lms_course_review.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Copyright (c) 2021, FOSS United and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
class LMSCourseReview(Document):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def submit_review(rating, review, course):
|
||||||
|
frappe.get_doc({
|
||||||
|
"doctype": "LMS Course Review",
|
||||||
|
"rating": rating,
|
||||||
|
"review": review,
|
||||||
|
"course": course
|
||||||
|
}).save(ignore_permissions=True)
|
||||||
|
return "OK"
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright (c) 2021, FOSS United and Contributors
|
# Copyright (c) 2021, FOSS United and Contributors
|
||||||
# See license.txt
|
# See license.txt
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
# import frappe
|
# import frappe
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
class TestLMSSketch(unittest.TestCase):
|
class TestLMSCourseReview(unittest.TestCase):
|
||||||
pass
|
pass
|
||||||
36
community/lms/doctype/lms_option/lms_option.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2021-06-07 10:46:10.402684",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"option",
|
||||||
|
"is_correct"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "option",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Option"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "is_correct",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Is Correct"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2021-06-07 10:48:45.290227",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "LMS",
|
||||||
|
"name": "LMS Option",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright (c) 2021, FOSS United and contributors
|
# Copyright (c) 2021, FOSS United and contributors
|
||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
# import frappe
|
# import frappe
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
|
||||||
class CodeRevision(Document):
|
class LMSOption(Document):
|
||||||
pass
|
pass
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2021, FOSS United and contributors
|
// Copyright (c) 2021, FOSS United and contributors
|
||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
frappe.ui.form.on('LMS Sketch', {
|
frappe.ui.form.on('LMS Quiz', {
|
||||||
// refresh: function(frm) {
|
// refresh: function(frm) {
|
||||||
|
|
||||||
// }
|
// }
|
||||||
@@ -1,45 +1,41 @@
|
|||||||
{
|
{
|
||||||
"actions": [],
|
"actions": [],
|
||||||
"autoname": "format:SKETCH-{#}",
|
"autoname": "field:title",
|
||||||
"creation": "2021-03-09 16:31:50.523524",
|
"creation": "2021-06-07 10:50:17.893625",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"title",
|
"title",
|
||||||
"runtime",
|
"questions",
|
||||||
"code",
|
"lesson"
|
||||||
"svg"
|
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldname": "title",
|
"fieldname": "title",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Title"
|
"label": "Title",
|
||||||
|
"unique": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "runtime",
|
"fieldname": "questions",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Table",
|
||||||
"label": "Runtime"
|
"label": "Questions",
|
||||||
|
"options": "LMS Quiz Question"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "code",
|
"fieldname": "lesson",
|
||||||
"fieldtype": "Code",
|
"fieldtype": "Link",
|
||||||
"label": "Code"
|
"label": "Lesson",
|
||||||
},
|
"options": "Lesson"
|
||||||
{
|
|
||||||
"fieldname": "svg",
|
|
||||||
"fieldtype": "Code",
|
|
||||||
"label": "SVG",
|
|
||||||
"read_only": 1
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-03-12 08:42:56.671658",
|
"modified": "2021-06-23 17:58:57.642873",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Sketch",
|
"name": "LMS Quiz",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
@@ -57,6 +53,5 @@
|
|||||||
],
|
],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"track_changes": 1,
|
"track_changes": 1
|
||||||
"track_views": 1
|
|
||||||
}
|
}
|
||||||
79
community/lms/doctype/lms_quiz/lms_quiz.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# Copyright (c) 2021, FOSS United and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from community.lms.doctype.lesson.lesson import update_progress
|
||||||
|
import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
import json
|
||||||
|
from frappe import _
|
||||||
|
from ..lesson.lesson import update_progress
|
||||||
|
|
||||||
|
class LMSQuiz(Document):
|
||||||
|
def validate(self):
|
||||||
|
self.validate_correct_answers()
|
||||||
|
|
||||||
|
def validate_correct_answers(self):
|
||||||
|
for question in self.questions:
|
||||||
|
correct_options = self.get_correct_options(question)
|
||||||
|
|
||||||
|
if len(correct_options) > 1:
|
||||||
|
question.multiple = 1
|
||||||
|
|
||||||
|
if not len(correct_options):
|
||||||
|
frappe.throw(_("At least one answer must be correct for this question: {0}").format(frappe.bold(question.question)))
|
||||||
|
|
||||||
|
def get_correct_options(self, question):
|
||||||
|
correct_option_fields = ["is_correct_1", "is_correct_2", "is_correct_3", "is_correct_4"]
|
||||||
|
return list(filter(lambda x: question.get(x) == 1, correct_option_fields))
|
||||||
|
|
||||||
|
def get_last_submission_details(self):
|
||||||
|
"""Returns the latest submission for this user.
|
||||||
|
"""
|
||||||
|
user = frappe.session.user
|
||||||
|
if not user or user == "Guest":
|
||||||
|
return
|
||||||
|
|
||||||
|
result = frappe.get_all('LMS Quiz Submission',
|
||||||
|
fields="*",
|
||||||
|
filters={
|
||||||
|
"owner": user,
|
||||||
|
"quiz": self.name
|
||||||
|
},
|
||||||
|
order_by="creation desc",
|
||||||
|
page_length=1)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
return result[0]
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def submit(quiz, result):
|
||||||
|
score = 0
|
||||||
|
answer_map = {
|
||||||
|
"is_correct_1": "option_1",
|
||||||
|
"is_correct_2": "option_2",
|
||||||
|
"is_correct_3": "option_3",
|
||||||
|
"is_correct_4": "option_4"
|
||||||
|
}
|
||||||
|
result = json.loads(result)
|
||||||
|
quiz_details = frappe.get_doc("LMS Quiz", quiz)
|
||||||
|
|
||||||
|
for response in result:
|
||||||
|
match = list(filter(lambda x: x.question == response.get("question"), quiz_details.questions))[0]
|
||||||
|
correct_options = quiz_details.get_correct_options(match)
|
||||||
|
correct_answers = [ match.get(answer_map[option]) for option in correct_options ]
|
||||||
|
|
||||||
|
if response.get("answer") == correct_answers:
|
||||||
|
response["result"] = "Right"
|
||||||
|
score += 1
|
||||||
|
else:
|
||||||
|
response["result"] = "Wrong"
|
||||||
|
response["answer"] = ("").join([ ans if idx == len(response.get("answer")) -1 else ans + ", " for idx, ans in enumerate(response.get("answer")) ])
|
||||||
|
|
||||||
|
frappe.get_doc({
|
||||||
|
"doctype": "LMS Quiz Submission",
|
||||||
|
"quiz": quiz,
|
||||||
|
"result": result,
|
||||||
|
"score": score
|
||||||
|
}).save(ignore_permissions=True)
|
||||||
|
update_progress(quiz_details.lesson)
|
||||||
|
return score
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright (c) 2021, FOSS United and Contributors
|
# Copyright (c) 2021, FOSS United and Contributors
|
||||||
# See license.txt
|
# See license.txt
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
# import frappe
|
# import frappe
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
class TestCodeRevision(unittest.TestCase):
|
class TestLMSQuiz(unittest.TestCase):
|
||||||
pass
|
pass
|
||||||
118
community/lms/doctype/lms_quiz_question/lms_quiz_question.json
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2021-06-07 10:48:57.994714",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"question",
|
||||||
|
"options_section",
|
||||||
|
"option_1",
|
||||||
|
"is_correct_1",
|
||||||
|
"section_break_5",
|
||||||
|
"option_2",
|
||||||
|
"is_correct_2",
|
||||||
|
"column_break_4",
|
||||||
|
"option_3",
|
||||||
|
"is_correct_3",
|
||||||
|
"section_break_11",
|
||||||
|
"option_4",
|
||||||
|
"is_correct_4",
|
||||||
|
"multiple"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "question",
|
||||||
|
"fieldtype": "Text",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Question",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "option_1",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Option 1",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "option_2",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Option 2",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "option_3",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Option 3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "option_4",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Option 4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"depends_on": "option_1",
|
||||||
|
"fieldname": "is_correct_1",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Is Correct"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"depends_on": "option_2",
|
||||||
|
"fieldname": "is_correct_2",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Is Correct"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"depends_on": "option_3",
|
||||||
|
"fieldname": "is_correct_3",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Is Correct"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"depends_on": "option_4",
|
||||||
|
"fieldname": "is_correct_4",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Is Correct"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "multiple",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Multiple Correct Answers",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "options_section",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_4",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_5",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_11",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2021-06-22 16:54:13.133859",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "LMS",
|
||||||
|
"name": "LMS Quiz Question",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Copyright (c) 2021, FOSS United and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
class LMSQuizQuestion(Document):
|
||||||
|
pass
|
||||||
0
community/lms/doctype/lms_quiz_result/__init__.py
Normal file
45
community/lms/doctype/lms_quiz_result/lms_quiz_result.json
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2021-06-07 14:19:23.683323",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"question",
|
||||||
|
"answer",
|
||||||
|
"result"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "question",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Question"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "result",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Result",
|
||||||
|
"options": "Right\nWrong"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "answer",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Users Response"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2021-06-22 18:32:28.813159",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "LMS",
|
||||||
|
"name": "LMS Quiz Result",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
||||||
8
community/lms/doctype/lms_quiz_result/lms_quiz_result.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Copyright (c) 2021, FOSS United and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
class LMSQuizResult(Document):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// Copyright (c) 2021, FOSS United and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
frappe.ui.form.on('LMS Quiz Submission', {
|
||||||
|
// refresh: function(frm) {
|
||||||
|
|
||||||
|
// }
|
||||||
|
});
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2021-06-07 14:19:54.958989",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"quiz",
|
||||||
|
"result",
|
||||||
|
"score"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "quiz",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Quiz",
|
||||||
|
"options": "LMS Quiz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "result",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Result",
|
||||||
|
"options": "LMS Quiz Result"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "score",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Score"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2021-06-07 14:19:54.958989",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "LMS",
|
||||||
|
"name": "LMS Quiz Submission",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Copyright (c) 2021, FOSS United and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
class LMSQuizSubmission(Document):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Copyright (c) 2021, FOSS United and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
class TestLMSQuizSubmission(unittest.TestCase):
|
||||||
|
pass
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
{
|
|
||||||
"actions": [],
|
|
||||||
"creation": "2021-03-05 15:10:53.906006",
|
|
||||||
"doctype": "DocType",
|
|
||||||
"engine": "InnoDB",
|
|
||||||
"field_order": [
|
|
||||||
"label",
|
|
||||||
"type",
|
|
||||||
"contents",
|
|
||||||
"code",
|
|
||||||
"attrs",
|
|
||||||
"index",
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldname": "label",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Label"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "type",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Type"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "contents",
|
|
||||||
"fieldtype": "Markdown Editor",
|
|
||||||
"label": "Contents"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "code",
|
|
||||||
"fieldtype": "Code",
|
|
||||||
"label": "Code"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "attrs",
|
|
||||||
"fieldtype": "Long Text",
|
|
||||||
"label": "attrs"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "index",
|
|
||||||
"fieldtype": "Int",
|
|
||||||
"label": "Index"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "id",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"label": "id"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"index_web_pages_for_search": 1,
|
|
||||||
"istable": 1,
|
|
||||||
"links": [],
|
|
||||||
"modified": "2021-05-19 18:55:26.019625",
|
|
||||||
"modified_by": "Administrator",
|
|
||||||
"module": "LMS",
|
|
||||||
"name": "LMS Section",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"permissions": [],
|
|
||||||
"sort_field": "modified",
|
|
||||||
"sort_order": "DESC",
|
|
||||||
"track_changes": 1
|
|
||||||
}
|
|
||||||
@@ -14,6 +14,10 @@ class LMSSection(Document):
|
|||||||
if self.type == "exercise":
|
if self.type == "exercise":
|
||||||
return frappe.get_doc("Exercise", self.id)
|
return frappe.get_doc("Exercise", self.id)
|
||||||
|
|
||||||
|
def get_quiz(self):
|
||||||
|
if self.type == "quiz":
|
||||||
|
return frappe.get_doc("LMS Quiz", self.id)
|
||||||
|
|
||||||
def get_latest_code_for_user(self):
|
def get_latest_code_for_user(self):
|
||||||
"""Returns the latest code for the logged in user.
|
"""Returns the latest code for the logged in user.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,126 +0,0 @@
|
|||||||
"""Utilities to work with livecode service.
|
|
||||||
"""
|
|
||||||
import websocket
|
|
||||||
import json
|
|
||||||
from .svg import SVG
|
|
||||||
import frappe
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
# Files to pass to livecode server
|
|
||||||
# The same code is part of livecode-canvas.js
|
|
||||||
# TODO: generate livecode-canvas.js from this file
|
|
||||||
START = '''
|
|
||||||
import sketch
|
|
||||||
code = open("main.py").read()
|
|
||||||
env = dict(sketch.__dict__)
|
|
||||||
exec(code, env)
|
|
||||||
'''
|
|
||||||
|
|
||||||
SKETCH = '''
|
|
||||||
import json
|
|
||||||
|
|
||||||
def sendmsg(msgtype, function, args):
|
|
||||||
"""Sends a message to the frontend.
|
|
||||||
|
|
||||||
The frontend will receive the specified message whenever
|
|
||||||
this function is called. The frontend can decide to some
|
|
||||||
action on each of these messages.
|
|
||||||
"""
|
|
||||||
msg = dict(msgtype=msgtype, function=function, args=args)
|
|
||||||
print("--MSG--", json.dumps(msg))
|
|
||||||
|
|
||||||
def _draw(func, **kwargs):
|
|
||||||
sendmsg(msgtype="draw", function=func, args=kwargs)
|
|
||||||
|
|
||||||
def circle(x, y, d):
|
|
||||||
"""Draws a circle of diameter d with center (x, y).
|
|
||||||
"""
|
|
||||||
_draw("circle", x=x, y=y, d=d)
|
|
||||||
|
|
||||||
def line(x1, y1, x2, y2):
|
|
||||||
"""Draws a line from point (x1, y1) to point (x2, y2).
|
|
||||||
"""
|
|
||||||
_draw("line", x1=x1, y1=y1, x2=x2, y2=y2)
|
|
||||||
|
|
||||||
def rect(x, y, w, h):
|
|
||||||
"""Draws a rectangle on the canvas.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
x: x coordinate of the top-left corner of the rectangle
|
|
||||||
y: y coordinate of the top-left corner of the rectangle
|
|
||||||
w: width of the rectangle
|
|
||||||
h: height of the rectangle
|
|
||||||
"""
|
|
||||||
_draw("rect", x=x, y=y, w=w, h=h)
|
|
||||||
|
|
||||||
def clear():
|
|
||||||
_draw("clear")
|
|
||||||
|
|
||||||
# clear the canvas on start
|
|
||||||
clear()
|
|
||||||
'''
|
|
||||||
|
|
||||||
def get_livecode_url():
|
|
||||||
doc = frappe.get_cached_doc("LMS Settings")
|
|
||||||
return doc.livecode_url
|
|
||||||
|
|
||||||
def get_livecode_ws_url():
|
|
||||||
url = urlparse(get_livecode_url())
|
|
||||||
protocol = "wss" if url.scheme == "https" else "ws"
|
|
||||||
return protocol + "://" + url.netloc + "/livecode"
|
|
||||||
|
|
||||||
def livecode_to_svg(livecode_ws_url, code, *, timeout=3):
|
|
||||||
"""Renders the code as svg.
|
|
||||||
"""
|
|
||||||
if livecode_ws_url is None:
|
|
||||||
livecode_ws_url = get_livecode_ws_url()
|
|
||||||
|
|
||||||
try:
|
|
||||||
ws = websocket.WebSocket()
|
|
||||||
ws.settimeout(timeout)
|
|
||||||
ws.connect(livecode_ws_url)
|
|
||||||
|
|
||||||
msg = {
|
|
||||||
"msgtype": "exec",
|
|
||||||
"runtime": "python",
|
|
||||||
"code": code,
|
|
||||||
"files": [
|
|
||||||
{"filename": "start.py", "contents": START},
|
|
||||||
{"filename": "sketch.py", "contents": SKETCH},
|
|
||||||
],
|
|
||||||
"command": ["python", "start.py"]
|
|
||||||
}
|
|
||||||
ws.send(json.dumps(msg))
|
|
||||||
|
|
||||||
messages = _read_messages(ws)
|
|
||||||
commands = [m for m in messages if m['msgtype'] == 'draw']
|
|
||||||
img = draw_image(commands)
|
|
||||||
return img.tostring()
|
|
||||||
except websocket.WebSocketException as e:
|
|
||||||
frappe.log_error(frappe.get_traceback(), 'livecode_to_svg failed')
|
|
||||||
|
|
||||||
def _read_messages(ws):
|
|
||||||
messages = []
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
msg = ws.recv()
|
|
||||||
if not msg:
|
|
||||||
break
|
|
||||||
messages.append(json.loads(msg))
|
|
||||||
except websocket.WebSocketTimeoutException as e:
|
|
||||||
print("Error:", e)
|
|
||||||
pass
|
|
||||||
return messages
|
|
||||||
|
|
||||||
def draw_image(commands):
|
|
||||||
img = SVG(width=300, height=300, viewBox="0 0 300 300", fill='none', stroke='black')
|
|
||||||
for c in commands:
|
|
||||||
args = c['args']
|
|
||||||
if c['function'] == 'circle':
|
|
||||||
img.circle(cx=args['x'], cy=args['y'], r=args['d']/2)
|
|
||||||
elif c['function'] == 'line':
|
|
||||||
img.line(x1=args['x1'], y1=args['y1'], x2=args['x2'], y2=args['y2'])
|
|
||||||
elif c['function'] == 'rect':
|
|
||||||
img.rect(x=args['x'], y=args['y'], width=args['w'], height=args['h'])
|
|
||||||
return img
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright (c) 2021, FOSS United and contributors
|
|
||||||
# For license information, please see license.txt
|
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
import hashlib
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
import frappe
|
|
||||||
from frappe.model.document import Document
|
|
||||||
from . import livecode
|
|
||||||
|
|
||||||
DEFAULT_IMAGE = """
|
|
||||||
<svg viewBox="0 0 300 300" width="300" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
</svg>
|
|
||||||
"""
|
|
||||||
|
|
||||||
class LMSSketch(Document):
|
|
||||||
@property
|
|
||||||
def sketch_id(self):
|
|
||||||
"""Returns the numeric part of the name.
|
|
||||||
|
|
||||||
For example, the skech_id will be "123" for sketch with name "SKETCH-123".
|
|
||||||
"""
|
|
||||||
return self.name.replace("SKETCH-", "")
|
|
||||||
|
|
||||||
def get_owner(self):
|
|
||||||
"""Returns the owner of this sketch as a document.
|
|
||||||
"""
|
|
||||||
return frappe.get_doc("User", self.owner)
|
|
||||||
|
|
||||||
def get_owner_name(self):
|
|
||||||
return self.get_owner().full_name
|
|
||||||
|
|
||||||
def get_livecode_url(self):
|
|
||||||
doc = frappe.get_cached_doc("LMS Settings")
|
|
||||||
return doc.livecode_url
|
|
||||||
|
|
||||||
def get_livecode_ws_url(self):
|
|
||||||
url = urlparse(self.get_livecode_url())
|
|
||||||
protocol = "wss" if url.scheme == "https" else "ws"
|
|
||||||
return protocol + "://" + url.netloc + "/livecode"
|
|
||||||
|
|
||||||
def to_svg(self):
|
|
||||||
return self.svg or self.render_svg()
|
|
||||||
|
|
||||||
def render_svg(self):
|
|
||||||
h = hashlib.md5(self.code.encode('utf-8')).hexdigest()
|
|
||||||
cache = frappe.cache()
|
|
||||||
key = "sketch-" + h
|
|
||||||
value = cache.get(key)
|
|
||||||
if value:
|
|
||||||
value = value.decode('utf-8')
|
|
||||||
else:
|
|
||||||
ws_url = self.get_livecode_ws_url()
|
|
||||||
value = livecode.livecode_to_svg(ws_url, self.code)
|
|
||||||
if value:
|
|
||||||
cache.set(key, value)
|
|
||||||
return value or DEFAULT_IMAGE
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_recent_sketches(limit=100, owner=None):
|
|
||||||
"""Returns the recent sketches.
|
|
||||||
"""
|
|
||||||
filters = {}
|
|
||||||
if owner:
|
|
||||||
filters = {"owner": owner}
|
|
||||||
sketches = frappe.get_all(
|
|
||||||
"LMS Sketch",
|
|
||||||
filters=filters,
|
|
||||||
fields='*',
|
|
||||||
order_by='modified desc',
|
|
||||||
page_length=limit
|
|
||||||
)
|
|
||||||
return [frappe.get_doc(doctype='LMS Sketch', **doc) for doc in sketches]
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<LMSSketch {self.name}>"
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def save_sketch(name, title, code):
|
|
||||||
if not name or name == "new":
|
|
||||||
doc = frappe.new_doc('LMS Sketch')
|
|
||||||
doc.title = title
|
|
||||||
doc.code = code
|
|
||||||
doc.runtime = 'python-canvas'
|
|
||||||
doc.insert(ignore_permissions=True)
|
|
||||||
status = "created"
|
|
||||||
else:
|
|
||||||
doc = frappe.get_doc("LMS Sketch", name)
|
|
||||||
|
|
||||||
if doc.owner != frappe.session.user:
|
|
||||||
return {
|
|
||||||
"ok": False,
|
|
||||||
"error": "Permission Denied"
|
|
||||||
}
|
|
||||||
doc.title = title
|
|
||||||
doc.code = code
|
|
||||||
doc.svg = ''
|
|
||||||
doc.save()
|
|
||||||
status = "updated"
|
|
||||||
return {
|
|
||||||
"ok": True,
|
|
||||||
"status": status,
|
|
||||||
"name": doc.name,
|
|
||||||
}
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
"""SVG rendering library.
|
|
||||||
|
|
||||||
USAGE:
|
|
||||||
from svg import SVG
|
|
||||||
|
|
||||||
svg = SVG(width=200, height=200)
|
|
||||||
svg.circle(cx=100, cy=200, r=50)
|
|
||||||
print(svg.tostring())
|
|
||||||
"""
|
|
||||||
from xml.etree import ElementTree
|
|
||||||
|
|
||||||
TAGNAMES = set([
|
|
||||||
"circle", "ellipse",
|
|
||||||
"line", "path", "rect", "polygon", "polyline",
|
|
||||||
"text", "textPath", "title",
|
|
||||||
"marker", "defs",
|
|
||||||
"g"
|
|
||||||
])
|
|
||||||
|
|
||||||
class Node:
|
|
||||||
"""SVG Node"""
|
|
||||||
def __init__(self, tag, **attrs):
|
|
||||||
self.tag = tag
|
|
||||||
self.attrs = dict((k.replace('_', '-'), str(v)) for k, v in attrs.items())
|
|
||||||
self.children = []
|
|
||||||
|
|
||||||
def node(self, tag, **attrs):
|
|
||||||
n = Node(tag, **attrs)
|
|
||||||
self.children.append(n)
|
|
||||||
return n
|
|
||||||
|
|
||||||
def apply(self, func):
|
|
||||||
"""Applies a function to this node and
|
|
||||||
all the children recursively.
|
|
||||||
"""
|
|
||||||
func(self)
|
|
||||||
for n in self.children:
|
|
||||||
n.apply(func)
|
|
||||||
|
|
||||||
def clone(self):
|
|
||||||
node = Node(self.tag, **self.attrs)
|
|
||||||
node.children = [n.clone() for n in self.children]
|
|
||||||
return node
|
|
||||||
|
|
||||||
def add_node(self, node):
|
|
||||||
if not isinstance(node, Node):
|
|
||||||
node = Text(node)
|
|
||||||
self.children.append(node)
|
|
||||||
|
|
||||||
def __getattr__(self, tag):
|
|
||||||
if tag not in TAGNAMES:
|
|
||||||
raise AttributeError(tag)
|
|
||||||
return lambda **attrs: self.node(tag, **attrs)
|
|
||||||
|
|
||||||
def translate(self, x, y):
|
|
||||||
return self.g(transform="translate(%s, %s)" % (x, y))
|
|
||||||
|
|
||||||
def scale(self, *args):
|
|
||||||
return self.g(transform="scale(%s)" % ", ".join(str(a) for a in args))
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "<%s .../>" % self.tag
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, type, value, traceback):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def build_tree(self, builder):
|
|
||||||
builder.start(self.tag, self.attrs)
|
|
||||||
for node in self.children:
|
|
||||||
node.build_tree(builder)
|
|
||||||
return builder.end(self.tag)
|
|
||||||
|
|
||||||
def _indent(self, elem, level=0):
|
|
||||||
"""Indent etree node for prettyprinting."""
|
|
||||||
|
|
||||||
i = "\n" + level*" "
|
|
||||||
if len(elem):
|
|
||||||
if not elem.text or not elem.text.strip():
|
|
||||||
elem.text = i + " "
|
|
||||||
if not elem.tail or not elem.tail.strip():
|
|
||||||
elem.tail = i
|
|
||||||
for elem in elem:
|
|
||||||
self._indent(elem, level+1)
|
|
||||||
if not elem.tail or not elem.tail.strip():
|
|
||||||
elem.tail = i
|
|
||||||
else:
|
|
||||||
if level and (not elem.tail or not elem.tail.strip()):
|
|
||||||
elem.tail = i
|
|
||||||
|
|
||||||
def save(self, filename, encoding='utf-8'):
|
|
||||||
f = open(filename, 'w')
|
|
||||||
f.write(self.tostring())
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
def tostring(self, encoding='utf-8'):
|
|
||||||
builder = ElementTree.TreeBuilder()
|
|
||||||
self.build_tree(builder)
|
|
||||||
e = builder.close()
|
|
||||||
self._indent(e)
|
|
||||||
return ElementTree.tostring(e, encoding).decode(encoding)
|
|
||||||
|
|
||||||
class Text(Node):
|
|
||||||
"""Text Node
|
|
||||||
|
|
||||||
>>> p = Node("p")
|
|
||||||
>>> p.add_node("hello, world!")
|
|
||||||
>>> p.tostring()
|
|
||||||
'<p>hello, world!</p>'
|
|
||||||
"""
|
|
||||||
def __init__(self, text):
|
|
||||||
Node.__init__(self, "__text__")
|
|
||||||
self._text = text
|
|
||||||
|
|
||||||
def build_tree(self, builder):
|
|
||||||
builder.data(str(self._text))
|
|
||||||
|
|
||||||
class SVG(Node):
|
|
||||||
"""
|
|
||||||
>>> svg = SVG(width=200, height=200)
|
|
||||||
>>> svg.rect(x=0, y=0, width=200, height=200, fill="blue")
|
|
||||||
<rect .../>
|
|
||||||
>>> with svg.translate(-50, -50) as g:
|
|
||||||
... g.rect(x=0, y=0, width=50, height=100, fill="red")
|
|
||||||
... g.rect(x=50, y=0, width=50, height=100, fill="green")
|
|
||||||
<rect .../>
|
|
||||||
<rect .../>
|
|
||||||
>>> print(svg.tostring())
|
|
||||||
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect x="0" y="0" width="200" height="200" fill="blue" />
|
|
||||||
<g transform="translate(-50, -50)">
|
|
||||||
<rect x="0" y="0" width="50" height="100" fill="red" />
|
|
||||||
<rect x="50" y="0" width="50" height="100" fill="green" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
"""
|
|
||||||
def __init__(self, **attrs):
|
|
||||||
attrs['xmlns'] = "http://www.w3.org/2000/svg"
|
|
||||||
Node.__init__(self, 'svg', **attrs)
|
|
||||||
|
|
||||||
@@ -36,6 +36,8 @@ def find_macros(text):
|
|||||||
('Exercise', 'four-circles')
|
('Exercise', 'four-circles')
|
||||||
]
|
]
|
||||||
"""
|
"""
|
||||||
|
if not text:
|
||||||
|
return []
|
||||||
macros = re.findall(MACRO_RE, text)
|
macros = re.findall(MACRO_RE, text)
|
||||||
# remove the quotes around the argument
|
# remove the quotes around the argument
|
||||||
return [(name, _remove_quotes(arg)) for name, arg in macros]
|
return [(name, _remove_quotes(arg)) for name, arg in macros]
|
||||||
@@ -91,11 +93,11 @@ class MacroInlineProcessor(InlineProcessor):
|
|||||||
macro = m.group(1)
|
macro = m.group(1)
|
||||||
arg = m.group(2)
|
arg = m.group(2)
|
||||||
html = render_macro(macro, arg)
|
html = render_macro(macro, arg)
|
||||||
html = sanitize_html(str(html))
|
html = sanitize_html(str(html), macro)
|
||||||
e = etree.fromstring(html)
|
e = etree.fromstring(html)
|
||||||
return e, m.start(0), m.end(0)
|
return e, m.start(0), m.end(0)
|
||||||
|
|
||||||
def sanitize_html(html):
|
def sanitize_html(html, macro):
|
||||||
"""Sanotize the html using BeautifulSoup.
|
"""Sanotize the html using BeautifulSoup.
|
||||||
|
|
||||||
The markdown processor request the correct markup and crashes on
|
The markdown processor request the correct markup and crashes on
|
||||||
@@ -104,4 +106,7 @@ def sanitize_html(html):
|
|||||||
"""
|
"""
|
||||||
soup = BeautifulSoup(html, features="lxml")
|
soup = BeautifulSoup(html, features="lxml")
|
||||||
nodes = soup.body.children
|
nodes = soup.body.children
|
||||||
return "<div>" + "\n".join(str(node) for node in nodes) + "</div>"
|
classname = ""
|
||||||
|
if macro == "YouTubeVideo":
|
||||||
|
classname = "lesson-video"
|
||||||
|
return "<div class='" + classname + "'>" + "\n".join(str(node) for node in nodes) + "</div>"
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
"""Handy module to make access to all doctypes from a single place.
|
"""Handy module to make access to all doctypes from a single place.
|
||||||
"""
|
"""
|
||||||
from .doctype.lms_course.lms_course import LMSCourse as Course
|
from .doctype.lms_course.lms_course import LMSCourse as Course
|
||||||
from .doctype.lms_sketch.lms_sketch import LMSSketch as Sketch
|
from .doctype.lms_batch_membership.lms_batch_membership import LMSBatchMembership as Membership
|
||||||
|
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
"""Utility to split the text in the topic into multiple sections.
|
|
||||||
|
|
||||||
{{ section(type="example", id="foo") }}
|
|
||||||
circle(100, 100, 50)
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
from dataclasses import dataclass
|
|
||||||
import re
|
|
||||||
from typing import List, Tuple, Dict, Iterator
|
|
||||||
|
|
||||||
RE_SECTION = re.compile(r"^\{\{\s(\w+)\s*(?:\((.*)\))?\s*\}\}\s*")
|
|
||||||
class SectionParser:
|
|
||||||
def parse(self, text: str) -> Iterator[Section]:
|
|
||||||
"""Parses given text into sections and return an iterator over sections.
|
|
||||||
"""
|
|
||||||
lines = text.splitlines()
|
|
||||||
marked_lines = self.parse_lines(lines)
|
|
||||||
return self.group_sections(marked_lines)
|
|
||||||
|
|
||||||
def parse_lines(self, lines: List[str]) -> List[Tuple[str, str, str]]:
|
|
||||||
for line in lines:
|
|
||||||
m = RE_SECTION.match(line)
|
|
||||||
if m:
|
|
||||||
yield m.group(1), self.parse_attrs(m.group(2)), None
|
|
||||||
else:
|
|
||||||
yield None, None, line
|
|
||||||
|
|
||||||
def parse_attrs(self, attrs_str: str) -> Dict[str, str]:
|
|
||||||
# XXX-Anand: Hack
|
|
||||||
code = "dict({})".format(attrs_str or "")
|
|
||||||
return eval(code)
|
|
||||||
|
|
||||||
def group_sections(self, marked_lines) -> Iterator[Section]:
|
|
||||||
index = 0
|
|
||||||
|
|
||||||
def make_section(type='text', id=None, label=None, **attrs):
|
|
||||||
nonlocal index
|
|
||||||
index += 1
|
|
||||||
|
|
||||||
id = id or f"section-{index}"
|
|
||||||
label = label or id
|
|
||||||
return Section(
|
|
||||||
type=type,
|
|
||||||
id=id,
|
|
||||||
label=label,
|
|
||||||
attrs=attrs)
|
|
||||||
|
|
||||||
section = make_section("text")
|
|
||||||
|
|
||||||
for mark, attrs, line in marked_lines:
|
|
||||||
if not mark:
|
|
||||||
section.append(line)
|
|
||||||
continue
|
|
||||||
|
|
||||||
yield section
|
|
||||||
|
|
||||||
if mark == 'end':
|
|
||||||
section = make_section(type='text')
|
|
||||||
else:
|
|
||||||
section = make_section(**attrs)
|
|
||||||
|
|
||||||
yield section
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Section:
|
|
||||||
"""One section of the Topic.
|
|
||||||
"""
|
|
||||||
type: str
|
|
||||||
id: str
|
|
||||||
label: str
|
|
||||||
contents: str = ""
|
|
||||||
attrs: dict = None
|
|
||||||
|
|
||||||
def append(self, line):
|
|
||||||
if not line.endswith("\n"):
|
|
||||||
line = line + "\n"
|
|
||||||
self.contents += line
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
attrs = dict(type=self.type, id=self.id, label=self.label, **self.attrs)
|
|
||||||
attrs_str = ", ".join(f'{k}="{v}"' for k, v in attrs.items())
|
|
||||||
return f'<Section({attrs_str})>'
|
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
"apply_document_permissions": 0,
|
"apply_document_permissions": 0,
|
||||||
"button_label": "Save",
|
"button_label": "Save",
|
||||||
"creation": "2021-04-20 11:37:49.135114",
|
"creation": "2021-04-20 11:37:49.135114",
|
||||||
"custom_css": ".datepicker.active {\n background-color: white;\n}",
|
"custom_css": ".datepicker.active {\n background-color: white;\n}\n\n[data-doctype=\"Web Form\"] {\n max-width: 720px;\n margin: 6rem auto;\n}",
|
||||||
"doc_type": "LMS Batch",
|
"doc_type": "LMS Batch",
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Web Form",
|
"doctype": "Web Form",
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
"is_standard": 1,
|
"is_standard": 1,
|
||||||
"login_required": 1,
|
"login_required": 1,
|
||||||
"max_attachment_size": 0,
|
"max_attachment_size": 0,
|
||||||
"modified": "2021-06-02 15:52:06.383260",
|
"modified": "2021-06-15 18:49:50.530001",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "add-a-new-batch",
|
"name": "add-a-new-batch",
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
{
|
{
|
||||||
"allow_read_on_all_link_options": 0,
|
"allow_read_on_all_link_options": 0,
|
||||||
"fieldname": "course",
|
"fieldname": "course",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Data",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Course",
|
"label": "Course",
|
||||||
"max_length": 0,
|
"max_length": 0,
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
frappe.ready(function() {
|
|
||||||
// bind events here
|
|
||||||
})
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
{
|
|
||||||
"accept_payment": 0,
|
|
||||||
"allow_comments": 0,
|
|
||||||
"allow_delete": 0,
|
|
||||||
"allow_edit": 0,
|
|
||||||
"allow_incomplete": 0,
|
|
||||||
"allow_multiple": 0,
|
|
||||||
"allow_print": 0,
|
|
||||||
"amount": 0.0,
|
|
||||||
"amount_based_on_field": 0,
|
|
||||||
"apply_document_permissions": 0,
|
|
||||||
"button_label": "Save",
|
|
||||||
"creation": "2021-04-15 13:32:14.171328",
|
|
||||||
"doc_type": "LMS Batch Membership",
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Web Form",
|
|
||||||
"idx": 0,
|
|
||||||
"is_standard": 1,
|
|
||||||
"login_required": 1,
|
|
||||||
"max_attachment_size": 0,
|
|
||||||
"modified": "2021-04-15 13:32:14.171328",
|
|
||||||
"modified_by": "Administrator",
|
|
||||||
"module": "LMS",
|
|
||||||
"name": "join-a-batch",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"payment_button_label": "Buy Now",
|
|
||||||
"published": 1,
|
|
||||||
"route": "join-a-batch",
|
|
||||||
"route_to_success_link": 0,
|
|
||||||
"show_attachments": 0,
|
|
||||||
"show_in_grid": 0,
|
|
||||||
"show_sidebar": 0,
|
|
||||||
"sidebar_items": [],
|
|
||||||
"success_url": "/join-a-batch",
|
|
||||||
"title": "Join a Batch",
|
|
||||||
"web_form_fields": [
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldtype": "Attach",
|
|
||||||
"hidden": 0,
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"show_in_filter": 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
0
community/lms/web_form/profile/__init__.py
Normal file
7
community/lms/web_form/profile/profile.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
frappe.ready(function () {
|
||||||
|
frappe.web_form.after_load = () => {
|
||||||
|
if (!frappe.utils.get_url_arg("name")) {
|
||||||
|
window.location.href = `/edit-profile?name=${frappe.session.user}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
199
community/lms/web_form/profile/profile.json
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
{
|
||||||
|
"accept_payment": 0,
|
||||||
|
"allow_comments": 0,
|
||||||
|
"allow_delete": 0,
|
||||||
|
"allow_edit": 1,
|
||||||
|
"allow_incomplete": 0,
|
||||||
|
"allow_multiple": 0,
|
||||||
|
"allow_print": 0,
|
||||||
|
"amount": 0.0,
|
||||||
|
"amount_based_on_field": 0,
|
||||||
|
"apply_document_permissions": 0,
|
||||||
|
"breadcrumbs": "",
|
||||||
|
"button_label": "Save",
|
||||||
|
"creation": "2021-06-30 13:48:13.682851",
|
||||||
|
"custom_css": "[data-doctype=\"Web Form\"] {\n max-width: 720px;\n margin: 6rem auto;\n}",
|
||||||
|
"doc_type": "User",
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Web Form",
|
||||||
|
"idx": 0,
|
||||||
|
"is_standard": 1,
|
||||||
|
"login_required": 1,
|
||||||
|
"max_attachment_size": 0,
|
||||||
|
"modified": "2021-07-14 17:15:15.424855",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "LMS",
|
||||||
|
"name": "profile",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"payment_button_label": "Buy Now",
|
||||||
|
"published": 1,
|
||||||
|
"route": "edit-profile",
|
||||||
|
"route_to_success_link": 0,
|
||||||
|
"show_attachments": 0,
|
||||||
|
"show_in_grid": 0,
|
||||||
|
"show_sidebar": 0,
|
||||||
|
"sidebar_items": [],
|
||||||
|
"success_url": "/profile",
|
||||||
|
"title": "Profile",
|
||||||
|
"web_form_fields": [
|
||||||
|
{
|
||||||
|
"allow_read_on_all_link_options": 0,
|
||||||
|
"fieldname": "first_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 0,
|
||||||
|
"label": "First Name",
|
||||||
|
"max_length": 0,
|
||||||
|
"max_value": 0,
|
||||||
|
"read_only": 0,
|
||||||
|
"reqd": 1,
|
||||||
|
"show_in_filter": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_read_on_all_link_options": 0,
|
||||||
|
"fieldname": "middle_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 0,
|
||||||
|
"label": "Middle Name (Optional)",
|
||||||
|
"max_length": 0,
|
||||||
|
"max_value": 0,
|
||||||
|
"read_only": 0,
|
||||||
|
"reqd": 0,
|
||||||
|
"show_in_filter": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_read_on_all_link_options": 0,
|
||||||
|
"fieldname": "last_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 0,
|
||||||
|
"label": "Last Name",
|
||||||
|
"max_length": 0,
|
||||||
|
"max_value": 0,
|
||||||
|
"read_only": 0,
|
||||||
|
"reqd": 0,
|
||||||
|
"show_in_filter": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_read_on_all_link_options": 0,
|
||||||
|
"description": "Get your globally recognized avatar from Gravatar.com",
|
||||||
|
"fieldname": "user_image",
|
||||||
|
"fieldtype": "Attach Image",
|
||||||
|
"hidden": 0,
|
||||||
|
"label": "User Image",
|
||||||
|
"max_length": 0,
|
||||||
|
"max_value": 0,
|
||||||
|
"read_only": 0,
|
||||||
|
"reqd": 0,
|
||||||
|
"show_in_filter": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_read_on_all_link_options": 0,
|
||||||
|
"fieldname": "mobile_no",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 0,
|
||||||
|
"label": "Mobile No",
|
||||||
|
"max_length": 0,
|
||||||
|
"max_value": 0,
|
||||||
|
"options": "Phone",
|
||||||
|
"read_only": 0,
|
||||||
|
"reqd": 0,
|
||||||
|
"show_in_filter": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_read_on_all_link_options": 0,
|
||||||
|
"fieldname": "bio",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"hidden": 0,
|
||||||
|
"label": "Bio",
|
||||||
|
"max_length": 0,
|
||||||
|
"max_value": 0,
|
||||||
|
"read_only": 0,
|
||||||
|
"reqd": 0,
|
||||||
|
"show_in_filter": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_read_on_all_link_options": 0,
|
||||||
|
"fieldname": "linkedin",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 0,
|
||||||
|
"label": "LinkedIn ID",
|
||||||
|
"max_length": 0,
|
||||||
|
"max_value": 0,
|
||||||
|
"read_only": 0,
|
||||||
|
"reqd": 0,
|
||||||
|
"show_in_filter": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_read_on_all_link_options": 0,
|
||||||
|
"fieldname": "github",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 0,
|
||||||
|
"label": "Github ID",
|
||||||
|
"max_length": 0,
|
||||||
|
"max_value": 0,
|
||||||
|
"read_only": 0,
|
||||||
|
"reqd": 0,
|
||||||
|
"show_in_filter": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_read_on_all_link_options": 0,
|
||||||
|
"fieldname": "medium",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 0,
|
||||||
|
"label": "Medium ID",
|
||||||
|
"max_length": 0,
|
||||||
|
"max_value": 0,
|
||||||
|
"read_only": 0,
|
||||||
|
"reqd": 0,
|
||||||
|
"show_in_filter": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_read_on_all_link_options": 0,
|
||||||
|
"fieldname": "city",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 0,
|
||||||
|
"label": "City",
|
||||||
|
"max_length": 0,
|
||||||
|
"max_value": 0,
|
||||||
|
"read_only": 0,
|
||||||
|
"reqd": 0,
|
||||||
|
"show_in_filter": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_read_on_all_link_options": 0,
|
||||||
|
"fieldname": "college",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 0,
|
||||||
|
"label": "College Name",
|
||||||
|
"max_length": 0,
|
||||||
|
"max_value": 0,
|
||||||
|
"read_only": 0,
|
||||||
|
"reqd": 0,
|
||||||
|
"show_in_filter": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_read_on_all_link_options": 0,
|
||||||
|
"depends_on": "college",
|
||||||
|
"fieldname": "branch",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 0,
|
||||||
|
"label": "Branch",
|
||||||
|
"max_length": 0,
|
||||||
|
"max_value": 0,
|
||||||
|
"read_only": 0,
|
||||||
|
"reqd": 0,
|
||||||
|
"show_in_filter": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_read_on_all_link_options": 0,
|
||||||
|
"fieldname": "profession",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 0,
|
||||||
|
"label": "Profession",
|
||||||
|
"max_length": 0,
|
||||||
|
"max_value": 0,
|
||||||
|
"read_only": 0,
|
||||||
|
"reqd": 0,
|
||||||
|
"show_in_filter": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
|
||||||
def get_context(context):
|
def get_context(context):
|
||||||
@@ -1,34 +1,61 @@
|
|||||||
<div class="mt-5">
|
<div class="mt-5">
|
||||||
<a class="anchor_style" href="/courses">Courses</a> /{% if course.is_mentor(frappe.session.user) %} <a class="anchor_style" href="/courses/{{ course.name }}"> {{ course.title }}</a> {% else %} <span class="text-muted"> {{ course.title }}</span> {% endif %}
|
<a class="anchor_style" href="/courses">Courses</a> /{% if course.is_mentor(frappe.session.user) %} <a
|
||||||
|
class="anchor_style" href="/courses/{{ course.name }}"> {{ course.title }}</a> {% else %} <span class="text-muted">
|
||||||
|
{{ course.title }}</span> {% endif %}
|
||||||
|
{% set all_memberships = course.get_all_memberships(frappe.session.user) %}
|
||||||
|
{% if membership and membership.batch and all_memberships | length > 1 %}
|
||||||
|
<a class="pull-right dropdown-item border rounded" style="width: 10rem;" href="#" id="navbarDropdown" role="button"
|
||||||
|
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
|
{{ membership.batch_title }}
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
|
||||||
|
{% for data in all_memberships %}
|
||||||
|
{% if data.batch != membership.batch %}
|
||||||
|
<a class="dropdown-item switch-batch"
|
||||||
|
href="/courses/{{ course.name }}/home?batch={{ data.batch }}">{{ data.batch_title }}</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if not membership %}
|
||||||
|
{% set display_class = "hide" %}
|
||||||
|
{% else %}
|
||||||
|
{% set display_class = "" %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<ul class="nav nav-tabs mt-4">
|
<ul class="nav nav-tabs mt-4">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" id="home" href="/courses/{{course.name}}/{{batch.name}}/home">Home</a>
|
<a class="nav-link" id="home" href="/courses/{{course.name}}/home{{ course.query_parameter }}">Home</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" id="learn" href="/courses/{{course.name}}/{{batch.name}}/learn">Learn</a>
|
{% set lesson_index = course.get_lesson_index(membership.current_lesson) if membership and membership.current_lesson
|
||||||
|
else '1.1' %}
|
||||||
|
<a class="nav-link" id="learn"
|
||||||
|
href="{{ course.get_learn_url(lesson_index) }}{{ course.query_parameter }}">Lessons</a>
|
||||||
</li>
|
</li>
|
||||||
<!-- <li class="nav-item">
|
<!-- <li class="nav-item">
|
||||||
<a class="nav-link" id="schedule" href="/courses/{{course.name}}/{{batch.name}}/schedule">Schedule</a>
|
<a class="nav-link" id="schedule" href="/courses/{{course.name}}/schedule">Schedule</a>
|
||||||
</li> -->
|
</li> -->
|
||||||
<li class="nav-item">
|
<li class="nav-item {{ display_class }}">
|
||||||
<a class="nav-link" id="members" href="/courses/{{course.name}}/{{batch.name}}/members">Members</a>
|
<a class="nav-link" id="members" href="/courses/{{course.name}}/members{{ course.query_parameter }}">Members</a>
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" id="discussion" href="/courses/{{course.name}}/{{batch.name}}/discuss">Discussion</a>
|
|
||||||
</li>
|
</li>
|
||||||
|
<!-- <li class="nav-item {{ display_class }}">
|
||||||
|
<a class="nav-link" id="discussion" href="/courses/{{course.name}}/discuss">Discussion</a>
|
||||||
|
</li> -->
|
||||||
<!-- <li class="nav-item">
|
<!-- <li class="nav-item">
|
||||||
<a class="nav-link" id="about" href="/courses/{{course.name}}/{{batch.name}}/about">About</a>
|
<a class="nav-link" id="about" href="/courses/{{course.name}}/about">About</a>
|
||||||
</li> -->
|
</li> -->
|
||||||
{% if batch.is_member(frappe.session.user, member_type="Mentor") %}
|
{% if membership and membership.batch and course.is_mentor(frappe.session.user) %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" id="progress" href="/courses/{{course.name}}/{{batch.name}}/progress">Progress</a>
|
<a class="nav-link" id="progress" href="/courses/{{course.name}}/progress{{ course.query_parameter }}">Progress</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
{% block script %}
|
||||||
<script>
|
<script>
|
||||||
frappe.ready(() => {
|
frappe.ready(() => {
|
||||||
var selector = document.querySelector(`a[href="${decodeURIComponent(window.location.pathname)}"]`)
|
var selector = document.querySelector(`a[href="${decodeURIComponent(window.location.pathname)}{{ course.query_parameter }}"]`)
|
||||||
if (selector) {
|
if (selector) {
|
||||||
selector.classList.add('active');
|
selector.classList.add('active');
|
||||||
}
|
}
|
||||||
@@ -37,3 +64,4 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
19
community/lms/widgets/BreadCrumb.html
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<div class="breadcrumb">
|
||||||
|
<a class="dark-links" href="/courses">All Courses</a>
|
||||||
|
<img class="ml-1 mr-1" src="/assets/community/icons/chevron-right.svg">
|
||||||
|
|
||||||
|
{% if course %}
|
||||||
|
{% if lesson %}
|
||||||
|
<a class="dark-links" href="/courses/{{ course.name }}">{{ course.title }}</a>
|
||||||
|
<img class="ml-1 mr-1" src="/assets/community/icons/chevron-right.svg">
|
||||||
|
<span class="muted-text"> {{ lesson.title }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="muted-text">{{ course.title }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if member_name %}
|
||||||
|
<span class="muted-text">{{ member_name }}</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
@@ -1,18 +1,98 @@
|
|||||||
<div class="chapter-teaser">
|
<div>
|
||||||
<div class="teaser-body">
|
|
||||||
<h3 class="chapter-title"><span class="mr-1">{{index}}.</span> {{ chapter.title }}</h3>
|
<div class="small-title chapter-title" data-target="#{{ chapter.get_slugified_chapter_title() }}"
|
||||||
<div class="chapter-description">
|
data-toggle="collapse" aria-expanded="false">
|
||||||
{{ chapter.description or "" }}
|
<img class="chapter-icon" src="/assets/community/icons/chevron-right.svg">
|
||||||
|
{{ index }}. {{ chapter.title }}
|
||||||
</div>
|
</div>
|
||||||
<div class="chapter-lessons">
|
|
||||||
|
<div class="chapter-content collapse navbar-collapse" id="{{ chapter.get_slugified_chapter_title() }}">
|
||||||
|
|
||||||
|
<div class="chapter-description muted-text">
|
||||||
|
{{ chapter.description }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lessons">
|
||||||
|
|
||||||
{% for lesson in chapter.get_lessons() %}
|
{% for lesson in chapter.get_lessons() %}
|
||||||
<div class="lesson-teaser">
|
|
||||||
<a {% if show_link %} class="anchor_style" href="{{ batch.get_learn_url(course.get_lesson_index(lesson.name)) }}" {% endif %}>{{ lesson.title }}</a>
|
<div class="lesson-info {% if membership.current_lesson == lesson.name %} active-lesson {% endif %}">
|
||||||
{% if show_progress and not course.is_mentor(frappe.session.user) and lesson.get_progress() %}
|
|
||||||
<a class="pull-right badge p-1 {{ lesson.get_slugified_class() }}"> <img class="progress-image" src="/assets/community/images/Vector.png"> {{ lesson.get_progress() }}</a>
|
{% if membership or lesson.include_in_preview %}
|
||||||
|
<a class="lesson-links"
|
||||||
|
href="{{ course.get_learn_url(course.get_lesson_index(lesson.name)) }}{{course.query_parameter}}"
|
||||||
|
data-course="{{ course.name }}">
|
||||||
|
{{ lesson.title }}
|
||||||
|
|
||||||
|
{% if membership %}
|
||||||
|
<img class="lesson-progress-tick {{ course.get_progress(lesson.name) != 'Complete' and 'hide' }}"
|
||||||
|
src="/assets/community/icons/check.svg">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
<div class="no-preview" title="This lesson is not available for preview">
|
||||||
|
<div class="lesson-links">
|
||||||
|
{{ lesson.title }}
|
||||||
|
<img class="ml-2" src="/assets/community/icons/lock.svg">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if index != course.get_chapters() | length %}
|
||||||
|
<div class="card-divider"></div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
frappe.ready(() => {
|
||||||
|
expand_the_active_chapter();
|
||||||
|
})
|
||||||
|
|
||||||
|
var expand_the_first_chapter = () => {
|
||||||
|
var elements = $(".collapse");
|
||||||
|
elements.each((i, element) => {
|
||||||
|
if (i <= 1) {
|
||||||
|
show_section(element);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var expand_the_active_chapter = () => {
|
||||||
|
|
||||||
|
/* Find anchor matching the URL for course details page */
|
||||||
|
var selector = $(`a[href="${decodeURIComponent(window.location.pathname)}"]`).parent();
|
||||||
|
if (!selector.length) {
|
||||||
|
selector = $(`a[href^="${decodeURIComponent(window.location.pathname)}"]`).parent();
|
||||||
|
}
|
||||||
|
if (selector.length && $(".course-details-page").length) {
|
||||||
|
$(".lesson-info").removeClass("active-lesson")
|
||||||
|
selector.addClass("active-lesson");
|
||||||
|
show_section(selector.parent().parent());
|
||||||
|
}
|
||||||
|
|
||||||
|
/* For course home page */
|
||||||
|
else if ($(".active-lesson").length) {
|
||||||
|
selector = $(".active-lesson")
|
||||||
|
show_section(selector.parent().parent());
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If no active chapter then exapand the first chapter */
|
||||||
|
else {
|
||||||
|
expand_the_first_chapter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var show_section = (element) => {
|
||||||
|
$(element).addClass("show");
|
||||||
|
$(element).siblings(".chapter-title").children(".chapter-icon").css("transform", "rotate(90deg)");
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|||||||
82
community/lms/widgets/CourseCard.html
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<div class="common-card-style course-card">
|
||||||
|
<div class="course-image" style="background-image: url({{ course.image }});">
|
||||||
|
<div class="course-tags">
|
||||||
|
{% for tag in course.get_tags() %}
|
||||||
|
<div class="course-card-pills">{{ tag }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="course-card-content">
|
||||||
|
<div class="course-card-meta muted-text">
|
||||||
|
{% if course.get_chapters() | length %}
|
||||||
|
<span>
|
||||||
|
{{ course.get_chapters() | length }} Chapters
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if course.get_chapters() | length and course.get_upcoming_batches() | length %}
|
||||||
|
<span class="font-weight-bold ml-3 mr-3"> . </span>
|
||||||
|
{% endif %}
|
||||||
|
{% if course.get_upcoming_batches() | length %}
|
||||||
|
<span class="">
|
||||||
|
{{ course.get_upcoming_batches() | length }} Open Batches
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="course-card-title">{{ course.title }}</div>
|
||||||
|
<div class="card-divider"></div>
|
||||||
|
<div class="course-card-meta-2">
|
||||||
|
{{ widgets.Avatar(member=course.get_instructor(), avatar_class="avatar-small") }}
|
||||||
|
<span class="course-instructor">
|
||||||
|
{{ course.get_instructor().full_name }}
|
||||||
|
</span>
|
||||||
|
<span class="course-student-count">
|
||||||
|
{% if course.get_students() | length %}
|
||||||
|
<span class="mr-4">
|
||||||
|
<img class="icon-background" src="/assets/community/icons/user.svg" />
|
||||||
|
{{ course.get_students() | length }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% set avg_rating = course.get_average_rating() %}
|
||||||
|
{% if avg_rating %}
|
||||||
|
<span>
|
||||||
|
<img class="icon-background" src="/assets/community/icons/rating.svg" />
|
||||||
|
{{ avg_rating }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% set membership = course.get_membership(frappe.session.user) %}
|
||||||
|
|
||||||
|
{% set lesson_index = course.get_lesson_index(membership.current_lesson) if membership and
|
||||||
|
membership.current_lesson
|
||||||
|
else '1.1' %}
|
||||||
|
|
||||||
|
{% set query_parameter = "?batch=" + membership.batch if membership and membership.batch else "" %}
|
||||||
|
|
||||||
|
{% if membership %}
|
||||||
|
|
||||||
|
<div class="view-course-link is-primary">
|
||||||
|
Continue Course <img class="ml-3" src="/assets/community/icons/white-arrow.svg" />
|
||||||
|
</div>
|
||||||
|
<a class="stretched-link" href="{{ course.get_learn_url(lesson_index) }}{{ query_parameter }}"></a>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
<div class="view-course-link">
|
||||||
|
View Course <img class="ml-3" src="/assets/community/icons/black-arrow.svg" />
|
||||||
|
</div>
|
||||||
|
<a class="stretched-link" href="/courses/{{ course.name }}"></a>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
frappe.ready(() => {
|
||||||
|
$(".course-card-title").each((i, element) => {
|
||||||
|
var title = $(element).text();
|
||||||
|
var length = $(window).width() <= 375 ? 60 : 65;
|
||||||
|
var suffix = title.length > length ? "..." : "";
|
||||||
|
$(element).text(title.substring(0, length) + suffix);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -1,5 +1,12 @@
|
|||||||
<h2>Course Outline</h2>
|
{% if course.get_chapters() | length %}
|
||||||
|
<div class="">
|
||||||
|
<div class="course-home-headings">
|
||||||
|
Course Outline
|
||||||
|
</div>
|
||||||
|
<div class="coure-outline">
|
||||||
{% for chapter in course.get_chapters() %}
|
{% for chapter in course.get_chapters() %}
|
||||||
{{ widgets.ChapterTeaser(index=loop.index, chapter=chapter, course=course, batch=batch, show_link=show_link, show_progress=show_progress)}}
|
{{ widgets.ChapterTeaser(index=loop.index, chapter=chapter, course=course, batch=batch, membership=membership) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<h3>Instructor</h3>
|
|
||||||
<div class="instructor">
|
<div class="instructor">
|
||||||
<div class="instructor-title">{{instructor.full_name}}</div>
|
{{ widgets.Avatar(member=instructor, avatar_class="avatar-medium") }}
|
||||||
<div class="instructor-subtitle">Created {{instructor.get_course_count()}} courses</div>
|
<a class="ml-1 instructor-title" href="/{{instructor.username}}">{{ instructor.full_name }}</a>
|
||||||
|
<div class="instructor-subtitle">Course Creator</div>
|
||||||
|
<!-- <div class="instructor-subtitle">Created {{instructor.get_course_count()}} courses</div> -->
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
15
community/lms/widgets/MemberCard.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<div class="common-card-style member-card {{dimension_class}} ">
|
||||||
|
{% set avatar_class = "avatar-large" if not dimension_class else "avatar-large"%}
|
||||||
|
{{ widgets.Avatar(member=member, avatar_class=avatar_class) }}
|
||||||
|
<div class="small-title member-card-title">
|
||||||
|
{{ member.full_name }}
|
||||||
|
</div>
|
||||||
|
{% set course_count = member.get_authored_courses() | length %}
|
||||||
|
{% if show_course_count and course_count > 0 %}
|
||||||
|
{% set suffix = "Courses" if course_count > 1 else "Course" %}
|
||||||
|
<div class="small-title">
|
||||||
|
Created {{ course_count }} {{ suffix }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<a class="stretched-link" href="/{{ member.username }}"></a>
|
||||||
|
</div>
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
<div class="batch">
|
<div class="batch">
|
||||||
<div class="batch-details">
|
<div class="batch-details">
|
||||||
<div>Session every {{batch.sessions_on}}</div>
|
<div class="">Session every {{batch.sessions_on}}</div>
|
||||||
<div>{{frappe.utils.format_time(batch.start_time, "short")}} -
|
<div>{{frappe.utils.format_time(batch.start_time, "short")}} -
|
||||||
{{frappe.utils.format_time(batch.end_time, "short")}}
|
{{frappe.utils.format_time(batch.end_time, "short")}}
|
||||||
</div>
|
</div>
|
||||||
<div>Starting {{frappe.utils.format_date(batch.start_date, "medium")}}</div>
|
<div>Starting {{frappe.utils.format_date(batch.start_date, "medium")}}</div>
|
||||||
<div class="course-type" style="color: #888; padding: 10px 0px;">mentors</div>
|
<div class="course-type" style="color: #888; padding: 10px 0px;">mentors</div>
|
||||||
|
|
||||||
{% for m in batch.get_mentors() %}
|
{% for m in course.get_mentors(batch.name) %}
|
||||||
<div>
|
<div>
|
||||||
{{ widgets.Avatar(member=m, avatar_class="avatar-medium" ) }}
|
{{ widgets.Avatar(member=m, avatar_class="avatar-medium" ) }}
|
||||||
<span class="instructor-title">{{m.full_name}}</span>
|
<span class="instructor-title">{{m.full_name}}</span>
|
||||||
@@ -18,9 +18,10 @@
|
|||||||
<div class="cta">
|
<div class="cta">
|
||||||
<div class="">
|
<div class="">
|
||||||
{% if can_manage %}
|
{% if can_manage %}
|
||||||
<a href="/courses/{{course.name}}/{{batch.name}}/home" class="btn btn-primary">Manage</a>
|
<a href="/courses/{{ course.name }}/home?batch={{ batch.name }}" class="btn btn-primary manage-batch" data-batch="{{ batch.name | urlencode }}"
|
||||||
|
data-course="{{ course.name | urlencode }}">Manage</a>
|
||||||
{% elif can_join %}
|
{% elif can_join %}
|
||||||
<button class="join-batch btn btn-primary" data-batch="{{ batch.name | urlencode }}"
|
<button class="join-batch btn btn-secondary" data-batch="{{ batch.name | urlencode }}"
|
||||||
data-course="{{ course.name | urlencode }}">Join this Batch</button>
|
data-course="{{ course.name | urlencode }}">Join this Batch</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
85
community/lms/widgets/Reviews.html
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
{% if course.get_reviews() | length %}
|
||||||
|
<div class="reviews-parent">
|
||||||
|
<div class="reviews-heading">
|
||||||
|
<div class="course-home-headings">Student Review</div>
|
||||||
|
{% if course.is_eligible_to_review(membership) %}
|
||||||
|
<a class="review-link" href="">
|
||||||
|
Provide your Feedback
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="reviews-section">
|
||||||
|
{% for review in course.get_reviews() %}
|
||||||
|
<div class="review-card">
|
||||||
|
<div class="common-card-style review-content small-title"> {{ review.review }} </div>
|
||||||
|
<div class="review-card-footer">
|
||||||
|
<div>
|
||||||
|
{{ widgets.Avatar(member=review.owner_details, avatar_class="avatar-medium") }}
|
||||||
|
<span class="course-instructor">
|
||||||
|
{{ review.owner_details.full_name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="rating">
|
||||||
|
{% for i in [1, 2, 3, 4, 5] %}
|
||||||
|
<svg class="icon icon-md {% if i <= review.rating %} star-click {% endif %}" data-rating="{{ i }}">
|
||||||
|
<use href="#icon-star"></use>
|
||||||
|
</svg>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade review-modal" id="review-modal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel"
|
||||||
|
aria-hidden="true">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="course-home-headings modal-headings">Review</div>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form class="review-form" id="review-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="clearfix">
|
||||||
|
<label class="control-label reqd" style="padding-right: 0px;">Rating</label>
|
||||||
|
</div>
|
||||||
|
<div class="control-input-wrapper">
|
||||||
|
<div class="control-input">
|
||||||
|
<div class="rating rating-field" id="rating">
|
||||||
|
{% for i in [1, 2, 3, 4, 5] %}
|
||||||
|
<svg class="icon icon-md icon-rating" data-rating="{{ i }}">
|
||||||
|
<use href="#icon-star"></use>
|
||||||
|
</svg>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="clearfix">
|
||||||
|
<label class="control-label reqd" style="padding-right: 0px;">Review</label>
|
||||||
|
</div>
|
||||||
|
<div class="control-input-wrapper">
|
||||||
|
<div class="control-input">
|
||||||
|
<textarea type="text" autocomplete="off" class="input-with-feedback form-control review-field" data-fieldtype="Text"
|
||||||
|
data-fieldname="feedback_comments" placeholder="" style="height: 300px;"
|
||||||
|
spellcheck="false"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="error-field muted-text"></p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<div class="button submit-review is-primary" data-course="{{ course.name | urlencode}}" id="submit-review">Submit</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
160
community/lms/workspace/lms/lms.json
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
{
|
||||||
|
"category": "Modules",
|
||||||
|
"charts": [],
|
||||||
|
"creation": "2021-06-29 13:05:28.741459",
|
||||||
|
"developer_mode_only": 0,
|
||||||
|
"disable_user_customization": 0,
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Workspace",
|
||||||
|
"extends_another_page": 0,
|
||||||
|
"hide_custom": 0,
|
||||||
|
"icon": "education",
|
||||||
|
"idx": 0,
|
||||||
|
"is_default": 0,
|
||||||
|
"is_standard": 1,
|
||||||
|
"label": "LMS",
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"hidden": 0,
|
||||||
|
"is_query_report": 0,
|
||||||
|
"label": "LMS",
|
||||||
|
"link_type": "DocType",
|
||||||
|
"onboard": 0,
|
||||||
|
"type": "Card Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": 0,
|
||||||
|
"is_query_report": 0,
|
||||||
|
"label": "LMS Course",
|
||||||
|
"link_to": "LMS Course",
|
||||||
|
"link_type": "DocType",
|
||||||
|
"onboard": 0,
|
||||||
|
"type": "Link"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": 0,
|
||||||
|
"is_query_report": 0,
|
||||||
|
"label": "LMS Batch",
|
||||||
|
"link_to": "LMS Batch",
|
||||||
|
"link_type": "DocType",
|
||||||
|
"onboard": 0,
|
||||||
|
"type": "Link"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": 0,
|
||||||
|
"is_query_report": 0,
|
||||||
|
"label": "LMS Batch Membership",
|
||||||
|
"link_to": "LMS Batch Membership",
|
||||||
|
"link_type": "DocType",
|
||||||
|
"onboard": 0,
|
||||||
|
"type": "Link"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": 0,
|
||||||
|
"is_query_report": 0,
|
||||||
|
"label": "LMS Course Mentor Mapping",
|
||||||
|
"link_to": "LMS Course Mentor Mapping",
|
||||||
|
"link_type": "DocType",
|
||||||
|
"onboard": 0,
|
||||||
|
"type": "Link"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": 0,
|
||||||
|
"is_query_report": 0,
|
||||||
|
"label": "Content",
|
||||||
|
"link_type": "DocType",
|
||||||
|
"onboard": 0,
|
||||||
|
"type": "Card Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": 0,
|
||||||
|
"is_query_report": 0,
|
||||||
|
"label": "Chapter",
|
||||||
|
"link_to": "Chapter",
|
||||||
|
"link_type": "DocType",
|
||||||
|
"onboard": 0,
|
||||||
|
"type": "Link"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": 0,
|
||||||
|
"is_query_report": 0,
|
||||||
|
"label": "Lesson",
|
||||||
|
"link_to": "Lesson",
|
||||||
|
"link_type": "DocType",
|
||||||
|
"onboard": 0,
|
||||||
|
"type": "Link"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": 0,
|
||||||
|
"is_query_report": 0,
|
||||||
|
"label": "Exercise",
|
||||||
|
"link_to": "Exercise",
|
||||||
|
"link_type": "DocType",
|
||||||
|
"onboard": 0,
|
||||||
|
"type": "Link"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": 0,
|
||||||
|
"is_query_report": 0,
|
||||||
|
"label": "Exercise Submission",
|
||||||
|
"link_to": "Exercise Submission",
|
||||||
|
"link_type": "DocType",
|
||||||
|
"onboard": 0,
|
||||||
|
"type": "Link"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": 0,
|
||||||
|
"is_query_report": 0,
|
||||||
|
"label": "LMS Quiz",
|
||||||
|
"link_to": "LMS Quiz",
|
||||||
|
"link_type": "DocType",
|
||||||
|
"onboard": 0,
|
||||||
|
"type": "Link"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": 0,
|
||||||
|
"is_query_report": 0,
|
||||||
|
"label": "LMS Quiz Submission",
|
||||||
|
"link_to": "LMS Quiz Submission",
|
||||||
|
"link_type": "DocType",
|
||||||
|
"onboard": 0,
|
||||||
|
"type": "Link"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"modified": "2021-06-29 15:11:07.324651",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "LMS",
|
||||||
|
"name": "LMS",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"pin_to_bottom": 0,
|
||||||
|
"pin_to_top": 1,
|
||||||
|
"shortcuts": [
|
||||||
|
{
|
||||||
|
"color": "#29CD42",
|
||||||
|
"doc_view": "List",
|
||||||
|
"format": "{} Published",
|
||||||
|
"label": "Courses",
|
||||||
|
"link_to": "LMS Course",
|
||||||
|
"stats_filter": "{\"is_published\": 1}",
|
||||||
|
"type": "DocType"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "#29CD42",
|
||||||
|
"doc_view": "List",
|
||||||
|
"format": "{} Active ",
|
||||||
|
"label": "Batches",
|
||||||
|
"link_to": "LMS Batch",
|
||||||
|
"stats_filter": "{\"status\": \"Active\"}",
|
||||||
|
"type": "DocType"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "#39E4A5",
|
||||||
|
"doc_view": "List",
|
||||||
|
"format": "{} Students",
|
||||||
|
"label": "Memberships",
|
||||||
|
"link_to": "LMS Batch Membership",
|
||||||
|
"stats_filter": "{\"member_type\": \"Student\"}",
|
||||||
|
"type": "DocType"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -5,15 +5,18 @@ import hashlib
|
|||||||
|
|
||||||
class CustomUser(User):
|
class CustomUser(User):
|
||||||
|
|
||||||
def get_course_count(self) -> int:
|
def get_authored_courses(self) -> int:
|
||||||
"""Returns the number of courses authored by this user.
|
"""Returns the number of courses authored by this user.
|
||||||
"""
|
"""
|
||||||
return frappe.db.count(
|
return frappe.get_all(
|
||||||
'LMS Course', {
|
'LMS Course', {
|
||||||
'owner': self.email
|
'owner': self.name
|
||||||
})
|
})
|
||||||
|
|
||||||
def get_palette(self):
|
def get_palette(self):
|
||||||
|
"""
|
||||||
|
Returns a color unique to each member for Avatar """
|
||||||
|
|
||||||
palette = [
|
palette = [
|
||||||
['--orange-avatar-bg', '--orange-avatar-color'],
|
['--orange-avatar-bg', '--orange-avatar-color'],
|
||||||
['--pink-avatar-bg', '--pink-avatar-color'],
|
['--pink-avatar-bg', '--pink-avatar-color'],
|
||||||
@@ -40,3 +43,28 @@ class CustomUser(User):
|
|||||||
'member_type': 'Mentor'
|
'member_type': 'Mentor'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def get_user_reviews(self):
|
||||||
|
""" Returns the reviews created by user """
|
||||||
|
return frappe.get_all("LMS Course Review",
|
||||||
|
{
|
||||||
|
"owner": self.name
|
||||||
|
})
|
||||||
|
|
||||||
|
def get_course_membership(self, member_type=None):
|
||||||
|
""" Returns all memberships of the user """
|
||||||
|
filters = {
|
||||||
|
"member": self.name
|
||||||
|
}
|
||||||
|
if member_type:
|
||||||
|
filters["member_type"] = member_type
|
||||||
|
|
||||||
|
return frappe.get_all("LMS Batch Membership", filters, ["name", "course"])
|
||||||
|
|
||||||
|
def get_mentored_courses(self):
|
||||||
|
""" Returns all courses mentored by this user """
|
||||||
|
return frappe.get_all("LMS Course Mentor Mapping",
|
||||||
|
{
|
||||||
|
"mentor": self.name
|
||||||
|
},
|
||||||
|
["name", "course"]
|
||||||
|
)
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ The PageExtension is used to load additinal stylesheets and scripts to
|
|||||||
be loaded in a webpage.
|
be loaded in a webpage.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
|
||||||
class PageExtension:
|
class PageExtension:
|
||||||
"""PageExtension is a plugin to inject custom styles and scripts
|
"""PageExtension is a plugin to inject custom styles and scripts
|
||||||
into a web page.
|
into a web page.
|
||||||
@@ -64,3 +66,43 @@ class ProfileTab:
|
|||||||
Every subclass must implement this.
|
Every subclass must implement this.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
class LiveCodeExtension(PageExtension):
|
||||||
|
def render_header(self):
|
||||||
|
livecode_url = frappe.get_value("LMS Settings", None, "livecode_url")
|
||||||
|
context = {
|
||||||
|
"livecode_url": livecode_url
|
||||||
|
}
|
||||||
|
return frappe.render_template(
|
||||||
|
"templates/livecode/extension_header.html",
|
||||||
|
context)
|
||||||
|
|
||||||
|
def render_footer(self):
|
||||||
|
livecode_url = frappe.get_value("LMS Settings", None, "livecode_url")
|
||||||
|
context = {
|
||||||
|
"livecode_url": livecode_url
|
||||||
|
}
|
||||||
|
return frappe.render_template(
|
||||||
|
"templates/livecode/extension_footer.html",
|
||||||
|
context)
|
||||||
|
|
||||||
|
def quiz_renderer(quiz_name):
|
||||||
|
quiz = frappe.get_doc("LMS Quiz", quiz_name)
|
||||||
|
context = dict(quiz=quiz)
|
||||||
|
return frappe.render_template("templates/quiz.html", context)
|
||||||
|
|
||||||
|
def exercise_renderer(argument):
|
||||||
|
exercise = frappe.get_doc("Exercise", argument)
|
||||||
|
context = dict(exercise=exercise)
|
||||||
|
return frappe.render_template("templates/exercise.html", context)
|
||||||
|
|
||||||
|
def youtube_video_renderer(video_id):
|
||||||
|
return f"""
|
||||||
|
<iframe width="100%" height="315"
|
||||||
|
src="https://www.youtube.com/embed/{video_id}"
|
||||||
|
title="YouTube video player"
|
||||||
|
frameborder="0"
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||||
|
allowfullscreen>
|
||||||
|
</iframe>
|
||||||
|
"""
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ h2 {
|
|||||||
|
|
||||||
.teaser-body {
|
.teaser-body {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
box-shadow: 0px 5px 10px rgb(0 0 0 / 10%)
|
||||||
}
|
}
|
||||||
.teaser-footer {
|
.teaser-footer {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
@@ -66,6 +67,15 @@ h2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.anchor_style {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anchor_style:hover {
|
||||||
|
text-decoration: none
|
||||||
|
}
|
||||||
|
|
||||||
section {
|
section {
|
||||||
padding: 60px 0px;
|
padding: 60px 0px;
|
||||||
}
|
}
|
||||||
@@ -128,41 +138,7 @@ section.lightgray {
|
|||||||
color: var(--tag-color);
|
color: var(--tag-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.course-header {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
.course-header {
|
|
||||||
margin-top: 20px;
|
|
||||||
padding: 20px;
|
|
||||||
background: var(--header-bg);
|
|
||||||
color: var(--header-color);
|
|
||||||
border-radius: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.course-author-avatar {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
border-radius: 50%;
|
|
||||||
margin-right: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.course-header h1 {
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
// .gray-section {
|
|
||||||
// background:#F6F6F6;
|
|
||||||
// border: 1px solid #C4C4C4;
|
|
||||||
// padding: 20px;
|
|
||||||
// margin: 20px 0px;
|
|
||||||
// }
|
|
||||||
|
|
||||||
.instructor-title {
|
.instructor-title {
|
||||||
font-weight: bold;
|
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,10 +256,6 @@ section.lightgray {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.chapter-description {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lesson-teaser {
|
.lesson-teaser {
|
||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
}
|
}
|
||||||
@@ -296,10 +268,6 @@ section.lightgray {
|
|||||||
margin: 20px 0px;
|
margin: 20px 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lesson-pagination {
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
|
|
||||||
.exercise-image svg {
|
.exercise-image svg {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
height: 200px;
|
height: 200px;
|
||||||
@@ -331,3 +299,9 @@ section.lightgray {
|
|||||||
margin: 40px 0px 0px 20px;
|
margin: 40px 0px 0px 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.no-preview-message {
|
||||||
|
width: fit-content;
|
||||||
|
margin: 50px 0px 50px;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|||||||
3
community/public/icons/black-arrow.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="13" height="8" viewBox="0 0 13 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.9649 0.313118C8.76964 0.117856 8.45306 0.117856 8.2578 0.313118C8.06253 0.50838 8.06253 0.824963 8.2578 1.02023L10.7374 3.49985L1.16683 3.49719C0.890684 3.49712 0.666764 3.72091 0.666687 3.99705C0.66661 4.2732 0.890406 4.49712 1.16655 4.49719L10.7377 4.49985L8.2578 6.97978C8.06253 7.17505 8.06253 7.49163 8.2578 7.68689C8.45306 7.88215 8.76964 7.88215 8.9649 7.68689L12.2982 4.35356C12.4935 4.1583 12.4935 3.84171 12.2982 3.64645L8.9649 0.313118Z" fill="#4C5A67"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 619 B |
3
community/public/icons/calendar.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.8947 2.47619C15.8947 2.2132 15.6885 2 15.4342 2C15.1798 2 14.9737 2.2132 14.9737 2.47619V3.90476H8.52631V2.47619C8.52631 2.2132 8.32013 2 8.06579 2C7.81145 2 7.60526 2.2132 7.60526 2.47619V3.90476H5.30263C4.03092 3.90476 3 4.97074 3 6.28571V10.0952V19.619C3 20.934 4.03092 22 5.30263 22H18.1974C19.4691 22 20.5 20.934 20.5 19.619V10.0952V6.28571C20.5 4.97074 19.4691 3.90476 18.1974 3.90476H15.8947V2.47619ZM19.579 9.61905V6.28571C19.579 5.49673 18.9604 4.85714 18.1974 4.85714H15.8947V6.28572C15.8947 6.54871 15.6885 6.76191 15.4342 6.76191C15.1798 6.76191 14.9737 6.54871 14.9737 6.28572V4.85714H8.52631V6.28572C8.52631 6.54871 8.32013 6.76191 8.06579 6.76191C7.81145 6.76191 7.60526 6.54871 7.60526 6.28572V4.85714H5.30263C4.53961 4.85714 3.92105 5.49673 3.92105 6.28571V9.61905H19.579ZM3.92105 10.5714H19.579V19.619C19.579 20.408 18.9604 21.0476 18.1974 21.0476H5.30263C4.53961 21.0476 3.92105 20.408 3.92105 19.619V10.5714Z" fill="#4C5A67"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
3
community/public/icons/check.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12ZM16.8734 10.1402C17.264 9.74969 17.264 9.11652 16.8734 8.726C16.4829 8.33547 15.8498 8.33547 15.4592 8.726L14.6259 9.55933L12.9592 11.226L10.333 13.8522L9.37345 12.8927L8.54011 12.0593C8.14959 11.6688 7.51643 11.6688 7.1259 12.0593C6.73538 12.4499 6.73538 13.083 7.1259 13.4735L7.95923 14.3069L9.6259 15.9735C9.81344 16.1611 10.0678 16.2664 10.333 16.2664C10.5982 16.2664 10.8526 16.1611 11.0401 15.9735L14.3734 12.6402L16.0401 10.9735L16.8734 10.1402Z" fill="#68D391"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 736 B |
3
community/public/icons/chevron-right.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M10 16L14 12L10 8" stroke="#4C5A67" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 197 B |
3
community/public/icons/chevron.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M8 10L12 14L16 10" stroke="#4C5A67" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 220 B |
3
community/public/icons/clock.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M3 11.9999C3 7.02937 7.02937 3 11.9999 3C16.9703 3 20.9997 7.02937 20.9997 11.9999C20.9997 16.9703 16.9703 20.9997 11.9999 20.9997C7.02937 20.9997 3 16.9703 3 11.9999ZM11.9999 2C6.47709 2 2 6.47709 2 11.9999C2 17.5226 6.47709 21.9997 11.9999 21.9997C17.5226 21.9997 21.9997 17.5226 21.9997 11.9999C21.9997 6.47709 17.5226 2 11.9999 2ZM12.5002 6.30027C12.5002 6.02413 12.2763 5.80027 12.0002 5.80027C11.724 5.80027 11.5002 6.02413 11.5002 6.30027V12.0002C11.5002 12.1896 11.6072 12.3627 11.7766 12.4474L15.5765 14.3474C15.8235 14.4709 16.1238 14.3708 16.2473 14.1238C16.3708 13.8768 16.2707 13.5764 16.0237 13.4529L12.5002 11.6912V6.30027Z" fill="#4C5A67"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 809 B |
3
community/public/icons/down-arrow.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="10" height="6" viewBox="0 0 10 6" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M1 1L5 5L9 1" stroke="#4C5A67" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 213 B |
9
community/public/icons/github.svg
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<rect x="2" y="2" width="20" height="20" fill="url(#pattern0)"/>
|
||||||
|
<defs>
|
||||||
|
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
|
||||||
|
<use xlink:href="#image0" transform="scale(0.05)"/>
|
||||||
|
</pattern>
|
||||||
|
<image id="image0" width="20" height="20" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAABiUlEQVQ4EZWU4VUCMQzHOwIjMAIb6AhuIBvgArb9QkLDh3MD3EA3wA10A9wAN0D/PdLLld5DeI+XuzT95Z9cU+cavxi7maf0GCi9BU6HwHI6//eBUhd5e9/Y1nZFSk+B5GggCqvtPq5l0aY456AqsOz+ARrAJMdI6aEJ9ZReb4INbThdtCCXOQrgeWRZepJvTeI5/ZjniHKLCCiN3SwrzaVWPdMSsFYCz07bN88p2iQ9kGWpTrU1RBPUtihEdSTHvO5Z3hUE6zl91Run3q1C7M0fyPapOKcIDX/dW2fVZaA2t7G55fIkH8qA4kvgLVPgnLMVZiB6phlgPctLS0nLh/Nn9+L4OQCsM0M3smoBrK+Hjeb8lI9UnWWAp4PnbXgmuVNQZJ77jaz6S6NcGHkUR6fDNHZ3MTXVBWBih5nWI1Myr2WhyqAY5wkNxvhpjFqUpbFqkUTXi8XmHEDyeW1SFASLUifjoc4c1Mn7ToFQNglTqQjAlwcYCdRvbVY1dQ/awPr5avZ6w9/7L3HVAW99VdWzAAAAAElFTkSuQmCC"/>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
3
community/public/icons/left-arrow.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="4" height="8" viewBox="0 0 4 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M3.33331 6.66663L0.666646 3.99996L3.33331 1.33329" stroke="#4C5A67" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 225 B |
3
community/public/icons/lock.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.7114 3.0002H11.6325C10.4336 2.98895 9.27915 3.45397 8.42278 4.29322C7.5658 5.13306 7.07747 6.27888 7.06517 7.47871L7.06514 7.47871V7.48384V8.17399H5C3.89543 8.17399 3 9.06943 3 10.174V18.9996C3 20.1041 3.89543 20.9996 5 20.9996H18.2602C19.3647 20.9996 20.2602 20.1041 20.2602 18.9996V10.174C20.2602 9.06942 19.3647 8.17399 18.2602 8.17399H16.195V7.56755C16.2063 6.36863 15.7412 5.21421 14.902 4.35784C14.0622 3.50086 12.9163 3.01253 11.7165 3.00023V3.0002H11.7114ZM15.195 8.17399V7.56514V7.56016H15.195C15.2043 6.62548 14.842 5.72537 14.1878 5.05777C13.5341 4.39076 12.6425 4.01043 11.7087 4.0002H11.6301H11.6251V4.00017C10.6904 3.99087 9.79031 4.35318 9.12271 5.00743C8.4557 5.6611 8.07537 6.55271 8.06514 7.48649V8.17399H15.195ZM5 9.17399C4.44772 9.17399 4 9.62171 4 10.174V18.9996C4 19.5518 4.44772 19.9996 5 19.9996H18.2602C18.8124 19.9996 19.2602 19.5518 19.2602 18.9996V10.174C19.2602 9.62171 18.8124 9.17399 18.2602 9.17399H5ZM11.6303 15.804C12.3026 15.804 12.8476 15.259 12.8476 14.5867C12.8476 13.9145 12.3026 13.3694 11.6303 13.3694C10.958 13.3694 10.413 13.9145 10.413 14.5867C10.413 15.259 10.958 15.804 11.6303 15.804ZM13.8476 14.5867C13.8476 15.8113 12.8549 16.804 11.6303 16.804C10.4057 16.804 9.413 15.8113 9.413 14.5867C9.413 13.3622 10.4057 12.3694 11.6303 12.3694C12.8549 12.3694 13.8476 13.3622 13.8476 14.5867Z" fill="#4C5A67"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
9
community/public/icons/medium.svg
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<rect x="2" y="3" width="20.7" height="18" fill="url(#pattern0)"/>
|
||||||
|
<defs>
|
||||||
|
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
|
||||||
|
<use xlink:href="#image0" transform="scale(0.0434783 0.05)"/>
|
||||||
|
</pattern>
|
||||||
|
<image id="image0" width="23" height="20" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABcAAAAUCAYAAABmvqYOAAABtUlEQVRIDa2UTU7CQBSAOYJH4CgcxSNwAIPoAgSEqAmIbohR/iQIgiK0nU75q4BiKz0ALnStxoULTMZMzWvGoUBrbPLy5jUz3zd5M61nKxJ7iR0kjz0uH1HEPhErH93+7eK1m+EoobEd3Z25kbSQrAtIJjQkrMxsJQCH7EQiIuQHMJvnJADlM5UkUukNvls3GHsFJL+yUH5MJe2eGvbwULaOp9KfmXzeywpaklzlYXb1cDTWl8ITqSOSyRYqAKeHaAfi33V6KtEmhuIETjKneR/GeK2F5CkP4mtJxkTTH13As4VpQ5B2eJBdPRiNiD4xXMFJ+bJuXjs7ILxTOl0T7BqeyRZIQxCXCu4ftL/Dc+cXC+H9wcACO9p5PHn4TnfMRvXqek6AFMU8RAqFWHlbQnv72bNi6Y2Fn+SKpCmiX4Lh3diCOoYHwpEgveOlcs3PSkqVmgVvd3tzYEdtATh8RKyk3mgSQUJfY03/HzgrKZarz11VfYI28NnseTAc8wVCUYX9p8CY3znAIWuG4aMQHmy1BSbaSVbBYa2dxNw5TIDMSpzCYS0rsYXDRCqhAbWb/COZrH8Ddw70agzSigAAAAAASUVORK5CYII="/>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
3
community/public/icons/play.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 18L16 12L8 6" stroke="#4C5A67" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 237 B |
3
community/public/icons/rating-filled.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 1.5C12.1903 1.5 12.3641 1.60804 12.4484 1.77869L15.4221 7.80323L22.0723 8.77526C22.2606 8.80278 22.4169 8.93478 22.4756 9.11578C22.5343 9.29679 22.4852 9.49541 22.3489 9.62818L17.5373 14.3147L18.6728 20.9355C18.705 21.1231 18.6279 21.3127 18.4739 21.4245C18.3199 21.5364 18.1157 21.5511 17.9473 21.4625L12 18.3349L6.05273 21.4625C5.88427 21.5511 5.68012 21.5364 5.52613 21.4245C5.37214 21.3127 5.29502 21.1231 5.3272 20.9355L6.46274 14.3147L1.65113 9.62818C1.51482 9.49541 1.46569 9.29679 1.52438 9.11578C1.58307 8.93478 1.73941 8.80278 1.92769 8.77526L8.57787 7.80323L11.5516 1.77869C11.6359 1.60804 11.8097 1.5 12 1.5Z" fill="#192734"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 795 B |
3
community/public/icons/rating.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z" stroke="#192734" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 278 B |
3
community/public/icons/side-arrow-white.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="4" height="8" viewBox="0 0 4 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0.666626 6.66675L3.33329 4.00008L0.666626 1.33341" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 224 B |
3
community/public/icons/tick.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg class="icon">
|
||||||
|
<use href="#icon-tick"></use>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 58 B |
4
community/public/icons/user.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M20 21V19C20 17.9391 19.5786 16.9217 18.8284 16.1716C18.0783 15.4214 17.0609 15 16 15H8C6.93913 15 5.92172 15.4214 5.17157 16.1716C4.42143 16.9217 4 17.9391 4 19V21" stroke="#192734" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M12 11C14.2091 11 16 9.20914 16 7C16 4.79086 14.2091 3 12 3C9.79086 3 8 4.79086 8 7C8 9.20914 9.79086 11 12 11Z" stroke="#192734" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 532 B |
3
community/public/icons/white-arrow.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="13" height="8" viewBox="0 0 13 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.96484 0.313118C8.76958 0.117856 8.453 0.117856 8.25773 0.313118C8.06247 0.50838 8.06247 0.824963 8.25773 1.02023L10.7374 3.49985L1.16676 3.49719C0.890623 3.49712 0.666703 3.72091 0.666626 3.99705C0.666549 4.2732 0.890345 4.49712 1.16649 4.49719L10.7377 4.49985L8.25773 6.97978C8.06247 7.17505 8.06247 7.49163 8.25773 7.68689C8.453 7.88215 8.76958 7.88215 8.96484 7.68689L12.2982 4.35356C12.4934 4.1583 12.4934 3.84171 12.2982 3.64645L8.96484 0.313118Z" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 621 B |
3
community/public/icons/white-tick.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="12" height="10" viewBox="0 0 12 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.2414 1.09763C11.3639 1.22781 11.3639 1.43886 11.2414 1.56904L4.33944 8.90237C4.21693 9.03254 4.01829 9.03254 3.89577 8.90237L0.758514 5.56904C0.635997 5.43886 0.635997 5.22781 0.758514 5.09763C0.881031 4.96746 1.07967 4.96746 1.20219 5.09763L4.11761 8.19526L10.7977 1.09763C10.9202 0.967456 11.1189 0.967456 11.2414 1.09763Z" fill="white" stroke="white" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 578 B |
|
Before Width: | Height: | Size: 206 B After Width: | Height: | Size: 200 B |
BIN
community/public/images/linkedin.png
Normal file
|
After Width: | Height: | Size: 564 B |
BIN
community/public/images/play.png
Normal file
|
After Width: | Height: | Size: 326 B |
BIN
community/public/images/profile-banner.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
10
community/templates/exercise.html
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<div class="exercise">
|
||||||
|
<h3>Exercise {{exercise.index_label}}: {{ exercise.title }}</h3>
|
||||||
|
<div class="exercise-description">{{frappe.utils.md_to_html(exercise.description)}}</div>
|
||||||
|
|
||||||
|
{% set submission = exercise.get_user_submission() %}
|
||||||
|
|
||||||
|
<pre class="exercise" id="exercise-{{exercise.name}}"
|
||||||
|
data-last-submitted="{{ submission and submission.creation or '' }}" data-name="{{ exercise.name }}"
|
||||||
|
data-image='{{ (submission and submission.image or "") | tojson }}'><code class="language-joy">{{ submission.solution if submission else exercise.code }}</code></pre>
|
||||||
|
</div>
|
||||||
168
community/templates/livecode/extension_footer.html
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
<script type="text/javascript" src="/assets/frappe/node_modules/moment/min/moment-with-locales.min.js"></script>
|
||||||
|
<script type="text/javascript" src="/assets/frappe/node_modules/moment-timezone/builds/moment-timezone-with-data.min.js"></script>
|
||||||
|
<script type="text/javascript" src="/assets/frappe/js/frappe/utils/datetime.js"></script>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
// comment_when is failing because of this
|
||||||
|
if (!frappe.sys_defaults) {
|
||||||
|
frappe.sys_defaults = {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/javascript" src="{{ livecode_url }}/static/livecode.js"></script>
|
||||||
|
|
||||||
|
<script type="text/javascript" src="/assets/mon_school/js/livecode-files.js"></script>
|
||||||
|
|
||||||
|
<template id="livecode-template">
|
||||||
|
<div class="livecode-editor livecode-editor-inline">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8 col-md-6">
|
||||||
|
<div class="controls">
|
||||||
|
<button class="run">Run</button>
|
||||||
|
|
||||||
|
<div class="exercise-controls pull-right">
|
||||||
|
<span style="padding-right: 10px;"><span class="last-submitted human-time" data-timestamp=""></span></span>
|
||||||
|
<button class="submit btn-primary">Submit</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="code-editor">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8 col-md-6">
|
||||||
|
<div class="code-wrapper">
|
||||||
|
<textarea class="code"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-4 col-md-6 canvas-wrapper">
|
||||||
|
<div class="svg-image" width="300" height="300"></div>
|
||||||
|
<pre class="output"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
function getLiveCodeOptions() {
|
||||||
|
return {
|
||||||
|
base_url: "{{ livecode_url }}",
|
||||||
|
runtime: "python",
|
||||||
|
files: LIVECODE_FILES, // loaded from livecode-files.js
|
||||||
|
command: ["python", "start.py"],
|
||||||
|
codemirror: true,
|
||||||
|
onMessage: {
|
||||||
|
image: function(editor, msg) {
|
||||||
|
const element = editor.parent.querySelector(".svg-image");
|
||||||
|
element.innerHTML = msg.image;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$(function() {
|
||||||
|
var editorLookup = {};
|
||||||
|
|
||||||
|
$("pre.example, pre.exercise").each((i, e) => {
|
||||||
|
var code = $(e).text();
|
||||||
|
var template = document.querySelector('#livecode-template');
|
||||||
|
var clone = template.content.cloneNode(true);
|
||||||
|
|
||||||
|
$(e)
|
||||||
|
.wrap('<div></div>')
|
||||||
|
.hide()
|
||||||
|
.parent()
|
||||||
|
.append(clone)
|
||||||
|
.find("textarea.code")
|
||||||
|
.val(code);
|
||||||
|
|
||||||
|
if ($(e).hasClass("exercise")) {
|
||||||
|
var last_submitted = $(e).data("last-submitted");
|
||||||
|
if (last_submitted) {
|
||||||
|
$(e).parent().find(".last-submitted")
|
||||||
|
.data("timestamp", last_submitted)
|
||||||
|
.html(__("Submitted {0}", [comment_when(last_submitted)]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$(e).parent().find(".exercise-controls").remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
var editor = new LiveCodeEditor(e.parentElement, {
|
||||||
|
...getLiveCodeOptions(),
|
||||||
|
codemirror: true,
|
||||||
|
onMessage: {
|
||||||
|
image: function(editor, msg) {
|
||||||
|
const canvasElement = editor.parent.querySelector("div.svg-image");
|
||||||
|
canvasElement.innerHTML = msg.image;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$(e).parent().find(".submit").on('click', function() {
|
||||||
|
var name = $(e).data("name");
|
||||||
|
let code = editor.codemirror.doc.getValue();
|
||||||
|
|
||||||
|
frappe.call("community.lms.api.submit_solution", {
|
||||||
|
"exercise": name,
|
||||||
|
"code": code
|
||||||
|
}).then(r => {
|
||||||
|
if (r.message.name) {
|
||||||
|
frappe.msgprint("Submitted successfully!");
|
||||||
|
|
||||||
|
let d = r.message.creation;
|
||||||
|
$(e).parent().find(".human-time").html(__("Submitted {0}", [comment_when(d)]));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".exercise-image").each((i, e) => {
|
||||||
|
var svg = JSON.parse($(e).data("image"));
|
||||||
|
$(e).html(svg);
|
||||||
|
});
|
||||||
|
|
||||||
|
$("pre.exercise").each((i, e) => {
|
||||||
|
var svg = JSON.parse($(e).data("image"));
|
||||||
|
$(e).parent().find(".svg-image").html(svg);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style type="text/css">
|
||||||
|
.svg-image {
|
||||||
|
border: 5px solid #ddd;
|
||||||
|
position: relative;
|
||||||
|
z-index: 0;
|
||||||
|
width: 310px;
|
||||||
|
height: 310px;
|
||||||
|
}
|
||||||
|
.livecode-editor {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.livecode-editor-small .svg-image {
|
||||||
|
border: 5px solid #ddd;
|
||||||
|
position: relative;
|
||||||
|
z-index: 0;
|
||||||
|
width: 210px;
|
||||||
|
height: 210px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* work-in-progress styles for showing admonition */
|
||||||
|
.admonition {
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
border-left: .5rem solid #888;
|
||||||
|
border-radius: .3em;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin: 1.5em 0;
|
||||||
|
padding: 0 0.5em;
|
||||||
|
}
|
||||||
|
.admonition-title {
|
||||||
|
padding: 0.5em 0px;
|
||||||
|
font-weight: bold;
|
||||||
|
padding-top:
|
||||||
|
}
|
||||||
|
</style>
|
||||||