From f9937033c7c242001efc4f4097738299dd8d2ffd Mon Sep 17 00:00:00 2001 From: ONE-FM Dev Date: Thu, 14 May 2026 13:32:46 +0200 Subject: [PATCH 1/3] ci(WI-000786): add linters.yml CI workflow Add 4-job linting workflow aligned with Frappe v15: - commit-lint: Validates semantic commit messages - linter: Runs Semgrep security rules - deps-vulnerable-check: Scans for vulnerable dependencies - precommit: Runs all pre-commit hooks Task: WI-000786 --- .github/workflows/linters.yml | 99 +++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 .github/workflows/linters.yml diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml new file mode 100644 index 0000000..00c926c --- /dev/null +++ b/.github/workflows/linters.yml @@ -0,0 +1,99 @@ +name: Linters + +on: + pull_request: + branches: [staging, test-production, version-15] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: linters-one_lms-${{ github.event_name }}-${{ github.event.number || github.run_id }} + cancel-in-progress: true + +jobs: + commit-lint: + name: 'Semantic Commits' + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 200 + - uses: actions/setup-node@v3 + with: + node-version: 18 + check-latest: true + + - name: Verify commitlint config exists + run: | + if [ ! -f "commitlint.config.js" ]; then + echo "Error: commitlint.config.js not found" + exit 1 + fi + + - name: Check commit titles + run: | + npm install @commitlint/cli @commitlint/config-conventional + npx commitlint --verbose --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} + + linter: + name: 'Semgrep Rules' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: '3.11' + cache: pip + + - name: Download Semgrep rules + run: git clone --depth 1 --branch main https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules + + - name: Run Semgrep rules + run: | + pip install semgrep + semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness + + deps-vulnerable-check: + name: 'Vulnerable Dependency Check' + runs-on: ubuntu-latest + + steps: + - uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - uses: actions/checkout@v4 + + - name: Cache pip + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + + - name: Install and run pip-audit + run: | + pip install pip-audit + cd ${GITHUB_WORKSPACE} + pip-audit --desc on . + + precommit: + name: 'Pre-commit Hooks' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Run pre-commit + uses: pre-commit/action@v3.0.1 From 62ae0c357f6b72a336e0236c8d89ab21769937bd Mon Sep 17 00:00:00 2001 From: Talleyrand333 Date: Thu, 21 May 2026 08:29:14 +0100 Subject: [PATCH 2/3] style: apply consistent indentation and formatting across python and vue files --- .eslintrc | 1 + .github/workflows/linters.yml | 2 +- commitlint.config.js | 3 +++ 3 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 commitlint.config.js diff --git a/.eslintrc b/.eslintrc index c5e7d68..63f2ba5 100644 --- a/.eslintrc +++ b/.eslintrc @@ -26,6 +26,7 @@ "root": true, "globals": { "frappe": true, + "tinymce": true, "Vue": true, "SetVueGlobals": true, "__": true, diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index 00c926c..ef9ba0c 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -51,7 +51,7 @@ jobs: cache: pip - name: Download Semgrep rules - run: git clone --depth 1 --branch main https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules + run: git clone --depth 1 --branch develop https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules - name: Run Semgrep rules run: | diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 0000000..6b60f45 --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ["@commitlint/config-conventional"] +}; From 3fc50b3701067818575dadf9bea3d6d57f71bc94 Mon Sep 17 00:00:00 2001 From: Talleyrand333 Date: Thu, 21 May 2026 09:14:38 +0100 Subject: [PATCH 3/3] style: run pre-commit to fix formatting and lint errors --- commitlint.config.js | 2 +- one_lms/__init__.py | 3 +- one_lms/after_migrate/execute.py | 219 +++-- one_lms/custom/custom_field/course_lesson.py | 36 +- .../custom_field/lms_assignment_submission.py | 38 +- one_lms/custom/custom_field/lms_category.py | 20 +- one_lms/custom/custom_field/lms_course.py | 50 +- one_lms/custom/custom_field/lms_enrollment.py | 58 +- .../custom_field/lms_quiz_submission.py | 52 +- one_lms/custom/custom_field/lms_settings.py | 120 +-- .../custom/property_setter/course_lesson.py | 88 +- one_lms/custom/property_setter/lms_quiz.py | 20 +- .../custom/property_setter/lms_quiz_result.py | 20 +- one_lms/hooks.py | 24 +- one_lms/notification/notifications.py | 180 ++-- .../lms_course_enrolment_request.js | 61 +- .../lms_course_enrolment_request.py | 293 ++++--- .../test_lms_course_enrolment_request.py | 3 +- .../lms_enrollment_tool.js | 91 +- .../lms_enrollment_tool.py | 34 +- .../test_lms_enrollment_tool.py | 3 +- .../lms_enrollment_tool_member.py | 3 +- one_lms/overrides/course_lesson.py | 8 +- .../overrides/lms_assignment_submission.py | 111 +-- one_lms/overrides/lms_batch.py | 71 +- one_lms/overrides/lms_certificate.py | 54 +- one_lms/overrides/lms_course.py | 40 +- one_lms/overrides/lms_enrollment.py | 86 +- one_lms/overrides/lms_quiz_submission.py | 13 +- one_lms/overrides/plugins.py | 53 +- one_lms/overrides/user.py | 19 +- one_lms/public/js/doctype_js/course_lesson.js | 10 +- .../public/js/doctype_js/lms_certificate.js | 17 +- one_lms/public/js/doctype_js/lms_course.js | 181 ++-- one_lms/public/js/frappe_tinymce.js | 214 ++--- .../components/CourseCardOverlay.vue | 238 ++--- one_lms/public/overrides/pages/Lesson.vue | 826 +++++++++--------- one_lms/setup/custom_field.py | 19 +- one_lms/setup/property_setter.py | 4 +- one_lms/setup/setup.py | 15 +- one_lms/templates/emails/default_email.html | 20 +- ..._assignment_submission_group_template.html | 12 +- .../emails/lms_course_completion.html | 10 +- .../emails/lms_course_enrollment.html | 2 +- .../lms_quiz_submission_group_template.html | 8 +- one_lms/utils.py | 99 +-- one_lms/www/lms.py | 30 +- 47 files changed, 1776 insertions(+), 1802 deletions(-) diff --git a/commitlint.config.js b/commitlint.config.js index 6b60f45..26a88cb 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -1,3 +1,3 @@ module.exports = { - extends: ["@commitlint/config-conventional"] + extends: ["@commitlint/config-conventional"], }; diff --git a/one_lms/__init__.py b/one_lms/__init__.py index ea124cb..70797cf 100644 --- a/one_lms/__init__.py +++ b/one_lms/__init__.py @@ -1,7 +1,8 @@ -from one_lms.overrides.plugins import assignment_renderer, quiz_renderer from lms import plugins from lms.lms import utils as lms_utils + from one_lms import utils +from one_lms.overrides.plugins import assignment_renderer, quiz_renderer __version__ = "0.0.1" diff --git a/one_lms/after_migrate/execute.py b/one_lms/after_migrate/execute.py index 5f55be6..50d8c5d 100644 --- a/one_lms/after_migrate/execute.py +++ b/one_lms/after_migrate/execute.py @@ -1,127 +1,126 @@ -import frappe import os +import shlex import shutil import subprocess -import shlex + +import frappe + def run_command(command_list, cwd=None): - """ - Executes a command from a list of arguments safely. - """ - # For display purposes, join the command list back into a readable string - command_str = shlex.join(command_list) - try: - # Execute the command list, shell=False is the default and is safer - result = subprocess.run( - command_list, - cwd=cwd, - check=True, - text=True, - capture_output=True - ) - # Print standard output and error - print(result.stdout) - if result.stderr: - print(result.stderr) - print(f"✅ Command '{command_str}' executed successfully.") - except subprocess.CalledProcessError as e: - # Handle errors in the command execution - print(f"❌ An error occurred while running the command: {command_str}") - print(f"Output: {e.stdout}") - print(f"Error: {e.stderr}") - - + """ + Executes a command from a list of arguments safely. + """ + # For display purposes, join the command list back into a readable string + command_str = shlex.join(command_list) + try: + # Execute the command list, shell=False is the default and is safer + result = subprocess.run(command_list, cwd=cwd, check=True, text=True, capture_output=True) + # Print standard output and error + print(result.stdout) + if result.stderr: + print(result.stderr) + print(f"✅ Command '{command_str}' executed successfully.") + except subprocess.CalledProcessError as e: + # Handle errors in the command execution + print(f"❌ An error occurred while running the command: {command_str}") + print(f"Output: {e.stdout}") + print(f"Error: {e.stderr}") + + def update_lesson(): - """ - Replaces the standard LMS Lesson.vue file with the custom version from one_lms - and triggers a build to apply the changes. - """ - print("🚀 Overriding LMS Lesson.vue file...") - - # Get the base path of the bench directory - bench_path = frappe.utils.get_bench_path() - - # Define the source and destination file paths - source_file = os.path.join( - bench_path, "apps", "lms", "frontend", "src", "pages", "Lesson.vue" - ) - replacement_file = os.path.join( - bench_path, "apps", "one_lms","one_lms","public","overrides", "pages", "Lesson.vue" - ) - - # Ensure the custom replacement file actually exists before proceeding - if not os.path.exists(replacement_file): - print(f"❌ Error: Replacement file not found at: {replacement_file}") - return False - - try: - # Copy the content from your custom file to the source file, overwriting it - print(f"📄 Copying from {replacement_file} to {source_file}") - shutil.copy2(replacement_file, source_file) - print("✅ Successfully replaced Lesson.vue.") - - return True - - except FileNotFoundError: - print(f"⚠️ Warning: Original file not found at: {source_file}. Skipping replacement.") - except Exception as e: - print(f"🔥 An unexpected error occurred: {e}") - frappe.log_error("LMS Lesson.vue Override Failed", frappe.get_traceback()) + """ + Replaces the standard LMS Lesson.vue file with the custom version from one_lms + and triggers a build to apply the changes. + """ + print("🚀 Overriding LMS Lesson.vue file...") + + # Get the base path of the bench directory + bench_path = frappe.utils.get_bench_path() + + # Define the source and destination file paths + source_file = os.path.join(bench_path, "apps", "lms", "frontend", "src", "pages", "Lesson.vue") + replacement_file = os.path.join( + bench_path, "apps", "one_lms", "one_lms", "public", "overrides", "pages", "Lesson.vue" + ) + + # Ensure the custom replacement file actually exists before proceeding + if not os.path.exists(replacement_file): + print(f"❌ Error: Replacement file not found at: {replacement_file}") + return False + + try: + # Copy the content from your custom file to the source file, overwriting it + print(f"📄 Copying from {replacement_file} to {source_file}") + shutil.copy2(replacement_file, source_file) + print("✅ Successfully replaced Lesson.vue.") + + return True + + except FileNotFoundError: + print(f"⚠️ Warning: Original file not found at: {source_file}. Skipping replacement.") + except Exception as e: + print(f"🔥 An unexpected error occurred: {e}") + frappe.log_error("LMS Lesson.vue Override Failed", frappe.get_traceback()) def update_course_card_overlay(): - """ - Replaces the standard LMS CourseCardOverlay.vue file with the custom version from one_lms - and triggers a build to apply the changes. - """ - print("🚀 Overriding LMS CourseCardOverlay.vue file...") - - # Get the base path of the bench directory - bench_path = frappe.utils.get_bench_path() - - # Define the source and destination file paths - source_file = os.path.join( - bench_path, "apps", "lms", "frontend", "src", "components", "CourseCardOverlay.vue", - ) - replacement_file = os.path.join( - bench_path, "apps", "one_lms","one_lms","public","overrides", "components", "CourseCardOverlay.vue" - ) - - # Ensure the custom replacement file actually exists before proceeding - if not os.path.exists(replacement_file): - print(f"❌ Error: Replacement file not found at: {replacement_file}") - return False - - try: - # Copy the content from your custom file to the source file, overwriting it - print(f"📄 Copying from {replacement_file} to {source_file}") - shutil.copy2(replacement_file, source_file) - print("✅ Successfully replaced CourseCardOverlay.vue.") - - return True - - except FileNotFoundError: - print(f"⚠️ Warning: Original file not found at: {source_file}. Skipping replacement.") - except Exception as e: - print(f"🔥 An unexpected error occurred: {e}") - frappe.log_error("LMS CourseCardOverlay.vue Override Failed", frappe.get_traceback()) + """ + Replaces the standard LMS CourseCardOverlay.vue file with the custom version from one_lms + and triggers a build to apply the changes. + """ + print("🚀 Overriding LMS CourseCardOverlay.vue file...") + + # Get the base path of the bench directory + bench_path = frappe.utils.get_bench_path() + + # Define the source and destination file paths + source_file = os.path.join( + bench_path, + "apps", + "lms", + "frontend", + "src", + "components", + "CourseCardOverlay.vue", + ) + replacement_file = os.path.join( + bench_path, "apps", "one_lms", "one_lms", "public", "overrides", "components", "CourseCardOverlay.vue" + ) + + # Ensure the custom replacement file actually exists before proceeding + if not os.path.exists(replacement_file): + print(f"❌ Error: Replacement file not found at: {replacement_file}") + return False + + try: + # Copy the content from your custom file to the source file, overwriting it + print(f"📄 Copying from {replacement_file} to {source_file}") + shutil.copy2(replacement_file, source_file) + print("✅ Successfully replaced CourseCardOverlay.vue.") + + return True + + except FileNotFoundError: + print(f"⚠️ Warning: Original file not found at: {source_file}. Skipping replacement.") + except Exception as e: + print(f"🔥 An unexpected error occurred: {e}") + frappe.log_error("LMS CourseCardOverlay.vue Override Failed", frappe.get_traceback()) def after_migrate(): - bench_path = frappe.utils.get_bench_path() - any_change = False + bench_path = frappe.utils.get_bench_path() + any_change = False - if update_lesson(): - any_change = True + if update_lesson(): + any_change = True - if update_course_card_overlay(): - any_change = True - - if any_change: + if update_course_card_overlay(): + any_change = True - # Trigger the build process to make the frontend changes live - print("🏗️ Running 'bench build' to apply frontend changes...") + if any_change: + # Trigger the build process to make the frontend changes live + print("🏗️ Running 'bench build' to apply frontend changes...") - run_command(['bench', 'build', '--app', 'lms'], cwd=bench_path) + run_command(["bench", "build", "--app", "lms"], cwd=bench_path) - print("🎉 Override process completed successfully!") + print("🎉 Override process completed successfully!") diff --git a/one_lms/custom/custom_field/course_lesson.py b/one_lms/custom/custom_field/course_lesson.py index 2b83436..78cf0f6 100644 --- a/one_lms/custom/custom_field/course_lesson.py +++ b/one_lms/custom/custom_field/course_lesson.py @@ -1,19 +1,19 @@ def get_course_lesson_custom_fields(): - return { - "Course Lesson": [ - { - "fieldname": "lms_assignment", - "fieldtype": "Link", - "label": "LMS Assignment", - "options": "LMS Assignment", - "insert_after": "section_break_16" - }, - { - "fieldname": "quiz", - "fieldtype": "Link", - "label": "Quiz", - "options": "LMS Quiz", - "insert_after": "column_break_9", - } - ] - } + return { + "Course Lesson": [ + { + "fieldname": "lms_assignment", + "fieldtype": "Link", + "label": "LMS Assignment", + "options": "LMS Assignment", + "insert_after": "section_break_16", + }, + { + "fieldname": "quiz", + "fieldtype": "Link", + "label": "Quiz", + "options": "LMS Quiz", + "insert_after": "column_break_9", + }, + ] + } diff --git a/one_lms/custom/custom_field/lms_assignment_submission.py b/one_lms/custom/custom_field/lms_assignment_submission.py index 324258d..bf09179 100644 --- a/one_lms/custom/custom_field/lms_assignment_submission.py +++ b/one_lms/custom/custom_field/lms_assignment_submission.py @@ -1,20 +1,20 @@ def get_lms_assignment_submission_custom_fields(): - return { - "LMS Assignment Submission": [ - { - "fieldname": "instructor_notified_submission", - "fieldtype": "Check", - "label": "Instructor Notified Submission", - "read_only": 1, - "insert_after": "column_break_ygdu", - "default": "0" - }, - { - "fieldname": "custom_employee_id", - "fieldtype": "Data", - "fetch_from": "member.username", - "label": "Employee ID", - "insert_after": "member_name" - } - ] - } + return { + "LMS Assignment Submission": [ + { + "fieldname": "instructor_notified_submission", + "fieldtype": "Check", + "label": "Instructor Notified Submission", + "read_only": 1, + "insert_after": "column_break_ygdu", + "default": "0", + }, + { + "fieldname": "custom_employee_id", + "fieldtype": "Data", + "fetch_from": "member.username", + "label": "Employee ID", + "insert_after": "member_name", + }, + ] + } diff --git a/one_lms/custom/custom_field/lms_category.py b/one_lms/custom/custom_field/lms_category.py index 43af510..479764d 100644 --- a/one_lms/custom/custom_field/lms_category.py +++ b/one_lms/custom/custom_field/lms_category.py @@ -1,11 +1,11 @@ def get_lms_category_custom_fields(): - return { - "LMS Category": [ - { - "fieldname": "image", - "fieldtype": "Attach Image", - "label": "Image", - "insert_after": "category", - } - ] - } + return { + "LMS Category": [ + { + "fieldname": "image", + "fieldtype": "Attach Image", + "label": "Image", + "insert_after": "category", + } + ] + } diff --git a/one_lms/custom/custom_field/lms_course.py b/one_lms/custom/custom_field/lms_course.py index d907fe5..1625791 100644 --- a/one_lms/custom/custom_field/lms_course.py +++ b/one_lms/custom/custom_field/lms_course.py @@ -1,26 +1,26 @@ def get_lms_course_custom_fields(): - return { - "LMS Course": [ - { - "fieldname": "allow_reenrollments", - "fieldtype": "Check", - "label": "Allow Re-Enrollments", - "default": "0", - "insert_after": "category", - }, - { - "fieldname": "template", - "fieldtype": "Link", - "label": "Template", - "options": "Print Format", - "depends_on": "enable_certification", - }, - { - "fieldname": "default_instructor", - "fieldtype": "Select", - "label": "Default Instructor", - "description": "The selected instructor will be displayed on the course certificates.", - "insert_after": "image", - } - ] - } + return { + "LMS Course": [ + { + "fieldname": "allow_reenrollments", + "fieldtype": "Check", + "label": "Allow Re-Enrollments", + "default": "0", + "insert_after": "category", + }, + { + "fieldname": "template", + "fieldtype": "Link", + "label": "Template", + "options": "Print Format", + "depends_on": "enable_certification", + }, + { + "fieldname": "default_instructor", + "fieldtype": "Select", + "label": "Default Instructor", + "description": "The selected instructor will be displayed on the course certificates.", + "insert_after": "image", + }, + ] + } diff --git a/one_lms/custom/custom_field/lms_enrollment.py b/one_lms/custom/custom_field/lms_enrollment.py index 0a516fe..9ac54f1 100644 --- a/one_lms/custom/custom_field/lms_enrollment.py +++ b/one_lms/custom/custom_field/lms_enrollment.py @@ -1,30 +1,30 @@ def get_lms_enrollment_custom_fields(): - return { - "LMS Enrollment": [ - { - "fieldname": "instructor_notified_completion", - "fieldtype": "Check", - "label": "Instructor Notified Completion", - "read_only": 1, - "insert_after": "role", - "default": "0", - "depends_on": "eval:doc.progress==100" - }, - { - "fieldname": "date", - "fieldtype": "Date", - "label": "Date", - "default": "Today", - "in_filter": 1, - "in_list_view": 1, - }, - { - "fieldname": "course_completion_date", - "fieldtype": "Date", - "label": "Course Completion Date", - "depends_on": "eval:doc.progress==100", - "read_only": 1, - "in_filter": 1, - } - ] - } + return { + "LMS Enrollment": [ + { + "fieldname": "instructor_notified_completion", + "fieldtype": "Check", + "label": "Instructor Notified Completion", + "read_only": 1, + "insert_after": "role", + "default": "0", + "depends_on": "eval:doc.progress==100", + }, + { + "fieldname": "date", + "fieldtype": "Date", + "label": "Date", + "default": "Today", + "in_filter": 1, + "in_list_view": 1, + }, + { + "fieldname": "course_completion_date", + "fieldtype": "Date", + "label": "Course Completion Date", + "depends_on": "eval:doc.progress==100", + "read_only": 1, + "in_filter": 1, + }, + ] + } diff --git a/one_lms/custom/custom_field/lms_quiz_submission.py b/one_lms/custom/custom_field/lms_quiz_submission.py index 723e130..5fae849 100644 --- a/one_lms/custom/custom_field/lms_quiz_submission.py +++ b/one_lms/custom/custom_field/lms_quiz_submission.py @@ -1,27 +1,27 @@ def get_lms_quiz_submission_custom_fields(): - return { - "LMS Quiz Submission": [ - { - "fieldname": "instructor_notified_submission", - "fieldtype": "Check", - "label": "Instructor Notified Submission", - "read_only": 1, - "insert_after": "result", - "default": "0" - }, - { - "fieldname": "custom_employee_id", - "fieldtype": "Data", - "fetch_from": "member.username", - "label": "Employee ID", - "insert_after": "member_name" - }, - { - "fieldname": "custom_date", - "fieldtype": "Datetime", - "label": "Submission Date", - "read_only": 1, - "insert_after": "owner" - } - ] - } + return { + "LMS Quiz Submission": [ + { + "fieldname": "instructor_notified_submission", + "fieldtype": "Check", + "label": "Instructor Notified Submission", + "read_only": 1, + "insert_after": "result", + "default": "0", + }, + { + "fieldname": "custom_employee_id", + "fieldtype": "Data", + "fetch_from": "member.username", + "label": "Employee ID", + "insert_after": "member_name", + }, + { + "fieldname": "custom_date", + "fieldtype": "Datetime", + "label": "Submission Date", + "read_only": 1, + "insert_after": "owner", + }, + ] + } diff --git a/one_lms/custom/custom_field/lms_settings.py b/one_lms/custom/custom_field/lms_settings.py index 6bbff7b..3c183e0 100644 --- a/one_lms/custom/custom_field/lms_settings.py +++ b/one_lms/custom/custom_field/lms_settings.py @@ -1,61 +1,61 @@ def get_lms_settings_custom_fields(): - return { - "LMS Settings": [ - { - "fieldname": "course_completion_notification_template", - "fieldtype": "Link", - "label": "Course Completion Notification Template", - "options": "Email Template", - "insert_after": "certification_template" - }, - { - "fieldname": "notification_settings_tab", - "fieldtype": "Tab Break", - "label": "Notification Settings", - "insert_after": "course_completion_notification_template" - }, - { - "fieldname": "notify_instructor_course_completion_eod", - "fieldtype": "Check", - "label": "Notify Instructor on Course Completion by EOD", - "default": "0", - "description": "Check it true to send the course completion notification to the instructors assigned in the corse by end of the day(at 11:50 pm)\nYou can configure custom 'Course Completion Notification Template' from the Email Templates tab.", - "insert_after": "notification_settings_tab" - }, - { - "fieldname": "notify_instructor_assignment_submission_eod", - "fieldtype": "Check", - "label": "Notify Instructor on Assignment Submission by EOD", - "default": "0", - "description": "Check it true to send the assignment submission notification to the instructors assigned in the corse by end of the day(at 11:50 pm)\nYou can configure custom 'Assignment Submission Template' from the Email Templates tab.\n\nCheck it false will send the assignment submission notification right after the creation", - "insert_after": "notify_instructor_course_completion_eod" - }, - { - "fieldname": "column_break_6xas", - "fieldtype": "Column Break", - "insert_after": "notify_instructor_assignment_submission_eod" - }, - { - "fieldname": "notify_instructor_quiz_submission_eod", - "fieldtype": "Check", - "label": "Notify Instructor on Quiz Submission by EOD", - "default": "0", - "description": "Check it true to send the quiz submission notification to the instructors assigned in the corse by end of the day(at 11:50 pm)\nYou can configure custom 'Quiz Submission Notification Template' from the Email Templates tab.", - "insert_after": "column_break_6xas" - }, - { - "fieldname": "quiz_submission_template", - "fieldtype": "Link", - "label": "Quiz Submission Notification Template", - "options": "Email Template", - "insert_after": "notify_instructor_quiz_submission_eod" - }, - { - "fieldname": "training_manager", - "fieldtype": "Link", - "label": "Training Manager", - "options": "User", - "insert_after": "search_placeholder" - } - ] - } + return { + "LMS Settings": [ + { + "fieldname": "course_completion_notification_template", + "fieldtype": "Link", + "label": "Course Completion Notification Template", + "options": "Email Template", + "insert_after": "certification_template", + }, + { + "fieldname": "notification_settings_tab", + "fieldtype": "Tab Break", + "label": "Notification Settings", + "insert_after": "course_completion_notification_template", + }, + { + "fieldname": "notify_instructor_course_completion_eod", + "fieldtype": "Check", + "label": "Notify Instructor on Course Completion by EOD", + "default": "0", + "description": "Check it true to send the course completion notification to the instructors assigned in the corse by end of the day(at 11:50 pm)\nYou can configure custom 'Course Completion Notification Template' from the Email Templates tab.", + "insert_after": "notification_settings_tab", + }, + { + "fieldname": "notify_instructor_assignment_submission_eod", + "fieldtype": "Check", + "label": "Notify Instructor on Assignment Submission by EOD", + "default": "0", + "description": "Check it true to send the assignment submission notification to the instructors assigned in the corse by end of the day(at 11:50 pm)\nYou can configure custom 'Assignment Submission Template' from the Email Templates tab.\n\nCheck it false will send the assignment submission notification right after the creation", + "insert_after": "notify_instructor_course_completion_eod", + }, + { + "fieldname": "column_break_6xas", + "fieldtype": "Column Break", + "insert_after": "notify_instructor_assignment_submission_eod", + }, + { + "fieldname": "notify_instructor_quiz_submission_eod", + "fieldtype": "Check", + "label": "Notify Instructor on Quiz Submission by EOD", + "default": "0", + "description": "Check it true to send the quiz submission notification to the instructors assigned in the corse by end of the day(at 11:50 pm)\nYou can configure custom 'Quiz Submission Notification Template' from the Email Templates tab.", + "insert_after": "column_break_6xas", + }, + { + "fieldname": "quiz_submission_template", + "fieldtype": "Link", + "label": "Quiz Submission Notification Template", + "options": "Email Template", + "insert_after": "notify_instructor_quiz_submission_eod", + }, + { + "fieldname": "training_manager", + "fieldtype": "Link", + "label": "Training Manager", + "options": "User", + "insert_after": "search_placeholder", + }, + ] + } diff --git a/one_lms/custom/property_setter/course_lesson.py b/one_lms/custom/property_setter/course_lesson.py index a0ef311..18d3944 100644 --- a/one_lms/custom/property_setter/course_lesson.py +++ b/one_lms/custom/property_setter/course_lesson.py @@ -1,45 +1,45 @@ def get_course_lesson_properties(): - return [ - { - "doctype_or_field": "DocField", - "doc_type": "Course Lesson", - "field_name": "body", - "property": "fieldtype", - "value": "Text Editor" - }, - { - "doctype_or_field": "DocField", - "doc_type": "Course Lesson", - "field_name": "instructor_notes", - "property": "fieldtype", - "value": "Text Editor" - }, - { - "doctype_or_field": "DocField", - "doc_type": "Course Lesson", - "field_name": "question", - "property": "read_only", - "value": "1" - }, - { - "doctype_or_field": "DocField", - "doc_type": "Course Lesson", - "field_name": "question", - "property": "fetch_from", - "value": "lms_assignment.question" - }, - { - "doctype_or_field": "DocField", - "doc_type": "Course Lesson", - "field_name": "quiz_id", - "property": "read_only", - "value": "1" - }, - { - "doctype_or_field": "DocField", - "doc_type": "Course Lesson", - "field_name": "quiz_id", - "property": "depends_on", - "value": "eval:doc.quiz" - } - ] \ No newline at end of file + return [ + { + "doctype_or_field": "DocField", + "doc_type": "Course Lesson", + "field_name": "body", + "property": "fieldtype", + "value": "Text Editor", + }, + { + "doctype_or_field": "DocField", + "doc_type": "Course Lesson", + "field_name": "instructor_notes", + "property": "fieldtype", + "value": "Text Editor", + }, + { + "doctype_or_field": "DocField", + "doc_type": "Course Lesson", + "field_name": "question", + "property": "read_only", + "value": "1", + }, + { + "doctype_or_field": "DocField", + "doc_type": "Course Lesson", + "field_name": "question", + "property": "fetch_from", + "value": "lms_assignment.question", + }, + { + "doctype_or_field": "DocField", + "doc_type": "Course Lesson", + "field_name": "quiz_id", + "property": "read_only", + "value": "1", + }, + { + "doctype_or_field": "DocField", + "doc_type": "Course Lesson", + "field_name": "quiz_id", + "property": "depends_on", + "value": "eval:doc.quiz", + }, + ] diff --git a/one_lms/custom/property_setter/lms_quiz.py b/one_lms/custom/property_setter/lms_quiz.py index c09b58e..d46a34d 100644 --- a/one_lms/custom/property_setter/lms_quiz.py +++ b/one_lms/custom/property_setter/lms_quiz.py @@ -1,11 +1,11 @@ def get_lms_quiz_properties(): - return [ - { - "doctype_or_field": "DocField", - "doc_type": "LMS Quiz", - "field_name": "lesson", - "property": "read_only", - "property_type": "Check", - "value": "0" - } - ] \ No newline at end of file + return [ + { + "doctype_or_field": "DocField", + "doc_type": "LMS Quiz", + "field_name": "lesson", + "property": "read_only", + "property_type": "Check", + "value": "0", + } + ] diff --git a/one_lms/custom/property_setter/lms_quiz_result.py b/one_lms/custom/property_setter/lms_quiz_result.py index bb9e7b8..f371c0d 100644 --- a/one_lms/custom/property_setter/lms_quiz_result.py +++ b/one_lms/custom/property_setter/lms_quiz_result.py @@ -1,11 +1,11 @@ def get_lms_quiz_result_properties(): - return [ - { - "doctype_or_field": "DocField", - "doc_type": "LMS Quiz Result", - "field_name": "answer", - "property": "fieldtype", - "property_type": "Data", - "value": "Small Text" - } - ] \ No newline at end of file + return [ + { + "doctype_or_field": "DocField", + "doc_type": "LMS Quiz Result", + "field_name": "answer", + "property": "fieldtype", + "property_type": "Data", + "value": "Small Text", + } + ] diff --git a/one_lms/hooks.py b/one_lms/hooks.py index 2283044..9a95e01 100644 --- a/one_lms/hooks.py +++ b/one_lms/hooks.py @@ -30,7 +30,7 @@ app_include_css = "/assets/one_lms/css/frappe_tinymce.css" app_include_js = [ "https://cdnjs.cloudflare.com/ajax/libs/tinymce/6.2.0/tinymce.min.js", - "/assets/one_lms/js/frappe_tinymce.js" + "/assets/one_lms/js/frappe_tinymce.js", ] # include js, css files in header of web template @@ -55,8 +55,8 @@ ] } doctype_js = { - "LMS Course" : "public/js/doctype_js/lms_course.js", - "Course Lesson" : "public/js/doctype_js/course_lesson.js" + "LMS Course": "public/js/doctype_js/lms_course.js", + "Course Lesson": "public/js/doctype_js/course_lesson.js", } # doctype_list_js = {"doctype" : "public/js/doctype_list.js"} # doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"} @@ -82,17 +82,15 @@ scheduler_events = { "cron": { "50 23 * * *": [ - 'one_lms.notification.notifications.notify_course_completion', - 'one_lms.notification.notifications.notify_assignment_submission', - 'one_lms.notification.notifications.notify_quiz_submission' + "one_lms.notification.notifications.notify_course_completion", + "one_lms.notification.notifications.notify_assignment_submission", + "one_lms.notification.notifications.notify_quiz_submission", ] } } # Template Overrides -override_template = { - "courses/course.html": "one_lms/www/courses/custom_course.html" -} +override_template = {"courses/course.html": "one_lms/www/courses/custom_course.html"} # Generators # ---------- @@ -205,15 +203,13 @@ # ------------------------------ # -after_migrate = [ - "one_lms.after_migrate.execute.after_migrate" -] +after_migrate = ["one_lms.after_migrate.execute.after_migrate"] override_whitelisted_methods = { "lms.lms.doctype.lms_assignment_submission.lms_assignment_submission.upload_assignment": "one_lms.overrides.lms_assignment_submission.upload_assignment", # "lms.lms.doctype.course_lesson.course_lesson.save_progress": "one_lms.overrides.course_lesson.save_progress", - "lms.lms.doctype.lms_certificate.lms_certificate.create_certificate": "one_lms.overrides.lms_certificate.create_certificate" + "lms.lms.doctype.lms_certificate.lms_certificate.create_certificate": "one_lms.overrides.lms_certificate.create_certificate", } @@ -223,7 +219,6 @@ "LMS Enrollment": "one_lms.overrides.lms_enrollment.LMSEnrollment", "LMS Quiz Submission": "one_lms.overrides.lms_quiz_submission.LMSQuizSubmission", "LMS Certificate": "one_lms.overrides.lms_certificate.LMSCertificate", - } # # each overriding function accepts a `data` argument; @@ -289,4 +284,3 @@ # default_log_clearing_doctypes = { # "Logging DocType Name": 30 # days to retain logs # } - diff --git a/one_lms/notification/notifications.py b/one_lms/notification/notifications.py index 90d8378..acdc171 100644 --- a/one_lms/notification/notifications.py +++ b/one_lms/notification/notifications.py @@ -1,18 +1,19 @@ import frappe from frappe import _ -from frappe.utils import validate_email_address from frappe.email.doctype.email_template.email_template import get_email_template +from frappe.utils import validate_email_address + def notify_course_completion(): - ''' - Method to notify the course completion on daily basis - ''' + """ + Method to notify the course completion on daily basis + """ # Check settings Notify Instructor on Course Completion by EOD is checked true if not frappe.db.get_single_value("LMS Settings", "notify_instructor_course_completion_eod"): return # Get all completed course(LMS Enrollment) that not send the notification to instructor - query = ''' + query = """ select course, member_name, member, name from @@ -21,14 +22,14 @@ def notify_course_completion(): instructor_notified_completion!=1 and progress=100 order by course - ''' + """ enrollment_details = frappe.db.sql(query, as_dict=True) enrollments = {} for item in enrollment_details: if item.course not in enrollments: - enrollments[item.course] = {'course': item.course, 'members': [], 'names': []} - enrollments[item.course]['members'].append(item.member) - enrollments[item.course]['names'].append(item.name) + enrollments[item.course] = {"course": item.course, "members": [], "names": []} + enrollments[item.course]["members"].append(item.member) + enrollments[item.course]["names"].append(item.name) enrollments = list(enrollments.values()) subject = _("Course Completion for Today") @@ -42,22 +43,20 @@ def notify_course_completion(): for enrollment in enrollments: # Get the instructor(s) of the course, they are the recipient(s) of the notification instructors_data = frappe.db.get_all( - 'Course Instructor', - fields=['instructor'], - filters={ - "parent": enrollment['course'], "parenttype": "LMS Course" - } + "Course Instructor", + fields=["instructor"], + filters={"parent": enrollment["course"], "parenttype": "LMS Course"}, ) - instructors = [item['instructor'] for item in instructors_data] + instructors = [item["instructor"] for item in instructors_data] for instructor in instructors: if not validate_email_address(instructor): instructors.remove(instructor) if instructors and len(instructors) > 0: args = { - "course_name": frappe.db.get_value('LMS Course', enrollment['course'], 'title'), - "course": enrollment['course'], - "members": enrollment['members'] + "course_name": frappe.db.get_value("LMS Course", enrollment["course"], "title"), + "course": enrollment["course"], + "members": enrollment["members"], } if custom_template: @@ -76,30 +75,24 @@ def notify_course_completion(): ) # Update LMS Enrollment instructor_notified_completion - query = ''' - update - `tabLMS Enrollment` - set - instructor_notified_completion=1 - ''' - if len(enrollment['names']) == 1: - query += "where name = '{0}'".format(enrollment['names'][0]) - else: - query += "where name in {0}".format(tuple(enrollment['names'])) - frappe.db.sql(query) - frappe.db.commit() + frappe.db.set_value( + "LMS Enrollment", + {"name": ["in", enrollment["names"]]}, + "instructor_notified_completion", + 1, + ) def notify_assignment_submission(): - ''' - Method to notify the assignment submission on daily basis - ''' + """ + Method to notify the assignment submission on daily basis + """ # Check settings Notify Instructor on Assignment Submission by EOD is checked true if not frappe.db.get_single_value("LMS Settings", "notify_instructor_assignment_submission_eod"): return # Get all Assignment Submission that not send the notification to instructor - query = ''' + query = """ select course, lesson, member_name, member, name, assignment_title, assignment from @@ -108,16 +101,26 @@ def notify_assignment_submission(): instructor_notified_submission!=1 and status='Not Graded' and course is NOT NULL order by course - ''' + """ submission_details = frappe.db.sql(query, as_dict=True) submissions = {} for item in submission_details: if item.course not in submissions: - submissions[item.course] = {'course': item.course, 'assignment_title': item.assignment_title, 'members': [], 'names': []} - member_details = {'member': item.member_name, 'member_id': item.member, 'assignment_submission': item.name, 'assignment_title': item.assignment_title} - submissions[item.course]['members'].append(member_details) - submissions[item.course]['names'].append(item.name) + submissions[item.course] = { + "course": item.course, + "assignment_title": item.assignment_title, + "members": [], + "names": [], + } + member_details = { + "member": item.member_name, + "member_id": item.member, + "assignment_submission": item.name, + "assignment_title": item.assignment_title, + } + submissions[item.course]["members"].append(member_details) + submissions[item.course]["names"].append(item.name) submissions = list(submissions.values()) subject = _("Assignment Submission for Today") @@ -131,22 +134,17 @@ def notify_assignment_submission(): for submission in submissions: # Get the instructor(s) of the course, they are the recipient(s) of the notification instructors_data = frappe.db.get_all( - 'Course Instructor', - fields=['instructor'], - filters={ - "parent": submission['course'], "parenttype": "LMS Course" - } + "Course Instructor", + fields=["instructor"], + filters={"parent": submission["course"], "parenttype": "LMS Course"}, ) - instructors = [item['instructor'] for item in instructors_data] + instructors = [item["instructor"] for item in instructors_data] for instructor in instructors: if not validate_email_address(instructor): instructors.remove(instructor) if instructors and len(instructors) > 0: - args = { - "assignment_title": submission['assignment_title'], - "members": submission['members'] - } + args = {"assignment_title": submission["assignment_title"], "members": submission["members"]} if custom_template: email_template = get_email_template(custom_template, args) @@ -163,31 +161,25 @@ def notify_assignment_submission(): header=["Assignment Submission on LMS", "green"], ) - # Update LMS Enrollment instructor_notified_completion - query = ''' - update - `tabLMS Assignment Submission` - set - instructor_notified_submission=1 - ''' - if len(submission['names']) == 1: - query += "where name = '{0}'".format(submission['names'][0]) - else: - query += "where name in {0}".format(tuple(submission['names'])) - frappe.db.sql(query) - frappe.db.commit() + # Update LMS Assignment Submission instructor_notified_submission + frappe.db.set_value( + "LMS Assignment Submission", + {"name": ["in", submission["names"]]}, + "instructor_notified_submission", + 1, + ) def notify_quiz_submission(): - ''' - Method to notify the quiz submission on daily basis - ''' + """ + Method to notify the quiz submission on daily basis + """ # Check settings Notify Instructor on Quiz Submission by EOD is checked true if not frappe.db.get_single_value("LMS Settings", "notify_instructor_quiz_submission_eod"): return # Get all Quiz Submission that not send the notification to instructor - query = ''' + query = """ select quiz, course, member_name, member, name, score, IF(percentage >= passing_percentage, "PASS", "FAIL") as result from @@ -196,16 +188,27 @@ def notify_quiz_submission(): instructor_notified_submission!=1 and course is NOT NULL order by course - ''' + """ submission_details = frappe.db.sql(query, as_dict=True) submissions = {} for item in submission_details: if item.course not in submissions: - submissions[item.course] = {'course': item.course, 'quiz': frappe.db.get_value('LMS Quiz', item.quiz, 'title'), 'members': [], 'names': []} - member_details = {'member': item.member_name, 'member_id': item.member, 'quiz_submission': item.name, 'score': item.score, 'result': item.result} - submissions[item.course]['members'].append(member_details) - submissions[item.course]['names'].append(item.name) + submissions[item.course] = { + "course": item.course, + "quiz": frappe.db.get_value("LMS Quiz", item.quiz, "title"), + "members": [], + "names": [], + } + member_details = { + "member": item.member_name, + "member_id": item.member, + "quiz_submission": item.name, + "score": item.score, + "result": item.result, + } + submissions[item.course]["members"].append(member_details) + submissions[item.course]["names"].append(item.name) submissions = list(submissions.values()) subject = _("Quiz Submission for Today") @@ -219,22 +222,17 @@ def notify_quiz_submission(): for submission in submissions: # Get the instructor(s) of the course, they are the recipient(s) of the notification instructors_data = frappe.db.get_all( - 'Course Instructor', - fields=['instructor'], - filters={ - "parent": submission['course'], "parenttype": "LMS Course" - } + "Course Instructor", + fields=["instructor"], + filters={"parent": submission["course"], "parenttype": "LMS Course"}, ) - instructors = [item['instructor'] for item in instructors_data] + instructors = [item["instructor"] for item in instructors_data] for instructor in instructors: if not validate_email_address(instructor): instructors.remove(instructor) if instructors and len(instructors) > 0: - args = { - "quiz": submission['quiz'], - "members": submission['members'] - } + args = {"quiz": submission["quiz"], "members": submission["members"]} if custom_template: email_template = get_email_template(custom_template, args) @@ -251,16 +249,10 @@ def notify_quiz_submission(): header=["Quiz Submission on LMS", "green"], ) - # Update LMS Enrollment instructor_notified_completion - query = ''' - update - `tabLMS Quiz Submission` - set - instructor_notified_submission=1 - ''' - if len(submission['names']) == 1: - query += "where name = '{0}'".format(submission['names'][0]) - else: - query += "where name in {0}".format(tuple(submission['names'])) - frappe.db.sql(query) - frappe.db.commit() \ No newline at end of file + # Update LMS Quiz Submission instructor_notified_submission + frappe.db.set_value( + "LMS Quiz Submission", + {"name": ["in", submission["names"]]}, + "instructor_notified_submission", + 1, + ) diff --git a/one_lms/one_lms/doctype/lms_course_enrolment_request/lms_course_enrolment_request.js b/one_lms/one_lms/doctype/lms_course_enrolment_request/lms_course_enrolment_request.js index 2b49cde..3831199 100644 --- a/one_lms/one_lms/doctype/lms_course_enrolment_request/lms_course_enrolment_request.js +++ b/one_lms/one_lms/doctype/lms_course_enrolment_request/lms_course_enrolment_request.js @@ -1,29 +1,40 @@ // Copyright (c) 2025, ONE-F-M and contributors // For license information, please see license.txt frappe.ui.form.on("LMS Course Enrolment Request", { - refresh(frm) { - if (frm.doc.status === "Open") { // Show only if status is "Open" - frm.events.enrolment_request_approve_reject(frm, "Approve"); - frm.events.enrolment_request_approve_reject(frm, "Reject"); - } - }, - enrolment_request_approve_reject(frm, action) { - frm.add_custom_button(action, function () { - frappe.call({ - doc: frm.doc, - method: "enrolment_request_approve_reject", - args: { action: action }, - callback: function (response) { - if (response.message === "success") { - if (action === "Approve") { - frappe.show_alert({ message: "Request Approved", indicator: "green" }); - } else { - frappe.show_alert({ message: "Request Rejected", indicator: "red" }); - } - frm.reload_doc(); - } - } - }); - }, "Actions"); - } + refresh(frm) { + if (frm.doc.status === "Open") { + // Show only if status is "Open" + frm.events.enrolment_request_approve_reject(frm, "Approve"); + frm.events.enrolment_request_approve_reject(frm, "Reject"); + } + }, + enrolment_request_approve_reject(frm, action) { + frm.add_custom_button( + action, + function () { + frappe.call({ + doc: frm.doc, + method: "enrolment_request_approve_reject", + args: { action: action }, + callback: function (response) { + if (response.message === "success") { + if (action === "Approve") { + frappe.show_alert({ + message: "Request Approved", + indicator: "green", + }); + } else { + frappe.show_alert({ + message: "Request Rejected", + indicator: "red", + }); + } + frm.reload_doc(); + } + }, + }); + }, + "Actions" + ); + }, }); diff --git a/one_lms/one_lms/doctype/lms_course_enrolment_request/lms_course_enrolment_request.py b/one_lms/one_lms/doctype/lms_course_enrolment_request/lms_course_enrolment_request.py index d4004c9..1b01b81 100644 --- a/one_lms/one_lms/doctype/lms_course_enrolment_request/lms_course_enrolment_request.py +++ b/one_lms/one_lms/doctype/lms_course_enrolment_request/lms_course_enrolment_request.py @@ -4,48 +4,48 @@ import frappe from frappe.model.document import Document + class LMSCourseEnrolmentRequest(Document): - def after_insert(self): - self.notify_instructors_on_request() - - def notify_instructors_on_request(self): - try: - instructors = self.get_instructors() - if instructors: - content = self.get_email_content_for_instructor() - context = dict( - header="Dear Instructor,
Good day.", - document_name=self.name, - document_type=self.doctype, - document_link=frappe.utils.get_url(self.get_url()), - description="A new course enrollment request has been submitted. Please review this request in the system and take the necessary action.", - content=content, - link_name="Link to the Course Enrolment Request", - ) - subject = f"Course Enrollment Request: {self.course}" - - msg = frappe.render_template('one_lms/templates/emails/default_email.html', context=context) - frappe.sendmail( - recipients=instructors, - subject=subject, - message=msg - ) - # Create Notification Log for each instructor - self.create_notification_log(instructors, subject, msg) - except Exception as e: - frappe.log_error(message=f"Error sending instructor notification: {e}", title="LMS Enrolment Request Notification") - - def get_instructors(self): - course_doc = frappe.get_doc("LMS Course", self.course) - instructors = [] - if course_doc.instructors: - for instructor in course_doc.instructors: - if instructor.instructor: - instructors.append(instructor.instructor) - return instructors - - def get_email_content_for_instructor(self): - return f"""The details of the enrolment request are shown below: + def after_insert(self): + self.notify_instructors_on_request() + + def notify_instructors_on_request(self): + try: + instructors = self.get_instructors() + if instructors: + content = self.get_email_content_for_instructor() + context = dict( + header="Dear Instructor,
Good day.", + document_name=self.name, + document_type=self.doctype, + document_link=frappe.utils.get_url(self.get_url()), + description="A new course enrollment request has been submitted. Please review this request in the system and take the necessary action.", + content=content, + link_name="Link to the Course Enrolment Request", + ) + subject = f"Course Enrollment Request: {self.course}" + + msg = frappe.render_template("one_lms/templates/emails/default_email.html", context=context) + frappe.sendmail(recipients=instructors, subject=subject, message=msg) + # Create Notification Log for each instructor + self.create_notification_log(instructors, subject, msg) + except Exception as e: + frappe.log_error( + message=f"Error sending instructor notification: {e}", + title="LMS Enrolment Request Notification", + ) + + def get_instructors(self): + course_doc = frappe.get_doc("LMS Course", self.course) + instructors = [] + if course_doc.instructors: + for instructor in course_doc.instructors: + if instructor.instructor: + instructors.append(instructor.instructor) + return instructors + + def get_email_content_for_instructor(self): + return f"""The details of the enrolment request are shown below:

Employee ID: {self.username}
@@ -58,79 +58,83 @@ def get_email_content_for_instructor(self): Request Date: {frappe.utils.format_datetime(self.creation, "medium")} """ - def create_notification_log(self, emails, subject, msg): - for user in emails: - notification_doc = frappe.get_doc({ - "doctype": "Notification Log", - "subject": subject, - "email_content": msg, - "document_type": self.doctype, - "document_name": self.name, - "from_user": frappe.session.user, - "type": "Alert", - "for_user": user - }) - notification_doc.insert(ignore_permissions=True) - - @frappe.whitelist() - def enrolment_request_approve_reject(self, action): - if action == "Approve": - self.db_set("status", "Approved") - elif action == "Reject": - self.db_set("status", "Rejected") - - self.on_update() - return "success" - - def on_update(self): - if self.status == "Approved": - self.create_enrollment() - self.notify_member_on_instructor_action() - - def create_enrollment(self): - if self.status != "Approved": - return - existing_enrollment = frappe.db.exists( - "LMS Enrollment", - {"course": self.course, "member": self.member, "member_type": "Student"}, - ) - if existing_enrollment: - return - frappe.get_doc({ - "doctype": "LMS Enrollment", - "course": self.course, - "role": "Member", - "member_type": "Student", - "member": self.member, - }).insert(ignore_permissions=True) - - def notify_member_on_instructor_action(self): - if self.status not in ["Approved", "Rejected"]: - return - content = self.get_email_content_for_member() - context = dict( - header="Dear {0},
Good day.".format(self.member), - document_name=self.name, - document_type=self.doctype, - document_link=frappe.utils.get_url(f"lms/courses/{self.course}"), - description=f"Your course enrollment request has been {self.status.lower()}.", - content=content, - link_name="Link to the Course Page", - ) - subject = f"Course Enrollment Request for {self.course} has been {self.status.lower()}" - - msg = frappe.render_template('one_lms/templates/emails/default_email.html', context=context) - # frappe.sendmail( - # recipients=[self.member], - # subject=subject, - # message=msg - # ) - # Create Notification Log for each instructor - self.create_notification_log([self.member], subject, msg) - self.send_push_notification() - - def get_email_content_for_member(self): - return f""" + def create_notification_log(self, emails, subject, msg): + for user in emails: + notification_doc = frappe.get_doc( + { + "doctype": "Notification Log", + "subject": subject, + "email_content": msg, + "document_type": self.doctype, + "document_name": self.name, + "from_user": frappe.session.user, + "type": "Alert", + "for_user": user, + } + ) + notification_doc.insert(ignore_permissions=True) + + @frappe.whitelist() + def enrolment_request_approve_reject(self, action): + if action == "Approve": + self.db_set("status", "Approved") + elif action == "Reject": + self.db_set("status", "Rejected") + + self.on_update() + return "success" + + def on_update(self): + if self.status == "Approved": + self.create_enrollment() + self.notify_member_on_instructor_action() + + def create_enrollment(self): + if self.status != "Approved": + return + existing_enrollment = frappe.db.exists( + "LMS Enrollment", + {"course": self.course, "member": self.member, "member_type": "Student"}, + ) + if existing_enrollment: + return + frappe.get_doc( + { + "doctype": "LMS Enrollment", + "course": self.course, + "role": "Member", + "member_type": "Student", + "member": self.member, + } + ).insert(ignore_permissions=True) + + def notify_member_on_instructor_action(self): + if self.status not in ["Approved", "Rejected"]: + return + content = self.get_email_content_for_member() + context = dict( + header=f"Dear {self.member},
Good day.", + document_name=self.name, + document_type=self.doctype, + document_link=frappe.utils.get_url(f"lms/courses/{self.course}"), + description=f"Your course enrollment request has been {self.status.lower()}.", + content=content, + link_name="Link to the Course Page", + ) + subject = f"Course Enrollment Request for {self.course} has been {self.status.lower()}" + + msg = frappe.render_template("one_lms/templates/emails/default_email.html", context=context) + # frappe.sendmail( + # recipients=[self.member], + # subject=subject, + # message=msg + # ) + # Create Notification Log for each instructor + self.create_notification_log([self.member], subject, msg) + self.send_push_notification() + + def get_email_content_for_member(self): + return f""" Course Name: {self.course}
Approver: {frappe.db.get_value("User", frappe.session.user, "full_name") or frappe.session.user} @@ -138,32 +142,34 @@ def get_email_content_for_member(self): Decision Date: {frappe.utils.format_datetime(self.modified, "medium")} """ - - def send_push_notification(self): - import requests, json - headers = { - "Content-Type": "application/json" - } - method = '/api/method/one_fm.api.api.push_notification_rest_api_for_lms' - site = getattr(frappe.local.conf, 'push_notification_backend_url', None) - if not site: - frappe.log_error(title="Error Sending Notification", message="Push notification site not set in site_config.json") - return - site = site.strip("/") - course_title = frappe.get_value("LMS Course", self.course, 'title') - data = { - "user_id": self.member - } - message = f""" + def send_push_notification(self): + import json + + import requests + + headers = {"Content-Type": "application/json"} + method = "/api/method/one_fm.api.api.push_notification_rest_api_for_lms" + site = getattr(frappe.local.conf, "push_notification_backend_url", None) + if not site: + frappe.log_error( + title="Error Sending Notification", + message="Push notification site not set in site_config.json", + ) + return + site = site.strip("/") + course_title = frappe.get_value("LMS Course", self.course, "title") + data = {"user_id": self.member} + message = f""" Your Enrollment Request for {course_title} has been {self.status.lower()}. """ - data['message'] = message - site_url = site + method - response = requests.post(site_url, data=json.dumps(data), headers=headers) - if response.status_code == 200: - return - else: - frappe.log_error(title="Error sending push notification", message=response.text) + data["message"] = message + site_url = site + method + response = requests.post(site_url, data=json.dumps(data), headers=headers) + if response.status_code == 200: + return + else: + frappe.log_error(title="Error sending push notification", message=response.text) + @frappe.whitelist() def create_lms_course_enrolment_request(course, member=None): @@ -176,10 +182,11 @@ def create_lms_course_enrolment_request(course, member=None): ).save(ignore_permissions=True) return "OK" + @frappe.whitelist() def has_pending_request(course, member): - pending_request = frappe.db.exists( - "LMS Course Enrolment Request", - {"course": course, "member": member, "status": "Open"}, - ) - return bool(pending_request) \ No newline at end of file + pending_request = frappe.db.exists( + "LMS Course Enrolment Request", + {"course": course, "member": member, "status": "Open"}, + ) + return bool(pending_request) diff --git a/one_lms/one_lms/doctype/lms_course_enrolment_request/test_lms_course_enrolment_request.py b/one_lms/one_lms/doctype/lms_course_enrolment_request/test_lms_course_enrolment_request.py index 62a8493..2e3e5fc 100644 --- a/one_lms/one_lms/doctype/lms_course_enrolment_request/test_lms_course_enrolment_request.py +++ b/one_lms/one_lms/doctype/lms_course_enrolment_request/test_lms_course_enrolment_request.py @@ -3,5 +3,6 @@ from frappe.tests.utils import FrappeTestCase + class TestLMSCourseEnrolmentRequest(FrappeTestCase): - pass + pass diff --git a/one_lms/one_lms/doctype/lms_enrollment_tool/lms_enrollment_tool.js b/one_lms/one_lms/doctype/lms_enrollment_tool/lms_enrollment_tool.js index 75a651c..6e69c3a 100644 --- a/one_lms/one_lms/doctype/lms_enrollment_tool/lms_enrollment_tool.js +++ b/one_lms/one_lms/doctype/lms_enrollment_tool/lms_enrollment_tool.js @@ -2,47 +2,52 @@ // For license information, please see license.txt frappe.ui.form.on("LMS Enrollment Tool", { - refresh(frm) { - frm.trigger("set_primary_action"); - frm.trigger("set_query_for_memeber"); - }, - set_primary_action(frm) { - frm.disable_save(); - frm.page.set_primary_action(__("Enroll to the Course"), () => { - if (frm.doc.members.length === 0) { - frappe.msgprint({ - message: __("Please set member in the Table to enrol"), - title: __("No Member added"), - indicator: "red" - }); - return; - } - frm.trigger("enrol_to_the_course"); - }); - }, - enrol_to_the_course(frm) { - frappe.call({ - method: "one_lms.one_lms.doctype.lms_enrollment_tool.lms_enrollment_tool.enrol_to_the_course", - args: { - members: frm.doc.members, - course: frm.doc.course - }, - freeze: true, - freeze_message: __("Enrolling the Members") - }).then((r) => { - if (!r.exc) { - frappe.show_alert({ message: __("Enrolled successfully"), indicator: "green" }); - frm.refresh(); - } - }); - }, - set_query_for_memeber(frm) { - frm.set_query("member", "members", function () { - return { - filters: { - ignore_user_type: 1, - }, - } - }); - } + refresh(frm) { + frm.trigger("set_primary_action"); + frm.trigger("set_query_for_memeber"); + }, + set_primary_action(frm) { + frm.disable_save(); + frm.page.set_primary_action(__("Enroll to the Course"), () => { + if (frm.doc.members.length === 0) { + frappe.msgprint({ + message: __("Please set member in the Table to enrol"), + title: __("No Member added"), + indicator: "red", + }); + return; + } + frm.trigger("enrol_to_the_course"); + }); + }, + enrol_to_the_course(frm) { + frappe + .call({ + method: "one_lms.one_lms.doctype.lms_enrollment_tool.lms_enrollment_tool.enrol_to_the_course", + args: { + members: frm.doc.members, + course: frm.doc.course, + }, + freeze: true, + freeze_message: __("Enrolling the Members"), + }) + .then((r) => { + if (!r.exc) { + frappe.show_alert({ + message: __("Enrolled successfully"), + indicator: "green", + }); + frm.refresh(); + } + }); + }, + set_query_for_memeber(frm) { + frm.set_query("member", "members", function () { + return { + filters: { + ignore_user_type: 1, + }, + }; + }); + }, }); diff --git a/one_lms/one_lms/doctype/lms_enrollment_tool/lms_enrollment_tool.py b/one_lms/one_lms/doctype/lms_enrollment_tool/lms_enrollment_tool.py index 8b8631b..d1c5d4a 100644 --- a/one_lms/one_lms/doctype/lms_enrollment_tool/lms_enrollment_tool.py +++ b/one_lms/one_lms/doctype/lms_enrollment_tool/lms_enrollment_tool.py @@ -1,31 +1,23 @@ # Copyright (c) 2024, Frappe and contributors # For license information, please see license.txt -import frappe -from frappe.model.document import Document import json +import frappe +from frappe.model.document import Document @frappe.whitelist() def enrol_to_the_course(members, course): - if isinstance(members, str): - members = json.loads(members) - for member in members: - if not frappe.db.exists( - "LMS Enrollment", - { - "member": member['member'], - "course": course - } - ): - lms_enrollment = frappe.get_doc( - dict( - doctype="LMS Enrollment", - member=member['member'], - course=course - ) - ) - lms_enrollment.insert() + if isinstance(members, str): + members = json.loads(members) + for member in members: + if not frappe.db.exists("LMS Enrollment", {"member": member["member"], "course": course}): + lms_enrollment = frappe.get_doc( + dict(doctype="LMS Enrollment", member=member["member"], course=course) + ) + lms_enrollment.insert() + + class LMSEnrollmentTool(Document): - pass \ No newline at end of file + pass diff --git a/one_lms/one_lms/doctype/lms_enrollment_tool/test_lms_enrollment_tool.py b/one_lms/one_lms/doctype/lms_enrollment_tool/test_lms_enrollment_tool.py index 5efa876..9ab1ffc 100644 --- a/one_lms/one_lms/doctype/lms_enrollment_tool/test_lms_enrollment_tool.py +++ b/one_lms/one_lms/doctype/lms_enrollment_tool/test_lms_enrollment_tool.py @@ -3,5 +3,6 @@ from frappe.tests.utils import FrappeTestCase + class TestLMSEnrollmentTool(FrappeTestCase): - pass + pass diff --git a/one_lms/one_lms/doctype/lms_enrollment_tool_member/lms_enrollment_tool_member.py b/one_lms/one_lms/doctype/lms_enrollment_tool_member/lms_enrollment_tool_member.py index 0c7b0cc..3f830ba 100644 --- a/one_lms/one_lms/doctype/lms_enrollment_tool_member/lms_enrollment_tool_member.py +++ b/one_lms/one_lms/doctype/lms_enrollment_tool_member/lms_enrollment_tool_member.py @@ -3,5 +3,6 @@ from frappe.model.document import Document + class LMSEnrollmentToolMember(Document): - pass + pass diff --git a/one_lms/overrides/course_lesson.py b/one_lms/overrides/course_lesson.py index 4322d92..8d34833 100644 --- a/one_lms/overrides/course_lesson.py +++ b/one_lms/overrides/course_lesson.py @@ -1,12 +1,12 @@ import frappe from lms.lms.utils import get_course_progress + from ...md import find_macros + @frappe.whitelist() def save_progress(lesson, course): - membership = frappe.db.exists( - "LMS Enrollment", {"member": frappe.session.user, "course": course} - ) + membership = frappe.db.exists("LMS Enrollment", {"member": frappe.session.user, "course": course}) if not membership: return 0 @@ -46,4 +46,4 @@ def save_progress(lesson, course): if progress == 100: frappe.db.set_value("LMS Enrollment", membership, "course_completion_date", frappe.utils.today()) frappe.db.commit() - return progress \ No newline at end of file + return progress diff --git a/one_lms/overrides/lms_assignment_submission.py b/one_lms/overrides/lms_assignment_submission.py index e802a87..6ca2730 100644 --- a/one_lms/overrides/lms_assignment_submission.py +++ b/one_lms/overrides/lms_assignment_submission.py @@ -2,61 +2,62 @@ from frappe import _ from frappe.utils import validate_url + @frappe.whitelist() def upload_assignment( - assignment_attachment=None, - answer=None, - assignment=None, - lesson=None, - status="Not Graded", - comments=None, - submission=None, + assignment_attachment=None, + answer=None, + assignment=None, + lesson=None, + status="Not Graded", + comments=None, + submission=None, ): - if frappe.session.user == "Guest": - return - - assignment_details = frappe.db.get_value( - "LMS Assignment", assignment, ["type", "grade_assignment"], as_dict=1 - ) - if not assignment_details and not assignment and lesson: - assignment = frappe.get_value("Course Lesson", lesson, "lms_assignment") - assignment_details = frappe.db.get_value( - "LMS Assignment", assignment, ["type", "grade_assignment"], as_dict=1 - ) - - assignment_type = assignment_details.type - - if assignment_type in ["URL", "Text"] and not answer: - frappe.throw(_("Please enter the URL for assignment submission.")) - - if assignment_type == "File" and not assignment_attachment: - frappe.throw(_("Please upload the assignment file.")) - - if assignment_type == "URL" and not validate_url(answer): - frappe.throw(_("Please enter a valid URL.")) - - if submission: - doc = frappe.get_doc("LMS Assignment Submission", submission) - else: - doc = frappe.get_doc( - { - "doctype": "LMS Assignment Submission", - "assignment": assignment, - "lesson": lesson, - "member": frappe.session.user, - "type": assignment_type, - } - ) - - doc.update( - { - "assignment_attachment": assignment_attachment, - "status": "Not Applicable" - if assignment_type == "Text" and not assignment_details.grade_assignment - else status, - "comments": comments, - "answer": answer, - } - ) - doc.save(ignore_permissions=True) - return doc.name + if frappe.session.user == "Guest": + return + + assignment_details = frappe.db.get_value( + "LMS Assignment", assignment, ["type", "grade_assignment"], as_dict=1 + ) + if not assignment_details and not assignment and lesson: + assignment = frappe.get_value("Course Lesson", lesson, "lms_assignment") + assignment_details = frappe.db.get_value( + "LMS Assignment", assignment, ["type", "grade_assignment"], as_dict=1 + ) + + assignment_type = assignment_details.type + + if assignment_type in ["URL", "Text"] and not answer: + frappe.throw(_("Please enter the URL for assignment submission.")) + + if assignment_type == "File" and not assignment_attachment: + frappe.throw(_("Please upload the assignment file.")) + + if assignment_type == "URL" and not validate_url(answer): + frappe.throw(_("Please enter a valid URL.")) + + if submission: + doc = frappe.get_doc("LMS Assignment Submission", submission) + else: + doc = frappe.get_doc( + { + "doctype": "LMS Assignment Submission", + "assignment": assignment, + "lesson": lesson, + "member": frappe.session.user, + "type": assignment_type, + } + ) + + doc.update( + { + "assignment_attachment": assignment_attachment, + "status": "Not Applicable" + if assignment_type == "Text" and not assignment_details.grade_assignment + else status, + "comments": comments, + "answer": answer, + } + ) + doc.save(ignore_permissions=True) + return doc.name diff --git a/one_lms/overrides/lms_batch.py b/one_lms/overrides/lms_batch.py index daef7b2..0f0c9e6 100644 --- a/one_lms/overrides/lms_batch.py +++ b/one_lms/overrides/lms_batch.py @@ -2,42 +2,41 @@ from frappe.email.doctype.email_template.email_template import get_email_template from lms.lms.doctype.lms_batch.lms_batch import LMSBatch as BaseLMSBatch + class LMSBatch(BaseLMSBatch): - def send_mail(self, student): - subject = frappe._("Enrollment Confirmation for the Next Training Batch") - template = "batch_confirmation" - custom_template = frappe.db.get_single_value( - "LMS Settings", "batch_confirmation_template" - ) + def send_mail(self, student): + subject = frappe._("Enrollment Confirmation for the Next Training Batch") + template = "batch_confirmation" + custom_template = frappe.db.get_single_value("LMS Settings", "batch_confirmation_template") - args = { - "student_name": student.student_name, - "start_time": self.start_time, - "start_date": self.start_date, - "end_date": self.end_date, - "end_time": self.end_time, - "medium": self.medium, - "name": self.name, - } - if self.courses: - args['courses'] = " \n ".join([i.title for i in self.courses]) - doc_dict = self.as_dict() - doc_keys = doc_dict.keys() - if doc_keys: - for each in doc_keys: - if not args.get(each): - args[each] = doc_dict.get(each) - if custom_template: - email_template = get_email_template(custom_template, args) - subject = email_template.get("subject") - content = email_template.get("message") + args = { + "student_name": student.student_name, + "start_time": self.start_time, + "start_date": self.start_date, + "end_date": self.end_date, + "end_time": self.end_time, + "medium": self.medium, + "name": self.name, + } + if self.courses: + args["courses"] = " \n ".join([i.title for i in self.courses]) + doc_dict = self.as_dict() + doc_keys = doc_dict.keys() + if doc_keys: + for each in doc_keys: + if not args.get(each): + args[each] = doc_dict.get(each) + if custom_template: + email_template = get_email_template(custom_template, args) + subject = email_template.get("subject") + content = email_template.get("message") - frappe.sendmail( - recipients=student.student, - subject=subject, - template=template if not custom_template else None, - content=content if custom_template else None, - args=args, - header=[subject, "green"], - retry=3, - ) + frappe.sendmail( + recipients=student.student, + subject=subject, + template=template if not custom_template else None, + content=content if custom_template else None, + args=args, + header=[subject, "green"], + retry=3, + ) diff --git a/one_lms/overrides/lms_certificate.py b/one_lms/overrides/lms_certificate.py index c1a4449..f3031e6 100644 --- a/one_lms/overrides/lms_certificate.py +++ b/one_lms/overrides/lms_certificate.py @@ -1,37 +1,39 @@ import frappe from frappe import _ +from frappe.email.doctype.email_template.email_template import get_email_template from frappe.model.document import Document from frappe.utils import add_years, nowdate -from lms.lms.utils import is_certified -from frappe.email.doctype.email_template.email_template import get_email_template from lms.lms.doctype.lms_certificate.lms_certificate import LMSCertificate as BaseLMSCertificate +from lms.lms.utils import is_certified + class LMSCertificate(BaseLMSCertificate): - def send_mail(self): - subject = _("Congratulations on getting certified!") - template = "certification" - custom_template = frappe.db.get_single_value("LMS Settings", "certification_template") - course_name, course_title = frappe.db.get_value("LMS Course", self.course, ["name", "title"]) + def send_mail(self): + subject = _("Congratulations on getting certified!") + template = "certification" + custom_template = frappe.db.get_single_value("LMS Settings", "certification_template") + course_name, course_title = frappe.db.get_value("LMS Course", self.course, ["name", "title"]) + + args = { + "student_name": self.member_name, + "course": course_title, + "certificate_name": self.name, + "certificate_link": f"{frappe.utils.get_url()}/courses/{course_name}/{self.name}", + } - args = { - "student_name": self.member_name, - "course": course_title, - "certificate_name": self.name, - "certificate_link":f"{frappe.utils.get_url()}/courses/{course_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"], + ) - 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"], - ) @frappe.whitelist() def create_certificate(course): @@ -70,4 +72,4 @@ def create_certificate(course): } ) certificate.save(ignore_permissions=True) - return certificate \ No newline at end of file + return certificate diff --git a/one_lms/overrides/lms_course.py b/one_lms/overrides/lms_course.py index 40558a1..3f4969c 100644 --- a/one_lms/overrides/lms_course.py +++ b/one_lms/overrides/lms_course.py @@ -1,30 +1,34 @@ import frappe from frappe.model.document import Document + def is_re_enrollment_allowed(course): - return frappe.db.get_value("LMS Course", course, "allow_reenrollments") + return frappe.db.get_value("LMS Course", course, "allow_reenrollments") + def re_enroll_member(course, member): - frappe.db.delete("LMS Course Progress", { "course": course, "member": member }) - enrollment = frappe.get_doc("LMS Enrollment", { "course": course, "member": member }) - enrollment.progress = 0 - enrollment.current_lesson = "" - enrollment.save(ignore_permissions=True) + frappe.db.delete("LMS Course Progress", {"course": course, "member": member}) + enrollment = frappe.get_doc("LMS Enrollment", {"course": course, "member": member}) + enrollment.progress = 0 + enrollment.current_lesson = "" + enrollment.save(ignore_permissions=True) + @frappe.whitelist() def re_enroll_single_member(course, member): - if not is_re_enrollment_allowed(course): - frappe.throw("Re-Enrollments are not allowed in this course") - if not frappe.db.exists("LMS Enrollment", { "course": course, "member": member }): - frappe.throw(f"{member} is currently not enrolled in {course}") - re_enroll_member(course=course, member=member) - return "OK" + if not is_re_enrollment_allowed(course): + frappe.throw("Re-Enrollments are not allowed in this course") + if not frappe.db.exists("LMS Enrollment", {"course": course, "member": member}): + frappe.throw(f"{member} is currently not enrolled in {course}") + re_enroll_member(course=course, member=member) + return "OK" + @frappe.whitelist() def re_enroll_all_members(course): - if not is_re_enrollment_allowed(course): - frappe.throw("Re-Enrollments are not allowed in this course") - enrollments = frappe.get_all("LMS Enrollment", {"course": course}, ["member", "course"]) - for enrollment in enrollments: - re_enroll_member(course=enrollment.course, member=enrollment.member) - return "OK" + if not is_re_enrollment_allowed(course): + frappe.throw("Re-Enrollments are not allowed in this course") + enrollments = frappe.get_all("LMS Enrollment", {"course": course}, ["member", "course"]) + for enrollment in enrollments: + re_enroll_member(course=enrollment.course, member=enrollment.member) + return "OK" diff --git a/one_lms/overrides/lms_enrollment.py b/one_lms/overrides/lms_enrollment.py index d2c6140..bca4b23 100644 --- a/one_lms/overrides/lms_enrollment.py +++ b/one_lms/overrides/lms_enrollment.py @@ -3,52 +3,48 @@ from frappe.model.document import Document from lms.lms.doctype.lms_enrollment.lms_enrollment import LMSEnrollment as BaseLMSEnrollment -class LMSEnrollment(BaseLMSEnrollment): - def validate(self): - - if self.is_new(): - self.notify_user() - - def notify_user(self): - """Notify the user that they have been Enrolled in a course. Add course details and include link as well""" - template = "one_lms/templates/emails/lms_course_enrollment.html" - try: - recipient = [self.member] - subject = "You have been Enrolled!" - course_title = frappe.get_value("LMS Course", self.course, 'title') - if not getattr(self, 'custom_date_', None): - self.custom_date_ = frappe.utils.nowdate() - args = { - 'course_name': course_title, - 'student_name': self.member_name, - 'enrollment_date': self.custom_date_, - 'course_url': f"{frappe.utils.get_url()}/courses/{self.course}/" - } - message = frappe.render_template(template, context=args) - frappe.sendmail( - recipients=recipient, - subject=subject, - message=message, - header=["Course Enrollment on LMS", "green"], - ) - frappe.msgprint("Employee Notified", alert=1) - except Exception as e: - frappe.msgprint("Error Notifying Employee", alert=1) - frappe.log_error(title="Error Notifying Employee", message=e) +class LMSEnrollment(BaseLMSEnrollment): + def validate(self): + if self.is_new(): + self.notify_user() - - def before_insert(self): - self.reset_course_progress() + def notify_user(self): + """Notify the user that they have been Enrolled in a course. Add course details and include link as well""" + template = "one_lms/templates/emails/lms_course_enrollment.html" + try: + recipient = [self.member] + subject = "You have been Enrolled!" + course_title = frappe.get_value("LMS Course", self.course, "title") + if not getattr(self, "custom_date_", None): + self.custom_date_ = frappe.utils.nowdate() + args = { + "course_name": course_title, + "student_name": self.member_name, + "enrollment_date": self.custom_date_, + "course_url": f"{frappe.utils.get_url()}/courses/{self.course}/", + } + message = frappe.render_template(template, context=args) + frappe.sendmail( + recipients=recipient, + subject=subject, + message=message, + header=["Course Enrollment on LMS", "green"], + ) + frappe.msgprint("Employee Notified", alert=1) + except Exception as e: + frappe.msgprint("Error Notifying Employee", alert=1) + frappe.log_error(title="Error Notifying Employee", message=e) - def reset_course_progress(self): - if not frappe.db.get_value("LMS Course", self.course, "allow_reenrollments"): - return - if not frappe.db.exists("LMS Enrollment", {"course": self.course, "member": self.member}): - return - frappe.db.sql( - "UPDATE `tabLMS Course Progress` SET status = 'Incomplete' WHERE course = %s AND member = %s", - (self.course, self.member) - ) + def before_insert(self): + self.reset_course_progress() - + def reset_course_progress(self): + if not frappe.db.get_value("LMS Course", self.course, "allow_reenrollments"): + return + if not frappe.db.exists("LMS Enrollment", {"course": self.course, "member": self.member}): + return + frappe.db.sql( + "UPDATE `tabLMS Course Progress` SET status = 'Incomplete' WHERE course = %s AND member = %s", + (self.course, self.member), + ) diff --git a/one_lms/overrides/lms_quiz_submission.py b/one_lms/overrides/lms_quiz_submission.py index 7e38512..b183d99 100644 --- a/one_lms/overrides/lms_quiz_submission.py +++ b/one_lms/overrides/lms_quiz_submission.py @@ -1,11 +1,12 @@ import frappe from lms.lms.doctype.lms_quiz_submission.lms_quiz_submission import LMSQuizSubmission as LMSBaseQuizSubmission + class LMSQuizSubmission(LMSBaseQuizSubmission): - def validate(self): - self.set_submission_date() - super().validate() + def validate(self): + self.set_submission_date() + super().validate() - def set_submission_date(self): - if self.is_new(): - self.custom_date = frappe.utils.now_datetime() + def set_submission_date(self): + if self.is_new(): + self.custom_date = frappe.utils.now_datetime() diff --git a/one_lms/overrides/plugins.py b/one_lms/overrides/plugins.py index b7239fe..e4207f8 100644 --- a/one_lms/overrides/plugins.py +++ b/one_lms/overrides/plugins.py @@ -1,32 +1,37 @@ import random -import frappe from urllib.parse import quote +import frappe +from frappe import _ + + def assignment_renderer(detail): - supported_types = { - "Document": ".doc,.docx,.xml,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "PDF": ".pdf", - "Image": ".png, .jpg, .jpeg", - "Video": "video/*", - } - question = detail.split("-")[0] - file_type = frappe.db.get_value("Course Lesson", {'question': question}, 'file_type') or "PDF" - accept = supported_types[file_type] if file_type else "" - return frappe.render_template( - "templates/assignment.html", - { - "question": question, - "file_type": file_type, - "accept": accept, - }, - ) + supported_types = { + "Document": ".doc,.docx,.xml,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "PDF": ".pdf", + "Image": ".png, .jpg, .jpeg", + "Video": "video/*", + } + question = detail.split("-")[0] + file_type = frappe.db.get_value("Course Lesson", {"question": question}, "file_type") or "PDF" + accept = supported_types[file_type] if file_type else "" + return frappe.render_template( + "templates/assignment.html", + { + "question": question, + "file_type": file_type, + "accept": accept, + }, + ) + def quiz_renderer(quiz_name): if frappe.session.user == "Guest": - return "
" + _( - "Quiz is not available to Guest users. Please login to continue." + return ( + "
" + + _("Quiz is not available to Guest users. Please login to continue.") + + "
" ) - +"
" quiz = frappe.db.get_value( "LMS Quiz", @@ -66,9 +71,7 @@ def quiz_renderer(quiz_name): quiz.questions = question_list - no_of_attempts = frappe.db.count( - "LMS Quiz Submission", {"owner": frappe.session.user, "quiz": quiz_name} - ) + no_of_attempts = frappe.db.count("LMS Quiz Submission", {"owner": frappe.session.user, "quiz": quiz_name}) if quiz.show_submission_history: all_submissions = frappe.get_all( @@ -89,4 +92,4 @@ def quiz_renderer(quiz_name): "all_submissions": all_submissions if quiz.show_submission_history else None, "hide_quiz": False, }, - ) \ No newline at end of file + ) diff --git a/one_lms/overrides/user.py b/one_lms/overrides/user.py index 6f74ad1..cec8e46 100644 --- a/one_lms/overrides/user.py +++ b/one_lms/overrides/user.py @@ -2,13 +2,14 @@ from frappe.core.doctype.user.user import User as BaseUser from frappe.desk.doctype.notification_settings.notification_settings import create_notification_settings + class User(BaseUser): - def after_insert(self): - create_notification_settings(self.name) - frappe.cache.delete_key("users_for_mentions") - frappe.cache.delete_key("enabled_users") - # Add LMS Student role on user creation if not already added - # On new user signup using SSO, LMS student role is not added by default - roles = frappe.get_roles(self.name) - if "LMS Student" not in roles: - self.add_roles("LMS Student") + def after_insert(self): + create_notification_settings(self.name) + frappe.cache.delete_key("users_for_mentions") + frappe.cache.delete_key("enabled_users") + # Add LMS Student role on user creation if not already added + # On new user signup using SSO, LMS student role is not added by default + roles = frappe.get_roles(self.name) + if "LMS Student" not in roles: + self.add_roles("LMS Student") diff --git a/one_lms/public/js/doctype_js/course_lesson.js b/one_lms/public/js/doctype_js/course_lesson.js index b40716b..bc7379b 100644 --- a/one_lms/public/js/doctype_js/course_lesson.js +++ b/one_lms/public/js/doctype_js/course_lesson.js @@ -1,7 +1,7 @@ frappe.ui.form.on("Course Lesson", { - quiz: function(frm) { - if (frm.doc.quiz) { - frm.set_value('quiz_id', frm.doc.quiz); - } - } + quiz: function (frm) { + if (frm.doc.quiz) { + frm.set_value("quiz_id", frm.doc.quiz); + } + }, }); diff --git a/one_lms/public/js/doctype_js/lms_certificate.js b/one_lms/public/js/doctype_js/lms_certificate.js index be063e1..37b4fb8 100644 --- a/one_lms/public/js/doctype_js/lms_certificate.js +++ b/one_lms/public/js/doctype_js/lms_certificate.js @@ -1,14 +1,13 @@ frappe.ui.form.on("LMS Certificate", { - course: (frm) => { + course: (frm) => { if (frm.doc.course) { - frappe.db.get_value("LMS Course", frm.doc.course, "template") - .then((r) => { - if (r && r.message && r.message.template) { - frm.set_value("template", r.message.template); - } - }); + frappe.db.get_value("LMS Course", frm.doc.course, "template").then((r) => { + if (r && r.message && r.message.template) { + frm.set_value("template", r.message.template); + } + }); } else { frm.set_value("template", null); } - } -}); \ No newline at end of file + }, +}); diff --git a/one_lms/public/js/doctype_js/lms_course.js b/one_lms/public/js/doctype_js/lms_course.js index 308b328..19937c7 100644 --- a/one_lms/public/js/doctype_js/lms_course.js +++ b/one_lms/public/js/doctype_js/lms_course.js @@ -1,23 +1,23 @@ frappe.ui.form.on("LMS Course", { - refresh: (frm) => { - add_web_link(frm); - set_currency(frm); - re_enroll_members(frm); - }, - onload: (frm) => { - frm.trigger("instructors"); - }, - instructors: async function(frm) { + refresh: (frm) => { + add_web_link(frm); + set_currency(frm); + re_enroll_members(frm); + }, + onload: (frm) => { + frm.trigger("instructors"); + }, + instructors: async function (frm) { const instructorRows = frm.doc.instructors || []; if (instructorRows.length === 0) { - frm.set_value('default_instructor', ''); - frm.set_df_property('default_instructor', 'options', ''); + frm.set_value("default_instructor", ""); + frm.set_df_property("default_instructor", "options", ""); return; } - const instructorIds = instructorRows.map(row => row.instructor).filter(Boolean); + const instructorIds = instructorRows.map((row) => row.instructor).filter(Boolean); if (instructorIds.length === 0) { - frm.set_value('default_instructor', ''); - frm.set_df_property('default_instructor', 'options', ''); + frm.set_value("default_instructor", ""); + frm.set_df_property("default_instructor", "options", ""); return; } try { @@ -25,102 +25,95 @@ frappe.ui.form.on("LMS Course", { method: "frappe.client.get_list", args: { doctype: "User", - filters: { "name": ["in", instructorIds] }, - fields: ["full_name"] + filters: { name: ["in", instructorIds] }, + fields: ["full_name"], }, - freeze: false + freeze: false, }); const users = response && response.message ? response.message : []; - const instructorNames = users.map(user => user.full_name).filter(Boolean); - frm.set_df_property('default_instructor', 'options', instructorNames.join('\n')); - frm.set_value('default_instructor', instructorNames[0] || ''); + const instructorNames = users.map((user) => user.full_name).filter(Boolean); + frm.set_df_property("default_instructor", "options", instructorNames.join("\n")); + frm.set_value("default_instructor", instructorNames[0] || ""); } catch (e) { - frm.set_value('default_instructor', ''); - frm.set_df_property('default_instructor', 'options', ''); - frappe.msgprint(__('Failed to fetch instructor names.')); + frm.set_value("default_instructor", ""); + frm.set_df_property("default_instructor", "options", ""); + frappe.msgprint(__("Failed to fetch instructor names.")); } - } + }, }); -const add_web_link = (frm) => - frm.add_web_link(`/courses/${frm.doc.name}`, "See on Website"); +const add_web_link = (frm) => frm.add_web_link(`/courses/${frm.doc.name}`, "See on Website"); const set_currency = (frm) => { - if (!frm.doc.currency) - frappe.db - .get_single_value("LMS Settings", "default_currency") - .then((value) => { - frm.set_value("currency", value); - }); + if (!frm.doc.currency) + frappe.db.get_single_value("LMS Settings", "default_currency").then((value) => { + frm.set_value("currency", value); + }); }; const re_enroll_members = (frm) => { - if (frm.doc.allow_reenrollments) { - frm.add_custom_button( - __("All Members"), - () => re_enroll_all_members(frm), - __("Re-Enroll") - ); - frm.add_custom_button( - __("Single Member"), - () => re_enroll_single_member(frm), - __("Re-Enroll") - ); - } + if (frm.doc.allow_reenrollments) { + frm.add_custom_button( + __("All Members"), + () => re_enroll_all_members(frm), + __("Re-Enroll") + ); + frm.add_custom_button( + __("Single Member"), + () => re_enroll_single_member(frm), + __("Re-Enroll") + ); + } }; const re_enroll_all_members = (frm) => { - frappe.confirm( - "Are you sure you want to re-enroll all members?", - () => { - frappe.call({ - method: "one_lms.overrides.lms_course.re_enroll_all_members", - freeze: true, - freeze_message: "Re-enrolling all members", - args: { course: frm.doc.name }, - callback: function (r) { - if (r.message == "OK") { - frappe.msgprint( - "All members re-enrolled successfully" - ); - } - }, - }); - }, - () => null - ); + frappe.confirm( + "Are you sure you want to re-enroll all members?", + () => { + frappe.call({ + method: "one_lms.overrides.lms_course.re_enroll_all_members", + freeze: true, + freeze_message: "Re-enrolling all members", + args: { course: frm.doc.name }, + callback: function (r) { + if (r.message == "OK") { + frappe.msgprint("All members re-enrolled successfully"); + } + }, + }); + }, + () => null + ); }; const re_enroll_single_member = function (frm) { - const dialog = new frappe.ui.Dialog({ - title: __("Re-Enroll Member"), - fields: [ - { - fieldtype: "Link", - label: __("User"), - fieldname: "user", - options: "User", - reqd: 1, - }, - ], - }); - dialog.set_primary_action(__("Create"), function () { - const data = dialog.get_values(); - if (!data) return; - frappe.call({ - method: "one_lms.overrides.lms_course.re_enroll_single_member", - freeze: true, - freeze_message: "Re-enrolling member", - args: { course: frm.doc.name, member: data.user }, - callback: function (r) { - dialog.hide(); - if (r.message == "OK") { - frappe.msgprint( - "Member re-enrolled successfully" - ); - } - }, - }); - }); - dialog.show(); + const dialog = new frappe.ui.Dialog({ + title: __("Re-Enroll Member"), + fields: [ + { + fieldtype: "Link", + label: __("User"), + fieldname: "user", + options: "User", + reqd: 1, + }, + ], + }); + dialog.set_primary_action(__("Create"), function () { + const data = dialog.get_values(); + if (!data) return; + frappe.call({ + method: "one_lms.overrides.lms_course.re_enroll_single_member", + freeze: true, + freeze_message: "Re-enrolling member", + args: { course: frm.doc.name, member: data.user }, + callback: function (r) { + dialog.hide(); + if (r.message == "OK") { + frappe.msgprint("Member re-enrolled successfully"); + } + }, + }); + }); + dialog.show(); }; diff --git a/one_lms/public/js/frappe_tinymce.js b/one_lms/public/js/frappe_tinymce.js index 1e03c80..9956678 100644 --- a/one_lms/public/js/frappe_tinymce.js +++ b/one_lms/public/js/frappe_tinymce.js @@ -1,80 +1,93 @@ - frappe.ui.form.ControlTextEditor = class ControlTextEditor extends frappe.ui.form.ControlCode { - make_wrapper() { - super.make_wrapper(); - } - - make_input() { - this.has_input = true; - this.make_quill_editor(); - } - - make_quill_editor() { - const that = this - this.quill_container = $('
').appendTo(this.input_area); - - tinymce.init({ - target: this.input_area, - toolbar: 'undo redo | bold italic underline strikethrough | fontfamily fontsize blocks | alignleft aligncenter alignright alignjustify | outdent indent | numlist bullist checklist | forecolor backcolor casechange permanentpen formatpainter removeformat | pagebreak | charmap emoticons | fullscreen preview save print | insertfile media pageembed template link anchor codesample | a11ycheck ltr rtl | showcomments addcomment | footnotes | mergetags | customHRButton | image rotateleft rotateright', - font_size_formats: '10px 11px 12px 14px 15px 16px 18px 24px 36px', - plugins: [ - 'autolink', 'charmap', 'emoticons', 'fullscreen', 'help', - 'image', 'link', 'lists', 'searchreplace', - 'table', 'visualblocks', 'visualchars', 'wordcount', 'media', 'anchor' + make_wrapper() { + super.make_wrapper(); + } + + make_input() { + this.has_input = true; + this.make_quill_editor(); + } + + make_quill_editor() { + const that = this; + this.quill_container = $("
").appendTo(this.input_area); + + tinymce.init({ + target: this.input_area, + toolbar: + "undo redo | bold italic underline strikethrough | fontfamily fontsize blocks | alignleft aligncenter alignright alignjustify | outdent indent | numlist bullist checklist | forecolor backcolor casechange permanentpen formatpainter removeformat | pagebreak | charmap emoticons | fullscreen preview save print | insertfile media pageembed template link anchor codesample | a11ycheck ltr rtl | showcomments addcomment | footnotes | mergetags | customHRButton | image rotateleft rotateright", + font_size_formats: "10px 11px 12px 14px 15px 16px 18px 24px 36px", + plugins: [ + "autolink", + "charmap", + "emoticons", + "fullscreen", + "help", + "image", + "link", + "lists", + "searchreplace", + "table", + "visualblocks", + "visualchars", + "wordcount", + "media", + "anchor", ], - powerpaste_googledocs_import: "prompt", - entity_encoding: 'raw', - convert_urls: true, - content_css: false, - toolbar_sticky: false, - promotion: false, - default_link_target: "_blank", - height: 500, - file_picker_types: 'image', + powerpaste_googledocs_import: "prompt", + entity_encoding: "raw", + convert_urls: true, + content_css: false, + toolbar_sticky: false, + promotion: false, + default_link_target: "_blank", + height: 500, + file_picker_types: "image", file_picker_callback: (cb, value, meta) => { - const input = document.createElement('input'); - input.setAttribute('type', 'file'); - input.setAttribute('accept', 'image/*'); - - input.addEventListener('change', (e) => { - const file = e.target.files[0]; - - const reader = new FileReader(); - reader.addEventListener('load', () => { - const id = 'blobid' + (new Date()).getTime(); - const blobCache = tinymce.activeEditor.editorUpload.blobCache; - const base64 = reader.result.split(',')[1]; - const blobInfo = blobCache.create(id, file, base64); - blobCache.add(blobInfo); - - cb(blobInfo.blobUri(), { title: file.name }); - }); - reader.readAsDataURL(file); + const input = document.createElement("input"); + input.setAttribute("type", "file"); + input.setAttribute("accept", "image/*"); + + input.addEventListener("change", (e) => { + const file = e.target.files[0]; + + const reader = new FileReader(); + reader.addEventListener("load", () => { + const id = "blobid" + new Date().getTime(); + const blobCache = tinymce.activeEditor.editorUpload.blobCache; + const base64 = reader.result.split(",")[1]; + const blobInfo = blobCache.create(id, file, base64); + blobCache.add(blobInfo); + + cb(blobInfo.blobUri(), { title: file.name }); + }); + reader.readAsDataURL(file); }); - + input.click(); - }, + }, // content_style: "body { font-family: Calibri, sans-serif; }", - font_family_formats: "Andale Mono=andale mono,times; Arial=arial,helvetica,sans-serif; Arial Black=arial black,avant garde; Book Antiqua=book antiqua,palatino; Calibri=Calibri, sans-serif; Comic Sans MS=comic sans ms,sans-serif; Courier New=courier new,courier; Georgia=georgia,palatino; Helvetica=helvetica; Impact=impact,chicago; Symbol=symbol; Tahoma=tahoma,arial,helvetica,sans-serif; Terminal=terminal,monaco; Times New Roman=times new roman,times; Trebuchet MS=trebuchet ms,geneva; Verdana=verdana,geneva; Webdings=webdings; Wingdings=wingdings,zapf dingbats", - setup: function(editor) { - that.editor_id = editor.id; - - editor.ui.registry.addButton('customHRButton', { - icon: 'horizontal-rule', - tooltip: 'Insert Horizontal Rule', - onAction: function (_) { - editor.selection.setContent('
'); - } - }); + font_family_formats: + "Andale Mono=andale mono,times; Arial=arial,helvetica,sans-serif; Arial Black=arial black,avant garde; Book Antiqua=book antiqua,palatino; Calibri=Calibri, sans-serif; Comic Sans MS=comic sans ms,sans-serif; Courier New=courier new,courier; Georgia=georgia,palatino; Helvetica=helvetica; Impact=impact,chicago; Symbol=symbol; Tahoma=tahoma,arial,helvetica,sans-serif; Terminal=terminal,monaco; Times New Roman=times new roman,times; Trebuchet MS=trebuchet ms,geneva; Verdana=verdana,geneva; Webdings=webdings; Wingdings=wingdings,zapf dingbats", + setup: function (editor) { + that.editor_id = editor.id; + + editor.ui.registry.addButton("customHRButton", { + icon: "horizontal-rule", + tooltip: "Insert Horizontal Rule", + onAction: function (_) { + editor.selection.setContent("
"); + }, + }); editor.ui.registry.addButton("rotateleft", { text: "⟲", tooltip: "Rotate Left", onAction: function () { - rotateImage(editor, -90); + rotateImage(editor, -90); }, - }); - + }); + editor.ui.registry.addButton("rotateright", { text: "⟳", tooltip: "Rotate Right", @@ -83,41 +96,42 @@ frappe.ui.form.ControlTextEditor = class ControlTextEditor extends frappe.ui.for }, }); - editor.on('Change', function(e) { - that.parse_validate_and_set_in_model(e.level.content); - }); - editor.on('init', function (e) { - editor.setContent(that.value || ""); - - let tinyMCEContainer = $('.tox-editor-container'); - tinyMCEContainer.css('z-index', 0); - }); - - } - }); - this.activeEditor = tinymce.activeEditor - } - - set_formatted_input(value) { - if (!this.frm) return; - - if (!value) { - this.activeEditor.setContent(""); - return; - } - - let bookmark = this.activeEditor.selection ? this.activeEditor.selection.getBookmark(2, true) : null; - this.activeEditor.setContent(value); - - if (bookmark) { - this.activeEditor.selection.moveToBookmark(bookmark); - } - } - - get_input_value() { - return this.activeEditor.getContent() - } -} + editor.on("Change", function (e) { + that.parse_validate_and_set_in_model(e.level.content); + }); + editor.on("init", function (e) { + editor.setContent(that.value || ""); + + let tinyMCEContainer = $(".tox-editor-container"); + tinyMCEContainer.css("z-index", 0); + }); + }, + }); + this.activeEditor = tinymce.activeEditor; + } + + set_formatted_input(value) { + if (!this.frm) return; + + if (!value) { + this.activeEditor.setContent(""); + return; + } + + let bookmark = this.activeEditor.selection + ? this.activeEditor.selection.getBookmark(2, true) + : null; + this.activeEditor.setContent(value); + + if (bookmark) { + this.activeEditor.selection.moveToBookmark(bookmark); + } + } + + get_input_value() { + return this.activeEditor.getContent(); + } +}; function rotateImage(editor, angle) { let img = editor.selection.getNode(); // Get selected image diff --git a/one_lms/public/overrides/components/CourseCardOverlay.vue b/one_lms/public/overrides/components/CourseCardOverlay.vue index a7e4fe0..8e6e50e 100644 --- a/one_lms/public/overrides/components/CourseCardOverlay.vue +++ b/one_lms/public/overrides/components/CourseCardOverlay.vue @@ -30,7 +30,7 @@ - {{ __('Continue Learning') }} + {{ __("Continue Learning") }} @@ -51,7 +51,7 @@ - {{ __('Buy this course') }} + {{ __("Buy this course") }} @@ -60,23 +60,23 @@ theme="blue" size="lg" > - {{ __('Contact the Administrator to enroll for this course.') }} + {{ __("Contact the Administrator to enroll for this course.") }} - + - {{ __('Edit') }} + {{ __("Edit") }}
-
- {{ __('This course has:') }} +
+ {{ __("This course has:") }}
- - {{ course.data.lessons }} {{ __('Lessons') }} - + {{ course.data.lessons }} {{ __("Lessons") }}
{{ formatAmount(course.data.enrollments) }} - {{ __('Enrolled Students') }} + {{ __("Enrolled Students") }}
- - {{ course.data.rating }} {{ __('Rating') }} - + {{ course.data.rating }} {{ __("Rating") }}
- {{ __('Certificate of Completion') }} + {{ __("Certificate of Completion") }}
- {{ __('Paid Certificate after Evaluation') }} + {{ __("Paid Certificate after Evaluation") }}
@@ -200,154 +193,167 @@ import { Star, TrendingUp, Users, -} from 'lucide-vue-next' -import { computed, inject, ref, onMounted } from 'vue' -import { Badge, Button, call, createResource, toast } from 'frappe-ui' -import { formatAmount } from '@/utils/' -import { capture } from '@/telemetry' -import { useRouter } from 'vue-router' -import CertificationLinks from '@/components/CertificationLinks.vue' -import CourseProgressSummary from '@/components/Modals/CourseProgressSummary.vue' +} from "lucide-vue-next"; +import { computed, inject, ref, onMounted } from "vue"; +import { Badge, Button, call, createResource, toast } from "frappe-ui"; +import { formatAmount } from "@/utils/"; +import { capture } from "@/telemetry"; +import { useRouter } from "vue-router"; +import CertificationLinks from "@/components/CertificationLinks.vue"; +import CourseProgressSummary from "@/components/Modals/CourseProgressSummary.vue"; -const router = useRouter() -const user = inject('$user') -const showProgressModal = ref(false) -const readOnlyMode = window.read_only_mode +const router = useRouter(); +const user = inject("$user"); +const showProgressModal = ref(false); +const readOnlyMode = window.read_only_mode; const props = defineProps({ course: { type: Object, default: null, }, -}) +}); const video_link = computed(() => { if (props.course.data.video_link) { - return 'https://www.youtube.com/embed/' + props.course.data.video_link + return "https://www.youtube.com/embed/" + props.course.data.video_link; } - return null -}) + return null; +}); -const requestPending = ref(false) +const requestPending = ref(false); const showRequestEnrolmentButton = computed(() => { - return (!props.course.data.membership && user.data && user.data.name !== 'Guest' && props.course.data.disable_self_learning) -}) + return ( + !props.course.data.membership && + user.data && + user.data.name !== "Guest" && + props.course.data.disable_self_learning + ); +}); function requestEnrolment() { - if (!user.data || user.data.name === 'Guest') { - window.location.href = `/login?redirect-to=/courses/${encodeURIComponent(props.course.data.name)}` - return - } - call('one_lms.one_lms.doctype.lms_course_enrolment_request.lms_course_enrolment_request.create_lms_course_enrolment_request', { - course: props.course.data.name, - member: user.data.name - }) - .then((data) => { - if (data === 'OK') { - toast.success(__('Enrollment request sent successfully')) - requestPending.value = true - } - }) - .catch((err) => { - toast.warning(__(err.messages?.[0] || err)) - console.error(err) - }) + if (!user.data || user.data.name === "Guest") { + window.location.href = `/login?redirect-to=/courses/${encodeURIComponent( + props.course.data.name + )}`; + return; + } + call( + "one_lms.one_lms.doctype.lms_course_enrolment_request.lms_course_enrolment_request.create_lms_course_enrolment_request", + { + course: props.course.data.name, + member: user.data.name, + } + ) + .then((data) => { + if (data === "OK") { + toast.success(__("Enrollment request sent successfully")); + requestPending.value = true; + } + }) + .catch((err) => { + toast.warning(__(err.messages?.[0] || err)); + console.error(err); + }); } function checkPendingRequest() { - if (!showRequestEnrolmentButton.value) return - call('one_lms.one_lms.doctype.lms_course_enrolment_request.lms_course_enrolment_request.has_pending_request', { - course: props.course.data.name, - member: user.data.name - }) - .then((data) => { - if (data) { - requestPending.value = true - } - }) - .catch((err) => {}) + if (!showRequestEnrolmentButton.value) return; + call( + "one_lms.one_lms.doctype.lms_course_enrolment_request.lms_course_enrolment_request.has_pending_request", + { + course: props.course.data.name, + member: user.data.name, + } + ) + .then((data) => { + if (data) { + requestPending.value = true; + } + }) + .catch((err) => {}); } onMounted(() => { - checkPendingRequest() -}) + checkPendingRequest(); +}); function enrollStudent() { if (!user.data) { - toast.success(__('You need to login first to enroll for this course')) + toast.success(__("You need to login first to enroll for this course")); setTimeout(() => { - window.location.href = `/login?redirect-to=${window.location.pathname}` - }, 500) + window.location.href = `/login?redirect-to=${window.location.pathname}`; + }, 500); } else { - call('lms.lms.doctype.lms_enrollment.lms_enrollment.create_membership', { + call("lms.lms.doctype.lms_enrollment.lms_enrollment.create_membership", { course: props.course.data.name, }) .then(() => { - capture('enrolled_in_course', { + capture("enrolled_in_course", { course: props.course.data.name, - }) - toast.success(__('You have been enrolled in this course')) + }); + toast.success(__("You have been enrolled in this course")); setTimeout(() => { router.push({ - name: 'Lesson', + name: "Lesson", params: { courseName: props.course.data.name, chapterNumber: 1, lessonNumber: 1, }, - }) - }, 1000) + }); + }, 1000); }) .catch((err) => { - toast.warning(__(err.messages?.[0] || err)) - console.error(err) - }) + toast.warning(__(err.messages?.[0] || err)); + console.error(err); + }); } } const is_instructor = () => { - let user_is_instructor = false + let user_is_instructor = false; props.course.data.instructors.forEach((instructor) => { if (!user_is_instructor && instructor.name == user.data?.name) { - user_is_instructor = true + user_is_instructor = true; } - }) - return user_is_instructor -} + }); + return user_is_instructor; +}; const canGetCertificate = computed(() => { if ( props.course.data?.enable_certification && props.course.data?.membership?.progress == 100 ) { - return true + return true; } - return false -}) + return false; +}); const certificate = createResource({ - url: 'lms.lms.doctype.lms_certificate.lms_certificate.create_certificate', + url: "lms.lms.doctype.lms_certificate.lms_certificate.create_certificate", makeParams(values) { return { course: values.course, - } + }; }, onSuccess(data) { window.open( `/api/method/frappe.utils.print_format.download_pdf?doctype=LMS+Certificate&name=${ data.name }&format=${encodeURIComponent(data.template)}`, - '_blank' - ) + "_blank" + ); }, -}) +}); const fetchCertificate = () => { certificate.submit({ course: props.course.data?.name, member: user.data?.name, - }) -} + }); +}; const showProgressSummary = () => { - showProgressModal.value = true -} + showProgressModal.value = true; +}; diff --git a/one_lms/public/overrides/pages/Lesson.vue b/one_lms/public/overrides/pages/Lesson.vue index 6e8916b..4ddf9ad 100644 --- a/one_lms/public/overrides/pages/Lesson.vue +++ b/one_lms/public/overrides/pages/Lesson.vue @@ -16,7 +16,7 @@ - {{ __('Video Statistics') }} + {{ __("Video Statistics") }}
@@ -27,13 +27,13 @@
- {{ __('This lesson is locked') }} + {{ __("This lesson is locked") }}
{{ __( - 'This lesson is not available for preview. Please enroll in the course to access it.' + "This lesson is not available for preview. Please enroll in the course to access it." ) }}
@@ -42,7 +42,7 @@ @click="enrollStudent()" variant="solid" > - {{ __('Start Learning') }} + {{ __("Start Learning") }} - {{ __('Contact the Administrator to enroll for this course.') }} + {{ __("Contact the Administrator to enroll for this course.") }}
@@ -74,9 +74,7 @@ 'w-full md:w-3/5 mx-auto border-none !pt-10': zenModeEnabled, }" > -
+
{{ lesson.data.title }} @@ -95,7 +93,7 @@ class="hidden group-hover:block rounded bg-gray-900 px-2 py-1 text-xs text-white shadow-xl absolute left-0 top-full mt-2" > {{ Math.ceil(lesson.data.membership.progress) }}% - {{ __('completed') }} + {{ __("completed") }}
@@ -111,7 +109,7 @@ - {{ __('Previous') }} + {{ __("Previous") }} @@ -127,20 +125,24 @@ }" > -
@@ -175,91 +177,87 @@ />
- - - - - -
-
-
-
- {{ __('Instructor Notes') }} + + + + + +
+
+
+
+ {{ __("Instructor Notes") }} +
+
-
-
- -
- -
-
-
-
- + v-else-if="lesson.data.instructor_notes" + class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal mt-8" + > + +
+ +
+
+
+
+ +
+
+
-
- -
@@ -270,7 +268,7 @@ v-if="user && lesson.data.membership" class="text-sm mt-4 mb-2 text-ink-gray-5" > - {{ Math.ceil(lessonProgress) }}% {{ __('completed') }} + {{ Math.ceil(lessonProgress) }}% {{ __("completed") }}
-
+
-
+

{{ header}}

@@ -67,14 +67,14 @@

{{ header}}

- +
- +

Address: @@ -90,9 +90,9 @@

Address:

The content of this email is confidential and intended for the recipient - specified in the message only. It is strictly forbidden to share any part of this message - with any third party, without the written consent of the sender. If you received this message by - mistake, please reply to this message and follow with its deletion, so that we can ensure such + specified in the message only. It is strictly forbidden to share any part of this message + with any third party, without the written consent of the sender. If you received this message by + mistake, please reply to this message and follow with its deletion, so that we can ensure such a mistake does not occur in the future.

diff --git a/one_lms/templates/emails/lms_assignment_submission_group_template.html b/one_lms/templates/emails/lms_assignment_submission_group_template.html index 6998913..52fed01 100644 --- a/one_lms/templates/emails/lms_assignment_submission_group_template.html +++ b/one_lms/templates/emails/lms_assignment_submission_group_template.html @@ -1,10 +1,10 @@ {% set date = frappe.format_date(frappe.utils.today()) %} -{% set course = frappe.db.get_value("LMS Course", course, ["title", "name", "image"], as_dict=True) %} +{% set course = frappe.db.get_value("LMS Course", course, ["title", "name", "image"], as_dict=True) %}

{{ _("The following trainees have successfully completed the course {0} on {1}").format(frappe.bold(course_name), date) }}

@@ -23,7 +23,7 @@ {% for member in members %} - {% set user = frappe.db.get_value("User", member, ["full_name", "username"], as_dict=True) %} + {% set user = frappe.db.get_value("User", member, ["full_name", "username"], as_dict=True) %} {{ loop.index }} {{ user.username }} diff --git a/one_lms/templates/emails/lms_course_enrollment.html b/one_lms/templates/emails/lms_course_enrollment.html index 0e433b0..b3a5986 100644 --- a/one_lms/templates/emails/lms_course_enrollment.html +++ b/one_lms/templates/emails/lms_course_enrollment.html @@ -15,7 +15,7 @@ {{ _("Course Name:") }} {{ course_name }}

- + {{ _("Enrollment Date:") }} {{ frappe.utils.format_date(enrollment_date, "medium") }}

diff --git a/one_lms/templates/emails/lms_quiz_submission_group_template.html b/one_lms/templates/emails/lms_quiz_submission_group_template.html index 9c7e072..2210567 100644 --- a/one_lms/templates/emails/lms_quiz_submission_group_template.html +++ b/one_lms/templates/emails/lms_quiz_submission_group_template.html @@ -1,14 +1,14 @@ {% set date = frappe.format_date(frappe.utils.today()) %} -{% set course = frappe.db.get_value("LMS Course", course, ["title", "name", "image"], as_dict=True) %} +{% set course = frappe.db.get_value("LMS Course", course, ["title", "name", "image"], as_dict=True) %}

{{ _("The following trainees have submitted their quiz.") }}

diff --git a/one_lms/utils.py b/one_lms/utils.py index 9285465..b67fb8f 100644 --- a/one_lms/utils.py +++ b/one_lms/utils.py @@ -3,64 +3,57 @@ from frappe.desk.doctype.notification_log.notification_log import make_notification_logs from lms.lms.utils import get_lesson_index, get_lesson_url + def create_notification_log(doc, topic): - course = topic.course if hasattr(topic, 'course') else None - instructors = frappe.db.get_all( - "Course Instructor", {"parent": course}, pluck="instructor" - ) - link = None - if topic.reference_doctype == "LMS Batch": - link = f"/batches/{topic.reference_docname}#discussions" - if topic.reference_doctype == "Course Lesson": - lesson_index = get_lesson_index(topic.reference_docname) - link = get_lesson_url(course, lesson_index) - notification = frappe._dict( - { - "subject": _("New reply on the topic {0}").format(topic.title), - "document_type": topic.reference_doctype, - "document_name": topic.reference_docname, - "for_user": topic.owner, - "from_user": doc.owner, - "link": link, - "type": "Alert", - } - ) - users = [] - if doc.owner != topic.owner: - users.append(topic.owner) + course = topic.course if hasattr(topic, "course") else None + instructors = frappe.db.get_all("Course Instructor", {"parent": course}, pluck="instructor") + link = None + if topic.reference_doctype == "LMS Batch": + link = f"/batches/{topic.reference_docname}#discussions" + if topic.reference_doctype == "Course Lesson": + lesson_index = get_lesson_index(topic.reference_docname) + link = get_lesson_url(course, lesson_index) + notification = frappe._dict( + { + "subject": _("New reply on the topic {0}").format(topic.title), + "document_type": topic.reference_doctype, + "document_name": topic.reference_docname, + "for_user": topic.owner, + "from_user": doc.owner, + "link": link, + "type": "Alert", + } + ) + users = [] + if doc.owner != topic.owner: + users.append(topic.owner) - if doc.owner not in instructors: - users += instructors - make_notification_logs(notification, users) - if not member: - member = frappe.session.user + if doc.owner not in instructors: + users += instructors + make_notification_logs(notification, users) - return frappe.db.get_value( - "LMS Course Progress", - {"course": course, "owner": member, "lesson": lesson}, - ["status"], - ) def get_course_lessons_progress(course): - """ - Fetch the progress of all lessons in the given course for a logged in user. - Args: - course (str): Name of the course. - Returns: - dict: A dictionary mapping lesson names to their progress status ('Complete' or other). - """ - lessons = frappe.get_all("Course Lesson", filters={"course": course}, fields=["name"]) - progress_data = {} - for lesson in lessons: - progress_data[lesson['name']] = get_progress(course, lesson['name']) - return progress_data + """ + Fetch the progress of all lessons in the given course for a logged in user. + Args: + course (str): Name of the course. + Returns: + dict: A dictionary mapping lesson names to their progress status ('Complete' or other). + """ + lessons = frappe.get_all("Course Lesson", filters={"course": course}, fields=["name"]) + progress_data = {} + for lesson in lessons: + progress_data[lesson["name"]] = get_progress(course, lesson["name"]) + return progress_data + def get_progress(course, lesson, member=None): - if not member: - member = frappe.session.user + if not member: + member = frappe.session.user - return frappe.db.get_value( - "LMS Course Progress", - {"course": course, "owner": member, "lesson": lesson}, - ["status"], - ) + return frappe.db.get_value( + "LMS Course Progress", + {"course": course, "owner": member, "lesson": lesson}, + ["status"], + ) diff --git a/one_lms/www/lms.py b/one_lms/www/lms.py index 86f7beb..d45e3f2 100644 --- a/one_lms/www/lms.py +++ b/one_lms/www/lms.py @@ -5,19 +5,19 @@ def get_context(context): - - app_path = frappe.form_dict.get("app_path", "") + app_path = frappe.form_dict.get("app_path", "") - if app_path and '/files/' in app_path: - if 'private/files/' in app_path: - new_path = '/' + app_path[app_path.index('private/files/'):] - elif '/files/' in app_path: - new_path = '/' + app_path[app_path.index('files/'):] - else: - new_path = f'/files/{app_path.split("/")[-1]}' - - frappe.local.flags.redirect_location = new_path - raise frappe.Redirect - - from lms.www.lms import get_context as lms_get_context - return lms_get_context() \ No newline at end of file + if app_path and "/files/" in app_path: + if "private/files/" in app_path: + new_path = "/" + app_path[app_path.index("private/files/") :] + elif "/files/" in app_path: + new_path = "/" + app_path[app_path.index("files/") :] + else: + new_path = f'/files/{app_path.split("/")[-1]}' + + frappe.local.flags.redirect_location = new_path + raise frappe.Redirect + + from lms.www.lms import get_context as lms_get_context + + return lms_get_context()