Compare commits

...

163 Commits

Author SHA1 Message Date
Jannat Patel
daeeb693d6 Merge pull request #519 from pateljannat/fix-username
fix: remove space from username
2023-05-22 17:54:53 +05:30
Jannat Patel
a0b06be422 test: fixed username length test 2023-05-19 18:03:36 +05:30
Jannat Patel
4b2ba96435 test: removed invalid tests 2023-05-19 17:46:55 +05:30
Jannat Patel
10510e204f fix: removed unused functions 2023-05-19 17:34:47 +05:30
Jannat Patel
032749dd01 fix: remove space from username 2023-05-19 16:38:47 +05:30
Jannat Patel
dc65bff772 Merge pull request #518 from pateljannat/telemetry-changes
fix: telemetry capture on client side
2023-05-18 14:52:35 +05:30
Jannat Patel
56266a3774 fix: load telemetry js irrespective of telemetry settings 2023-05-18 14:42:27 +05:30
Jannat Patel
93f0f8ab44 fix: telemetry capture on client side 2023-05-18 14:26:48 +05:30
Jannat Patel
611cc4d5a1 Merge pull request #515 from pateljannat/class-improvements
fix: Class improvements
2023-05-16 17:18:53 +05:30
Jannat Patel
552b0c9616 fix: sort classes by start date 2023-05-16 17:10:50 +05:30
Jannat Patel
1327b033e6 fix: removed student details doctype 2023-05-16 16:47:41 +05:30
Jannat Patel
ae42828771 fix: course and student interactions in class 2023-05-16 16:14:13 +05:30
Jannat Patel
1df6319164 chore: fixed conflicts 2023-05-15 19:44:50 +05:30
Jannat Patel
95012072fc Merge pull request #514 from pateljannat/social-links
fix: course details link preview
2023-05-15 14:11:52 +05:30
Jannat Patel
c01c248202 fix: course details link preview 2023-05-15 13:13:45 +05:30
Jannat Patel
0093025e5d Merge pull request #513 from pateljannat/style-issue
fix: change primary color back to frappe blue
2023-05-12 16:37:46 +05:30
Jannat Patel
20398cb934 fix: primary color 2023-05-12 16:29:39 +05:30
Jannat Patel
2792eb53b7 Merge pull request #511 from pateljannat/analytics
feat: analytics for course creation journey
2023-05-11 16:50:34 +05:30
Jannat Patel
b2f8f796b9 Merge pull request #512 from pateljannat/illegal-course
fix: course creation url in LMS
2023-05-11 16:38:23 +05:30
Jannat Patel
39bb3149c9 fix: course creation url in LMS 2023-05-11 16:26:09 +05:30
Jannat Patel
1e610f7fbb feat: analytics for course creation journey 2023-05-11 15:29:11 +05:30
Jannat Patel
c760fd5776 Merge pull request #510 from pateljannat/new-design-system
New design system
2023-05-11 12:53:45 +05:30
Jannat Patel
2162963926 fix: removed unnecessary lines of code 2023-05-11 12:40:43 +05:30
Jannat Patel
c3f3a110c0 fix: validations and UI 2023-05-11 12:01:32 +05:30
Jannat Patel
0e444ab7d3 test: fixed course creation test 2023-05-10 19:35:45 +05:30
Jannat Patel
752fe5b4ba feat: reorder chapters, lessons 2023-05-08 19:07:28 +05:30
Jannat Patel
dce369638a Merge pull request #509 from pateljannat/community-page-guest-access
fix: don't allow guest users to search the people page
2023-05-05 14:50:20 +05:30
Jannat Patel
911cfe9d2f Merge pull request #508 from pateljannat/share-certificate-on-creation
fix: share certificate with member on creation
2023-05-05 13:26:23 +05:30
Jannat Patel
f4e882ba3e fix: don't allow guest users to search the people page 2023-05-05 13:22:07 +05:30
Jannat Patel
211c69bb41 feat: Video and Header component for a lesson 2023-05-05 11:53:04 +05:30
Jannat Patel
b592172b82 Merge pull request #507 from pateljannat/class-seat-count
feat: Class seat count, start time and end time
2023-05-04 12:11:20 +05:30
Jannat Patel
40445cbb94 fix: share certificate with member on creation 2023-05-04 12:08:01 +05:30
Jannat Patel
20c93b3a6b fix: fetch start and end time in modal 2023-05-03 23:10:46 +05:30
Jannat Patel
e2bf324fb4 feat: start and end time in class 2023-05-03 23:08:02 +05:30
Jannat Patel
b47ff80e9d feat: seat count from class dialog 2023-05-03 22:47:39 +05:30
Jannat Patel
e10feb3c36 feat: seat count in class 2023-05-03 21:02:57 +05:30
Jannat Patel
b8471dd753 feat: paid classes and seat count 2023-05-03 20:55:49 +05:30
Jannat Patel
125c06952a Merge branch 'main' of https://github.com/frappe/lms into class-improvements 2023-05-03 17:41:44 +05:30
Jannat Patel
4336839932 feat: edit existing lesson 2023-05-03 17:41:18 +05:30
Jannat Patel
bbdfaa32e9 Merge branch 'main' of https://github.com/frappe/lms into new-design-system 2023-05-02 14:46:24 +05:30
Jannat Patel
4f52a73029 Merge pull request #506 from pateljannat/revert-text-editor
revert: text editor for lesson and course
2023-05-02 14:46:03 +05:30
Jannat Patel
1a2f693fea revert: text editor for lesson and course 2023-05-02 14:22:43 +05:30
Jannat Patel
ab8b76cada feat: lesson editor youtube and quiz components 2023-05-02 11:09:46 +05:30
Jannat Patel
b5240f0eec feat: class improvements 2023-04-27 22:29:45 +05:30
Jannat Patel
7777bd02e3 feat: lesson edit page 2023-04-27 10:20:50 +05:30
Jannat Patel
fcdd70dcc7 feat: course outline page 2023-04-26 11:46:08 +05:30
Jannat Patel
4eb5390ad8 feat: redesign lesson page 2023-04-25 20:54:57 +05:30
Jannat Patel
3b5c47222d feat: data in course fields 2023-04-21 18:17:37 +05:30
Jannat Patel
f97ae4e4e2 feat: adding editorjs 2023-04-21 09:58:59 +05:30
Jannat Patel
33db16a1a2 Merge branch 'new-editor' of https://github.com/pateljannat/lms into new-design-system 2023-04-20 17:55:36 +05:30
Jannat Patel
6232f8703e feat: new design system for exisitng course home 2023-04-20 17:55:03 +05:30
Jannat Patel
e6621ad866 Merge pull request #499 from pateljannat/certificate
feat: Certificate export as PDF
2023-04-18 18:45:34 +05:30
Jannat Patel
6a1533191a Merge branch 'main' into certificate 2023-04-18 18:14:41 +05:30
Jannat Patel
a0782c7bf7 feat: editor js import 2023-04-18 11:51:57 +05:30
Jannat Patel
2b6436915d feat: design for course list count 2023-04-18 09:14:59 +05:30
Jannat Patel
4c220a67f2 Merge pull request #498 from pateljannat/new-class-modal
feat: New Class Dialog
2023-04-17 21:52:43 +05:30
Jannat Patel
110aab00d6 fix: removed unnecessary code 2023-04-17 16:53:48 +05:30
Jannat Patel
8e8111d272 fix: certificate page UI 2023-04-17 15:00:18 +05:30
Jannat Patel
53d2e288d4 feat: certificate pdf and custom certificate 2023-04-14 17:38:24 +05:30
Jannat Patel
262c1ea371 feat: certificate download as pdf 2023-04-12 23:15:52 +05:30
Jannat Patel
7be9eb09e8 fix: exponse class info only to moderators 2023-04-11 17:09:38 +05:30
Jannat Patel
5fb7e88318 feat: edit class 2023-04-11 16:45:34 +05:30
Jannat Patel
d9c50714f4 fix: reload lesson assignment doctype in patch 2023-04-10 22:44:47 +05:30
Jannat Patel
0a91e5aa05 fix: reload question doctype in patch 2023-04-10 22:35:33 +05:30
Jannat Patel
9b70b4212f feat: class dialog 2023-04-10 22:34:24 +05:30
Jannat Patel
9f525d69b6 Merge pull request #497 from pateljannat/text-editor
feat: Text editor for course description and Lesson Content
2023-04-07 21:25:03 +05:30
Jannat Patel
b09c4753da fix: removed print statements 2023-04-07 21:06:16 +05:30
Jannat Patel
e4b4556210 feat: text editor for description 2023-04-06 18:01:12 +05:30
Jannat Patel
fafd132768 feat: text editor for lesson body 2023-04-05 14:13:18 +05:30
Jannat Patel
67dc6d1f29 Merge pull request #496 from pateljannat/cypress
test: Course Creation UI
2023-04-04 16:48:52 +05:30
Jannat Patel
969cb37cfe test: changed test user 2023-04-03 22:23:04 +05:30
Jannat Patel
94c2be9919 test: removed beforeEach 2023-04-03 22:13:26 +05:30
Jannat Patel
5089285913 test: mariadb password 2023-04-03 22:01:14 +05:30
Jannat Patel
e80920ad6c test: setup requirements 2023-04-03 21:46:25 +05:30
Jannat Patel
d6606ab898 test: site_config in helper 2023-04-03 21:41:30 +05:30
Jannat Patel
41599348c4 test: added install_dependencies and install script 2023-04-03 15:44:21 +05:30
Jannat Patel
92a8ce6ef4 test: course creation flow 2023-04-03 15:19:54 +05:30
Jannat Patel
690a86bb69 Merge branch 'main' of https://github.com/frappe/lms into cypress 2023-03-31 16:46:29 +05:30
Jannat Patel
cde4b61cea Merge pull request #494 from pateljannat/move-lessons
feat: move chapters and lessons
2023-03-31 16:45:26 +05:30
Jannat Patel
f361c42a30 feat: move chapters and lessons 2023-03-31 16:30:38 +05:30
Jannat Patel
065646ed5d test: cypress setup 2023-03-29 17:13:51 +05:30
Jannat Patel
de399166f1 fix: only allow class students to see progress 2023-03-28 16:30:14 +05:30
Jannat Patel
a3a4d7fbd0 Merge pull request #493 from pateljannat/notification-on-assignment-submission
feat: notification on assignment submission
2023-03-28 16:03:23 +05:30
Jannat Patel
a7c1595978 fix: spacing in email 2023-03-27 17:38:26 +05:30
Jannat Patel
1a8d113ad8 feat: notification on assignment submission 2023-03-27 17:32:43 +05:30
Jannat Patel
72cbbf147f Merge pull request #491 from pateljannat/quiz-input
feat: quiz with user input
2023-03-27 13:04:39 +05:30
Jannat Patel
a0e6462c13 fix: new question with possible answers 2023-03-27 12:53:14 +05:30
Jannat Patel
b37f259804 test: quiz with no possible answer 2023-03-27 08:47:07 +05:30
Jannat Patel
2fbe5dacb2 feat: user input quiz portal form 2023-03-24 18:06:42 +05:30
Jannat Patel
3150cf2510 feat: quiz with user input 2023-03-23 22:22:57 +05:30
Jannat Patel
3b1b375d5b Merge pull request #487 from pateljannat/live-class
feat: live class
2023-03-16 12:30:05 +05:30
Jannat Patel
3c0a29d4c7 fix: removed unnecessary changes 2023-03-16 12:24:12 +05:30
Jannat Patel
817bc4441f feat: ui for class cards 2023-03-16 11:23:24 +05:30
Jannat Patel
07e1aaaa66 fix: removed print statements 2023-03-15 12:15:44 +05:30
Jannat Patel
35b77a8908 feat: timezone and recording 2023-03-15 10:52:05 +05:30
Jannat Patel
d5b4af95ff Merge branch 'main' of https://github.com/frappe/lms into live-class 2023-03-14 14:36:22 +05:30
Jannat Patel
fc6a50b13f docs: docker instructions update in readme 2023-03-14 13:19:27 +05:30
Jannat Patel
8415abec6e Merge pull request #489 from pateljannat/delete-roles-after-uninstall
fix: delete roles after uninstall
2023-03-14 11:01:50 +05:30
Jannat Patel
ea9ca67d1e fix: delete using before_uninstall hook 2023-03-14 10:56:08 +05:30
Jannat Patel
8201506c5f fix: delete roles after uninstall 2023-03-14 10:32:31 +05:30
Jannat Patel
5fc879b0ef fix: formatting 2023-03-13 18:14:14 +05:30
Jannat Patel
bfde847045 Merge pull request #488 from pateljannat/session_redirection_issues
fix: redirection after completing setup wizard
2023-03-13 18:03:42 +05:30
Jannat Patel
d96e3f4f9f fix: don't show past classes 2023-03-13 18:03:03 +05:30
Jannat Patel
0593a9fb30 fix: add student section 2023-03-13 15:47:51 +05:30
Jannat Patel
c03cca21e8 fix: redirection after completing setup wizard 2023-03-10 23:08:00 +05:30
Jannat Patel
170b1b0dcc fix: live class card layout 2023-03-09 21:40:49 +05:30
Jannat Patel
cb9c7966d9 fix: uncomment live class event creation 2023-03-08 10:19:44 +05:30
Jannat Patel
b9c2222951 feat: create event for live class 2023-03-06 19:45:54 +05:30
Jannat Patel
dfef5ca26c Merge branch 'main' of https://github.com/frappe/lms into live-class 2023-03-03 18:45:39 +05:30
Jannat Patel
bce60a8657 Merge pull request #484 from pateljannat/mini-fix 2023-03-03 18:35:36 +05:30
Jannat Patel
2b244bb4f4 test: fix lms exercise doctype name 2023-03-03 18:23:49 +05:30
Jannat Patel
31b08eb545 test: fix lms exercise doctype name 2023-03-03 18:16:29 +05:30
Jannat Patel
9b7817a57f fix: exercise conflict and progress member 2023-03-03 18:03:35 +05:30
Jannat Patel
bb0f3d5962 feat: live class list display 2023-03-03 15:15:49 +05:30
Jannat Patel
23c78d5801 Merge branch 'main' of https://github.com/frappe/lms 2023-03-02 22:48:29 +05:30
Jannat Patel
cd3236976f fix: class list settings 2023-03-02 22:48:26 +05:30
Jannat Patel
e6096bf9ed feat: live class modal 2023-03-02 19:33:12 +05:30
Jannat Patel
a502603915 Merge pull request #483 from pateljannat/certificate-request 2023-03-01 17:49:37 +05:30
Jannat Patel
7566565f55 fix: certificate request notification 2023-02-28 19:55:21 +05:30
Faris Ansari
f56f0b5366 Update README.md 2023-02-28 13:21:47 +05:30
Jannat Patel
34870b4625 feat: live class 2023-02-28 09:19:37 +05:30
Jannat Patel
3ee592a989 Merge pull request #482 from pateljannat/misc-fix 2023-02-27 09:13:59 +05:30
Jannat Patel
d6d7e05b51 fix: quiz and assignment submission 2023-02-27 09:05:25 +05:30
Jannat Patel
07eaec2ded Merge pull request #480 from pateljannat/class-fixes 2023-02-23 14:12:44 +05:30
Jannat Patel
296a7e6023 fix: translation syntax 2023-02-23 13:04:47 +05:30
Jannat Patel
54827edd7e fix: modified timestamp 2023-02-23 13:01:45 +05:30
Jannat Patel
d87fb81cf3 fix: class issues 2023-02-23 12:40:07 +05:30
Jannat Patel
99a7c47798 Merge pull request #478 from pateljannat/preview-video 2023-02-21 10:31:33 +05:30
Jannat Patel
080a02589c fix: accept only embed id for preview video 2023-02-21 09:52:20 +05:30
Jannat Patel
d1e7549da9 Merge pull request #477 from pateljannat/evaluation-event 2023-02-17 11:01:54 +05:30
Jannat Patel
8e1ef1dc77 fix: layout of certification request 2023-02-17 10:52:50 +05:30
Jannat Patel
619a2f9d80 fix: only link calendars that are enabled 2023-02-16 20:20:16 +05:30
Jannat Patel
926444767b feat: event for evaluation 2023-02-16 20:11:03 +05:30
Jannat Patel
6bf4020ad1 Merge pull request #474 from fproldan/translations_es 2023-02-09 10:59:50 +05:30
Jannat Patel
cb63ad8ed2 Merge pull request #475 from pateljannat/default-view 2023-02-08 13:07:16 +05:30
Jannat Patel
458ed9ad95 feat: default home setting 2023-02-07 19:58:06 +05:30
fproldan
a11df1a237 feat: es translations 2023-02-01 12:48:11 -03:00
fproldan
352d4b9ab9 feat: es translations 2023-02-01 12:43:17 -03:00
Jannat Patel
275ded0658 fix: fullscreen for preview video 2023-01-18 15:07:04 +05:30
Jannat Patel
c8f3350761 fix: creator patch 2023-01-17 12:27:32 +05:30
Jannat Patel
e3112d8dcf fix: og tags 2023-01-16 17:53:54 +05:30
Jannat Patel
5009900c0e Merge pull request #469 from pateljannat/job-changes 2023-01-16 09:54:58 +05:30
Jannat Patel
0f60f1a58b fix: see on website link in job opportunity 2023-01-11 10:11:57 +05:30
Jannat Patel
8f88518187 Merge pull request #464 from pateljannat/people 2023-01-09 16:33:45 +05:30
Jannat Patel
05bcead7d1 fix: profile webform layout 2023-01-09 16:24:24 +05:30
Jannat Patel
6268989306 fix: user custom field positions 2023-01-09 12:51:33 +05:30
Jannat Patel
43ba835b52 fix: renamed community to people 2023-01-06 18:27:04 +05:30
Jannat Patel
9240bc9130 feat: card background 2023-01-05 13:25:59 +05:30
Jannat Patel
fb70aee055 chore: fix pre-commit-config 2023-01-05 11:19:48 +05:30
Jannat Patel
7bf69eb77d chore: fix pre-commit-config 2023-01-05 11:18:32 +05:30
Jannat Patel
7ded9a23be Merge pull request #463 from pateljannat/user-singles-issue 2023-01-05 09:38:25 +05:30
Jannat Patel
281af15d65 fix: converted query to qb 2023-01-04 16:57:35 +05:30
Jannat Patel
ec31c96120 fix: linters 2023-01-04 16:29:07 +05:30
Jannat Patel
b970eb1541 fix: user doctype singles issue 2023-01-03 22:04:59 +05:30
Jannat Patel
7f6b90d5f4 Merge pull request #461 from NagariaHussain/fix-meta 2023-01-03 18:49:52 +05:30
Hussain Nagaria
d28096ede6 Revert "feat: add meta image field in LMS course"
This reverts commit 37e8c3ab84.
2023-01-03 17:32:49 +05:30
Hussain Nagaria
12b2b0d0eb fix: linter 2023-01-03 17:01:00 +05:30
Hussain Nagaria
a0e281fb30 feat(ux): add View in Website button in LMS Course 2023-01-03 16:52:50 +05:30
Hussain Nagaria
37e8c3ab84 feat: add meta image field in LMS course 2023-01-03 16:48:52 +05:30
Hussain Nagaria
16cb564a6a fix: render meta block in portal base pages 2023-01-03 16:48:16 +05:30
Jannat Patel
cd88657bc9 Merge pull request #460 from pateljannat/readme 2022-12-30 18:11:42 +05:30
Jannat Patel
d82a32b06a docs: grammar correction in readme 2022-12-30 18:03:01 +05:30
Jannat Patel
094bd943ee docs: updated screenshots in readme 2022-12-30 15:58:31 +05:30
149 changed files with 8083 additions and 2175 deletions

46
.github/helper/install.sh vendored Normal file
View File

@@ -0,0 +1,46 @@
#!/bin/bash
set -e
cd ~ || exit
echo "Setting Up Bench..."
pip install frappe-bench
bench -v init frappe-bench --skip-assets --python "$(which python)"
cd ./frappe-bench || exit
bench -v setup requirements
echo "Setting Up LMS App..."
bench get-app lms "${GITHUB_WORKSPACE}"
echo "Setting Up Sites & Database..."
mkdir ~/frappe-bench/sites/lms.test
cp "${GITHUB_WORKSPACE}/.github/helper/site_config.json" ~/frappe-bench/sites/lms.test/site_config.json
mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "SET GLOBAL character_set_server = 'utf8mb4'";
mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'";
mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "CREATE DATABASE test_lms";
mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "CREATE USER 'test_lms'@'localhost' IDENTIFIED BY 'test_lms'";
mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "GRANT ALL PRIVILEGES ON \`test_lms\`.* TO 'test_lms'@'localhost'";
mariadb --host 127.0.0.1 --port 3306 -u root -p123 -e "FLUSH PRIVILEGES";
echo "Setting Up Procfile..."
sed -i 's/^watch:/# watch:/g' Procfile
sed -i 's/^schedule:/# schedule:/g' Procfile
echo "Starting Bench..."
bench start &> bench_start.log &
CI=Yes bench build &
build_pid=$!
bench --site lms.test reinstall --yes
bench --site lms.test install-app lms
wait $build_pid

13
.github/helper/install_dependencies.sh vendored Normal file
View File

@@ -0,0 +1,13 @@
#!/bin/bash
set -e
echo "Setting Up System Dependencies..."
sudo apt update
sudo apt install libcups2-dev redis-server mariadb-client-10.6
install_wkhtmltopdf() {
wget -q https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-1/wkhtmltox_0.12.6-1.focal_amd64.deb
sudo apt install ./wkhtmltox_0.12.6-1.focal_amd64.deb
}
install_wkhtmltopdf &

20
.github/helper/site_config.json vendored Normal file
View File

@@ -0,0 +1,20 @@
{
"db_host": "127.0.0.1",
"db_port": 3306,
"db_name": "test_lms",
"db_password": "test_lms",
"allow_tests": true,
"enable_ui_tests": true,
"db_type": "mariadb",
"auto_email_id": "test@example.com",
"mail_server": "smtp.example.com",
"mail_login": "test@example.com",
"mail_password": "test",
"admin_password": "admin",
"root_login": "root",
"root_password": "123",
"host_name": "http://lms.test:8000",
"monitor": 1,
"server_script_enabled": true,
"mute_emails": true
}

View File

@@ -1,4 +1,4 @@
name: Run tests
name: Server Tests
on:
push:
branches:

116
.github/workflows/ui-tests.yml vendored Normal file
View File

@@ -0,0 +1,116 @@
name: UI
on:
pull_request:
workflow_dispatch:
push:
branches: [ main ]
permissions:
# Do not change this as GITHUB_TOKEN is being used by roulette
contents: read
jobs:
test:
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'frappe' }}
timeout-minutes: 60
strategy:
fail-fast: false
name: UI Tests (Cypress)
services:
mariadb:
image: mariadb:10.6
env:
MARIADB_ROOT_PASSWORD: 123
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
steps:
- name: Clone
uses: actions/checkout@v3
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Check for valid Python & Merge Conflicts
run: |
python -m compileall -q -f "${GITHUB_WORKSPACE}"
if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}"
then echo "Found merge conflicts"
exit 1
fi
- uses: actions/setup-node@v3
with:
node-version: 16
check-latest: true
- name: Add to Hosts
run: |
echo "127.0.0.1 lms.test" | sudo tee -a /etc/hosts
- name: Cache pip
uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }}
restore-keys: |
${{ runner.os }}-pip-
${{ runner.os }}-
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-ui-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-ui-
- name: Cache cypress binary
uses: actions/cache@v3
with:
path: ~/.cache/Cypress
key: ${{ runner.os }}-cypress
- name: Install Dependencies
run: |
bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh
bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
env:
BEFORE: ${{ env.GITHUB_EVENT_PATH.before }}
AFTER: ${{ env.GITHUB_EVENT_PATH.after }}
TYPE: ui
DB: mariadb
- name: Site Setup
run: |
cd ~/frappe-bench/
bench --site lms.test execute frappe.utils.install.complete_setup_wizard
bench --site lms.test execute frappe.tests.ui_test_helpers.create_test_user
- name: UI Tests
run: cd ~/frappe-bench/ && bench --site lms.test run-ui-tests lms --headless
env:
CYPRESS_BASE_URL: http://lms.test:8000
CYPRESS_RECORD_KEY: 095366ec-7b9f-41bd-aeec-03bb76d627fe
- name: Stop server and wait for coverage file
run: |
ps -ef | grep "[f]rappe serve" | awk '{print $2}' | xargs kill -s SIGINT
sleep 5
- name: Show bench output
if: ${{ always() }}
run: cat ~/frappe-bench/bench_start.log || true

2
.gitignore vendored
View File

@@ -8,3 +8,5 @@ lms/public/dist
__pycache__/
*.py[cod]
*$py.class
node_modules
package-lock.json

View File

@@ -7,11 +7,9 @@ repos:
rev: v4.3.0
hooks:
- id: trailing-whitespace
files: "frappe.*"
files: "lms.*"
exclude: ".*json$|.*txt$|.*csv|.*md|.*svg"
- id: check-yaml
- id: no-commit-to-branch
args: ['--branch', 'main']
- id: check-merge-conflict
- id: check-ast
- id: check-json

View File

@@ -2,7 +2,7 @@
<a href="https://www.frappelms.com/">
<img src="https://www.frappelms.com/files/flms.svg" alt="Frappe LMS" width="100" height="100">
</a>
<p align="center">Easy to use, open source, Learning Management System</p>
<p align="center">Easy to use, open source, learning management system.</p>
</p>
<p align="center">
@@ -12,35 +12,34 @@
</p>
<img width="1402" alt="Lesson" src="https://frappelms.com/files/fs-banner71f330.png">
<details>
<summary>Show more screenshots</summary>
![Screenshot 1](/lms/public/images/ss1.png)
![Screenshot 2](/lms/public/images/ss2.png)
![Screenshot 3](/lms/public/images/ss3.png)
<img width="1520" alt="ss1" src="https://user-images.githubusercontent.com/31363128/210056046-584bc8aa-d28c-4514-b031-73817012837d.png">
<img width="830" alt="ss2" src="https://user-images.githubusercontent.com/31363128/210056097-36849182-6db0-43a2-8c62-5333cd2aedf4.png">
<img width="941" alt="ss3" src="https://user-images.githubusercontent.com/31363128/210056134-01a7c429-1ef4-434e-9d43-128dda35d7e5.png">
</details>
Frappe LMS is an easy-to-use, open-source learning management system. You can use it to create and share online courses. The app has a clear UI that helps students focus only on what's important and assists in distraction-free learning.
You can create courses and lessons through simple forms. Lessons can be in the form of text, videos, quizzes or a combination of all these. You can keep your students engaged with quizzes to help revise and test the concepts learned.Course Instructors and Students can reach out to each other through the discussions section available for each lesson and get queries resolved.
You can create courses and lessons through simple forms. Lessons can be in the form of text, videos, quizzes or a combination of all these. You can keep your students engaged with quizzes to help revise and test the concepts learned. Course Instructors and Students can reach out to each other through the discussions section available for each lesson and get queries resolved.
## Features
- Create online courses. 📚
- Add detailed descriptions and preview video to the course. 🎬
- Add videos, quizzes and assignments to your lessons and make them interesting and interactive 📝
- Add detailed descriptions and preview videos to the course. 🎬
- Add videos, quizzes, and assignments to your lessons and make them interesting and interactive 📝
- Discussions section below each lesson where instructors and students can interact with each other. 💬
- Create classes to group your students based on courses and track their progress 🏛
- Statistics dashboard that provides all important numbers at a glimpse. 📈
- Job Board where users can post and look for jobs. 💼
- People directory with each person's profile page 👨‍👩‍👧‍👦
- Set cover image, profile photo, short bio and other professional information. 🦹🏼‍♀️
- Set cover image, profile photo, short bio, and other professional information. 🦹🏼‍♀️
- Simple layout that optimizes readability 🤓
- Delightful user-experience in overall usage ✨
- Delightful user experience in overall usage ✨
## Tech Stack
Frappe LMS is built on [Frappe Framework](https://frappeframework.com) which is a batteries-included python web-framework.
Frappe LMS is built on [Frappe Framework](https://frappeframework.com) which is a batteries-included python web framework.
These are some of the tools it's built on:
- [Python](https://www.python.org)
- [Redis](https://redis.io/)
@@ -48,24 +47,23 @@ These are some of the tools it's built on:
- [Socket.io](https://socket.io/)
## Local Setup
### Docker
You need Docker, docker-compose and git setup on your machine. Refer [Docker documentation](https://docs.docker.com/). After that, run the following commands:
You need Docker, docker-compose, and git setup on your machine. Refer to [Docker documentation](https://docs.docker.com/). After that, run the following commands:
```
git clone https://github.com/frappe/lms
cd lms/docker
cd apps/lms/docker
docker-compose up
```
Wait for sometime until the setup script creates a site. After that you can
access `http://localhost:8000` in your browser and the app's login screen
should show up.
Wait for some time until the setup script creates a site. After that, you can access `http://localhost:8000` in your browser and the app's login screen should show up.
### Frappe Bench
Currently, this app depends on the `develop` branch of [frappe](https://github.com/frappe/frappe).
1. Setup frappe-bench by following [this guide](https://frappeframework.com/docs/v14/user/en/installation)
1. In the frappe-bench directory, run `bench start` and keep it running. Open a new terminal session and cd into `frappe-bench` directory.
1. In the frappe-bench directory, run `bench start` and keep it running. Open a new terminal session and cd into the `frappe-bench` directory.
1. Run the following commands:
```sh
bench new-site lms.test
@@ -73,16 +71,16 @@ Currently, this app depends on the `develop` branch of [frappe](https://github.c
bench --site lms.test install-app lms
bench --site lms.test add-to-hosts
1. Now, you can access the site at `http://gameplan.test:8080`
1. Now, you can access the site at `http://lms.test:8000`
## Deployment
Frappe LMS is an app built on top of Frappe Framework. So, you can follow any deployment guide for hosting a Frappe Framework based site.
Frappe LMS is an app built on top of the Frappe Framework. So, you can follow any deployment guide for hosting a Frappe Framework-based site.
### Managed Hosting
Frappe LMS can be deployed in a few clicks on [Frappe Cloud](https://frappecloud.com/marketplace/apps/lms).
### Self hosting
### Self-hosting
If you want to self-host, you can follow official [Frappe Bench Installation](https://github.com/frappe/bench#installation) instructions.
## Bugs and Feature Requests

18
cypress.config.js Normal file
View File

@@ -0,0 +1,18 @@
const { defineConfig } = require("cypress");
module.exports = defineConfig({
projectId: "vandxn",
adminPassword: "admin",
testUser: "frappe@example.com",
defaultCommandTimeout: 20000,
pageLoadTimeout: 15000,
video: true,
videoUploadOnPasses: false,
retries: {
runMode: 2,
openMode: 0,
},
e2e: {
baseUrl: "http://test_site_ui:8000",
},
});

View File

@@ -0,0 +1,110 @@
describe("Course Creation", () => {
it("creates a new course", () => {
cy.login();
cy.visit("/courses");
// Create a course
cy.get("a.btn").contains("Create a Course").click();
cy.wait(1000);
cy.url().should("include", "/courses/new-course/edit");
cy.get("#title").type("Test Course");
cy.get("#intro").type("Test Course Short Introduction");
cy.get("#description").type("Test Course Description");
cy.get("#video-link").type("-LPmw2Znl2c");
cy.get("#tags-input").type("Test");
cy.get("#published").check();
cy.wait(1000);
cy.button("Save").click();
// Add Chapter
cy.wait(1000);
cy.link("Course Outline").click();
cy.wait(1000);
cy.get(".edit-header .btn-add-chapter").click();
cy.get("#chapter-title").type("Test Chapter");
cy.get("#chapter-description").type("Test Chapter Description");
cy.button("Save").click();
// Add Lesson
cy.wait(1000);
cy.link("Add Lesson").click();
cy.wait(1000);
cy.get("#lesson-title").type("Test Lesson");
// Content
cy.get(".ce-block").click().type("{enter}");
cy.get(".ce-toolbar__plus").click();
cy.get('[data-item-name="youtube"]').click();
cy.get('input[data-fieldname="youtube"]').type("GoDtyItReto");
cy.button("Insert").click();
cy.wait(1000);
cy.get(".ce-block:last").click().type("{enter}");
cy.get(".ce-block:last")
.click()
.type(
"This is an extremely big paragraph that is meant to test the UI. This is a very long paragraph. It contains more than once sentence. Its meant to be this long as this is a UI test. Its unbearably long and I'm not sure why I'm typing this much. I'm just going to keep typing until I feel like its long enough. I think its long enough now. I'm going to stop typing now."
);
cy.button("Save").click();
// View Course
cy.wait(1000);
cy.visit("/courses");
cy.get(".course-card-title:first").contains("Test Course");
cy.get(".course-card:first").click();
cy.url().should("include", "/courses/test-course");
cy.get("#title").contains("Test Course");
cy.get(".preview-video").should(
"have.attr",
"src",
"https://www.youtube.com/embed/-LPmw2Znl2c"
);
cy.get("#intro").contains("Test Course Short Introduction");
// View Chapter
cy.get(".chapter-title-main:first").contains("Test Chapter");
cy.get(".chapter-description:first").contains(
"Test Chapter Description"
);
cy.get(".lesson-info:first").contains("Test Lesson");
cy.get(".lesson-info:first").click();
// View Lesson
cy.wait(1000);
cy.url().should("include", "learn/1.1");
cy.get("#title").contains("Test Lesson");
cy.get(".lesson-video iframe").should(
"have.attr",
"src",
"https://www.youtube.com/embed/GoDtyItReto"
);
cy.get(".lesson-content-card").contains(
"This is an extremely big paragraph that is meant to test the UI. This is a very long paragraph. It contains more than once sentence. Its meant to be this long as this is a UI test. Its unbearably long and I'm not sure why I'm typing this much. I'm just going to keep typing until I feel like its long enough. I think its long enough now. I'm going to stop typing now."
);
// Add Discussion
cy.get(".reply").click();
cy.wait(500);
cy.get(".topic-title").type("Question Title");
cy.get(".comment-field").type(
"Question Content. This is a very long question. It contains more than once sentence. Its meant to be this long as this is a UI test."
);
cy.get(".submit-discussion").click();
// View Discussion
cy.wait(1000);
cy.get(".discussion-topic-title:first").contains("Question Title");
cy.get(".sidebar-parent:first").click();
cy.get(".reply-text").contains(
"Question Content. This is a very long question. It contains more than once sentence. Its meant to be this long as this is a UI test."
);
cy.get(".comment-field:visible").type(
"This is a reply to the previous comment. Its not that long."
);
cy.get(".submit-discussion:visible").click();
cy.wait(1000);
cy.get(".reply-text:last p").contains(
"This is a reply to the previous comment. Its not that long."
);
});
});

View File

@@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View File

@@ -0,0 +1,55 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
Cypress.Commands.add("login", (email, password) => {
if (!email) {
email = Cypress.config("testUser") || "Administrator";
}
if (!password) {
password = Cypress.config("adminPassword");
}
cy.request({
url: "/api/method/login",
method: "POST",
body: { usr: email, pwd: password },
});
});
Cypress.Commands.add("button", (text) => {
return cy.get(`button:contains("${text}")`);
});
Cypress.Commands.add("link", (text) => {
return cy.get(`a:contains("${text}")`);
});
Cypress.Commands.add("iconButton", (text) => {
return cy.get(`button[aria-label="${text}"]`);
});
Cypress.Commands.add("dialog", (selector) => {
return cy.get(`[role=dialog] ${selector}`);
});

20
cypress/support/e2e.js Normal file
View File

@@ -0,0 +1,20 @@
// ***********************************************************
// This example support/e2e.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import "./commands";
// Alternatively you can use CommonJS syntax:
// require('./commands')

View File

@@ -609,13 +609,13 @@
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"insert_after": "profession",
"insert_after": "hide_private",
"is_system_generated": 1,
"is_virtual": 0,
"label": "Education Details",
"length": 0,
"mandatory_depends_on": null,
"modified": "2021-12-31 11:57:55.170625",
"modified": "2021-12-31 11:57:55.170620",
"module": null,
"name": "User-education_details",
"no_copy": 0,
@@ -662,7 +662,7 @@
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"insert_after": "hide_private",
"insert_after": "bio",
"is_system_generated": 1,
"is_virtual": 0,
"label": "Profile Complete",
@@ -721,7 +721,7 @@
"label": "Hide my Private Information from others",
"length": 0,
"mandatory_depends_on": null,
"modified": "2021-12-31 11:57:47.942968",
"modified": "2021-12-31 11:57:47.942969",
"module": null,
"name": "User-hide_my_private_information_from_others",
"no_copy": 0,
@@ -768,13 +768,13 @@
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"insert_after": "profile_complete",
"insert_after": "user_category",
"is_system_generated": 1,
"is_virtual": 0,
"label": "Cover Image",
"length": 0,
"mandatory_depends_on": null,
"modified": "2021-12-31 10:59:52.682112",
"modified": "2021-12-31 10:59:52.682115",
"module": null,
"name": "User-cover_image",
"no_copy": 0,
@@ -821,13 +821,13 @@
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"insert_after": "cover_image",
"insert_after": "interest",
"is_system_generated": 1,
"is_virtual": 0,
"label": "I am looking for a job",
"length": 0,
"mandatory_depends_on": null,
"modified": "2021-12-31 12:56:32.110403",
"modified": "2021-12-31 12:56:32.110405",
"module": null,
"name": "User-looking_for_job",
"no_copy": 0,

View File

@@ -60,7 +60,7 @@ web_include_js = ["website.bundle.js"]
# before_install = "lms.install.before_install"
after_install = "lms.install.after_install"
after_sync = "lms.install.after_sync"
after_uninstall = "lms.install.after_uninstall"
before_uninstall = "lms.install.before_uninstall"
setup_wizard_requires = "assets/lms/js/setup_wizard.js"
@@ -138,12 +138,18 @@ fixtures = ["Custom Field", "Function", "Industry"]
website_route_rules = [
{"from_route": "/sketches/<sketch>", "to_route": "sketches/sketch"},
{"from_route": "/courses/<course>", "to_route": "courses/course"},
{"from_route": "/courses/<course>/edit", "to_route": "courses/create"},
{"from_route": "/courses/<course>/outline", "to_route": "courses/outline"},
{"from_route": "/courses/<course>/<certificate>", "to_route": "courses/certificate"},
{"from_route": "/courses/<course>/learn", "to_route": "batch/learn"},
{
"from_route": "/courses/<course>/learn/<int:chapter>.<int:lesson>",
"to_route": "batch/learn",
},
{
"from_route": "/courses/<course>/learn/<int:chapter>.<int:lesson>/edit",
"to_route": "batch/edit",
},
{"from_route": "/quizzes", "to_route": "batch/quiz_list"},
{"from_route": "/quizzes/<quizname>", "to_route": "batch/quiz"},
{"from_route": "/classes/<classname>", "to_route": "classes/class"},
@@ -179,6 +185,7 @@ website_route_rules = [
website_redirects = [
{"source": "/update-profile", "target": "/edit-profile"},
{"source": "/dashboard", "target": "/courses"},
{"source": "/community", "target": "/people"},
]
update_website_context = [
@@ -228,6 +235,7 @@ jinja = {
"lms.lms.utils.get_filtered_membership",
"lms.lms.utils.show_start_learing_cta",
"lms.lms.utils.can_create_courses",
"lms.lms.utils.get_telemetry_boot_info",
],
"filters": [],
}
@@ -287,6 +295,4 @@ profile_url_prefix = "/users/"
signup_form_template = "lms.plugins.show_custom_signup"
on_login = "lms.overrides.user.on_login"
on_session_creation = "lms.overrides.user.on_session_creation"

View File

@@ -44,8 +44,9 @@ def add_pages_to_nav():
).save()
def after_uninstall():
def before_uninstall():
delete_custom_fields()
delete_lms_roles()
def create_lms_roles():
@@ -53,6 +54,13 @@ def create_lms_roles():
create_moderator_role()
def delete_lms_roles():
roles = ["Course Creator", "Moderator"]
for role in roles:
if frappe.db.exists("Role", role):
frappe.db.delete("Role", role)
def set_default_home():
frappe.db.set_value("Portal Settings", None, "default_portal_home", "/courses")
@@ -126,4 +134,3 @@ def delete_custom_fields():
for field in fields:
frappe.db.delete("Custom Field", {"fieldname": field})
frappe.db.commit()

View File

@@ -2,6 +2,8 @@
// For license information, please see license.txt
frappe.ui.form.on("Job Opportunity", {
// refresh: function(frm) {
// }
refresh: (frm) => {
if (frm.doc.name)
frm.add_web_link(`/jobs/${frm.doc.name}`, "See on Website");
},
});

View File

@@ -21,7 +21,7 @@ def submit_solution(exercise, code):
@exerecise: name of the exercise to submit
@code: solution to the exercise
"""
ex = frappe.get_doc("Exercise", exercise)
ex = frappe.get_doc("LMS Exercise", exercise)
if not ex:
return
doc = ex.submit(code)

View File

@@ -3,7 +3,9 @@
# import frappe
from frappe.model.document import Document
from frappe.utils.telemetry import capture
class CourseChapter(Document):
pass
def after_insert(self):
capture("chapter_created", "lms")

View File

@@ -7,7 +7,7 @@ frappe.ui.form.on("Course Lesson", {
},
setup_help(frm) {
let quiz_link = `<a href="/app/lms-quiz"> ${__("Quiz List")} </a>`;
let exercise_link = `<a href="/app/exercise"> ${__(
let exercise_link = `<a href="/app/lms-exercise"> ${__(
"Exercise List"
)} </a>`;
let file_link = `<a href="/app/file"> ${__("File DocType")} </a>`;

View File

@@ -135,7 +135,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-12-28 16:01:42.191123",
"modified": "2023-05-02 12:42:16.926753",
"modified_by": "Administrator",
"module": "LMS",
"name": "Course Lesson",

View File

@@ -4,9 +4,8 @@
import frappe
from frappe import _
from frappe.model.document import Document
from lms.lms.utils import get_course_progress, get_lesson_url
from frappe.utils.telemetry import capture
from lms.lms.utils import get_course_progress
from ...md import find_macros
@@ -24,8 +23,11 @@ class CourseLesson(Document):
for section in dynamic_documents:
self.update_lesson_name_in_document(section)
def after_insert(self):
capture("lesson_created", "lms")
def update_lesson_name_in_document(self, section):
doctype_map = {"Exercise": "Exercise", "Quiz": "LMS Quiz"}
doctype_map = {"Exercise": "LMS Exercise", "Quiz": "LMS Quiz"}
macros = find_macros(self.body)
documents = [value for name, value in macros if name == section]
index = 1
@@ -53,7 +55,7 @@ class CourseLesson(Document):
ex.course = None
ex.index_ = 0
ex.index_label = ""
ex.save()
ex.save(ignore_permissions=True)
def check_and_create_folder(self):
args = {
@@ -71,7 +73,7 @@ class CourseLesson(Document):
macros = find_macros(self.body)
exercises = [value for name, value in macros if name == "Exercise"]
return [frappe.get_doc("Exercise", name) for name in exercises]
return [frappe.get_doc("LMS Exercise", name) for name in exercises]
def get_progress(self):
return frappe.db.get_value(
@@ -92,14 +94,9 @@ def save_progress(lesson, course, status):
if not membership:
return
if frappe.db.exists(
"LMS Course Progress",
{"lesson": lesson, "owner": frappe.session.user, "course": course},
):
doc = frappe.get_doc(
"LMS Course Progress",
{"lesson": lesson, "owner": frappe.session.user, "course": course},
)
filters = {"lesson": lesson, "owner": frappe.session.user, "course": course}
if frappe.db.exists("LMS Course Progress", filters):
doc = frappe.get_doc("LMS Course Progress", filters)
doc.status = status
doc.save(ignore_permissions=True)
else:
@@ -108,6 +105,7 @@ def save_progress(lesson, course, status):
"doctype": "LMS Course Progress",
"lesson": lesson,
"status": status,
"member": frappe.session.user,
}
).save(ignore_permissions=True)

View File

@@ -30,7 +30,7 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Exercise",
"options": "Exercise",
"options": "LMS Exercise",
"search_index": 1
},
{

View File

@@ -25,7 +25,7 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Exercise",
"options": "Exercise"
"options": "LMS Exercise"
},
{
"fetch_from": "exercise.title",

View File

@@ -9,6 +9,7 @@
"assignment",
"lesson",
"course",
"evaluator",
"status",
"column_break_3",
"member",
@@ -56,10 +57,11 @@
{
"fetch_from": "lesson.course",
"fieldname": "course",
"fieldtype": "Data",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Course",
"options": "LMS Course",
"read_only": 1
},
{
@@ -73,12 +75,20 @@
"fieldname": "comments",
"fieldtype": "Small Text",
"label": "Comments"
},
{
"fetch_from": "course.evaluator",
"fieldname": "evaluator",
"fieldtype": "Link",
"label": "Evaluator",
"options": "User",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"make_attachments_public": 1,
"modified": "2022-11-16 12:11:59.472025",
"modified": "2023-03-27 13:24:18.696868",
"modified_by": "Administrator",
"module": "LMS",
"name": "Lesson Assignment",

View File

@@ -58,4 +58,4 @@ def grade_assignment(name, result, comments):
doc = frappe.get_doc("Lesson Assignment", name)
doc.status = result
doc.comments = comments
doc.save()
doc.save(ignore_permissions=True)

View File

@@ -56,7 +56,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-04-06 11:49:36.077370",
"modified": "2023-04-14 12:33:37.839625",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Certificate",

View File

@@ -5,13 +5,14 @@ import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import add_years, nowdate
from frappe.utils.pdf import get_pdf
from lms.lms.utils import is_certified
class LMSCertificate(Document):
def before_insert(self):
def validate(self):
self.validate_duplicate_certificate()
def validate_duplicate_certificate(self):
certificates = frappe.get_all(
"LMS Certificate", {"member": self.member, "course": self.course}
)
@@ -22,6 +23,18 @@ class LMSCertificate(Document):
_("{0} is already certified for the course {1}").format(full_name, course_name)
)
def after_insert(self):
share = frappe.get_doc(
{
"doctype": "DocShare",
"read": 1,
"share_doctype": "LMS Certificate",
"share_name": self.name,
"user": self.member,
}
)
share.save(ignore_permissions=True)
@frappe.whitelist()
def create_certificate(course):
@@ -47,10 +60,3 @@ def create_certificate(course):
)
certificate.save(ignore_permissions=True)
return certificate
@frappe.whitelist()
def get_certificate_pdf(html):
frappe.local.response.filename = "certificate.pdf"
frappe.local.response.filecontent = get_pdf(html, {"orientation": "LandScape"})
frappe.local.response.type = "pdf"

View File

@@ -3,7 +3,7 @@
frappe.ui.form.on("LMS Certificate Evaluation", {
refresh: function (frm) {
if (frm.doc.status == "Pass") {
if (!frm.is_new() && frm.doc.status == "Pass") {
frm.add_custom_button(__("Create LMS Certificate"), () => {
frappe.model.open_mapped_doc({
method: "lms.lms.doctype.lms_certificate_evaluation.lms_certificate_evaluation.create_lms_certificate",

View File

@@ -9,8 +9,9 @@
"member",
"member_name",
"column_break_5",
"course",
"status",
"course",
"class",
"section_break_6",
"date",
"start_time",
@@ -93,11 +94,17 @@
{
"fieldname": "column_break_10",
"fieldtype": "Column Break"
},
{
"fieldname": "class",
"fieldtype": "Link",
"label": "Class",
"options": "LMS Class"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-11-23 11:49:01.400292",
"modified": "2023-02-22 16:00:34.361934",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Certificate Evaluation",

View File

@@ -3,12 +3,17 @@
frappe.ui.form.on("LMS Certificate Request", {
refresh: function (frm) {
frm.add_custom_button(__("Create LMS Certificate Evaluation"), () => {
frappe.model.open_mapped_doc({
method: "lms.lms.doctype.lms_certificate_request.lms_certificate_request.create_lms_certificate_evaluation",
frm: frm,
});
});
if (!frm.is_new()) {
frm.add_custom_button(
__("Create LMS Certificate Evaluation"),
() => {
frappe.model.open_mapped_doc({
method: "lms.lms.doctype.lms_certificate_request.lms_certificate_request.create_lms_certificate_evaluation",
frm: frm,
});
}
);
}
},
onload: function (frm) {

View File

@@ -7,12 +7,14 @@
"engine": "InnoDB",
"field_order": [
"course",
"member",
"member_name",
"evaluator",
"column_break_4",
"member",
"member_name",
"section_break_lifi",
"date",
"day",
"column_break_ddyh",
"start_time",
"end_time"
],
@@ -29,7 +31,7 @@
{
"fieldname": "member",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Member",
"options": "User",
"reqd": 1
@@ -39,7 +41,7 @@
"fieldname": "evaluator",
"fieldtype": "Link",
"label": "Evaluator",
"options": "Course Evaluator",
"options": "User",
"read_only": 1
},
{
@@ -66,7 +68,6 @@
{
"fieldname": "end_time",
"fieldtype": "Time",
"in_list_view": 1,
"label": "End Time",
"reqd": 1
},
@@ -80,11 +81,19 @@
"fieldtype": "Data",
"label": "Member Name",
"read_only": 1
},
{
"fieldname": "section_break_lifi",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_ddyh",
"fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-04-06 11:33:33.711545",
"modified": "2023-02-28 19:53:17.534351",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Certificate Request",
@@ -99,6 +108,7 @@
"read": 1,
"report": 1,
"role": "System Manager",
"select": 1,
"share": 1,
"write": 1
}

View File

@@ -12,6 +12,10 @@ class LMSCertificateRequest(Document):
def validate(self):
self.validate_if_existing_requests()
def after_insert(self):
if frappe.db.get_single_value("LMS Settings", "send_calendar_invite_for_evaluations"):
self.create_event()
def validate_if_existing_requests(self):
existing_requests = frappe.get_all(
"LMS Certificate Request",
@@ -30,6 +34,48 @@ class LMSCertificateRequest(Document):
)
)
def create_event(self):
calendar = frappe.db.get_value(
"Google Calendar", {"user": self.evaluator, "enable": 1}, "name"
)
if calendar:
event = frappe.get_doc(
{
"doctype": "Event",
"subject": f"Evaluation of {self.member_name}",
"starts_on": f"{self.date} {self.start_time}",
"ends_on": f"{self.date} {self.end_time}",
}
)
event.save()
participants = [self.member, self.evaluator]
for participant in participants:
contact_name = frappe.db.get_value("Contact", {"email_id": participant}, "name")
frappe.get_doc(
{
"doctype": "Event Participants",
"reference_doctype": "Contact",
"reference_docname": contact_name,
"email": participant,
"parent": event.name,
"parenttype": "Event",
"parentfield": "event_participants",
}
).save()
event.reload()
event.update(
{
"sync_with_google_calendar": 1,
"add_video_conferencing": 1,
"google_calendar": calendar,
}
)
event.save()
@frappe.whitelist()
def create_certificate_request(course, date, day, start_time, end_time):

View File

@@ -2,6 +2,13 @@
// For license information, please see license.txt
frappe.ui.form.on("LMS Class", {
// refresh: function(frm) {
// }
onload: function (frm) {
frm.set_query("student", "students", function (doc) {
return {
filters: {
ignore_user_type: 1,
},
};
});
},
});

View File

@@ -11,9 +11,13 @@
"title",
"start_date",
"end_date",
"paid_class",
"column_break_4",
"description",
"seat_count",
"start_time",
"end_time",
"section_break_6",
"description",
"students",
"courses",
"custom_component"
@@ -29,6 +33,7 @@
{
"fieldname": "end_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "End Date",
"reqd": 1
},
@@ -60,6 +65,7 @@
{
"fieldname": "start_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Start Date",
"reqd": 1
},
@@ -69,11 +75,32 @@
"fieldtype": "Code",
"label": "Custom Component",
"options": "HTML"
},
{
"default": "0",
"fieldname": "paid_class",
"fieldtype": "Check",
"label": "Paid Class"
},
{
"fieldname": "seat_count",
"fieldtype": "Int",
"label": "Seat Count"
},
{
"fieldname": "start_time",
"fieldtype": "Time",
"label": "Start Time"
},
{
"fieldname": "end_time",
"fieldtype": "Time",
"label": "End Time"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-11-25 10:37:24.250557",
"modified": "2023-05-03 23:07:06.725720",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Class",
@@ -107,5 +134,6 @@
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
"states": [],
"title_field": "title"
}

View File

@@ -4,24 +4,43 @@
import frappe
from frappe.model.document import Document
from frappe import _
from frappe.utils import cint
from frappe.utils import cint, format_date, format_datetime
import requests
import base64
import json
class LMSClass(Document):
def validate(self):
validate_membership(self)
if self.seat_count:
self.validate_seats_left()
self.validate_duplicate_students()
self.validate_membership()
def validate_duplicate_students(self):
students = [row.student for row in self.students]
duplicates = {student for student in students if students.count(student) > 1}
if len(duplicates):
frappe.throw(
_("Student {0} has already been added to this class.").format(
frappe.bold(next(iter(duplicates)))
)
)
def validate_membership(self):
for course in self.courses:
for student in self.students:
filters = {
"doctype": "LMS Batch Membership",
"member": student.student,
"course": course.course,
}
if not frappe.db.exists(filters):
frappe.get_doc(filters).save()
def validate_membership(self):
for course in self.courses:
for student in self.students:
filters = {
"doctype": "LMS Batch Membership",
"member": student.student,
"course": course.course,
}
if not frappe.db.exists(filters):
frappe.get_doc(filters).save()
def validate_seats_left(self):
if cint(self.seat_count) < len(self.students):
frappe.throw(_("There are no seats available in this class."))
@frappe.whitelist()
@@ -29,6 +48,17 @@ def add_student(email, class_name):
if not frappe.db.exists("User", email):
frappe.throw(_("There is no such user. Please create a user with this Email ID."))
filters = {
"student": email,
"parent": class_name,
"parenttype": "LMS Class",
"parentfield": "students",
}
if frappe.db.exists("Class Student", filters):
frappe.throw(
_("Student {0} has already been added to this class.").format(frappe.bold(email))
)
frappe.get_doc(
{
"doctype": "Class Student",
@@ -45,22 +75,108 @@ def add_student(email, class_name):
@frappe.whitelist()
def remove_student(student, class_name):
frappe.db.delete("Class Student", {"student": student, "parent": class_name})
return True
@frappe.whitelist()
def update_course(class_name, course, value):
if cint(value):
doc = frappe.get_doc(
def remove_course(course, parent):
frappe.db.delete("Class Course", {"course": course, "parent": parent})
@frappe.whitelist()
def create_live_class(
class_name, title, duration, date, time, timezone, auto_recording, description=None
):
date = format_date(date, "yyyy-mm-dd", True)
payload = {
"topic": title,
"start_time": format_datetime(f"{date} {time}", "yyyy-MM-ddTHH:mm:ssZ"),
"duration": duration,
"agenda": description,
"private_meeting": True,
"auto_recording": "none"
if auto_recording == "No Recording"
else auto_recording.lower(),
"timezone": timezone,
}
headers = {
"Authorization": "Bearer " + authenticate(),
"content-type": "application/json",
}
response = requests.post(
"https://api.zoom.us/v2/users/me/meetings", headers=headers, data=json.dumps(payload)
)
if response.status_code == 201:
data = json.loads(response.text)
payload.update(
{
"doctype": "Class Course",
"parent": class_name,
"course": course,
"parenttype": "LMS Class",
"parentfield": "courses",
"doctype": "LMS Live Class",
"start_url": data.get("start_url"),
"join_url": data.get("join_url"),
"title": title,
"host": frappe.session.user,
"date": date,
"time": time,
"class_name": class_name,
"password": data.get("password"),
"description": description,
"auto_recording": auto_recording,
}
)
doc.save()
class_details = frappe.get_doc(payload)
class_details.save()
return class_details
def authenticate():
zoom = frappe.get_single("Zoom Settings")
if not zoom.enable:
frappe.throw(_("Please enable Zoom Settings to use this feature."))
authenticate_url = f"https://zoom.us/oauth/token?grant_type=account_credentials&account_id={zoom.account_id}"
headers = {
"Authorization": "Basic "
+ base64.b64encode(
bytes(
zoom.client_id
+ ":"
+ zoom.get_password(fieldname="client_secret", raise_exception=False),
encoding="utf8",
)
).decode()
}
response = requests.request("POST", authenticate_url, headers=headers)
return response.json()["access_token"]
@frappe.whitelist()
def create_class(
title,
start_date,
end_date,
description=None,
seat_count=0,
start_time=None,
end_time=None,
name=None,
):
if name:
class_details = frappe.get_doc("LMS Class", name)
else:
frappe.db.delete("Class Course", {"parent": class_name, "course": course})
return True
class_details = frappe.get_doc({"doctype": "LMS Class"})
class_details.update(
{
"title": title,
"start_date": start_date,
"end_date": end_date,
"description": description,
"seat_count": seat_count,
"start_time": start_time,
"end_time": end_time,
}
)
class_details.save()
return class_details

View File

@@ -27,4 +27,7 @@ frappe.ui.form.on("LMS Course", {
};
});
},
refresh: (frm) => {
frm.add_web_link(`/courses/${frm.doc.name}`, "See on Website");
},
});

View File

@@ -53,12 +53,11 @@
"in_list_view": 1,
"label": "Title",
"reqd": 1,
"unique": 1,
"width": "200"
},
{
"fieldname": "description",
"fieldtype": "Markdown Editor",
"fieldtype": "Text Editor",
"label": "Description",
"reqd": 1
},
@@ -261,7 +260,7 @@
}
],
"make_attachments_public": 1,
"modified": "2022-09-14 13:26:53.153822",
"modified": "2023-05-11 17:08:19.763405",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Course",

View File

@@ -2,19 +2,19 @@
# For license information, please see license.txt
import json
import random
import frappe
from frappe.model.document import Document
from frappe.utils import cint
from frappe.utils.telemetry import capture
from lms.lms.utils import get_chapters
from ...utils import generate_slug, validate_image
class LMSCourse(Document):
def validate(self):
self.validate_instructors()
self.validate_video_link()
self.validate_status()
self.image = validate_image(self.image)
@@ -30,6 +30,10 @@ class LMSCourse(Document):
}
).save(ignore_permissions=True)
def validate_video_link(self):
if self.video_link and "/" in self.video_link:
self.video_link = self.video_link.split("/")[-1]
def validate_status(self):
if self.published:
self.status = "Approved"
@@ -38,6 +42,9 @@ class LMSCourse(Document):
if not self.upcoming and self.has_value_changed("upcoming"):
self.send_email_to_interested_users()
def after_insert(self):
capture("course_created", "lms")
def send_email_to_interested_users(self):
interested_users = frappe.get_all(
"LMS Course Interest", {"course": self.name}, ["name", "user"]
@@ -67,7 +74,10 @@ class LMSCourse(Document):
def autoname(self):
if not self.name:
self.name = generate_slug(self.title, "LMS Course")
title = self.title
if self.title == "New Course":
title = self.title + str(random.randint(0, 99))
self.name = generate_slug(title, "LMS Course")
def __repr__(self):
return f"<Course#{self.name}>"
@@ -301,3 +311,43 @@ def save_lesson(
lesson_reference.save(ignore_permissions=True)
return doc.name
@frappe.whitelist()
def reorder_lesson(old_chapter, old_lesson_array, new_chapter, new_lesson_array):
if old_chapter == new_chapter:
sort_lessons(new_chapter, new_lesson_array)
else:
sort_lessons(old_chapter, old_lesson_array)
sort_lessons(new_chapter, new_lesson_array)
def sort_lessons(chapter, lesson_array):
lesson_array = json.loads(lesson_array)
for les in lesson_array:
ref = frappe.get_all("Lesson Reference", {"lesson": les}, ["name", "idx"])
if ref:
frappe.db.set_value(
"Lesson Reference",
ref[0].name,
{
"parent": chapter,
"idx": lesson_array.index(les) + 1,
},
)
@frappe.whitelist()
def reorder_chapter(chapter_array):
chapter_array = json.loads(chapter_array)
for chap in chapter_array:
ref = frappe.get_all("Chapter Reference", {"chapter": chap}, ["name", "idx"])
if ref:
frappe.db.set_value(
"Chapter Reference",
ref[0].name,
{
"idx": chapter_array.index(chap) + 1,
},
)

View File

@@ -35,7 +35,7 @@ class TestLMSCourse(unittest.TestCase):
if frappe.db.exists("LMS Course", "test-course"):
frappe.db.delete("Exercise Submission", {"course": "test-course"})
frappe.db.delete("Exercise Latest Submission", {"course": "test-course"})
frappe.db.delete("Exercise", {"course": "test-course"})
frappe.db.delete("LMS Exercise", {"course": "test-course"})
frappe.db.delete("LMS Batch Membership", {"course": "test-course"})
frappe.db.delete("LMS Batch", {"course": "test-course"})
frappe.db.delete("LMS Course Mentor Mapping", {"course": "test-course"})

View File

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

View File

@@ -99,7 +99,7 @@
"modified": "2021-09-29 15:27:55.585874",
"modified_by": "Administrator",
"module": "LMS",
"name": "Exercise",
"name": "LMS Exercise",
"owner": "Administrator",
"permissions": [
{

View File

@@ -7,7 +7,7 @@ from frappe.model.document import Document
from lms.lms.utils import get_membership
class Exercise(Document):
class LMSExercise(Document):
def get_user_submission(self):
"""Returns the latest submission for this user."""
user = frappe.session.user

View File

@@ -8,7 +8,7 @@ import frappe
from lms.lms.doctype.lms_course.test_lms_course import new_course
class TestExercise(unittest.TestCase):
class TestLMSExercise(unittest.TestCase):
def new_exercise(self):
course = new_course("Test Course")
member = frappe.get_doc(
@@ -21,7 +21,7 @@ class TestExercise(unittest.TestCase):
member.insert()
e = frappe.get_doc(
{
"doctype": "Exercise",
"doctype": "LMS Exercise",
"name": "test-problem",
"course": course.name,
"title": "Test Problem",
@@ -51,4 +51,4 @@ class TestExercise(unittest.TestCase):
def tearDown(self):
frappe.db.sql("delete from `tabLMS Batch Membership`")
frappe.db.sql("delete from `tabExercise Submission`")
frappe.db.sql("delete from `tabExercise`")
frappe.db.sql("delete from `tabLMS Exercise`")

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2023, Frappe and contributors
// For license information, please see license.txt
// frappe.ui.form.on("LMS Live Class", {
// refresh(frm) {
// },
// });

View File

@@ -0,0 +1,164 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2023-03-02 10:59:01.741349",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"title",
"host",
"class_name",
"password",
"auto_recording",
"column_break_astv",
"description",
"section_break_glxh",
"date",
"timezone",
"column_break_spvt",
"time",
"duration",
"section_break_yrpq",
"start_url",
"column_break_yokr",
"join_url"
],
"fields": [
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title",
"reqd": 1
},
{
"fieldname": "description",
"fieldtype": "Text",
"label": "Description"
},
{
"fieldname": "date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Date",
"reqd": 1
},
{
"fieldname": "duration",
"fieldtype": "Int",
"label": "Duration",
"reqd": 1
},
{
"fieldname": "timezone",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Timezone",
"reqd": 1
},
{
"fieldname": "host",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Host",
"options": "User",
"reqd": 1
},
{
"fieldname": "column_break_astv",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_glxh",
"fieldtype": "Section Break",
"label": "Date and Time"
},
{
"fieldname": "column_break_spvt",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_yrpq",
"fieldtype": "Section Break"
},
{
"fieldname": "start_url",
"fieldtype": "Small Text",
"label": "Start URL",
"read_only": 1
},
{
"fieldname": "column_break_yokr",
"fieldtype": "Column Break"
},
{
"fieldname": "join_url",
"fieldtype": "Small Text",
"label": "Join URL",
"read_only": 1
},
{
"fieldname": "password",
"fieldtype": "Password",
"label": "Password"
},
{
"fieldname": "time",
"fieldtype": "Time",
"label": "Time",
"reqd": 1
},
{
"fieldname": "class_name",
"fieldtype": "Link",
"label": "Class",
"options": "LMS Class"
},
{
"default": "No Recording",
"fieldname": "auto_recording",
"fieldtype": "Select",
"label": "Auto Recording",
"options": "No Recording\nLocal\nCloud"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-03-14 18:44:48.813102",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Live Class",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Moderator",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -0,0 +1,63 @@
# Copyright (c) 2023, Frappe and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
from datetime import timedelta
from frappe.utils import cint, get_datetime
class LMSLiveClass(Document):
def after_insert(self):
calendar = frappe.db.get_value(
"Google Calendar", {"user": frappe.session.user, "enable": 1}, "name"
)
if calendar:
event = self.create_event()
self.add_event_participants(event, calendar)
def create_event(self):
start = f"{self.date} {self.time}"
event = frappe.get_doc(
{
"doctype": "Event",
"subject": f"Live Class on {self.title}",
"starts_on": start,
"ends_on": get_datetime(start) + timedelta(minutes=cint(self.duration)),
}
)
event.save()
return event
def add_event_participants(self, event, calendar):
participants = frappe.get_all(
"Class Student", {"parent": self.class_name}, pluck="student"
)
participants.append(frappe.session.user)
for participant in participants:
frappe.get_doc(
{
"doctype": "Event Participants",
"reference_doctype": "User",
"reference_docname": participant,
"email": participant,
"parent": event.name,
"parenttype": "Event",
"parentfield": "event_participants",
}
).save()
event.reload()
event.update(
{
"sync_with_google_calendar": 1,
"google_calendar": calendar,
"description": f"A Live Class has been scheduled on {frappe.utils.format_date(self.date, 'medium')} at { frappe.utils.format_time(self.time, 'hh:mm a')}. Click on this link to join. {self.join_url}. {self.description}",
}
)
event.save()

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2023, Frappe and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestLMSLiveClass(FrappeTestCase):
pass

View File

@@ -21,17 +21,39 @@ class LMSQuiz(Document):
def validate_correct_answers(self):
for question in self.questions:
correct_options = self.get_correct_options(question)
if question.type == "Choices":
self.validate_correct_options(question)
else:
self.validate_possible_answer(question)
if len(correct_options) > 1:
question.multiple = 1
def validate_correct_options(self, question):
correct_options = self.get_correct_options(question)
if not len(correct_options):
frappe.throw(
_("At least one option must be correct for this question: {0}").format(
frappe.bold(question.question)
)
if len(correct_options) > 1:
question.multiple = 1
if not len(correct_options):
frappe.throw(
_("At least one option must be correct for this question: {0}").format(
frappe.bold(question.question)
)
)
def validate_possible_answer(self, question):
possible_answers_fields = [
"possibility_1",
"possibility_2",
"possibility_3",
"possibility_4",
]
possible_answers = list(filter(lambda x: question.get(x), possible_answers_fields))
if not len(possible_answers):
frappe.throw(
_("Add at least one possible answer for this question: {0}").format(
frappe.bold(question.question)
)
)
def get_correct_options(self, question):
correct_option_fields = [
@@ -126,17 +148,54 @@ def save_quiz(quiz_title, questions, quiz):
}
)
question_doc.update({"question": row["question"]})
for num in range(1, 5):
question_doc.update(
{
"option_" + cstr(num): row["option_" + cstr(num)],
"explanation_" + cstr(num): row["explanation_" + cstr(num)],
"is_correct_" + cstr(num): row["is_correct_" + cstr(num)],
}
)
question_doc.update(row)
question_doc.save(ignore_permissions=True)
return doc.name
@frappe.whitelist()
def check_answer(question, type, answer):
if type == "Choices":
return check_choice_answers(question, answer)
else:
return check_input_answers(question, answer)
def check_choice_answers(question, answer):
fields = []
for num in range(1, 5):
fields.append(f"option_{cstr(num)}")
fields.append(f"is_correct_{cstr(num)}")
question_details = frappe.db.get_value(
"LMS Quiz Question", question, fields, as_dict=1
)
for num in range(1, 5):
if question_details[f"option_{num}"] == answer:
return question_details[f"is_correct_{num}"]
return 0
def check_input_answers(question, answer):
fields = []
for num in range(1, 5):
fields.append(f"possibility_{cstr(num)}")
question_details = frappe.db.get_value(
"LMS Quiz Question", question, fields, as_dict=1
)
for num in range(1, 5):
current_possibility = question_details[f"possibility_{num}"]
if current_possibility and current_possibility.lower() == answer.lower():
return 1
return 0
@frappe.whitelist()
def get_user_quizzes():
return frappe.get_all(
"LMS Quiz", filters={"owner": frappe.session.user}, fields=["name", "title"]
)

View File

@@ -19,7 +19,8 @@ class TestLMSQuiz(unittest.TestCase):
quiz.append(
"questions",
{
"question": "Question multiple",
"question": "Question Multiple",
"type": "Choices",
"option_1": "Option 1",
"is_correct_1": 1,
"option_2": "Option 2",
@@ -35,12 +36,24 @@ class TestLMSQuiz(unittest.TestCase):
"questions",
{
"question": "Question no correct option",
"type": "Choices",
"option_1": "Option 1",
"option_2": "Option 2",
},
)
self.assertRaises(frappe.ValidationError, quiz.save)
def test_with_no_possible_answers(self):
quiz = frappe.get_doc("LMS Quiz", "test-quiz")
quiz.append(
"questions",
{
"question": "Question Possible Answers",
"type": "User Input",
},
)
self.assertRaises(frappe.ValidationError, quiz.save)
@classmethod
def tearDownClass(cls) -> None:
frappe.db.delete("LMS Quiz", "test-quiz")

View File

@@ -6,6 +6,7 @@
"engine": "InnoDB",
"field_order": [
"question",
"type",
"options_section",
"option_1",
"is_correct_1",
@@ -26,6 +27,13 @@
"is_correct_4",
"column_break_20",
"explanation_4",
"section_break_mnhr",
"possibility_1",
"possibility_3",
"column_break_vnaj",
"possibility_2",
"possibility_4",
"section_break_c1lf",
"multiple"
],
"fields": [
@@ -40,13 +48,13 @@
"fieldname": "option_1",
"fieldtype": "Data",
"label": "Option 1",
"reqd": 1
"mandatory_depends_on": "eval: doc.type == 'Choices'"
},
{
"fieldname": "option_2",
"fieldtype": "Data",
"label": "Option 2",
"reqd": 1
"mandatory_depends_on": "eval: doc.type == 'Choices'"
},
{
"fieldname": "option_3",
@@ -95,18 +103,22 @@
"read_only": 1
},
{
"depends_on": "eval: doc.type == 'Choices'",
"fieldname": "options_section",
"fieldtype": "Section Break"
},
{
"depends_on": "eval: doc.type == 'Choices'",
"fieldname": "column_break_4",
"fieldtype": "Section Break"
},
{
"depends_on": "eval: doc.type == 'Choices'",
"fieldname": "section_break_5",
"fieldtype": "Section Break"
},
{
"depends_on": "eval: doc.type == 'Choices'",
"fieldname": "section_break_11",
"fieldtype": "Section Break"
},
@@ -149,12 +161,52 @@
{
"fieldname": "column_break_20",
"fieldtype": "Column Break"
},
{
"fieldname": "type",
"fieldtype": "Select",
"label": "Type",
"options": "Choices\nUser Input"
},
{
"depends_on": "eval: doc.type == 'User Input'",
"fieldname": "section_break_mnhr",
"fieldtype": "Section Break"
},
{
"fieldname": "possibility_1",
"fieldtype": "Small Text",
"label": "Possible Answer 1",
"mandatory_depends_on": "eval: doc.type == 'User Input'"
},
{
"fieldname": "possibility_2",
"fieldtype": "Small Text",
"label": "Possible Answer 2"
},
{
"fieldname": "possibility_3",
"fieldtype": "Small Text",
"label": "Possible Answer 3"
},
{
"fieldname": "possibility_4",
"fieldtype": "Small Text",
"label": "Possible Answer 4"
},
{
"fieldname": "section_break_c1lf",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_vnaj",
"fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-07-19 19:35:28.446236",
"modified": "2023-03-17 18:22:20.324536",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Quiz Question",
@@ -162,5 +214,6 @@
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -11,7 +11,7 @@ class LMSSection(Document):
def get_exercise(self):
if self.type == "exercise":
return frappe.get_doc("Exercise", self.id)
return frappe.get_doc("LMS Exercise", self.id)
def get_quiz(self):
if self.type == "quiz":

View File

@@ -5,13 +5,18 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"default_home",
"send_calendar_invite_for_evaluations",
"allow_student_progress",
"column_break_zdel",
"is_onboarding_complete",
"force_profile_completion",
"section_break_szgq",
"search_placeholder",
"portal_course_creation",
"is_onboarding_complete",
"column_break_2",
"custom_certificate_template",
"livecode_url",
"force_profile_completion",
"signup_settings_tab",
"signup_settings_section",
"terms_of_use",
"terms_page",
@@ -22,6 +27,7 @@
"column_break_12",
"cookie_policy",
"cookie_policy_page",
"mentor_request_tab",
"mentor_request_section",
"mentor_request_creation",
"mentor_request_status_update"
@@ -81,8 +87,7 @@
},
{
"fieldname": "signup_settings_section",
"fieldtype": "Section Break",
"label": "Signup Settings"
"fieldtype": "Section Break"
},
{
"default": "0",
@@ -133,24 +138,54 @@
"fieldtype": "Check",
"label": "Ask User Category during Signup"
},
{
"fieldname": "custom_certificate_template",
"fieldtype": "Link",
"label": "Custom Certificate Template",
"options": "Web Template"
},
{
"default": "0",
"fieldname": "is_onboarding_complete",
"fieldtype": "Check",
"label": "Is Onboarding Complete",
"read_only": 1
},
{
"default": "0",
"fieldname": "default_home",
"fieldtype": "Check",
"label": "Make LMS the default home"
},
{
"fieldname": "column_break_zdel",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "send_calendar_invite_for_evaluations",
"fieldtype": "Check",
"label": "Send calendar invite for evaluations"
},
{
"fieldname": "section_break_szgq",
"fieldtype": "Section Break"
},
{
"fieldname": "signup_settings_tab",
"fieldtype": "Tab Break",
"label": "Signup Settings"
},
{
"fieldname": "mentor_request_tab",
"fieldtype": "Tab Break",
"label": "Mentor Request"
},
{
"default": "0",
"fieldname": "allow_student_progress",
"fieldtype": "Check",
"label": "Allow students to see each others progress in class"
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2022-12-20 11:44:06.317159",
"modified": "2023-04-17 12:54:44.706101",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Settings",

View File

@@ -4,7 +4,38 @@
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import get_url_to_list
class LMSSettings(Document):
pass
def validate(self):
self.validate_google_settings()
def validate_google_settings(self):
if self.send_calendar_invite_for_evaluations:
google_settings = frappe.get_single("Google Settings")
if not google_settings.enable:
frappe.throw(
_("Enable Google API in Google Settings to send calendar invites for evaluations.")
)
if not google_settings.client_id or not google_settings.client_secret:
frappe.throw(
_(
"Enter Client Id and Client Secret in Google Settings to send calendar invites for evaluations."
)
)
calendars = frappe.db.count("Google Calendar")
if not calendars:
frappe.throw(
_(
"Please add <a href='{0}'>{1}</a> for <a href='{2}'>{3}</a> to send calendar invites for evaluations."
).format(
get_url_to_list("Google Calendar"),
frappe.bold("Google Calendar"),
get_url_to_list("Course Evaluator"),
frappe.bold("Course Evaluator"),
)
)

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2023, Frappe and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestZoomSettings(FrappeTestCase):
pass

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2023, Frappe and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Zoom Settings", {
// refresh(frm) {
// },
// });

View File

@@ -0,0 +1,71 @@
{
"actions": [],
"creation": "2023-02-27 14:30:28.696814",
"default_view": "List",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"enable",
"sb_00",
"account_id",
"client_id",
"client_secret"
],
"fields": [
{
"default": "0",
"fieldname": "enable",
"fieldtype": "Check",
"label": "Enable"
},
{
"depends_on": "enable",
"fieldname": "sb_00",
"fieldtype": "Section Break",
"label": "OAuth Client ID"
},
{
"description": "The Client ID obtained from the Google Cloud Console under <a href=\"https://console.cloud.google.com/apis/credentials\">\n\"APIs &amp; Services\" &gt; \"Credentials\"\n</a>",
"fieldname": "client_id",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Client ID",
"mandatory_depends_on": "google_drive_picker_enabled"
},
{
"fieldname": "client_secret",
"fieldtype": "Password",
"in_list_view": 1,
"label": "Client Secret"
},
{
"fieldname": "account_id",
"fieldtype": "Data",
"label": "Account ID"
}
],
"issingle": 1,
"links": [],
"modified": "2023-03-01 17:15:59.722497",
"modified_by": "Administrator",
"module": "LMS",
"name": "Zoom Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "ASC",
"states": [],
"track_changes": 1
}

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2023, Frappe and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class ZoomSettings(Document):
pass

View File

@@ -0,0 +1,27 @@
{
"attach_print": 0,
"channel": "Email",
"creation": "2023-03-27 16:34:03.505645",
"days_in_advance": 0,
"docstatus": 0,
"doctype": "Notification",
"document_type": "Lesson Assignment",
"enabled": 1,
"event": "New",
"idx": 0,
"is_standard": 1,
"message": "<h3> {{ _(\"Assignment Submission\") }}\n\n{% set title = frappe.db.get_value(\"Course Lesson\", doc.lesson, \"title\") %}\n\n<p> {{ _(\"{0} has submitted their assignment for the lesson {1}\").format(doc.member_name, title) }} </p>\n\n <p> {{ _(\" Please evaluate and grade the assignment. \") }} </p>",
"modified": "2023-03-27 16:46:44.564007",
"modified_by": "Administrator",
"module": "LMS",
"name": "Assignment Submission Notification",
"owner": "Administrator",
"recipients": [
{
"receiver_by_document_field": "evaluator"
}
],
"send_system_notification": 0,
"send_to_all_assignees": 0,
"subject": "Assignment Submission"
}

View File

@@ -0,0 +1,11 @@
<div style="background-color: #f4f5f6; padding: 1rem;">
<div style="background-color: #ffffff; width: 75%; margin: 0 auto; padding: 1rem;">
<h3> {{ _("Assignment Submission") }} </h3>
{% set title = frappe.db.get_value("Course Lesson", doc.lesson, "title") %}
<br>
<p> {{ _("{0} has submitted their assignment for the lesson {1}").format(frappe.bold(doc.member_name), frappe.bold(title)) }}
</p>
<p> {{ _(" Please evaluate and grade the assignment.") }} </p>
</div>
</div>

View File

@@ -10,8 +10,8 @@
"event": "New",
"idx": 0,
"is_standard": 1,
"message": "{% set title = frappe.db.get_value(\"LMS Course\", doc.course, \"title\") %}\n<p> {{ _('Your evaluation for the course ${0} has been scheduled on ${1} at ${2}.').format(title, frappe.utils.format_date(doc.date, \"medium\"), frappe.utils.format_time(doc.start_time, \"short\")) }}</p>\n<p> {{ _(\"Please prepare well and be on time for the evaluations.\") }} </p>\n",
"modified": "2022-06-03 11:49:01.310656",
"message": "{% set title = frappe.db.get_value(\"LMS Course\", doc.course, \"title\") %}\n\n<p> {{ _(\"Hey {0}\").format(doc.member_name) }} </p>\n<p> {{ _('Your evaluation for the course {0} has been scheduled on {1} at {2}.').format(title, frappe.utils.format_date(doc.date, \"medium\"), frappe.utils.format_time(doc.start_time, \"short\")) }}</p>\n<p> {{ _(\"Please prepare well and be on time for the evaluations.\") }} </p>\n",
"modified": "2023-02-28 19:53:47.716135",
"modified_by": "Administrator",
"module": "LMS",
"name": "Certificate Request Creation",
@@ -19,6 +19,9 @@
"recipients": [
{
"receiver_by_document_field": "member"
},
{
"receiver_by_document_field": "evaluator"
}
],
"send_system_notification": 0,

View File

View File

@@ -0,0 +1,32 @@
{
"absolute_value": 0,
"align_labels_right": 0,
"creation": "2023-02-22 21:36:54.560420",
"css": ".outer-border {\n font-family: \"Inter\" sans-serif;\n font-size: 16px;\n border-radius: 0.5rem;\n border: 1px solid #E2E6E9;\n padding: 1rem;\n}\n\n.inner-border {\n border: 10px solid #0089FF;\n border-radius: 8px;\n text-align: center;\n padding: 6rem 4rem;\n background-color: #FFFFFF;\n}\n\n.certificate-logo {\n height: 1.5rem;\n margin-bottom: 4rem;\n}\n\n.certificate-name {\n font-size: 2rem;\n font-weight: 500;\n color: #192734;\n margin-bottom: 0.5rem;\n}\n\n.certificate-footer {\n margin: 4rem auto 0;\n width: 70%;\n text-align: center;\n}\n\n.certificate-footer-item {\n color: #192734;\n}\n\n.cursive-font {\n font-family: cursive;\n font-weight: 600;\n}\n\n.certificate-divider {\n margin: 0.5rem 0;\n}\n\n.certificate-expiry {\n margin-left: 2rem;\n}",
"custom_format": 1,
"disabled": 0,
"doc_type": "LMS Certificate",
"docstatus": 0,
"doctype": "Print Format",
"font_size": 14,
"format_data": "{\"header\":\"<div class=\\\"document-header\\\">\\n\\t<h3>LMS Certificate</h3>\\n\\t<p>{{ doc.name }}</p>\\n</div>\",\"sections\":[{\"label\":\"\",\"columns\":[{\"label\":\"\",\"fields\":[{\"label\":\"Course\",\"fieldname\":\"course\",\"fieldtype\":\"Link\",\"options\":\"LMS Course\"},{\"label\":\"Member\",\"fieldname\":\"member\",\"fieldtype\":\"Link\",\"options\":\"User\"},{\"label\":\"Member Name\",\"fieldname\":\"member_name\",\"fieldtype\":\"Data\"},{\"label\":\"Evaluator\",\"fieldname\":\"evaluator\",\"fieldtype\":\"Data\",\"options\":\"\"}]},{\"label\":\"\",\"fields\":[{\"label\":\"Issue Date\",\"fieldname\":\"issue_date\",\"fieldtype\":\"Date\"},{\"label\":\"Expiry Date\",\"fieldname\":\"expiry_date\",\"fieldtype\":\"Date\"},{\"label\":\"Version\",\"fieldname\":\"version\",\"fieldtype\":\"Select\",\"options\":\"V13\\nV14\"},{\"label\":\"Module Names for Certificate\",\"fieldname\":\"module_names_for_certificate\",\"fieldtype\":\"Data\"}]}],\"has_fields\":true}]}",
"html": "{% set certificate = frappe.db.get_value(\"LMS Certificate\", doc.name, [\"name\", \"member\", \"issue_date\", \"expiry_date\", \"course\"], as_dict=True) %}\n{% set member = frappe.db.get_value(\"User\", doc.member, [\"full_name\"], as_dict=True) %}\n{% set course = frappe.db.get_value(\"LMS Course\", doc.course, [\"title\", \"name\", \"image\"], as_dict=True) %}\n{% set logo = frappe.db.get_single_value(\"Website Settings\", \"banner_image\") %}\n{% set instructors = frappe.get_all(\"Course Instructor\", {\"parent\": doc.course}, pluck=\"instructor\", order_by=\"idx\") %}\n\n<meta name=\"pdfkit-orientation\" content=\"Landscape\">\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500&display=swap\" rel=\"stylesheet\">\n\n<div class=\"outer-border\">\n <div class=\"inner-border\">\n \n <img src=\"{{ logo }}\" class=\"certificate-logo\">\n <div>\n {{ _(\"This certifies that\") }}\n </div>\n \n <div class=\"certificate-name\" style=\"\">\n {{ member.full_name }}\n </div>\n <div>\n {{ _(\"has successfully completed the course on\") }}\n <b> {{ course.title }} </b>\n on {{ frappe.utils.format_date(certificate.issue_date, \"medium\") }}.\n </div>\n \n <table class=\"certificate-footer\">\n <tr>\n {% if instructors %}\n <td>\n <div class=\"certificate-footer-item cursive-font\">\n {% for i in instructors %}\n \t\t\t\t\t{{ frappe.db.get_value(\"User\", i, \"full_name\") }}\n \t\t\t\t\t{% if not loop.last %}\n \t\t\t\t\t,\n \t\t\t\t\t{% endif %}\n \t\t\t\t\t{% endfor %}\n </div>\n <hr class=\"certificate-divider\">\n <div class=\"text-center\"> {{ _(\"Course Instructor\") }} </div>\n </td>\n {% endif %}\n \n {% if certificate.expiry_date %}\n <td style=\"width: 30%\"></td>\n \n <td class=\"certificate-expiry\">\n <div class=\"certificate-footer-item\">\n {{ frappe.utils.format_date(certificate.expiry_date, \"medium\") }}\n </div>\n <hr class=\"certificate-divider\">\n <div class=\"text-center\"> {{ _(\"Expiry Date\") }} </div>\n </td>\n {% endif %}\n </tr>\n </table>\n </div>\n </div>",
"idx": 0,
"line_breaks": 0,
"margin_bottom": 0.0,
"margin_left": 0.0,
"margin_right": 0.0,
"margin_top": 0.0,
"modified": "2023-04-17 13:46:38.633751",
"modified_by": "Administrator",
"module": "LMS",
"name": "Certificate",
"owner": "Administrator",
"page_number": "Hide",
"print_format_builder": 0,
"print_format_builder_beta": 1,
"print_format_type": "Jinja",
"raw_printing": 0,
"show_section_headings": 0,
"standard": "Yes"
}

View File

@@ -93,18 +93,24 @@ def get_chapters(course):
return chapters
def get_lessons(course, chapter=None):
def get_lessons(course, chapter=None, get_details=True):
"""If chapter is passed, returns lessons of only that chapter.
Else returns lessons of all chapters of the course"""
lessons = []
lesson_count = 0
if chapter:
return get_lesson_details(chapter)
if get_details:
return get_lesson_details(chapter)
else:
return frappe.db.count("Lesson Reference", {"parent": chapter.name})
for chapter in get_chapters(course):
lesson = get_lesson_details(chapter)
lessons += lesson
if get_details:
lessons += get_lesson_details(chapter)
else:
lesson_count += frappe.db.count("Lesson Reference", {"parent": chapter.name})
return lessons
return lessons if get_details else lesson_count
def get_lesson_details(chapter):
@@ -135,8 +141,8 @@ def get_lesson_details(chapter):
macros = find_macros(lesson_details.body)
for macro in macros:
if macro[0] == "YouTubeVideo":
lesson_details.icon = "icon-video"
if macro[0] == "YouTubeVideo" or macro[0] == "Video":
lesson_details.icon = "icon-youtube"
elif macro[0] == "Quiz":
lesson_details.icon = "icon-quiz"
lessons.append(lesson_details)
@@ -495,12 +501,17 @@ def can_create_courses(member=None):
if not member:
member = frappe.session.user
if frappe.session.user == "Guest":
return False
if has_course_instructor_role(member) or has_course_moderator_role(member):
return True
portal_course_creation = frappe.db.get_single_value(
"LMS Settings", "portal_course_creation"
)
return frappe.session.user != "Guest" and (
portal_course_creation == "Anyone" or has_course_instructor_role(member)
)
return portal_course_creation == "Anyone"
def has_course_moderator_role(member=None):
@@ -611,15 +622,17 @@ def get_filtered_membership(course, memberships):
def show_start_learing_cta(course, membership):
return (
not course.disable_self_learning
and not membership
and not course.upcoming
and not check_profile_restriction()
and not is_instructor(course.name)
and course.status == "Approved"
and has_lessons(course)
)
if course.disable_self_learning or course.upcoming:
return False
if is_instructor(course.name):
return False
if course.status != "Approved":
return False
if not has_lessons(course):
return False
if not membership:
return True
def has_lessons(course):
@@ -685,3 +698,19 @@ def get_course_completion_data():
}
],
}
def get_telemetry_boot_info():
POSTHOG_PROJECT_FIELD = "posthog_project_id"
POSTHOG_HOST_FIELD = "posthog_host"
if not frappe.conf.get(POSTHOG_HOST_FIELD) or not frappe.conf.get(
POSTHOG_PROJECT_FIELD
):
return {}
return {
"posthog_host": frappe.conf.get(POSTHOG_HOST_FIELD),
"posthog_project_id": frappe.conf.get(POSTHOG_PROJECT_FIELD),
"enable_telemetry": 1,
}

View File

@@ -1,3 +0,0 @@
frappe.ready(function () {
// bind events here
});

View File

@@ -1,87 +0,0 @@
{
"accept_payment": 0,
"allow_comments": 0,
"allow_delete": 0,
"allow_edit": 0,
"allow_incomplete": 0,
"allow_multiple": 0,
"allow_print": 0,
"amount": 0.0,
"amount_based_on_field": 0,
"apply_document_permissions": 0,
"button_label": "Save",
"creation": "2022-11-11 12:10:29.640675",
"custom_css": "",
"doc_type": "LMS Class",
"docstatus": 0,
"doctype": "Web Form",
"idx": 0,
"is_standard": 1,
"list_columns": [],
"login_required": 0,
"max_attachment_size": 0,
"modified": "2022-11-21 10:56:01.627821",
"modified_by": "Administrator",
"module": "LMS",
"name": "class",
"owner": "Administrator",
"payment_button_label": "Buy Now",
"published": 1,
"route": "class",
"show_attachments": 0,
"show_list": 0,
"show_sidebar": 0,
"success_title": "",
"success_url": "/classes",
"title": "Class",
"web_form_fields": [
{
"allow_read_on_all_link_options": 0,
"fieldname": "title",
"fieldtype": "Data",
"hidden": 0,
"label": "Title",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 1,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "start_date",
"fieldtype": "Date",
"hidden": 0,
"label": "Start Date",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 1,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "end_date",
"fieldtype": "Date",
"hidden": 0,
"label": "End Date",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 1,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "description",
"fieldtype": "Small Text",
"hidden": 0,
"label": "Description",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
}
]
}

View File

@@ -1,7 +1,10 @@
frappe.ready(function () {
frappe.web_form.after_save = () => {
setTimeout(() => {
window.history.back();
});
let data = frappe.web_form.get_values();
if (data.class) {
setTimeout(() => {
window.location.href = `/classes/${data.class}`;
}, 2000);
}
};
});

View File

@@ -20,7 +20,7 @@
"list_columns": [],
"login_required": 1,
"max_attachment_size": 0,
"modified": "2022-11-25 17:05:30.851109",
"modified": "2023-02-23 13:04:00.405266",
"modified_by": "Administrator",
"module": "LMS",
"name": "evaluation",
@@ -29,7 +29,7 @@
"published": 1,
"route": "evaluation",
"show_attachments": 0,
"show_list": 0,
"show_list": 1,
"show_sidebar": 0,
"title": "Evaluation",
"web_form_fields": [
@@ -59,6 +59,31 @@
"reqd": 1,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "summary",
"fieldtype": "Small Text",
"hidden": 0,
"label": "Summary",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "",
"fieldtype": "Column Break",
"hidden": 0,
"label": "",
"max_length": 0,
"max_value": 0,
"options": "",
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "date",
@@ -95,19 +120,6 @@
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "",
"fieldtype": "Column Break",
"hidden": 0,
"label": "",
"max_length": 0,
"max_value": 0,
"options": "",
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "rating",
@@ -135,13 +147,14 @@
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "summary",
"fieldtype": "Small Text",
"hidden": 0,
"label": "Summary",
"fieldname": "class",
"fieldtype": "Link",
"hidden": 1,
"label": "Class",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"options": "LMS Class",
"read_only": 1,
"reqd": 0,
"show_in_filter": 0
}

View File

@@ -13,7 +13,7 @@
"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}",
"custom_css": "",
"doc_type": "User",
"docstatus": 0,
"doctype": "Web Form",
@@ -22,7 +22,7 @@
"list_columns": [],
"login_required": 1,
"max_attachment_size": 0,
"modified": "2022-09-05 13:08:40.071348",
"modified": "2023-01-09 15:45:11.411692",
"modified_by": "Administrator",
"module": "LMS",
"name": "profile",
@@ -100,22 +100,10 @@
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "headline",
"fieldname": "city",
"fieldtype": "Data",
"hidden": 0,
"label": "Headline",
"max_length": 0,
"max_value": 0,
"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",
"label": "City",
"max_length": 0,
"max_value": 0,
"read_only": 0,
@@ -135,6 +123,31 @@
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "",
"fieldtype": "Column Break",
"hidden": 0,
"label": "",
"max_length": 0,
"max_value": 0,
"options": "",
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "headline",
"fieldtype": "Data",
"hidden": 0,
"label": "Headline",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "linkedin",
@@ -173,10 +186,10 @@
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "city",
"fieldtype": "Data",
"fieldname": "looking_for_job",
"fieldtype": "Check",
"hidden": 0,
"label": "City",
"label": "I am looking for a job",
"max_length": 0,
"max_value": 0,
"read_only": 0,
@@ -185,16 +198,42 @@
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "education_details",
"fieldtype": "Section Break",
"fieldname": "bio",
"fieldtype": "Small Text",
"hidden": 0,
"label": "Education Details",
"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": "",
"fieldtype": "Page Break",
"hidden": 0,
"label": "",
"max_length": 0,
"max_value": 0,
"options": "",
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "",
"fieldtype": "Section Break",
"hidden": 0,
"label": "",
"max_length": 0,
"max_value": 0,
"options": "",
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "education",
@@ -297,187 +336,6 @@
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "carrer_preference_details",
"fieldtype": "Section Break",
"hidden": 0,
"label": "Career Preference",
"max_length": 0,
"max_value": 0,
"options": "",
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "preferred_functions",
"fieldtype": "Table",
"hidden": 0,
"label": "Preferred Functions",
"max_length": 0,
"max_value": 0,
"options": "Preferred Function",
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "preferred_industries",
"fieldtype": "Table",
"hidden": 0,
"label": "Preferred Industries",
"max_length": 0,
"max_value": 0,
"options": "Preferred Industry",
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "preferred_location",
"fieldtype": "Data",
"hidden": 0,
"label": "Preferred Locations",
"max_length": 0,
"max_value": 0,
"options": "",
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "dream_companies",
"fieldtype": "Data",
"hidden": 0,
"label": "Dream Companies",
"max_length": 0,
"max_value": 0,
"options": "",
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "work_environment",
"fieldtype": "Section Break",
"hidden": 0,
"label": "Work Environment",
"max_length": 0,
"max_value": 0,
"options": "",
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "attire",
"fieldtype": "Select",
"hidden": 0,
"label": "Attire Preference",
"max_length": 0,
"max_value": 0,
"options": "Casual Wear\nFormal Wear",
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "collaboration",
"fieldtype": "Select",
"hidden": 0,
"label": "Collaboration Preference",
"max_length": 0,
"max_value": 0,
"options": "Individual Work\nTeam Work\nBoth Individual and Team Work",
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "role",
"fieldtype": "Select",
"hidden": 0,
"label": "Role Preference",
"max_length": 0,
"max_value": 0,
"options": "Clearly Defined Role\nUnstructured Role",
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "location_preference",
"fieldtype": "Select",
"hidden": 0,
"label": "Location Preference",
"max_length": 0,
"max_value": 0,
"options": "Travel\nOffice close to Home",
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "time",
"fieldtype": "Select",
"hidden": 0,
"label": "Time Preference",
"max_length": 0,
"max_value": 0,
"options": "Flexible Time\nFixed 9-5",
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "company_type",
"fieldtype": "Select",
"hidden": 0,
"label": "Company Type",
"max_length": 0,
"max_value": 0,
"options": "Corporate Organization\nStartup Organization",
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "looking_for_job",
"fieldtype": "Check",
"hidden": 0,
"label": "I am looking for a job",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"description": "Private Information includes your Mobile Number, Email Address, Grade Type, Grade and Work Environment Preferences",
"fieldname": "hide_private",
"fieldtype": "Check",
"hidden": 0,
"label": "Hide my Private Information from others",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
}
]
}

View File

@@ -1,78 +1,66 @@
{% set chapters = get_chapters(course.name) %}
{% set is_instructor = is_instructor(course.name) %}
{% if course.edit_mode or chapters | length %}
{% if chapters | length %}
<div class="course-home-outline">
{% if course.edit_mode and course.name %}
<button class="btn btn-md btn-secondary btn-chapter pull-right"> {{ _("New Chapter") }} </button>
{% endif %}
{% if course.name and (course.edit_mode or chapters | length) %}
<div class="course-home-headings" id="outline-heading">
{% if not lesson_page %}
<div class="page-title mb-8" id="outline-heading" data-course="{{ course.name }}">
{{ _("Course Content") }}
</div>
{% endif %}
{% if course.edit_mode and course.name and not chapters | length %}
<div class="chapter-parent chapter-edit new-chapter">
<div contenteditable="true" data-placeholder="{{ _('Chapter Name') }}" class="chapter-title-main"></div>
<div class="chapter-description small my-2" contenteditable="true" data-placeholder="{{ _('Short Description') }}"></div>
<button class="btn btn-sm btn-secondary d-block btn-save-chapter" data-index="1"> {{ _('Save') }} </button>
</div>
<!-- <div class="mb-2">
<span>
{{ chapters | length }} chapters
</span>
<span>
. {{ get_lessons(course.name, None, False) }} lessons
</span>
</div> -->
{% endif %}
{% if chapters | length %}
<div>
{% for chapter in chapters %}
<div class="chapter-parent {% if course.edit_mode %} chapter-edit {% endif %} ">
<div class="chapter-title" {% if not course.edit_mode %} data-toggle="collapse" aria-expanded="false"
data-target="#{{ get_slugified_chapter_title(chapter.title) }}" {% endif %} >
{% if not course.edit_mode %}
{% set lessons = get_lessons(course.name, chapter) %}
<div class="chapter-parent" data-chapter="{{ chapter.name }}">
<div class="chapter-title" data-toggle="collapse" aria-expanded="false"
data-target="#{{ get_slugified_chapter_title(chapter.title) }}">
<img class="chapter-icon" src="/assets/lms/icons/chevron-right.svg">
{% endif %}
<div class="w-100 chapter-title-main" {% if course.edit_mode %} contenteditable="true" {% endif %} >{{ chapter.title }}</div>
<div class="chapter-title-main">
{{ chapter.title }}
</div>
<!-- <div class="small ml-auto">
{{ lessons | length }} lessons
</div> -->
</div>
{% set lessons = get_lessons(course.name, chapter) %}
<div class="chapter-content {% if not course.edit_mode %} collapse navbar-collapse {% endif %} "
id="{{ get_slugified_chapter_title(chapter.title) }}">
<div class="chapter-content collapse navbar-collapse" id="{{ get_slugified_chapter_title(chapter.title) }}">
{% if chapter.description or course.edit_mode %}
<div {% if course.edit_mode %} contenteditable="true" {% endif %} class="chapter-description
{% if not course.edit_mode %} mx-8 mb-2 {% endif %} "
data-placeholder="{{ _('Short Description') }}">{{ chapter.description }}</div>
{% endif %}
{% if course.edit_mode %}
<div class="mt-2">
<button class="btn btn-sm btn-secondary btn-save-chapter"
data-index="{{ loop.index }}" data-chapter="{{ chapter.name }}"> {{ _('Save') }} </button>
<a class="btn btn-sm btn-secondary btn-lesson ml-2"
href="/courses/{{ course.name }}/learn/{{loop.index}}.{{ lessons | length + 1 }}?edit=1"> {{ _("New Lesson") }} </a>
{% if chapter.description %}
<div class="chapter-description">
{{ chapter.description }}
</div>
{% endif %}
{% set is_instructor = is_instructor(course.name) %}
{% if lessons | length %}
<div class="lessons">
{% if course.edit_mode %}
<b class="course-meta"> {{ _("Lessons") }}: </b>
{% endif %}
{% if lessons | length %}
{% for lesson in lessons %}
{% set active = membership.current_lesson == lesson.name %}
<div class="lesson-info {% if active and not course.edit_mode %} active-lesson {% endif %}">
<div data-lesson="{{ lesson.name }}" class="lesson-info {% if active %} active-lesson {% endif %}">
{% if membership or lesson.include_in_preview or is_instructor or has_course_moderator_role() %}
<a class="lesson-links" data-course="{{ course.name }}"
<a class="lesson-links" href="{{ get_lesson_url(course.name, lesson.number) }}{{course.query_parameter}}"
{% if is_instructor and not lesson.include_in_preview %}
title="{{ _('This lesson is not available for preview. As you are the Instructor of the course only you can see it.') }}"
{% endif %}
href="{{ get_lesson_url(course.name, lesson.number) }}{% if course.edit_mode and is_instructor %}?edit=1{% endif %}{{course.query_parameter}}">
{% endif %}>
<svg class="icon icon-sm mr-2">
<use class="" href="#{{ lesson.icon }}">
@@ -81,15 +69,15 @@
<span>{{ lesson.title }}</span>
{% if membership %}
<svg class="icon icon-sm lesson-progress-tick {{ get_progress(course.name, lesson.name) != 'Complete' and 'hide' }}">
<use class="" href="#icon-green-check">
<svg class="icon icon-md lesson-progress-tick ml-auto {{ get_progress(course.name, lesson.name) != 'Complete' and 'hide' }}">
<use class="" href="#icon-success">
</svg>
{% endif %}
</a>
{% else %}
<div class="no-preview" title="This lesson is not available for preview" data-course="{{ course.name }}">
<div class="no-preview" title="This lesson is not available for preview">
<div class="lesson-links">
<svg class="icon icon-sm mr-2">
<use class="" href="#icon-lock-gray">
@@ -102,12 +90,13 @@
</div>
{% endfor %}
</div>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
@@ -119,87 +108,3 @@
{{ widgets.NoPreviewModal(course=course, membership=membership) }}
{% endif %}
<script>
frappe.ready(() => {
expand_the_active_chapter();
$(".chapter-title").unbind().click((e) => {
rotate_chapter_icon(e);
});
$(".no-preview").click((e) => {
show_no_preview_dialog(e);
});
});
const expand_the_first_chapter = () => {
let elements = $(".course-home-outline .collapse");
elements.each((i, element) => {
if (i < 1) {
show_section(element);
return false;
}
});
};
const expand_the_active_chapter = () => {
/* Find anchor matching the URL for course details page */
let 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");
$(".lesson-info").each((i, elem) => {
let href = $(elem).find("use").attr("href");
href.endsWith("blue") && $(elem).find("use").attr("href", href.substring(0, href.length - 5));
})
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();
}
};
const show_section = (element) => {
$(element).addClass("show");
$(element).siblings(".chapter-title").children(".chapter-icon").css("transform", "rotate(90deg)");
$(element).siblings(".chapter-title").attr("aria-expanded", true);
};
const rotate_chapter_icon = (e) => {
let icon = $(e.currentTarget).children(".chapter-icon");
if (icon.css("transform") == "none") {
icon.css("transform", "rotate(90deg)");
} else {
icon.css("transform", "none");
}
};
const show_no_preview_dialog = (e) => {
$("#no-preview-modal").modal("show");
};
</script>

View File

@@ -1,19 +1,31 @@
{% set color = get_palette(member.full_name) %}
<div class="common-card-style member-card">
{{ widgets.Avatar(member=member, avatar_class=avatar_class) }}
<div class="bold-title mt-4">
{{ member.full_name }}
</div>
<div class="d-flex">
{{ widgets.Avatar(member=member, avatar_class=avatar_class) }}
{% if member.headline %}
<div> {{ member.headline }} </div>
{% endif %}
<div class="ml-3 my-auto">
<div class="member-card-title">
{{ member.full_name }}
</div>
{% if member.headline %}
<div> {{ member.headline }} </div>
{% endif %}
{% if member.looking_for_job %}
<div class="indicator-pill green"> {{ _("Open Network") }} </div>
{% endif %}
{% set course_count = get_authored_courses(member.name, True) | length %}
{% set suffix = "Courses" if course_count > 1 else "Course" %}
{% if show_course_count and course_count > 0 %}
<div class="">
Created {{ course_count }} {{ suffix }}
</div>
{% endif %}
</div>
<a class="stretched-link" href="{{ get_profile_url(member.username) }}"></a>
</div>
{% set course_count = get_authored_courses(member.name, True) | length %}
{% if show_course_count and course_count > 0 %}
{% set suffix = "Courses" if course_count > 1 else "Course" %}
<div class="">
Created {{ course_count }} {{ suffix }}
</div>
{% endif %}
<a class="stretched-link" href="{{ get_profile_url(member.username) }}"></a>
</div>

View File

@@ -9,13 +9,14 @@
"label": "Enrollments"
}
],
"content": "[{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Get Started</b></span>\",\"col\":12}},{\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"/courses\\\" draggable=\\\"false\\\">Visit LMS Portal</a>\",\"col\":4}},{\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"/courses/new-course\\\" draggable=\\\"false\\\">Create a Course</a>\",\"col\":4}},{\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"/app/web-page/new-web-page-1\\\">Setup a Home Page</a>\",\"col\":4}},{\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"/app/website-settings/Website%20Settings\\\">Website Settings</a>\",\"col\":4}},{\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"https://docs.frappelms.com\\\">Documentation</a>\",\"col\":4}},{\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"https://frappe.school/courses/introducing-frappe-lms\\\">Video Tutorials</a>\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Signups\",\"col\":6}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Enrollments\",\"col\":6}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span style=\\\"font-size: 18px;\\\"><b>Statistics</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Users\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Course\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Enrollments\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Course Completed\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Certificate\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Evaluation\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Master</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Course Data\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Course Stats\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Certification\",\"col\":4}}]",
"content": "[{\"id\":\"jNO4sdKxHu\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Get Started</b></span>\",\"col\":12}},{\"id\":\"5s0qRBc4rY\",\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"/courses\\\" draggable=\\\"false\\\">Visit LMS Portal</a>\",\"col\":4}},{\"id\":\"lGMuNLpmv-\",\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"/courses/new-course/edit\\\">Create a Course</a>\",\"col\":4}},{\"id\":\"3TVyc9AkPy\",\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"/app/web-page/new-web-page-1\\\">Setup a Home Page</a>\",\"col\":4}},{\"id\":\"9zcbqpu2gm\",\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"/app/lms-settings/LMS%20Settings\\\">LMS Setting</a>\",\"col\":4}},{\"id\":\"0ATmnKmXjc\",\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"https://docs.frappelms.com\\\">Documentation</a>\",\"col\":4}},{\"id\":\"7tGB2TYPmn\",\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"https://frappe.school/courses/introducing-frappe-lms\\\">Video Tutorials</a>\",\"col\":4}},{\"id\":\"C128a4abjX\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"5q4sPiv2ci\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Signups\",\"col\":6}},{\"id\":\"8NSaRaEV5u\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Enrollments\",\"col\":6}},{\"id\":\"kMuzko0uAU\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"iuvIOHmztI\",\"type\":\"header\",\"data\":{\"text\":\"<span style=\\\"font-size: 18px;\\\"><b>Statistics</b></span>\",\"col\":12}},{\"id\":\"l0VTd66Uy2\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Users\",\"col\":4}},{\"id\":\"wAWZin1KKk\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Course\",\"col\":4}},{\"id\":\"RLrIlFx0Hd\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Enrollments\",\"col\":4}},{\"id\":\"OuhWkhCQmq\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Course Completed\",\"col\":4}},{\"id\":\"3g8QmNqUXG\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Certificate\",\"col\":4}},{\"id\":\"EZsdsujs8N\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Evaluation\",\"col\":4}},{\"id\":\"s-nfsFQbGV\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"jeOBWBzHEa\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Master</b></span>\",\"col\":12}},{\"id\":\"sVhgfS5GIh\",\"type\":\"card\",\"data\":{\"card_name\":\"Course Data\",\"col\":4}},{\"id\":\"Iea0snm4Fg\",\"type\":\"card\",\"data\":{\"card_name\":\"Course Stats\",\"col\":4}},{\"id\":\"bZB7RqOl6a\",\"type\":\"card\",\"data\":{\"card_name\":\"Certification\",\"col\":4}}]",
"creation": "2021-10-21 17:20:01.358903",
"docstatus": 0,
"doctype": "Workspace",
"hide_custom": 0,
"icon": "education",
"idx": 0,
"is_hidden": 0,
"label": "LMS",
"links": [
{
@@ -143,10 +144,11 @@
"type": "Link"
}
],
"modified": "2022-12-28 17:45:18.539185",
"modified": "2023-05-11 15:41:25.514442",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS",
"number_cards": [],
"owner": "Administrator",
"parent_page": "",
"public": 1,

View File

@@ -20,14 +20,6 @@ class TestCustomUser(unittest.TestCase):
user = new_user("Username", "test-without-username@example.com")
self.assertTrue(user.username)
def test_with_illegal_characters(self):
user = new_user("Username$$", "test_with_illegal_characters@example.com")
self.assertEqual(user.username[:8], "username")
def test_with_underscore_at_end(self):
user = new_user("Username___", "test_with_underscore_at_end@example.com")
self.assertNotEqual(user.username[-1], "_")
def test_with_short_first_name(self):
user = new_user("USN", "test_with_short_first_name@example.com")
self.assertGreaterEqual(len(user.username), 4)
@@ -37,8 +29,6 @@ class TestCustomUser(unittest.TestCase):
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

@@ -9,64 +9,29 @@ from frappe.core.doctype.user.user import User
from frappe.utils import cint, escape_html, random_string
from frappe.website.utils import is_signup_disabled
from lms.lms.utils import validate_image
from frappe.website.utils import cleanup_page_name
from frappe.model.naming import append_number_if_name_exists
from lms.widgets import Widgets
class CustomUser(User):
def validate(self):
super().validate()
self.validate_username_characters()
self.validate_username_duplicates()
self.validate_completion()
self.user_image = validate_image(self.user_image)
self.cover_image = validate_image(self.cover_image)
def validate_username_characters(self):
if self.username and len(self.username):
other_conditions = (
self.username[0] == "_" or self.username[-1] == "_" or "-" in self.username
def validate_username_duplicates(self):
while not self.username or self.username_exists():
self.username = append_number_if_name_exists(
self.doctype, cleanup_page_name(self.full_name), fieldname="username"
)
else:
other_conditions = ""
if " " in self.username:
self.username = self.username.replace(" ", "")
regex = re.compile(r"[@!#$%^&*()<>?/\|}{~:-]")
if self.is_new():
if not self.username:
self.username = self.get_username_from_first_name()
if self.username.find(" "):
self.username.replace(" ", "")
if len(self.username) < 4:
self.username = self.email.replace("@", "").replace(".", "")
if regex.search(self.username) or other_conditions:
self.username = self.remove_illegal_characters()
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 regex.search(self.username):
frappe.throw(_("Username can only contain alphabets, numbers and underscore."))
if other_conditions:
if "-" in self.username:
frappe.throw(_("Username cannot contain a Hyphen(-)"))
else:
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(r"[^\w]+", "", self.username).strip("_")
if len(self.username) < 4:
self.username = self.email.replace("@", "").replace(".", "")
def validate_skills(self):
unique_skills = []
@@ -273,7 +238,6 @@ def sign_up(email, full_name, verify_terms, user_category):
def set_country_from_ip(login_manager=None, user=None):
if not user and login_manager:
user = login_manager.user
user_country = frappe.db.get_value("User", user, "country")
# if user_country:
# return
@@ -294,16 +258,14 @@ def get_country_code():
return
def on_login(login_manager):
set_country_from_ip()
def on_session_creation(login_manager):
if frappe.db.get_single_value("System Settings", "setup_complete"):
if frappe.db.get_single_value(
"System Settings", "setup_complete"
) and frappe.db.get_single_value("LMS Settings", "default_home"):
frappe.local.response["home_page"] = "/courses"
@frappe.whitelist(allow_guest=True)
@frappe.whitelist()
def search_users(start=0, text=""):
or_filters = get_or_filters(text)
count = len(get_users(or_filters, 0, 900000000, text))
@@ -350,7 +312,7 @@ def get_user_details(users):
details = frappe.db.get_value(
"User",
user,
["name", "username", "full_name", "user_image", "headline"],
["name", "username", "full_name", "user_image", "headline", "looking_for_job"],
as_dict=True,
)
user_details.append(Widgets().MemberCard(member=details, avatar_class="avatar-large"))

View File

@@ -57,8 +57,8 @@ class ProfilePage(BaseRenderer):
self.renderer = None
def can_render(self):
if "." in self.path:
return False
"""if "." in self.path:
return False"""
# has prefix and path starts with prefix?
prefix = get_profile_url_prefix().lstrip("/")
@@ -67,8 +67,8 @@ class ProfilePage(BaseRenderer):
# not a userpage?
username = self.get_username()
if RE_INVALID_USERNAME.search(username):
return False
""" if RE_INVALID_USERNAME.search(username):
return False """
# if there is prefix then we can allow all usernames
if prefix:
return True

View File

@@ -34,7 +34,7 @@ lms.patches.v0_0.create_course_instructor_role #29-08-2022
lms.patches.v0_0.create_course_moderator_role
lms.patches.v0_0.set_dashboard #11-10-2022
lms.patches.v0_0.set_courses_page_as_home
lms.patches.v0_0.set_member_in_progress #09-11-2022
lms.patches.v0_0.set_member_in_progress #03-03-2023
lms.patches.v0_0.convert_progress_to_float
lms.patches.v0_0.add_pages_to_nav #25-11-2022
lms.patches.v0_0.change_role_names
@@ -44,3 +44,13 @@ lms.patches.v0_0.rename_instructor_role
lms.patches.v0_0.change_course_creation_settings #12-12-2022
lms.patches.v0_0.check_onboarding_status #21-12-2022
lms.patches.v0_0.assignment_file_type
lms.patches.v0_0.user_singles_issue #23-11-2022
lms.patches.v0_0.rename_community_to_users #06-01-2023
lms.patches.v0_0.video_embed_link
lms.patches.v0_0.rename_exercise_doctype
lms.patches.v0_0.add_question_type #09-04-2023
lms.patches.v0_0.add_evaluator_to_assignment #09-04-2023
lms.patches.v0_0.share_certificates
execute:frappe.delete_doc("Web Form", "class", ignore_missing=True, force=True)
lms.patches.v0_0.amend_course_and_lesson_editor_fields
lms.patches.v0_0.convert_course_description_to_html #11-05-2023

View File

@@ -0,0 +1,9 @@
import frappe
def execute():
frappe.reload_doc("lms", "doctype", "lesson_assignment")
assignments = frappe.get_all("Lesson Assignment", fields=["name", "course"])
for assignment in assignments:
evaluator = frappe.db.get_value("LMS Course", assignment.course, "evaluator")
frappe.db.set_value("Lesson Assignment", assignment.name, "evaluator", evaluator)

View File

@@ -0,0 +1,9 @@
import frappe
def execute():
frappe.reload_doc("lms", "doctype", "lms_quiz_question")
questions = frappe.get_all("LMS Quiz Question", pluck="name")
for question in questions:
frappe.db.set_value("LMS Quiz Question", question, "type", "Choices")

View File

@@ -0,0 +1,37 @@
import frappe
from frappe.utils import to_markdown
def execute():
amend_lesson_content()
amend_course_description()
def amend_lesson_content():
lesson_content_field = frappe.db.get_value(
"DocField", {"parent": "Course Lesson", "fieldname": "body"}, "fieldtype"
)
if lesson_content_field == "Text Editor":
lessons = frappe.get_all("Course Lesson", fields=["name", "body"])
for lesson in lessons:
frappe.db.set_value("Course Lesson", lesson.name, "body", to_markdown(lesson.body))
frappe.reload_doc("lms", "doctype", "course_lesson")
def amend_course_description():
course_description_field = frappe.db.get_value(
"DocField", {"parent": "LMS Course", "fieldname": "description"}, "fieldtype"
)
if course_description_field == "Text Editor":
courses = frappe.get_all("LMS Course", fields=["name", "description"])
for course in courses:
frappe.db.set_value(
"LMS Course", course.name, "description", to_markdown(course.description)
)
frappe.reload_doc("lms", "doctype", "lms_course")

View File

@@ -0,0 +1,12 @@
import frappe
from lms.lms.md import markdown_to_html
def execute():
courses = frappe.get_all("LMS Course", fields=["name", "description"])
for course in courses:
html = markdown_to_html(course.description)
frappe.db.set_value("LMS Course", course.name, "description", html)
frappe.reload_doc("lms", "doctype", "lms_course")

View File

@@ -0,0 +1,12 @@
import frappe
from lms.lms.md import markdown_to_html
def execute():
lessons = frappe.get_all("Course Lesson", fields=["name", "body"])
for lesson in lessons:
html = markdown_to_html(lesson.body)
frappe.db.set_value("Course Lesson", lesson.name, "body", html)
frappe.reload_doc("lms", "doctype", "course_lesson")

View File

@@ -1,9 +1,6 @@
from venv import create
import frappe
from lms.install import create_instructor_role
from lms.install import create_course_creator_role
def execute():
create_instructor_role()
create_course_creator_role()

View File

@@ -0,0 +1,7 @@
import frappe
def execute():
doc = frappe.db.exists("Top Bar Item", {"url": "/community"})
if doc:
frappe.db.set_value("Top Bar Item", doc, {"url": "/people", "label": "People"})

View File

@@ -0,0 +1,13 @@
import frappe
from frappe.model.rename_doc import rename_doc
def execute():
if frappe.db.exists("DocType", "LMS Exercise"):
return
frappe.flags.ignore_route_conflict_validation = True
rename_doc("DocType", "Exercise", "LMS Exercise")
frappe.flags.ignore_route_conflict_validation = False
frappe.reload_doctype("LMS Exercise", force=True)

View File

@@ -3,9 +3,12 @@ import frappe
def execute():
frappe.reload_doc("lms", "doctype", "lms_course_progress")
progress_records = frappe.get_all("LMS Course Progress", fields=["name", "owner"])
progress_records = frappe.get_all(
"LMS Course Progress", fields=["name", "owner", "member"]
)
for progress in progress_records:
full_name = frappe.db.get_value("User", progress.owner, "full_name")
frappe.db.set_value("LMS Course Progress", progress.name, "member", progress.owner)
frappe.db.set_value("LMS Course Progress", progress.name, "member_name", full_name)
if not progress.member:
full_name = frappe.db.get_value("User", progress.owner, "full_name")
frappe.db.set_value("LMS Course Progress", progress.name, "member", progress.owner)
frappe.db.set_value("LMS Course Progress", progress.name, "member_name", full_name)

View File

@@ -0,0 +1,25 @@
import frappe
def execute():
certificates = frappe.get_all("LMS Certificate", fields=["member", "name"])
for certificate in certificates:
if not frappe.db.exists(
"DocShare",
{
"share_doctype": "LMS Certificate",
"share_name": certificate.name,
"user": certificate.member,
},
):
share = frappe.get_doc(
{
"doctype": "DocShare",
"user": certificate.member,
"share_doctype": "LMS Certificate",
"share_name": certificate.name,
"read": 1,
}
)
share.save(ignore_permissions=True)

View File

@@ -0,0 +1,10 @@
import frappe
def execute():
table = frappe.qb.DocType("Singles")
q = frappe.qb.from_(table).select(table.field).where(table.doctype == "User")
rows = q.run()
if len(rows):
frappe.db.delete("Singles", {"doctype": "User"})

View File

@@ -0,0 +1,11 @@
import frappe
def execute():
courses = frappe.get_all(
"LMS Course", {"video_link": ["is", "set"]}, ["name", "video_link"]
)
for course in courses:
if course.video_link:
link = course.video_link.split("/")[-1]
frappe.db.set_value("LMS Course", course.name, "video_link", link)

View File

@@ -119,7 +119,7 @@ def quiz_renderer(quiz_name):
def exercise_renderer(argument):
exercise = frappe.get_doc("Exercise", argument)
exercise = frappe.get_doc("LMS Exercise", argument)
context = dict(exercise=exercise)
return frappe.render_template("templates/exercise.html", context)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" id="icon-quiz" viewBox="0 0 1024 1024" stroke="#1F272E">
<path d="M512 0C229.232 0 0 229.232 0 512c0 282.784 229.232 512 512 512 282.784 0 512.017-229.216 512.017-512C1024.017 229.232 794.785 0 512 0zm0 961.008c-247.024 0-448-201.984-448-449.01 0-247.024 200.976-448 448-448s448.017 200.977 448.017 448S759.025 961.009 512 961.009zm-47.056-160.529h80.512v-81.248h-80.512zm46.112-576.944c-46.88 0-85.503 12.64-115.839 37.889-30.336 25.263-45.088 75.855-44.336 117.775l1.184 2.336h73.44c0-25.008 8.336-60.944 25.008-73.84 16.656-12.88 36.848-19.328 60.56-19.328 27.328 0 48.336 7.424 63.073 22.271 14.72 14.848 22.063 36.08 22.063 63.664 0 23.184-5.44 42.976-16.368 59.376-10.96 16.4-29.328 39.841-55.088 70.322-26.576 23.967-42.992 43.231-49.232 57.807-6.256 14.592-9.504 40.768-9.744 78.512h76.96c0-23.68 1.503-41.136 4.496-52.336 2.975-11.184 11.504-23.823 25.568-37.888 30.224-29.152 54.496-57.664 72.88-85.551 18.336-27.857 27.52-58.593 27.52-92.193 0-46.88-14.176-83.408-42.577-109.568-28.416-26.176-68.272-39.248-119.568-39.248z" fill="#1F272E"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -5,9 +5,39 @@
<svg id="icon-video-blue" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 9C1 4.58172 4.58172 1 9 1C11.1217 1 13.1566 1.84286 14.6569 3.34315C16.1571 4.84343 17 6.87827 17 9C17 13.4182 13.4182 17 9 17C4.58172 17 1 13.4182 1 9ZM8.00636 12.0679L11.8766 9.51133C12.0614 9.40191 12.174 9.2084 12.174 9C12.174 8.79161 12.0614 8.59809 11.8766 8.48867L8.00636 5.932C7.79102 5.78453 7.51 5.75869 7.2694 5.86422C7.0288 5.96977 6.86529 6.1906 6.84063 6.44334V11.5567C6.86529 11.8094 7.0288 12.0302 7.2694 12.1358C7.51 12.2413 7.79102 12.2155 8.00636 12.0679Z" fill="#2D95F0"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" id="icon-quiz" viewBox="0 0 1024 1024" stroke="#1F272E">
<path d="M512 0C229.232 0 0 229.232 0 512c0 282.784 229.232 512 512 512 282.784 0 512.017-229.216 512.017-512C1024.017 229.232 794.785 0 512 0zm0 961.008c-247.024 0-448-201.984-448-449.01 0-247.024 200.976-448 448-448s448.017 200.977 448.017 448S759.025 961.009 512 961.009zm-47.056-160.529h80.512v-81.248h-80.512zm46.112-576.944c-46.88 0-85.503 12.64-115.839 37.889-30.336 25.263-45.088 75.855-44.336 117.775l1.184 2.336h73.44c0-25.008 8.336-60.944 25.008-73.84 16.656-12.88 36.848-19.328 60.56-19.328 27.328 0 48.336 7.424 63.073 22.271 14.72 14.848 22.063 36.08 22.063 63.664 0 23.184-5.44 42.976-16.368 59.376-10.96 16.4-29.328 39.841-55.088 70.322-26.576 23.967-42.992 43.231-49.232 57.807-6.256 14.592-9.504 40.768-9.744 78.512h76.96c0-23.68 1.503-41.136 4.496-52.336 2.975-11.184 11.504-23.823 25.568-37.888 30.224-29.152 54.496-57.664 72.88-85.551 18.336-27.857 27.52-58.593 27.52-92.193 0-46.88-14.176-83.408-42.577-109.568-28.416-26.176-68.272-39.248-119.568-39.248z" fill="#1F272E"/>
<svg width="16" height="16" viewBox="0 0 16 16" id="icon-youtube" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_3613_984)">
<mask id="mask0_3613_984" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
<path d="M16 0H0V16H16V0Z" fill="white"/>
</mask>
<g mask="url(#mask0_3613_984)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.77778 12.1111H13.2222C13.7132 12.1111 14.1111 11.7132 14.1111 11.2222V2.77778C14.1111 2.28686 13.7132 1.88889 13.2222 1.88889H2.77778C2.28686 1.88889 1.88889 2.28686 1.88889 2.77778V11.2222C1.88889 11.7132 2.28686 12.1111 2.77778 12.1111ZM13.2222 13C14.2041 13 15 12.2041 15 11.2222V2.77778C15 1.79594 14.2041 1 13.2222 1H2.77778C1.79594 1 1 1.79594 1 2.77778V11.2222C1 12.2041 1.79594 13 2.77778 13H13.2222ZM5.99989 4.76006C5.99989 4.22072 6.60701 3.90461 7.04886 4.21391L10.328 6.50932C10.7072 6.77472 10.7072 7.33622 10.328 7.60163L7.04887 9.89707C6.60701 10.2063 5.99989 9.89022 5.99989 9.35084V4.76006ZM6.88878 5.18688V8.92409L9.55822 7.05548L6.88878 5.18688ZM5 13.5556C4.75454 13.5556 4.55556 13.7546 4.55556 14C4.55556 14.2454 4.75454 14.4444 5 14.4444H11C11.2454 14.4444 11.4444 14.2454 11.4444 14C11.4444 13.7546 11.2454 13.5556 11 13.5556H5Z" fill="#525252"/>
</g>
</g>
<defs>
<clipPath id="clip0_3613_984">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>
<svg width="16" height="16" id="icon-quiz" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_3613_978)">
<mask id="mask0_3613_978" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
<path d="M16 0H0V16H16V0Z" fill="white"/>
</mask>
<g mask="url(#mask0_3613_978)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.1111 8C14.1111 11.3751 11.3751 14.1111 8 14.1111C4.62492 14.1111 1.88889 11.3751 1.88889 8C1.88889 4.62492 4.62492 1.88889 8 1.88889C11.3751 1.88889 14.1111 4.62492 14.1111 8ZM15 8C15 11.866 11.866 15 8 15C4.13401 15 1 11.866 1 8C1 4.13401 4.13401 1 8 1C11.866 1 15 4.13401 15 8ZM7.37988 9.72462V9.79298H8.39062V9.72462C8.39062 9.4512 8.41992 9.23307 8.47852 9.07031C8.53711 8.90427 8.62826 8.76269 8.75196 8.64551C8.87891 8.52832 9.04329 8.4095 9.24516 8.28907C9.56089 8.09375 9.80987 7.85775 9.99218 7.58106C10.1745 7.30111 10.2656 6.96093 10.2656 6.56055C10.2656 6.17644 10.1745 5.83952 9.99218 5.5498C9.81316 5.26009 9.56089 5.03386 9.23538 4.87109C8.90987 4.70834 8.52734 4.62695 8.08789 4.62695C7.69401 4.62695 7.33268 4.70345 7.0039 4.85644C6.67513 5.00619 6.41146 5.22916 6.21289 5.52539C6.01432 5.81836 5.90852 6.17644 5.89551 6.59961H6.96972C6.986 6.34896 7.04948 6.14551 7.16016 5.98926C7.27084 5.82975 7.40756 5.71257 7.57031 5.6377C7.73633 5.56283 7.90885 5.52539 8.08789 5.52539C8.28972 5.52539 8.47364 5.56771 8.63964 5.65235C8.80892 5.73698 8.94071 5.8558 9.0352 6.00879C9.1328 6.16179 9.1816 6.34244 9.1816 6.55078C9.1816 6.81771 9.11004 7.0472 8.96676 7.23926C8.82356 7.42806 8.64453 7.58594 8.42969 7.71289C8.21159 7.84961 8.02278 7.98958 7.86328 8.13281C7.70703 8.27278 7.58659 8.46322 7.50196 8.7041C7.42057 8.94169 7.37988 9.28187 7.37988 9.72462ZM7.37988 11.8682C7.51986 12.0016 7.69076 12.0684 7.89258 12.0684C8.09765 12.0684 8.26855 12.0016 8.40527 11.8682C8.54524 11.7315 8.61524 11.5638 8.61524 11.3652C8.61524 11.1634 8.54524 10.9957 8.40527 10.8623C8.26855 10.7256 8.09765 10.6572 7.89258 10.6572C7.69076 10.6572 7.51986 10.7256 7.37988 10.8623C7.24316 10.9957 7.17481 11.1634 7.17481 11.3652C7.17481 11.5638 7.24316 11.7315 7.37988 11.8682Z" fill="#525252"/>
</g>
</g>
<defs>
<clipPath id="clip0_3613_978">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>
<svg id="icon-quiz-blue" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1511_749)">
<path d="M512 0C229.232 0 0 229.232 0 512C0 794.784 229.232 1024 512 1024C794.784 1024 1024.02 794.784 1024.02 512C1024.02 229.232 794.785 0 512 0ZM512 961.008C264.976 961.008 64 759.024 64 511.998C64 264.974 264.976 63.998 512 63.998C759.024 63.998 960.017 264.975 960.017 511.998C960.017 759.021 759.025 961.009 512 961.009V961.008ZM464.944 800.479H545.456V719.231H464.944V800.479ZM511.056 223.535C464.176 223.535 425.553 236.175 395.217 261.424C364.881 286.687 350.129 337.279 350.881 379.199L352.065 381.535H425.505C425.505 356.527 433.841 320.591 450.513 307.695C467.169 294.815 487.361 288.367 511.073 288.367C538.401 288.367 559.409 295.791 574.146 310.638C588.866 325.486 596.209 346.718 596.209 374.302C596.209 397.486 590.769 417.278 579.841 433.678C568.881 450.078 550.513 473.519 524.753 504C498.177 527.967 481.761 547.231 475.521 561.807C469.265 576.399 466.017 602.575 465.777 640.319H542.737C542.737 616.639 544.24 599.183 547.233 587.983C550.208 576.799 558.737 564.16 572.801 550.095C603.025 520.943 627.297 492.431 645.681 464.544C664.017 436.687 673.201 405.951 673.201 372.351C673.201 325.471 659.025 288.943 630.624 262.783C602.208 236.607 562.352 223.535 511.056 223.535V223.535Z" fill="#2D95F0" stroke="#2D95F0"/>
@@ -70,4 +100,15 @@
<svg id="icon-green-check-circled" width="24" height="24" viewBox="0 0 27 27" 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>
<svg xmlns="http://www.w3.org/2000/svg" id="icon-clock" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#1F272E" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="feather feather-clock">
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
<svg width="20" height="20" viewBox="0 0 20 20" id="icon-success" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 18.75C14.8325 18.75 18.75 14.8325 18.75 10C18.75 5.16751 14.8325 1.25 10 1.25C5.16751 1.25 1.25 5.16751 1.25 10C1.25 14.8325 5.16751 18.75 10 18.75ZM13.966 7.48104C14.1856 7.21471 14.1477 6.8208 13.8813 6.60122C13.615 6.38164 13.2211 6.41954 13.0015 6.68587L8.68984 11.9155L7.01289 9.74823C6.80165 9.47524 6.40911 9.42517 6.13611 9.6364C5.86311 9.84764 5.81304 10.2402 6.02428 10.5132L8.18004 13.2993C8.29633 13.4495 8.47467 13.5388 8.66468 13.5417C8.85468 13.5447 9.0357 13.461 9.15658 13.3144L13.966 7.48104Z" fill="#171717"/>
</svg>
<svg width="16" height="16" id="icon-drag" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 3C4 3.82843 4.67157 4.5 5.5 4.5C6.32843 4.5 7 3.82843 7 3C7 2.17157 6.32843 1.5 5.5 1.5C4.67157 1.5 4 2.17157 4 3ZM5.5 9.5C4.67157 9.5 4 8.82843 4 8C4 7.17157 4.67157 6.5 5.5 6.5C6.32843 6.5 7 7.17157 7 8C7 8.82843 6.32843 9.5 5.5 9.5ZM5.5 14.5C4.67157 14.5 4 13.8284 4 13C4 12.1716 4.67157 11.5 5.5 11.5C6.32843 11.5 7 12.1716 7 13C7 13.8284 6.32843 14.5 5.5 14.5ZM9 3C9 3.82843 9.67157 4.5 10.5 4.5C11.3284 4.5 12 3.82843 12 3C12 2.17157 11.3284 1.5 10.5 1.5C9.67157 1.5 9 2.17157 9 3ZM10.5 9.5C9.67157 9.5 9 8.82843 9 8C9 7.17157 9.67157 6.5 10.5 6.5C11.3284 6.5 12 7.17157 12 8C12 8.82843 11.3284 9.5 10.5 9.5ZM10.5 14.5C9.67157 14.5 9 13.8284 9 13C9 12.1716 9.67157 11.5 10.5 11.5C11.3284 11.5 12 12.1716 12 13C12 13.8284 11.3284 14.5 10.5 14.5Z" fill="#171717"/>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -0,0 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" id="icon-upload" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 14.5C11.5899 14.5 14.5 11.5899 14.5 8C14.5 4.41015 11.5899 1.5 8 1.5C4.41015 1.5 1.5 4.41015 1.5 8C1.5 11.5899
4.41015 14.5 8 14.5Z" stroke="#505A62" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 4.75V11.1351" stroke="#505A62" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.29102 7.45833L7.99935 4.75L10.7077 7.45833" stroke="#505A62" stroke-miterlimit="10" stroke-linecap="round"
stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 637 B

View File

@@ -0,0 +1,10 @@
<svg id="icon-youtube" width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_779_38008)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.125 13.625H14.875C15.4273 13.625 15.875 13.1773 15.875 12.625V3.125C15.875 2.57272 15.4273 2.125 14.875 2.125H3.125C2.57272 2.125 2.125 2.57272 2.125 3.125V12.625C2.125 13.1773 2.57272 13.625 3.125 13.625ZM14.875 14.625C15.9796 14.625 16.875 13.7296 16.875 12.625V3.125C16.875 2.02043 15.9796 1.125 14.875 1.125H3.125C2.02043 1.125 1.125 2.02043 1.125 3.125V12.625C1.125 13.7296 2.02043 14.625 3.125 14.625H14.875ZM6.74988 5.35507C6.74988 4.74831 7.43289 4.39269 7.92997 4.74065L11.619 7.32298C12.0456 7.62156 12.0456 8.25325 11.619 8.55183L7.92998 11.1342C7.43289 11.4821 6.74988 11.1265 6.74988 10.5197V5.35507ZM7.74988 5.83524V10.0396L10.753 7.93741L7.74988 5.83524ZM5.625 15.25C5.34886 15.25 5.125 15.4739 5.125 15.75C5.125 16.0261 5.34886 16.25 5.625 16.25H12.375C12.6511 16.25 12.875 16.0261 12.875 15.75C12.875 15.4739 12.6511 15.25 12.375 15.25H5.625Z" fill="#171717"/>
</g>
<defs>
<clipPath id="clip0_779_38008">
<rect width="18" height="18" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

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