Compare commits

...

102 Commits

Author SHA1 Message Date
pateljannat
66aace247c fix: conditions and tests 2021-08-10 10:28:59 +05:30
pateljannat
bc3db06960 fix: username issues 2021-08-09 16:54:02 +05:30
Jannat Patel
d5067a4bcd Merge pull request #169 from fossunited/username-validations
fix: Username validations
2021-08-06 15:05:36 +05:30
pateljannat
04d44510de fix: redirect after edit profile save 2021-08-06 14:41:37 +05:30
pateljannat
844fcc9bca fix: username validations 2021-08-06 14:41:11 +05:30
Jannat Patel
145b5efab0 Merge pull request #168 from fossunited/minor-issues
fix: quiz, course outline, and lesson indexing
2021-08-05 18:41:15 +05:30
pateljannat
4079ed97b9 fix: quiz, course outline, and lesson indexing 2021-08-05 18:26:41 +05:30
pateljannat
63d70fc037 fix: username space and empty validations 2021-08-05 15:51:21 +05:30
Jannat Patel
ce86b5deda Merge pull request #167 from fossunited/issues
fix: cleanup
2021-08-04 19:18:45 +05:30
pateljannat
037e946bbe fix: mentors cleanup 2021-08-04 19:07:14 +05:30
pateljannat
a51c8de1eb fix: profile progress and review links 2021-08-04 14:01:52 +05:30
Jannat Patel
53dc517180 Merge pull request #165 from fossunited/quiz-cleanup
refactor: Quiz cleanup
2021-08-03 19:04:37 +05:30
pateljannat
44ca940c6b fix: quiz enhancements and tests 2021-08-03 18:30:52 +05:30
pateljannat
c0b688c720 Merge branch 'main' of https://github.com/frappe/community into quiz-cleanup 2021-07-30 18:35:23 +05:30
Jannat Patel
861d5f231d Merge pull request #164 from fossunited/issues
fix: default image, meta, reviews, lesson headers
2021-07-30 17:07:19 +05:30
pateljannat
d14b4f55a6 fix: default image, meta, reviews, lesson headers 2021-07-30 16:24:56 +05:30
Jannat Patel
db9a6c3eda Merge pull request #163 from fossunited/chapter-lesson-patch-fix
fix: chapter lesson patch
2021-07-29 13:48:23 +05:30
pateljannat
a667643681 fix: chapter lesson patch 2021-07-29 13:43:31 +05:30
Jannat Patel
f278e4b6a5 Merge pull request #162 from fossunited/lesson-enhancements
fix: lesson indexing
2021-07-29 12:12:14 +05:30
pateljannat
33a12c2dec fix: lesson indexing 2021-07-29 11:54:30 +05:30
Jannat Patel
508f90f459 Merge pull request #161 from fossunited/upcoming-courses
fix: upcoming course and doctype cleanup
2021-07-26 12:22:02 +05:30
pateljannat
709f0c2274 fix: profile profession section width 2021-07-26 11:42:30 +05:30
pateljannat
be47700e7c Merge branch 'main' of https://github.com/frappe/community into upcoming-courses 2021-07-26 11:14:38 +05:30
pateljannat
40842830a4 fix: profile page social icons position 2021-07-26 11:14:32 +05:30
pateljannat
11d070fa0d fix: condition for chapter description 2021-07-23 19:16:36 +05:30
pateljannat
dd2f830a33 fix: upcoming course and doctype cleanup 2021-07-23 19:07:26 +05:30
pateljannat
5431fcb450 fix: quiz ui change 2021-07-23 17:55:41 +05:30
Jannat Patel
324033e9ee Merge pull request #160 from fossunited/only-show-published-courses
fix: minor issues
2021-07-20 17:35:18 +05:30
pateljannat
86596d0cfe fix: minor issues 2021-07-20 17:19:18 +05:30
Jannat Patel
9323cfd748 Merge pull request #159 from rmehta/fix-global-container
fix(style): max-width on container padding
2021-07-20 13:06:36 +05:30
Jannat Patel
d125b02cec fix: added max width to container padding 2021-07-20 13:05:15 +05:30
pateljannat
282c4c5351 feat: explanation field in quiz 2021-07-20 09:36:22 +05:30
Rushabh Mehta
276d64a66a fix(style): don't mess with global container styles 2021-07-20 09:30:58 +05:30
Rushabh Mehta
79eb381a41 Merge pull request #157 from rmehta/remove-old-styles
fix(cleanup): remove old styles
2021-07-19 17:19:54 +05:30
Rushabh Mehta
44f9c0dfd3 fix(minor): remove old styles 2021-07-19 17:14:52 +05:30
Jannat Patel
0ca4cd724e Merge pull request #156 from fossunited/fix-only-show-published-courses
fix: only show the published courses on All Courses page
2021-07-19 17:11:03 +05:30
Anand Chitipothu
8a3e31f021 fix: only show the published courses on All Courses page
Closes #155
2021-07-19 17:03:38 +05:30
Jannat Patel
9be8a1af0b Merge pull request #154 from fossunited/fix-invite-email
Fix invite email
2021-07-19 13:43:36 +05:30
Anand Chitipothu
b9cac20613 fix: the delay in sending signup email 2021-07-19 13:32:18 +05:30
Anand Chitipothu
e6d5e6d37b fix: "Hello None" in the signup email
We were trying to show the full_name, but invite request only knows the email.
2021-07-19 13:31:44 +05:30
Jannat Patel
0abfcac7da Merge pull request #153 from fossunited/improve-email-templates
fix: fixed the email message sent out on signup
2021-07-19 13:23:55 +05:30
Anand Chitipothu
b70e8b9acc fix: fixed the email message sent out on signup
Currently updated keeping Mon.School in mind.
2021-07-19 13:14:13 +05:30
Jannat Patel
3b1e1aa3c3 Merge pull request #152 from fossunited/cleanup
fix: Cleanup
2021-07-19 12:49:22 +05:30
pateljannat
8f74c74d50 fix: removed unused styles and folders 2021-07-19 10:55:06 +05:30
pateljannat
d2f435016c fix: layout cleanup 2021-07-16 20:24:35 +05:30
pateljannat
389b35802b Merge branch 'main' of https://github.com/frappe/community into cleanup 2021-07-15 17:36:31 +05:30
Jannat Patel
a9192a74f9 Merge pull request #151 from fossunited/redesign-fixes
fix: Profile page, course card ratings, lesson completion tick
2021-07-15 17:36:17 +05:30
pateljannat
1366c7cf75 fix: removed unwanted image 2021-07-15 17:16:49 +05:30
pateljannat
eaa9e8e3ea fix: added linkedin icon 2021-07-15 17:05:54 +05:30
pateljannat
5ecae0df61 fix: removed unused pages 2021-07-15 17:01:15 +05:30
pateljannat
4891be1d8c fix: profile page fixes and course completion tick 2021-07-15 11:16:01 +05:30
pateljannat
ec852fc255 fix: rating on course card and profile page responsive 2021-07-13 16:54:45 +05:30
Jannat Patel
47f2d3cb7b Merge pull request #148 from fossunited/redesign
feat: Redesign
2021-07-13 15:22:59 +05:30
pateljannat
37820c1e19 Merge branch 'redesign' of https://github.com/frappe/community into redesign 2021-07-13 15:15:29 +05:30
pateljannat
230fab3bb2 fix: responsive design 2021-07-13 15:15:23 +05:30
Jannat Patel
d292d2d093 Merge pull request #149 from fossunited/disable-exercises-for-non-members
feat: include membership info in page context
2021-07-12 10:59:28 +05:30
Anand Chitipothu
51d5db01e9 feat: include membership info in page context
Added `is_member` field to page_context of learn page. This is required to
disable exercises to non-members.
2021-07-11 12:55:26 +05:30
pateljannat
0fd760df81 fix: pre tag overflow 2021-07-09 18:32:51 +05:30
pateljannat
71d0a89968 fix: made image non mandatory, remove unnecessary code 2021-07-09 15:05:46 +05:30
pateljannat
d939a63412 responsive fixes 2021-07-09 13:24:38 +05:30
pateljannat
daaa2d2fe2 responsive fixes 2021-07-09 13:03:42 +05:30
pateljannat
6dd7cb19df Merge branch 'main' of https://github.com/frappe/community into redesign 2021-07-09 09:50:38 +05:30
pateljannat
b1de2481a8 feat: profile page and other issues 2021-07-09 09:48:08 +05:30
pateljannat
27c01b3b0c fix: course details interactions 2021-07-08 10:55:03 +05:30
Jannat Patel
b7aa9aff51 Merge pull request #147 from fossunited/fix-profile
fix: error on profile page
2021-07-08 10:41:08 +05:30
Anand Chitipothu
524a041fb9 fix: error on profile page
Profile page was importing Sketch which was removed recently, even
though it was not using that. Removed it to fix the issue.
2021-07-08 10:33:12 +05:30
Jannat Patel
9de0203914 Merge pull request #145 from fossunited/sketch-cleanup
Sketch cleanup
2021-07-07 11:22:05 +05:30
pateljannat
0ed5309b97 feat: course details page structure 2021-07-06 20:51:20 +05:30
pateljannat
68fd32d536 fix: links and breadcrumbs 2021-07-06 18:13:09 +05:30
pateljannat
5ea3b25d21 feat: course home 2021-07-06 17:58:36 +05:30
Anand Chitipothu
2c24412633 refactor: removed the unused dashboard portal page 2021-07-06 14:20:00 +05:30
Anand Chitipothu
1b8a45ba4a refactor: removed sketch doctype and portal page for home
Both of these will be moved to mon_school.
2021-07-06 13:20:41 +05:30
Jannat Patel
3dd4adbc1f Merge pull request #143 from fossunited/switch-batch
feat: added a utililty to switch a student from one batch to another
2021-07-05 19:04:52 +05:30
Jannat Patel
0c52c9c4bc Merge pull request #144 from fossunited/page-context
feat: make it possible to enable tracking for livecode execution
2021-07-05 19:03:47 +05:30
Anand Chitipothu
9caf44cdbd feat: make it possible to enable tracking for livecode execution
Tracking of livecode execution is made possible by making the page
context with course, batch and lesson available in js.

Added a global page_context variable in js and the data for that gets
initialzied in the learn.py.
2021-07-02 23:58:59 +05:30
pateljannat
45d88bdc08 feat: course header wide 2021-07-02 15:43:21 +05:30
Anand Chitipothu
94b3ccd3d9 feat: added a utililty to switch a student from one batch to another 2021-07-01 17:29:02 +05:30
pateljannat
ee8273fd30 feat: new fields in user doctype and new web form 2021-06-30 16:16:22 +05:30
pateljannat
60c1449f40 Merge branch 'main' of https://github.com/frappe/community into redesign 2021-06-29 19:28:50 +05:30
Jannat Patel
67708325ae Merge pull request #141 from fossunited/workspace
feat: lms workspace
2021-06-29 15:24:22 +05:30
pateljannat
3e99577401 feat: lms workspace 2021-06-29 15:15:49 +05:30
pateljannat
5e916dc2c8 feat: review card style 2021-06-29 12:58:12 +05:30
pateljannat
0c64d46e99 feat: reviews 2021-06-28 20:27:17 +05:30
pateljannat
3aa974f8bd fix: removed review doctype 2021-06-28 13:26:29 +05:30
Jannat Patel
621d01d502 Merge pull request #140 from fossunited/exercise-refactor
fix: enabled livecode on community
2021-06-28 13:11:26 +05:30
pateljannat
aa20136223 fix: undo status change on livecode 2021-06-28 13:05:20 +05:30
pateljannat
9bc5408a44 feat: course card redesign 2021-06-28 12:52:10 +05:30
pateljannat
5a7afb3092 fix: added livecode editor in community 2021-06-24 16:38:02 +05:30
Jannat Patel
f8948ac2ef Merge pull request #138 from fossunited/learn-page-fix
fix: learn page
2021-06-24 12:28:34 +05:30
pateljannat
8b1576a028 fix: learn page 2021-06-24 12:21:25 +05:30
Jannat Patel
56d8a72a7d Merge pull request #136 from fossunited/quiz
feat: Quizzes, Youtube Video integration and Other Minor Fixes
2021-06-24 10:34:36 +05:30
pateljannat
f6c11ce52f fix: conflicts 2021-06-24 10:27:01 +05:30
pateljannat
0284c9305c fix: quiz progress and youtube video integration 2021-06-24 10:25:23 +05:30
Jannat Patel
d785fb7562 Merge pull request #127 from fossunited/livecode-cleanup
refactor: removed the portal pages for showing sketches
2021-06-23 13:09:26 +05:30
Anand Chitipothu
9f50af4ebd refactor: removed the portal pages for showing sketches
Moved them to mon_school.
2021-06-23 12:53:35 +05:30
Jannat Patel
4c3645f0d4 Merge pull request #133 from fossunited/mon-fixes-01
Various fixes from mon.school
2021-06-23 11:44:35 +05:30
Anand Chitipothu
20b3ae7d76 fix: error in linking lessons on course page
The course was adding `{{ no such element: community.lms.doctype.lms_course.lms_course.LMSCourse object['query_parameter'] }}`
to the lesson links. Fixed it by setting query_parameter to "".
2021-06-23 10:27:01 +05:30
Anand Chitipothu
f303be4db5 fix: error in find_macros when the input is empty
Added a special case to handle this issue.
2021-06-22 18:12:31 +05:30
Anand Chitipothu
fc1c393f15 feat: allow a student to be mentor of another batch
This is a requirement for mon.school. The students are of the first
batch are now mentors of new batches.
2021-06-22 18:09:21 +05:30
pateljannat
5d96bf544d fix: conflicts 2021-06-22 12:28:12 +05:30
Jannat Patel
5abfa35095 Merge pull request #132 from fossunited/learning-modes
feat: learning modes and batch switching
2021-06-22 12:23:46 +05:30
pateljannat
1cb81de5c0 feat: lms quizzes 2021-06-09 13:17:42 +05:30
153 changed files with 4732 additions and 2437 deletions

3
.gitignore vendored
View File

@@ -5,3 +5,6 @@
tags
community/docs/current
community/public/dist
__pycache__/
*.py[cod]
*$py.class

View File

@@ -1,5 +1,5 @@
{% set color = member.get_palette() %}
<a href="/{{member.username}}">
<a class="button-links" href="/{{member.username}}">
<span class="avatar {{ avatar_class }}" title="{{ member.full_name }}">
{% if member.user_image %}
<img class="avatar-frame standard-image" style="object-fit: cover;" src="{{ member.user_image }}" title="{{ member.full_name }}">

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

View File

@@ -104,6 +104,8 @@ doc_events = {
# ]
#}
fixtures = ["Custom Field"]
# Testing
# -------
@@ -134,7 +136,6 @@ primary_rules = [
{"from_route": "/courses/<course>/<topic>", "to_route": "courses/topic"},
{"from_route": "/hackathons/<hackathon>", "to_route": "hackathons/hackathon"},
{"from_route": "/hackathons/<hackathon>/<project>", "to_route": "hackathons/project"},
{"from_route": "/dashboard", "to_route": ""},
{"from_route": "/add-a-new-batch", "to_route": "add-a-new-batch"},
{"from_route": "/courses/<course>/home", "to_route": "batch/home"},
{"from_route": "/courses/<course>/learn", "to_route": "batch/learn"},
@@ -161,11 +162,12 @@ whitelist = [
"/socket.io",
"/hackathons",
"/dashboard",
"/join-request"
"/join-request",
"/add-a-new-batch",
"/new-sign-up",
"/message"
"/about"
"/message",
"/about",
"/edit-profile"
]
whitelist_rules = [{"from_route": p, "to_route": p[1:]} for p in whitelist]
@@ -176,6 +178,10 @@ 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'
## Specify the additional tabs to be included in the user profile page.
@@ -187,5 +193,13 @@ update_website_context = 'community.widgets.update_website_context'
## subclass of community.community.plugins.PageExtension
# community_lesson_page_extension = None
community_lesson_page_extensions = [
"community.plugins.LiveCodeExtension"
]
## 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",
}

View File

@@ -2,7 +2,15 @@
// For license information, please see license.txt
frappe.ui.form.on('Chapter', {
// refresh: function(frm) {
// }
onload: function (frm) {
frm.set_query("lesson", "lessons", function () {
return {
filters: {
"chapter": frm.doc.name,
}
};
});
}
});

View File

@@ -9,8 +9,7 @@
"course",
"title",
"description",
"locked",
"index_"
"lessons"
],
"fields": [
{
@@ -24,12 +23,6 @@
"fieldtype": "Markdown Editor",
"label": "Description"
},
{
"default": "0",
"fieldname": "locked",
"fieldtype": "Check",
"label": "Locked"
},
{
"fieldname": "course",
"fieldtype": "Link",
@@ -38,10 +31,10 @@
"options": "LMS Course"
},
{
"default": "1",
"fieldname": "index_",
"fieldtype": "Int",
"label": "Index"
"fieldname": "lessons",
"fieldtype": "Table",
"label": "Lessons",
"options": "Lessons"
}
],
"index_web_pages_for_search": 1,
@@ -52,7 +45,7 @@
"link_fieldname": "chapter"
}
],
"modified": "2021-05-13 21:05:20.531890",
"modified": "2021-07-27 16:28:08.667964",
"modified_by": "Administrator",
"module": "LMS",
"name": "Chapter",

View File

@@ -7,9 +7,4 @@ import frappe
from frappe.model.document import Document
class Chapter(Document):
def get_lessons(self):
rows = frappe.db.get_all("Lesson",
filters={"chapter": self.name},
fields='name',
order_by="index_")
return [frappe.get_doc('Lesson', row['name']) for row in rows]
pass

View File

@@ -0,0 +1,32 @@
{
"actions": [],
"creation": "2021-07-27 16:25:02.903245",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"chapter"
],
"fields": [
{
"fieldname": "chapter",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Chapter",
"options": "Chapter",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-07-27 16:25:02.903245",
"modified_by": "Administrator",
"module": "LMS",
"name": "Chapters",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

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

View File

@@ -3,7 +3,6 @@
import frappe
from frappe.model.document import Document
# from ..lms_sketch.livecode import livecode_to_svg
class Exercise(Document):
def get_user_submission(self):

View File

@@ -6,12 +6,17 @@
"engine": "InnoDB",
"field_order": [
"exercise",
"solution",
"status",
"batch",
"column_break_4",
"exercise_title",
"course",
"batch",
"lesson",
"image"
"section_break_8",
"solution",
"image",
"test_results",
"comments"
],
"fields": [
{
@@ -21,12 +26,6 @@
"label": "Exercise",
"options": "Exercise"
},
{
"fieldname": "solution",
"fieldtype": "Code",
"in_list_view": 1,
"label": "Solution"
},
{
"fetch_from": "exercise.title",
"fieldname": "exercise_title",
@@ -61,11 +60,41 @@
"fieldtype": "Code",
"label": "Image",
"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,
"links": [],
"modified": "2021-05-21 11:28:45.833018",
"modified": "2021-06-24 16:22:50.570845",
"modified_by": "Administrator",
"module": "LMS",
"name": "Exercise Submission",

View File

@@ -30,10 +30,13 @@ class InviteRequest(Document):
return user
def send_email(self):
subject = _("Your request has been approved.")
site_name = "Mon.School"
subject = _("Welcome to {0}!").format(site_name)
args = {
"full_name": self.full_name,
"signup_form_link": "/new-sign-up?invite_code={0}".format(self.name),
"site_name": site_name,
"site_url": frappe.utils.get_url()
}
frappe.sendmail(
@@ -42,7 +45,8 @@ class InviteRequest(Document):
subject=subject,
header=[subject, "green"],
template = "lms_invite_request_approved",
args=args)
args=args,
now=True)
@frappe.whitelist(allow_guest=True)
def create_invite_request(invite_email):

View File

@@ -2,7 +2,47 @@
// For license information, please see license.txt
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>
`);
}
});

View File

@@ -7,14 +7,14 @@
"engine": "InnoDB",
"field_order": [
"chapter",
"lesson_type",
"include_in_preview",
"column_break_4",
"title",
"index_",
"index_label",
"section_break_6",
"body"
"body",
"help_section",
"help"
],
"fields": [
{
@@ -24,26 +24,12 @@
"label": "Chapter",
"options": "Chapter"
},
{
"default": "Video",
"fieldname": "lesson_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Lesson Type",
"options": "Video\nText\nQuiz"
},
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title"
},
{
"default": "1",
"fieldname": "index_",
"fieldtype": "Int",
"label": "Index"
},
{
"fieldname": "body",
"fieldtype": "Markdown Editor",
@@ -52,7 +38,6 @@
{
"fieldname": "index_label",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Index Label",
"read_only": 1
},
@@ -69,11 +54,20 @@
{
"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,
"links": [],
"modified": "2021-06-11 19:03:23.138165",
"modified": "2021-07-27 16:28:29.203624",
"modified_by": "Administrator",
"module": "LMS",
"name": "Lesson",

View File

@@ -9,27 +9,35 @@ from ...md import markdown_to_html, find_macros
class Lesson(Document):
def before_save(self):
macros = find_macros(self.body)
exercises = [value for name, value in macros if name == "Exercise"]
dynamic_documents = ["Exercise", "Quiz"]
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
for name in exercises:
e = frappe.get_doc("Exercise", name)
for name in documents:
e = frappe.get_doc(doctype_map[section], name)
e.lesson = self.name
e.index_ = index
e.save()
index += 1
self.update_orphan_exercises(exercises)
self.update_orphan_documents(doctype_map[section], documents)
def update_orphan_exercises(self, active_exercises):
"""Updates the exercises that were previously part of this lesson,
def update_orphan_documents(self, doctype, documents):
"""Updates the documents that were previously part of this lesson,
but not any more.
"""
linked_exercises = {row['name'] for row in frappe.get_all('Exercise', {"lesson": self.name})}
active_exercises = set(active_exercises)
orphan_exercises = linked_exercises - active_exercises
for name in orphan_exercises:
ex = frappe.get_doc("Exercise", name)
linked_documents = {row['name'] for row in frappe.get_all(doctype, {"lesson": self.name})}
active_documents = set(documents)
orphan_documents = linked_documents - active_documents
for name in orphan_documents:
ex = frappe.get_doc(doctype, name)
ex.lesson = None
ex.index_ = 0
ex.index_label = ""
@@ -55,32 +63,35 @@ class Lesson(Document):
return
@frappe.whitelist()
def save_progress(lesson, course):
def save_progress(lesson, course, status):
if not frappe.db.exists("LMS Batch Membership",
{
"member": frappe.session.user,
"course": course
}):
return
if frappe.db.exists("LMS Course Progress",
{
"lesson": lesson,
"owner": frappe.session.user
"owner": frappe.session.user,
"course": course
}):
return
lesson_details = frappe.get_doc("Lesson", lesson)
dynamic_content = find_macros(lesson_details.body)
status = "Complete"
if dynamic_content:
status = "Partially Complete"
frappe.get_doc({
"doctype": "LMS Course Progress",
"lesson": lesson_details.name,
"status": status
}).save(ignore_permissions=True)
doc = frappe.get_doc("LMS Course Progress",
{
"lesson": lesson,
"owner": frappe.session.user,
"course": course
})
doc.status = status
doc.save(ignore_permissions=True)
else:
frappe.get_doc({
"doctype": "LMS Course Progress",
"lesson": lesson,
"status": status,
}).save(ignore_permissions=True)
return "OK"
def update_progress(lesson):
user = frappe.session.user
@@ -92,13 +103,30 @@ def update_progress(lesson):
course_progress.save(ignore_permissions=True)
def all_dynamic_content_submitted(lesson, user):
all_exercises_submitted = check_all_exercise_submission(lesson, user)
all_quiz_submitted = check_all_quiz_submitted(lesson, user)
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)
all_exercises_submitted = False
if not len(exercise_names):
return True
query = {
"exercise": ["in", exercise_names],
"owner": user
}
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

View File

@@ -0,0 +1,31 @@
{
"actions": [],
"creation": "2021-07-27 16:25:48.269536",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"lesson"
],
"fields": [
{
"fieldname": "lesson",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Lesson",
"options": "Lesson"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-07-27 16:53:52.732191",
"modified_by": "Administrator",
"module": "LMS",
"name": "Lessons",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

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

View File

@@ -71,3 +71,38 @@ def save_message(message, batch):
"message": message
})
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()

View File

@@ -10,5 +10,5 @@ frappe.ui.form.on('LMS Batch Membership', {
}
};
});
},
}
});

View File

@@ -37,6 +37,7 @@
"fieldname": "member_type",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Member Type",
"options": "\nStudent\nMentor\nStaff"
},
@@ -44,7 +45,6 @@
"default": "Member",
"fieldname": "role",
"fieldtype": "Select",
"in_standard_filter": 1,
"label": "Role",
"options": "\nMember\nAdmin"
},
@@ -63,10 +63,10 @@
{
"fetch_from": "batch.course",
"fieldname": "course",
"fieldtype": "Data",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Course",
"read_only": 1
"options": "LMS Course"
},
{
"fieldname": "current_lesson",
@@ -84,7 +84,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-06-21 12:10:28.808803",
"modified": "2021-08-04 17:10:42.708479",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Batch Membership",

View File

@@ -32,20 +32,28 @@ class LMSBatchMembership(Document):
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):
course = frappe.db.get_value("LMS Batch", self.batch, "course")
previous_membership = frappe.get_all("LMS Batch Membership",
filters={
"member": self.member,
"name": ["!=", self.name]
},
fields=["batch", "member_type", "name"]
)
"""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
for membership in previous_membership:
batch_course = frappe.db.get_value("LMS Batch", membership.batch, "course")
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")
frappe.throw(_("{0} is already a {1} of {2} course through {3} batch").format(member_name, membership.member_type, course, membership.batch))
course = frappe.db.get_value("LMS Batch", self.batch, "course")
memberships = frappe.get_all(
"LMS Batch Membership",
filters={
"member": self.member,
"name": ["!=", self.name],
"member_type": "Student",
"course": self.course
},
fields=["batch", "member_type", "name"]
)
if memberships:
membership = memberships[0]
member_name = frappe.db.get_value("User", self.member, "full_name")
frappe.throw(_("{0} is already a Student of {1} course through {2} batch").format(member_name, course, membership.batch))
@frappe.whitelist()
def create_membership(course, batch=None, member=None, member_type="Student", role="Member"):

View File

@@ -2,7 +2,15 @@
// For license information, please see license.txt
frappe.ui.form.on('LMS Course', {
// refresh: function(frm) {
// }
onload: function (frm) {
frm.set_query("chapter", "chapters", function () {
return {
filters: {
"course": frm.doc.name,
}
};
});
}
});

View File

@@ -1,11 +1,5 @@
{
"actions": [
{
"action": "community.lms.doctype.lms_course.lms_course.reindex_lessons",
"action_type": "Server Action",
"group": "Reindex",
"label": "Reindex Lessons"
},
{
"action": "community.lms.doctype.lms_course.lms_course.reindex_exercises",
"action_type": "Server Action",
@@ -21,14 +15,17 @@
"engine": "InnoDB",
"field_order": [
"title",
"is_published",
"disable_self_learning",
"column_break_3",
"short_code",
"video_link",
"image",
"column_break_3",
"tags",
"is_published",
"upcoming",
"disable_self_learning",
"section_break_5",
"short_introduction",
"description"
"description",
"chapters"
],
"fields": [
{
@@ -51,11 +48,6 @@
"fieldtype": "Check",
"label": "Published"
},
{
"fieldname": "short_code",
"fieldtype": "Data",
"label": "Short Code"
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
@@ -80,6 +72,28 @@
"fieldname": "disable_self_learning",
"fieldtype": "Check",
"label": "Disable Self Learning"
},
{
"fieldname": "image",
"fieldtype": "Attach Image",
"label": "Preview Image"
},
{
"fieldname": "tags",
"fieldtype": "Data",
"label": "Tags"
},
{
"default": "0",
"fieldname": "upcoming",
"fieldtype": "Check",
"label": "Is an Upcoming Course"
},
{
"fieldname": "chapters",
"fieldtype": "Table",
"label": "Chapters",
"options": "Chapters"
}
],
"index_web_pages_for_search": 1,
@@ -99,14 +113,9 @@
"group": "Mentors",
"link_doctype": "LMS Course Mentor Mapping",
"link_fieldname": "course"
},
{
"group": "Mentors",
"link_doctype": "LMS Mentor Request",
"link_fieldname": "course"
}
],
"modified": "2021-06-21 11:34:04.552376",
"modified": "2021-07-28 19:01:50.677445",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Course",
@@ -129,6 +138,5 @@
"sort_field": "creation",
"sort_order": "DESC",
"title_field": "title",
"track_changes": 1,
"track_views": 1
"track_changes": 1
}

View File

@@ -8,9 +8,11 @@ from frappe.model.document import Document
import json
from ...utils import slugify
from community.query import find, find_all
from frappe.utils import flt
from frappe.utils import flt, cint
from ...utils import slugify
class LMSCourse(Document):
@staticmethod
def find(name):
"""Returns the course with specified name.
@@ -72,8 +74,11 @@ class LMSCourse(Document):
mentors = frappe.get_all("LMS Course Mentor Mapping", {"course": self.name}, ["mentor"])
for mentor in mentors:
member = frappe.get_doc("User", mentor.mentor)
# TODO: change this to count query
member.batch_count = len(frappe.get_all("LMS Batch Membership", {"member": member.name, "member_type": "Mentor"}))
member.batch_count = frappe.db.count("LMS Batch Membership",
{
"member": member.name,
"member_type": "Mentor"
})
course_mentors.append(member)
return course_mentors
@@ -112,8 +117,55 @@ class LMSCourse(Document):
def get_chapters(self):
"""Returns all chapters of this course.
"""
# TODO: chapters should have a way to specify the order
return find_all("Chapter", course=self.name, order_by="index_")
chapters = []
for row in self.chapters:
chapter_details = frappe.db.get_value("Chapter", row.chapter,
["name", "title", "description"],
as_dict=True)
chapter_details.idx = row.idx
chapters.append(chapter_details)
return chapters
def get_lessons(self, chapter=None):
""" If chapter is passed, returns lessons of only that chapter.
Else returns lessons of all chapters of the course """
lessons = []
if chapter:
return self.get_lesson_details(chapter)
for chapter in self.get_chapters():
lesson = self.get_lesson_details(chapter)
lessons += lesson
return lessons
def get_lesson_details(self, chapter):
lessons = []
lesson_list = frappe.get_all("Lessons", {"parent": chapter.name},
["lesson", "idx"], order_by="idx")
for row in lesson_list:
lesson_details = frappe.get_doc("Lesson", row.lesson)
lesson_details.number = flt("{}.{}".format(chapter.idx, row.idx))
lessons.append(lesson_details)
return lessons
def get_slugified_chapter_title(self, chapter):
return slugify(chapter)
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):
return find("LMS Batch", name=batch_name, course=self.name)
@@ -138,38 +190,18 @@ class LMSCourse(Document):
visibility="Public")
return batches
def get_chapter(self, index):
return find("Chapter", course=self.name, index_=index)
def get_lesson(self, chapter_index, lesson_index):
chapter_name = frappe.get_value(
"Chapter",
{"course": self.name, "index_": chapter_index},
"name")
lesson_name = chapter_name and frappe.get_value(
"Lesson",
{"chapter": chapter_name, "index_": lesson_index},
"name")
return lesson_name and frappe.get_doc("Lesson", lesson_name)
def get_lesson_index(self, lesson_name):
"""Returns the {chapter_index}.{lesson_index} for the lesson.
"""
lesson = frappe.get_doc("Lesson", lesson_name)
chapter = frappe.get_doc("Chapter", lesson.chapter)
return f"{chapter.index_}.{lesson.index_}"
lesson = frappe.db.get_value("Lessons", {"lesson": lesson_name}, ["idx", "parent"], as_dict=True)
if not lesson:
return None
def reindex_lessons(self):
for i, c in enumerate(self.get_chapters(), start=1):
c.index_ = i
c.save()
self._reindex_lessons_in_chapter(c)
chapter = frappe.db.get_value("Chapters", {"chapter": lesson.parent}, ["idx"], as_dict=True)
if not chapter:
return None
def _reindex_lessons_in_chapter(self, c):
for i, lesson in enumerate(c.get_lessons(), start=1):
lesson.index = i
lesson.index_label = f"{c.index_}.{i}"
lesson.save()
return f"{chapter.idx}.{lesson.idx}"
def reindex_exercises(self):
for i, c in enumerate(self.get_chapters(), start=1):
@@ -180,7 +212,7 @@ class LMSCourse(Document):
def _reindex_exercises_in_chapter(self, c):
i = 1
for lesson in c.get_lessons():
for lesson in self.get_lessons(c):
for exercise in lesson.get_exercises():
exercise.index_ = i
exercise.index_label = f"{c.index_}.{i}"
@@ -199,30 +231,22 @@ class LMSCourse(Document):
}
if batch:
filters["batch"] = batch
return frappe.db.get_value("LMS Batch Membership", filters, ["name","batch", "current_lesson"], as_dict=True)
def get_all_memberships(self, member=frappe.session.user):
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")
print(all_memberships)
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.
"""
@@ -239,58 +263,57 @@ class LMSCourse(Document):
member_names = [m['member'] for m in memberships]
return find_all("User", name=["IN", member_names])
def get_outline(self):
return CourseOutline(self)
def get_tags(self):
return self.tags.split(",") if self.tags else []
class CourseOutline:
def __init__(self, course):
self.course = course
self.chapters = self.get_chapters()
self.lessons = self.get_lessons()
def get_reviews(self):
reviews = frappe.get_all("LMS Course Review",
{
"course": self.name
},
["review", "rating", "owner"],
order_by= "creation desc")
def get_next(self, current):
current = flt(current)
numbers = sorted(lesson['number'] for lesson in self.lessons)
try:
index = numbers.index(current)
return numbers[index+1]
except IndexError:
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_prev(self, current):
def get_progress(self, lesson):
return frappe.db.get_value("LMS Course Progress",
{
"course": self.name,
"owner": frappe.session.user,
"lesson": lesson
},
["status"])
def get_neighbours(self, current, lessons):
current = flt(current)
numbers = sorted(lesson['number'] for lesson in self.lessons)
try:
index = numbers.index(current)
if index == 0:
return None
return numbers[index-1]
except IndexError:
return None
def get_chapters(self):
return frappe.db.get_all("Chapter",
filters={"course": self.course.name},
fields=["name", "title", "index_"],
order_by="index_")
def get_lessons(self):
chapters = [c['name'] for c in self.chapters]
lessons = frappe.db.get_all("Lesson",
filters={"chapter": ["IN", chapters]},
fields=["name", "title", "chapter", "index_"])
chapter_numbers = {c['name']: c['index_'] for c in self.chapters}
for lesson in lessons:
lesson['number'] = flt("{}.{}".format(chapter_numbers[lesson['chapter']], lesson['index_']))
return lessons
@frappe.whitelist()
def reindex_lessons(doc):
course_data = json.loads(doc)
course = frappe.get_doc("LMS Course", course_data['name'])
course.reindex_lessons()
frappe.msgprint("All lessons in this course have been re-indexed.")
numbers = sorted(lesson.number for lesson in lessons)
index = numbers.index(current)
return {
"prev": numbers[index-1] if index-1 >= 0 else None,
"next": numbers[index+1] if index+1 < len(numbers) else None
}
@frappe.whitelist()
def reindex_exercises(doc):

View File

@@ -2,7 +2,13 @@
// For license information, please see license.txt
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,
}
};
});
},
});

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2021, FOSS United and contributors
// For license information, please see license.txt
frappe.ui.form.on('LMS Course Review', {
// refresh: function(frm) {
// }
});

View File

@@ -0,0 +1,57 @@
{
"actions": [],
"creation": "2021-06-28 13:36:36.146718",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"review",
"rating",
"course"
],
"fields": [
{
"fieldname": "review",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Review"
},
{
"fieldname": "rating",
"fieldtype": "Rating",
"in_list_view": 1,
"label": "Rating"
},
{
"fieldname": "course",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Course",
"options": "LMS Course"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-07-05 14:57:03.841430",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Course Review",
"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
}

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

View File

@@ -1,10 +1,8 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, FOSS United and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestLMSSketch(unittest.TestCase):
class TestLMSCourseReview(unittest.TestCase):
pass

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

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

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2021, FOSS United and contributors
// For license information, please see license.txt
frappe.ui.form.on('LMS Sketch', {
frappe.ui.form.on('LMS Quiz', {
// refresh: function(frm) {
// }

View File

@@ -1,45 +1,42 @@
{
"actions": [],
"autoname": "format:SKETCH-{#}",
"creation": "2021-03-09 16:31:50.523524",
"autoname": "field:title",
"creation": "2021-06-07 10:50:17.893625",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"title",
"runtime",
"code",
"svg"
"questions",
"lesson"
],
"fields": [
{
"fieldname": "title",
"fieldtype": "Data",
"label": "Title"
"label": "Title",
"unique": 1
},
{
"fieldname": "runtime",
"fieldtype": "Data",
"label": "Runtime"
"fieldname": "questions",
"fieldtype": "Table",
"label": "Questions",
"options": "LMS Quiz Question"
},
{
"fieldname": "code",
"fieldtype": "Code",
"label": "Code"
},
{
"fieldname": "svg",
"fieldtype": "Code",
"label": "SVG",
"fieldname": "lesson",
"fieldtype": "Link",
"label": "Lesson",
"options": "Lesson",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-03-12 08:42:56.671658",
"modified": "2021-07-23 19:06:12.551633",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Sketch",
"name": "LMS Quiz",
"owner": "Administrator",
"permissions": [
{
@@ -57,6 +54,5 @@
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_views": 1
"track_changes": 1
}

View File

@@ -0,0 +1,75 @@
# 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 quiz_summary(quiz, results):
score = 0
results = json.loads(results)
for result in results:
correct = result["is_correct"][0]
result["question"] = frappe.db.get_value("LMS Quiz Question",
{"parent": quiz, "idx": result["question_index"]},
["question"])
for point in result["is_correct"]:
correct = correct and point
result["result"] = "Right" if correct else "Wrong"
score += correct
del result["is_correct"]
del result["question_index"]
frappe.get_doc({
"doctype": "LMS Quiz Submission",
"quiz": quiz,
"result": results,
"score": score
}).save(ignore_permissions=True)
return score

View File

@@ -0,0 +1,41 @@
# Copyright (c) 2021, FOSS United and Contributors
# See license.txt
# import frappe
import unittest
import frappe
class TestLMSQuiz(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
frappe.get_doc({
"doctype": "LMS Quiz",
"title": "Test Quiz"
}).save()
def test_with_multiple_options(self):
quiz = frappe.get_doc("LMS Quiz", "Test Quiz")
quiz.append("questions", {
"question": "Question multiple",
"option_1": "Option 1",
"is_correct_1": 1,
"option_2": "Option 2",
"is_correct_2": 1
})
quiz.save()
self.assertTrue(quiz.questions[0].multiple)
def test_with_no_correct_option(self):
quiz = frappe.get_doc("LMS Quiz", "Test Quiz")
quiz.append("questions", {
"question": "Question no correct option",
"option_1": "Option 1",
"option_2": "Option 2",
})
self.assertRaises(frappe.ValidationError, quiz.save)
@classmethod
def tearDownClass(cls) -> None:
frappe.db.delete("LMS Quiz", "Test Quiz")
frappe.db.delete("LMS Quiz Question", {"parent": "Test Quiz"})

View File

@@ -0,0 +1,166 @@
{
"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",
"column_break_5",
"explanation_1",
"section_break_5",
"option_2",
"is_correct_2",
"column_break_10",
"explanation_2",
"column_break_4",
"option_3",
"is_correct_3",
"column_break_15",
"explanation_3",
"section_break_11",
"option_4",
"is_correct_4",
"column_break_20",
"explanation_4",
"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"
},
{
"depends_on": "option_1",
"fieldname": "explanation_1",
"fieldtype": "Data",
"label": "Explanation"
},
{
"depends_on": "option_2",
"fieldname": "explanation_2",
"fieldtype": "Data",
"label": "Explanation"
},
{
"depends_on": "option_3",
"fieldname": "explanation_3",
"fieldtype": "Data",
"label": "Explanation"
},
{
"depends_on": "option_4",
"fieldname": "explanation_4",
"fieldtype": "Data",
"label": "Explanation"
},
{
"fieldname": "column_break_5",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_10",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_15",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_20",
"fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-07-19 19:35:28.446236",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Quiz Question",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

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

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

View 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

View File

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

View File

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

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

View File

@@ -0,0 +1,8 @@
# Copyright (c) 2021, FOSS United and Contributors
# See license.txt
# import frappe
import unittest
class TestLMSQuizSubmission(unittest.TestCase):
pass

View File

@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, FOSS United and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
class LMSSection(Document):
def __repr__(self):
return f"<LMSSection {self.label!r}>"
def get_exercise(self):
if self.type == "exercise":
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):
"""Returns the latest code for the logged in user.
"""
if not frappe.session.user or frappe.session.user == "Guest":
return self.contents
result = frappe.get_all('Code Revision',
fields=["code"],
filters={
"author": frappe.session.user,
"section": self.name
},
order_by="creation desc",
page_length=1)
if result:
return result[0]['code']
else:
return self.contents

View File

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

View File

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

View File

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

View File

@@ -36,6 +36,8 @@ def find_macros(text):
('Exercise', 'four-circles')
]
"""
if not text:
return []
macros = re.findall(MACRO_RE, text)
# remove the quotes around the argument
return [(name, _remove_quotes(arg)) for name, arg in macros]
@@ -91,11 +93,11 @@ class MacroInlineProcessor(InlineProcessor):
macro = m.group(1)
arg = m.group(2)
html = render_macro(macro, arg)
html = sanitize_html(str(html))
html = sanitize_html(str(html), macro)
e = etree.fromstring(html)
return e, m.start(0), m.end(0)
def sanitize_html(html):
def sanitize_html(html, macro):
"""Sanotize the html using BeautifulSoup.
The markdown processor request the correct markup and crashes on
@@ -104,4 +106,7 @@ def sanitize_html(html):
"""
soup = BeautifulSoup(html, features="lxml")
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>"

View File

@@ -1,5 +1,4 @@
"""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_sketch.lms_sketch import LMSSketch as Sketch
from .doctype.lms_batch_membership.lms_batch_membership import LMSBatchMembership as Membership

View File

@@ -0,0 +1,14 @@
frappe.ready(function () {
frappe.web_form.after_load = () => {
if (!frappe.utils.get_url_arg("name")) {
window.location.href = `/edit-profile?name=${frappe.session.user}`;
}
}
frappe.web_form.after_save = () => {
setTimeout(() => {
window.location.href = `/${frappe.web_form.get_value(["username"])}`;
})
}
})

View File

@@ -0,0 +1,212 @@
{
"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",
"client_script": "",
"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-08-06 14:40:39.013776",
"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,
"fieldname": "username",
"fieldtype": "Data",
"hidden": 0,
"label": "Username",
"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
}
]
}

View File

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

View File

@@ -1,65 +0,0 @@
<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 %}
{% set all_memberships = course.get_all_memberships() %}
{% if all_memberships | length > 1 %}
<a class="nav-link pull-right" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="false">
Switch Batch
</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>
{% endif %}
</div>
{% if not membership %}
{% set display_class = "hide" %}
{% else %}
{% set display_class = "" %}
{% endif %}
<ul class="nav nav-tabs mt-4">
<li class="nav-item">
<a class="nav-link" id="home" href="/courses/{{course.name}}/home{{ course.query_parameter }}">Home</a>
</li>
<li class="nav-item">
{% 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 class="nav-item">
<a class="nav-link" id="schedule" href="/courses/{{course.name}}/schedule">Schedule</a>
</li> -->
<li class="nav-item {{ display_class }}">
<a class="nav-link" id="members" href="/courses/{{course.name}}/members{{ course.query_parameter }}">Members</a>
</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">
<a class="nav-link" id="about" href="/courses/{{course.name}}/about">About</a>
</li> -->
{% if membership and membership.batch and course.is_mentor(frappe.session.user) %}
<li class="nav-item">
<a class="nav-link" id="progress" href="/courses/{{course.name}}/progress{{ course.query_parameter }}">Progress</a>
</li>
{% endif %}
</ul>
{% block script %}
<script>
frappe.ready(() => {
var selector = document.querySelector(`a[href="${decodeURIComponent(window.location.pathname)}{{ course.query_parameter }}"]`)
if (selector) {
selector.classList.add('active');
}
else {
$("#learn").addClass('active')
}
})
</script>
{% endblock %}

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

View File

@@ -1,53 +1,100 @@
<div class="chapter-teaser">
<div class="teaser-body">
<div class="chapter-title mb-5 font-weight-bold"><span class="mr-1">{{index}}.</span> {{ chapter.title }}</div>
<div class="chapter-description">
{{ chapter.description or "" }}
<div>
<div class="small-title chapter-title" data-target="#{{ course.get_slugified_chapter_title(chapter.title) }}"
data-toggle="collapse" aria-expanded="false">
<img class="chapter-icon" src="/assets/community/icons/chevron-right.svg">
{{ index }}. {{ chapter.title }}
</div>
<div class="chapter-content collapse navbar-collapse" id="{{ course.get_slugified_chapter_title(chapter.title) }}">
{% if chapter.description %}
<div class="chapter-description muted-text">
{{ chapter.description }}
</div>
<div class="chapter-lessons">
{% for lesson in chapter.get_lessons() %}
<div class="lesson-teaser">
<a {% if show_link or lesson.include_in_preview %}
href="{{ course.get_learn_url(course.get_lesson_index(lesson.name)) }}{{course.query_parameter}}" {% else %} href="" class="no-preview"
{% endif %} data-course="{{ course.name }}">{{ lesson.title }}</a>
{% if show_progress and not course.is_mentor(frappe.session.user) and lesson.get_progress() %}
<span class="ml-5 badge p-2 {{ lesson.get_slugified_class() }}"> {{ lesson.get_progress() }}</span>
{% endif %}
<div class="lessons">
{% for lesson in course.get_lessons(chapter) %}
<div class="lesson-info{% if membership.current_lesson == lesson.name %} active-lesson {% endif %}">
{% if membership or lesson.include_in_preview %}
<a class="lesson-links"
href="{{ course.get_learn_url(lesson.number) }}{{course.query_parameter}}"
data-course="{{ course.name }}">
{{ lesson.title }}
{% if membership %}
<img class="ml-1 lesson-progress-tick {{ course.get_progress(lesson.name) != 'Complete' and 'hide' }}"
src="/assets/community/icons/check.svg">
{% 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>
{% endfor %}
</div>
</div>
</div>
{% if index != course.get_chapters() | length %}
<div class="card-divider"></div>
{% endif %}
<script>
frappe.ready(() => {
var d;
$(".no-preview").click((e) => {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
var message = __("Please enroll for this course to access the lesson.");
var label = __("Checkout Upcoming Batches");
var action = "checkout_upcoming_batches";
d = frappe.msgprint({
title: __("This lesson is not available for preview!"),
message: message,
primary_action: {
"label": label,
"client_action": action,
}
});
})
window.redirect_to_login = () => {
window.location.href = `/login?redirect-to=/courses/${$(".no-preview").attr("data-course")}`
}
window.checkout_upcoming_batches = () => {
if ($(".upcoming").length > 0) {
$('html,body').animate({ scrollTop: $(".upcoming").offset().top }, 300);
}
frappe.hide_msgprint();
}
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>

View File

@@ -0,0 +1,90 @@
<div class="common-card-style course-card">
<div class="course-image {% if not course.image %}default-image{% endif %}"
{% if course.image %} style="background-image: url( {{ course.image }} );" {% endif %}>
<div class="course-tags">
{% for tag in course.get_tags() %}
<div class="course-card-pills">{{ tag }}</div>
{% endfor %}
</div>
{% if not course.image %}
<div class="default-image-text">{{ course.title[0] }}</div>
{% endif %}
</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 course.upcoming %}
<div class="view-course-link is-default">
Upcoming Course <img class="ml-3" src="/assets/community/icons/black-arrow.svg" />
</div>
<a class="stretched-link" href="/courses/{{ course.name }}"></a>
{% elif 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>

View File

@@ -1,7 +1,12 @@
<div class="mt-5">
<h3> Course Outline </h3>
{% 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)}}
{% endfor %}
{% 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() %}
{{ widgets.ChapterTeaser(index=loop.index, chapter=chapter, course=course, batch=batch, membership=membership) }}
{% endfor %}
</div>
</div>
{% endif %}

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

View File

@@ -1,30 +0,0 @@
<div class="batch">
<div class="batch-details">
<div class="">Session every {{batch.sessions_on}}</div>
<div>{{frappe.utils.format_time(batch.start_time, "short")}} -
{{frappe.utils.format_time(batch.end_time, "short")}}
</div>
<div>Starting {{frappe.utils.format_date(batch.start_date, "medium")}}</div>
<div class="course-type" style="color: #888; padding: 10px 0px;">mentors</div>
{% for m in course.get_mentors(batch.name) %}
<div>
{{ widgets.Avatar(member=m, avatar_class="avatar-medium" ) }}
<span class="instructor-title">{{m.full_name}}</span>
</div>
{% endfor %}
</div>
{% if can_manage or can_join %}
<div class="cta">
<div class="">
{% if can_manage %}
<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 %}
<button class="join-batch btn btn-secondary" data-batch="{{ batch.name | urlencode }}"
data-course="{{ course.name | urlencode }}">Join this Batch</button>
{% endif %}
</div>
</div>
{% endif %}
</div>

View File

@@ -0,0 +1,91 @@
<div class="reviews-parent">
{% set reviews = course.get_reviews() %}
{% if reviews | length or course.is_eligible_to_review(membership) %}
<div class="mb-5">
<span class="course-home-headings">Reviews</span>
{% if course.is_eligible_to_review(membership) %}
<span class="review-link button is-secondary pull-right">
Write a review
</span>
{% endif %}
</div>
{% endif %}
{% if reviews | length %}
<div class="reviews-section">
{% for review in 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") }}
<a class="button-links" href="/{{review.owner_details.username}}">
<span class="course-instructor">
{{ review.owner_details.full_name }}
</span>
</a>
</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>
{% endif %}
</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">&times;</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>

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

View File

@@ -0,0 +1,63 @@
# Copyright (c) 2021, FOSS United and Contributors
# See license.txt
# import frappe
import unittest
import frappe
class TestCustomUser(unittest.TestCase):
def test_with_basic_username(self):
new_user = frappe.get_doc({
"doctype": "User",
"email": "test_with_basic_username@example.com",
"first_name": "Username"
}).insert()
self.assertEqual(new_user.username, "username")
def test_without_username(self):
""" The user in this test has the same first name as the user of the test test_with_basic_username.
In such cases frappe makes the username of the second user empty.
The condition in community app should override this and save a username. """
new_user = frappe.get_doc({
"doctype": "User",
"email": "test-without-username@example.com",
"first_name": "Username"
}).insert()
self.assertTrue(new_user.username)
def test_with_illegal_characters(self):
new_user = frappe.get_doc({
"doctype": "User",
"email": "test_with_illegal_characters@example.com",
"first_name": "Username$$"
}).insert()
self.assertEqual(new_user.username[:8], "username")
def test_with_underscore_at_end(self):
new_user = frappe.get_doc({
"doctype": "User",
"email": "test_with_underscore_at_end@example.com",
"first_name": "Username___"
}).insert()
self.assertNotEqual(new_user.username[-1], "_")
def test_with_short_first_name(self):
new_user = frappe.get_doc({
"doctype": "User",
"email": "test_with_short_first_name@example.com",
"first_name": "USN"
}).insert()
self.assertGreaterEqual(len(new_user.username), 4)
@classmethod
def tearDownClass(cls) -> None:
users = [
"test_with_basic_username@example.com",
"test-without-username@example.com",
"test_with_illegal_characters@example.com",
"test_with_underscore_at_end@example.com",
"test_with_short_first_name@example.com"
]
frappe.db.delete("User", {"name": ["in", users]})

View File

@@ -2,18 +2,70 @@ import frappe
from frappe.core.doctype.user.user import User
from frappe.utils import cint
import hashlib
import random
import re
from frappe import _
class CustomUser(User):
def get_course_count(self) -> int:
def validate(self):
super(CustomUser, self).validate()
self.validate_username_characters()
def validate_username_characters(self):
if len(self.username):
underscore_condition = self.username[0] == "_" or self.username[-1] == "_"
else:
underscore_condition = ''
if self.is_new():
if not self.username:
self.username = self.get_username_from_first_name()
if self.username.find(" "):
self.username.replace(" ", "")
if not re.match("^[A-Za-z0-9_]*$", self.username) or underscore_condition:
self.username = self.remove_illegal_characters()
if len(self.username) < 4:
self.username = self.email.replace("@", "").replace(".", "")
while self.username_exists():
self.username = self.remove_illegal_characters() + str(random.randint(0, 99))
else:
if not self.username:
frappe.throw(_("Username already exists."))
if not re.match("^[A-Za-z0-9_]*$", self.username):
frappe.throw(_("Username can only contain alphabets, numbers and unedrscore."))
if underscore_condition:
frappe.throw(_("First and Last character of username cannot be Underscore(_)."))
if len(self.username) < 4:
frappe.throw(_("Username cannot be less than 4 characters"))
def get_username_from_first_name(self):
return frappe.scrub(self.first_name) + str(random.randint(0, 99))
def remove_illegal_characters(self):
return re.sub("[^\w]+", "", self.username).strip("_")
def get_authored_courses(self) -> int:
"""Returns the number of courses authored by this user.
"""
return frappe.db.count(
return frappe.get_all(
'LMS Course', {
'owner': self.email
'owner': self.name,
'is_published': True
})
def get_palette(self):
"""
Returns a color unique to each member for Avatar """
palette = [
['--orange-avatar-bg', '--orange-avatar-color'],
['--pink-avatar-bg', '--pink-avatar-color'],
@@ -40,3 +92,35 @@ class CustomUser(User):
'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 """
mentored_courses = []
mapping = frappe.get_all("LMS Course Mentor Mapping",
{
"mentor": self.name,
},
["name", "course"]
)
for map in mapping:
if frappe.db.get_value("LMS Course", map.course, "is_published"):
mentored_courses.append(map)
return mentored_courses

View File

@@ -6,3 +6,4 @@ community.patches.replace_member_with_user_in_batch_membership
community.patches.replace_member_with_user_in_course_mentor_mapping
community.patches.replace_member_with_user_in_lms_message
community.patches.replace_member_with_user_in_mentor_request
community.patches.v0_0.chapter_lesson_index_table

View File

@@ -0,0 +1,50 @@
from __future__ import unicode_literals
import frappe
def execute():
frappe.reload_doc("lms", "doctype", "lms_course")
frappe.reload_doc("lms", "doctype", "chapter")
frappe.reload_doc("lms", "doctype", "lesson")
frappe.reload_doc("lms", "doctype", "lessons")
frappe.reload_doc("lms", "doctype", "chapters")
update_chapters()
update_lessons()
def update_chapters():
courses = frappe.get_all("LMS Course", pluck="name")
for course in courses:
course_details = frappe.get_doc("LMS Course", course)
chapters = frappe.get_all("Chapter",
{
"course": course
},
["name"],
order_by= "index_"
)
for chapter in chapters:
course_details.append("chapters",
{
"chapter": chapter.name
})
course_details.save()
def update_lessons():
chapters = frappe.get_all("Chapter", pluck="name")
for chapter in chapters:
chapter_details = frappe.get_doc("Chapter", chapter)
lessons = frappe.get_all("Lesson",
{
"chapter": chapter
},
["name"],
order_by= "index_"
)
for lesson in lessons:
chapter_details.append("lessons",
{
"lesson": lesson.name
})
chapter_details.save()

View File

@@ -14,6 +14,8 @@ The PageExtension is used to load additinal stylesheets and scripts to
be loaded in a webpage.
"""
import frappe
class PageExtension:
"""PageExtension is a plugin to inject custom styles and scripts
into a web page.
@@ -64,3 +66,43 @@ class ProfileTab:
Every subclass must implement this.
"""
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>
"""

View File

@@ -1,4 +1,3 @@
@import "./style.css";
@import "./vars.css";
@import "./style.less";

File diff suppressed because it is too large Load Diff

View File

@@ -1,353 +0,0 @@
h2 {
margin: 20px 0px;
color: black;
}
.teaser {
background: white;
border-radius: 9px;
border: 1px solid #C4C4C4;
.teaser-body {
padding: 20px;
box-shadow: 0px 5px 10px rgb(0 0 0 / 10%)
}
.teaser-footer {
padding: 20px;
}
}
.sketch-teaser {
.teaser();
width: 220px;
margin-bottom: 30px;
margin-top: 30px;
svg {
width: 200px;
height: 200px;
}
.sketch-image {
padding: 10px;
}
.sketch-footer {
border-top: 1px solid#C4C4C4;
padding: 10px;
background: #F6F6F6;
border-radius: 0px 0px 10px 10px;
}
}
.course-teaser {
.teaser();
color: #444;
margin-bottom: 20px;
margin-top: 20px;
h3, h4 {
color: black;
font-weight: bold;
}
.course-body, .course-footer {
padding: 20px;
}
.course-body {
min-height: 8em;
}
.course-footer {
border-top: 1px solid #ddd;
}
a, a:hover {
color: inherit;
text-decoration: none;
}
}
.anchor_style {
color: inherit;
}
.anchor_style:hover {
text-decoration: none
}
.no-preview:hover {
cursor: pointer;
color: #2490ef;
}
section {
padding: 60px 0px;
}
section h2 {
margin-bottom: 40px;
font-size: 48px;
line-height: 58px;
font-weight: bold;
}
section.lightgray {
background: #F6F6F6;
}
#hero .jumbotron {
background: inherit;
}
.chapter-teaser {
.teaser();
color: #444;
margin: 20px 0px;
h3, h4 {
color: black;
font-weight: bold;
}
}
.field-width {
width: 40%;
display: inline-block;
}
.footer-grouped-links {
display: none;
}
.footer-info {
border-top: 0px;
margin-top: 0px;
.footer-col-right {
padding-top: 1.8rem;
}
}
.web-footer {
border-top: 1px solid #E2E6E9;
padding: 0px;
padding: 2rem 0px;
margin-top: 2rem;
}
.course-type {
text-transform: uppercase;
font-size: 1.0em;
font-weight: bold;
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 {
color: black;
}
.instructor-subtitle {
font-size: 0.8em;
color: var(--text-color);
}
// .mentors-wrapper {
// .gray-section();
// }
.chapter-number {
background: var(--text-color);
color: white;
height: 24px;
min-width: 24px;
margin-right: 5px;
}
.sidebar {
background: var(--sidebar-bg);
border: 1px solid var(--sidebar-border);
margin: 20px 0px;
border-radius: 10px;
padding: 1px 20px 20px 20px;
}
.sidebar h3 {
margin-top: 20px;
color: black;
}
.sidebar-batch {
background: var(--sidebar-bg);
color: var(--text-color);
position: fixed;
left: 0;
height: 100%;
}
.sidebar-batch a {
padding: 16px 8px 8px 16px;
display: block;
}
.sidebar .notice {
margin-top: 10px;
padding: 10px;
border-radius: 10px;
border: 1px dashed var(--text-color);
}
.sidebar .notice a {
color: inherit;
text-decoration: underline;
}
// LiveCode editor
.livecode-editor {
.CodeMirror {
border: 1px solid #ddd;
background: #ffe;
height: auto;
}
.CodeMirror-scroll {
max-height: 310px;
min-height: 310px;
}
.controls {
padding: 10px 0px;
}
canvas {
border: 5px solid #ddd;
position: relative;
z-index: 0;
}
.output {
position: absolute;
z-index: 1;
width: 300px;
left: 0px;
top: 0px;
background-color: rgba(255, 255, 255, 0);
max-height: 300px;
white-space: pre-wrap;
word-wrap: break-word;
margin: 0px;
margin-left: 20px;
padding: 4px;
color: #888;
}
@media (max-width: 768px) {
.canvas-wrapper {
padding-top: 10px;
}
.code-wrapper {
min-height: 50px;
}
.CodeMirror {
min-height: 50px;
}
}
}
.sketch-header {
input#sketch-title {
font-weight: bold;
}
}
.chapter-description {
margin-bottom: 10px;
}
.lesson-teaser {
line-height: 40px;
}
#hero h1 {
color: black !important;
}
.lesson-page {
margin: 20px 0px;
}
.lesson-pagination {
clear: both;
}
.exercise-image svg {
width: 200px;
height: 200px;
border: 1px solid #ddd;
margin-bottom: 20px;
}
.svg-200 svg {
width: 200px;
height: 200px;
}
.livecode-editor-small .livecode-editor {
.CodeMirror-scroll {
max-height: 160px;
min-height: 160px;
}
canvas {
width: 150px;
height: 150px;
}
}
.mentor-dashboard {
margin-top: 20px;
.submission {
margin: 40px 0px 0px 20px;
}
}
.no-preview-message {
width: fit-content;
margin: 50px 0px 50px;
color: black;
}

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" fill="#68D391" stroke="#68D391" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 12H16" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 425 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-minus-circle"><circle cx="12" cy="12" r="10"></circle><line x1="8" y1="12" x2="16" y2="12"></line></svg>

After

Width:  |  Height:  |  Size: 309 B

View 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

View 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

View 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

View 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

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check"><polyline points="20 6 9 17 4 12"></polyline></svg>

After

Width:  |  Height:  |  Size: 262 B

View 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

View 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

View 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

View File

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 20C15.5228 20 20 15.5228 20 10C20 4.47715 15.5228 0 10 0C4.47715 0 0 4.47715 0 10C0 15.5228 4.47715 20 10 20ZM6.0429 6.04289C6.43342 5.65237 7.06659 5.65237 7.45711 6.04289L10.0011 8.58692L12.5451 6.04294C12.9356 5.65242 13.5688 5.65242 13.9593 6.04294C14.3499 6.43347 14.3499 7.06663 13.9593 7.45716L11.4154 10.0011L13.9593 12.5451C14.3499 12.9356 14.3499 13.5688 13.9593 13.9593C13.5688 14.3499 12.9357 14.3499 12.5451 13.9593L10.0011 11.4154L7.45711 13.9594C7.06659 14.3499 6.43342 14.3499 6.0429 13.9594C5.65237 13.5689 5.65237 12.9357 6.0429 12.5452L8.58693 10.0011L6.0429 7.45711C5.65237 7.06658 5.65237 6.43342 6.0429 6.04289Z" fill="#F56B6B"/>
</svg>

After

Width:  |  Height:  |  Size: 808 B

Some files were not shown because too many files have changed in this diff Show More