Compare commits

..

154 Commits

Author SHA1 Message Date
Jannat Patel
919a5265c2 Merge pull request #1240 from pateljannat/fix-onboarding-data
fix: onboarding data update
2025-01-10 15:06:19 +05:30
Jannat Patel
507d08f37c fix: onboarding data update 2025-01-10 15:04:08 +05:30
Jannat Patel
40c295aa37 fix: slides rendering issue 2024-08-29 11:24:03 +05:30
Jannat Patel
2905a6af1a Merge pull request #749 from pateljannat/batch-conditions
fix: batch registration button conditions
2024-03-07 10:33:39 +05:30
Jannat Patel
4cc27adb8b fix: batch registration button conditions 2024-03-07 10:21:18 +05:30
Jannat Patel
a5d000f702 fix: quix show correct answers 2024-02-21 17:02:18 +05:30
Jannat Patel
39aa1d443d fix: removed unnecessary start learning buttons 2024-02-19 17:07:22 +05:30
Jannat Patel
4b4086afb3 fix: active site capture 2024-02-07 11:14:12 +05:30
Jannat Patel
b0bb7d32ca chore: capture active sites 2024-02-06 10:12:49 +05:30
Jannat Patel
ff1bd91223 feat: cohort description 2024-02-05 23:06:26 +05:30
Jannat Patel
3dad3580bb fix: batch self enrollment 2024-02-01 10:52:23 +05:30
Jannat Patel
8f687145be fix: pricing issue 2024-01-25 23:13:43 +05:30
Jannat Patel
9c405edd09 Merge branch 'main' of https://github.com/frappe/lms 2024-01-25 15:45:41 +05:30
Jannat Patel
fe791dc478 fix: batch self enrollment 2024-01-25 15:45:27 +05:30
Jannat Patel
42417621fa Merge pull request #712 from IslemMedjahdi/patch-1
Update docker-installation.md
2024-01-23 13:30:10 +05:30
Jannat Patel
d3b3d85c84 fix: redirect to login if guest before batch enrollment 2024-01-22 11:19:32 +05:30
Md Hussain Nagaria
b700013704 fix: only show start date for single day batches (#726) 2024-01-22 10:55:36 +05:30
Jannat Patel
bac229c731 Merge pull request #725 from pateljannat/students-in-batch
feat: self enrolment in batches
2024-01-22 10:54:20 +05:30
Jannat Patel
28043e634b fix: removed unnecessary roles 2024-01-22 10:43:02 +05:30
Md Hussain Nagaria
b672108155 fix: remove $ typos (#723) 2024-01-22 10:33:55 +05:30
Jannat Patel
5e569ab0e6 feat: self enrollment in batches 2024-01-19 23:56:48 +05:30
Jannat Patel
43a07e53a6 Merge branch 'main' of https://github.com/frappe/lms 2024-01-18 11:32:12 +05:30
Jannat Patel
fbd83196fc fix: job page logos 2024-01-18 11:31:50 +05:30
Jannat Patel
465f4e1e96 docs: fixed readme logo dimensions 2024-01-17 14:48:48 +05:30
Jannat Patel
43d409ce64 Merge branch 'main' of https://github.com/frappe/lms 2024-01-17 14:47:11 +05:30
Jannat Patel
a5fc52ec29 fix: logo on readme 2024-01-17 14:46:48 +05:30
Jannat Patel
a9b06575d0 Merge pull request #718 from pateljannat/change-jobs-route
fix: changed jobs route to job-openings
2024-01-17 11:24:06 +05:30
Jannat Patel
3070cbed3c fix: changed jobs route to job-openings 2024-01-17 11:00:31 +05:30
Medjahdi Islem
d712881e16 Update docker-installation.md 2024-01-01 15:32:00 +01:00
Jannat Patel
991dc7f8c8 fix: batch card 2023-12-28 12:04:06 +05:30
Jannat Patel
6b6c8da785 Merge pull request #706 from pateljannat/evaluation-request-validations
fix: evaluation dates validation
2023-12-22 22:54:23 +05:30
Jannat Patel
f40fbaed3e fix: check country from ip for multicurrency 2023-12-21 14:22:58 +05:30
Jannat Patel
4973386dd0 Merge pull request #710 from pateljannat/usd-pricing
feat: usd pricing
2023-12-21 12:58:06 +05:30
Jannat Patel
13536b8bad feat: usd pricing 2023-12-21 12:28:11 +05:30
Jannat Patel
caea7e334c Merge pull request #707 from pateljannat/batch-meta-image
fix: batch meta image saving
2023-12-21 09:56:50 +05:30
Jannat Patel
b248774774 Merge pull request #708 from pateljannat/confirmation-email
fix: copy of enrolment email
2023-12-20 12:16:03 +05:30
Jannat Patel
7a9d6325d5 fix: copy of enrollment email 2023-12-20 11:49:32 +05:30
Jannat Patel
b0d0b41502 fix: batch meta image saving 2023-12-20 11:37:54 +05:30
Jannat Patel
30c89cb13c fix: evaluation dates validation 2023-12-20 11:00:09 +05:30
Jannat Patel
9175737b9c Merge pull request #705 from pateljannat/email-address-assignment-issue
fix: assignment issue
2023-12-19 11:49:53 +05:30
Jannat Patel
7ae772205a Merge pull request #704 from pateljannat/evaluation-fields
fix: evaluation fields
2023-12-19 11:15:32 +05:30
Jannat Patel
00b0a20c83 fix: translations 2023-12-19 10:54:56 +05:30
Jannat Patel
6604866342 fix: assignment issue 2023-12-19 10:51:36 +05:30
Jannat Patel
881c3d943a fix: evaluation fields 2023-12-18 20:06:43 +05:30
Jannat Patel
d5118cc91f fix: razorpay payment in other currency 2023-12-14 16:08:07 +05:30
Jannat Patel
ac74cbdf72 fix: razorpay payment in other currency 2023-12-14 15:53:07 +05:30
Jannat Patel
01f7fc3cff fix: razorpay payment in other currency 2023-12-14 15:12:23 +05:30
Jannat Patel
85c850e5bf fix: cohorts 2023-12-14 15:08:42 +05:30
Jannat Patel
67dfffdd58 fix: calendar day view range 2023-12-13 10:41:15 +05:30
Jannat Patel
ae4aadb8d3 Merge pull request #702 from pateljannat/timetable-mobile-view
feat: day view for timetable on mobile
2023-12-12 12:15:02 +05:30
Jannat Patel
e5dc2bad6a feat: day view for timetable on mobile 2023-12-12 12:00:24 +05:30
Jannat Patel
0e2fabf139 Merge pull request #700 from pateljannat/fix-milestones
fix: future date access on timetables for moderators
2023-12-08 16:06:38 +05:30
Jannat Patel
c45a372e83 fix: future date access on timetables for moderators 2023-12-08 15:27:33 +05:30
Jannat Patel
98ecb4c27c Merge pull request #699 from pateljannat/all-day-events-style
fix: All day events style
2023-12-07 17:37:44 +05:30
Jannat Patel
9023094326 fix: timetable style 2023-12-07 16:00:11 +05:30
Jannat Patel
497de05db2 fix: all day events style 2023-12-07 15:53:10 +05:30
Jannat Patel
cb3224664e Merge pull request #695 from pateljannat/all-day-events
feat: all day events
2023-11-30 12:12:09 +05:30
Jannat Patel
9b532a5470 fix: editing evaluation end date from lms portal 2023-11-30 12:02:43 +05:30
Jannat Patel
f1f9d9790b feat: all day events 2023-11-30 11:51:36 +05:30
Jannat Patel
96190910a7 Merge pull request #694 from pateljannat/evaluation-end-date
feat: evaluation end date
2023-11-29 22:58:53 +05:30
Jannat Patel
6484763d37 fix: removed additional roles 2023-11-29 22:32:52 +05:30
Jannat Patel
6f1e7624ec feat:evaluation end date from lms portal 2023-11-29 18:01:44 +05:30
Jannat Patel
eef5bd6062 feat: evaluation end date 2023-11-29 17:36:34 +05:30
Md Hussain Nagaria
de60fbb25a fix(LMS Batch): add portal web link to form view (#692) 2023-11-27 22:19:07 +05:30
Jannat Patel
fd9a638879 fix: quiz and timetable issues 2023-11-24 12:37:34 +05:30
Jannat Patel
ddcb718a3a fix: quiz submission questions 2023-11-23 11:50:22 +05:30
Jannat Patel
a17a7453e7 fix: ceil the percentage 2023-11-22 17:49:47 +05:30
Jannat Patel
479be0b8ee Merge pull request #681 from pateljannat/notify-mentions
feat: misc changes
2023-11-22 17:34:44 +05:30
Jannat Patel
6f40c357b3 fix: quiz submission page rendering 2023-11-22 17:27:42 +05:30
Jannat Patel
81db6c544d fix: mention email link 2023-11-22 13:04:04 +05:30
Jannat Patel
be4e3aa963 Merge pull request #682 from pateljannat/course-creation-error
fix: course creation issue
2023-11-21 14:38:27 +05:30
Jannat Patel
6da0c07a3d fix: course creation issue 2023-11-21 14:13:11 +05:30
Jannat Patel
b4ad10ca35 Merge pull request #680 from pateljannat/lesson-creation-issue
fix: encode chapter during lesson creation
2023-11-21 13:46:07 +05:30
Jannat Patel
2388b878dc fix: encode chapter during lesson creation 2023-11-21 13:35:47 +05:30
Jannat Patel
8cdaa7877a feat: discussions mention notifications 2023-11-21 13:32:12 +05:30
Jannat Patel
d314287883 Merge pull request #679 from pateljannat/upcoming-batch
fix: upcoming batches based on start time
2023-11-17 11:00:07 +05:30
Jannat Patel
b70dfc8e82 fix: upcoming batches based on start time 2023-11-17 10:45:29 +05:30
Jannat Patel
a5a7184f9a Merge pull request #676 from pateljannat/pyproject
build: added pyproject.toml
2023-11-09 15:54:12 +05:30
Jannat Patel
4e019d0a43 Merge pull request #677 from pateljannat/get-certificate-issue
fix: get certificate button visibility
2023-11-09 15:50:35 +05:30
Jannat Patel
8453b54360 fix: syntax of pyproject 2023-11-09 15:46:51 +05:30
Jannat Patel
9f9dfdb26d fix: get certificate button visibility 2023-11-09 15:27:46 +05:30
Jannat Patel
9fd4984247 fix: get certificate button visibility 2023-11-09 15:22:05 +05:30
Jannat Patel
9ebd64f47d build: added pyproject.toml 2023-11-09 15:18:06 +05:30
Jannat Patel
4316a37ed6 Merge pull request #675 from pateljannat/certificate-fix
fix: certificates in profile
2023-11-09 12:58:47 +05:30
Jannat Patel
2d745460e8 fix: certificates in profile 2023-11-09 12:29:23 +05:30
Jannat Patel
b5258b6d9f Merge pull request #674 from pateljannat/assignment-fix
fix: assignment submission email
2023-11-07 12:05:21 +05:30
Jannat Patel
41b076c0db fix: user type filter on assignment submission 2023-11-07 11:42:31 +05:30
Jannat Patel
9d65e5e398 Merge pull request #673 from rtdany10/total-mark-issue
fix: mandatory total marks issue
2023-11-07 11:41:34 +05:30
Jannat Patel
7250bf7d65 fix: changed recipient in assignment submission email 2023-11-07 11:32:26 +05:30
Jannat Patel
4d7b247378 fix: assignment submission email 2023-11-07 11:31:58 +05:30
Dany Robert
0aaa58cd54 fix: mandatory total marks issue 2023-11-07 04:48:32 +00:00
Jannat Patel
014b85f12c Merge pull request #672 from pateljannat/user-category-default
fix: default user category
2023-11-06 19:14:19 +05:30
Jannat Patel
929f97cb72 test: skip invite email if in test 2023-11-06 19:08:30 +05:30
Jannat Patel
de9cb935ee test: skip certificate email if in test 2023-11-06 18:58:12 +05:30
Jannat Patel
9aafc176e4 fix: default user category 2023-11-06 18:39:52 +05:30
Jannat Patel
0488ae8305 Merge pull request #670 from pateljannat/cert-fixes
fix: certificate border and email
2023-11-02 12:29:55 +05:30
Jannat Patel
60fd317d98 feat: certification email 2023-11-02 12:20:11 +05:30
Jannat Patel
e54435d85d fix: certificate border 2023-11-01 18:27:17 +05:30
Jannat Patel
3a23b91c90 Merge pull request #669 from pateljannat/batch-tabs-customisation
feat: batch tabs settings
2023-10-30 18:42:56 +05:30
Jannat Patel
69591577bf feat: batch tabs settings 2023-10-30 18:30:58 +05:30
Jannat Patel
e56afba6d3 Merge pull request #645 from tundebabzy/644
fix: 10th lesson access issue
2023-10-27 18:45:00 +05:30
Jannat Patel
98536ce4c7 Merge pull request #668 from frappe/pateljannat-security-md
chore: created security policy
2023-10-27 18:36:39 +05:30
Jannat Patel
05282178dd fix: removed functional programing code 2023-10-27 18:30:45 +05:30
Jannat Patel
1af547288c chore: fix linters 2023-10-27 17:53:35 +05:30
Jannat Patel
b4af82acbc chore: fix linters 2023-10-27 17:21:28 +05:30
Jannat Patel
50fbe00d23 Merge pull request #667 from pateljannat/batch-ic
fix: misc batch issues
2023-10-27 17:18:20 +05:30
Jannat Patel
b44428677e chore: created security policy 2023-10-27 17:17:10 +05:30
Tunde Akinyanmi
d67faa1610 forgot to remove the LessonBookmark class 2023-10-27 12:05:37 +01:00
Tunde Akinyanmi
7b3f4c29d8 remove LessonBookmark abstraction. 2023-10-27 12:00:44 +01:00
Jannat Patel
a49871c5b1 fix: misc batch issues 2023-10-27 16:04:03 +05:30
Jannat Patel
e4005792af Merge pull request #665 from saadchaudharry/main
Fix:timetable validation
2023-10-27 12:04:36 +05:30
saadindictrans
8c0c09a21b Fix:timetable validation 2023-10-27 11:33:18 +05:30
Jannat Patel
a9b05f4256 Merge pull request #662 from pateljannat/batch-source
feat: batch source
2023-10-26 20:35:57 +05:30
Jannat Patel
cb6013a7a6 fix: source doctype name during install 2023-10-26 18:09:05 +05:30
Jannat Patel
bb23b78a4f fix: made source mandatory in billing form 2023-10-26 18:00:11 +05:30
Jannat Patel
243277012f feat: batch source 2023-10-26 17:51:43 +05:30
Jannat Patel
c9ed8a4b03 Merge pull request #661 from pateljannat/fix-eval-slot
fix: evaluation slots
2023-10-26 15:25:55 +05:30
Jannat Patel
d413acaef3 fix: evaluation slots 2023-10-26 15:14:35 +05:30
Jannat Patel
d6aad6cd74 Merge branch 'main' into 644 2023-10-26 14:45:56 +05:30
Jannat Patel
ca45e43003 Merge pull request #658 from pateljannat/certification-fix
fix: certificate download template
2023-10-26 12:52:14 +05:30
Jannat Patel
ad39530705 fix: certificate download template 2023-10-26 11:30:26 +05:30
Jannat Patel
a6c2378b56 fix: certificate template pathc 2023-10-25 14:25:42 +05:30
Jannat Patel
c073d2201d Merge pull request #657 from pateljannat/course-certificates
feat: certificate template
2023-10-25 14:15:54 +05:30
Jannat Patel
6d70de2eb1 feat: certificate template 2023-10-25 13:08:56 +05:30
Jannat Patel
48982e8f4a Merge pull request #655 from pateljannat/append-student-email
fix: Append student email to batch
2023-10-25 11:47:54 +05:30
Jannat Patel
397128f980 chore: merged conflicts 2023-10-25 11:36:07 +05:30
Jannat Patel
1d77fd3f94 Merge pull request #654 from pateljannat/timetable-milestones
feat: timetable milestones
2023-10-25 11:28:56 +05:30
Jannat Patel
60e78e8e74 feat: emails tab 2023-10-23 20:12:48 +05:30
Jannat Patel
4a9ccc6fde fix: cc in student email 2023-10-23 19:14:59 +05:30
Jannat Patel
a707095fae fix: link student emails to batch 2023-10-23 19:11:36 +05:30
Jannat Patel
d4f662f65e feat: timetable milestones 2023-10-23 16:35:35 +05:30
Jannat Patel
509b1365d9 Merge pull request #646 from pateljannat/quiz-refactor
feat: Quiz Refactor
2023-10-20 11:31:31 +05:30
Jannat Patel
d0b236e381 fix: translations 2023-10-20 11:08:10 +05:30
Jannat Patel
fe98265636 test: fix quiz tests 2023-10-20 10:56:47 +05:30
Jannat Patel
3f7d1b1e83 feat: add questions from LMS Portal 2023-10-19 22:15:30 +05:30
Jannat Patel
52cde329c1 Merge branch 'quiz-refactor' of https://github.com/pateljannat/lms into quiz-refactor 2023-10-18 23:21:57 +05:30
Jannat Patel
68b2dd6147 feat: quiz question from UI 2023-10-18 23:21:49 +05:30
Jannat Patel
5fa0d022dc Merge branch 'main' into quiz-refactor 2023-10-18 15:47:58 +05:30
Jannat Patel
d996a5c53f Merge pull request #649 from NagariaHussain/feat-reply-to
feat: add reply_to in email students dialog
2023-10-18 12:35:46 +05:30
Jannat Patel
b6dfc6ed4d Merge pull request #650 from NagariaHussain/feat-payment-info
feat: store batch/course in LMS Payment
2023-10-18 12:35:33 +05:30
Jannat Patel
c7c2ba83f3 Merge branch 'main' into feat-payment-info 2023-10-18 12:12:19 +05:30
Hussain Nagaria
2bffabff05 style: fix linter 2023-10-18 12:11:23 +05:30
Hussain Nagaria
697e81df10 feat: add reply_to in email students 2023-10-18 12:11:23 +05:30
Jannat Patel
f1b791845b Merge pull request #651 from pateljannat/editorjs-para
chore: bumped down paragraph plugin of editor js
2023-10-18 11:58:26 +05:30
Jannat Patel
6310845cdd chore: bunped down paragraph plugin of editor js 2023-10-18 11:47:15 +05:30
Hussain Nagaria
230cca63f3 feat: store batch/course in LMS payment 2023-10-17 23:18:43 +05:30
Hussain Nagaria
af9f4d4b1e fix: remove unused import 2023-10-17 22:17:28 +05:30
Jannat Patel
0111ff9c99 feat: quiz marks and passing percentage 2023-10-17 20:06:04 +05:30
Jannat Patel
12bec14c92 feat: quiz validations and marks 2023-10-16 19:52:36 +05:30
Jannat Patel
174ea1ddd4 chore: resolved conflicts 2023-10-16 11:19:34 +05:30
Tunde Akinyanmi
038a7463e1 update get_neighbours to use LessonBookmark and
return the correct bookmark string
2023-10-13 18:39:11 +01:00
Tunde Akinyanmi
a702909216 add LessonBookmark which is a data structure that
represents a bookmark.

While the underlying data structure is a tuple, it makes it easy to
abstract most of the logic we need and therefore allow the code to be
more readable.
2023-10-13 18:32:15 +01:00
Tunde Akinyanmi
8effd5614f remove cast operation from str to float.
It cause loss of the bookmark data
2023-10-13 18:14:11 +01:00
Jannat Patel
1046d28092 feat: quiz refactor 2023-10-11 12:25:46 +05:30
117 changed files with 2625 additions and 1016 deletions

View File

@@ -1,6 +1,6 @@
<p align="center"> <p align="center">
<a href="https://www.frappelms.com/"> <a href="https://www.frappelms.com/">
<img src="https://frappelms.com/files/lms-logo-medium.png" alt="Frappe LMS" width="120px" height="25px"> <img src="https://frappe.io/files/lms.png" alt="Frappe LMS" width="50px" height="50px">
</a> </a>
<p align="center">Easy to use, open source, learning management system.</p> <p align="center">Easy to use, open source, learning management system.</p>
</p> </p>

5
SECURITY.md Normal file
View File

@@ -0,0 +1,5 @@
# Security Policy
The Frappe team and community take security issues seriously. To report a security issue, please go through the information mentioned [here](https://frappe.io/security).
We appreciate your efforts to responsibly disclose your findings. We'll endeavor to respond quickly and will keep you updated throughout the process.

View File

@@ -13,6 +13,6 @@ module.exports = defineConfig({
openMode: 0, openMode: 0,
}, },
e2e: { e2e: {
baseUrl: "http://dd1:8000", baseUrl: "http://pyp:8000",
}, },
}); });

View File

@@ -4,6 +4,8 @@
$ git clone https://github.com/frappe/lms.git $ git clone https://github.com/frappe/lms.git
$ cd lms $ cd lms
$ cd docker
``` ```
**Step 2:** Run docker-compose **Step 2:** Run docker-compose

1
frappe-ui Submodule

Submodule frappe-ui added at 2898a0bdd1

View File

@@ -138,12 +138,12 @@
"label": "User Category", "label": "User Category",
"length": 0, "length": 0,
"mandatory_depends_on": null, "mandatory_depends_on": null,
"modified": "2022-04-19 13:02:18.219508", "modified": "2022-04-19 13:02:18.219510",
"module": "LMS", "module": "LMS",
"name": "User-user_category", "name": "User-user_category",
"no_copy": 0, "no_copy": 0,
"non_negative": 0, "non_negative": 0,
"options": "Business Owner\nManager (Sales/Marketing/Customer)\nEmployee\nStudent\nFreelancer/Just looking\nOthers", "options": "\nBusiness Owner\nManager (Sales/Marketing/Customer)\nEmployee\nStudent\nFreelancer/Just looking\nOthers",
"permlevel": 0, "permlevel": 0,
"precision": "", "precision": "",
"print_hide": 0, "print_hide": 0,

View File

@@ -97,8 +97,7 @@ override_doctype_class = {
# Hook on document methods and events # Hook on document methods and events
doc_events = { doc_events = {
"Discussion Reply": {"after_insert": "lms.lms.utils.create_notification_log"}, "Discussion Reply": {"after_insert": "lms.lms.utils.handle_notifications"},
"Course Lesson": {"on_update": "lms.lms.doctype.lms_quiz.lms_quiz.update_lesson_info"},
} }
# Scheduled Tasks # Scheduled Tasks
@@ -119,9 +118,9 @@ fixtures = ["Custom Field", "Function", "Industry"]
# Overriding Methods # Overriding Methods
# ------------------------------ # ------------------------------
# #
# override_whitelisted_methods = { override_whitelisted_methods = {
# "frappe.desk.doctype.event.event.get_events": "lms.event.get_events" # "frappe.desk.search.get_names_for_mentions": "lms.lms.utils.get_names_for_mentions",
# } }
# #
# each overriding function accepts a `data` argument; # each overriding function accepts a `data` argument;
# generated from the base implementation of the doctype dashboard, # generated from the base implementation of the doctype dashboard,
@@ -174,7 +173,8 @@ website_route_rules = [
"to_route": "cohorts/join", "to_route": "cohorts/join",
}, },
{"from_route": "/users", "to_route": "profiles/profile"}, {"from_route": "/users", "to_route": "profiles/profile"},
{"from_route": "/jobs/<job>", "to_route": "jobs/job"}, {"from_route": "/job-openings", "to_route": "jobs_openings/index"},
{"from_route": "/job-openings/<job>", "to_route": "jobs_openings/job"},
{ {
"from_route": "/batches/<batchname>/students/<username>", "from_route": "/batches/<batchname>/students/<username>",
"to_route": "/batches/progress", "to_route": "/batches/progress",

View File

@@ -4,11 +4,11 @@ from frappe.desk.page.setup_wizard.setup_wizard import add_all_roles_to
def after_install(): def after_install():
add_pages_to_nav() add_pages_to_nav()
create_batch_source()
def after_sync(): def after_sync():
create_lms_roles() create_lms_roles()
set_default_home()
set_default_certificate_print_format() set_default_certificate_print_format()
add_all_roles_to("Administrator") add_all_roles_to("Administrator")
@@ -19,7 +19,7 @@ def add_pages_to_nav():
{"label": "Courses", "url": "/courses", "parent": "Explore", "idx": 2}, {"label": "Courses", "url": "/courses", "parent": "Explore", "idx": 2},
{"label": "Batches", "url": "/batches", "parent": "Explore", "idx": 3}, {"label": "Batches", "url": "/batches", "parent": "Explore", "idx": 3},
{"label": "Statistics", "url": "/statistics", "parent": "Explore", "idx": 4}, {"label": "Statistics", "url": "/statistics", "parent": "Explore", "idx": 4},
{"label": "Jobs", "url": "/jobs", "parent": "Explore", "idx": 5}, {"label": "Jobs", "url": "/job-openings", "parent": "Explore", "idx": 5},
{"label": "People", "url": "/community", "parent": "Explore", "idx": 6}, {"label": "People", "url": "/community", "parent": "Explore", "idx": 6},
] ]
@@ -64,10 +64,6 @@ def delete_lms_roles():
frappe.db.delete("Role", role) frappe.db.delete("Role", role)
def set_default_home():
frappe.db.set_single_value("Portal Settings", "default_portal_home", "/courses")
def create_course_creator_role(): def create_course_creator_role():
if not frappe.db.exists("Role", "Course Creator"): if not frappe.db.exists("Role", "Course Creator"):
role = frappe.get_doc( role = frappe.get_doc(
@@ -182,3 +178,20 @@ def delete_custom_fields():
for field in fields: for field in fields:
frappe.db.delete("Custom Field", {"fieldname": field}) frappe.db.delete("Custom Field", {"fieldname": field})
def create_batch_source():
sources = [
"Newsletter",
"LinkedIn",
"Twitter",
"Website",
"Friend/Colleague/Connection",
"Google Search",
]
for source in sources:
if not frappe.db.exists("LMS Source", source):
doc = frappe.new_doc("LMS Source")
doc.source = source
doc.save()

View File

@@ -4,6 +4,6 @@
frappe.ui.form.on("Job Opportunity", { frappe.ui.form.on("Job Opportunity", {
refresh: (frm) => { refresh: (frm) => {
if (frm.doc.name) if (frm.doc.name)
frm.add_web_link(`/jobs/${frm.doc.name}`, "See on Website"); frm.add_web_link(`/job-openings/${frm.doc.name}`, "See on Website");
}, },
}); });

View File

@@ -1,7 +1,7 @@
frappe.ready(function () { frappe.ready(function () {
frappe.web_form.after_save = () => { frappe.web_form.after_save = () => {
setTimeout(() => { setTimeout(() => {
window.location.href = `/jobs`; window.location.href = `/job-openings`;
}); });
}; };
}); });

View File

@@ -20,7 +20,7 @@
"list_columns": [], "list_columns": [],
"login_required": 1, "login_required": 1,
"max_attachment_size": 0, "max_attachment_size": 0,
"modified": "2022-09-15 17:22:43.957184", "modified": "2022-09-15 17:22:43.957185",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Job", "module": "Job",
"name": "job-opportunity", "name": "job-opportunity",
@@ -32,7 +32,7 @@
"show_list": 1, "show_list": 1,
"show_sidebar": 0, "show_sidebar": 0,
"success_message": "", "success_message": "",
"success_url": "/jobs", "success_url": "/job-openings",
"title": "Job Opportunity", "title": "Job Opportunity",
"web_form_fields": [ "web_form_fields": [
{ {

View File

@@ -9,11 +9,12 @@
"field_order": [ "field_order": [
"student_details_section", "student_details_section",
"student", "student",
"payment",
"confirmation_email_sent",
"column_break_oduu",
"student_name", "student_name",
"username" "username",
"column_break_oduu",
"payment",
"source",
"confirmation_email_sent"
], ],
"fields": [ "fields": [
{ {
@@ -59,12 +60,18 @@
"fieldname": "confirmation_email_sent", "fieldname": "confirmation_email_sent",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Confirmation Email Sent" "label": "Confirmation Email Sent"
},
{
"fieldname": "source",
"fieldtype": "Link",
"label": "Source",
"options": "LMS Source"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-10-09 17:09:50.481794", "modified": "2023-10-26 16:52:04.266693",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "Batch Student", "name": "Batch Student",

View File

@@ -1,9 +1,23 @@
# Copyright (c) 2022, Frappe and contributors # Copyright (c) 2022, Frappe and contributors
# For license information, please see license.txt # For license information, please see license.txt
# import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
class BatchStudent(Document): class BatchStudent(Document):
pass pass
@frappe.whitelist()
def enroll_batch(batch_name):
if frappe.db.exists(
"Batch Student", {"student": frappe.session.user, "parent": batch_name}
):
frappe.throw("You are already enrolled in this batch")
enrollment = frappe.new_doc("Batch Student")
enrollment.student = frappe.session.user
enrollment.parent = batch_name
enrollment.parentfield = "students"
enrollment.parenttype = "LMS Batch"
enrollment.save(ignore_permissions=True)

View File

@@ -99,8 +99,14 @@ def save_progress(lesson, course, status):
quizzes = [value for name, value in macros if name == "Quiz"] quizzes = [value for name, value in macros if name == "Quiz"]
for quiz in quizzes: for quiz in quizzes:
passing_percentage = frappe.db.get_value("LMS Quiz", quiz, "passing_percentage")
if not frappe.db.exists( if not frappe.db.exists(
"LMS Quiz Submission", {"quiz": quiz, "owner": frappe.session.user} "LMS Quiz Submission",
{
"quiz": quiz,
"owner": frappe.session.user,
"percentage": [">=", passing_percentage],
},
): ):
return 0 return 0

View File

@@ -11,7 +11,11 @@ from frappe.utils.password import get_decrypted_password
class InviteRequest(Document): class InviteRequest(Document):
def on_update(self): def on_update(self):
if self.has_value_changed("status") and self.status == "Approved": if (
self.has_value_changed("status")
and self.status == "Approved"
and not frappe.flags.in_test
):
self.send_email() self.send_email()
def create_user(self, password): def create_user(self, password):

View File

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

View File

@@ -4,13 +4,18 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import validate_url from frappe.utils import validate_url, validate_email_address
from frappe.email.doctype.email_template.email_template import get_email_template
class LMSAssignmentSubmission(Document): class LMSAssignmentSubmission(Document):
def validate(self): def validate(self):
self.validate_duplicates() self.validate_duplicates()
def after_insert(self):
if not frappe.flags.in_test:
self.send_mail()
def validate_duplicates(self): def validate_duplicates(self):
if frappe.db.exists( if frappe.db.exists(
"LMS Assignment Submission", "LMS Assignment Submission",
@@ -23,6 +28,38 @@ class LMSAssignmentSubmission(Document):
) )
) )
def send_mail(self):
subject = _("New Assignment Submission")
template = "assignment_submission"
custom_template = frappe.db.get_single_value(
"LMS Settings", "assignment_submission_template"
)
args = {
"member_name": self.member_name,
"assignment_name": self.assignment,
"assignment_title": self.assignment_title,
"submission_name": self.name,
}
moderators = frappe.get_all("Has Role", {"role": "Moderator"}, pluck="parent")
for moderator in moderators:
if not validate_email_address(moderator):
moderators.remove(moderator)
if custom_template:
email_template = get_email_template(custom_template, args)
subject = email_template.get("subject")
content = email_template.get("message")
frappe.sendmail(
recipients=moderators,
subject=subject,
template=template if not custom_template else None,
content=content if custom_template else None,
args=args,
header=[subject, "green"],
)
@frappe.whitelist() @frappe.whitelist()
def upload_assignment( def upload_assignment(

View File

@@ -28,11 +28,19 @@ frappe.ui.form.on("LMS Batch", {
}, },
}; };
}); });
if (frm.doc.timetable.length && !frm.doc.timetable_legends.length) {
set_default_legends(frm);
}
}, },
timetable_template: function (frm) { timetable_template: function (frm) {
set_timetable(frm); set_timetable(frm);
}, },
refresh: (frm) => {
frm.add_web_link(`/batches/details/${frm.doc.name}`, "See on website");
},
}); });
const set_timetable = (frm) => { const set_timetable = (frm) => {
@@ -52,6 +60,7 @@ const set_timetable = (frm) => {
"start_time", "start_time",
"end_time", "end_time",
"duration", "duration",
"milestone",
], ],
filters: { filters: {
parent: frm.doc.timetable_template, parent: frm.doc.timetable_template,
@@ -82,6 +91,7 @@ const add_timetable_rows = (frm, timetable) => {
.format("HH:mm") .format("HH:mm")
: null; : null;
child.duration = row.duration; child.duration = row.duration;
child.milestone = row.milestone;
}); });
frm.refresh_field("timetable"); frm.refresh_field("timetable");
@@ -121,3 +131,37 @@ const add_legend_rows = (frm, legends) => {
frm.refresh_field("timetable_legends"); frm.refresh_field("timetable_legends");
frm.save(); frm.save();
}; };
const set_default_legends = (frm) => {
const data = [
{
reference_doctype: "Course Lesson",
label: "Lesson",
color: "#449CF0",
},
{
reference_doctype: "LMS Quiz",
label: "LMS Quiz",
color: "#39E4A5",
},
{
reference_doctype: "LMS Assignment",
label: "LMS Assignment",
color: "#ECAD4B",
},
{
reference_doctype: "LMS Live Class",
label: "LMS Live Class",
color: "#bb8be8",
},
];
data.forEach((detail) => {
let child = frm.add_child("timetable_legends");
child.reference_doctype = detail.reference_doctype;
child.label = detail.label;
child.color = detail.color;
});
frm.refresh_field("timetable_legends");
frm.save();
};

View File

@@ -15,11 +15,13 @@
"start_time", "start_time",
"end_time", "end_time",
"published", "published",
"allow_self_enrollment",
"section_break_rgfj", "section_break_rgfj",
"medium", "medium",
"category", "category",
"column_break_flwy", "column_break_flwy",
"seat_count", "seat_count",
"evaluation_end_date",
"section_break_6", "section_break_6",
"description", "description",
"batch_details_raw", "batch_details_raw",
@@ -45,6 +47,7 @@
"column_break_iens", "column_break_iens",
"amount", "amount",
"currency", "currency",
"amount_usd",
"customisations_tab", "customisations_tab",
"section_break_ubxi", "section_break_ubxi",
"custom_component", "custom_component",
@@ -120,12 +123,14 @@
{ {
"fieldname": "start_time", "fieldname": "start_time",
"fieldtype": "Time", "fieldtype": "Time",
"label": "Start Time" "label": "Start Time",
"reqd": 1
}, },
{ {
"fieldname": "end_time", "fieldname": "end_time",
"fieldtype": "Time", "fieldtype": "Time",
"label": "End Time" "label": "End Time",
"reqd": 1
}, },
{ {
"fieldname": "assessment_tab", "fieldname": "assessment_tab",
@@ -277,11 +282,29 @@
"fieldname": "allow_future", "fieldname": "allow_future",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Allow accessing future dates" "label": "Allow accessing future dates"
},
{
"fieldname": "evaluation_end_date",
"fieldtype": "Date",
"label": "Evaluation End Date"
},
{
"depends_on": "paid_batch",
"description": "If you set an amount here, then the USD equivalent setting will not get applied.",
"fieldname": "amount_usd",
"fieldtype": "Currency",
"label": "Amount (USD)"
},
{
"default": "0",
"fieldname": "allow_self_enrollment",
"fieldtype": "Check",
"label": "Allow Self Enrollment"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2023-10-12 12:53:37.351989", "modified": "2024-01-22 10:42:42.872995",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Batch", "name": "LMS Batch",

View File

@@ -12,9 +12,7 @@ from frappe.utils import (
cint, cint,
format_date, format_date,
format_datetime, format_datetime,
add_to_date, get_time,
getdate,
get_datetime,
) )
from lms.lms.utils import get_lessons, get_lesson_index, get_lesson_url 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 lms.www.utils import get_quiz_details, get_assignment_details
@@ -31,6 +29,7 @@ class LMSBatch(Document):
self.validate_membership() self.validate_membership()
self.validate_timetable() self.validate_timetable()
self.send_confirmation_mail() self.send_confirmation_mail()
self.validate_evaluation_end_date()
def validate_duplicate_students(self): def validate_duplicate_students(self):
students = [row.student for row in self.students] students = [row.student for row in self.students]
@@ -66,11 +65,14 @@ class LMSBatch(Document):
def send_confirmation_mail(self): def send_confirmation_mail(self):
for student in self.students: for student in self.students:
if not student.confirmation_email_sent: if not student.confirmation_email_sent:
self.send_mail(student) self.send_mail(student)
student.confirmation_email_sent = 1 student.confirmation_email_sent = 1
def validate_evaluation_end_date(self):
if self.evaluation_end_date and self.evaluation_end_date < self.end_date:
frappe.throw(_("Evaluation end date cannot be less than the batch end date."))
def send_mail(self, student): def send_mail(self, student):
subject = _("Enrollment Confirmation for the Next Training Batch") subject = _("Enrollment Confirmation for the Next Training Batch")
template = "batch_confirmation" template = "batch_confirmation"
@@ -119,23 +121,27 @@ class LMSBatch(Document):
def validate_timetable(self): def validate_timetable(self):
for schedule in self.timetable: for schedule in self.timetable:
if schedule.start_time and schedule.end_time: if schedule.start_time and schedule.end_time:
if ( if get_time(schedule.start_time) > get_time(schedule.end_time) or get_time(
schedule.start_time > schedule.end_time or schedule.start_time == schedule.end_time schedule.start_time
): ) == get_time(schedule.end_time):
frappe.throw( frappe.throw(
_("Row #{0} Start time cannot be greater than or equal to end time.").format( _("Row #{0} Start time cannot be greater than or equal to end time.").format(
schedule.idx schedule.idx
) )
) )
if schedule.start_time < self.start_time or schedule.start_time > self.end_time: if get_time(schedule.start_time) < get_time(self.start_time) or get_time(
schedule.start_time
) > get_time(self.end_time):
frappe.throw( frappe.throw(
_("Row #{0} Start time cannot be outside the batch duration.").format( _("Row #{0} Start time cannot be outside the batch duration.").format(
schedule.idx schedule.idx
) )
) )
if schedule.end_time < self.start_time or schedule.end_time > self.end_time: if get_time(schedule.end_time) < get_time(self.start_time) or get_time(
schedule.end_time
) > get_time(self.end_time):
frappe.throw( frappe.throw(
_("Row #{0} End time cannot be outside the batch duration.").format(schedule.idx) _("Row #{0} End time cannot be outside the batch duration.").format(schedule.idx)
) )
@@ -250,8 +256,10 @@ def create_batch(
paid_batch=0, paid_batch=0,
amount=0, amount=0,
currency=None, currency=None,
amount_usd=0,
name=None, name=None,
published=0, published=0,
evaluation_end_date=None,
): ):
frappe.only_for("Moderator") frappe.only_for("Moderator")
if name: if name:
@@ -267,7 +275,7 @@ def create_batch(
"description": description, "description": description,
"batch_details": batch_details, "batch_details": batch_details,
"batch_details_raw": batch_details_raw, "batch_details_raw": batch_details_raw,
"image": meta_image, "meta_image": meta_image,
"seat_count": seat_count, "seat_count": seat_count,
"start_time": start_time, "start_time": start_time,
"end_time": end_time, "end_time": end_time,
@@ -276,7 +284,9 @@ def create_batch(
"paid_batch": paid_batch, "paid_batch": paid_batch,
"amount": amount, "amount": amount,
"currency": currency, "currency": currency,
"amount_usd": amount_usd,
"published": published, "published": published,
"evaluation_end_date": evaluation_end_date,
} }
) )
doc.save() doc.save()
@@ -325,7 +335,17 @@ def get_batch_timetable(batch):
timetable = frappe.get_all( timetable = frappe.get_all(
"LMS Batch Timetable", "LMS Batch Timetable",
filters={"parent": batch}, filters={"parent": batch},
fields=["reference_doctype", "reference_docname", "date", "start_time", "end_time"], fields=[
"reference_doctype",
"reference_docname",
"date",
"start_time",
"end_time",
"milestone",
"name",
"idx",
"parent",
],
order_by="date", order_by="date",
) )
@@ -362,20 +382,26 @@ def get_timetable_details(timetable):
assessment = frappe._dict({"assessment_name": entry.reference_docname}) assessment = frappe._dict({"assessment_name": entry.reference_docname})
if entry.reference_doctype == "Course Lesson": if entry.reference_doctype == "Course Lesson":
entry.icon = "icon-list"
course = frappe.db.get_value( course = frappe.db.get_value(
entry.reference_doctype, entry.reference_docname, "course" entry.reference_doctype, entry.reference_docname, "course"
) )
entry.url = get_lesson_url(course, get_lesson_index(entry.reference_docname)) entry.url = get_lesson_url(course, get_lesson_index(entry.reference_docname))
entry.completed = (
True
if frappe.db.exists(
"LMS Course Progress",
{"lesson": entry.reference_docname, "member": frappe.session.user},
)
else False
)
elif entry.reference_doctype == "LMS Quiz": elif entry.reference_doctype == "LMS Quiz":
entry.icon = "icon-quiz"
entry.url = "/quizzes" entry.url = "/quizzes"
details = get_quiz_details(assessment, frappe.session.user) details = get_quiz_details(assessment, frappe.session.user)
entry.update(details) entry.update(details)
elif entry.reference_doctype == "LMS Assignment": elif entry.reference_doctype == "LMS Assignment":
entry.icon = "icon-quiz"
details = get_assignment_details(assessment, frappe.session.user) details = get_assignment_details(assessment, frappe.session.user)
entry.update(details) entry.update(details)
@@ -384,12 +410,37 @@ def get_timetable_details(timetable):
@frappe.whitelist() @frappe.whitelist()
def send_email_to_students(batch, subject, reply_to, message): def is_milestone_complete(idx, batch):
frappe.only_for("Moderator") previous_rows = frappe.get_all(
students = frappe.get_all("Batch Student", {"parent": batch}, pluck="student") "LMS Batch Timetable",
frappe.sendmail( filters={"parent": batch, "idx": ["<", cint(idx)]},
recipients=students, fields=["reference_doctype", "reference_docname", "idx"],
subject=subject, order_by="idx",
reply_to=reply_to,
message=message
) )
for row in previous_rows:
if row.reference_doctype == "Course Lesson":
if not frappe.db.exists(
"LMS Course Progress",
{"member": frappe.session.user, "lesson": row.reference_docname},
):
return False
if row.reference_doctype == "LMS Quiz":
passing_percentage = frappe.db.get_value(
row.reference_doctype, row.reference_docname, "passing_percentage"
)
if not frappe.db.exists(
"LMS Quiz Submission",
{"quiz": row.reference_docname, "member": frappe.session.user},
):
return False
if row.reference_doctype == "LMS Assignment":
if not frappe.db.exists(
"LMS Assignment Submission",
{"assignment": row.reference_docname, "member": frappe.session.user},
):
return False
return True

View File

@@ -16,7 +16,8 @@
"column_break_merq", "column_break_merq",
"start_time", "start_time",
"end_time", "end_time",
"duration" "duration",
"milestone"
], ],
"fields": [ "fields": [
{ {
@@ -69,12 +70,17 @@
"fieldname": "day", "fieldname": "day",
"fieldtype": "Int", "fieldtype": "Int",
"label": "Day" "label": "Day"
},
{
"fieldname": "milestone",
"fieldtype": "Check",
"label": "Milestone"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-10-03 17:40:31.530181", "modified": "2023-10-20 11:58:01.782921",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Batch Timetable", "name": "LMS Batch Timetable",

View File

@@ -10,6 +10,14 @@ frappe.ui.form.on("LMS Certificate", {
}, },
}; };
}); });
frm.set_query("template", function (doc) {
return {
filters: {
doc_type: "LMS Certificate",
},
};
});
}, },
refresh: (frm) => { refresh: (frm) => {
if (frm.doc.name) if (frm.doc.name)

View File

@@ -8,11 +8,12 @@
"course", "course",
"member", "member",
"member_name", "member_name",
"published", "template",
"column_break_3", "column_break_3",
"issue_date", "issue_date",
"expiry_date", "expiry_date",
"batch_name" "batch_name",
"published"
], ],
"fields": [ "fields": [
{ {
@@ -67,11 +68,18 @@
"fieldname": "published", "fieldname": "published",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Publish on Participant Page" "label": "Publish on Participant Page"
},
{
"fieldname": "template",
"fieldtype": "Link",
"label": "Template",
"options": "Print Format",
"reqd": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2023-09-13 11:03:23.479255", "modified": "2023-10-25 12:20:56.091979",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Certificate", "name": "LMS Certificate",

View File

@@ -6,12 +6,42 @@ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import add_years, nowdate from frappe.utils import add_years, nowdate
from lms.lms.utils import is_certified from lms.lms.utils import is_certified
from frappe.email.doctype.email_template.email_template import get_email_template
class LMSCertificate(Document): class LMSCertificate(Document):
def validate(self): def validate(self):
self.validate_duplicate_certificate() self.validate_duplicate_certificate()
def after_insert(self):
if not frappe.flags.in_test:
self.send_mail()
def send_mail(self):
subject = _("Congratulations on getting certified!")
template = "certification"
custom_template = frappe.db.get_single_value("LMS Settings", "certification_template")
args = {
"student_name": self.member_name,
"course_name": self.course,
"course_title": frappe.db.get_value("LMS Course", self.course, "title"),
"certificate_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=self.member,
subject=subject,
template=template if not custom_template else None,
content=content if custom_template else None,
args=args,
header=[subject, "green"],
)
def validate_duplicate_certificate(self): def validate_duplicate_certificate(self):
certificates = frappe.get_all( certificates = frappe.get_all(
"LMS Certificate", "LMS Certificate",
@@ -48,6 +78,15 @@ def create_certificate(course):
if expires_after_yrs: if expires_after_yrs:
expiry_date = add_years(nowdate(), expires_after_yrs) expiry_date = add_years(nowdate(), expires_after_yrs)
default_certificate_template = frappe.db.get_value(
"Property Setter",
{
"doc_type": "LMS Certificate",
"property": "default_print_format",
},
"value",
)
certificate = frappe.get_doc( certificate = frappe.get_doc(
{ {
"doctype": "LMS Certificate", "doctype": "LMS Certificate",
@@ -55,6 +94,7 @@ def create_certificate(course):
"course": course, "course": course,
"issue_date": nowdate(), "issue_date": nowdate(),
"expiry_date": expiry_date, "expiry_date": expiry_date,
"template": default_certificate_template,
} }
) )
certificate.save(ignore_permissions=True) certificate.save(ignore_permissions=True)

View File

@@ -47,12 +47,13 @@
"fieldtype": "Rating", "fieldtype": "Rating",
"in_list_view": 1, "in_list_view": 1,
"label": "Rating", "label": "Rating",
"reqd": 1 "mandatory_depends_on": "eval:doc.status != 'Pending' && doc.status != 'In Progress'"
}, },
{ {
"fieldname": "summary", "fieldname": "summary",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Summary" "label": "Summary",
"mandatory_depends_on": "eval:doc.status != 'Pending' && doc.status != 'In Progress'"
}, },
{ {
"fieldname": "date", "fieldname": "date",
@@ -106,7 +107,7 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2023-09-26 19:44:43.594892", "modified": "2023-12-18 20:03:27.040073",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Certificate Evaluation", "name": "LMS Certificate Evaluation",

View File

@@ -2,13 +2,19 @@
# For license information, please see license.txt # For license information, please see license.txt
import frappe import frappe
from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from lms.lms.utils import has_course_moderator_role from lms.lms.utils import has_course_moderator_role
class LMSCertificateEvaluation(Document): class LMSCertificateEvaluation(Document):
pass def validate(self):
self.validate_rating()
def validate_rating(self):
if self.status not in ["Pending", "In Progress"] and self.rating == 0:
frappe.throw(_("Rating cannot be 0"))
def has_website_permission(doc, ptype, user, verbose=False): def has_website_permission(doc, ptype, user, verbose=False):

View File

@@ -103,13 +103,13 @@
"fieldname": "batch_name", "fieldname": "batch_name",
"fieldtype": "Link", "fieldtype": "Link",
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Batch Name", "label": "Batch",
"options": "LMS Batch" "options": "LMS Batch"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2023-08-23 14:50:37.618352", "modified": "2023-11-29 15:00:30.617298",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Certificate Request", "name": "LMS Certificate Request",

View File

@@ -11,7 +11,20 @@ from lms.lms.utils import get_evaluator
class LMSCertificateRequest(Document): class LMSCertificateRequest(Document):
def validate(self): def validate(self):
self.validate_slot()
self.validate_if_existing_requests() self.validate_if_existing_requests()
self.validate_evaluation_end_date()
def validate_slot(self):
if frappe.db.exists(
"LMS Certificate Request",
{
"evaluator": self.evaluator,
"date": self.date,
"start_time": self.start_time,
},
):
frappe.throw(_("The slot is already booked by another participant."))
def validate_if_existing_requests(self): def validate_if_existing_requests(self):
existing_requests = frappe.get_all( existing_requests = frappe.get_all(
@@ -32,6 +45,20 @@ class LMSCertificateRequest(Document):
) )
) )
def validate_evaluation_end_date(self):
if self.batch_name:
evaluation_end_date = frappe.db.get_value(
"LMS Batch", self.batch_name, "evaluation_end_date"
)
if evaluation_end_date:
if getdate(self.date) > getdate(evaluation_end_date):
frappe.throw(
_("You cannot schedule evaluations after {0}.").format(
format_date(evaluation_end_date, "medium")
)
)
def schedule_evals(): def schedule_evals():
if frappe.db.get_single_value("LMS Settings", "send_calendar_invite_for_evaluations"): if frappe.db.get_single_value("LMS Settings", "send_calendar_invite_for_evaluations"):
@@ -104,7 +131,9 @@ def update_meeting_details(eval, event, calendar):
@frappe.whitelist() @frappe.whitelist()
def create_certificate_request(course, date, day, start_time, end_time, batch=None): def create_certificate_request(
course, date, day, start_time, end_time, batch_name=None
):
is_member = frappe.db.exists( is_member = frappe.db.exists(
{"doctype": "LMS Enrollment", "course": course, "member": frappe.session.user} {"doctype": "LMS Enrollment", "course": course, "member": frappe.session.user}
) )
@@ -115,13 +144,13 @@ def create_certificate_request(course, date, day, start_time, end_time, batch=No
eval.update( eval.update(
{ {
"course": course, "course": course,
"evaluator": get_evaluator(course, batch), "evaluator": get_evaluator(course, batch_name),
"member": frappe.session.user, "member": frappe.session.user,
"date": date, "date": date,
"day": day, "day": day,
"start_time": start_time, "start_time": start_time,
"end_time": end_time, "end_time": end_time,
"batch": batch, "batch_name": batch_name,
} }
) )
eval.save(ignore_permissions=True) eval.save(ignore_permissions=True)

View File

@@ -34,8 +34,10 @@
"related_courses", "related_courses",
"pricing_section", "pricing_section",
"paid_course", "paid_course",
"currency", "column_break_acoj",
"course_price", "course_price",
"currency",
"amount_usd",
"certification_section", "certification_section",
"enable_certification", "enable_certification",
"expiry", "expiry",
@@ -222,12 +224,22 @@
"fieldname": "course_price", "fieldname": "course_price",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Course Price", "label": "Course Price",
"option": "currency",
"mandatory_depends_on": "paid_course" "mandatory_depends_on": "paid_course"
}, },
{ {
"fieldname": "column_break_rxww", "fieldname": "column_break_rxww",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"fieldname": "column_break_acoj",
"fieldtype": "Column Break"
},
{
"depends_on": "paid_course",
"description": "If you set an amount here, then the USD equivalent setting will not get applied.",
"fieldname": "amount_usd",
"fieldtype": "Currency",
"label": "Amount (USD)"
} }
], ],
"is_published_field": "published", "is_published_field": "published",
@@ -254,7 +266,7 @@
} }
], ],
"make_attachments_public": 1, "make_attachments_public": 1,
"modified": "2023-08-28 11:09:11.945066", "modified": "2023-12-21 12:27:32.559901",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Course", "name": "LMS Course",

View File

@@ -1,8 +1,14 @@
// Copyright (c) 2023, Frappe and contributors // Copyright (c) 2023, Frappe and contributors
// For license information, please see license.txt // For license information, please see license.txt
// frappe.ui.form.on("LMS Payment", { frappe.ui.form.on("LMS Payment", {
// refresh(frm) { onload(frm) {
frm.set_query("member", function (doc) {
// }, return {
// }); filters: {
ignore_user_type: 1,
},
};
});
},
});

View File

@@ -8,8 +8,11 @@
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"payment_for_document_type",
"member", "member",
"source",
"column_break_rqkd", "column_break_rqkd",
"payment_for_document",
"billing_name", "billing_name",
"payment_received", "payment_received",
"payment_details_section", "payment_details_section",
@@ -115,11 +118,29 @@
"fieldname": "amount_with_gst", "fieldname": "amount_with_gst",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Amount with GST" "label": "Amount with GST"
},
{
"fieldname": "payment_for_document_type",
"fieldtype": "Select",
"label": "Payment for Document Type",
"options": "\nLMS Course\nLMS Batch"
},
{
"fieldname": "payment_for_document",
"fieldtype": "Dynamic Link",
"label": "Payment for Document",
"options": "payment_for_document_type"
},
{
"fieldname": "source",
"fieldtype": "Link",
"label": "Source",
"options": "LMS Source"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2023-09-12 10:40:22.721371", "modified": "2023-10-26 16:54:12.408274",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Payment", "name": "LMS Payment",

View File

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

View File

@@ -0,0 +1,245 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "format:QTS-{YYYY}-{#####}",
"creation": "2023-10-10 10:24:14.035772",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"question",
"type",
"multiple",
"section_break_ytxi",
"option_1",
"is_correct_1",
"column_break_fpvl",
"explanation_1",
"section_break_eiaa",
"option_2",
"is_correct_2",
"column_break_akwy",
"explanation_2",
"section_break_cwqv",
"option_3",
"is_correct_3",
"column_break_atpl",
"explanation_3",
"section_break_yqel",
"option_4",
"is_correct_4",
"column_break_lknb",
"explanation_4",
"section_break_hkfe",
"possibility_1",
"possibility_3",
"column_break_wpjr",
"possibility_2",
"possibility_4"
],
"fields": [
{
"fieldname": "question",
"fieldtype": "Text Editor",
"in_list_view": 1,
"label": "Question"
},
{
"fieldname": "type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Type",
"options": "Choices\nUser Input"
},
{
"depends_on": "eval:doc.type == \"Choices\";",
"fieldname": "section_break_ytxi",
"fieldtype": "Section Break"
},
{
"fieldname": "option_1",
"fieldtype": "Small Text",
"label": "Option 1",
"mandatory_depends_on": "eval: doc.type == 'Choices'"
},
{
"default": "0",
"fieldname": "is_correct_1",
"fieldtype": "Check",
"label": "Is Correct"
},
{
"fieldname": "column_break_fpvl",
"fieldtype": "Column Break"
},
{
"fieldname": "explanation_1",
"fieldtype": "Small Text",
"label": "Explanation"
},
{
"depends_on": "eval:doc.type == \"Choices\";",
"fieldname": "section_break_eiaa",
"fieldtype": "Section Break"
},
{
"fieldname": "option_2",
"fieldtype": "Small Text",
"label": "Option 2",
"mandatory_depends_on": "eval: doc.type == 'Choices'"
},
{
"default": "0",
"fieldname": "is_correct_2",
"fieldtype": "Check",
"label": "Is Correct"
},
{
"fieldname": "column_break_akwy",
"fieldtype": "Column Break"
},
{
"fieldname": "explanation_2",
"fieldtype": "Small Text",
"label": "Explanation "
},
{
"depends_on": "eval: doc.type == 'Choices'",
"fieldname": "section_break_cwqv",
"fieldtype": "Section Break"
},
{
"fieldname": "option_3",
"fieldtype": "Small Text",
"label": "Option 3"
},
{
"default": "0",
"fieldname": "is_correct_3",
"fieldtype": "Check",
"label": "Is Correct"
},
{
"fieldname": "column_break_atpl",
"fieldtype": "Column Break"
},
{
"fieldname": "explanation_3",
"fieldtype": "Small Text",
"label": "Explanation"
},
{
"depends_on": "eval: doc.type == 'Choices'",
"fieldname": "section_break_yqel",
"fieldtype": "Section Break"
},
{
"fieldname": "option_4",
"fieldtype": "Small Text",
"label": "Option 4"
},
{
"default": "0",
"fieldname": "is_correct_4",
"fieldtype": "Check",
"label": "Is Correct"
},
{
"fieldname": "column_break_lknb",
"fieldtype": "Column Break"
},
{
"fieldname": "explanation_4",
"fieldtype": "Small Text",
"label": "Explanation"
},
{
"default": "0",
"fieldname": "multiple",
"fieldtype": "Check",
"hidden": 1,
"label": "Multiple Correct Answers"
},
{
"depends_on": "eval: doc.type == 'User Input'",
"fieldname": "section_break_hkfe",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_wpjr",
"fieldtype": "Column Break"
},
{
"fieldname": "possibility_1",
"fieldtype": "Small Text",
"label": "Possible Answer 1",
"mandatory_depends_on": "eval: doc.type == 'User Input'"
},
{
"fieldname": "possibility_3",
"fieldtype": "Small Text",
"label": "Possible Answer 3"
},
{
"fieldname": "possibility_2",
"fieldtype": "Small Text",
"label": "Possible Answer 2"
},
{
"fieldname": "possibility_4",
"fieldtype": "Small Text",
"label": "Possible Answer 4"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-10-18 21:58:42.653317",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Question",
"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
},
{
"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": "Course Creator",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "question"
}

View File

@@ -0,0 +1,92 @@
# Copyright (c) 2023, Frappe and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
from lms.lms.utils import has_course_instructor_role, has_course_moderator_role
class LMSQuestion(Document):
def validate(self):
validate_correct_answers(self)
def validate_correct_answers(question):
if question.type == "Choices":
validate_duplicate_options(question)
validate_correct_options(question)
else:
validate_possible_answer(question)
def validate_duplicate_options(question):
options = []
for num in range(1, 5):
if question.get(f"option_{num}"):
options.append(question.get(f"option_{num}"))
if len(set(options)) != len(options):
frappe.throw(_("Duplicate options found for this question."))
def validate_correct_options(question):
correct_options = get_correct_options(question)
if len(correct_options) > 1:
question.multiple = 1
if not len(correct_options):
frappe.throw(_("At least one option must be correct for this question."))
def validate_possible_answer(question):
possible_answers = []
possible_answers_fields = [
"possibility_1",
"possibility_2",
"possibility_3",
"possibility_4",
]
for field in possible_answers_fields:
if question.get(field):
possible_answers.append(field)
if not len(possible_answers):
frappe.throw(
_("Add at least one possible answer for this question: {0}").format(
frappe.bold(question.question)
)
)
def get_correct_options(question):
correct_options = []
correct_option_fields = [
"is_correct_1",
"is_correct_2",
"is_correct_3",
"is_correct_4",
]
for field in correct_option_fields:
if question.get(field) == 1:
correct_options.append(field)
return correct_options
@frappe.whitelist()
def get_question_details(question):
if not has_course_instructor_role() or not has_course_moderator_role():
return
fields = ["question", "type", "name"]
for i in range(1, 5):
fields.append(f"option_{i}")
fields.append(f"is_correct_{i}")
fields.append(f"explanation_{i}")
fields.append(f"possibility_{i}")
return frappe.db.get_value("LMS Question", question, fields, as_dict=1)

View File

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

View File

@@ -5,3 +5,13 @@ frappe.ui.form.on("LMS Quiz", {
// refresh: function(frm) { // refresh: function(frm) {
// } // }
}); });
frappe.ui.form.on("LMS Quiz Question", {
marks: function (frm) {
total_marks = 0;
frm.doc.questions.forEach((question) => {
total_marks += question.marks;
});
frm.doc.total_marks = total_marks;
},
});

View File

@@ -12,6 +12,10 @@
"column_break_gaac", "column_break_gaac",
"max_attempts", "max_attempts",
"show_submission_history", "show_submission_history",
"section_break_hsiv",
"passing_percentage",
"column_break_rocd",
"total_marks",
"section_break_sbjx", "section_break_sbjx",
"questions", "questions",
"section_break_3", "section_break_3",
@@ -43,7 +47,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"default": "1", "default": "0",
"fieldname": "max_attempts", "fieldname": "max_attempts",
"fieldtype": "Int", "fieldtype": "Int",
"label": "Max Attempts" "label": "Max Attempts"
@@ -90,11 +94,35 @@
"fieldname": "show_submission_history", "fieldname": "show_submission_history",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Show Submission History" "label": "Show Submission History"
},
{
"fieldname": "section_break_hsiv",
"fieldtype": "Section Break"
},
{
"fieldname": "passing_percentage",
"fieldtype": "Int",
"label": "Passing Percentage",
"non_negative": 1,
"reqd": 1
},
{
"fieldname": "column_break_rocd",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "total_marks",
"fieldtype": "Int",
"label": "Total Marks",
"non_negative": 1,
"read_only": 1,
"reqd": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2023-07-04 15:26:24.457745", "modified": "2023-11-07 10:11:49.126789",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Quiz", "name": "LMS Quiz",
@@ -123,6 +151,18 @@
"role": "Moderator", "role": "Moderator",
"share": 1, "share": 1,
"write": 1 "write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Course Creator",
"share": 1,
"write": 1
} }
], ],
"show_title_field_in_link": 1, "show_title_field_in_link": 1,

View File

@@ -5,7 +5,8 @@ import json
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cstr from frappe.utils import cstr, comma_and
from lms.lms.doctype.lms_question.lms_question import validate_correct_answers
from lms.lms.utils import ( from lms.lms.utils import (
generate_slug, generate_slug,
has_course_moderator_role, has_course_moderator_role,
@@ -14,13 +15,22 @@ from lms.lms.utils import (
class LMSQuiz(Document): class LMSQuiz(Document):
def validate(self):
self.validate_duplicate_questions()
self.total_marks = set_total_marks(self.name, self.questions)
def validate_duplicate_questions(self):
questions = [row.question for row in self.questions]
rows = [i + 1 for i, x in enumerate(questions) if questions.count(x) > 1]
if len(rows):
frappe.throw(
_("Rows {0} have the duplicate questions.").format(frappe.bold(comma_and(rows)))
)
def autoname(self): def autoname(self):
if not self.name: if not self.name:
self.name = generate_slug(self.title, "LMS Quiz") self.name = generate_slug(self.title, "LMS Quiz")
def validate(self):
validate_correct_answers(self.questions)
def get_last_submission_details(self): def get_last_submission_details(self):
"""Returns the latest submission for this user.""" """Returns the latest submission for this user."""
user = frappe.session.user user = frappe.session.user
@@ -39,76 +49,11 @@ class LMSQuiz(Document):
return result[0] return result[0]
def get_correct_options(question): def set_total_marks(quiz, questions):
correct_option_fields = [ marks = 0
"is_correct_1",
"is_correct_2",
"is_correct_3",
"is_correct_4",
]
return list(filter(lambda x: question.get(x) == 1, correct_option_fields))
def validate_correct_answers(questions):
for question in questions: for question in questions:
if question.type == "Choices": marks += question.get("marks")
validate_duplicate_options(question) return marks
validate_correct_options(question)
else:
validate_possible_answer(question)
def validate_duplicate_options(question):
options = []
for num in range(1, 5):
if question.get(f"option_{num}"):
options.append(question.get(f"option_{num}"))
if len(set(options)) != len(options):
frappe.throw(
_("Duplicate options found for this question: {0}").format(
frappe.bold(question.question)
)
)
def validate_correct_options(question):
correct_options = get_correct_options(question)
if len(correct_options) > 1:
question.multiple = 1
if not len(correct_options):
frappe.throw(
_("At least one option must be correct for this question: {0}").format(
frappe.bold(question.question)
)
)
def validate_possible_answer(question):
possible_answers_fields = [
"possibility_1",
"possibility_2",
"possibility_3",
"possibility_4",
]
possible_answers = list(filter(lambda x: question.get(x), possible_answers_fields))
if not len(possible_answers):
frappe.throw(
_("Add at least one possible answer for this question: {0}").format(
frappe.bold(question.question)
)
)
def update_lesson_info(doc, method):
if doc.quiz_id:
frappe.db.set_value(
"LMS Quiz", doc.quiz_id, {"lesson": doc.name, "course": doc.course}
)
@frappe.whitelist() @frappe.whitelist()
@@ -118,45 +63,73 @@ def quiz_summary(quiz, results):
for result in results: for result in results:
correct = result["is_correct"][0] correct = result["is_correct"][0]
result["question"] = frappe.db.get_value(
"LMS Quiz Question",
{"parent": quiz, "idx": result["question_index"] + 1},
["question"],
)
for point in result["is_correct"]: for point in result["is_correct"]:
correct = correct and point correct = correct and point
result["is_correct"] = correct result["is_correct"] = correct
score += correct
question_details = frappe.db.get_value(
"LMS Quiz Question",
{"parent": quiz, "idx": result["question_index"]},
["question", "marks"],
as_dict=1,
)
result["question_name"] = question_details.question
result["question"] = frappe.db.get_value(
"LMS Question", question_details.question, "question"
)
marks = question_details.marks if correct else 0
result["marks"] = marks
score += marks
del result["question_index"] del result["question_index"]
quiz_details = frappe.db.get_value(
"LMS Quiz", quiz, ["total_marks", "passing_percentage"], as_dict=1
)
score_out_of = quiz_details.total_marks
percentage = (score / score_out_of) * 100
submission = frappe.get_doc( submission = frappe.get_doc(
{ {
"doctype": "LMS Quiz Submission", "doctype": "LMS Quiz Submission",
"quiz": quiz, "quiz": quiz,
"result": results, "result": results,
"score": score, "score": score,
"score_out_of": score_out_of,
"member": frappe.session.user, "member": frappe.session.user,
"percentage": percentage,
"passing_percentage": quiz_details.passing_percentage,
} }
) )
submission.save(ignore_permissions=True) submission.save(ignore_permissions=True)
return { return {
"score": score, "score": score,
"score_out_of": score_out_of,
"submission": submission.name, "submission": submission.name,
"pass": percentage == quiz_details.passing_percentage,
"percentage": percentage,
} }
@frappe.whitelist() @frappe.whitelist()
def save_quiz( def save_quiz(
quiz_title, max_attempts=1, quiz=None, show_answers=1, show_submission_history=0 quiz_title,
passing_percentage,
questions,
max_attempts=0,
quiz=None,
show_answers=1,
show_submission_history=0,
): ):
if not has_course_moderator_role() or not has_course_instructor_role(): if not has_course_moderator_role() or not has_course_instructor_role():
return return
values = { values = {
"title": quiz_title, "title": quiz_title,
"passing_percentage": passing_percentage,
"max_attempts": max_attempts, "max_attempts": max_attempts,
"show_answers": show_answers, "show_answers": show_answers,
"show_submission_history": show_submission_history, "show_submission_history": show_submission_history,
@@ -164,41 +137,77 @@ def save_quiz(
if quiz: if quiz:
frappe.db.set_value("LMS Quiz", quiz, values) frappe.db.set_value("LMS Quiz", quiz, values)
update_questions(quiz, questions)
return quiz return quiz
else: else:
doc = frappe.new_doc("LMS Quiz") doc = frappe.new_doc("LMS Quiz")
doc.update(values) doc.update(values)
doc.save(ignore_permissions=True) doc.save()
update_questions(doc.name, questions)
return doc.name return doc.name
def update_questions(quiz, questions):
questions = json.loads(questions)
delete_questions(quiz, questions)
add_questions(quiz, questions)
frappe.db.set_value("LMS Quiz", quiz, "total_marks", set_total_marks(quiz, questions))
def delete_questions(quiz, questions):
existing_questions = frappe.get_all(
"LMS Quiz Question",
{
"parent": quiz,
},
pluck="name",
)
current_questions = [question.get("question_name") for question in questions]
for question in existing_questions:
if question not in current_questions:
frappe.db.delete("LMS Quiz Question", question)
def add_questions(quiz, questions):
for index, question in enumerate(questions):
question = frappe._dict(question)
if question.question_name:
doc = frappe.get_doc("LMS Quiz Question", question.question_name)
else:
doc = frappe.new_doc("LMS Quiz Question")
doc.update(
{
"parent": quiz,
"parenttype": "LMS Quiz",
"parentfield": "questions",
"idx": index + 1,
}
)
doc.update({"question": question.question, "marks": question.marks})
doc.save()
@frappe.whitelist() @frappe.whitelist()
def save_question(quiz, values, index): def save_question(quiz, values, index):
values = frappe._dict(json.loads(values)) values = frappe._dict(json.loads(values))
validate_correct_answers([values])
if values.get("name"): if values.get("name"):
doc = frappe.get_doc("LMS Quiz Question", values.get("name")) doc = frappe.get_doc("LMS Question", values.get("name"))
else: else:
doc = frappe.new_doc("LMS Quiz Question") doc = frappe.new_doc("LMS Question")
doc.update( doc.update(
{ {
"question": values["question"], "question": values.question,
"type": values["type"], "type": values["type"],
} }
) )
if not values.get("name"):
doc.update(
{
"parent": quiz,
"parenttype": "LMS Quiz",
"parentfield": "questions",
"idx": index,
}
)
for num in range(1, 5): for num in range(1, 5):
if values.get(f"option_{num}"): if values.get(f"option_{num}"):
doc.update( doc.update(
@@ -222,9 +231,8 @@ def save_question(quiz, values, index):
} }
) )
doc.save(ignore_permissions=True) doc.save()
return doc.name
return quiz
@frappe.whitelist() @frappe.whitelist()
@@ -257,13 +265,13 @@ def check_choice_answers(question, answers):
fields.append(f"option_{cstr(num)}") fields.append(f"option_{cstr(num)}")
fields.append(f"is_correct_{cstr(num)}") fields.append(f"is_correct_{cstr(num)}")
question_details = frappe.db.get_value( question_details = frappe.db.get_value("LMS Question", question, fields, as_dict=1)
"LMS Quiz Question", question, fields, as_dict=1
)
for num in range(1, 5): for num in range(1, 5):
if question_details[f"option_{num}"] in answers: if question_details[f"option_{num}"] in answers:
is_correct.append(question_details[f"is_correct_{num}"]) is_correct.append(question_details[f"is_correct_{num}"])
elif question_details[f"is_correct_{num}"]:
is_correct.append(2)
else: else:
is_correct.append(0) is_correct.append(0)
@@ -275,9 +283,7 @@ def check_input_answers(question, answer):
for num in range(1, 5): for num in range(1, 5):
fields.append(f"possibility_{cstr(num)}") fields.append(f"possibility_{cstr(num)}")
question_details = frappe.db.get_value( question_details = frappe.db.get_value("LMS Question", question, fields, as_dict=1)
"LMS Quiz Question", question, fields, as_dict=1
)
for num in range(1, 5): for num in range(1, 5):
current_possibility = question_details[f"possibility_{num}"] current_possibility = question_details[f"possibility_{num}"]
if current_possibility and current_possibility.lower() == answer.lower(): if current_possibility and current_possibility.lower() == answer.lower():

View File

@@ -10,51 +10,36 @@ import frappe
class TestLMSQuiz(unittest.TestCase): class TestLMSQuiz(unittest.TestCase):
@classmethod @classmethod
def setUpClass(cls) -> None: def setUpClass(cls) -> None:
frappe.get_doc({"doctype": "LMS Quiz", "title": "Test Quiz"}).save( frappe.get_doc(
ignore_permissions=True {"doctype": "LMS Quiz", "title": "Test Quiz", "passing_percentage": 90}
) ).save(ignore_permissions=True)
def test_with_multiple_options(self): def test_with_multiple_options(self):
quiz = frappe.get_doc("LMS Quiz", "test-quiz") question = frappe.new_doc("LMS Question")
quiz.append( question.question = "Question Multiple"
"questions", question.type = "Choices"
{ question.option_1 = "Option 1"
"question": "Question Multiple", question.is_correct_1 = 1
"type": "Choices", question.option_2 = "Option 2"
"option_1": "Option 1", question.is_correct_2 = 1
"is_correct_1": 1, question.save()
"option_2": "Option 2", self.assertTrue(question.multiple)
"is_correct_2": 1,
},
)
quiz.save()
self.assertTrue(quiz.questions[0].multiple)
def test_with_no_correct_option(self): def test_with_no_correct_option(self):
quiz = frappe.get_doc("LMS Quiz", "test-quiz") question = frappe.new_doc("LMS Question")
quiz.append( question.question = "Question Multiple"
"questions", question.type = "Choices"
{ question.option_1 = "Option 1"
"question": "Question no correct option", question.option_2 = "Option 2"
"type": "Choices", self.assertRaises(frappe.ValidationError, question.save)
"option_1": "Option 1",
"option_2": "Option 2",
},
)
self.assertRaises(frappe.ValidationError, quiz.save)
def test_with_no_possible_answers(self): def test_with_no_possible_answers(self):
quiz = frappe.get_doc("LMS Quiz", "test-quiz") question = frappe.new_doc("LMS Question")
quiz.append( question.question = "Question Multiple"
"questions", question.type = "User Input"
{ self.assertRaises(frappe.ValidationError, question.save)
"question": "Question Possible Answers",
"type": "User Input",
},
)
self.assertRaises(frappe.ValidationError, quiz.save)
@classmethod @classmethod
def tearDownClass(cls) -> None: def tearDownClass(cls) -> None:
frappe.db.delete("LMS Quiz", "test-quiz") frappe.db.delete("LMS Quiz", "test-quiz")
frappe.db.delete("LMS Quiz Question", {"parent": "test-quiz"}) frappe.db.delete("LMS Question")

View File

@@ -6,208 +6,31 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"question", "question",
"type", "marks"
"options_section",
"option_1",
"is_correct_1",
"column_break_5",
"explanation_1",
"section_break_5",
"option_2",
"is_correct_2",
"column_break_10",
"explanation_2",
"column_break_4",
"option_3",
"is_correct_3",
"column_break_15",
"explanation_3",
"section_break_11",
"option_4",
"is_correct_4",
"column_break_20",
"explanation_4",
"section_break_mnhr",
"possibility_1",
"possibility_3",
"column_break_vnaj",
"possibility_2",
"possibility_4",
"section_break_c1lf",
"multiple"
], ],
"fields": [ "fields": [
{ {
"fieldname": "question", "fieldname": "question",
"fieldtype": "Text Editor", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
"label": "Question", "label": "Question",
"options": "LMS Question",
"reqd": 1 "reqd": 1
}, },
{ {
"fieldname": "option_1", "default": "1",
"fieldtype": "Small Text", "fieldname": "marks",
"label": "Option 1", "fieldtype": "Int",
"mandatory_depends_on": "eval: doc.type == 'Choices'"
},
{
"fieldname": "option_2",
"fieldtype": "Small Text",
"label": "Option 2",
"mandatory_depends_on": "eval: doc.type == 'Choices'"
},
{
"fieldname": "option_3",
"fieldtype": "Small Text",
"label": "Option 3"
},
{
"fieldname": "option_4",
"fieldtype": "Small Text",
"label": "Option 4"
},
{
"default": "0",
"depends_on": "option_1",
"fieldname": "is_correct_1",
"fieldtype": "Check",
"label": "Is Correct"
},
{
"default": "0",
"depends_on": "option_2",
"fieldname": "is_correct_2",
"fieldtype": "Check",
"label": "Is Correct"
},
{
"default": "0",
"depends_on": "option_3",
"fieldname": "is_correct_3",
"fieldtype": "Check",
"label": "Is Correct"
},
{
"default": "0",
"depends_on": "option_4",
"fieldname": "is_correct_4",
"fieldtype": "Check",
"label": "Is Correct"
},
{
"default": "0",
"fieldname": "multiple",
"fieldtype": "Check",
"hidden": 1,
"label": "Multiple Correct Answers",
"read_only": 1
},
{
"depends_on": "eval: doc.type == 'Choices'",
"fieldname": "options_section",
"fieldtype": "Section Break"
},
{
"depends_on": "eval: doc.type == 'Choices'",
"fieldname": "column_break_4",
"fieldtype": "Section Break"
},
{
"depends_on": "eval: doc.type == 'Choices'",
"fieldname": "section_break_5",
"fieldtype": "Section Break"
},
{
"depends_on": "eval: doc.type == 'Choices'",
"fieldname": "section_break_11",
"fieldtype": "Section Break"
},
{
"depends_on": "option_1",
"fieldname": "explanation_1",
"fieldtype": "Data",
"label": "Explanation"
},
{
"depends_on": "option_2",
"fieldname": "explanation_2",
"fieldtype": "Data",
"label": "Explanation"
},
{
"depends_on": "option_3",
"fieldname": "explanation_3",
"fieldtype": "Data",
"label": "Explanation"
},
{
"depends_on": "option_4",
"fieldname": "explanation_4",
"fieldtype": "Data",
"label": "Explanation"
},
{
"fieldname": "column_break_5",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_10",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_15",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_20",
"fieldtype": "Column Break"
},
{
"fieldname": "type",
"fieldtype": "Select",
"in_list_view": 1, "in_list_view": 1,
"label": "Type", "label": "Marks",
"options": "Choices\nUser Input" "non_negative": 1,
}, "reqd": 1
{
"depends_on": "eval: doc.type == 'User Input'",
"fieldname": "section_break_mnhr",
"fieldtype": "Section Break"
},
{
"fieldname": "possibility_1",
"fieldtype": "Small Text",
"label": "Possible Answer 1",
"mandatory_depends_on": "eval: doc.type == 'User Input'"
},
{
"fieldname": "possibility_2",
"fieldtype": "Small Text",
"label": "Possible Answer 2"
},
{
"fieldname": "possibility_3",
"fieldtype": "Small Text",
"label": "Possible Answer 3"
},
{
"fieldname": "possibility_4",
"fieldtype": "Small Text",
"label": "Possible Answer 4"
},
{
"fieldname": "section_break_c1lf",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_vnaj",
"fieldtype": "Column Break"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-07-04 16:43:49.837134", "modified": "2023-10-16 19:51:03.893144",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Quiz Question", "name": "LMS Quiz Question",

View File

@@ -6,7 +6,11 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"question", "question",
"section_break_fztv",
"question_name",
"answer", "answer",
"column_break_flus",
"marks",
"is_correct" "is_correct"
], ],
"fields": [ "fields": [
@@ -31,12 +35,33 @@
"in_list_view": 1, "in_list_view": 1,
"label": "Is Correct", "label": "Is Correct",
"read_only": 1 "read_only": 1
},
{
"fieldname": "section_break_fztv",
"fieldtype": "Section Break"
},
{
"fieldname": "question_name",
"fieldtype": "Link",
"label": "Question Name",
"options": "LMS Question"
},
{
"fieldname": "column_break_flus",
"fieldtype": "Column Break"
},
{
"fieldname": "marks",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Marks",
"read_only": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-11-24 11:15:45.931119", "modified": "2023-10-17 11:55:25.641214",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Quiz Result", "name": "LMS Quiz Result",

View File

@@ -6,11 +6,16 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"quiz", "quiz",
"score",
"course", "course",
"column_break_3", "column_break_3",
"member", "member",
"member_name", "member_name",
"section_break_dkpn",
"score",
"score_out_of",
"column_break_gkip",
"percentage",
"passing_percentage",
"section_break_6", "section_break_6",
"result" "result"
], ],
@@ -31,9 +36,11 @@
}, },
{ {
"fieldname": "score", "fieldname": "score",
"fieldtype": "Data", "fieldtype": "Int",
"in_list_view": 1, "in_list_view": 1,
"label": "Score" "label": "Score",
"read_only": 1,
"reqd": 1
}, },
{ {
"fieldname": "member", "fieldname": "member",
@@ -65,12 +72,45 @@
"label": "Course", "label": "Course",
"options": "LMS Course", "options": "LMS Course",
"read_only": 1 "read_only": 1
},
{
"fetch_from": "quiz.total_marks",
"fieldname": "score_out_of",
"fieldtype": "Int",
"label": "Score Out Of",
"non_negative": 1,
"read_only": 1,
"reqd": 1
},
{
"fieldname": "section_break_dkpn",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_gkip",
"fieldtype": "Column Break"
},
{
"fieldname": "percentage",
"fieldtype": "Int",
"label": "Percentage",
"non_negative": 1,
"read_only": 1,
"reqd": 1
},
{
"fieldname": "passing_percentage",
"fieldtype": "Int",
"label": "Passing Percentage",
"non_negative": 1,
"read_only": 1,
"reqd": 1
} }
], ],
"in_create": 1, "in_create": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2022-11-15 15:27:07.770945", "modified": "2023-10-17 13:07:27.979975",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Quiz Submission", "name": "LMS Quiz Submission",

View File

@@ -6,4 +6,10 @@ from frappe.model.document import Document
class LMSQuizSubmission(Document): class LMSQuizSubmission(Document):
pass def before_insert(self):
if not self.percentage:
self.set_percentage()
def set_percentage(self):
if self.score and self.score_out_of:
self.percentage = (self.score / self.score_out_of) * 100

View File

@@ -16,18 +16,16 @@
"portal_course_creation", "portal_course_creation",
"section_break_szgq", "section_break_szgq",
"send_calendar_invite_for_evaluations", "send_calendar_invite_for_evaluations",
"batch_confirmation_template", "show_day_view",
"column_break_2",
"allow_student_progress", "allow_student_progress",
"payment_section", "column_break_2",
"razorpay_key", "show_dashboard",
"razorpay_secret", "show_courses",
"apply_gst", "show_students",
"column_break_cfcv", "show_assessments",
"default_currency", "show_live_class",
"show_usd_equivalent", "show_discussions",
"apply_rounding", "show_emails",
"exception_country",
"signup_settings_tab", "signup_settings_tab",
"signup_settings_section", "signup_settings_section",
"terms_of_use", "terms_of_use",
@@ -42,7 +40,22 @@
"mentor_request_tab", "mentor_request_tab",
"mentor_request_section", "mentor_request_section",
"mentor_request_creation", "mentor_request_creation",
"mentor_request_status_update" "mentor_request_status_update",
"payment_settings_tab",
"payment_section",
"razorpay_key",
"razorpay_secret",
"apply_gst",
"column_break_cfcv",
"default_currency",
"show_usd_equivalent",
"apply_rounding",
"exception_country",
"email_templates_tab",
"certification_template",
"batch_confirmation_template",
"column_break_uwsp",
"assignment_submission_template"
], ],
"fields": [ "fields": [
{ {
@@ -71,7 +84,8 @@
}, },
{ {
"fieldname": "column_break_2", "fieldname": "column_break_2",
"fieldtype": "Column Break" "fieldtype": "Column Break",
"label": "Show Tab in Batch"
}, },
{ {
"fieldname": "search_placeholder", "fieldname": "search_placeholder",
@@ -199,8 +213,7 @@
}, },
{ {
"fieldname": "payment_section", "fieldname": "payment_section",
"fieldtype": "Section Break", "fieldtype": "Section Break"
"label": "Payment"
}, },
{ {
"fieldname": "default_currency", "fieldname": "default_currency",
@@ -261,12 +274,86 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Batch Confirmation Template", "label": "Batch Confirmation Template",
"options": "Email Template" "options": "Email Template"
},
{
"default": "1",
"fieldname": "show_courses",
"fieldtype": "Check",
"label": "Courses"
},
{
"default": "1",
"fieldname": "show_students",
"fieldtype": "Check",
"label": "Students"
},
{
"default": "1",
"fieldname": "show_assessments",
"fieldtype": "Check",
"label": "Assessments"
},
{
"default": "1",
"fieldname": "show_live_class",
"fieldtype": "Check",
"label": "Live Class"
},
{
"default": "1",
"fieldname": "show_discussions",
"fieldtype": "Check",
"label": "Discussions"
},
{
"default": "1",
"fieldname": "show_emails",
"fieldtype": "Check",
"label": "Emails"
},
{
"fieldname": "payment_settings_tab",
"fieldtype": "Tab Break",
"label": "Payment Settings"
},
{
"default": "1",
"fieldname": "show_dashboard",
"fieldtype": "Check",
"label": "Dashboard"
},
{
"fieldname": "certification_template",
"fieldtype": "Link",
"label": "Certificate Email Template",
"options": "Email Template"
},
{
"fieldname": "email_templates_tab",
"fieldtype": "Tab Break",
"label": "Email Templates"
},
{
"fieldname": "assignment_submission_template",
"fieldtype": "Link",
"label": "Assignment Submission Template",
"options": "Email Template"
},
{
"fieldname": "column_break_uwsp",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "show_day_view",
"fieldtype": "Check",
"label": "Show Day View in Timetable"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2023-10-09 17:27:28.615355", "modified": "2023-12-12 10:32:13.638368",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Settings", "name": "LMS Settings",

View File

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

View File

@@ -0,0 +1,69 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:source",
"creation": "2023-10-26 16:28:53.932278",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"source"
],
"fields": [
{
"fieldname": "source",
"fieldtype": "Data",
"label": "Source",
"unique": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-10-26 17:25:09.144367",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Source",
"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
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "LMS Student",
"select": 1,
"share": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "source"
}

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 LMSSource(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 TestLMSSource(FrappeTestCase):
pass

View File

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

View File

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

View File

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

View File

@@ -11,7 +11,8 @@
"idx": 0, "idx": 0,
"is_standard": 1, "is_standard": 1,
"message": "{% set title = frappe.db.get_value(\"LMS Course\", doc.course, \"title\") %}\n\n<p> {{ _(\"Hey {0}\").format(doc.member_name) }} </p>\n<p> {{ _('Your evaluation for the course {0} has been scheduled on {1} at {2}.').format(title, frappe.utils.format_date(doc.date, \"medium\"), frappe.utils.format_time(doc.start_time, \"short\")) }}</p>\n<p> {{ _(\"Please prepare well and be on time for the evaluations.\") }} </p>\n", "message": "{% set title = frappe.db.get_value(\"LMS Course\", doc.course, \"title\") %}\n\n<p> {{ _(\"Hey {0}\").format(doc.member_name) }} </p>\n<p> {{ _('Your evaluation for the course {0} has been scheduled on {1} at {2}.').format(title, frappe.utils.format_date(doc.date, \"medium\"), frappe.utils.format_time(doc.start_time, \"short\")) }}</p>\n<p> {{ _(\"Please prepare well and be on time for the evaluations.\") }} </p>\n",
"modified": "2023-02-28 19:53:47.716135", "message_type": "HTML",
"modified": "2023-11-29 17:34:54.514031",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "Certificate Request Creation", "name": "Certificate Request Creation",
@@ -27,4 +28,4 @@
"send_system_notification": 0, "send_system_notification": 0,
"send_to_all_assignees": 0, "send_to_all_assignees": 0,
"subject": "Your evaluation slot has been booked" "subject": "Your evaluation slot has been booked"
} }

View File

@@ -1 +0,0 @@
<p> {{ _("Please prepare well and be on time for the evaluations.") }} </p>

View File

@@ -1,5 +1,5 @@
{% set title = frappe.db.get_value("LMS Course", doc.course, "title") %} {% set title = frappe.db.get_value("LMS Course", doc.course, "title") %}
<p> {{ _("Hey {0}").format(doc.member_name) }} </p> <p> {{ _('Your evaluation for the course ${0} has been scheduled on ${1} at ${2}.').format(title, frappe.utils.format_date(doc.date, "medium"), frappe.utils.format_time(doc.start_time, "short")) }}</p>
<p> {{ _('Your evaluation for the course {0} has been scheduled on {1} at {2}.').format(title, frappe.utils.format_date(doc.date, "medium"), frappe.utils.format_time(doc.start_time, "short")) }}</p>
<p> {{ _("Please prepare well and be on time for the evaluations.") }} </p> <p> {{ _("Please prepare well and be on time for the evaluations.") }} </p>

View File

@@ -11,8 +11,9 @@
"event": "Days Before", "event": "Days Before",
"idx": 0, "idx": 0,
"is_standard": 1, "is_standard": 1,
"message": "{% set title = frappe.db.get_value(\"LMS Course\", doc.course, \"title\") %}\n<p> {{ _('Your evaluation for the course ${0} has been scheduled on ${1} at ${2}.').format(title, frappe.utils.format_date(doc.date, \"medium\"), frappe.utils.format_time(doc.start_time, \"short\")) }}</p>\n<p> {{ _(\"Please prepare well and be on time for the evaluations.\") }} </p>\n", "message": "{% set title = frappe.db.get_value(\"LMS Course\", doc.course, \"title\") %}\n\n<p> {{ _('Your evaluation for the course {0} has been scheduled on {1} at {2}.').format(title, frappe.utils.format_date(doc.date, \"medium\"), frappe.utils.format_time(doc.start_time, \"short\")) }}</p>\n\n<p> {{ _(\"Please prepare well and be on time for the evaluations.\") }} </p>\n",
"modified": "2022-06-03 11:51:02.681803", "message_type": "HTML",
"modified": "2023-11-29 17:26:53.355501",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "Certificate Request Reminder", "name": "Certificate Request Reminder",
@@ -20,9 +21,12 @@
"recipients": [ "recipients": [
{ {
"receiver_by_document_field": "member" "receiver_by_document_field": "member"
},
{
"receiver_by_document_field": "evaluator"
} }
], ],
"send_system_notification": 0, "send_system_notification": 0,
"send_to_all_assignees": 0, "send_to_all_assignees": 0,
"subject": "Reminder for Certificate Evaluation" "subject": "Reminder for Certificate Evaluation"
} }

View File

@@ -1,3 +0,0 @@
{% set title = frappe.db.get_value("LMS Course", doc.course, "title") %}
<p> {{ _('Your evaluation for the course ${0} has been scheduled on ${1} at ${2}.').format(title, frappe.utils.format_date(doc.date, "medium"), frappe.utils.format_time(doc.start_time, "short")) }}</p>
<p> {{ _("Please prepare well and be on time for the evaluations.") }} </p>

View File

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

View File

@@ -4,10 +4,16 @@ import frappe
import json import json
import razorpay import razorpay
import requests import requests
import base64
from frappe import _ from frappe import _
from frappe.desk.doctype.dashboard_chart.dashboard_chart import get_result from frappe.desk.doctype.dashboard_chart.dashboard_chart import get_result
from frappe.desk.doctype.notification_log.notification_log import make_notification_logs from frappe.desk.doctype.notification_log.notification_log import (
make_notification_logs,
enqueue_create_notification,
get_title,
)
from frappe.utils import get_fullname
from frappe.desk.search import get_user_groups
from frappe.desk.notifications import extract_mentions
from frappe.utils import ( from frappe.utils import (
add_months, add_months,
cint, cint,
@@ -150,7 +156,7 @@ def get_lesson_details(chapter):
], ],
as_dict=True, as_dict=True,
) )
lesson_details.number = flt(f"{chapter.idx}.{row.idx}") lesson_details.number = f"{chapter.idx}.{row.idx}"
lesson_details.icon = get_lesson_icon(lesson_details.body) lesson_details.icon = get_lesson_icon(lesson_details.body)
lessons.append(lesson_details) lessons.append(lesson_details)
return lessons return lessons
@@ -549,6 +555,9 @@ def can_create_courses(course, member=None):
if portal_course_creation == "Anyone" and member in instructors: if portal_course_creation == "Anyone" and member in instructors:
return True return True
if not course and has_course_instructor_role(member):
return True
return False return False
@@ -604,17 +613,20 @@ def validate_image(path):
return path return path
def create_notification_log(doc, method): def handle_notifications(doc, method):
topic = frappe.db.get_value( topic = frappe.db.get_value(
"Discussion Topic", "Discussion Topic",
doc.topic, doc.topic,
["reference_doctype", "reference_docname", "owner", "title"], ["reference_doctype", "reference_docname", "owner", "title"],
as_dict=1, as_dict=1,
) )
if topic.reference_doctype not in ["Course Lesson", "LMS Batch"]:
if topic.reference_doctype != "Course Lesson":
return return
create_notification_log(doc, topic)
notify_mentions(doc, topic)
def create_notification_log(doc, topic):
course = frappe.db.get_value("Course Lesson", topic.reference_docname, "course") course = frappe.db.get_value("Course Lesson", topic.reference_docname, "course")
instructors = frappe.db.get_all( instructors = frappe.db.get_all(
"Course Instructor", {"parent": course}, pluck="instructor" "Course Instructor", {"parent": course}, pluck="instructor"
@@ -641,6 +653,47 @@ def create_notification_log(doc, method):
make_notification_logs(notification, users) make_notification_logs(notification, users)
def notify_mentions(doc, topic):
mentions = extract_mentions(doc.reply)
if not mentions:
return
sender_fullname = get_fullname(doc.owner)
recipients = [
frappe.db.get_value(
"User",
{"enabled": 1, "name": name},
"email",
)
for name in mentions
]
subject = _("{0} mentioned you in a comment").format(sender_fullname)
template = "mention_template"
if topic.reference_doctype == "LMS Batch":
link = f"/batches/{topic.reference_docname}#discussions"
if topic.reference_doctype == "Course Lesson":
course = frappe.db.get_value("Course Lesson", topic.reference_docname, "course")
lesson_index = get_lesson_index(topic.reference_docname)
link = get_lesson_url(course, lesson_index)
args = {
"sender": sender_fullname,
"content": doc.reply,
"link": link,
}
for recipient in recipients:
frappe.sendmail(
recipients=recipient,
subject=subject,
template=template,
args=args,
header=[subject, "green"],
retry=3,
)
def get_lesson_count(course): def get_lesson_count(course):
lesson_count = 0 lesson_count = 0
chapters = frappe.get_all("Chapter Reference", {"parent": course}, ["chapter"]) chapters = frappe.get_all("Chapter Reference", {"parent": course}, ["chapter"])
@@ -774,6 +827,17 @@ def get_telemetry_boot_info():
def is_onboarding_complete(): def is_onboarding_complete():
onboarding_status = frappe.db.get_single_value(
"LMS Settings", "is_onboarding_complete"
)
if onboarding_status:
return {
"is_onboarded": onboarding_status,
"course_created": True,
"chapter_created": True,
"lesson_created": True,
"first_course": None,
}
course_created = frappe.db.a_row_exists("LMS Course") course_created = frappe.db.a_row_exists("LMS Course")
chapter_created = frappe.db.a_row_exists("Course Chapter") chapter_created = frappe.db.a_row_exists("Course Chapter")
lesson_created = frappe.db.a_row_exists("Course Lesson") lesson_created = frappe.db.a_row_exists("Course Lesson")
@@ -782,7 +846,7 @@ def is_onboarding_complete():
frappe.db.set_single_value("LMS Settings", "is_onboarding_complete", 1) frappe.db.set_single_value("LMS Settings", "is_onboarding_complete", 1)
return { return {
"is_onboarded": frappe.db.get_single_value("LMS Settings", "is_onboarding_complete"), "is_onboarded": onboarding_status,
"course_created": course_created, "course_created": course_created,
"chapter_created": chapter_created, "chapter_created": chapter_created,
"lesson_created": lesson_created, "lesson_created": lesson_created,
@@ -853,8 +917,9 @@ def get_payment_options(doctype, docname, phone, country):
validate_phone_number(phone, True) validate_phone_number(phone, True)
details = get_details(doctype, docname) details = get_details(doctype, docname)
details.amount, details.currency = check_multicurrency( details.amount, details.currency = check_multicurrency(
details.amount, details.currency, country details.amount, details.currency, country, details.amount_usd
) )
if details.currency == "INR": if details.currency == "INR":
details.amount, details.gst_applied = apply_gst(details.amount, country) details.amount, details.gst_applied = apply_gst(details.amount, country)
@@ -867,7 +932,7 @@ def get_payment_options(doctype, docname, phone, country):
"name": frappe.db.get_single_value("Website Settings", "app_name"), "name": frappe.db.get_single_value("Website Settings", "app_name"),
"description": _("Payment for {0} course").format(details["title"]), "description": _("Payment for {0} course").format(details["title"]),
"order_id": order["id"], "order_id": order["id"],
"amount": order["amount"] * 100, "amount": cint(order["amount"]) * 100,
"currency": order["currency"], "currency": order["currency"],
"prefill": { "prefill": {
"name": frappe.db.get_value("User", frappe.session.user, "full_name"), "name": frappe.db.get_value("User", frappe.session.user, "full_name"),
@@ -878,16 +943,21 @@ def get_payment_options(doctype, docname, phone, country):
return options return options
def check_multicurrency(amount, currency, country=None): def check_multicurrency(amount, currency, country=None, amount_usd=None):
show_usd_equivalent = frappe.db.get_single_value("LMS Settings", "show_usd_equivalent") show_usd_equivalent = frappe.db.get_single_value("LMS Settings", "show_usd_equivalent")
exception_country = frappe.get_all( exception_country = frappe.get_all(
"Payment Country", filters={"parent": "LMS Settings"}, pluck="country" "Payment Country", filters={"parent": "LMS Settings"}, pluck="country"
) )
apply_rounding = frappe.db.get_single_value("LMS Settings", "apply_rounding") country = (
country = country or frappe.db.get_value( country
"Address", {"email_id": frappe.session.user}, "country" or frappe.db.get_value("Address", {"email_id": frappe.session.user}, "country")
or frappe.db.get_value("User", frappe.session.user, "country")
or get_country_code()
) )
if amount_usd and country and country not in exception_country:
return amount_usd, "USD"
if not show_usd_equivalent or currency == "USD": if not show_usd_equivalent or currency == "USD":
return amount, currency return amount, currency
@@ -898,8 +968,9 @@ def check_multicurrency(amount, currency, country=None):
amount = amount * exchange_rate amount = amount * exchange_rate
currency = "USD" currency = "USD"
apply_rounding = frappe.db.get_single_value("LMS Settings", "apply_rounding")
if apply_rounding and amount % 100 != 0: if apply_rounding and amount % 100 != 0:
amount = ceil(amount + 100 - amount % 100) amount = amount + 100 - amount % 100
return amount, currency return amount, currency
@@ -923,7 +994,7 @@ def get_details(doctype, docname):
details = frappe.db.get_value( details = frappe.db.get_value(
"LMS Course", "LMS Course",
docname, docname,
["name", "title", "paid_course", "currency", "course_price as amount"], ["name", "title", "paid_course", "currency", "course_price as amount", "amount_usd"],
as_dict=True, as_dict=True,
) )
if not details.paid_course: if not details.paid_course:
@@ -932,7 +1003,7 @@ def get_details(doctype, docname):
details = frappe.db.get_value( details = frappe.db.get_value(
"LMS Batch", "LMS Batch",
docname, docname,
["name", "title", "paid_batch", "currency", "amount"], ["name", "title", "paid_batch", "currency", "amount", "amount_usd"],
as_dict=True, as_dict=True,
) )
if not details.paid_batch: if not details.paid_batch:
@@ -981,13 +1052,15 @@ def create_order(client, amount, currency):
try: try:
return client.order.create( return client.order.create(
{ {
"amount": amount * 100, "amount": cint(amount) * 100,
"currency": currency, "currency": currency,
} }
) )
except Exception as e: except Exception as e:
frappe.throw( frappe.throw(
_("Error during payment: {0}. Please contact the Administrator.").format(e) _(
"Error during payment: {0} Please contact the Administrator. Amount {1} Currency {2} Formatted {3}"
).format(e, amount, currency, cint(amount))
) )
@@ -1029,19 +1102,23 @@ def record_payment(address, response, client, doctype, docname):
"amount_with_gst": payment_details["amount_with_gst"], "amount_with_gst": payment_details["amount_with_gst"],
"gstin": address.gstin, "gstin": address.gstin,
"pan": address.pan, "pan": address.pan,
"source": address.source,
"payment_for_document_type": doctype,
"payment_for_document": docname,
} }
) )
payment_doc.save(ignore_permissions=True) payment_doc.save(ignore_permissions=True)
return payment_doc.name return payment_doc
def get_payment_details(doctype, docname, address): def get_payment_details(doctype, docname, address):
amount_field = "course_price" if doctype == "LMS Course" else "amount" amount_field = "course_price" if doctype == "LMS Course" else "amount"
amount = frappe.db.get_value(doctype, docname, amount_field) amount = frappe.db.get_value(doctype, docname, amount_field)
currency = frappe.db.get_value(doctype, docname, "currency") currency = frappe.db.get_value(doctype, docname, "currency")
amount_usd = frappe.db.get_value(doctype, docname, "amount_usd")
amount_with_gst = 0 amount_with_gst = 0
amount, currency = check_multicurrency(amount, currency) amount, currency = check_multicurrency(amount, currency, None, amount_usd)
if currency == "INR" and address.country == "India": if currency == "INR" and address.country == "India":
amount_with_gst, gst_applied = apply_gst(amount, address.country) amount_with_gst, gst_applied = apply_gst(amount, address.country)
@@ -1055,7 +1132,7 @@ def get_payment_details(doctype, docname, address):
def create_membership(course, payment): def create_membership(course, payment):
membership = frappe.new_doc("LMS Enrollment") membership = frappe.new_doc("LMS Enrollment")
membership.update( membership.update(
{"member": frappe.session.user, "course": course, "payment": payment} {"member": frappe.session.user, "course": course, "payment": payment.name}
) )
membership.save(ignore_permissions=True) membership.save(ignore_permissions=True)
return f"/courses/{course}/learn/1.1" return f"/courses/{course}/learn/1.1"
@@ -1066,7 +1143,8 @@ def add_student_to_batch(batchname, payment):
student.update( student.update(
{ {
"student": frappe.session.user, "student": frappe.session.user,
"payment": payment, "payment": payment.name,
"source": payment.source,
"parent": batchname, "parent": batchname,
"parenttype": "LMS Batch", "parenttype": "LMS Batch",
"parentfield": "students", "parentfield": "students",
@@ -1089,3 +1167,16 @@ def change_currency(amount, currency, country=None):
amount = cint(amount) amount = cint(amount)
amount, currency = check_multicurrency(amount, currency, country) amount, currency = check_multicurrency(amount, currency, country)
return fmt_money(amount, 0, currency) return fmt_money(amount, 0, currency)
def get_country_code():
ip = frappe.local.request_ip
res = requests.get(f"http://ip-api.com/json/{ip}")
try:
data = res.json()
if data.get("status") != "fail":
return frappe.db.get_value("Country", {"code": data.get("countryCode")}, "name")
except Exception:
pass
return

View File

@@ -29,10 +29,6 @@
<button class="btn btn-primary btn-sm notify-me pull-right" data-course="{{course.name | urlencode}}"> <button class="btn btn-primary btn-sm notify-me pull-right" data-course="{{course.name | urlencode}}">
{{ _("Notify me when available") }} {{ _("Notify me when available") }}
</button> </button>
{% elif show_start_learing_cta(course, membership) %}
<button class="btn btn-primary btn-sm enroll-in-course pull-right" data-course="{{ course.name | urlencode}}">
{{ _("Start Learning") }}
</button>
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}

View File

@@ -5,7 +5,7 @@ from frappe import _
from frappe.core.doctype.user.user import User from frappe.core.doctype.user.user import User
from frappe.utils import cint, escape_html, random_string from frappe.utils import cint, escape_html, random_string
from frappe.website.utils import is_signup_disabled from frappe.website.utils import is_signup_disabled
from lms.lms.utils import get_average_rating from lms.lms.utils import get_average_rating, get_country_code
from frappe.website.utils import cleanup_page_name from frappe.website.utils import cleanup_page_name
from frappe.model.naming import append_number_if_name_exists from frappe.model.naming import append_number_if_name_exists
from lms.widgets import Widgets from lms.widgets import Widgets
@@ -260,19 +260,6 @@ def set_country_from_ip(login_manager=None, user=None):
return return
def get_country_code():
ip = frappe.local.request_ip
res = requests.get(f"http://ip-api.com/json/{ip}")
try:
data = res.json()
if data.get("status") != "fail":
return frappe.db.get_value("Country", {"code": data.get("countryCode")}, "name")
except Exception:
pass
return
def on_session_creation(login_manager): def on_session_creation(login_manager):
if frappe.db.get_single_value( if frappe.db.get_single_value(
"System Settings", "setup_complete" "System Settings", "setup_complete"

View File

@@ -1,3 +1,4 @@
[pre_model_sync]
community.patches.set_email_preferences community.patches.set_email_preferences
community.patches.change_name_for_community_members community.patches.change_name_for_community_members
community.patches.save_abbr_for_community_members community.patches.save_abbr_for_community_members
@@ -71,4 +72,14 @@ lms.patches.v1_0.publish_batches
lms.patches.v1_0.publish_certificates lms.patches.v1_0.publish_certificates
lms.patches.v1_0.change_naming_for_batch_course #14-09-2023 lms.patches.v1_0.change_naming_for_batch_course #14-09-2023
execute:frappe.permissions.reset_perms("LMS Enrollment") execute:frappe.permissions.reset_perms("LMS Enrollment")
lms.patches.v1_0.create_student_role lms.patches.v1_0.create_student_role
lms.patches.v1_0.mark_confirmation_for_batch_students
lms.patches.v1_0.create_quiz_questions
lms.patches.v1_0.add_default_marks #16-10-2023
lms.patches.v1_0.add_certificate_template #26-10-2023
lms.patches.v1_0.create_batch_source
[post_model_sync]
lms.patches.v1_0.batch_tabs_settings
execute:frappe.delete_doc("Notification", "Assignment Submission Notification")
lms.patches.v1_0.change_jobs_url #17-01-2024

View File

@@ -0,0 +1,20 @@
import frappe
def execute():
frappe.reload_doc("lms", "doctype", "lms_certificate")
default_certificate_template = frappe.db.get_value(
"Property Setter",
{
"doc_type": "LMS Certificate",
"property": "default_print_format",
},
"value",
)
if frappe.db.exists("Print Format", default_certificate_template):
certificates = frappe.get_all("LMS Certificate", pluck="name")
for certificate in certificates:
frappe.db.set_value(
"LMS Certificate", certificate, "template", default_certificate_template
)

View File

@@ -0,0 +1,18 @@
import frappe
def execute():
frappe.reload_doc("lms", "doctype", "lms_quiz_question")
frappe.reload_doc("lms", "doctype", "lms_quiz")
questions = frappe.get_all("LMS Quiz Question", pluck="name")
for question in questions:
frappe.db.set_value("LMS Quiz Question", question, "marks", 1)
quizzes = frappe.get_all("LMS Quiz", pluck="name")
for quiz in quizzes:
questions_count = frappe.db.count("LMS Quiz Question", {"parent": quiz})
frappe.db.set_value(
"LMS Quiz", quiz, {"total_marks": questions_count, "passing_percentage": 100}
)

View File

@@ -0,0 +1,16 @@
import frappe
def execute():
fields = [
"show_dashboard",
"show_courses",
"show_students",
"show_emails",
"show_assessments",
"show_discussions",
"show_live_class",
]
for field in fields:
frappe.db.set_single_value("LMS Settings", field, 1)

View File

@@ -0,0 +1,15 @@
import frappe
def execute():
jobs_link = frappe.db.exists(
"Top Bar Item",
{
"label": "Jobs",
"url": "/jobs",
"parent_label": "Explore",
},
)
if jobs_link:
frappe.db.set_value("Top Bar Item", jobs_link, "url", "/job-openings")

View File

@@ -0,0 +1,7 @@
import frappe
from lms.install import create_batch_source
def execute():
frappe.reload_doc("lms", "doctype", "lms_source")
create_batch_source()

View File

@@ -0,0 +1,43 @@
import frappe
def execute():
frappe.reload_doc("lms", "doctype", "lms_question")
fields = ["name", "question", "type", "multiple"]
for num in range(1, 5):
fields.append(f"option_{num}")
fields.append(f"is_correct_{num}")
fields.append(f"explanation_{num}")
fields.append(f"possibility_{num}")
questions = frappe.get_all(
"LMS Quiz Question",
fields=fields,
)
for question in questions:
print(question.name)
doc = frappe.new_doc("LMS Question")
doc.update(
{
"question": question.question,
"type": question.type,
"multiple": question.multiple,
}
)
for num in range(1, 5):
if question.get(f"option_{num}"):
doc.update(
{
f"option_{num}": question[f"option_{num}"],
f"is_correct_{num}": question[f"is_correct_{num}"],
f"explanation_{num}": question[f"explanation_{num}"],
f"possibility_{num}": question[f"possibility_{num}"],
}
)
doc.save()
print(doc.name)
frappe.db.set_value("LMS Quiz Question", question.name, "question", doc.name)

View File

@@ -0,0 +1,9 @@
import frappe
def execute():
frappe.reload_doc("lms", "doctype", "batch_student")
students = frappe.get_all("Batch Student", pluck="name")
for student in students:
frappe.db.set_value("Batch Student", student, "confirmation_email_sent", 1)

View File

@@ -109,7 +109,39 @@ def quiz_renderer(quiz_name):
) )
+"</div>" +"</div>"
quiz = frappe.get_doc("LMS Quiz", quiz_name) quiz = frappe.db.get_value(
"LMS Quiz",
quiz_name,
[
"name",
"title",
"max_attempts",
"show_answers",
"show_submission_history",
"passing_percentage",
],
as_dict=True,
)
quiz.questions = []
fields = ["name", "question", "type", "multiple"]
for num in range(1, 5):
fields.append(f"option_{num}")
fields.append(f"is_correct_{num}")
fields.append(f"explanation_{num}")
fields.append(f"possibility_{num}")
questions = frappe.get_all(
"LMS Quiz Question",
filters={"parent": quiz.name},
fields=["question", "marks"],
order_by="idx",
)
for question in questions:
details = frappe.db.get_value("LMS Question", question.question, fields, as_dict=1)
details["marks"] = question.marks
quiz.questions.append(details)
no_of_attempts = frappe.db.count( no_of_attempts = frappe.db.count(
"LMS Quiz Submission", {"owner": frappe.session.user, "quiz": quiz_name} "LMS Quiz Submission", {"owner": frappe.session.user, "quiz": quiz_name}
) )

View File

@@ -785,12 +785,13 @@ input[type=checkbox] {
} }
.breadcrumb { .breadcrumb {
display: flex; display: flex;
align-items: center; align-items: center;
font-size: var(--text-base); font-size: var(--text-base);
line-height: 20px; line-height: 20px;
color: var(--gray-900); color: var(--gray-900);
padding: 0; padding: 0;
border-radius: 0;
} }
.course-details-outline { .course-details-outline {
@@ -2386,6 +2387,7 @@ select {
border: 1px solid var(--gray-200) !important; border: 1px solid var(--gray-200) !important;
border-radius: var(--border-radius-md) !important; border-radius: var(--border-radius-md) !important;
background-color: var(--gray-100) !important; background-color: var(--gray-100) !important;
overflow: auto;
} }
.toastui-calendar-panel .toastui-calendar-day-names.toastui-calendar-week { .toastui-calendar-panel .toastui-calendar-day-names.toastui-calendar-week {
@@ -2441,13 +2443,23 @@ select {
} }
.calendar-legends { .calendar-legends {
display: flex; display: grid;
align-items: center; grid-template-columns: repeat(4, 1fr);
justify-content: space-between; width: 75%;
width: 50%;
margin: 0 auto 1rem; margin: 0 auto 1rem;
} }
@media (max-width: 767px) {
.calendar-legends {
grid-template-columns: repeat(2, 1fr);
width: 100%;
}
.legend-item {
margin-bottom: 0.5rem;
}
}
.batch-details { .batch-details {
width: 50%; width: 50%;
margin: 2rem 0; margin: 2rem 0;
@@ -2473,4 +2485,16 @@ select {
.modal-body .ql-container { .modal-body .ql-container {
max-height: unset !important; max-height: unset !important;
}
.questions-table .row-index {
display: none;
}
.text-color {
color: var(--text-color);
}
.toastui-calendar-weekday-event-block {
box-shadow: none !important;
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -267,15 +267,6 @@ const open_batch_dialog = () => {
fieldname: "published", fieldname: "published",
default: batch_info && batch_info.published, default: batch_info && batch_info.published,
}, },
{
fieldtype: "Column Break",
},
{
fieldtype: "Int",
label: __("Seat Count"),
fieldname: "seat_count",
default: batch_info && batch_info.seat_count,
},
{ {
fieldtype: "Section Break", fieldtype: "Section Break",
}, },
@@ -293,13 +284,6 @@ const open_batch_dialog = () => {
reqd: 1, reqd: 1,
default: batch_info && batch_info.end_date, default: batch_info && batch_info.end_date,
}, },
{
fieldtype: "Select",
label: __("Medium"),
fieldname: "medium",
options: ["Online", "Offline"],
default: (batch_info && batch_info.medium) || "Online",
},
{ {
fieldtype: "Column Break", fieldtype: "Column Break",
}, },
@@ -308,12 +292,24 @@ const open_batch_dialog = () => {
label: __("Start Time"), label: __("Start Time"),
fieldname: "start_time", fieldname: "start_time",
default: batch_info && batch_info.start_time, default: batch_info && batch_info.start_time,
reqd: 1,
}, },
{ {
fieldtype: "Time", fieldtype: "Time",
label: __("End Time"), label: __("End Time"),
fieldname: "end_time", fieldname: "end_time",
default: batch_info && batch_info.end_time, default: batch_info && batch_info.end_time,
reqd: 1,
},
{
fieldtype: "Section Break",
},
{
fieldtype: "Select",
label: __("Medium"),
fieldname: "medium",
options: ["Online", "Offline"],
default: (batch_info && batch_info.medium) || "Online",
}, },
{ {
fieldtype: "Link", fieldtype: "Link",
@@ -323,6 +319,21 @@ const open_batch_dialog = () => {
only_select: 1, only_select: 1,
default: batch_info && batch_info.category, default: batch_info && batch_info.category,
}, },
{
fieldtype: "Column Break",
},
{
fieldtype: "Int",
label: __("Seat Count"),
fieldname: "seat_count",
default: batch_info && batch_info.seat_count,
},
{
fieldtype: "Date",
label: __("Evaluation End Date"),
fieldname: "evaluation_end_date",
default: batch_info && batch_info.evaluation_end_date,
},
{ {
fieldtype: "Section Break", fieldtype: "Section Break",
}, },
@@ -381,6 +392,15 @@ const open_batch_dialog = () => {
depends_on: "paid_batch", depends_on: "paid_batch",
only_select: 1, only_select: 1,
}, },
{
fieldtype: "Currency",
label: __("Amount (USD)"),
fieldname: "amount_usd",
depends_on: "paid_batch",
description: __(
"If you set an amount here, then the USD equivalent setting will not get applied."
),
},
], ],
primary_action_label: __("Save"), primary_action_label: __("Save"),
primary_action: (values) => { primary_action: (values) => {

View File

@@ -1,5 +1,3 @@
{% set certificates = get_certificates(user) %}
{% if certificates | length %} {% if certificates | length %}
<div class="cards-parent"> <div class="cards-parent">
{% for certificate in certificates %} {% for certificate in certificates %}

View File

@@ -0,0 +1,10 @@
<p>
{{ _("{0} has submitted the assignment {1}").format(frappe.bold(member_name), frappe.bold(assignment_title)) }}
</p>
<br>
<p> {{ _(" Please evaluate and grade it.") }} </p>
<br>`
<a href="/assignment-submission/{{ assignment_name }}/{{ submission_name }}">
{{ _("Open Assignment") }}
</a>

View File

@@ -3,7 +3,7 @@
</p> </p>
<br> <br>
<p> <p>
{{ _("I am pleased to inform you that your enrollment for the upcoming training batch has been successfully processed. Congratulations!") }} {{ _("We are pleased to inform you that you have been enrolled in our upcoming batch. Congratulations!") }}
</p> </p>
<br> <br>
<p> <p>

View File

@@ -0,0 +1,21 @@
<p>
{{ _("Dear ") }} {{ student_name }},
</p>
<br>
<p>
{{ _("I am delighted to inform you that you have successfully earned your certification for the {0} course. Congratulations!").format(frappe.bold(course_title)) }}
</p>
<br>
<p>
{{ _("With this certification, you can now showcase your updated skills and share your achievement with your colleagues and on LinkedIn. To access your certificate, please click on the link provided below.") }}
</p>
<br>
<a href="/courses/{{ course_name }}/{{certificate_name}}">{{ _("Certificate Link") }}</a>
<br>
<p>
{{ _("Once again, congratulations on this significant accomplishment.")}}
</p>
<br>
<p>
{{ _("Best Regards") }}
</p>

View File

@@ -0,0 +1,11 @@
<p>
{{ _("{0} mentioned you in a comment in your batch.").format(sender) }}
</p>
<p>
<blockquote>
{{ content | markdown }}
</blockquote>
</p>
<div class="more-info">
<a href="{{ link }}">{{ _("Check Discussion") }}</a>
</div>

View File

@@ -6,6 +6,12 @@
{{ _("This quiz consists of {0} questions.").format(quiz.questions | length) }} {{ _("This quiz consists of {0} questions.").format(quiz.questions | length) }}
</li> </li>
{% if quiz.passing_percentage %}
<li>
{{ _("You will have to get {0}% correct answers in order to pass the quiz.").format(quiz.passing_percentage) }}
</li>
{% endif %}
{% if quiz.max_attempts %} {% if quiz.max_attempts %}
{% set suffix = "times" if quiz.max_attempts > 1 else "time" %} {% set suffix = "times" if quiz.max_attempts > 1 else "time" %}
<li> <li>
@@ -18,8 +24,7 @@
{{ _("The quiz has a time limit. For each question you will be given {0} seconds.").format(quiz.time) }} {{ _("The quiz has a time limit. For each question you will be given {0} seconds.").format(quiz.time) }}
</li> </li>
{% endif %} {% endif %}
</ul> </ul>
<div id="start-banner" class="common-card-style column-card align-items-center"> <div id="start-banner" class="common-card-style column-card align-items-center">
@@ -50,8 +55,12 @@
<div class="question hide" data-name="{{ question.name }}" data-type="{{ question.type }}" <div class="question hide" data-name="{{ question.name }}" data-type="{{ question.type }}"
data-multi="{{ question.multiple }}" data-qt-index="{{ loop.index }}"> data-multi="{{ question.multiple }}" data-qt-index="{{ loop.index }}">
<div> <div>
<div class="pull-right font-weight-bold">
{{ question.marks }} {{ _("Marks") }}
</div>
<div class="question-number"> <div class="question-number">
{{ _("Question ") }}{{ loop.index }}: {{ instruction }}</div> {{ _("Question ") }}{{ loop.index }}: {{ instruction }}
</div>
<div class="question-text"> <div class="question-text">
{{ question.question }} {{ question.question }}
</div> </div>

View File

@@ -4,6 +4,7 @@ frappe.ready(() => {
this.answer = []; this.answer = [];
this.is_correct = []; this.is_correct = [];
this.show_answers = $("#quiz-title").data("show-answers"); this.show_answers = $("#quiz-title").data("show-answers");
this.current_index = 0;
localStorage.removeItem($("#quiz-title").data("name")); localStorage.removeItem($("#quiz-title").data("name"));
$(".btn-start-quiz").click((e) => { $(".btn-start-quiz").click((e) => {
@@ -37,7 +38,6 @@ frappe.ready(() => {
$("#next").click((e) => { $("#next").click((e) => {
e.preventDefault(); e.preventDefault();
if (!this.show_answers) check_answer(); if (!this.show_answers) check_answer();
mark_active_question(e); mark_active_question(e);
}); });
@@ -48,7 +48,7 @@ frappe.ready(() => {
const mark_active_question = (e = undefined) => { const mark_active_question = (e = undefined) => {
let total_questions = $(".question").length; let total_questions = $(".question").length;
let current_index = $(".active-question").attr("data-qt-index") || 0; let current_index = this.current_index;
let next_index = parseInt(current_index) + 1; let next_index = parseInt(current_index) + 1;
if (this.show_answers) { if (this.show_answers) {
@@ -120,7 +120,6 @@ const enable_check = (e) => {
const quiz_summary = (e = undefined) => { const quiz_summary = (e = undefined) => {
e && e.preventDefault(); e && e.preventDefault();
let quiz_name = $("#quiz-title").data("name"); let quiz_name = $("#quiz-title").data("name");
let total_questions = $(".question").length;
let self = this; let self = this;
frappe.call({ frappe.call({
@@ -135,14 +134,20 @@ const quiz_summary = (e = undefined) => {
$(".quiz-footer span").addClass("hide"); $(".quiz-footer span").addClass("hide");
$("#quiz-form").prepend( $("#quiz-form").prepend(
`<div class="summary bold-heading text-center"> `<div class="summary bold-heading text-center">
${__("You got")} ${Math.ceil(data.message.percentage)}% ${__("correct answers")}
</div>
<div class="summary bold-heading text-center mt-2">
${__("Your score is")} ${data.message.score} ${__("Your score is")} ${data.message.score}
${__("out of")} ${total_questions} ${__("out of")} ${data.message.score_out_of}
</div>` </div>`
); );
$("#try-again").attr("data-submission", data.message.submission); $("#try-again").attr("data-submission", data.message.submission);
$("#try-again").removeClass("hide"); $("#try-again").removeClass("hide");
self.quiz_submitted = true; self.quiz_submitted = true;
if (this.hasOwnProperty("marked_as_complete")) { if (
this.hasOwnProperty("marked_as_complete") &&
data.message.pass
) {
mark_progress(); mark_progress();
} }
}, },
@@ -165,7 +170,7 @@ const check_answer = (e = undefined) => {
e && e.preventDefault(); e && e.preventDefault();
let answer = $(".active-question textarea"); let answer = $(".active-question textarea");
let total_questions = $(".question").length; let total_questions = $(".question").length;
let current_index = $(".active-question").attr("data-qt-index"); let current_index = this.current_index;
if (answer.length && !answer.val().trim()) { if (answer.length && !answer.val().trim()) {
frappe.throw(__("Please enter your answer")); frappe.throw(__("Please enter your answer"));
@@ -177,12 +182,13 @@ const check_answer = (e = undefined) => {
$(".explanation").removeClass("hide"); $(".explanation").removeClass("hide");
$("#check").addClass("hide"); $("#check").addClass("hide");
if (current_index == total_questions) { if (current_index == total_questions - 1) {
$("#summary").removeClass("hide"); $("#summary").removeClass("hide");
} else if (this.show_answers) { } else if (this.show_answers) {
$("#next").removeClass("hide"); $("#next").removeClass("hide");
} }
parse_options(); parse_options();
this.current_index += 1;
}; };
const parse_options = () => { const parse_options = () => {
@@ -233,7 +239,9 @@ const parse_choices = (element, is_correct) => {
? add_icon(elem, "check") ? add_icon(elem, "check")
: add_icon(elem, "wrong"); : add_icon(elem, "wrong");
} else { } else {
add_icon(elem, "minus-circle"); if (this.show_answers && is_correct[i] == 2)
add_icon(elem, "minus-circle-green");
else add_icon(elem, "minus-circle");
} }
}); });
}; };
@@ -272,12 +280,10 @@ const add_icon = (element, icon) => {
}; };
const add_to_local_storage = () => { const add_to_local_storage = () => {
let current_index = $(".active-question").attr("data-qt-index");
let quiz_name = $("#quiz-title").data("name"); let quiz_name = $("#quiz-title").data("name");
let quiz_stored = JSON.parse(localStorage.getItem(quiz_name)); let quiz_stored = JSON.parse(localStorage.getItem(quiz_name));
let quiz_obj = { let quiz_obj = {
question_index: current_index - 1, question_index: this.current_index,
answer: self.answer.join(), answer: self.answer.join(),
is_correct: self.is_correct, is_correct: self.is_correct,
}; };

View File

@@ -6,8 +6,4 @@
<a class="btn btn-secondary btn-sm" href="/login?redirect-to=/courses/{{ course.name }}"> <a class="btn btn-secondary btn-sm" href="/login?redirect-to=/courses/{{ course.name }}">
{{ _("Write a review") }} {{ _("Write a review") }}
</a> </a>
{% elif show_start_learing_cta(course, membership) %}
<div class="btn btn-secondary btn-sm enroll-in-course" data-course="{{ course.name | urlencode }}">
{{ _("Start Learning") }}
</div>
{% endif %} {% endif %}

View File

@@ -7,7 +7,7 @@ def get_context(context):
context.no_cache = 1 context.no_cache = 1
if frappe.session.user == "Guest": if frappe.session.user == "Guest":
raise frappe.PermissionError(_("You don't have permission to access this page.")) raise frappe.PermissionError(_("Please login to submit the assignment."))
context.is_moderator = has_course_moderator_role() context.is_moderator = has_course_moderator_role()
submission = frappe.form_dict["submission"] submission = frappe.form_dict["submission"]

View File

@@ -70,7 +70,7 @@
{{ _("Title") }} {{ _("Title") }}
</div> </div>
<div class=""> <div class="">
<input id="lesson-title" type="text" class="field-input" data-index="{{ lesson_index }}" data-chapter="{{ chapter }}" data-course="{{ course.name }}" {% if lesson.name %} data-lesson="{{ lesson.name }}" value="{{ lesson.title }}" {% endif %}> <input id="lesson-title" type="text" class="field-input" data-index="{{ lesson_index }}" data-chapter="{{ chapter | urlencode }}" data-course="{{ course.name }}" {% if lesson.name %} data-lesson="{{ lesson.name }}" value="{{ lesson.title }}" {% endif %}>
</div> </div>
</div> </div>
@@ -127,7 +127,7 @@
{%- block script %} {%- block script %}
{{ super() }} {{ super() }}
<script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@latest"></script> <script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/paragraph@latest"></script> <script src="https://cdn.jsdelivr.net/npm/@editorjs/paragraph@2.10.0"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/header@latest"></script> <script src="https://cdn.jsdelivr.net/npm/@editorjs/header@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/embed@latest"></script> <script src="https://cdn.jsdelivr.net/npm/@editorjs/embed@latest"></script>
{% endblock %} {% endblock %}

View File

@@ -49,9 +49,9 @@ const get_tools = () => {
vimeo: true, vimeo: true,
codepen: true, codepen: true,
slides: { slides: {
regex: /https:\/\/docs\.google\.com\/presentation\/d\/e\/([A-Za-z0-9_-]+)\/pub/, regex: /https:\/\/docs\.google\.com\/presentation\/d\/([A-Za-z0-9_-]+)\/pub/,
embedUrl: embedUrl:
"https://docs.google.com/presentation/d/e/<%= remote_id %>/embed", "https://docs.google.com/presentation/d/<%= remote_id %>/embed",
html: "<iframe width='100%' height='300' frameborder='0' allowfullscreen='true'></iframe>", html: "<iframe width='100%' height='300' frameborder='0' allowfullscreen='true'></iframe>",
}, },
}, },
@@ -234,7 +234,7 @@ const save = () => {
args: { args: {
title: $("#lesson-title").val(), title: $("#lesson-title").val(),
body: this.lesson_content_data, body: this.lesson_content_data,
chapter: $("#lesson-title").data("chapter"), chapter: decodeURIComponent($("#lesson-title").data("chapter")),
preview: $("#preview").prop("checked") ? 1 : 0, preview: $("#preview").prop("checked") ? 1 : 0,
idx: $("#lesson-title").data("index"), idx: $("#lesson-title").data("index"),
lesson: lesson ? lesson : "", lesson: lesson ? lesson : "",
@@ -429,9 +429,9 @@ class Quiz {
} }
render_quiz(quiz) { render_quiz(quiz) {
return `<div class="common-card-style p-2 my-2 bold-heading"> return `<a class="common-card-style p-20 my-2 justify-center bold-heading" target="_blank" href=/quizzes/${quiz}>
Quiz: ${quiz} Quiz: ${quiz}
</div>`; </a>`;
} }
validate(savedData) { validate(savedData) {

View File

@@ -107,10 +107,13 @@ def get_page_extensions(context):
def get_neighbours(current, lessons): def get_neighbours(current, lessons):
current = flt(current) numbers = [lesson.number for lesson in lessons]
numbers = sorted(lesson.number for lesson in lessons) tuples_list = [tuple(int(x) for x in s.split(".")) for s in numbers]
index = numbers.index(current) sorted_tuples = sorted(tuples_list)
sorted_numbers = [".".join(str(num) for num in t) for t in sorted_tuples]
index = sorted_numbers.index(current)
return { return {
"prev": numbers[index - 1] if index - 1 >= 0 else None, "prev": sorted_numbers[index - 1] if index - 1 >= 0 else None,
"next": numbers[index + 1] if index + 1 < len(numbers) else None, "next": sorted_numbers[index + 1] if index + 1 < len(sorted_numbers) else None,
} }

View File

@@ -16,25 +16,9 @@
{% macro QuizForm(quiz) %} {% macro QuizForm(quiz) %}
<div id="quiz-form" {% if quiz.name %} data-name="{{ quiz.name }}" data-index="{{ quiz.questions | length }}" {% endif %}> <div id="quiz-form" {% if quiz.name %} data-name="{{ quiz.name }}" data-index="{{ quiz.questions | length }}" {% endif %}>
{{ QuizDetails(quiz) }} {{ QuizDetails(quiz) }}
{% if quiz.questions %} <div class="field-group">
<div class="field-group"> <div class="questions-table"></div>
<div class="field-label mb-1"> </div>
{{ _("Questions") }}
</div>
<div class="common-card-style column-card px-3 py-0">
{% for question in quiz.questions %}
{{ Question(question, loop.index) }}
{% endfor %}
</div>
<button class="btn btn-secondary btn-sm btn-add-question mt-4">
{{ _("Add Question") }}
</button>
</div>
{% endif %}
{% if quiz.name and not quiz.questions | length %}
{{ EmptyState() }}
{% endif %}
</div> </div>
{% endmacro %} {% endmacro %}
@@ -59,11 +43,6 @@
</div> </div>
<div class="align-self-center"> <div class="align-self-center">
{% if quiz.name %}
<button class="btn btn-secondary btn-sm btn-add-question mr-2">
{{ _("Add Question") }}
</button>
{% endif %}
<button class="btn btn-primary btn-sm btn-save-quiz"> <button class="btn btn-primary btn-sm btn-save-quiz">
{{ _("Save") }} {{ _("Save") }}
</button> </button>
@@ -98,18 +77,30 @@
{{ _("Enter the maximum number of times a user can attempt this quiz") }} {{ _("Enter the maximum number of times a user can attempt this quiz") }}
</div> </div>
<div> <div>
{% set max_attempts = quiz.max_attempts if quiz.name else 1 %} {% set max_attempts = quiz.max_attempts if quiz.name else 0 %}
<input type="number" class="field-input" id="max-attempts" value="{{ max_attempts }}"> <input type="number" class="field-input" id="max-attempts" value="{{ max_attempts }}">
</div> </div>
</div> </div>
<div class="field-group">
<div class="field-label reqd">
{{ _("Passing Percentage") }}
</div>
<div class="field-description">
{{ _("Minimum percentage required to pass this quiz.") }}
</div>
<div>
<input type="number" class="field-input" id="passing-percentage" value="{{ quiz.passing_percentage }}">
</div>
</div>
<div class="field-group vertically-center"> <div class="field-group vertically-center">
{% set show_answers = quiz.show_answers or not quiz.name %} {% set show_answers = quiz.show_answers or not quiz.name %}
<label for="show-answers" class="vertically-center mb-0"> <label for="show-answers" class="vertically-center mb-0">
<input type="checkbox" id="show-answers" {% if show_answers %} checked {% endif %}> <input type="checkbox" id="show-answers" {% if show_answers %} checked {% endif %}>
{{ _("Show Answers") }} {{ _("Show Answers") }}
</label> </label>
<label for="upcoming" class="vertically-center mb-0 ml-20"> <label for="show-submission-history" class="vertically-center mb-0 ml-20">
<input type="checkbox" id="show-submission-history" {% if quiz.show_submission_history %} checked {% endif %}> <input type="checkbox" id="show-submission-history" {% if quiz.show_submission_history %} checked {% endif %}>
{{ _("Show Submission History") }} {{ _("Show Submission History") }}
</label> </label>
@@ -151,5 +142,9 @@
{%- block script %} {%- block script %}
{{ super() }} {{ super() }}
{{ include_script('controls.bundle.js') }} {% if has_course_instructor_role() or has_course_moderator_role() %}
<script>
const quiz_questions = {{ quiz.questions or [] }}
</script>
{% endif %}
{% endblock %} {% endblock %}

View File

@@ -1,17 +1,21 @@
frappe.ready(() => { frappe.ready(() => {
$(".btn-save-quiz").click((e) => { if ($(".questions-table").length) {
save_quiz({ frappe.require("controls.bundle.js", () => {
quiz_title: $("#quiz-title").val(), create_questions_table();
max_attempts: $("#max-attempts").val(),
}); });
}
$(".btn-save-quiz").click((e) => {
save_quiz();
}); });
$(".question-row").click((e) => { $(".question-row").click((e) => {
edit_question(e); edit_question(e);
}); });
$(".btn-add-question").click((e) => { $(document).on("click", ".questions-table .link-btn", (e) => {
show_question_modal(); e.preventDefault();
fetch_question_data(e);
}); });
}); });
@@ -31,6 +35,8 @@ const show_question_modal = (values = {}) => {
}; };
const get_question_fields = (values = {}) => { const get_question_fields = (values = {}) => {
if (!values.question) values = {};
let dialog_fields = [ let dialog_fields = [
{ {
fieldtype: "Text Editor", fieldtype: "Text Editor",
@@ -66,6 +72,7 @@ const get_question_fields = (values = {}) => {
if (num <= 2) option.mandatory_depends_on = "eval:doc.type=='Choices'"; if (num <= 2) option.mandatory_depends_on = "eval:doc.type=='Choices'";
dialog_fields.push(option); dialog_fields.push(option);
console.log(dialog_fields);
dialog_fields.push({ dialog_fields.push({
fieldtype: "Data", fieldtype: "Data",
@@ -120,12 +127,16 @@ const edit_question = (e) => {
const save_quiz = (values) => { const save_quiz = (values) => {
validate_mandatory(); validate_mandatory();
validate_questions();
frappe.call({ frappe.call({
method: "lms.lms.doctype.lms_quiz.lms_quiz.save_quiz", method: "lms.lms.doctype.lms_quiz.lms_quiz.save_quiz",
args: { args: {
quiz_title: values.quiz_title, quiz_title: $("#quiz-title").val(),
max_attempts: values.max_attempts, max_attempts: $("#max-attempts").val(),
passing_percentage: $("#passing-percentage").val(),
quiz: $("#quiz-form").data("name") || "", quiz: $("#quiz-form").data("name") || "",
questions: this.table.get_value("questions"),
show_answers: $("#show-answers").is(":checked") ? 1 : 0, show_answers: $("#show-answers").is(":checked") ? 1 : 0,
show_submission_history: $("#show-submission-history").is( show_submission_history: $("#show-submission-history").is(
":checked" ":checked"
@@ -146,13 +157,45 @@ const save_quiz = (values) => {
}; };
const validate_mandatory = () => { const validate_mandatory = () => {
if (!$("#quiz-title").val()) { let fields = ["#quiz-title", "#passing-percentage"];
let error = $("p") fields.forEach((field, idx) => {
.addClass("error-message") if (!$(field).val()) {
.text(__("Please enter a Quiz Title")); let error = $("p")
$(error).insertAfter("#quiz-title"); .addClass("error-message")
$("#quiz-title").focus(); .text(__("Please enter a value"));
throw "Title is mandatory"; $(error).insertAfter(field);
scroll_to_element($(field));
throw "This field is mandatory";
}
});
};
const validate_questions = () => {
let questions = this.table.get_value("questions");
if (!questions.length) {
frappe.throw(__("Please add a question."));
}
questions.forEach((question, index) => {
if (!question.question) {
frappe.throw(__("Please add question in row") + " " + (index + 1));
}
if (!question.marks) {
frappe.throw(__("Please add marks in row") + " " + (index + 1));
}
});
};
const scroll_to_element = (element) => {
if ($(element).length) {
$([document.documentElement, document.body]).animate(
{
scrollTop: $(element).offset().top - 100,
},
1000
);
} }
}; };
@@ -167,13 +210,98 @@ const save_question = (values) => {
callback: (data) => { callback: (data) => {
if (data.message) this.question_dialog.hide(); if (data.message) this.question_dialog.hide();
frappe.show_alert({ if (values.name) {
message: __("Saved"), frappe.show_alert({
indicator: "green", message: __("Saved"),
}); indicator: "green",
setTimeout(() => { });
window.location.reload(); setTimeout(() => {
}, 1000); window.location.reload();
}, 1000);
} else {
let details = {
question: data.message,
};
index = this.table.get_value("questions").length;
add_question_row(details, index);
}
},
});
};
const create_questions_table = () => {
this.table = new frappe.ui.FieldGroup({
fields: [
{
fieldname: "questions",
fieldtype: "Table",
in_place_edit: 1,
label: __("Questions"),
fields: [
{
fieldname: "question",
fieldtype: "Link",
label: __("Question"),
options: "LMS Question",
in_list_view: 1,
only_select: 1,
reqd: 1,
},
{
fieldname: "marks",
fieldtype: "Int",
label: __("Marks"),
in_list_view: 1,
reqd: 1,
},
{
fieldname: "question_name",
fieldname: "Link",
options: "LMS Quiz Question",
label: __("Question Name"),
},
],
},
],
body: $(".questions-table").get(0),
});
this.table.make();
$(".questions-table .form-section:last").removeClass("empty-section");
$(".questions-table .frappe-control").removeClass("hide-control");
$(".questions-table .form-column").addClass("p-0");
quiz_questions.forEach((question, idx) => {
add_question_row(question, idx);
});
this.table.fields_dict["questions"].grid.add_custom_button(
"New Question",
show_question_modal,
"bottom"
);
};
const add_question_row = (question, idx) => {
this.table.fields_dict["questions"].grid.add_new_row();
this.table.get_value("questions")[idx] = {
question: question.question,
marks: question.marks,
};
this.table.refresh();
};
const fetch_question_data = (e) => {
let question_name = $(e.currentTarget)
.find(".btn-open")
.attr("href")
.split("/")[3];
frappe.call({
method: "lms.lms.doctype.lms_question.lms_question.get_question_details",
args: {
question: question_name,
},
callback: (data) => {
show_question_modal(data.message);
}, },
}); });
}; };

View File

@@ -18,14 +18,22 @@ def get_context(context):
if quizname == "new-quiz": if quizname == "new-quiz":
context.quiz = frappe._dict() context.quiz = frappe._dict()
else: else:
fields_arr = ["name", "question", "type"]
context.quiz = frappe.db.get_value( context.quiz = frappe.db.get_value(
"LMS Quiz", "LMS Quiz",
quizname, quizname,
["title", "name", "max_attempts", "show_answers", "show_submission_history"], [
"title",
"name",
"max_attempts",
"passing_percentage",
"show_answers",
"show_submission_history",
],
as_dict=1, as_dict=1,
) )
fields_arr = ["name", "question", "marks"]
context.quiz.questions = frappe.get_all( context.quiz.questions = frappe.get_all(
"LMS Quiz Question", {"parent": quizname}, fields_arr, order_by="idx" "LMS Quiz Question", {"parent": quizname}, fields_arr, order_by="idx"
) )

View File

@@ -49,11 +49,14 @@
<use href="#icon-calendar"></use> <use href="#icon-calendar"></use>
</svg> </svg>
<span> <span>
{{ frappe.utils.format_date(batch_info.start_date, "long") }} - {{ frappe.utils.format_date(batch_info.start_date, "long") }}
</span> </span>
{% if batch_info.start_date != batch_info.end_date %}
<span> <span>
{{ frappe.utils.format_date(batch_info.end_date, "long") }} - {{ frappe.utils.format_date(batch_info.end_date, "long") }}
</span> </span>
{% endif %}
</div> </div>
<span class="seperator"></span> <span class="seperator"></span>
@@ -75,14 +78,6 @@
</div> </div>
</div> </div>
{% if is_moderator %}
<div class="mt-4">
<button class="btn btn-secondary btn-sm btn-email">
{{ _("Email to Students") }}
</button>
</div>
{% endif %}
{% if batch_info.custom_component %} {% if batch_info.custom_component %}
<div class="mt-4"> <div class="mt-4">
{{ batch_info.custom_component }} {{ batch_info.custom_component }}
@@ -96,8 +91,7 @@
<div class="mt-4"> <div class="mt-4">
<ul class="nav lms-nav" id="batches-tab"> <ul class="nav lms-nav" id="batches-tab">
{% if settings.show_dashboard and is_student %}
{% if is_student %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link {% if is_student %} active {% endif %}" data-toggle="tab" href="#dashboard"> <a class="nav-link {% if is_student %} active {% endif %}" data-toggle="tab" href="#dashboard">
{{ _("Dashboard") }} {{ _("Dashboard") }}
@@ -105,6 +99,7 @@
</li> </li>
{% endif %} {% endif %}
{% if settings.show_courses %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link {% if not is_student %} active {% endif %}" data-toggle="tab" href="#courses"> <a class="nav-link {% if not is_student %} active {% endif %}" data-toggle="tab" href="#courses">
{{ _("Courses") }} {{ _("Courses") }}
@@ -113,6 +108,7 @@
</span> </span>
</a> </a>
</li> </li>
{% endif %}
{% if show_timetable %} {% if show_timetable %}
<li class="nav-item"> <li class="nav-item">
@@ -123,40 +119,59 @@
{% endif %} {% endif %}
{% if is_moderator %} {% if is_moderator %}
<li class="nav-item"> {% if settings.show_students %}
<a class="nav-link" data-toggle="tab" href="#students"> <li class="nav-item">
{{ _("Students") }} <a class="nav-link" data-toggle="tab" href="#students">
<span class="course-list-count"> {{ _("Students") }}
{{ batch_students | length }} <span class="course-list-count">
</span> {{ batch_students | length }}
</a> </span>
</li> </a>
</li>
{% endif %}
<li class="nav-item"> {% if settings.show_assessments %}
<a class="nav-link" data-toggle="tab" href="#assessments"> <li class="nav-item">
{{ _("Assessments") }} <a class="nav-link" data-toggle="tab" href="#assessments">
<span class="course-list-count"> {{ _("Assessments") }}
{{ assessments | length }} <span class="course-list-count">
</span> {{ assessments | length }}
</a> </span>
</li> </a>
</li>
{% endif %}
{% if settings.show_emails %}
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#emails">
{{ _("Emails") }}
<span class="course-list-count">
{{ batch_emails | length }}
</span>
</a>
</li>
{% endif %}
{% endif %} {% endif %}
{% if batch_students | length and (is_moderator or is_student) %} {% if batch_students | length and (is_moderator or is_student) %}
<li class="nav-item"> {% if settings.show_discussions %}
<a class="nav-link" data-toggle="tab" href="#discussions"> <li class="nav-item">
{{ _("Discussions") }} <a class="nav-link" data-toggle="tab" href="#discussions">
</a> {{ _("Discussions") }}
</li> </a>
</li>
{% endif %}
<li class="nav-item"> {% if settings.show_live_class %}
<a class="nav-link" data-toggle="tab" href="#live-class"> <li class="nav-item">
{{ _("Live Class") }} <a class="nav-link" data-toggle="tab" href="#live-class">
<span class="course-list-count"> {{ _("Live Class") }}
{{ live_classes | length }} <span class="course-list-count">
</span> {{ live_classes | length }}
</a> </span>
</li> </a>
</li>
{% endif %}
{% endif %} {% endif %}
{% if custom_tabs_header %} {% if custom_tabs_header %}
@@ -168,15 +183,17 @@
<div class="tab-content"> <div class="tab-content">
{% if is_student %} {% if settings.show_dashboard and is_student %}
<div class="tab-pane {% if is_student %} active {% endif %}" id="dashboard" role="tabpanel" aria-labelledby="dashboard"> <div class="tab-pane {% if is_student %} active {% endif %}" id="dashboard" role="tabpanel" aria-labelledby="dashboard">
{{ Dashboard(batch_info, batch_courses, current_student) }} {{ Dashboard(batch_info, batch_courses, current_student) }}
</div> </div>
{% endif %} {% endif %}
{% if settings.show_courses %}
<div class="tab-pane {% if not is_student %} active {% endif %}" id="courses" role="tabpanel" aria-labelledby="courses"> <div class="tab-pane {% if not is_student %} active {% endif %}" id="courses" role="tabpanel" aria-labelledby="courses">
{{ CoursesSection(batch_info, batch_courses) }} {{ CoursesSection(batch_info, batch_courses) }}
</div> </div>
{% endif %}
{% if show_timetable %} {% if show_timetable %}
<div class="tab-pane" id="timetable" role="tabpanel" aria-labelledby="timetable"> <div class="tab-pane" id="timetable" role="tabpanel" aria-labelledby="timetable">
@@ -185,23 +202,37 @@
{% endif %} {% endif %}
{% if is_moderator %} {% if is_moderator %}
<div class="tab-pane" id="students" role="tabpanel" aria-labelledby="students"> {% if settings.show_students %}
{{ StudentsSection(batch_info, batch_students) }} <div class="tab-pane" id="students" role="tabpanel" aria-labelledby="students">
</div> {{ StudentsSection(batch_info, batch_students) }}
</div>
{% endif %}
<div class="tab-pane" id="assessments" role="tabpanel" aria-labelledby="assessments"> {% if settings.show_assessments %}
{{ AssessmentsSection(batch_info) }} <div class="tab-pane" id="assessments" role="tabpanel" aria-labelledby="assessments">
</div> {{ AssessmentsSection(batch_info) }}
</div>
{% endif %}
{% if settings.show_emails %}
<div class="tab-pane" id="emails" role="tabpanel" aria-labelledby="emails">
{{ EmailsSection() }}
</div>
{% endif %}
{% endif %} {% endif %}
{% if batch_students | length and (is_moderator or is_student or is_evaluator) %} {% if batch_students | length and (is_moderator or is_student or is_evaluator) %}
<div class="tab-pane" id="discussions" role="tabpanel" aria-labelledby="discussions"> {% if settings.show_discussions %}
{{ Discussions(batch_info) }} <div class="tab-pane" id="discussions" role="tabpanel" aria-labelledby="discussions">
</div> {{ Discussions(batch_info) }}
</div>
{% endif %}
<div class="tab-pane" id="live-class" role="tabpanel" aria-labelledby="live-class"> {% if settings.show_live_class %}
{{ LiveClassSection(batch_info, live_classes) }} <div class="tab-pane" id="live-class" role="tabpanel" aria-labelledby="live-class">
</div> {{ LiveClassSection(batch_info, live_classes) }}
</div>
{% endif %}
{% endif %} {% endif %}
{% if custom_tabs_content %} {% if custom_tabs_content %}
@@ -376,6 +407,41 @@
</article> </article>
{% endmacro %} {% endmacro %}
{% macro EmailsSection() %}
<div class="my-4">
<button class="btn btn-secondary btn-sm btn-email">
{{ _("Email to Students") }}
</button>
</div>
<div>
{% for email in batch_emails %}
<div class="frappe-card mb-5">
<div class="flex justify-between m-1">
<span class="text-color flex">
<span class="margin-right">
{% set member = frappe.db.get_value("User", email.sender, ["full_name", "username", "name", "user_image"], as_dict=1) %}
{{ widgets.Avatar(member=member, avatar_class="avatar-small") }}
</span>
<span>
{{ member.full_name }}
<div class="text-muted">
<span class="frappe-timestamp" data-timestamp="{{ email.communication_date }}" title="{{ communication_date }}">
{{ frappe.utils.pretty_date(email.communication_date) }}
</span>
</div>
</span>
</span>
</div>
<div class="ml-10">
{{ email.content }}
</div>
</div>
{% endfor %}
</div>
{% endmacro %}
{% macro AssessmentList(assessments) %} {% macro AssessmentList(assessments) %}
{% if assessments | length %} {% if assessments | length %}
<div class="form-grid"> <div class="form-grid">
@@ -575,9 +641,12 @@
frappe.boot.single_types = [] frappe.boot.single_types = []
let courses = {{ course_list | json }}; let courses = {{ course_list | json }};
const legends = {{ legends | json }}; const legends = {{ legends | json }};
const allow_future = {{ batch_info.allow_future }} const allow_future = {{ batch_info.allow_future }};
const is_student = "{{ is_student or '' }}";
const evaluation_end_date = "{{ batch_info.evaluation_end_date if batch_info.evaluation_end_date else '' }}"
const show_day_view = {{ settings.show_day_view }};
</script> </script>
<link rel="stylesheet" href="https://uicdn.toast.com/calendar/latest/toastui-calendar.min.css" /> <link rel="stylesheet" href="https://uicdn.toast.com/calendar/latest/toastui-calendar.min.css" />
<script src="https://uicdn.toast.com/calendar/latest/toastui-calendar.min.js"></script> <script src="https://uicdn.toast.com/calendar/latest/toastui-calendar.min.js"></script>
{% endblock %} {% endblock %}

View File

@@ -517,6 +517,10 @@ const open_evaluation_form = (e) => {
}, },
filter_description: " ", filter_description: " ",
only_select: 1, only_select: 1,
change: () => {
this.eval_form.set_value("date", "");
$("[data-fieldname='slots']").html("");
},
}, },
{ {
fieldtype: "Date", fieldtype: "Date",
@@ -526,8 +530,11 @@ const open_evaluation_form = (e) => {
min_date: new Date( min_date: new Date(
frappe.datetime.add_days(frappe.datetime.get_today(), 1) frappe.datetime.add_days(frappe.datetime.get_today(), 1)
), ),
max_date: evaluation_end_date
? new Date(evaluation_end_date)
: "",
change: () => { change: () => {
get_slots(); if (this.eval_form.get_value("date")) get_slots();
}, },
}, },
{ {
@@ -552,7 +559,7 @@ const get_slots = () => {
args: { args: {
course: this.eval_form.get_value("course"), course: this.eval_form.get_value("course"),
date: this.eval_form.get_value("date"), date: this.eval_form.get_value("date"),
batch_name: $(".class-details").data("batch"), batch: $(".class-details").data("batch"),
}, },
callback: (r) => { callback: (r) => {
if (r.message) { if (r.message) {
@@ -653,7 +660,8 @@ const setup_calendar = (events) => {
const options = get_calendar_options(element, calendar_id); const options = get_calendar_options(element, calendar_id);
const calendar = new Calendar(container, options); const calendar = new Calendar(container, options);
this.calendar_ = calendar; this.calendar_ = calendar;
create_events(calendar, events);
create_events(calendar, events, calendar_id);
add_links_to_events(calendar, events); add_links_to_events(calendar, events);
scroll_to_date(calendar, events); scroll_to_date(calendar, events);
set_calendar_range(calendar, events); set_calendar_range(calendar, events);
@@ -664,7 +672,7 @@ const get_calendar_options = (element, calendar_id) => {
const end_time = element.data("end"); const end_time = element.data("end");
return { return {
defaultView: "week", defaultView: $(window).width() < 768 || show_day_view ? "day" : "week",
usageStatistics: false, usageStatistics: false,
week: { week: {
narrowWeekend: true, narrowWeekend: true,
@@ -684,13 +692,35 @@ const get_calendar_options = (element, calendar_id) => {
}, },
], ],
template: { template: {
time: function (event) { allday: function (event) {
return `<div class="calendar-event-time"> let hide = event.raw.completed ? "" : "hide";
<div> ${frappe.datetime.get_time(event.start.d.d)} - return `<div class="calendar-event-time" title="${
${frappe.datetime.get_time(event.end.d.d)} </div> event.title
} - ${frappe.datetime.get_time(
event.start.d.d
)} - ${frappe.datetime.get_time(event.end.d.d)}">
<img class='icon icon-sm pull-right ${hide}' src="/assets/lms/icons/check.svg">
<div class="calendar-event-title"> ${event.title} </div> <div class="calendar-event-title"> ${event.title} </div>
</div>`; </div>`;
}, },
time: function (event) {
let hide = event.raw.completed ? "" : "hide";
return `<div class="calendar-event-time" title="${
event.title
} - ${frappe.datetime.get_time(
event.start.d.d
)} - ${frappe.datetime.get_time(event.end.d.d)}">
<img class='icon icon-sm pull-right ${hide}' src="/assets/lms/icons/check.svg">
<div>
<span class="calendar-event-title"> ${event.title} </span>
<span>
${frappe.datetime.get_time(event.start.d.d)} - ${frappe.datetime.get_time(
event.end.d.d
)}
</span>
</div>
</div>`;
},
}, },
}; };
}; };
@@ -703,9 +733,10 @@ const create_events = (calendar, events, calendar_id) => {
id: `event${idx}`, id: `event${idx}`,
calendarId: calendar_id, calendarId: calendar_id,
title: event.title, title: event.title,
start: `${event.date}T${event.start_time}`, start: `${event.date}T${format_time(event.start_time)}`,
end: `${event.date}T${event.end_time}`, end: `${event.date}T${format_time(event.end_time)}`,
isAllday: event.start_time ? false : true, isAllday: event.start_time ? false : true,
category: event.start_time ? "time" : "allday",
borderColor: clr, borderColor: clr,
backgroundColor: "var(--fg-color)", backgroundColor: "var(--fg-color)",
customStyle: { customStyle: {
@@ -716,6 +747,11 @@ const create_events = (calendar, events, calendar_id) => {
}, },
raw: { raw: {
url: event.url, url: event.url,
milestone: event.milestone,
name: event.name,
idx: event.idx,
parent: event.parent,
completed: event.completed,
}, },
}); });
}); });
@@ -723,47 +759,92 @@ const create_events = (calendar, events, calendar_id) => {
calendar.createEvents(calendar_events); calendar.createEvents(calendar_events);
}; };
const format_time = (time) => {
if (!time) return "00:00:00";
let time_arr = time.split(":");
if (time_arr[0] < 10) time_arr[0] = "0" + time_arr[0];
return time_arr.join(":");
};
const add_links_to_events = (calendar) => { const add_links_to_events = (calendar) => {
calendar.on("clickEvent", ({ event }) => { calendar.on("clickEvent", ({ event }) => {
let event_date = event.start.d.d; let event_date = event.start.d.d;
event_date = moment(event_date).format("YYYY-MM-DD"); event_date = moment(event_date).format("YYYY-MM-DD");
let current_date = moment().format("YYYY-MM-DD"); let current_date = moment().format("YYYY-MM-DD");
if (allow_future || moment(event_date).isSameOrBefore(current_date)) {
window.open(event.raw.url, "_blank"); if (
} is_student &&
!moment(event_date).isSameOrBefore(current_date) &&
!allow_future
)
return;
if (is_student && event.raw.milestone) {
frappe.call({
method: "lms.lms.doctype.lms_batch.lms_batch.is_milestone_complete",
args: {
idx: event.raw.idx,
batch: event.raw.parent,
},
callback: (data) => {
if (data.message) window.open(event.raw.url, "_blank");
else
frappe.show_alert({
message:
"Please complete all previous activities to proceed.",
indicator: "red",
});
},
});
} else window.open(event.raw.url, "_blank");
}); });
}; };
const scroll_to_date = (calendar, events) => { const scroll_to_date = (calendar, events) => {
if ( if (
new Date() < new Date(events[0].date) || new Date() < new Date(events[0].date) ||
new Date() > new Date(events.slice(-1).date) new Date() > new Date(events.slice(-1)[0].date)
) { ) {
calendar.setDate(new Date(events[0].date)); calendar.setDate(new Date(events[0].date));
} }
}; };
const set_calendar_range = (calendar, events) => { const set_calendar_range = (calendar, events) => {
let week_start = moment(calendar.getDateRangeStart().d.d); let day_view = $(window).width() < 768 || show_day_view ? true : false;
let week_end = moment(calendar.getDateRangeEnd().d.d); if (day_view) {
let calendar_date = moment(calendar.getDate().d.d).format(
"DD MMMM YYYY"
);
$(".calendar-range").text(`${calendar_date}`);
$(".calendar-range").text( if (moment(calendar_date).isSameOrBefore(moment(events[0].date)))
`${moment(week_start).format("DD MMMM YYYY")} - ${moment( $("#prev-week").hide();
week_end else $("#prev-week").show();
).format("DD MMMM YYYY")}`
);
if (week_start.diff(moment(events[0].date), "days") <= 0) { if (
$("#prev-week").hide(); moment(calendar_date).isSameOrAfter(
moment(events.slice(-1)[0].date)
)
)
$("#next-week").hide();
else $("#next-week").show();
} else { } else {
$("#prev-week").show(); let week_start = moment(calendar.getDateRangeStart().d.d);
} let week_end = moment(calendar.getDateRangeEnd().d.d);
if (week_end.diff(moment(events.slice(-1)[0].date), "days") > 0) { $(".calendar-range").text(
$("#next-week").hide(); `${moment(week_start).format("DD MMMM YYYY")} - ${moment(
} else { week_end
$("#next-week").show(); ).format("DD MMMM YYYY")}`
);
if (week_start.diff(moment(events[0].date), "days") <= 0)
$("#prev-week").hide();
else $("#prev-week").show();
if (week_end.diff(moment(events.slice(-1)[0].date), "days") > 0)
$("#next-week").hide();
else $("#next-week").show();
} }
}; };
@@ -808,12 +889,33 @@ const email_to_students = () => {
const send_email = (values) => { const send_email = (values) => {
frappe.call({ frappe.call({
method: "lms.lms.doctype.lms_batch.lms_batch.send_email_to_students", method: "frappe.client.get_list",
args: { args: {
batch: $(".class-details").data("batch"), doctype: "Batch Student",
parent: "LMS Batch",
fields: ["student"],
filters: {
parent: $(".class-details").data("batch"),
},
},
callback: (data) => {
send_email_to_students(data.message, values);
},
});
};
const send_email_to_students = (students, values) => {
students = students.map((row) => row.student);
frappe.call({
method: "frappe.core.doctype.communication.email.make",
args: {
recipients: students.join(", "),
cc: values.reply_to,
subject: values.subject, subject: values.subject,
reply_to: values.reply_to, content: values.message,
message: values.message, doctype: "LMS Batch",
name: $(".class-details").data("batch"),
send_email: 1,
}, },
callback: (r) => { callback: (r) => {
this.email_dialog.hide(); this.email_dialog.hide();
@@ -821,6 +923,9 @@ const send_email = (values) => {
message: __("Email sent successfully"), message: __("Email sent successfully"),
indicator: "green", indicator: "green",
}); });
setTimeout(() => {
window.location.reload();
}, 2000);
}, },
}); });
}; };

View File

@@ -43,6 +43,8 @@ def get_context(context):
"batch_details", "batch_details",
"published", "published",
"allow_future", "allow_future",
"evaluation_end_date",
"meta_image",
], ],
as_dict=True, as_dict=True,
) )
@@ -71,6 +73,13 @@ def get_context(context):
) )
context.course_name_list = [course.course for course in context.batch_courses] context.course_name_list = [course.course for course in context.batch_courses]
context.assessments = get_assessments(batch_name) context.assessments = get_assessments(batch_name)
context.batch_emails = frappe.get_all(
"Communication",
filters={"reference_doctype": "LMS Batch", "reference_name": batch_name},
fields=["subject", "content", "recipients", "cc", "communication_date", "sender"],
order_by="communication_date desc",
)
context.batch_students = get_class_student_details( context.batch_students = get_class_student_details(
batch_students, batch_courses, context.assessments batch_students, batch_courses, context.assessments
) )
@@ -98,9 +107,9 @@ def get_context(context):
}, },
) )
context.legends = get_legends(batch_name) context.legends = get_legends(batch_name)
context.settings = frappe.get_single("LMS Settings")
custom_tabs = frappe.get_hooks("lms_batch_tabs") custom_tabs = frappe.get_hooks("lms_batch_tabs")
if custom_tabs: if custom_tabs:
context.custom_tabs_header = custom_tabs.get("header_html")[0] context.custom_tabs_header = custom_tabs.get("header_html")[0]
context.custom_tabs_content = custom_tabs.get("content_html")[0] context.custom_tabs_content = custom_tabs.get("content_html")[0]
@@ -148,7 +157,6 @@ def get_class_course_details(batch_courses):
"image", "image",
"upcoming", "upcoming",
"short_introduction", "short_introduction",
"image",
"paid_course", "paid_course",
"course_price", "course_price",
"enable_certification", "enable_certification",

View File

@@ -1,238 +1,214 @@
{% extends "lms/templates/lms_base.html" %} {% extends "lms/templates/lms_base.html" %} {% block title %} {{
{% block title %} _(batch_info.title) }} {% endblock %} {% block page_content %}
{{ _(batch_info.title) }}
{% endblock %}
{% block page_content %}
<div class="common-page-style lms-page-style"> <div class="common-page-style lms-page-style">
{{ BatchHeader(batch_info) }} {{ BatchHeader(batch_info) }}
<div class="container"> <div class="container">
{{ BatchOverlay(batch_info, courses, students) }} {{ BatchOverlay(batch_info, courses, students) }}
<div class="pt-10"> <div class="pt-10">
{{ BatchDetails(batch_info) }} {{ BatchDetails(batch_info) }} {{ CourseList(courses) }}
{{ CourseList(courses) }} </div>
</div>
</div> </div>
{{ BatchDetailsRaw() }} {{ BatchDetailsRaw() }}
</div> </div>
{% endblock %} {% endblock %} {% macro BatchHeader(batch_info) %}
{% macro BatchHeader(batch_info) %}
<div class="course-head-container"> <div class="course-head-container">
<div class="container"> <div class="container">
<div class="course-card-wide"> <div class="course-card-wide">
{{ BreadCrumb(batch_info) }} {{ BreadCrumb(batch_info) }} {{ BatchHeaderDetails(batch_info,
{{ BatchHeaderDetails(batch_info, courses, students) }} courses, students) }}
</div> </div>
</div> </div>
</div> </div>
{% endmacro %} {% endmacro %} {% macro BreadCrumb(batch_info) %}
{% macro BreadCrumb(batch_info) %}
<article class="mb-8"> <article class="mb-8">
<a class="dark-links" href="/batches"> <a class="dark-links" href="/batches"> {{ _("All Batches") }} </a>
{{ _("All Batches") }} <img class="" src="/assets/lms/icons/chevron-right.svg" />
</a> <span class="breadcrumb-destination"> {{ _("Batch Details") }} </span>
<img class="" src="/assets/lms/icons/chevron-right.svg">
<span class="breadcrumb-destination">
{{ _("Batch Details") }}
</span>
</article> </article>
{% endmacro %} {% endmacro %} {% macro BatchHeaderDetails(batch_info, courses, students) %}
<div class="class-details" data-batch="{{ batch_info.name }}">
<div class="page-title">{{ batch_info.title }}</div>
{% macro BatchHeaderDetails(batch_info, courses, students) %} <div class="">{{ batch_info.description }}</div>
<div class="class-details" data-batch="{{ batch_info.name }}">
<div class="page-title"> <div class="mt-8">
{{ batch_info.title }} <svg class="icon icon-sm">
</div> <use href="#icon-calendar"></use>
</svg>
<span>
{{ frappe.utils.format_date(batch_info.start_date, "long") }}
</span>
{% if batch_info.start_date != batch_info.end_date %}
<span>
- {{ frappe.utils.format_date(batch_info.end_date, "long") }}
</span>
{% endif %}
</div>
<div class=""> {% if batch_info.start_time and batch_info.end_time %}
{{ batch_info.description }} <div class="mt-1">
</div> <svg class="icon icon-sm">
<use href="#icon-clock"></use>
<div class="mt-8"> </svg>
<svg class="icon icon-sm"> <span>
<use href="#icon-calendar"></use> {{ frappe.utils.format_time(batch_info.start_time, "hh:mm a") }} -
</svg> </span>
<span> <span>
{{ frappe.utils.format_date(batch_info.start_date, "long") }} - {{ frappe.utils.format_time(batch_info.end_time, "hh:mm a") }}
</span> </span>
<span> </div>
{{ frappe.utils.format_date(batch_info.end_date, "long") }} {% endif %}
</span> </div>
</div> {% endmacro %} {% macro BatchOverlay(batch_info, courses, students) %}
{% if batch_info.start_time and batch_info.end_time %}
<div class="mt-1">
<svg class="icon icon-sm">
<use href="#icon-clock"></use>
</svg>
<span>
{{ frappe.utils.format_time(batch_info.start_time, "hh:mm a") }} -
</span>
<span>
{{ frappe.utils.format_time(batch_info.end_time, "hh:mm a") }}
</span>
</div>
{% endif %}
</div>
{% endmacro %}
{% macro BatchOverlay(batch_info, courses, students) %}
<div class="course-overlay-card class-overlay"> <div class="course-overlay-card class-overlay">
<div class="course-overlay-content">
{% if batch_info.seat_count %} {% if seats_left %}
<div class="indicator-pill green pull-right">
{{ _("Seats Available") }}: {{ seats_left }}
</div>
{% else %}
<div class="indicator-pill red pull-right">
{{ _("No seats left") }}
</div>
{% endif %} {% endif %} {% if batch_info.paid_batch %}
<div class="bold-heading">
{{ frappe.utils.fmt_money(batch_info.amount, 0, batch_info.currency)
}}
</div>
{% endif %}
<div class="course-overlay-content"> <div class="vertically-center mt-2">
<svg class="icon icon-md mr-1">
<use href="#icon-education"></use>
</svg>
{{ courses | length }} {{ _("Courses") }}
</div>
{% if batch_info.seat_count %} <div class="mt-2">
{% if seats_left %} <svg class="icon icon-sm">
<div class="indicator-pill green pull-right"> <use href="#icon-calendar"></use>
{{ _("Seats Available") }}: {{ seats_left }} </svg>
</div> <span>
{% else %} {{ frappe.utils.format_date(batch_info.start_date, "long") }}
<div class="indicator-pill red pull-right"> </span>
{{ _("No seats left") }} {% if batch_info.start_date != batch_info.end_date %}
</div> <span>
{% endif %} - {{ frappe.utils.format_date(batch_info.end_date, "long") }}
{% endif %} </span>
{% endif %}
</div>
{% if batch_info.paid_batch %} {% if batch_info.start_time and batch_info.end_time %}
<div class="bold-heading"> <div class="mt-2">
{{ frappe.utils.fmt_money(batch_info.amount, 0, batch_info.currency) }} <svg class="icon icon-sm">
</div> <use href="#icon-clock"></use>
{% endif %} </svg>
<span>
{{ frappe.utils.format_time(batch_info.start_time, "hh:mm a") }}
-
</span>
<span>
{{ frappe.utils.format_time(batch_info.end_time, "hh:mm a") }}
</span>
</div>
{% endif %}
<div class="vertically-center mt-2"> <div class="mt-2">
<svg class="icon icon-md mr-1"> {% if is_moderator or is_evaluator %}
<use href="#icon-education"></use> <a
</svg> class="btn btn-primary wide-button"
{{ courses | length }} {{ _("Courses") }} href="/batches/{{ batch_info.name }}"
</div> >
{{ _("Manage Batch") }}
<div class="mt-2"> </a>
<svg class="icon icon-sm"> {% elif batch_info.paid_batch and batch_info.start_date >
<use href="#icon-calendar"></use> frappe.utils.getdate() %}
</svg> <a
<span> class="btn btn-primary wide-button {% if batch_info.seat_count and not seats_left %} hide {% endif %}"
{{ frappe.utils.format_date(batch_info.start_date, "long") }} - href="/billing/batch/{{ batch_info.name }}"
</span> >
<span> {{ _("Register Now") }}
{{ frappe.utils.format_date(batch_info.end_date, "long") }} </a>
</span> {% elif batch_info.allow_self_enrollment and batch_info.seat_count
</div> and seats_left and batch_info.start_date > frappe.utils.getdate() %}
<button class="btn btn-primary wide-button enroll-batch">
{% if batch_info.start_time and batch_info.end_time %} {{ _("Enroll Now") }}
<div class="mt-2"> </button>
<svg class="icon icon-sm"> {% else %}
<use href="#icon-clock"></use> <div class="alert alert-info">
</svg> {{ _("To join this batch, please contact the Administrator.") }}
<span> </div>
{{ frappe.utils.format_time(batch_info.start_time, "hh:mm a") }} - {% endif %}
</span> </div>
<span> {% if is_moderator %}
{{ frappe.utils.format_time(batch_info.end_time, "hh:mm a") }} <div class="mt-2">
</span> <div class="btn btn-secondary wide-button" id="create-batch">
</div> {{ _("Edit") }}
{% endif %} </div>
</div>
<div class="mt-2"> {% endif %}
{% if is_moderator or is_evaluator %} </div>
<a class="btn btn-primary wide-button" href="/batches/{{ batch_info.name }}">
{{ _("Manage Batch") }}
</a>
{% elif batch_info.paid_batch %}
<a class="btn btn-primary wide-button {% if batch_info.seat_count and not seats_left %} hide {% endif %}"
href="/billing/batch/{{ batch_info.name }}">
{{ _("Register Now") }}
</a>
{% else %}
<div class="alert alert-info">
{{ _("To join this batch, please contact the Administrator.") }}
</div>
{% endif %}
</div>
{% if is_moderator %}
<div class="mt-2">
<div class="btn btn-secondary wide-button" id="create-batch">
{{ _("Edit") }}
</div>
</div>
{% endif %}
</div>
</div> </div>
{% endmacro %} {% endmacro %} {% macro BatchDetails(batch_info) %}
<div class="batch-details">{{ batch_info.batch_details }}</div>
{% endmacro %} {% macro CourseList(courses) %} {% if courses | length or
{% macro BatchDetails(batch_info) %} is_moderator %}
<div class="batch-details">
{{ batch_info.batch_details }}
</div>
{% endmacro %}
{% macro CourseList(courses) %}
{% if courses | length or is_moderator %}
<div class="batch-course-list"> <div class="batch-course-list">
<div class="align-center flex">
<div class="page-title">{{ _("Courses") }}</div>
{% if is_moderator %}
<button class="btn btn-default btn-sm btn-add-course ml-4">
{{ _("Add Course") }}
</button>
{% endif %}
</div>
<div class="flex align-center"> {% if courses | length %}
<div class="page-title">
{{ _("Courses") }}
</div>
{% if is_moderator %}
<button class="btn btn-default btn-sm btn-add-course ml-4">
{{ _("Add Course") }}
</button>
{% endif %}
</div>
{% if courses | length %}
<div class="cards-parent mt-2"> <div class="cards-parent mt-2">
{% for course in courses %} {% for course in courses %}
<div class="h-100"> <div class="h-100">
{% if is_moderator %} {% if is_moderator %}
<div class="card-buttons"> <div class="card-buttons">
<button class="btn icon-btn btn-default btn-edit-course" <button
data-name="{{ course.batch_course }}" data-course="{{ course.name }}" class="btn icon-btn btn-default btn-edit-course"
{% if course.evaluator %} data-evaluator="{{ course.evaluator }}" {% endif %}> data-name="{{ course.batch_course }}"
<svg class="icon icon-sm"> data-course="{{ course.name }}"
<use href="#icon-edit"></use> {%
</svg> if
</button> course.evaluator
<button class="btn icon-btn btn-default btn-remove-course ml-2" data-course="{{ course.name }}"> %}
<svg class="icon icon-sm"> data-evaluator="{{ course.evaluator }}"
<use href="#icon-delete"></use> {%
</svg> endif
</button> %}
</div> >
{% endif %} <svg class="icon icon-sm">
{{ widgets.CourseCard(course=course, read_only=False) }} <use href="#icon-edit"></use>
</svg>
</button>
<button
class="btn icon-btn btn-default btn-remove-course ml-2"
data-course="{{ course.name }}"
>
<svg class="icon icon-sm">
<use href="#icon-delete"></use>
</svg>
</button>
</div>
{% endif %} {{ widgets.CourseCard(course=course, read_only=False) }}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% else %} {% else %}
<div class=""> <div class="">{{ _("No courses") }}</div>
{{ _("No courses") }}
</div>
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %} {% endmacro %} {% macro BatchDetailsRaw() %} {% if
{% endmacro %} batch_info.batch_details_raw %}
<div class="mt-10 pt-10">{{ batch_info.batch_details_raw }}</div>
{% endif %} {% endmacro %} {%- block script %} {{ super() }} {% if is_moderator
{% macro BatchDetailsRaw() %} %}
{% if batch_info.batch_details_raw %} <script>
<div class="mt-10 pt-10"> let batch_info = {{ batch_info | json }};
{{ batch_info.batch_details_raw }} </script>
</div> {% endif %} {% endblock %}
{% endif %}
{% endmacro %}
{%- block script %}
{{ super() }}
{% if is_moderator %}
<script>
let batch_info = {{ batch_info | json }};
</script>
{% endif %}
{% endblock %}

View File

@@ -12,6 +12,10 @@ frappe.ready(() => {
$(".btn-remove-course").click((e) => { $(".btn-remove-course").click((e) => {
remove_course(e); remove_course(e);
}); });
$(".enroll-batch").click((e) => {
enroll_batch(e);
});
}); });
const show_course_modal = (e) => { const show_course_modal = (e) => {
@@ -54,6 +58,30 @@ const show_course_modal = (e) => {
}, 1000); }, 1000);
}; };
const enroll_batch = (e) => {
let batch_name = $(".class-details").data("batch");
if (frappe.session.user == "Guest") {
window.location.href =
"/login?redirect-to=/batches/details/" + batch_name;
}
frappe.call({
method: "lms.lms.doctype.batch_student.batch_student.enroll_batch",
args: {
batch_name: batch_name,
},
callback(r) {
frappe.show_alert(
{
message: __("Successfully Enrolled"),
indicator: "green",
},
2000
);
window.location.href = `/batches/${batch_name}`;
},
});
};
const add_course = (values, course_name) => { const add_course = (values, course_name) => {
frappe.call({ frappe.call({
method: "lms.lms.doctype.lms_batch.lms_batch.add_course", method: "lms.lms.doctype.lms_batch.lms_batch.add_course",

View File

@@ -33,13 +33,19 @@ def get_context(context):
"published", "published",
"meta_image", "meta_image",
"batch_details_raw", "batch_details_raw",
"evaluation_end_date",
"amount_usd",
"allow_self_enrollment",
], ],
as_dict=1, as_dict=1,
) )
if context.batch_info.amount and context.batch_info.currency: if context.batch_info.amount and context.batch_info.currency:
amount, currency = check_multicurrency( amount, currency = check_multicurrency(
context.batch_info.amount, context.batch_info.currency context.batch_info.amount,
context.batch_info.currency,
None,
context.batch_info.amount_usd,
) )
context.batch_info.amount = amount context.batch_info.amount = amount
context.batch_info.currency = currency context.batch_info.currency = currency

View File

@@ -140,10 +140,24 @@
<use href="#icon-calendar"></use> <use href="#icon-calendar"></use>
</svg> </svg>
<span> <span>
{{ frappe.utils.format_date(batch.start_date, "medium") }} - {{ frappe.utils.format_date(batch.start_date, "medium") }}
</span>
{% if batch.start_date != batch.end_date %}
<span>
- {{ frappe.utils.format_date(batch.end_date, "long") }}
</span>
{% endif %}
</div>
<div class="mb-2">
<svg class="icon icon-sm">
<use href="#icon-clock"></use>
</svg>
<span>
{{ frappe.utils.format_time(batch.start_time, "HH:mm a") }} -
</span> </span>
<span> <span>
{{ frappe.utils.format_date(batch.end_date, "medium") }} {{ frappe.utils.format_time(batch.end_time, "HH:mm a") }}
</span> </span>
</div> </div>
@@ -192,4 +206,4 @@
let batch_info = null; let batch_info = null;
</script> </script>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@@ -1,5 +1,5 @@
import frappe import frappe
from frappe.utils import getdate from frappe.utils import getdate, get_time_str, nowtime
from lms.lms.utils import ( from lms.lms.utils import (
has_course_moderator_role, has_course_moderator_role,
has_course_evaluator_role, has_course_evaluator_role,
@@ -19,11 +19,14 @@ def get_context(context):
"description", "description",
"start_date", "start_date",
"end_date", "end_date",
"start_time",
"end_time",
"paid_batch", "paid_batch",
"amount", "amount",
"currency", "currency",
"seat_count", "seat_count",
"published", "published",
"amount_usd",
], ],
order_by="start_date", order_by="start_date",
) )
@@ -34,7 +37,9 @@ def get_context(context):
batch.course_count = frappe.db.count("Batch Course", {"parent": batch.name}) batch.course_count = frappe.db.count("Batch Course", {"parent": batch.name})
if batch.amount and batch.currency: if batch.amount and batch.currency:
amount, currency = check_multicurrency(batch.amount, batch.currency) amount, currency = check_multicurrency(
batch.amount, batch.currency, None, batch.amount_usd
)
batch.amount = amount batch.amount = amount
batch.currency = currency batch.currency = currency
@@ -43,12 +48,16 @@ def get_context(context):
) )
if not batch.published: if not batch.published:
private_batches.append(batch) private_batches.append(batch)
elif getdate(batch.start_date) <= getdate(): elif getdate(batch.start_date) < getdate():
past_batches.append(batch)
elif (
getdate(batch.start_date) == getdate() and get_time_str(batch.start_time) < nowtime()
):
past_batches.append(batch) past_batches.append(batch)
else: else:
upcoming_batches.append(batch) upcoming_batches.append(batch)
context.past_batches = sorted(past_batches, key=lambda d: d.start_date) context.past_batches = sorted(past_batches, key=lambda d: d.start_date, reverse=True)
context.upcoming_batches = sorted(upcoming_batches, key=lambda d: d.start_date) context.upcoming_batches = sorted(upcoming_batches, key=lambda d: d.start_date)
context.private_batches = sorted(private_batches, key=lambda d: d.start_date) context.private_batches = sorted(private_batches, key=lambda d: d.start_date)
@@ -83,5 +92,6 @@ def get_context(context):
batchinfo.seats_left = batchinfo.seat_count - batchinfo.student_count batchinfo.seats_left = batchinfo.seat_count - batchinfo.student_count
my_batches_info.append(batchinfo) my_batches_info.append(batchinfo)
my_batches_info = sorted(my_batches_info, key=lambda d: d.start_date, reverse=True)
context.my_batches = my_batches_info context.my_batches = my_batches_info

View File

@@ -38,7 +38,7 @@
<div class="flex"> <div class="flex">
<div class="field-label"> <div class="field-label">
{{ _("Total Price: ") }} {{ _("Total Price: ") }}
<span class="total-price">{{ frappe.utils.fmt_money(amount, 2, currency) }}</span> <span class="total-price">{{ frappe.utils.fmt_money(amount_with_gst, 2, currency) if gst_applied else frappe.utils.fmt_money(amount, 2, currency) }}</span>
</div> </div>
</div> </div>
{% if gst_applied %} {% if gst_applied %}

View File

@@ -40,15 +40,15 @@ const setup_billing = () => {
reqd: 1, reqd: 1,
default: address && address.city, default: address && address.city,
}, },
{
fieldtype: "Column Break",
},
{ {
fieldtype: "Data", fieldtype: "Data",
label: __("State/Province"), label: __("State/Province"),
fieldname: "state", fieldname: "state",
default: address && address.state, default: address && address.state,
}, },
{
fieldtype: "Column Break",
},
{ {
fieldtype: "Link", fieldtype: "Link",
label: __("Country"), label: __("Country"),
@@ -75,6 +75,14 @@ const setup_billing = () => {
reqd: 1, reqd: 1,
default: address && address.phone, default: address && address.phone,
}, },
{
fieldtype: "Link",
label: __("Where did you hear about this?"),
fieldname: "source",
options: "LMS Source",
only_select: 1,
reqd: 1,
},
{ {
fieldtype: "Section Break", fieldtype: "Section Break",
label: __("GST Details"), label: __("GST Details"),
@@ -106,6 +114,7 @@ const setup_billing = () => {
const generate_payment_link = (e) => { const generate_payment_link = (e) => {
let new_address = this.billing.get_values(); let new_address = this.billing.get_values();
validate_address(new_address);
let doctype = $(e.currentTarget).attr("data-doctype"); let doctype = $(e.currentTarget).attr("data-doctype");
let docname = decodeURIComponent($(e.currentTarget).attr("data-name")); let docname = decodeURIComponent($(e.currentTarget).attr("data-name"));
@@ -174,8 +183,10 @@ const change_currency = () => {
if (current_price != data.message) { if (current_price != data.message) {
update_price(data.message); update_price(data.message);
} }
if (!data.message.includes("INR")) { if (data.message.includes("INR")) {
$("#gst-message").addClass("hide"); $("#gst-message").removeClass("hide").addClass("show");
} else {
$("#gst-message").removeClass("show").addClass("hide");
} }
}, },
}); });
@@ -188,3 +199,48 @@ const update_price = (price) => {
indicator: "yellow", indicator: "yellow",
}); });
}; };
const validate_address = (billing_address) => {
if (billing_address.country == "India" && !billing_address.state)
frappe.throw(__("State is mandatory."));
const states = [
"Andhra Pradesh",
"Arunachal Pradesh",
"Assam",
"Bihar",
"Chhattisgarh",
"Goa",
"Gujarat",
"Haryana",
"Himachal Pradesh",
"Jharkhand",
"Karnataka",
"Kerala",
"Madhya Pradesh",
"Maharashtra",
"Manipur",
"Meghalaya",
"Mizoram",
"Nagaland",
"Odisha",
"Punjab",
"Rajasthan",
"Sikkim",
"Tamil Nadu",
"Telangana",
"Tripura",
"Uttar Pradesh",
"Uttarakhand",
"West Bengal",
];
if (
billing_address.country == "India" &&
!states.includes(billing_address.state)
)
frappe.throw(
__(
"Please enter a valid state with correct spelling and the first letter capitalized."
)
);
};

View File

@@ -15,20 +15,22 @@ def get_context(context):
validate_access(doctype, docname, module) validate_access(doctype, docname, module)
get_billing_details(context) get_billing_details(context)
context.original_currency = context.currency
context.original_amount = (
(context.amount * 1.18) if context.original_currency == "INR" else context.amount
)
context.exception_country = frappe.get_all( context.exception_country = frappe.get_all(
"Payment Country", filters={"parent": "LMS Settings"}, pluck="country" "Payment Country", filters={"parent": "LMS Settings"}, pluck="country"
) )
context.amount, context.currency = check_multicurrency( context.amount, context.currency = check_multicurrency(
context.amount, context.currency context.amount, context.currency, None, context.amount_usd
) )
context.address = get_address() context.address = get_address()
if context.currency == "INR": if context.currency == "INR":
context.amount, context.gst_applied = apply_gst(context.amount, None) context.amount_with_gst, context.gst_applied = apply_gst(context.amount, None)
context.original_amount = context.amount
context.original_currency = context.currency
def validate_access(doctype, docname, module): def validate_access(doctype, docname, module):
@@ -61,7 +63,7 @@ def get_billing_details(context):
details = frappe.db.get_value( details = frappe.db.get_value(
"LMS Course", "LMS Course",
context.docname, context.docname,
["title", "name", "paid_course", "course_price as amount", "currency"], ["title", "name", "paid_course", "course_price as amount", "currency", "amount_usd"],
as_dict=True, as_dict=True,
) )
@@ -72,7 +74,7 @@ def get_billing_details(context):
details = frappe.db.get_value( details = frappe.db.get_value(
"LMS Batch", "LMS Batch",
context.docname, context.docname,
["title", "name", "paid_batch", "amount", "currency"], ["title", "name", "paid_batch", "amount", "currency", "amount_usd"],
as_dict=True, as_dict=True,
) )
@@ -84,6 +86,7 @@ def get_billing_details(context):
context.title = details.title context.title = details.title
context.amount = details.amount context.amount = details.amount
context.currency = details.currency context.currency = details.currency
context.amount_usd = details.amount_usd
def get_address(): def get_address():

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