Compare commits

..

1 Commits

Author SHA1 Message Date
Jannat Patel
f01e634445 docs: grammar correction in readme 2022-12-30 17:06:04 +05:30
149 changed files with 2166 additions and 8076 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

2
.gitignore vendored
View File

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

View File

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

View File

@@ -52,7 +52,7 @@ These are some of the tools it's built on:
You need Docker, docker-compose, and git setup on your machine. Refer to [Docker documentation](https://docs.docker.com/). After that, run the following commands: You need Docker, docker-compose, and git setup on your machine. Refer to [Docker documentation](https://docs.docker.com/). After that, run the following commands:
``` ```
git clone https://github.com/frappe/lms git clone https://github.com/frappe/lms
cd apps/lms/docker cd lms/docker
docker-compose up docker-compose up
``` ```
@@ -71,7 +71,7 @@ Currently, this app depends on the `develop` branch of [frappe](https://github.c
bench --site lms.test install-app lms bench --site lms.test install-app lms
bench --site lms.test add-to-hosts bench --site lms.test add-to-hosts
1. Now, you can access the site at `http://lms.test:8000` 1. Now, you can access the site at `http://gameplan.test:8080`
## Deployment ## Deployment

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -99,7 +99,7 @@
"modified": "2021-09-29 15:27:55.585874", "modified": "2021-09-29 15:27:55.585874",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Exercise", "name": "Exercise",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@@ -120,4 +120,4 @@
"sort_order": "ASC", "sort_order": "ASC",
"title_field": "title", "title_field": "title",
"track_changes": 1 "track_changes": 1
} }

View File

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

View File

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

View File

@@ -30,7 +30,7 @@
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Exercise", "label": "Exercise",
"options": "LMS Exercise", "options": "Exercise",
"search_index": 1 "search_index": 1
}, },
{ {
@@ -163,4 +163,4 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1 "track_changes": 1
} }

View File

@@ -25,7 +25,7 @@
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
"label": "Exercise", "label": "Exercise",
"options": "LMS Exercise" "options": "Exercise"
}, },
{ {
"fetch_from": "exercise.title", "fetch_from": "exercise.title",
@@ -123,4 +123,4 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1 "track_changes": 1
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,6 +20,14 @@ class TestCustomUser(unittest.TestCase):
user = new_user("Username", "test-without-username@example.com") user = new_user("Username", "test-without-username@example.com")
self.assertTrue(user.username) self.assertTrue(user.username)
def test_with_illegal_characters(self):
user = new_user("Username$$", "test_with_illegal_characters@example.com")
self.assertEqual(user.username[:8], "username")
def test_with_underscore_at_end(self):
user = new_user("Username___", "test_with_underscore_at_end@example.com")
self.assertNotEqual(user.username[-1], "_")
def test_with_short_first_name(self): def test_with_short_first_name(self):
user = new_user("USN", "test_with_short_first_name@example.com") user = new_user("USN", "test_with_short_first_name@example.com")
self.assertGreaterEqual(len(user.username), 4) self.assertGreaterEqual(len(user.username), 4)
@@ -29,6 +37,8 @@ class TestCustomUser(unittest.TestCase):
users = [ users = [
"test_with_basic_username@example.com", "test_with_basic_username@example.com",
"test-without-username@example.com", "test-without-username@example.com",
"test_with_illegal_characters@example.com",
"test_with_underscore_at_end@example.com",
"test_with_short_first_name@example.com", "test_with_short_first_name@example.com",
] ]
frappe.db.delete("User", {"name": ["in", users]}) frappe.db.delete("User", {"name": ["in", users]})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

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

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

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

Before

Width:  |  Height:  |  Size: 637 B

View File

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

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,6 +1,5 @@
frappe.ready(() => { frappe.ready(() => {
setup_file_size(); setup_file_size();
pin_header();
$(".join-batch").click((e) => { $(".join-batch").click((e) => {
join_course(e); join_course(e);
@@ -10,6 +9,14 @@ frappe.ready(() => {
notify_user(e); notify_user(e);
}); });
$(".btn-chapter").click((e) => {
add_chapter(e);
});
$(document).on("click", ".btn-save-chapter", (e) => {
save_chapter(e);
});
$(".nav-link").click((e) => { $(".nav-link").click((e) => {
change_hash(e); change_hash(e);
}); });
@@ -24,36 +31,8 @@ frappe.ready(() => {
generate_graph("Lesson Completion", "#lesson-completion"); generate_graph("Lesson Completion", "#lesson-completion");
generate_course_completion_graph(); generate_course_completion_graph();
} }
expand_the_active_chapter();
$(".chapter-title")
.unbind()
.click((e) => {
rotate_chapter_icon(e);
});
$(".no-preview").click((e) => {
show_no_preview_dialog(e);
});
$("#create-class").click((e) => {
open_class_dialog(e);
});
}); });
const pin_header = () => {
const el = document.querySelector(".sticky");
if (el) {
const observer = new IntersectionObserver(
([e]) =>
e.target.classList.toggle("is-pinned", e.intersectionRatio < 1),
{ threshold: [1] }
);
observer.observe(el);
}
};
const setup_file_size = () => { const setup_file_size = () => {
frappe.provide("frappe.form.formatters"); frappe.provide("frappe.form.formatters");
frappe.form.formatters.FileSize = file_size; frappe.form.formatters.FileSize = file_size;
@@ -70,7 +49,7 @@ const file_size = (value) => {
const join_course = (e) => { const join_course = (e) => {
e.preventDefault(); e.preventDefault();
let course = $("#outline-heading").attr("data-course"); let course = $(e.currentTarget).attr("data-course");
if (frappe.session.user == "Guest") { if (frappe.session.user == "Guest") {
window.location.href = `/login?redirect-to=/courses/${course}`; window.location.href = `/login?redirect-to=/courses/${course}`;
return; return;
@@ -104,7 +83,7 @@ const join_course = (e) => {
const notify_user = (e) => { const notify_user = (e) => {
e.preventDefault(); e.preventDefault();
var course = decodeURIComponent($("#outline-heading").attr("data-course")); var course = decodeURIComponent($(e.currentTarget).attr("data-course"));
if (frappe.session.user == "Guest") { if (frappe.session.user == "Guest") {
window.location.href = `/login?redirect-to=/courses/${course}`; window.location.href = `/login?redirect-to=/courses/${course}`;
return; return;
@@ -133,6 +112,65 @@ const notify_user = (e) => {
}); });
}; };
const add_chapter = (e) => {
if ($(".new-chapter").length) {
scroll_to_chapter_container();
return;
}
let next_index = $("[data-index]").last().data("index") + 1 || 1;
let add_after = $(`.chapter-parent:last`).length
? $(`.chapter-parent:last`)
: $("#outline-heading");
$(`<div class="chapter-parent chapter-edit new-chapter">
<div contenteditable="true" data-placeholder="${__(
"Chapter Name"
)}" class="chapter-title-main"></div>
<div class="chapter-description small my-2" contenteditable="true"
data-placeholder="${__("Short Description")}"></div>
<button class="btn btn-sm btn-secondary d-block btn-save-chapter"
data-index="${next_index}"> ${__("Save")} </button>
</div>`).insertAfter(add_after);
scroll_to_chapter_container();
};
const scroll_to_chapter_container = () => {
$([document.documentElement, document.body]).animate(
{
scrollTop: $(".new-chapter").offset().top,
},
1000
);
$(".new-chapter").find(".chapter-title-main").focus();
};
const save_chapter = (e) => {
let target = $(e.currentTarget);
let parent = target.closest(".chapter-parent");
frappe.call({
method: "lms.lms.doctype.lms_course.lms_course.save_chapter",
args: {
course: $("#title").data("course"),
title: parent.find(".chapter-title-main").text(),
chapter_description: parent.find(".chapter-description").text(),
idx: target.data("index"),
chapter: target.data("chapter") ? target.data("chapter") : "",
},
callback: (data) => {
frappe.show_alert({
message: __("Saved"),
indicator: "green",
});
setTimeout(() => {
window.location.reload();
}, 1000);
},
});
};
const generate_graph = (chart_name, element, type = "line") => { const generate_graph = (chart_name, element, type = "line") => {
let date = frappe.datetime; let date = frappe.datetime;
@@ -188,164 +226,3 @@ const change_hash = (e) => {
const open_tab = () => { const open_tab = () => {
$(`a[href="${window.location.hash}"]`).click(); $(`a[href="${window.location.hash}"]`).click();
}; };
const expand_the_first_chapter = () => {
let elements = $(".course-home-outline .collapse");
elements.each((i, element) => {
if (i < 1) {
show_section(element);
return false;
}
});
};
const expand_the_active_chapter = () => {
/* Find anchor matching the URL for course details page */
let selector = $(
`a[href="${decodeURIComponent(window.location.pathname)}"]`
).parent();
if (!selector.length) {
selector = $(
`a[href^="${decodeURIComponent(window.location.pathname)}"]`
).parent();
}
if (selector.length && $(".course-details-page").length) {
expand_for_course_details(selector);
} else if ($(".active-lesson").length) {
/* For course home page */
selector = $(".active-lesson");
show_section(selector.parent().parent());
} else {
/* If no active chapter then exapand the first chapter */
expand_the_first_chapter();
}
};
const expand_for_course_details = (selector) => {
$(".lesson-info").removeClass("active-lesson");
$(".lesson-info").each((i, elem) => {
let href = $(elem).find("use").attr("href");
href.endsWith("blue") &&
$(elem)
.find("use")
.attr("href", href.substring(0, href.length - 5));
});
selector.addClass("active-lesson");
show_section(selector.parent().parent());
};
const show_section = (element) => {
$(element).addClass("show");
$(element)
.siblings(".chapter-title")
.children(".chapter-icon")
.css("transform", "rotate(90deg)");
$(element).siblings(".chapter-title").attr("aria-expanded", true);
};
const rotate_chapter_icon = (e) => {
let icon = $(e.currentTarget).children(".chapter-icon");
if (icon.css("transform") == "none") {
icon.css("transform", "rotate(90deg)");
} else {
icon.css("transform", "none");
}
};
const show_no_preview_dialog = (e) => {
$("#no-preview-modal").modal("show");
};
const open_class_dialog = (e) => {
this.class_dialog = new frappe.ui.Dialog({
title: __("New Class"),
fields: [
{
fieldtype: "Data",
label: __("Title"),
fieldname: "title",
reqd: 1,
default: class_info && class_info.title,
},
{
fieldtype: "Date",
label: __("Start Date"),
fieldname: "start_date",
reqd: 1,
default: class_info && class_info.start_date,
},
{
fieldtype: "Date",
label: __("End Date"),
fieldname: "end_date",
reqd: 1,
default: class_info && class_info.end_date,
},
{
fieldtype: "Column Break",
},
{
fieldtype: "Int",
label: __("Seat Count"),
fieldname: "seat_count",
default: class_info && class_info.seat_count,
},
{
fieldtype: "Time",
label: __("Start Time"),
fieldname: "start_time",
default: class_info && class_info.start_time,
},
{
fieldtype: "Time",
label: __("End Time"),
fieldname: "end_time",
default: class_info && class_info.end_time,
},
{
fieldtype: "Section Break",
},
{
fieldtype: "Small Text",
label: __("Description"),
fieldname: "description",
default: class_info && class_info.description,
},
],
primary_action_label: __("Save"),
primary_action: (values) => {
create_class(values);
},
});
this.class_dialog.show();
};
const create_class = (values) => {
frappe.call({
method: "lms.lms.doctype.lms_class.lms_class.create_class",
args: {
title: values.title,
start_date: values.start_date,
end_date: values.end_date,
description: values.description,
seat_count: values.seat_count,
start_time: values.start_time,
end_time: values.end_time,
name: class_info && class_info.name,
},
callback: (r) => {
if (r.message) {
frappe.show_alert({
message: class_info
? __("Class Updated")
: __("Class Created"),
indicator: "green",
});
this.class_dialog.hide();
window.location.href = `/classes/${r.message.name}`;
}
},
});
};

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