Compare commits

...

438 Commits

Author SHA1 Message Date
Hussain Nagaria
a35638d289 feat: add reply_to in email students 2023-10-17 22:01:04 +05:30
Jannat Patel
a02365c223 Merge pull request #642 from pateljannat/course-permissions
fix: permissions
2023-10-13 19:43:50 +05:30
Jannat Patel
2d589aefa2 Merge pull request #643 from pateljannat/timetable-customisations
feat: timetable customisations
2023-10-13 19:39:15 +05:30
Jannat Patel
bc2dc679a8 fix: revert ci changes 2023-10-13 18:15:49 +05:30
Jannat Patel
f2432d78ee ci: added collation server for mariadb 2023-10-13 16:29:47 +05:30
Jannat Patel
f27eecce1f ci: added collation server for mariadb 2023-10-13 16:17:58 +05:30
Jannat Patel
caf967f2e2 ci: added collation server for mariadb 2023-10-13 16:13:45 +05:30
Jannat Patel
eecc9b53df ci: added collation server for mariadb 2023-10-13 16:07:01 +05:30
Jannat Patel
8e12cae91f ci: added collation server for mariadb 2023-10-13 15:58:07 +05:30
Jannat Patel
12c5ad54e7 ci: added collation server for mariadb 2023-10-13 15:24:12 +05:30
Jannat Patel
c20fa7e093 ci: added collation server for mariadb 2023-10-13 14:50:50 +05:30
Jannat Patel
4c83264c4a ci: added collation server for mariadb 2023-10-13 13:14:35 +05:30
Jannat Patel
f592cf08d8 ci: added collation server for mariadb 2023-10-13 12:47:36 +05:30
Jannat Patel
bf0cb25a88 ci: added collation server for mariadb 2023-10-13 12:33:16 +05:30
Jannat Patel
2ff3d83d8f ci: added collation server for mariadb 2023-10-13 11:52:38 +05:30
Jannat Patel
3f5c3e89c8 ci: added collation server for mariadb 2023-10-13 11:45:42 +05:30
Jannat Patel
a1bb7962bc ci: added collation server for mariadb 2023-10-13 11:40:17 +05:30
Jannat Patel
bf5cc5e1d1 ci: fixed mariadb options 2023-10-13 11:24:58 +05:30
Jannat Patel
d840d2fc18 ci: fixed step in server tests script 2023-10-13 11:19:27 +05:30
Jannat Patel
1e458921e8 ci: fix server tests script 2023-10-13 11:17:41 +05:30
Jannat Patel
55feb41998 feat: timetable customisations 2023-10-13 10:59:44 +05:30
Jannat Patel
a7dbdd844b feat: batch customisations 2023-10-12 21:20:36 +05:30
Jannat Patel
f3d6ad6c84 fix: course permissions 2023-10-11 13:40:07 +05:30
Jannat Patel
a0255e1743 feat: send email to batch students 2023-10-11 12:58:07 +05:30
Jannat Patel
affd2b47bd Merge pull request #640 from pateljannat/registration-email
feat: batch registration confirmation email
2023-10-10 10:16:12 +05:30
Jannat Patel
814870fd69 feat: batch regisration confirmation email 2023-10-09 18:53:50 +05:30
Jannat Patel
50c1a566a8 Merge pull request #639 from pateljannat/summary
feat: assignment as text
2023-10-07 11:08:10 +05:30
Jannat Patel
47783997c6 feat: assignment as text 2023-10-06 15:32:53 +05:30
Jannat Patel
70d8505596 Merge pull request #637 from pateljannat/batch-emails
feat: send email to batch students
2023-10-05 21:59:46 +05:30
Jannat Patel
6e8cf9ca25 fix: category on batch edit 2023-10-05 17:24:04 +05:30
Jannat Patel
c9cda6c6f5 fix: show email button only to moderators 2023-10-05 16:24:11 +05:30
Jannat Patel
6c4d3ea37e feat: send email to batch students 2023-10-05 16:12:02 +05:30
Jannat Patel
8ad0e99b3c Merge pull request #634 from pateljannat/template-days
feat: Days and Duration in timetable template
2023-10-04 13:06:36 +05:30
Jannat Patel
60277ed6e9 fix: onboarding style 2023-10-04 12:12:00 +05:30
Jannat Patel
7b570420ca fix: content collapse 2023-10-04 11:07:01 +05:30
Jannat Patel
9205b59e29 feat: days in timetable template 2023-10-04 10:44:16 +05:30
Jannat Patel
685c5babe5 Merge pull request #633 from pateljannat/pdf-upload-issue
fix: pdf rendering in lessons
2023-10-03 16:24:18 +05:30
Jannat Patel
681dc8fbc1 fix: pdf rendering in lessons 2023-10-03 16:09:11 +05:30
Jannat Patel
7c623b1a8d Merge pull request #632 from pateljannat/audio-in-lesson
feat: audio in lessons
2023-10-03 13:53:30 +05:30
Jannat Patel
277c089adc feat: audio in lessons 2023-10-03 13:33:59 +05:30
Jannat Patel
cfc3c231ff fix: permissions for lms enrollment 2023-10-02 12:55:35 +05:30
Jannat Patel
1000a22490 fix: permissions for lms enrollment 2023-10-02 12:52:36 +05:30
Jannat Patel
65c0ebac50 Merge pull request #629 from pateljannat/student-role
feat: LMS Student Role
2023-09-29 19:22:23 +05:30
Jannat Patel
80843ec44b feat: assign LMS Student role to all signups 2023-09-29 18:59:08 +05:30
Jannat Patel
be23220e01 feat: lms student role 2023-09-29 17:14:00 +05:30
Jannat Patel
c82c10d17e Merge pull request #628 from pateljannat/batch-customisations
feat: batch customisations
2023-09-29 10:59:41 +05:30
Jannat Patel
5918b8be60 feat: batch customisations 2023-09-28 19:11:46 +05:30
Jannat Patel
647a0a8ff1 Merge pull request #626 from pateljannat/ins-notes-changes
feat: editor js for instructor notes
2023-09-28 11:32:00 +05:30
Jannat Patel
bf3c6bc6be test: change lesson sequence 2023-09-28 10:39:30 +05:30
Jannat Patel
cc7832614b fix: hide course header for students if no courses in batch 2023-09-28 10:14:10 +05:30
Jannat Patel
d5387a0d1a test: fix course creation test 2023-09-27 19:57:37 +05:30
Jannat Patel
5d7ad973a2 Merge pull request #627 from pateljannat/batch-meta
feat: batch meta and raw details
2023-09-27 19:46:28 +05:30
Jannat Patel
0fcea692c7 feat: batch meta and raw details 2023-09-27 19:21:57 +05:30
Jannat Patel
db71f1271b feat: editor js for instructor notes 2023-09-27 17:59:36 +05:30
Jannat Patel
3fde923190 Merge pull request #624 from pateljannat/billing-flow-issues
fix: billing flow issues
2023-09-27 09:17:53 +05:30
Jannat Patel
4f97760e8a fix: billing flow issues 2023-09-26 22:40:00 +05:30
Jannat Patel
5614a6203f Merge pull request #622 from pateljannat/issues
fix: sanitized inputs for people and course creation page
2023-09-25 22:49:25 +05:30
Jannat Patel
5727b7cd73 fix: sanitized inputs for people and course creation page 2023-09-25 22:08:37 +05:30
Jannat Patel
1c0644aa7a Merge pull request #619 from pateljannat/state-validation
fix: billing flow
2023-09-25 17:57:50 +05:30
Jannat Patel
23e6ebe8ee fix: multicurrency on course pages 2023-09-25 17:50:36 +05:30
Jannat Patel
5602c0b6c3 Merge branch 'main' of https://github.com/frappe/lms into state-validation 2023-09-25 09:48:09 +05:30
Jannat Patel
8e59c10b90 Merge pull request #610 from pateljannat/ins-notes
fix: instructor notes
2023-09-25 09:39:45 +05:30
Jannat Patel
90587b0508 fix: price update on country 2023-09-22 21:46:08 +05:30
Jannat Patel
153a8428f7 fix: billing flow 2023-09-21 12:52:31 +05:30
Jannat Patel
3d00d96716 Merge pull request #614 from pateljannat/timetable
feat: Batch Timetable
2023-09-20 13:07:09 +05:30
Jannat Patel
fb9824301f fix: removed live class from template row 2023-09-20 13:00:51 +05:30
Jannat Patel
33f4e82399 fix: batch copy 2023-09-20 12:40:04 +05:30
Jannat Patel
0d99269109 feat: live class checkbox 2023-09-20 12:09:02 +05:30
Jannat Patel
8098532215 feat: timetable legends and template 2023-09-18 19:16:57 +05:30
Jannat Patel
24e9f46e2f feat: batch timetable 2023-09-15 21:55:06 +05:30
Jannat Patel
7c3b40f9d5 Merge pull request #613 from pateljannat/change-batch-course-naming
fix: batch course sequence id issue
2023-09-14 12:47:44 +05:30
Jannat Patel
29860583f4 fix: batch course sequence id issue 2023-09-14 12:34:34 +05:30
Jannat Patel
9c00a5561a Merge pull request #600 from pateljannat/paid-class
feat: Batches Revamp
2023-09-13 15:18:55 +05:30
Jannat Patel
82b8853f39 fix: patches 2023-09-13 15:10:52 +05:30
Jannat Patel
c4ab91a565 feat: certified participants page 2023-09-13 13:07:20 +05:30
Jannat Patel
87e5096f5d fix: course card edit and delete button position 2023-09-13 10:39:32 +05:30
Jannat Patel
1a07021bbf fix: quiz list in lesson page 2023-09-12 18:03:56 +05:30
Jannat Patel
6ab4f15d0c feat: publish batches 2023-09-12 15:09:13 +05:30
Jannat Patel
f137f8e048 feat: multicurrency 2023-09-12 12:13:41 +05:30
Jannat Patel
04501143ec feat: apply_gst in batches 2023-09-11 22:44:28 +05:30
Jannat Patel
07276f5c17 fix: renamed class to batch for live classes 2023-09-04 23:18:45 +05:30
Jannat Patel
a9bd01b34e fix: instructor notes 2023-09-01 23:14:58 +05:30
Jannat Patel
93db82305f Merge pull request #607 from pateljannat/lesson-embed
feat: embeds in lesson
2023-08-31 23:38:23 +05:30
Jannat Patel
ce09f27373 fix: assignment renderer 2023-08-31 23:32:46 +05:30
Jannat Patel
ffd9d56896 feat: embed pdf 2023-08-31 23:29:56 +05:30
Jannat Patel
833e714a1f feat: embeds in lesson 2023-08-31 21:33:09 +05:30
Jannat Patel
e402f322f6 Merge pull request #606 from pateljannat/instructor-notes
feat: instructor notes
2023-08-31 12:18:56 +05:30
Jannat Patel
2a0636b32b fix: field descriptions 2023-08-31 11:58:08 +05:30
Jannat Patel
677dc59399 feat: instructor notes 2023-08-31 11:49:51 +05:30
Jannat Patel
db408b21d2 chore: resolve conflicts 2023-08-30 14:38:18 +05:30
Jannat Patel
fe08f4cf09 Merge pull request #605 from pateljannat/assignment-url
feat: Assignment as URL
2023-08-30 13:26:35 +05:30
Jannat Patel
ccb7c1485b fix: validation for URL 2023-08-30 13:10:22 +05:30
Jannat Patel
4a76f42e35 feat: assignment url 2023-08-30 13:01:45 +05:30
Jannat Patel
6f31d50a27 fix: payment fetch failure after payment completion 2023-08-30 11:50:12 +05:30
Jannat Patel
018c26b885 fix: course enrollment 2023-08-29 22:25:40 +05:30
Jannat Patel
f9f70f208f feat: certificate generation dialog 2023-08-29 16:50:14 +05:30
Jannat Patel
b940ddca25 fix: batch ui and ux 2023-08-29 09:55:40 +05:30
Jannat Patel
cd82527ef3 Merge branch 'main' of https://github.com/frappe/lms into paid-class 2023-08-28 11:15:27 +05:30
Jannat Patel
f600f016ae fix: permissions for lms course doctype 2023-08-28 11:14:08 +05:30
Jannat Patel
27101ffd31 fix: renamed class to batch 2023-08-28 10:59:52 +05:30
Jannat Patel
9376b0b010 chore: resolved conflicts 2023-08-26 16:00:44 +05:30
Jannat Patel
e2c1f6aa2d Merge pull request #602 from pateljannat/rename-batch-doctype
refactor: renamed batch and membership doctypes
2023-08-26 15:53:56 +05:30
Jannat Patel
e2287cc73c Merge pull request #586 from niraj2477/fix-quiz-submission
fix: Add question in quiz submission
2023-08-26 15:53:22 +05:30
Jannat Patel
c350ce1b60 fix: replaced batch references 2023-08-26 15:44:19 +05:30
Jannat Patel
09dbe0fed7 fix: replaced instances of batch to batch_old 2023-08-26 15:28:03 +05:30
Jannat Patel
12b3d16662 Merge branch 'main' of https://github.com/frappe/lms into rename-batch-doctype 2023-08-25 18:04:28 +05:30
Jannat Patel
1676329eb2 Merge pull request #572 from tahir-zaqout/fix-instructors
fix: course when instructor is not set
2023-08-25 18:02:59 +05:30
Jannat Patel
86434ea320 fix: removed fields array as there is pluck 2023-08-25 17:45:46 +05:30
Jannat Patel
de6b4f7fb2 test: fix batch fieldname 2023-08-25 17:05:55 +05:30
Jannat Patel
6e488cba3e test: fix enrollment tests 2023-08-25 16:44:04 +05:30
Jannat Patel
03a6cc85fe test: enrollment and exercise test fixed 2023-08-25 15:19:29 +05:30
Jannat Patel
b8b32681bf refactor: renamed batch and membership doctypes 2023-08-25 14:46:11 +05:30
Jannat Patel
7f67c6c6d9 Merge pull request #601 from pateljannat/link-class-to-eval-flow
feat: link class in evaluation flow
2023-08-25 10:08:38 +05:30
Jannat Patel
0487b2c987 chore: resolved conflicts 2023-08-24 22:17:45 +05:30
Jannat Patel
d5f10db250 feat: capture gst information 2023-08-24 22:15:55 +05:30
Jannat Patel
74df0f19cf fix: class filter on certification doctypes 2023-08-23 14:52:56 +05:30
Jannat Patel
47c19b4e3d feat: gst fields in class student 2023-08-23 13:00:54 +05:30
Jannat Patel
b197e36ba7 Merge pull request #599 from pateljannat/filter-mobile
fix: styling of course list menu
2023-08-23 10:43:37 +05:30
Jannat Patel
4b049dcf71 fix: styling of course list menu 2023-08-22 19:16:22 +05:30
Jannat Patel
04ed7f412f feat: class billing 2023-08-22 18:34:42 +05:30
Jannat Patel
ac64e59c43 chore: resolved conflicts 2023-08-19 12:26:21 +05:30
Jannat Patel
d5524a8d67 feat: registration information in class student 2023-08-19 12:24:13 +05:30
Jannat Patel
bc350a2661 Merge pull request #595 from pateljannat/revert-registration
revert: class registration
2023-08-18 19:09:19 +05:30
Jannat Patel
575fb623ba revert: class registration 2023-08-18 18:59:02 +05:30
Jannat Patel
b7783659c9 Merge branch 'main' of https://github.com/frappe/lms 2023-08-18 17:39:02 +05:30
Jannat Patel
bddd4743a7 fix: reload course doctype 2023-08-18 17:38:20 +05:30
Jannat Patel
ce8ac15faa Merge pull request #594 from pateljannat/user-validate-image
fix: dont validate user uploaded files
2023-08-18 15:19:57 +05:30
Jannat Patel
46e7c83fae fix: dont validate user uploaded files 2023-08-18 15:13:34 +05:30
Jannat Patel
3dcb55f603 Merge pull request #592 from pateljannat/course-filter
feat: course filters
2023-08-18 13:05:24 +05:30
Jannat Patel
fb3e0832d6 chore: removed unnecessary print statements 2023-08-18 12:49:43 +05:30
Jannat Patel
982d6c9045 fix: popularity filter for enrolled and authored courses 2023-08-18 12:39:19 +05:30
Jannat Patel
a061a89ee7 fix: paid course details on course card template 2023-08-17 22:06:25 +05:30
Jannat Patel
00bb71f714 chore: resolved conflicts 2023-08-17 21:25:52 +05:30
Jannat Patel
e23cfac5a7 Merge pull request #589 from pateljannat/paid-courses
feat: Paid courses
2023-08-17 14:44:29 +05:30
Jannat Patel
ed651959c1 fix: amount information in membership 2023-08-17 14:20:30 +05:30
Jannat Patel
01a1632a5a feat: course filters 2023-08-16 22:01:59 +05:30
Jannat Patel
c2a6697f72 fix: show price on all course cards 2023-08-16 11:51:05 +05:30
Jannat Patel
211775dba5 fix: formatting 2023-08-16 11:00:06 +05:30
Jannat Patel
2ba85ba6a7 feat: paid course from course creation form 2023-08-14 19:30:33 +05:30
Jannat Patel
3b3f1d692f Merge branch 'main' of https://github.com/frappe/lms into paid-courses 2023-08-14 09:59:46 +05:30
Ankush Menat
d90bb1e5ea fix: escape arguments 2023-08-13 23:37:16 +05:30
Jannat Patel
0c14a1ab4c feat: payment verification and membership 2023-08-11 19:45:12 +05:30
Jannat Patel
ea27acc683 Merge branch 'main' of https://github.com/frappe/lms into paid-courses 2023-08-10 13:33:44 +05:30
14987
b2122e7707 fix: add question in quiz submission 2023-08-10 13:19:33 +05:30
Jannat Patel
9cdc8a50f6 Merge pull request #580 from pateljannat/class-flow
feat: Class flow
2023-08-10 13:14:27 +05:30
Jannat Patel
03620be7bb fix: class schedule date and time validation 2023-08-10 13:00:26 +05:30
Jannat Patel
55296cd9cc fix: show progress on class schedule 2023-08-10 11:51:16 +05:30
Jannat Patel
5fefea1434 Merge branch 'main' of https://github.com/frappe/lms into class-flow 2023-08-09 17:22:44 +05:30
Jannat Patel
66dbe68a15 Merge pull request #583 from pateljannat/certificate-share
fix: Certificate share
2023-08-09 17:10:14 +05:30
Jannat Patel
066e2ddc69 fix: print format value 2023-08-09 17:03:10 +05:30
Jannat Patel
59f08ad4da fix: raise error if certificate is not found in any condition 2023-08-09 16:57:25 +05:30
Jannat Patel
551936e7c4 fix: show logo on certificate only if its present 2023-08-09 16:29:36 +05:30
Jannat Patel
4660240395 fix: certificate share 2023-08-09 14:29:51 +05:30
Jannat Patel
5d0a50242e fix: progress marking for normal content 2023-08-09 12:37:56 +05:30
Jannat Patel
141a778c9a chore: resolved conflicts 2023-08-09 11:49:00 +05:30
Jannat Patel
d83d6cf2d8 Merge pull request #559 from pateljannat/class-revamp
feat: discussions and class dashboard
2023-08-09 11:18:44 +05:30
Jannat Patel
71dc32098e fix: removed unnecessary code 2023-08-09 11:09:17 +05:30
Jannat Patel
6e47b4a941 test: fix discussions test 2023-08-09 10:53:52 +05:30
Jannat Patel
8479e90aeb fix: allow evaluators to access the discussions section 2023-08-09 10:16:48 +05:30
Jannat Patel
24276b779d fix: progress access by students 2023-08-09 10:13:02 +05:30
Jannat Patel
47e254ed9b fix: flow UI and quiz progress 2023-08-07 21:17:23 +05:30
Jannat Patel
9c021ef3b1 feat: razorpay order creation and checkout redirection 2023-08-04 19:37:08 +05:30
Jannat Patel
c284e95dc8 feat: validate razorpay data 2023-08-03 11:46:59 +05:30
Jannat Patel
39663a872c Merge branch 'main' of https://github.com/frappe/lms into paid-courses 2023-08-02 18:50:57 +05:30
Jannat Patel
14cefca735 Merge pull request #579 from pateljannat/fix-gha-dependencies
ci: remove mysql
2023-08-02 18:27:49 +05:30
Jannat Patel
55c56207c2 ci: remove mysql 2023-08-02 18:19:45 +05:30
Jannat Patel
79d9f31db7 feat: paid courses 2023-08-02 18:08:42 +05:30
Jannat Patel
845b906851 feat: redirection from class flow 2023-08-01 18:02:02 +05:30
Jannat Patel
5d2b19cc43 feat: learning flow in class 2023-08-01 10:10:06 +05:30
Jannat Patel
a5bc30f776 Merge pull request #576 from pateljannat/evaluation-duplication
fix: eval request duplicate conditions
2023-07-31 14:49:44 +05:30
Jannat Patel
cce77a475a chore: removed print statement 2023-07-31 14:42:35 +05:30
Jannat Patel
13a26321f5 fix: eval request duplicate conditions 2023-07-31 13:25:12 +05:30
Jannat Patel
e7a2eb7373 feat: student dashboard 2023-07-31 12:36:13 +05:30
tahirAnvil
1cc168404a fix: get_instructors function 2023-07-27 12:07:11 +03:00
Jannat Patel
cef3d21ab3 Merge branch 'main' of https://github.com/frappe/lms into class-revamp 2023-07-26 12:27:16 +05:30
Jannat Patel
d0ac0e4523 Merge pull request #571 from pateljannat/fix-escription
fix: course card descriptions
2023-07-26 12:24:34 +05:30
Jannat Patel
abaa7754a6 fix: adding dependency mariadb-client-core-10.6 for UI Test 2023-07-26 12:17:32 +05:30
Jannat Patel
02e875cbdc Merge branch 'main' of https://github.com/frappe/lms into fix-escription 2023-07-26 11:44:24 +05:30
Jannat Patel
a218257952 fix: course card descriptions 2023-07-26 11:09:35 +05:30
Jannat Patel
7dfcabde5e chore: resolved conflicts 2023-07-26 11:00:27 +05:30
Jannat Patel
6573602dfc Merge pull request #570 from pateljannat/remove-dependencies
chore: remove dev dependencies
2023-07-25 19:27:19 +05:30
Jannat Patel
3c374f48b3 ci: manually installing cypress 2023-07-25 19:20:55 +05:30
Jannat Patel
2412ef0260 fix: dependencies 2023-07-25 18:48:31 +05:30
Jannat Patel
d4dcfcdbc6 chore: remove dev dependencies 2023-07-25 18:06:25 +05:30
Jannat Patel
60aec7c801 Merge pull request #569 from pateljannat/fix-telemetry
fix: telemetry
2023-07-25 17:29:36 +05:30
Jannat Patel
1862d726ad fix: telemetry 2023-07-25 17:15:56 +05:30
Jannat Patel
3d27d5f755 fix: upcoming evals query 2023-07-25 10:50:56 +05:30
Jannat Patel
0b3f76590f Merge branch 'main' of https://github.com/frappe/lms into class-revamp 2023-07-24 18:25:58 +05:30
Jannat Patel
294832834c fix: ignore permissions while eval creations 2023-07-24 18:25:36 +05:30
Jannat Patel
e3338e0236 Merge pull request #562 from pateljannat/class-evaluator
Class evaluator
2023-07-24 18:23:19 +05:30
Jannat Patel
5c7ae55775 Merge branch 'main' of https://github.com/frappe/lms into class-evaluator 2023-07-24 17:14:46 +05:30
Jannat Patel
bc8827547e fix: scheduled the eval event creation 2023-07-24 17:10:03 +05:30
Jannat Patel
7990675c5c fix: evaluator in evals and link field descriptions 2023-07-21 12:46:33 +05:30
Jannat Patel
0182db8030 fix: show upcoming evals in progress page 2023-07-20 20:16:13 +05:30
Jannat Patel
295feccb49 fix: check booked slots against both day and time 2023-07-17 14:28:25 +05:30
Jannat Patel
b5005f41fe feat: schedule evaluations 2023-07-14 20:56:21 +05:30
Jannat Patel
37f06a8ba4 Merge branch 'main' of https://github.com/frappe/lms into class-evaluator 2023-07-14 10:34:22 +05:30
Jannat Patel
71d421898d Merge pull request #561 from pateljannat/fix-docker
fix: use node 18 for docker
2023-07-13 17:16:05 +05:30
Jannat Patel
bf5a69cee4 use node 18 for docker 2023-07-13 17:08:05 +05:30
Jannat Patel
11e6b8a372 feat: class evaluators 2023-07-13 15:10:53 +05:30
Jannat Patel
d763dba204 discussions in class 2023-07-11 19:29:30 +05:30
Jannat Patel
9e5cd84214 Merge pull request #558 from pateljannat/quiz-with-no-answer-check
feat: show and hide quiz answers
2023-07-10 15:03:56 +05:30
Jannat Patel
14cf0c9ae1 test: fix flaky course creation test 2023-07-10 14:56:36 +05:30
Jannat Patel
7d410e9ec8 fix: removed print statement 2023-07-10 14:30:05 +05:30
Jannat Patel
07c3d423aa Merge branch 'main' of https://github.com/frappe/lms into quiz-with-no-answer-check 2023-07-10 12:33:25 +05:30
Jannat Patel
5d8003549f Merge pull request #556 from pateljannat/minor-ux
fix: UX Improvements
2023-07-10 12:01:13 +05:30
Jannat Patel
b286daad16 feat: show and hide quiz answers 2023-07-04 19:42:12 +05:30
Jannat Patel
caa9144a12 fix: progress page width 2023-07-03 14:58:12 +05:30
Jannat Patel
3606902753 fix: ux improvements 2023-06-28 16:55:43 +05:30
Jannat Patel
1abb75a58e fix: certificate duplication validation 2023-06-27 14:37:53 +05:30
Jannat Patel
d35c15c384 Merge pull request #554 from pateljannat/class-ui
fix: class improvements
2023-06-27 14:35:56 +05:30
Jannat Patel
1888209027 test: upgrade node version 2023-06-27 12:51:31 +05:30
Jannat Patel
f3830bfdd5 fix: assessment validation 2023-06-27 12:17:00 +05:30
Jannat Patel
0e1b91f1ec fix: validate duplication 2023-06-26 21:16:27 +05:30
Jannat Patel
8353aa24f3 feat: description in class card 2023-06-26 13:00:45 +05:30
Jannat Patel
99f1a8dfc3 fix: class improvements 2023-06-23 20:16:48 +05:30
Jannat Patel
3d8237008f Merge pull request #551 from pateljannat/quiz-in-classes
feat: quiz in classes
2023-06-22 11:51:13 +05:30
Jannat Patel
22199da7d4 fix: linters 2023-06-22 11:25:32 +05:30
Jannat Patel
d8e11f69cc fix: quiz creation url 2023-06-22 11:14:15 +05:30
Jannat Patel
c9a5c0801e fix: redirect after quiz submission 2023-06-22 10:53:11 +05:30
Jannat Patel
bb0abe27cd fix: redirect after quiz submission 2023-06-22 10:52:57 +05:30
Jannat Patel
7d18e1d928 fix: quiz max attempts 2023-06-21 20:11:30 +05:30
Jannat Patel
da72513f6a feat: quiz in classes 2023-06-20 20:12:10 +05:30
Jannat Patel
6f8c161e03 Merge pull request #549 from pateljannat/profile-form
fix: profile web form
2023-06-19 11:25:36 +05:30
Jannat Patel
ac1f02971f fix: profile web form 2023-06-19 10:55:10 +05:30
Jannat Patel
d19538abd2 Merge pull request #545 from pateljannat/quiz-enhancements-2
fix: quiz list in dialog
2023-06-16 14:27:30 +05:30
Jannat Patel
b1ab7e7783 fix: removed unnecesary comments 2023-06-16 13:44:55 +05:30
Jannat Patel
35c080fcc2 fix: quiz dialog display 2023-06-16 13:08:49 +05:30
Jannat Patel
43e89d9dc2 Merge branch 'main' of https://github.com/frappe/lms into quiz-enhancements-2 2023-06-16 10:12:02 +05:30
Jannat Patel
547b69dd31 Merge pull request #544 from pateljannat/class-medium-and-category
feat: class medium and category
2023-06-15 17:28:24 +05:30
Jannat Patel
1ff8514b22 feat: class medium and category 2023-06-15 17:07:28 +05:30
Jannat Patel
5fffe51c4e fix: quiz in lessons 2023-06-15 11:18:17 +05:30
Jannat Patel
af9aa3e37b fix: verison and readme 2023-06-14 15:03:18 +05:30
Jannat Patel
d644ee7ccd Merge branch 'main' of https://github.com/frappe/lms 2023-06-14 14:39:49 +05:30
Jannat Patel
08e3278ca0 feat: track visit on lesson page 2023-06-14 14:39:14 +05:30
Jannat Patel
85feaa00fc Merge pull request #540 from pateljannat/quiz-modal
fix: quiz enhancements
2023-06-14 10:38:14 +05:30
Jannat Patel
76ba5c188e fix: arguements for set_single_value 2023-06-13 20:40:48 +05:30
Jannat Patel
9941e0e936 fix: linters and tests 2023-06-13 20:21:02 +05:30
Jannat Patel
89206f94f0 fix: show only first line in questions table 2023-06-13 19:39:00 +05:30
Jannat Patel
43128d7ea3 chore: resolved conflicts 2023-06-13 18:58:44 +05:30
Jannat Patel
6f1026434d fix: quiz enhancements 2023-06-13 18:44:37 +05:30
Jannat Patel
db35e3e425 Merge pull request #539 from pateljannat/quiz-enhancements
feat: quiz option as small text
2023-06-12 10:27:07 +05:30
Jannat Patel
1d8de792a5 feat: quiz option as small text 2023-06-09 18:09:54 +05:30
Jannat Patel
0db47dfee1 Merge pull request #537 from pateljannat/general
fix: misc issues
2023-06-08 11:19:58 +05:30
Jannat Patel
fc086fdbc3 fix: misc issues 2023-06-08 11:02:42 +05:30
Jannat Patel
8a86d19b79 Merge pull request #535 from pateljannat/course-publish-issue
fix: show course settings only to moderators
2023-06-07 16:56:47 +05:30
Jannat Patel
9198302f7e fix: show course settings only to moderators 2023-06-07 16:53:46 +05:30
Jannat Patel
d076451ea8 Merge pull request #534 from pateljannat/onboarding-redirects
fix: Onboarding redirects
2023-06-07 16:02:48 +05:30
Jannat Patel
c2e9ef59d6 fix: onboarding conditions 2023-06-07 15:43:44 +05:30
Jannat Patel
100f72de9d Merge branch 'main' of https://github.com/frappe/lms into onboarding-redirects 2023-06-07 15:06:14 +05:30
Jannat Patel
5a32109d5d docs: updated FC link in readme 2023-06-06 22:20:27 +05:30
Jannat Patel
f5e7934906 docs: fixed FC signup button 2023-06-06 22:09:41 +05:30
Jannat Patel
9800c24939 docs: FC signup button on readme 2023-06-06 22:08:24 +05:30
Jannat Patel
dcd81e0a3f fix: onboarding links 2023-06-06 22:06:01 +05:30
Jannat Patel
6574b55440 docs: fix logo in readme 2023-06-06 12:57:06 +05:30
Jannat Patel
8474d1c8c4 docs: PH embed on readme 2023-06-06 12:55:32 +05:30
Jannat Patel
38ae9ab9f5 Merge pull request #532 from pateljannat/video-with-spaces
fix: embed video with spaces in name
2023-06-06 09:59:03 +05:30
Jannat Patel
659b35f03e fix: embed video with spaces in name 2023-06-05 16:17:43 +05:30
Jannat Patel
414f126f1e Merge pull request #529 from pateljannat/lesson-assignment-fixes
fix: assignment submission in lesson
2023-06-05 10:59:07 +05:30
Jannat Patel
b336d769c8 fix: assignment submission in lesson 2023-06-05 10:15:40 +05:30
Jannat Patel
c173953c6a Merge pull request #520 from pateljannat/assignments-in-classes
feat: assignments in class
2023-06-02 15:56:49 +05:30
Jannat Patel
afac45e65f fix: assignment evaluator 2023-06-02 15:37:02 +05:30
Jannat Patel
f0aa5b8744 fix: new assessment doc creation 2023-06-02 14:33:55 +05:30
Jannat Patel
a101e7b089 fix: class pages 2023-06-02 13:53:09 +05:30
Jannat Patel
85903d5385 feat: my class tab 2023-06-01 22:44:32 +05:30
Jannat Patel
fe80ef9b85 fix: progress view 2023-06-01 14:29:15 +05:30
Jannat Patel
961f8c1627 Merge branch 'main' of https://github.com/frappe/lms into assignments-in-classes 2023-06-01 12:56:54 +05:30
Jannat Patel
6c7fc9b317 Merge pull request #522 from pateljannat/quiz-answer-fix
fix: possible answer check
2023-05-31 11:30:02 +05:30
Jannat Patel
2afa14d68e fix: possible answer check 2023-05-31 11:13:39 +05:30
Jannat Patel
f6cdac4826 feat: assessment in progress 2023-05-31 10:35:12 +05:30
Jannat Patel
bb39999b84 feat: assessment tab in class 2023-05-30 22:11:14 +05:30
Jannat Patel
70a036e5a7 fix: remove lesson assignment references 2023-05-26 17:20:53 +05:30
Jannat Patel
0432751050 feat: lesson assignment renamed to lms assignment submission 2023-05-25 22:44:56 +05:30
Jannat Patel
36b3b1d086 Update README.md 2023-05-24 22:46:21 +05:30
Jannat Patel
6a783e540b docs: updated readme logo 2023-05-24 22:39:56 +05:30
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
310 changed files with 13838 additions and 4501 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

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

@@ -0,0 +1,14 @@
#!/bin/bash
set -e
echo "Setting Up System Dependencies..."
sudo apt update
sudo apt remove mysql-server mysql-client
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
}

32
.github/try-on-f-cloud.svg vendored Normal file
View File

@@ -0,0 +1,32 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="4 2 193 52">
<g filter="url(#filter0_dd)">
<rect x="4" y="2" width="193" height="52" rx="6" fill="#2490EF"/>
<path d="M28 22.2891H32.8786V35.5H36.2088V22.2891H41.0874V19.5H28V22.2891Z" fill="white"/>
<path d="M41.6982 35.5H45.0129V28.7109C45.0129 27.2344 46.0866 26.2188 47.5494 26.2188C48.0085 26.2188 48.6388 26.2969 48.95 26.3984V23.4453C48.6543 23.375 48.2419 23.3281 47.9074 23.3281C46.5691 23.3281 45.472 24.1094 45.0362 25.5938H44.9117V23.5H41.6982V35.5Z" fill="white"/>
<path d="M52.8331 40C55.2996 40 56.6068 38.7344 57.2837 36.7969L61.9289 23.5156L58.4197 23.5L55.9221 32.3125H55.7976L53.3233 23.5H49.8374L54.1247 35.8437L53.9302 36.3516C53.4944 37.4766 52.6619 37.5312 51.4947 37.1719L50.7478 39.6562C51.2224 39.8594 51.9927 40 52.8331 40Z" fill="white"/>
<path d="M73.6142 35.7344C77.2401 35.7344 79.4966 33.2422 79.4966 29.5469C79.4966 25.8281 77.2401 23.3438 73.6142 23.3438C69.9883 23.3438 67.7319 25.8281 67.7319 29.5469C67.7319 33.2422 69.9883 35.7344 73.6142 35.7344ZM73.6298 33.1562C71.9569 33.1562 71.101 31.6171 71.101 29.5233C71.101 27.4296 71.9569 25.8827 73.6298 25.8827C75.2715 25.8827 76.1274 27.4296 76.1274 29.5233C76.1274 31.6171 75.2715 33.1562 73.6298 33.1562Z" fill="white"/>
<path d="M84.7253 28.5625C84.7331 27.0156 85.6512 26.1094 86.9895 26.1094C88.3201 26.1094 89.1215 26.9844 89.1137 28.4531V35.5H92.4284V27.8594C92.4284 25.0625 90.7945 23.3438 88.3046 23.3438C86.5306 23.3438 85.2466 24.2187 84.7097 25.6172H84.5697V23.5H81.4106V35.5H84.7253V28.5625Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M102.429 19.5H113.429V22.3141H102.429V19.5ZM102.429 35.5V26.6794H112.699V29.4982H105.94V35.5H102.429Z" fill="white"/>
<path d="M131.584 24.9625C131.09 21.5057 128.345 19.5 124.785 19.5C120.589 19.5 117.429 22.463 117.429 27.4924C117.429 32.5142 120.55 35.4848 124.785 35.4848C128.604 35.4848 131.137 33.0916 131.584 30.1211L128.651 30.1059C128.282 31.9293 126.745 32.9549 124.824 32.9549C122.22 32.9549 120.354 31.0632 120.354 27.4924C120.354 23.9824 122.204 22.0299 124.832 22.0299C126.784 22.0299 128.314 23.1011 128.651 24.9625H131.584Z" fill="white"/>
<path d="M136.409 19.7124H133.571V35.2718H136.409V19.7124Z" fill="white"/>
<path d="M144.031 35.5001C147.56 35.5001 149.803 33.0917 149.803 29.483C149.803 25.8667 147.56 23.4507 144.031 23.4507C140.502 23.4507 138.259 25.8667 138.259 29.483C138.259 33.0917 140.502 35.5001 144.031 35.5001ZM144.047 33.2969C142.094 33.2969 141.137 31.6103 141.137 29.4754C141.137 27.3406 142.094 25.6312 144.047 25.6312C145.968 25.6312 146.925 27.3406 146.925 29.4754C146.925 31.6103 145.968 33.2969 144.047 33.2969Z" fill="white"/>
<path d="M159.338 30.3641C159.338 32.1419 158.028 33.0232 156.773 33.0232C155.409 33.0232 154.499 32.0887 154.499 30.6072V23.6025H151.66V31.0327C151.66 33.8361 153.307 35.4239 155.675 35.4239C157.479 35.4239 158.749 34.5046 159.298 33.1979H159.424V35.272H162.176V23.6025H159.338V30.3641Z" fill="white"/>
<path d="M169.014 35.4769C171.084 35.4769 172.017 34.2841 172.464 33.4332H172.637V35.2718H175.429V19.7124H172.582V25.532H172.464C172.033 24.6887 171.147 23.4503 169.022 23.4503C166.238 23.4503 164.05 25.5624 164.05 29.4522C164.05 33.2965 166.175 35.4769 169.014 35.4769ZM169.806 33.2205C167.931 33.2205 166.943 31.6251 166.943 29.437C166.943 27.2642 167.916 25.7067 169.806 25.7067C171.633 25.7067 172.637 27.173 172.637 29.437C172.637 31.701 171.617 33.2205 169.806 33.2205Z" fill="white"/>
</g>
<defs>
<filter id="filter0_dd" x="0" y="0" width="201" height="60" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="0.25"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="2"/>
<feGaussianBlur stdDeviation="2"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.13 0"/>
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -1,4 +1,4 @@
name: Run tests
name: Server Tests
on:
push:
branches:
@@ -36,7 +36,7 @@ jobs:
- name: setup node
uses: actions/setup-node@v2
with:
node-version: '14'
node-version: '18'
check-latest: true
- name: setup cache for bench
uses: actions/cache@v2
@@ -77,5 +77,4 @@ jobs:
run: bench --site frappe.local build
- name: run tests
working-directory: /home/runner/frappe-bench
run: bench --site frappe.local run-tests --app lms
run: bench --site frappe.local run-tests --app lms

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

@@ -0,0 +1,121 @@
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: 18
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: cypress pre-requisites
run: |
cd ~/frappe-bench/apps/lms
yarn add cypress@^10 --no-lockfile
- 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

@@ -1,46 +1,64 @@
<p align="center">
<a href="https://www.frappelms.com/">
<img src="https://www.frappelms.com/files/flms.svg" alt="Frappe LMS" width="100" height="100">
<img src="https://frappelms.com/files/lms-logo-medium.png" alt="Frappe LMS" width="120px" height="25px">
</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>
&nbsp;
<p align="center">
<a href="https://www.producthunt.com/posts/frappe-lms?utm_source=badge-top-post-topic-badge&utm_medium=badge&utm_souce=badge-frappe&#0045;lms" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-topic-badge.svg?post_id=396079&theme=dark&period=weekly&topic_id=204" alt="Frappe&#0032;LMS - Easy&#0032;to&#0032;use&#0044;&#0032;100&#0037;&#0032;open&#0032;source&#0032;learning&#0032;management&#0032;system | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
</p>
<div align="center" style="max-height: 40px;">
<a href="https://frappecloud.com/lms/signup">
<img src=".github/try-on-f-cloud.svg" height="40">
</a>
</div>
&nbsp;
<p align="center">
<a href="https://dashboard.cypress.io/projects/vandxn/runs">
<img alt="cypress" src="https://img.shields.io/endpoint?url=https://dashboard.cypress.io/badge/simple/vandxn/main&style=flat&logo=cypress">
</a>
<a href="https://github.com/frappe/lms/blob/main/LICENSE">
<img alt="license" src="https://img.shields.io/badge/license-AGPLv3-blue">
</a>
</p>
<img width="1402" alt="Lesson" src="https://frappelms.com/files/fs-banner71f330.png">
<img width="1402" alt="Lesson" src="https://frappelms.com/files/banner.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 🏛
- Create batches 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 +66,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 +90,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://dd1:8000",
},
});

View File

@@ -0,0 +1,133 @@
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.wait(500);
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(".collapse-section.collapsed:first").click();
cy.get("#lesson-content .ce-block")
.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. {enter}"
);
cy.get("#lesson-content .ce-toolbar__plus").click();
cy.get('#lesson-content [data-item-name="youtube"]').click();
cy.get('input[data-fieldname="youtube"]').type("GoDtyItReto");
cy.button("Insert").click();
cy.wait(1000);
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(".discussion-modal").should("be.visible");
// Enter title
cy.get(".modal .topic-title")
.type("Discussion from tests")
.should("have.value", "Discussion from tests");
// Enter comment
cy.get(".modal .discussions-comment").type(
"This is a discussion from the cypress ui tests."
);
// Submit
cy.get(".modal .submit-discussion").click();
cy.wait(2000);
// Check if discussion is added to page and content is visible
cy.get(".sidebar-parent:first .discussion-topic-title").should(
"have.text",
"Discussion from tests"
);
cy.get(".sidebar-parent:first .discussion-topic-title").click();
cy.get(".discussion-on-page:visible").should("have.class", "show");
cy.get(
".discussion-on-page:visible .reply-card .reply-text .ql-editor p"
).should(
"have.text",
"This is a discussion from the cypress ui tests."
);
cy.get(".discussion-form:visible .discussions-comment").type(
"This is a discussion from the cypress ui tests. \n\nThis comment was entered through the commentbox on the page."
);
cy.get(".discussion-form:visible .submit-discussion").click();
cy.wait(3000);
cy.get(".discussion-on-page:visible").should("have.class", "show");
cy.get(".discussion-on-page:visible")
.children(".reply-card")
.eq(1)
.find(".reply-text")
.should(
"have.text",
"This is a discussion from the cypress ui tests. This comment was entered through the commentbox on the page.\n"
);
});
});

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

@@ -8,6 +8,8 @@ else
echo "Creating new bench..."
fi
export PATH="${NVM_DIR}/versions/node/v${NODE_VERSION_DEVELOP}/bin/:${PATH}"
bench init --skip-redis-config-generation frappe-bench
cd frappe-bench
@@ -36,4 +38,4 @@ bench --site lms.localhost clear-cache
bench --site lms.localhost set-config mute_emails 1
bench use lms.localhost
bench start
bench start

View File

@@ -1 +1 @@
__version__ = "0.0.1"
__version__ = "1.0.0"

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"
@@ -103,11 +103,11 @@ doc_events = {
# Scheduled Tasks
# ---------------
# scheduler_events = {
# "daily": [
# "erpnext.stock.reorder_item.reorder_item"
# ]
# }
scheduler_events = {
"hourly": [
"lms.lms.doctype.lms_certificate_request.lms_certificate_request.schedule_evals"
]
}
fixtures = ["Custom Field", "Function", "Industry"]
@@ -138,15 +138,21 @@ 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"},
{"from_route": "/batches/<batchname>", "to_route": "batches/batch"},
{"from_route": "/courses/<course>/progress", "to_route": "batch/progress"},
{"from_route": "/courses/<course>/join", "to_route": "batch/join"},
{"from_route": "/courses/<course>/manage", "to_route": "cohorts"},
@@ -170,15 +176,36 @@ website_route_rules = [
{"from_route": "/users", "to_route": "profiles/profile"},
{"from_route": "/jobs/<job>", "to_route": "jobs/job"},
{
"from_route": "/classes/<classname>/students/<username>",
"to_route": "/classes/progress",
"from_route": "/batches/<batchname>/students/<username>",
"to_route": "/batches/progress",
},
{"from_route": "/assignments/<assignment>", "to_route": "assignments/assignment"},
{
"from_route": "/assignment-submission/<assignment>/<submission>",
"to_route": "assignment_submission/assignment_submission",
},
{
"from_route": "/quiz-submission/<quiz>/<submission>",
"to_route": "quiz_submission/quiz_submission",
},
{
"from_route": "/billing/<module>/<modulename>",
"to_route": "billing/billing",
},
{
"from_route": "/batches/details/<batchname>",
"to_route": "batches/batch_details",
},
{
"from_route": "/certified-participants",
"to_route": "certified_participants/certified_participants",
},
]
website_redirects = [
{"source": "/update-profile", "target": "/edit-profile"},
{"source": "/dashboard", "target": "/courses"},
{"source": "/community", "target": "/people"},
]
update_website_context = [
@@ -228,6 +255,9 @@ 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",
"lms.lms.utils.is_onboarding_complete",
"lms.www.utils.is_student",
],
"filters": [],
}
@@ -274,6 +304,9 @@ lms_markdown_macro_renderers = {
"YouTubeVideo": "lms.plugins.youtube_video_renderer",
"Video": "lms.plugins.video_renderer",
"Assignment": "lms.plugins.assignment_renderer",
"Embed": "lms.plugins.embed_renderer",
"Audio": "lms.plugins.audio_renderer",
"PDF": "lms.plugins.pdf_renderer",
}
# page_renderer to manage profile pages
@@ -287,6 +320,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

@@ -9,6 +9,7 @@ def after_install():
def after_sync():
create_lms_roles()
set_default_home()
set_default_certificate_print_format()
add_all_roles_to("Administrator")
@@ -16,7 +17,7 @@ def add_pages_to_nav():
pages = [
{"label": "Explore", "idx": 1},
{"label": "Courses", "url": "/courses", "parent": "Explore", "idx": 2},
{"label": "Classes", "url": "/classes", "parent": "Explore", "idx": 3},
{"label": "Batches", "url": "/batches", "parent": "Explore", "idx": 3},
{"label": "Statistics", "url": "/statistics", "parent": "Explore", "idx": 4},
{"label": "Jobs", "url": "/jobs", "parent": "Explore", "idx": 5},
{"label": "People", "url": "/community", "parent": "Explore", "idx": 6},
@@ -44,17 +45,27 @@ def add_pages_to_nav():
).save()
def after_uninstall():
def before_uninstall():
delete_custom_fields()
delete_lms_roles()
def create_lms_roles():
create_course_creator_role()
create_moderator_role()
create_evaluator_role()
create_lms_student_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")
frappe.db.set_single_value("Portal Settings", "default_portal_home", "/courses")
def create_course_creator_role():
@@ -67,7 +78,7 @@ def create_course_creator_role():
"desk_access": 0,
}
)
role.save(ignore_permissions=True)
role.save()
def create_moderator_role():
@@ -80,7 +91,52 @@ def create_moderator_role():
"desk_access": 0,
}
)
role.save(ignore_permissions=True)
role.save()
def create_evaluator_role():
if not frappe.db.exists("Role", "Class Evaluator"):
role = frappe.new_doc("Role")
role.update(
{
"role_name": "Class Evaluator",
"home_page": "",
"desk_access": 0,
}
)
role.save()
def create_lms_student_role():
if not frappe.db.exists("Role", "LMS Student"):
role = frappe.new_doc("Role")
role.update(
{
"role_name": "LMS Student",
"home_page": "",
"desk_access": 0,
}
)
role.save()
def set_default_certificate_print_format():
filters = {
"doc_type": "LMS Certificate",
"property": "default_print_format",
}
if not frappe.db.exists("Property Setter", filters):
filters.update(
{
"doctype_or_field": "DocType",
"property_type": "Data",
"value": "Certificate",
}
)
doc = frappe.new_doc("Property Setter")
doc.update(filters)
doc.save()
def delete_custom_fields():
@@ -126,4 +182,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

@@ -116,7 +116,7 @@
"index_web_pages_for_search": 1,
"links": [],
"make_attachments_public": 1,
"modified": "2022-09-15 17:22:21.662675",
"modified": "2023-09-29 17:03:30.825021",
"modified_by": "Administrator",
"module": "Job",
"name": "Job Opportunity",
@@ -144,7 +144,7 @@
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"role": "LMS Student",
"select": 1,
"share": 1,
"write": 1

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)
@@ -32,15 +32,15 @@ def submit_solution(exercise, code):
def save_current_lesson(course_name, lesson_name):
"""Saves the current lesson for a student/mentor."""
name = frappe.get_value(
doctype="LMS Batch Membership",
doctype="LMS Enrollment",
filters={"course": course_name, "member": frappe.session.user},
fieldname="name",
)
if not name:
return
doc = frappe.get_doc("LMS Batch Membership", name)
doc = frappe.get_doc("LMS Enrollment", name)
doc.current_lesson = lesson_name
doc.save(ignore_permissions=True)
doc.save()
return {"current_lesson": doc.current_lesson}
@@ -66,7 +66,7 @@ def join_cohort(course, cohort, subgroup, invite_code):
return {"ok": True, "status": "record found"}
else:
doc = frappe.get_doc(data)
doc.insert(ignore_permissions=True)
doc.insert()
return {"ok": True, "status": "record created"}
@@ -82,7 +82,7 @@ def approve_cohort_join_request(join_request):
return {"ok": False, "error": "Permission Deined"}
r.status = "Accepted"
r.save(ignore_permissions=True)
r.save()
return {"ok": True}
@@ -98,7 +98,7 @@ def reject_cohort_join_request(join_request):
return {"ok": False, "error": "Permission Deined"}
r.status = "Rejected"
r.save(ignore_permissions=True)
r.save()
return {"ok": True}
@@ -115,7 +115,7 @@ def undo_reject_cohort_join_request(join_request):
return {"ok": False, "error": "Permission Deined"}
r.status = "Pending"
r.save(ignore_permissions=True)
r.save()
return {"ok": True}

View File

@@ -7,7 +7,7 @@
"custom_options": "{\"type\": \"line\", \"axisOptions\": {\"xIsSeries\": 1}, \"lineOptions\": {\"regionFill\": 1}}",
"docstatus": 0,
"doctype": "Dashboard Chart",
"document_type": "LMS Batch Membership",
"document_type": "LMS Enrollment",
"dynamic_filters_json": "[]",
"filters_json": "[]",
"group_by_type": "Count",
@@ -15,7 +15,7 @@
"is_public": 1,
"is_standard": 1,
"last_synced_on": "2022-10-20 10:46:56.859520",
"modified": "2022-10-20 11:30:26.863008",
"modified": "2022-10-20 11:30:26.863009",
"modified_by": "Administrator",
"module": "LMS",
"name": "Course Enrollments",

View File

@@ -7,7 +7,8 @@
"engine": "InnoDB",
"field_order": [
"course",
"title"
"title",
"evaluator"
],
"fields": [
{
@@ -23,17 +24,24 @@
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title",
"label": "Course Title",
"read_only": 1
},
{
"fieldname": "evaluator",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Evaluator",
"options": "Course Evaluator"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2022-11-11 15:51:45.560864",
"modified": "2023-08-28 10:03:02.960844",
"modified_by": "Administrator",
"module": "LMS",
"name": "Class Course",
"name": "Batch Course",
"naming_rule": "Autoincrement",
"owner": "Administrator",
"permissions": [],

View File

@@ -5,5 +5,5 @@
from frappe.model.document import Document
class ClassCourse(Document):
class BatchCourse(Document):
pass

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2022, Frappe and contributors
// For license information, please see license.txt
frappe.ui.form.on("Class Student", {
frappe.ui.form.on("Batch Student", {
// refresh: function(frm) {
// }
});

View File

@@ -7,7 +7,11 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"student_details_section",
"student",
"payment",
"confirmation_email_sent",
"column_break_oduu",
"student_name",
"username"
],
@@ -34,15 +38,36 @@
"fieldtype": "Data",
"label": "Username",
"read_only": 1
},
{
"fieldname": "student_details_section",
"fieldtype": "Section Break",
"label": "Student Details"
},
{
"fieldname": "column_break_oduu",
"fieldtype": "Column Break"
},
{
"fieldname": "payment",
"fieldtype": "Link",
"label": "Payment",
"options": "LMS Payment"
},
{
"default": "0",
"fieldname": "confirmation_email_sent",
"fieldtype": "Check",
"label": "Confirmation Email Sent"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2022-11-15 11:13:39.410578",
"modified": "2023-10-09 17:09:50.481794",
"modified_by": "Administrator",
"module": "LMS",
"name": "Class Student",
"name": "Batch Student",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",

View File

@@ -5,5 +5,5 @@
from frappe.model.document import Document
class ClassStudent(Document):
class BatchStudent(Document):
pass

View File

@@ -5,5 +5,5 @@
from frappe.tests.utils import FrappeTestCase
class TestLMSClass(FrappeTestCase):
class TestBatchStudent(FrappeTestCase):
pass

View File

@@ -16,7 +16,7 @@ class Cohort(Document):
if include_counts:
mentors = self._get_subgroup_counts("Cohort Mentor")
students = self._get_subgroup_counts("LMS Batch Membership")
students = self._get_subgroup_counts("LMS Enrollment")
join_requests = self._get_subgroup_counts("Cohort Join Request", status="Pending")
for s in subgroups:
s.num_mentors = mentors.get(s.name, 0)
@@ -56,7 +56,7 @@ class Cohort(Document):
return {
"subgroups": self._get_count("Cohort Subgroup"),
"mentors": self._get_count("Cohort Mentor"),
"students": self._get_count("LMS Batch Membership"),
"students": self._get_count("LMS Enrollment"),
"join_requests": self._get_count("Cohort Join Request", status="Pending"),
}

View File

@@ -51,7 +51,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-12-16 15:06:03.985221",
"modified": "2023-09-29 17:08:18.950560",
"modified_by": "Administrator",
"module": "LMS",
"name": "Cohort Join Request",
@@ -68,9 +68,21 @@
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "LMS Student",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -13,7 +13,7 @@ class CohortJoinRequest(Document):
def ensure_student(self):
# case 1 - user is already a member
q = {
"doctype": "LMS Batch Membership",
"doctype": "LMS Enrollment",
"cohort": self.cohort,
"subgroup": self.subgroup,
"member": self.email,
@@ -26,21 +26,21 @@ class CohortJoinRequest(Document):
cohort = frappe.get_doc("Cohort", self.cohort)
q = {
"doctype": "LMS Batch Membership",
"doctype": "LMS Enrollment",
"course": cohort.course,
"member": self.email,
"member_type": "Student",
}
name = frappe.db.exists(q)
if name:
doc = frappe.get_doc("LMS Batch Membership", name)
doc = frappe.get_doc("LMS Enrollment", name)
doc.cohort = self.cohort
doc.subgroup = self.subgroup
doc.save(ignore_permissions=True)
else:
# case 3 - user has not signed up for this course yet
data = {
"doctype": "LMS Batch Membership",
"doctype": "LMS Enrollment",
"course": cohort.course,
"cohort": self.cohort,
"subgroup": self.subgroup,

View File

@@ -23,7 +23,7 @@ class CohortSubgroup(Document):
def has_student(self, email):
"""Check if given user is a student of this subgroup."""
q = {"doctype": "LMS Batch Membership", "subgroup": self.name, "member": email}
q = {"doctype": "LMS Enrollment", "subgroup": self.name, "member": email}
return frappe.db.exists(q)
def has_join_request(self, email):
@@ -45,7 +45,7 @@ class CohortSubgroup(Document):
def get_students(self):
emails = frappe.get_all(
"LMS Batch Membership",
"LMS Enrollment",
filters={"subgroup": self.name},
fields=["member"],
pluck="member",

View File

@@ -59,7 +59,7 @@
"link_fieldname": "chapter"
}
],
"modified": "2022-03-14 17:57:00.707416",
"modified": "2023-09-29 17:03:58.013819",
"modified_by": "Administrator",
"module": "LMS",
"name": "Course Chapter",
@@ -86,7 +86,7 @@
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"role": "LMS Student",
"select": 1,
"share": 1,
"write": 1

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

@@ -27,7 +27,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-04-01 15:14:03.300260",
"modified": "2023-07-13 11:30:22.641076",
"modified_by": "Administrator",
"module": "LMS",
"name": "Course Evaluator",
@@ -45,6 +45,30 @@
"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
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Class Evaluator",
"share": 1,
"write": 1
}
],
"sort_field": "modified",

View File

@@ -4,6 +4,7 @@
import frappe
from frappe import _
from frappe.model.document import Document
from lms.lms.utils import get_evaluator
class CourseEvaluator(Document):
@@ -36,21 +37,26 @@ class CourseEvaluator(Document):
@frappe.whitelist()
def get_schedule(course, date):
evaluator = frappe.db.get_value("LMS Course", course, "evaluator")
def get_schedule(course, date, batch=None):
evaluator = get_evaluator(course, batch)
all_slots = frappe.get_all(
"Evaluator Schedule",
filters={"parent": evaluator},
fields=["day", "start_time", "end_time"],
order_by="start_time",
)
booked_slots = frappe.get_all(
"LMS Certificate Request",
filters={"evaluator": evaluator, "date": date},
fields=["start_time"],
fields=["start_time", "day"],
)
for slot in booked_slots:
same_slot = list(filter(lambda x: x.start_time == slot.start_time, all_slots))
same_slot = list(
filter(lambda x: x.start_time == slot.start_time and x.day == slot.day, all_slots)
)
if len(same_slot):
all_slots.remove(same_slot[0])

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

@@ -24,6 +24,7 @@
"file_type",
"section_break_11",
"body",
"instructor_notes",
"help_section",
"help"
],
@@ -131,11 +132,16 @@
{
"fieldname": "column_break_15",
"fieldtype": "Column Break"
},
{
"fieldname": "instructor_notes",
"fieldtype": "Markdown Editor",
"label": "Instructor Notes"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-12-28 16:01:42.191123",
"modified": "2023-09-29 17:04:19.252897",
"modified_by": "Administrator",
"module": "LMS",
"name": "Course Lesson",
@@ -163,7 +169,7 @@
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"role": "LMS Student",
"select": 1,
"share": 1,
"write": 1

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(
@@ -87,19 +89,24 @@ class CourseLesson(Document):
@frappe.whitelist()
def save_progress(lesson, course, status):
membership = frappe.db.exists(
"LMS Batch Membership", {"member": frappe.session.user, "course": course}
"LMS Enrollment", {"member": frappe.session.user, "course": course}
)
if not membership:
return
return 0
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},
)
body = frappe.db.get_value("Course Lesson", lesson, "body")
macros = find_macros(body)
quizzes = [value for name, value in macros if name == "Quiz"]
for quiz in quizzes:
if not frappe.db.exists(
"LMS Quiz Submission", {"quiz": quiz, "owner": frappe.session.user}
):
return 0
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,11 +115,12 @@ def save_progress(lesson, course, status):
"doctype": "LMS Course Progress",
"lesson": lesson,
"status": status,
"member": frappe.session.user,
}
).save(ignore_permissions=True)
progress = get_course_progress(course)
frappe.db.set_value("LMS Batch Membership", membership, "progress", progress)
frappe.db.set_value("LMS Enrollment", membership, "progress", progress)
return progress

View File

@@ -7,7 +7,7 @@
"field_order": [
"exercise",
"status",
"batch",
"batch_old",
"column_break_4",
"exercise_title",
"course",
@@ -30,7 +30,7 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Exercise",
"options": "Exercise",
"options": "LMS Exercise",
"search_index": 1
},
{
@@ -40,10 +40,10 @@
"options": "Correct\nIncorrect"
},
{
"fieldname": "batch",
"fieldname": "batch_old",
"fieldtype": "Link",
"label": "Batch",
"options": "LMS Batch"
"label": "Batch Old",
"options": "LMS Batch Old"
},
{
"fieldname": "column_break_4",
@@ -110,7 +110,7 @@
"fieldname": "member",
"fieldtype": "Link",
"label": "Member",
"options": "LMS Batch Membership"
"options": "LMS Enrollment"
},
{
"fetch_from": "member.member",
@@ -141,7 +141,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-12-08 22:58:46.312861",
"modified": "2021-12-08 22:58:46.312863",
"modified_by": "Administrator",
"module": "LMS",
"name": "Exercise Latest Submission",
@@ -163,4 +163,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}
}

View File

@@ -7,7 +7,7 @@
"field_order": [
"exercise",
"status",
"batch",
"batch_old",
"column_break_4",
"exercise_title",
"course",
@@ -25,7 +25,7 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Exercise",
"options": "Exercise"
"options": "LMS Exercise"
},
{
"fetch_from": "exercise.title",
@@ -44,10 +44,10 @@
"read_only": 1
},
{
"fieldname": "batch",
"fieldname": "batch_old",
"fieldtype": "Link",
"label": "Batch",
"options": "LMS Batch"
"label": "Batch Old",
"options": "LMS Batch Old"
},
{
"fetch_from": "exercise.lesson",
@@ -96,12 +96,12 @@
"fieldname": "member",
"fieldtype": "Link",
"label": "Member",
"options": "LMS Batch Membership"
"options": "LMS Enrollment"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-12-08 22:25:05.809376",
"modified": "2021-12-08 22:25:05.809377",
"modified_by": "Administrator",
"module": "LMS",
"name": "Exercise Submission",
@@ -123,4 +123,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}
}

View File

@@ -19,7 +19,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-12-21 09:34:35.018280",
"modified": "2023-09-29 17:04:58.167481",
"modified_by": "Administrator",
"module": "LMS",
"name": "Function",
@@ -44,11 +44,12 @@
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"role": "LMS Student",
"select": 1,
"share": 1
}
],
"sort_field": "modified",
"sort_order": "DESC"
"sort_order": "DESC",
"states": []
}

View File

@@ -19,7 +19,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-12-21 09:35:20.443192",
"modified": "2023-09-29 17:05:27.231982",
"modified_by": "Administrator",
"module": "LMS",
"name": "Industry",
@@ -44,11 +44,12 @@
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"role": "LMS Student",
"select": 1,
"share": 1
}
],
"sort_field": "modified",
"sort_order": "DESC"
"sort_order": "DESC",
"states": []
}

View File

@@ -1,117 +0,0 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2021-12-21 16:15:22.651658",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"assignment",
"lesson",
"course",
"status",
"column_break_3",
"member",
"member_name",
"comments"
],
"fields": [
{
"fieldname": "lesson",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Lesson",
"options": "Course Lesson",
"reqd": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "assignment",
"fieldtype": "Attach",
"label": "Assignment",
"reqd": 1
},
{
"fieldname": "member",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Member",
"options": "User",
"reqd": 1
},
{
"fetch_from": "member.full_name",
"fieldname": "member_name",
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Member Name",
"read_only": 1
},
{
"fetch_from": "lesson.course",
"fieldname": "course",
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Course",
"read_only": 1
},
{
"default": "Not Graded",
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "Pass\nFail\nNot Graded"
},
{
"fieldname": "comments",
"fieldtype": "Small Text",
"label": "Comments"
}
],
"index_web_pages_for_search": 1,
"links": [],
"make_attachments_public": 1,
"modified": "2022-11-16 12:11:59.472025",
"modified_by": "Administrator",
"module": "LMS",
"name": "Lesson Assignment",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [
{
"color": "Green",
"title": "Pass"
},
{
"color": "Orange",
"title": "Not Graded"
},
{
"color": "Red",
"title": "Fail"
}
],
"title_field": "lesson"
}

View File

@@ -1,61 +0,0 @@
# Copyright (c) 2021, Frappe and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
class LessonAssignment(Document):
def validate(self):
self.validate_duplicates()
def validate_duplicates(self):
if frappe.db.exists(
"Lesson Assignment",
{"lesson": self.lesson, "member": self.member, "name": ["!=", self.name]},
):
lesson_title = frappe.db.get_value("Course Lesson", self.lesson, "title")
frappe.throw(
_("Assignment for Lesson {0} by {1} already exists.").format(
lesson_title, self.member_name
)
)
@frappe.whitelist()
def upload_assignment(assignment, lesson):
args = {
"doctype": "Lesson Assignment",
"lesson": lesson,
"member": frappe.session.user,
}
if frappe.db.exists(args):
del args["doctype"]
frappe.db.set_value("Lesson Assignment", args, "assignment", assignment)
else:
args.update({"assignment": assignment})
lesson_work = frappe.get_doc(args)
lesson_work.save(ignore_permissions=True)
@frappe.whitelist()
def get_assignment(lesson):
assignment = frappe.db.get_value(
"Lesson Assignment",
{"lesson": lesson, "member": frappe.session.user},
["lesson", "member", "assignment", "comments", "status"],
as_dict=True,
)
assignment.file_name = frappe.db.get_value(
"File", {"file_url": assignment.assignment}, "file_name"
)
return assignment
@frappe.whitelist()
def grade_assignment(name, result, comments):
doc = frappe.get_doc("Lesson Assignment", name)
doc.status = result
doc.comments = comments
doc.save()

View File

@@ -0,0 +1,40 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2023-05-29 14:50:07.910319",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"assessment_type",
"assessment_name"
],
"fields": [
{
"fieldname": "assessment_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Assessment Type",
"options": "DocType"
},
{
"fieldname": "assessment_name",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Assessment Name",
"options": "assessment_type"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-05-29 14:56:36.602399",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Assessment",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

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

View File

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

View File

@@ -0,0 +1,104 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "format: ASG-{#####}",
"creation": "2023-05-26 19:41:26.025081",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"title",
"grade_assignment",
"question",
"column_break_hmwv",
"type",
"show_answer",
"answer"
],
"fields": [
{
"fieldname": "question",
"fieldtype": "Text Editor",
"label": "Question"
},
{
"fieldname": "type",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Type",
"options": "Document\nPDF\nURL\nImage\nText"
},
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Title"
},
{
"fieldname": "column_break_hmwv",
"fieldtype": "Column Break"
},
{
"default": "0",
"depends_on": "eval:doc.type == \"Text\"",
"fieldname": "show_answer",
"fieldtype": "Check",
"label": "Show Answer"
},
{
"depends_on": "show_answer",
"fieldname": "answer",
"fieldtype": "Text Editor",
"label": "Answer"
},
{
"default": "1",
"depends_on": "eval:doc.type == \"Text\"",
"fieldname": "grade_assignment",
"fieldtype": "Check",
"label": "Grade Assignment"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-10-06 12:08:46.898950",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Assignment",
"naming_rule": "Expression (old style)",
"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
}
],
"show_title_field_in_link": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "title"
}

View File

@@ -0,0 +1,25 @@
# Copyright (c) 2023, Frappe and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
from lms.lms.utils import has_course_moderator_role, has_course_instructor_role
class LMSAssignment(Document):
pass
@frappe.whitelist()
def save_assignment(assignment, title, type, question):
if not has_course_moderator_role() or not has_course_instructor_role():
return
if assignment:
doc = frappe.get_doc("LMS Assignment", assignment)
else:
doc = frappe.get_doc({"doctype": "LMS Assignment"})
doc.update({"title": title, "type": type, "question": question})
doc.save(ignore_permissions=True)
return doc.name

View File

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

View File

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

View File

@@ -0,0 +1,200 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "format: ASG-SUB-{#####}",
"creation": "2021-12-21 16:15:22.651658",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"assignment",
"assignment_title",
"type",
"column_break_3",
"member",
"member_name",
"section_break_dlzh",
"question",
"column_break_zvis",
"assignment_attachment",
"answer",
"section_break_rqal",
"status",
"evaluator",
"column_break_esgd",
"comments",
"section_break_cwaw",
"lesson",
"course",
"column_break_ygdu"
],
"fields": [
{
"fieldname": "lesson",
"fieldtype": "Link",
"label": "Lesson",
"options": "Course Lesson"
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "assignment",
"fieldtype": "Link",
"label": "Assignment",
"options": "LMS Assignment"
},
{
"fieldname": "member",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Member",
"options": "User",
"reqd": 1
},
{
"fetch_from": "member.full_name",
"fieldname": "member_name",
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Member Name",
"read_only": 1
},
{
"fetch_from": "lesson.course",
"fieldname": "course",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Course",
"options": "LMS Course",
"read_only": 1
},
{
"default": "Not Graded",
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "Pass\nFail\nNot Graded\nNot Applicable"
},
{
"fieldname": "comments",
"fieldtype": "Small Text",
"label": "Comments"
},
{
"fetch_from": "course.evaluator",
"fieldname": "evaluator",
"fieldtype": "Link",
"label": "Evaluator",
"options": "User",
"read_only": 1
},
{
"depends_on": "eval:!([\"URL\", \"Text\"]).includes(doc.type);",
"fieldname": "assignment_attachment",
"fieldtype": "Attach",
"label": "Assignment Attachment",
"mandatory_depends_on": "eval:doc.type != \"URL\";"
},
{
"fetch_from": "assignment.type",
"fieldname": "type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Type",
"options": "Document\nPDF\nURL\nImage\nText"
},
{
"fetch_from": "assignment.question",
"fieldname": "question",
"fieldtype": "Text Editor",
"label": "Question",
"read_only": 1
},
{
"fetch_from": "assignment.title",
"fieldname": "assignment_title",
"fieldtype": "Data",
"label": "Assignment Title"
},
{
"fieldname": "section_break_rqal",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_esgd",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_cwaw",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_ygdu",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:([\"URL\", \"Text\"]).includes(doc.type);",
"fieldname": "answer",
"fieldtype": "Text Editor",
"label": "Answer",
"mandatory_depends_on": "eval:doc.type == \"URL\";"
},
{
"fieldname": "section_break_dlzh",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_zvis",
"fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"links": [],
"make_attachments_public": 1,
"modified": "2023-10-06 15:14:55.984714",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Assignment Submission",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [
{
"color": "Green",
"title": "Pass"
},
{
"color": "Orange",
"title": "Not Graded"
},
{
"color": "Red",
"title": "Fail"
},
{
"color": "Blue",
"title": "Not Applicable"
}
],
"title_field": "assignment_title"
}

View File

@@ -0,0 +1,100 @@
# Copyright (c) 2021, Frappe and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import validate_url
class LMSAssignmentSubmission(Document):
def validate(self):
self.validate_duplicates()
def validate_duplicates(self):
if frappe.db.exists(
"LMS Assignment Submission",
{"assignment": self.assignment, "member": self.member, "name": ["!=", self.name]},
):
lesson_title = frappe.db.get_value("Course Lesson", self.lesson, "title")
frappe.throw(
_("Assignment for Lesson {0} by {1} already exists.").format(
lesson_title, self.member_name
)
)
@frappe.whitelist()
def upload_assignment(
assignment_attachment=None,
answer=None,
assignment=None,
lesson=None,
status="Not Graded",
comments=None,
submission=None,
):
if frappe.session.user == "Guest":
return
assignment_details = frappe.db.get_value(
"LMS Assignment", assignment, ["type", "grade_assignment"], as_dict=1
)
assignment_type = assignment_details.type
if assignment_type in ["URL", "Text"] and not answer:
frappe.throw(_("Please enter the URL for assignment submission."))
if assignment_type == "File" and not assignment_attachment:
frappe.throw(_("Please upload the assignment file."))
if assignment_type == "URL" and not validate_url(answer):
frappe.throw(_("Please enter a valid URL."))
if submission:
doc = frappe.get_doc("LMS Assignment Submission", submission)
else:
doc = frappe.get_doc(
{
"doctype": "LMS Assignment Submission",
"assignment": assignment,
"lesson": lesson,
"member": frappe.session.user,
"type": assignment_type,
}
)
doc.update(
{
"assignment_attachment": assignment_attachment,
"status": "Not Applicable"
if assignment_type == "Text" and not assignment_details.grade_assignment
else status,
"comments": comments,
"answer": answer,
}
)
doc.save(ignore_permissions=True)
return doc.name
@frappe.whitelist()
def get_assignment(lesson):
assignment = frappe.db.get_value(
"LMS Assignment Submission",
{"lesson": lesson, "member": frappe.session.user},
["name", "lesson", "member", "assignment_attachment", "comments", "status"],
as_dict=True,
)
assignment.file_name = frappe.db.get_value(
"File", {"file_url": assignment.assignment_attachment}, "file_name"
)
return assignment
@frappe.whitelist()
def grade_assignment(name, result, comments):
doc = frappe.get_doc("LMS Assignment Submission", name)
doc.status = result
doc.comments = comments
doc.save(ignore_permissions=True)

View File

@@ -5,5 +5,5 @@
import unittest
class TestLessonAssignment(unittest.TestCase):
class TestLMSAssignmentSubmission(unittest.TestCase):
pass

View File

@@ -1,7 +1,123 @@
// Copyright (c) 2021, FOSS United and contributors
// Copyright (c) 2022, Frappe and contributors
// For license information, please see license.txt
frappe.ui.form.on("LMS Batch", {
// refresh: function(frm) {
// }
onload: function (frm) {
frm.set_query("student", "students", function (doc) {
return {
filters: {
ignore_user_type: 1,
},
};
});
frm.set_query("reference_doctype", "timetable", function () {
let doctypes = ["Course Lesson", "LMS Quiz", "LMS Assignment"];
return {
filters: {
name: ["in", doctypes],
},
};
});
frm.set_query("reference_doctype", "timetable_legends", function () {
let doctypes = ["Course Lesson", "LMS Quiz", "LMS Assignment"];
return {
filters: {
name: ["in", doctypes],
},
};
});
},
timetable_template: function (frm) {
set_timetable(frm);
},
});
const set_timetable = (frm) => {
if (frm.doc.timetable_template) {
frm.clear_table("timetable");
frm.refresh_fields();
frappe.call({
method: "frappe.client.get_list",
args: {
doctype: "LMS Batch Timetable",
parent: "LMS Timetable Template",
fields: [
"reference_doctype",
"reference_docname",
"day",
"start_time",
"end_time",
"duration",
],
filters: {
parent: frm.doc.timetable_template,
parenttype: "LMS Timetable Template",
},
order_by: "idx",
},
callback: (data) => {
add_timetable_rows(frm, data.message);
},
});
}
};
const add_timetable_rows = (frm, timetable) => {
timetable.forEach((row) => {
let child = frm.add_child("timetable");
child.reference_doctype = row.reference_doctype;
child.reference_docname = row.reference_docname;
child.date = frappe.datetime.add_days(frm.doc.start_date, row.day - 1);
child.start_time = row.start_time;
child.end_time = row.end_time
? row.end_time
: row.duration
? moment
.utc(row.start_time, "HH:mm")
.add(row.duration, "hour")
.format("HH:mm")
: null;
child.duration = row.duration;
});
frm.refresh_field("timetable");
set_legends(frm);
};
const set_legends = (frm) => {
if (frm.doc.timetable_template) {
frm.clear_table("timetable_legends");
frm.refresh_fields();
frappe.call({
method: "frappe.client.get_list",
args: {
doctype: "LMS Timetable Legend",
parent: "LMS Timetable Template",
fields: ["reference_doctype", "label", "color"],
filters: {
parent: frm.doc.timetable_template,
parenttype: "LMS Timetable Template",
},
order_by: "idx",
},
callback: (data) => {
add_legend_rows(frm, data.message);
},
});
}
};
const add_legend_rows = (frm, legends) => {
legends.forEach((row) => {
let child = frm.add_child("timetable_legends");
child.reference_doctype = row.reference_doctype;
child.label = row.label;
child.color = row.color;
});
frm.refresh_field("timetable_legends");
frm.save();
};

View File

@@ -1,132 +1,291 @@
{
"actions": [],
"autoname": "format: BATCH-{#####}",
"creation": "2021-03-18 19:37:34.614796",
"allow_rename": 1,
"autoname": "format: CLS-{#####}",
"creation": "2022-11-09 16:14:05.876933",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"course",
"start_date",
"start_time",
"column_break_3",
"title",
"sessions_on",
"start_date",
"end_date",
"column_break_4",
"start_time",
"end_time",
"section_break_5",
"published",
"section_break_rgfj",
"medium",
"category",
"column_break_flwy",
"seat_count",
"section_break_6",
"description",
"section_break_7",
"visibility",
"membership",
"column_break_9",
"status",
"stage"
"batch_details_raw",
"column_break_hlqw",
"batch_details",
"meta_image",
"section_break_jgji",
"students",
"courses",
"assessment_tab",
"assessment",
"schedule_tab",
"timetable_template",
"column_break_anya",
"show_live_class",
"allow_future",
"section_break_ontp",
"timetable",
"timetable_legends",
"pricing_tab",
"section_break_gsac",
"paid_batch",
"column_break_iens",
"amount",
"currency",
"customisations_tab",
"section_break_ubxi",
"custom_component",
"column_break_pxgb",
"custom_script"
],
"fields": [
{
"fieldname": "course",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Course",
"options": "LMS Course",
"reqd": 1
},
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title",
"reqd": 1
},
{
"fieldname": "end_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "End Date",
"reqd": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "description",
"fieldtype": "Markdown Editor",
"label": "Description"
"fieldtype": "Small Text",
"label": "Description",
"reqd": 1
},
{
"default": "Public",
"fieldname": "visibility",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Visibility",
"options": "Public\nUnlisted\nPrivate"
"fieldname": "section_break_6",
"fieldtype": "Section Break"
},
{
"fieldname": "membership",
"fieldtype": "Select",
"label": "Membership",
"options": "\nOpen\nRestricted\nInvite Only\nClosed"
"fieldname": "students",
"fieldtype": "Table",
"label": "Students",
"options": "Batch Student"
},
{
"default": "Active",
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Active\nInactive"
},
{
"default": "Ready",
"fieldname": "stage",
"fieldtype": "Select",
"label": "Stage",
"options": "Ready\nIn Progress\nCompleted\nCancelled"
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_5",
"fieldtype": "Section Break",
"label": "Batch Description"
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_7",
"fieldtype": "Section Break",
"label": "Batch Settings"
"fieldname": "courses",
"fieldtype": "Table",
"label": "Courses",
"options": "Batch Course"
},
{
"fieldname": "start_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Start Date"
"label": "Start Date",
"reqd": 1
},
{
"fieldname": "custom_component",
"fieldtype": "Code",
"label": "Custom HTML",
"options": "HTML"
},
{
"default": "0",
"description": "Students will be enrolled in a paid batch once they complete the payment",
"fieldname": "paid_batch",
"fieldtype": "Check",
"label": "Paid Batch"
},
{
"fieldname": "seat_count",
"fieldtype": "Int",
"label": "Seat Count"
},
{
"fieldname": "start_time",
"fieldtype": "Time",
"in_list_view": 1,
"label": "Start Time"
},
{
"fieldname": "sessions_on",
"fieldtype": "Data",
"label": "Sessions On Days"
},
{
"fieldname": "end_time",
"fieldtype": "Time",
"in_list_view": 1,
"label": "End Time"
},
{
"fieldname": "assessment_tab",
"fieldtype": "Tab Break",
"label": "Assessment"
},
{
"fieldname": "assessment",
"fieldtype": "Table",
"label": "Assessment",
"options": "LMS Assessment"
},
{
"fieldname": "section_break_rgfj",
"fieldtype": "Section Break"
},
{
"default": "Online",
"fieldname": "medium",
"fieldtype": "Select",
"label": "Medium",
"options": "Online\nOffline"
},
{
"fieldname": "column_break_flwy",
"fieldtype": "Column Break"
},
{
"fieldname": "category",
"fieldtype": "Link",
"label": "Category",
"options": "LMS Category"
},
{
"description": "These customisations will work on the main batch page.",
"fieldname": "section_break_ubxi",
"fieldtype": "Section Break"
},
{
"fieldname": "schedule_tab",
"fieldtype": "Tab Break",
"label": "Timetable"
},
{
"fieldname": "section_break_gsac",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_iens",
"fieldtype": "Column Break"
},
{
"depends_on": "paid_batch",
"fieldname": "amount",
"fieldtype": "Currency",
"label": "Amount"
},
{
"depends_on": "paid_batch",
"fieldname": "currency",
"fieldtype": "Link",
"label": "Currency",
"options": "Currency"
},
{
"fieldname": "batch_details",
"fieldtype": "Text Editor",
"label": "Batch Details",
"reqd": 1
},
{
"default": "0",
"fieldname": "published",
"fieldtype": "Check",
"label": "Published"
},
{
"fieldname": "timetable",
"fieldtype": "Table",
"label": "Timetable",
"options": "LMS Batch Timetable"
},
{
"fieldname": "timetable_template",
"fieldtype": "Link",
"label": "Timetable Template",
"options": "LMS Timetable Template"
},
{
"fieldname": "column_break_anya",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "show_live_class",
"fieldtype": "Check",
"label": "Show live class"
},
{
"fieldname": "section_break_ontp",
"fieldtype": "Section Break"
},
{
"fieldname": "batch_details_raw",
"fieldtype": "HTML Editor",
"label": "Batch Details Raw"
},
{
"fieldname": "column_break_hlqw",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_jgji",
"fieldtype": "Section Break"
},
{
"fieldname": "meta_image",
"fieldtype": "Attach Image",
"label": "Meta Image"
},
{
"fieldname": "column_break_pxgb",
"fieldtype": "Column Break"
},
{
"fieldname": "customisations_tab",
"fieldtype": "Tab Break",
"label": "Customisations"
},
{
"fieldname": "pricing_tab",
"fieldtype": "Tab Break",
"label": "Pricing"
},
{
"fieldname": "custom_script",
"fieldtype": "Code",
"label": "Custom Script (JavaScript)",
"options": "Javascript"
},
{
"fieldname": "timetable_legends",
"fieldtype": "Table",
"label": "Timetable Legends",
"options": "LMS Timetable Legend"
},
{
"default": "1",
"fieldname": "allow_future",
"fieldtype": "Check",
"label": "Allow accessing future dates"
}
],
"index_web_pages_for_search": 1,
"links": [
{
"group": "Members",
"link_doctype": "LMS Batch Membership",
"link_fieldname": "batch"
}
],
"modified": "2022-09-28 18:43:22.955907",
"links": [],
"modified": "2023-10-12 12:53:37.351989",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Batch",
"naming_rule": "Expression",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -140,11 +299,23 @@
"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
}
],
"quick_entry": 1,
"show_title_field_in_link": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
"title_field": "title"
}

View File

@@ -1,93 +1,395 @@
# Copyright (c) 2021, FOSS United and contributors
# Copyright (c) 2022, Frappe and contributors
# For license information, please see license.txt
import frappe
import requests
import base64
import json
from frappe import _
from datetime import timedelta
from frappe.model.document import Document
from lms.lms.doctype.lms_batch_membership.lms_batch_membership import create_membership
from lms.lms.utils import is_mentor
from frappe.utils import (
cint,
format_date,
format_datetime,
add_to_date,
getdate,
get_datetime,
)
from lms.lms.utils import get_lessons, get_lesson_index, get_lesson_url
from lms.www.utils import get_quiz_details, get_assignment_details
from frappe.email.doctype.email_template.email_template import get_email_template
class LMSBatch(Document):
def validate(self):
pass
# self.validate_if_mentor()
if self.seat_count:
self.validate_seats_left()
self.validate_duplicate_courses()
self.validate_duplicate_students()
self.validate_duplicate_assessments()
self.validate_membership()
self.validate_timetable()
self.send_confirmation_mail()
def validate_if_mentor(self):
if not is_mentor(self.course, frappe.session.user):
course_title = frappe.db.get_value("LMS Course", self.course, "title")
frappe.throw(_("You are not a mentor of the course {0}").format(course_title))
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 batch.").format(
frappe.bold(next(iter(duplicates)))
)
)
def after_insert(self):
create_membership(batch=self.name, course=self.course, member_type="Mentor")
def validate_duplicate_courses(self):
courses = [row.course for row in self.courses]
duplicates = {course for course in courses if courses.count(course) > 1}
if len(duplicates):
title = frappe.db.get_value("LMS Course", next(iter(duplicates)), "title")
frappe.throw(
_("Course {0} has already been added to this batch.").format(frappe.bold(title))
)
def is_member(self, email, member_type=None):
"""Checks if a person is part of a batch.
def validate_duplicate_assessments(self):
assessments = [row.assessment_name for row in self.assessment]
for assessment in self.assessment:
if assessments.count(assessment.assessment_name) > 1:
title = frappe.db.get_value(
assessment.assessment_type, assessment.assessment_name, "title"
)
frappe.throw(
_("Assessment {0} has already been added to this batch.").format(
frappe.bold(title)
)
)
If member_type is specified, checks if the person is a Student/Mentor.
"""
def send_confirmation_mail(self):
for student in self.students:
filters = {"batch": self.name, "member": email}
if member_type:
filters["member_type"] = member_type
return frappe.db.exists("LMS Batch Membership", filters)
if not student.confirmation_email_sent:
self.send_mail(student)
student.confirmation_email_sent = 1
def get_membership(self, email):
"""Returns the membership document of given user."""
name = frappe.get_value(
doctype="LMS Batch Membership",
filters={"batch": self.name, "member": email},
fieldname="name",
def send_mail(self, student):
subject = _("Enrollment Confirmation for the Next Training Batch")
template = "batch_confirmation"
custom_template = frappe.db.get_single_value(
"LMS Settings", "batch_confirmation_template"
)
return frappe.get_doc("LMS Batch Membership", name)
def get_current_lesson(self, user):
"""Returns the name of the current lesson for the given user."""
membership = self.get_membership(user)
return membership and membership.current_lesson
args = {
"student_name": student.student_name,
"start_time": self.start_time,
"start_date": self.start_date,
"medium": self.medium,
"name": self.name,
}
if custom_template:
email_template = get_email_template(custom_template, args)
subject = email_template.get("subject")
content = email_template.get("message")
frappe.sendmail(
recipients=student.student,
subject=subject,
template=template if not custom_template else None,
content=content if custom_template else None,
args=args,
header=[subject, "green"],
retry=3,
)
def validate_membership(self):
for course in self.courses:
for student in self.students:
filters = {
"doctype": "LMS Enrollment",
"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 batch."))
def validate_timetable(self):
for schedule in self.timetable:
if schedule.start_time and schedule.end_time:
if (
schedule.start_time > schedule.end_time or schedule.start_time == schedule.end_time
):
frappe.throw(
_("Row #{0} Start time cannot be greater than or equal to end time.").format(
schedule.idx
)
)
if schedule.start_time < self.start_time or schedule.start_time > self.end_time:
frappe.throw(
_("Row #{0} Start time cannot be outside the batch duration.").format(
schedule.idx
)
)
if schedule.end_time < self.start_time or schedule.end_time > self.end_time:
frappe.throw(
_("Row #{0} End time cannot be outside the batch duration.").format(schedule.idx)
)
if schedule.date < self.start_date or schedule.date > self.end_date:
frappe.throw(
_("Row #{0} Date cannot be outside the batch duration.").format(schedule.idx)
)
@frappe.whitelist()
def save_message(message, batch):
doc = frappe.get_doc(
def remove_student(student, batch_name):
frappe.only_for("Moderator")
frappe.db.delete("Batch Student", {"student": student, "parent": batch_name})
@frappe.whitelist()
def remove_course(course, parent):
frappe.only_for("Moderator")
frappe.db.delete("Batch Course", {"course": course, "parent": parent})
@frappe.whitelist()
def remove_assessment(assessment, parent):
frappe.only_for("Moderator")
frappe.db.delete("LMS Assessment", {"assessment_name": assessment, "parent": parent})
@frappe.whitelist()
def create_live_class(
batch_name, title, duration, date, time, timezone, auto_recording, description=None
):
date = format_date(date, "yyyy-mm-dd", True)
frappe.only_for("Moderator")
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": "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,
"batch_name": batch_name,
"password": data.get("password"),
"description": description,
"auto_recording": auto_recording,
}
)
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_batch(
title,
start_date,
end_date,
description=None,
batch_details=None,
batch_details_raw=None,
meta_image=None,
seat_count=0,
start_time=None,
end_time=None,
medium="Online",
category=None,
paid_batch=0,
amount=0,
currency=None,
name=None,
published=0,
):
frappe.only_for("Moderator")
if name:
doc = frappe.get_doc("LMS Batch", name)
else:
doc = frappe.get_doc({"doctype": "LMS Batch"})
doc.update(
{
"doctype": "LMS Message",
"batch": batch,
"author": frappe.session.user,
"message": message,
"title": title,
"start_date": start_date,
"end_date": end_date,
"description": description,
"batch_details": batch_details,
"batch_details_raw": batch_details_raw,
"image": meta_image,
"seat_count": seat_count,
"start_time": start_time,
"end_time": end_time,
"medium": medium,
"category": category,
"paid_batch": paid_batch,
"amount": amount,
"currency": currency,
"published": published,
}
)
doc.save(ignore_permissions=True)
doc.save()
return doc
def switch_batch(course_name, email, batch_name):
"""Switches the user from the current batch of the course to a new batch."""
membership = frappe.get_last_doc(
"LMS Batch Membership", filters={"course": course_name, "member": email}
@frappe.whitelist()
def fetch_lessons(courses):
lessons = []
courses = json.loads(courses)
for course in courses:
lessons.extend(get_lessons(course.get("course")))
return lessons
@frappe.whitelist()
def add_course(course, parent, name=None, evaluator=None):
frappe.only_for("Moderator")
if frappe.db.exists("Batch Course", {"course": course, "parent": parent}):
frappe.throw(_("Course already added to the batch."))
if name:
doc = frappe.get_doc("Batch Course", name)
else:
doc = frappe.new_doc("Batch Course")
doc.update(
{
"course": course,
"evaluator": evaluator,
"parent": parent,
"parentfield": "courses",
"parenttype": "LMS Batch",
}
)
doc.save()
return doc.name
@frappe.whitelist()
def get_batch_timetable(batch):
timetable = frappe.get_all(
"LMS Batch Timetable",
filters={"parent": batch},
fields=["reference_doctype", "reference_docname", "date", "start_time", "end_time"],
order_by="date",
)
batch = frappe.get_doc("LMS Batch", batch_name)
if not batch:
raise ValueError(f"Invalid Batch: {batch_name}")
show_live_class = frappe.db.get_value("LMS Batch", batch, "show_live_class")
if show_live_class:
live_classes = get_live_classes(batch)
timetable.extend(live_classes)
if batch.course != course_name:
raise ValueError("Can not switch batches across courses")
timetable = get_timetable_details(timetable)
return timetable
if batch.is_member(email):
print(f"{email} is already a member of {batch.title}")
return
old_batch = frappe.get_doc("LMS Batch", membership.batch)
def get_live_classes(batch):
live_classes = frappe.get_all(
"LMS Live Class",
{"batch_name": batch},
["name", "title", "date", "time as start_time", "duration", "join_url as url"],
order_by="date",
)
for class_ in live_classes:
class_.end_time = class_.start_time + timedelta(minutes=class_.duration)
class_.reference_doctype = "LMS Live Class"
class_.reference_docname = class_.name
class_.icon = "icon-call"
print("updating membership", membership.name)
membership.batch = batch_name
membership.save()
return live_classes
# update exercise submissions
filters = {"owner": email, "batch": old_batch.name}
for name in frappe.db.get_all("Exercise Submission", filters=filters, pluck="name"):
doc = frappe.get_doc("Exercise Submission", name)
print("updating exercise submission", name)
doc.batch = batch_name
doc.save()
def get_timetable_details(timetable):
for entry in timetable:
entry.title = frappe.db.get_value(
entry.reference_doctype, entry.reference_docname, "title"
)
assessment = frappe._dict({"assessment_name": entry.reference_docname})
if entry.reference_doctype == "Course Lesson":
entry.icon = "icon-list"
course = frappe.db.get_value(
entry.reference_doctype, entry.reference_docname, "course"
)
entry.url = get_lesson_url(course, get_lesson_index(entry.reference_docname))
elif entry.reference_doctype == "LMS Quiz":
entry.icon = "icon-quiz"
entry.url = "/quizzes"
details = get_quiz_details(assessment, frappe.session.user)
entry.update(details)
elif entry.reference_doctype == "LMS Assignment":
entry.icon = "icon-quiz"
details = get_assignment_details(assessment, frappe.session.user)
entry.update(details)
timetable = sorted(timetable, key=lambda k: k["date"])
return timetable
@frappe.whitelist()
def send_email_to_students(batch, subject, reply_to, message):
frappe.only_for("Moderator")
students = frappe.get_all("Batch Student", {"parent": batch}, pluck="student")
frappe.sendmail(
recipients=students,
subject=subject,
reply_to=reply_to,
message=message
)

View File

@@ -1,9 +1,9 @@
# Copyright (c) 2021, FOSS United and Contributors
# Copyright (c) 2022, Frappe and Contributors
# See license.txt
# import frappe
import unittest
from frappe.tests.utils import FrappeTestCase
class TestLMSBatch(unittest.TestCase):
class TestLMSClass(FrappeTestCase):
pass

View File

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

View File

@@ -0,0 +1,150 @@
{
"actions": [],
"autoname": "format: BATCH-{#####}",
"creation": "2021-03-18 19:37:34.614796",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"course",
"start_date",
"start_time",
"column_break_3",
"title",
"sessions_on",
"end_time",
"section_break_5",
"description",
"section_break_7",
"visibility",
"membership",
"column_break_9",
"status",
"stage"
],
"fields": [
{
"fieldname": "course",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Course",
"options": "LMS Course",
"reqd": 1
},
{
"fieldname": "title",
"fieldtype": "Data",
"label": "Title",
"reqd": 1
},
{
"fieldname": "description",
"fieldtype": "Markdown Editor",
"label": "Description"
},
{
"default": "Public",
"fieldname": "visibility",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Visibility",
"options": "Public\nUnlisted\nPrivate"
},
{
"fieldname": "membership",
"fieldtype": "Select",
"label": "Membership",
"options": "\nOpen\nRestricted\nInvite Only\nClosed"
},
{
"default": "Active",
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Active\nInactive"
},
{
"default": "Ready",
"fieldname": "stage",
"fieldtype": "Select",
"label": "Stage",
"options": "Ready\nIn Progress\nCompleted\nCancelled"
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_5",
"fieldtype": "Section Break",
"label": "Batch Description"
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_7",
"fieldtype": "Section Break",
"label": "Batch Settings"
},
{
"fieldname": "start_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Start Date"
},
{
"fieldname": "start_time",
"fieldtype": "Time",
"in_list_view": 1,
"label": "Start Time"
},
{
"fieldname": "sessions_on",
"fieldtype": "Data",
"label": "Sessions On Days"
},
{
"fieldname": "end_time",
"fieldtype": "Time",
"in_list_view": 1,
"label": "End Time"
}
],
"index_web_pages_for_search": 1,
"links": [
{
"group": "Members",
"link_doctype": "LMS Enrollment",
"link_fieldname": "batch_old"
}
],
"modified": "2022-09-28 18:43:22.955907",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Batch Old",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -0,0 +1,92 @@
# Copyright (c) 2021, FOSS United and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
from lms.lms.doctype.lms_enrollment.lms_enrollment import create_membership
from lms.lms.utils import is_mentor
class LMSBatchOld(Document):
def validate(self):
pass
# self.validate_if_mentor()
def validate_if_mentor(self):
if not is_mentor(self.course, frappe.session.user):
course_title = frappe.db.get_value("LMS Course", self.course, "title")
frappe.throw(_("You are not a mentor of the course {0}").format(course_title))
def after_insert(self):
create_membership(batch=self.name, course=self.course, member_type="Mentor")
def is_member(self, email, member_type=None):
"""Checks if a person is part of a batch.
If member_type is specified, checks if the person is a Student/Mentor.
"""
filters = {"batch_old": self.name, "member": email}
if member_type:
filters["member_type"] = member_type
return frappe.db.exists("LMS Enrollment", filters)
def get_membership(self, email):
"""Returns the membership document of given user."""
name = frappe.get_value(
doctype="LMS Enrollment",
filters={"batch_old": self.name, "member": email},
fieldname="name",
)
return frappe.get_doc("LMS Enrollment", name)
def get_current_lesson(self, user):
"""Returns the name of the current lesson for the given user."""
membership = self.get_membership(user)
return membership and membership.current_lesson
@frappe.whitelist()
def save_message(message, batch):
doc = frappe.get_doc(
{
"doctype": "LMS Message",
"batch_old": batch,
"author": frappe.session.user,
"message": message,
}
)
doc.save(ignore_permissions=True)
def switch_batch(course_name, email, batch_name):
"""Switches the user from the current batch of the course to a new batch."""
membership = frappe.get_last_doc(
"LMS Enrollment", filters={"course": course_name, "member": email}
)
batch = frappe.get_doc("LMS Batch Old", batch_name)
if not batch:
raise ValueError(f"Invalid Batch: {batch_name}")
if batch.course != course_name:
raise ValueError("Can not switch batches across courses")
if batch.is_member(email):
print(f"{email} is already a member of {batch.title}")
return
old_batch = frappe.get_doc("LMS Batch Old", membership.batch_old)
membership.batch_old = batch_name
membership.save()
# update exercise submissions
filters = {"owner": email, "batch_old": old_batch.name}
for name in frappe.db.get_all("Exercise Submission", filters=filters, pluck="name"):
doc = frappe.get_doc("Exercise Submission", name)
print("updating exercise submission", name)
doc.batch_old = batch_name
doc.save()

View File

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

View File

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

View File

@@ -0,0 +1,87 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "hash",
"creation": "2023-09-14 12:44:51.098956",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"column_break_htdc",
"reference_doctype",
"reference_docname",
"date",
"day",
"column_break_merq",
"start_time",
"end_time",
"duration"
],
"fields": [
{
"fieldname": "reference_doctype",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Reference DocType",
"options": "DocType"
},
{
"fieldname": "reference_docname",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Reference DocName",
"options": "reference_doctype"
},
{
"fieldname": "column_break_merq",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.parenttype == \"LMS Batch\";",
"fieldname": "date",
"fieldtype": "Date",
"label": "Date"
},
{
"fieldname": "start_time",
"fieldtype": "Time",
"in_list_view": 1,
"label": "Start Time"
},
{
"fieldname": "duration",
"fieldtype": "Data",
"label": "Duration"
},
{
"fieldname": "column_break_htdc",
"fieldtype": "Column Break"
},
{
"fieldname": "end_time",
"fieldtype": "Time",
"in_list_view": 1,
"label": "End Time"
},
{
"depends_on": "eval: doc.parenttype == \"LMS Timetable Template\";",
"fieldname": "day",
"fieldtype": "Int",
"label": "Day"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-10-03 17:40:31.530181",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Batch Timetable",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

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

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2023, Frappe and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestLMSBatchTimetable(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("LMS Category", {
// refresh(frm) {
// },
// });

View File

@@ -0,0 +1,59 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:category",
"creation": "2023-06-15 12:40:36.484165",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"category"
],
"fields": [
{
"fieldname": "category",
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Category",
"unique": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-06-15 15:14:11.341961",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Category",
"naming_rule": "By fieldname",
"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": [],
"title_field": "category"
}

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

View File

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

View File

@@ -8,9 +8,11 @@
"course",
"member",
"member_name",
"published",
"column_break_3",
"issue_date",
"expiry_date"
"expiry_date",
"batch_name"
],
"fields": [
{
@@ -52,11 +54,24 @@
"fieldtype": "Data",
"label": "Member Name",
"read_only": 1
},
{
"fieldname": "batch_name",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Batch",
"options": "LMS Batch"
},
{
"default": "0",
"fieldname": "published",
"fieldtype": "Check",
"label": "Publish on Participant Page"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-04-06 11:49:36.077370",
"modified": "2023-09-13 11:03:23.479255",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Certificate",
@@ -73,6 +88,18 @@
"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",

View File

@@ -5,15 +5,17 @@ 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}
"LMS Certificate",
{"member": self.member, "course": self.course, "name": ["!=", self.name]},
)
if len(certificates):
full_name = frappe.db.get_value("User", self.member, "full_name")
@@ -22,6 +24,16 @@ class LMSCertificate(Document):
_("{0} is already certified for the course {1}").format(full_name, course_name)
)
def on_update(self):
frappe.share.add_docshare(
self.doctype,
self.name,
self.member,
write=1,
share=1,
flags={"ignore_share_permission": True},
)
@frappe.whitelist()
def create_certificate(course):
@@ -47,10 +59,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

@@ -8,15 +8,16 @@
"field_order": [
"member",
"member_name",
"column_break_5",
"course",
"status",
"section_break_6",
"column_break_5",
"date",
"start_time",
"end_time",
"column_break_10",
"batch_name",
"section_break_6",
"rating",
"status",
"column_break_10",
"summary"
],
"fields": [
@@ -83,21 +84,29 @@
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "Pass\nFail",
"options": "Pending\nIn Progress\nPass\nFail",
"reqd": 1
},
{
"fieldname": "section_break_6",
"fieldtype": "Section Break"
"fieldtype": "Section Break",
"label": "Evaluation Details"
},
{
"fieldname": "column_break_10",
"fieldtype": "Column Break"
},
{
"fieldname": "batch_name",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Batch Name",
"options": "LMS Batch"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-11-23 11:49:01.400292",
"modified": "2023-09-26 19:44:43.594892",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Certificate Evaluation",
@@ -114,10 +123,39 @@
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Class Evaluator",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"states": [
{
"color": "Green",
"title": "Pass"
},
{
"color": "Red",
"title": "Fail"
},
{
"color": "Blue",
"title": "Pending"
},
{
"color": "Orange",
"title": "In Progress"
}
],
"title_field": "member_name"
}

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,16 @@
"engine": "InnoDB",
"field_order": [
"course",
"evaluator",
"batch_name",
"column_break_4",
"member",
"member_name",
"evaluator",
"column_break_4",
"section_break_lifi",
"date",
"day",
"google_meet_link",
"column_break_ddyh",
"start_time",
"end_time"
],
@@ -29,17 +33,18 @@
{
"fieldname": "member",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Member",
"options": "User",
"reqd": 1
},
{
"fetch_from": "course.evaluator",
"fetch_if_empty": 1,
"fieldname": "evaluator",
"fieldtype": "Link",
"label": "Evaluator",
"options": "Course Evaluator",
"options": "User",
"read_only": 1
},
{
@@ -66,7 +71,6 @@
{
"fieldname": "end_time",
"fieldtype": "Time",
"in_list_view": 1,
"label": "End Time",
"reqd": 1
},
@@ -80,11 +84,32 @@
"fieldtype": "Data",
"label": "Member Name",
"read_only": 1
},
{
"fieldname": "section_break_lifi",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_ddyh",
"fieldtype": "Column Break"
},
{
"fieldname": "google_meet_link",
"fieldtype": "Data",
"label": "Google Meet Link",
"read_only": 1
},
{
"fieldname": "batch_name",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Batch Name",
"options": "LMS Batch"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-04-06 11:33:33.711545",
"modified": "2023-08-23 14:50:37.618352",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Certificate Request",
@@ -99,6 +124,31 @@
"read": 1,
"report": 1,
"role": "System Manager",
"select": 1,
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Class Evaluator",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Moderator",
"share": 1,
"write": 1
}

View File

@@ -5,7 +5,8 @@ import frappe
from frappe import _
from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
from frappe.utils import format_date, format_time, getdate
from frappe.utils import format_date, format_time, getdate, add_to_date, get_datetime
from lms.lms.utils import get_evaluator
class LMSCertificateRequest(Document):
@@ -15,12 +16,13 @@ class LMSCertificateRequest(Document):
def validate_if_existing_requests(self):
existing_requests = frappe.get_all(
"LMS Certificate Request",
{"member": self.member, "course": self.course},
{"member": self.member, "course": self.course, "name": ["!=", self.name]},
["date", "start_time", "course"],
)
for req in existing_requests:
if req.date == getdate(self.date) and getdate() <= getdate(self.date):
if req.date == getdate(self.date) or getdate() <= getdate(req.date):
course_title = frappe.db.get_value("LMS Course", req.course, "title")
frappe.throw(
_("You already have an evaluation on {0} at {1} for the course {2}.").format(
@@ -31,26 +33,98 @@ class LMSCertificateRequest(Document):
)
def schedule_evals():
if frappe.db.get_single_value("LMS Settings", "send_calendar_invite_for_evaluations"):
one_hour_ago = add_to_date(get_datetime(), hours=-1)
evals = frappe.get_all(
"LMS Certificate Request",
{"creation": [">=", one_hour_ago], "google_meet_link": ["is", "not set"]},
["name", "member", "member_name", "evaluator", "date", "start_time", "end_time"],
)
for eval in evals:
setup_calendar_event(eval)
def setup_calendar_event(eval):
calendar = frappe.db.get_value(
"Google Calendar", {"user": eval.evaluator, "enable": 1}, "name"
)
if calendar:
event = create_event(eval)
add_participants(eval, event)
update_meeting_details(eval, event, calendar)
def create_event(eval):
event = frappe.get_doc(
{
"doctype": "Event",
"subject": f"Evaluation of {eval.member_name}",
"starts_on": f"{eval.date} {eval.start_time}",
"ends_on": f"{eval.date} {eval.end_time}",
}
)
event.save()
return event
def add_participants(eval, event):
participants = [eval.member, eval.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()
def update_meeting_details(eval, event, calendar):
event.reload()
event.update(
{
"sync_with_google_calendar": 1,
"add_video_conferencing": 1,
"google_calendar": calendar,
}
)
event.save()
event.reload()
frappe.db.set_value(
"LMS Certificate Request", eval.name, "google_meet_link", event.google_meet_link
)
@frappe.whitelist()
def create_certificate_request(course, date, day, start_time, end_time):
def create_certificate_request(course, date, day, start_time, end_time, batch=None):
is_member = frappe.db.exists(
{"doctype": "LMS Batch Membership", "course": course, "member": frappe.session.user}
{"doctype": "LMS Enrollment", "course": course, "member": frappe.session.user}
)
if not is_member:
return
frappe.get_doc(
eval = frappe.new_doc("LMS Certificate Request")
eval.update(
{
"doctype": "LMS Certificate Request",
"course": course,
"evaluator": get_evaluator(course, batch),
"member": frappe.session.user,
"date": date,
"day": day,
"start_time": start_time,
"end_time": end_time,
"batch": batch,
}
).save(ignore_permissions=True)
)
eval.save(ignore_permissions=True)
@frappe.whitelist()

View File

@@ -1,111 +0,0 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "format: CLS-{#####}",
"creation": "2022-11-09 16:14:05.876933",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"title",
"start_date",
"end_date",
"column_break_4",
"description",
"section_break_6",
"students",
"courses",
"custom_component"
],
"fields": [
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title",
"reqd": 1
},
{
"fieldname": "end_date",
"fieldtype": "Date",
"label": "End Date",
"reqd": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Description"
},
{
"fieldname": "section_break_6",
"fieldtype": "Section Break"
},
{
"fieldname": "students",
"fieldtype": "Table",
"label": "Students",
"options": "Class Student"
},
{
"fieldname": "courses",
"fieldtype": "Table",
"label": "Courses",
"options": "Class Course"
},
{
"fieldname": "start_date",
"fieldtype": "Date",
"label": "Start Date",
"reqd": 1
},
{
"description": "The HTML code entered here will be displayed on the class details page.",
"fieldname": "custom_component",
"fieldtype": "Code",
"label": "Custom Component",
"options": "HTML"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-11-25 10:37:24.250557",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Class",
"naming_rule": "Expression (old style)",
"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": []
}

View File

@@ -1,66 +0,0 @@
# Copyright (c) 2022, Frappe and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
from frappe import _
from frappe.utils import cint
class LMSClass(Document):
def validate(self):
validate_membership(self)
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()
@frappe.whitelist()
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."))
frappe.get_doc(
{
"doctype": "Class Student",
"student": email,
"student_name": frappe.db.get_value("User", email, "full_name"),
"parent": class_name,
"parenttype": "LMS Class",
"parentfield": "students",
}
).save()
return True
@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(
{
"doctype": "Class Course",
"parent": class_name,
"course": course,
"parenttype": "LMS Class",
"parentfield": "courses",
}
)
doc.save()
else:
frappe.db.delete("Class Course", {"parent": class_name, "course": course})
return True

View File

@@ -11,14 +11,6 @@ frappe.ui.form.on("LMS Course", {
};
});
frm.set_query("instructor", "instructors", function () {
return {
filters: {
ignore_user_type: 1,
},
};
});
frm.set_query("course", "related_courses", function () {
return {
filters: {
@@ -27,4 +19,14 @@ frappe.ui.form.on("LMS Course", {
};
});
},
refresh: (frm) => {
frm.add_web_link(`/courses/${frm.doc.name}`, "See on Website");
if (!frm.doc.currency)
frappe.db
.get_single_value("LMS Settings", "default_currency")
.then((value) => {
frm.set_value("currency", value);
});
},
});

View File

@@ -32,19 +32,18 @@
"description",
"chapters",
"related_courses",
"pricing_section",
"paid_course",
"currency",
"course_price",
"certification_section",
"enable_certification",
"expiry",
"section_break_23",
"max_attempts",
"column_break_rxww",
"grant_certificate_after",
"evaluator",
"column_break_26",
"max_attempts",
"duration",
"pricing_section",
"paid_certificate",
"currency",
"price_certificate"
"duration"
],
"fields": [
{
@@ -53,12 +52,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
},
@@ -171,13 +169,6 @@
"fieldname": "column_break_12",
"fieldtype": "Column Break"
},
{
"default": "0",
"depends_on": "enable_certification",
"fieldname": "paid_certificate",
"fieldtype": "Check",
"label": "Paid Certificate"
},
{
"depends_on": "enable_certification",
"fieldname": "grant_certificate_after",
@@ -194,24 +185,16 @@
"options": "Course Evaluator"
},
{
"depends_on": "eval: doc.grant_certificate_after == \"Evaluation\"",
"fieldname": "pricing_section",
"fieldtype": "Section Break",
"label": "Pricing"
},
{
"depends_on": "paid_certificate",
"fieldname": "price_certificate",
"fieldtype": "Currency",
"label": "Certificate Price",
"mandatory_depends_on": "paid_certificate"
},
{
"depends_on": "paid_certificate",
"depends_on": "paid_course",
"fieldname": "currency",
"fieldtype": "Link",
"label": "Currency",
"mandatory_depends_on": "paid_certificate",
"mandatory_depends_on": "paid_course",
"options": "Currency"
},
{
@@ -229,11 +212,21 @@
"options": "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12"
},
{
"fieldname": "section_break_23",
"fieldtype": "Section Break"
"default": "0",
"fieldname": "paid_course",
"fieldtype": "Check",
"label": "Paid Course"
},
{
"fieldname": "column_break_26",
"depends_on": "paid_course",
"fieldname": "course_price",
"fieldtype": "Currency",
"label": "Course Price",
"option": "currency",
"mandatory_depends_on": "paid_course"
},
{
"fieldname": "column_break_rxww",
"fieldtype": "Column Break"
}
],
@@ -246,7 +239,7 @@
},
{
"group": "Batches",
"link_doctype": "LMS Batch",
"link_doctype": "LMS Batch Old",
"link_fieldname": "course"
},
{
@@ -261,7 +254,7 @@
}
],
"make_attachments_public": 1,
"modified": "2022-09-14 13:26:53.153822",
"modified": "2023-08-28 11:09:11.945066",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Course",
@@ -276,20 +269,18 @@
"read": 1,
"report": 1,
"role": "System Manager",
"select": 1,
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"select": 1,
"role": "Course Creator",
"share": 1,
"write": 1
}

View File

@@ -2,19 +2,20 @@
# 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 lms.lms.utils import get_chapters
from frappe.utils.telemetry import capture
from lms.lms.utils import get_chapters, can_create_courses
from ...utils import generate_slug, validate_image
from frappe import _
class LMSCourse(Document):
def validate(self):
self.validate_instructors()
self.validate_video_link()
self.validate_status()
self.image = validate_image(self.image)
@@ -30,6 +31,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 +43,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 +75,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}>"
@@ -107,20 +118,18 @@ class LMSCourse(Document):
return
batch_name = frappe.get_value(
doctype="LMS Batch Membership",
doctype="LMS Enrollment",
filters={"course": self.name, "member_type": "Student", "member": email},
fieldname="batch",
fieldname="batch_old",
)
return batch_name and frappe.get_doc("LMS Batch", batch_name)
return batch_name and frappe.get_doc("LMS Batch Old", batch_name)
def get_batches(self, mentor=None):
batches = frappe.get_all("LMS Batch", {"course": self.name})
batches = frappe.get_all("LMS Batch Old", {"course": self.name})
if mentor:
# TODO: optimize this
memberships = frappe.db.get_all(
"LMS Batch Membership", {"member": mentor}, ["batch"]
)
batch_names = {m.batch for m in memberships}
memberships = frappe.db.get_all("LMS Enrollment", {"member": mentor}, ["batch_old"])
batch_names = {m.batch_old for m in memberships}
return [b for b in batches if b.name in batch_names]
def get_cohorts(self):
@@ -150,10 +159,12 @@ class LMSCourse(Document):
def get_all_memberships(self, member):
all_memberships = frappe.get_all(
"LMS Batch Membership", {"member": member, "course": self.name}, ["batch"]
"LMS Enrollment", {"member": member, "course": self.name}, ["batch_old"]
)
for membership in all_memberships:
membership.batch_title = frappe.db.get_value("LMS Batch", membership.batch, "title")
membership.batch_title = frappe.db.get_value(
"LMS Batch Old", membership.batch_old, "title"
)
return all_memberships
@@ -201,7 +212,13 @@ def save_course(
published,
upcoming,
image=None,
paid_course=False,
course_price=None,
currency=None,
):
if not can_create_courses(course):
return
if course:
doc = frappe.get_doc("LMS Course", course)
else:
@@ -217,6 +234,9 @@ def save_course(
"tags": tags,
"published": cint(published),
"upcoming": cint(upcoming),
"paid_course": cint(paid_course),
"course_price": course_price,
"currency": currency,
}
)
doc.save(ignore_permissions=True)
@@ -260,6 +280,7 @@ def save_lesson(
preview,
idx,
lesson,
instructor_notes=None,
youtube=None,
quiz_id=None,
question=None,
@@ -275,6 +296,7 @@ def save_lesson(
"chapter": chapter,
"title": title,
"body": body,
"instructor_notes": instructor_notes,
"include_in_preview": preview,
"youtube": youtube,
"quiz_id": quiz_id,
@@ -301,3 +323,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

@@ -12,7 +12,6 @@ class TestLMSCourse(unittest.TestCase):
def test_new_course(self):
course = new_course("Test Course")
assert course.title == "Test Course"
assert course.name == "test-course"
# disabled this test as it is failing
def _test_add_mentors(self):
@@ -35,9 +34,11 @@ 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 Batch Membership", {"course": "test-course"})
frappe.db.delete("LMS Batch", {"course": "test-course"})
frappe.db.delete("LMS Exercise", {"course": "test-course"})
frappe.db.delete("LMS Enrollment", {"course": "test-course"})
frappe.db.delete("Course Lesson", {"course": "test-course"})
frappe.db.delete("Course Chapter", {"course": "test-course"})
frappe.db.delete("LMS Batch Old", {"course": "test-course"})
frappe.db.delete("LMS Course Mentor Mapping", {"course": "test-course"})
frappe.db.delete("Course Instructor", {"parent": "test-course"})
frappe.db.sql("delete from `tabCourse Instructor`")
@@ -50,14 +51,14 @@ def new_user(name, email):
return frappe.get_doc("User", user)
else:
filters = {
"doctype": "User",
"email": email,
"first_name": name,
"send_welcome_email": False,
}
doc = frappe.get_doc(filters)
doc.insert()
doc = frappe.new_doc("User")
doc.update(filters)
doc.save()
return doc
@@ -68,7 +69,6 @@ def new_course(title, additional_filters=None):
else:
create_evaluator()
filters = {
"doctype": "LMS Course",
"title": title,
"short_introduction": title,
"description": title,
@@ -77,8 +77,9 @@ def new_course(title, additional_filters=None):
if additional_filters:
filters.update(additional_filters)
doc = frappe.get_doc(filters)
doc.insert(ignore_permissions=True)
doc = frappe.new_doc("LMS Course")
doc.update(filters)
doc.save()
return doc

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2021, FOSS United and contributors
// For license information, please see license.txt
frappe.ui.form.on("LMS Batch Membership", {
frappe.ui.form.on("LMS Enrollment", {
onload: function (frm) {
frm.set_query("member", function (doc) {
return {

View File

@@ -7,7 +7,7 @@
"field_order": [
"course",
"member_type",
"batch",
"payment",
"column_break_3",
"member",
"member_name",
@@ -15,6 +15,7 @@
"section_break_8",
"cohort",
"subgroup",
"batch_old",
"column_break_12",
"current_lesson",
"progress",
@@ -22,10 +23,10 @@
],
"fields": [
{
"fieldname": "batch",
"fieldname": "batch_old",
"fieldtype": "Link",
"label": "Batch",
"options": "LMS Batch"
"label": "Batch Old",
"options": "LMS Batch Old"
},
{
"fieldname": "member",
@@ -112,14 +113,20 @@
{
"fieldname": "section_break_8",
"fieldtype": "Section Break"
},
{
"fieldname": "payment",
"fieldtype": "Link",
"label": "Payment",
"options": "LMS Payment"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-10-10 12:38:17.839526",
"modified": "2023-10-02 12:41:25.139734",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Batch Membership",
"name": "LMS Enrollment",
"owner": "Administrator",
"permissions": [
{
@@ -133,6 +140,31 @@
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "LMS Student",
"select": 1,
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Moderator",
"select": 1,
"share": 1,
"write": 1
}
],
"quick_entry": 1,
@@ -141,4 +173,4 @@
"sort_order": "DESC",
"states": [],
"title_field": "member_name"
}
}

View File

@@ -6,17 +6,17 @@ from frappe import _
from frappe.model.document import Document
class LMSBatchMembership(Document):
class LMSEnrollment(Document):
def validate(self):
self.validate_membership_in_same_batch()
self.validate_membership_in_different_batch_same_course()
def validate_membership_in_same_batch(self):
filters = {"member": self.member, "course": self.course, "name": ["!=", self.name]}
if self.batch:
filters["batch"] = self.batch
if self.batch_old:
filters["batch_old"] = self.batch_old
previous_membership = frappe.db.get_value(
"LMS Batch Membership", filters, fieldname=["member_type", "member"], as_dict=1
"LMS Enrollment", filters, fieldname=["member_type", "member"], as_dict=1
)
if previous_membership:
@@ -34,16 +34,16 @@ class LMSBatchMembership(Document):
if self.member_type != "Student":
return
course = frappe.db.get_value("LMS Batch", self.batch, "course")
course = frappe.db.get_value("LMS Batch Old", self.batch_old, "course")
memberships = frappe.get_all(
"LMS Batch Membership",
"LMS Enrollment",
filters={
"member": self.member,
"name": ["!=", self.name],
"member_type": "Student",
"course": self.course,
},
fields=["batch", "member_type", "name"],
fields=["batch_old", "member_type", "name"],
)
if memberships:
@@ -51,7 +51,7 @@ class LMSBatchMembership(Document):
member_name = frappe.db.get_value("User", self.member, "full_name")
frappe.throw(
_("{0} is already a Student of {1} course through {2} batch").format(
member_name, course, membership.batch
member_name, course, membership.batch_old
)
)
@@ -62,8 +62,8 @@ def create_membership(
):
frappe.get_doc(
{
"doctype": "LMS Batch Membership",
"batch": batch,
"doctype": "LMS Enrollment",
"batch_old": batch,
"course": course,
"role": role,
"member_type": member_type,
@@ -76,15 +76,13 @@ def create_membership(
@frappe.whitelist()
def update_current_membership(batch, course, member):
all_memberships = frappe.get_all(
"LMS Batch Membership", {"member": member, "course": course}
"LMS Enrollment", {"member": member, "course": course}
)
for membership in all_memberships:
frappe.db.set_value("LMS Batch Membership", membership.name, "is_current", 0)
frappe.db.set_value("LMS Enrollment", membership.name, "is_current", 0)
current_membership = frappe.get_all(
"LMS Batch Membership", {"batch": batch, "member": member}
"LMS Enrollment", {"batch_old": batch, "member": member}
)
if len(current_membership):
frappe.db.set_value(
"LMS Batch Membership", current_membership[0].name, "is_current", 1
)
frappe.db.set_value("LMS Enrollment", current_membership[0].name, "is_current", 1)

View File

@@ -8,12 +8,12 @@ import frappe
from lms.lms.doctype.lms_course.test_lms_course import new_course, new_user
class TestLMSBatchMembership(unittest.TestCase):
class TestLMSEnrollment(unittest.TestCase):
def setUp(self):
frappe.db.sql("DELETE FROM `tabLMS Batch Membership`")
frappe.db.sql("DELETE FROM `tabLMS Batch`")
frappe.db.sql("delete from `tabLMS Course Mentor Mapping`")
frappe.db.sql("DELETE FROM `tabUser` where email like '%@test.com'")
frappe.db.delete("LMS Enrollment")
frappe.db.delete("LMS Batch Old")
frappe.db.delete("LMS Course Mentor Mapping")
frappe.db.delete("User", {"email": ("like", "%@test.com")})
def new_course_batch(self):
course = new_course("Test Course")
@@ -26,7 +26,7 @@ class TestLMSBatchMembership(unittest.TestCase):
batch = frappe.get_doc(
{
"doctype": "LMS Batch",
"doctype": "LMS Batch Old",
"name": "test-batch",
"title": "Test Batch",
"course": course.name,
@@ -37,13 +37,14 @@ class TestLMSBatchMembership(unittest.TestCase):
frappe.session.user = "Administrator"
return course, batch
def add_membership(self, batch_name, member_name, member_type="Student"):
def add_membership(self, batch_name, member_name, course, member_type="Student"):
doc = frappe.get_doc(
{
"doctype": "LMS Batch Membership",
"batch": batch_name,
"doctype": "LMS Enrollment",
"batch_old": batch_name,
"member": member_name,
"member_type": member_type,
"course": course,
}
)
doc.insert()
@@ -52,7 +53,7 @@ class TestLMSBatchMembership(unittest.TestCase):
def test_membership(self):
course, batch = self.new_course_batch()
member = new_user("Test", "test01@test.com")
membership = self.add_membership(batch.name, member.name)
membership = self.add_membership(batch.name, member.name, course.name)
assert membership.course == course.name
assert membership.member_name == member.full_name
@@ -60,7 +61,7 @@ class TestLMSBatchMembership(unittest.TestCase):
def test_membership_change_role(self):
course, batch = self.new_course_batch()
member = new_user("Test", "test01@test.com")
membership = self.add_membership(batch.name, member.name)
membership = self.add_membership(batch.name, member.name, course.name)
# it should be possible to change role
membership.role = "Admin"

View File

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

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