Compare commits

..

12 Commits

Author SHA1 Message Date
Jannat Patel
f38b1226df Merge pull request #321 from pateljannat/version-13-lms 2022-04-04 12:01:18 +05:30
Jannat Patel
1f3513db8b fix: get_palette function call for avatar 2022-04-04 09:47:27 +05:30
Jannat Patel
3eb0f13fb0 fix: moved Avatar widget to lms module 2022-04-04 09:32:25 +05:30
Jannat Patel
1277133ec6 refactor: removed unused modules from modules.txt 2022-04-04 09:24:03 +05:30
Jannat Patel
7337aea0dc refactor: renamed school references to lms 2022-04-04 08:23:07 +05:30
Jannat Patel
32b601cf34 refactor: renamed school to lms 2022-04-01 20:11:07 +05:30
Jannat Patel
d4dc901925 fix: rectified css path in hooks 2021-12-06 17:47:55 +05:30
Jannat Patel
64e581533b fix: included build files through hooks 2021-12-06 17:36:46 +05:30
Jannat Patel
0873d704d2 fix: reverting to old build system 2021-12-06 16:33:23 +05:30
Jannat Patel
8ee57f0254 fix: remove page_renderer and references 2021-12-06 13:10:42 +05:30
Jannat Patel
7c5021132d fix: remove page renderer for profile page 2021-12-06 10:51:50 +05:30
Jannat Patel
740c0d10ca get profile url jinja function issue 2021-12-03 16:29:29 +05:30
614 changed files with 13983 additions and 40488 deletions

View File

@@ -9,7 +9,7 @@ root = true
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_style = tab
indent_style = space
trim_trailing_whitespace = true
# Python
@@ -26,4 +26,4 @@ indent_style = tab
# HTML, CSS, javascript, JSON and YAML
[*.{html,css,js,json,yml,yaml}]
indent_size = 4
indent_size = 2

37
.flake8
View File

@@ -1,37 +0,0 @@
[flake8]
ignore =
E121,
E126,
E127,
E128,
E203,
E225,
E226,
E231,
E241,
E251,
E261,
E265,
E302,
E303,
E305,
E402,
E501,
E741,
W291,
W292,
W293,
W391,
W503,
W504,
F403,
B007,
B950,
W191,
E124, # closing bracket, irritating while writing QB code
E131, # continuation line unaligned for hanging indent
E123, # closing bracket does not match indentation of opening bracket's line
E101, # ensured by use of black
max-line-length = 200
exclude=.github/helper/semgrep_rules

View File

@@ -1,74 +0,0 @@
[flake8]
ignore =
B001,
B007,
B009,
B010,
B950,
E101,
E111,
E114,
E116,
E117,
E121,
E122,
E123,
E124,
E125,
E126,
E127,
E128,
E131,
E201,
E202,
E203,
E211,
E221,
E222,
E223,
E224,
E225,
E226,
E228,
E231,
E241,
E242,
E251,
E261,
E262,
E265,
E266,
E271,
E272,
E273,
E274,
E301,
E302,
E303,
E305,
E306,
E402,
E501,
E502,
E701,
E702,
E703,
E741,
F401,
F403,
F405,
W191,
W291,
W292,
W293,
W391,
W503,
W504,
E711,
E129,
F841,
E713,
E712,
max-line-length = 200

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

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

Before

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -1,4 +1,4 @@
name: Server Tests
name: Run tests
on:
push:
branches:
@@ -32,11 +32,11 @@ jobs:
- name: setup python
uses: actions/setup-python@v2
with:
python-version: '3.10'
python-version: '3.9'
- name: setup node
uses: actions/setup-node@v2
with:
node-version: '18'
node-version: '14'
check-latest: true
- name: setup cache for bench
uses: actions/cache@v2
@@ -65,7 +65,7 @@ jobs:
run: bench new-site --mariadb-root-password root --admin-password admin frappe.local
- name: install lms app
working-directory: /home/runner/frappe-bench
run: bench --site frappe.local install-app lms
run: bench --verbose --site frappe.local install-app lms
- name: setup requirements
working-directory: /home/runner/frappe-bench
run: bench setup requirements --dev
@@ -78,3 +78,4 @@ jobs:
- name: run tests
working-directory: /home/runner/frappe-bench
run: bench --site frappe.local run-tests --app lms

View File

@@ -1,33 +0,0 @@
name: Linters
on:
pull_request:
workflow_dispatch:
push:
branches: [ main ]
jobs:
linters:
name: Semantic Commits
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install and Run Pre-commit
uses: pre-commit/action@v2.0.3
- name: Download Semgrep rules
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules
- name: Download semgrep
run: pip install semgrep
- name: Run Semgrep rules
run: semgrep ci --config ./frappe-semgrep-rules/rules

View File

@@ -1,19 +0,0 @@
name: Semantic Pull Request
on:
push:
branches: [ main ]
pull_request: {}
jobs:
# This workflow contains a single job called "build"
semantic:
name: Validate PR title
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
- uses: zeke/semantic-pull-requests@main

View File

@@ -1,121 +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: 18
check-latest: true
- name: Add to Hosts
run: |
echo "127.0.0.1 lms.test" | sudo tee -a /etc/hosts
- name: Cache pip
uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }}
restore-keys: |
${{ runner.os }}-pip-
${{ runner.os }}-
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-ui-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-ui-
- name: Cache cypress binary
uses: actions/cache@v3
with:
path: ~/.cache/Cypress
key: ${{ runner.os }}-cypress
- name: Install Dependencies
run: |
bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh
bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
env:
BEFORE: ${{ env.GITHUB_EVENT_PATH.before }}
AFTER: ${{ env.GITHUB_EVENT_PATH.after }}
TYPE: ui
DB: mariadb
- name: Site Setup
run: |
cd ~/frappe-bench/
bench --site lms.test execute frappe.utils.install.complete_setup_wizard
bench --site lms.test execute frappe.tests.ui_test_helpers.create_test_user
- name: cypress pre-requisites
run: |
cd ~/frappe-bench/apps/lms
yarn add cypress@^10 --no-lockfile
- name: UI Tests
run: cd ~/frappe-bench/ && bench --site lms.test run-ui-tests lms --headless
env:
CYPRESS_BASE_URL: http://lms.test:8000
CYPRESS_RECORD_KEY: 095366ec-7b9f-41bd-aeec-03bb76d627fe
- name: Stop server and wait for coverage file
run: |
ps -ef | grep "[f]rappe serve" | awk '{print $2}' | xargs kill -s SIGINT
sleep 5
- name: Show bench output
if: ${{ always() }}
run: cat ~/frappe-bench/bench_start.log || true

2
.gitignore vendored
View File

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

View File

@@ -1,57 +0,0 @@
exclude: 'node_modules|.git'
default_stages: [commit]
fail_fast: false
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
hooks:
- id: trailing-whitespace
files: "lms.*"
exclude: ".*json$|.*txt$|.*csv|.*md|.*svg"
- id: check-yaml
- id: check-merge-conflict
- id: check-ast
- id: check-json
- id: check-toml
- id: debug-statements
- repo: https://github.com/asottile/pyupgrade
rev: v2.34.0
hooks:
- id: pyupgrade
args: ['--py310-plus']
- repo: https://github.com/adityahase/black
rev: 9cb0a69f4d0030cdf687eddf314468b39ed54119
hooks:
- id: black
additional_dependencies: ['click==8.0.4']
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.7.1
hooks:
- id: prettier
types_or: [javascript]
# Ignore any files that might contain jinja / bundles
exclude: |
(?x)^(
lms/public/dist/.*|
.*node_modules.*|
.*boilerplate.*|
lms/www/website_script.js|
lms/templates/includes/.*|
lms/public/js/lib/.*
)$
- repo: https://github.com/PyCQA/flake8
rev: 5.0.4
hooks:
- id: flake8
additional_dependencies: ['flake8-bugbear',]
args: ['--config', '.github/helper/flake8.conf']
ci:
autoupdate_schedule: weekly
skip: []
submodules: false

110
README.md
View File

@@ -1,109 +1,25 @@
<p align="center">
<a href="https://www.frappelms.com/">
<img src="https://frappelms.com/files/lms-logo-medium.png" alt="Frappe LMS" width="120px" height="25px">
</a>
<p align="center">Easy to use, open source, learning management system.</p>
</p>
## LMS
Create online courses without much hassle.
&nbsp;
<p align="center">
<a href="https://www.producthunt.com/posts/frappe-lms?utm_source=badge-top-post-topic-badge&utm_medium=badge&utm_souce=badge-frappe&#0045;lms" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-topic-badge.svg?post_id=396079&theme=dark&period=weekly&topic_id=204" alt="Frappe&#0032;LMS - Easy&#0032;to&#0032;use&#0044;&#0032;100&#0037;&#0032;open&#0032;source&#0032;learning&#0032;management&#0032;system | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
</p>
<div align="center" style="max-height: 40px;">
<a href="https://frappecloud.com/lms/signup">
<img src=".github/try-on-f-cloud.svg" height="40">
</a>
</div>
&nbsp;
<p align="center">
<a href="https://dashboard.cypress.io/projects/vandxn/runs">
<img alt="cypress" src="https://img.shields.io/endpoint?url=https://dashboard.cypress.io/badge/simple/vandxn/main&style=flat&logo=cypress">
</a>
<a href="https://github.com/frappe/lms/blob/main/LICENSE">
<img alt="license" src="https://img.shields.io/badge/license-AGPLv3-blue">
</a>
</p>
<img width="1402" alt="Lesson" src="https://frappelms.com/files/banner.png">
<details>
<summary>Show more screenshots</summary>
<img width="1520" alt="ss1" src="https://user-images.githubusercontent.com/31363128/210056046-584bc8aa-d28c-4514-b031-73817012837d.png">
<img width="830" alt="ss2" src="https://user-images.githubusercontent.com/31363128/210056097-36849182-6db0-43a2-8c62-5333cd2aedf4.png">
<img width="941" alt="ss3" src="https://user-images.githubusercontent.com/31363128/210056134-01a7c429-1ef4-434e-9d43-128dda35d7e5.png">
</details>
Frappe LMS is an easy-to-use, open-source learning management system. You can use it to create and share online courses. The app has a clear UI that helps students focus only on what's important and assists in distraction-free learning.
You can create courses and lessons through simple forms. Lessons can be in the form of text, videos, quizzes or a combination of all these. You can keep your students engaged with quizzes to help revise and test the concepts learned. Course Instructors and Students can reach out to each other through the discussions section available for each lesson and get queries resolved.
![Course Home](/lms/public/images/course-home.png)
## Features
- Create online courses. 📚
- Add detailed descriptions and preview videos to the course. 🎬
- Add videos, quizzes, and assignments to your lessons and make them interesting and interactive 📝
- Discussions section below each lesson where instructors and students can interact with each other. 💬
- Create batches to group your students based on courses and track their progress 🏛
- Statistics dashboard that provides all important numbers at a glimpse. 📈
- Job Board where users can post and look for jobs. 💼
- People directory with each person's profile page 👨‍👩‍👧‍👦
- Set cover image, profile photo, short bio, and other professional information. 🦹🏼‍♀️
- Simple layout that optimizes readability 🤓
- Delightful user experience in overall usage ✨
## Tech Stack
1. Simple Backend Forms.
1. The UI is clean and minimal.
1. Lessons can be in the form of texts, videos, quizzes or a combination of all of these.
Frappe LMS is built on [Frappe Framework](https://frappeframework.com) which is a batteries-included python web framework.
These are some of the tools it's built on:
- [Python](https://www.python.org)
- [Redis](https://redis.io/)
- [MariaDB](https://mariadb.org/)
- [Socket.io](https://socket.io/)
## Development Setup
## Local Setup
### Docker
You need Docker, docker-compose, and git setup on your machine. Refer to [Docker documentation](https://docs.docker.com/). After that, run the following commands:
```
git clone https://github.com/frappe/lms
cd apps/lms/docker
docker-compose up
```
Wait for some time until the setup script creates a site. After that, you can access `http://localhost:8000` in your browser and the app's login screen should show up.
### Frappe Bench
Currently, this app depends on the `develop` branch of [frappe](https://github.com/frappe/frappe).
1. Setup frappe-bench by following [this guide](https://frappeframework.com/docs/v14/user/en/installation)
1. In the frappe-bench directory, run `bench start` and keep it running. Open a new terminal session and cd into the `frappe-bench` directory.
1. Run the following commands:
```sh
bench new-site lms.test
bench get-app lms
bench --site lms.test install-app lms
bench --site lms.test add-to-hosts
1. Now, you can access the site at `http://lms.test:8000`
1. [Through Docker](docker-installation.md)
1. [Direct install through bench](bench-installation.md)
## Deployment
Frappe LMS is an app built on top of the Frappe Framework. So, you can follow any deployment guide for hosting a Frappe Framework-based site.
### Contributing
### Managed Hosting
Frappe LMS can be deployed in a few clicks on [Frappe Cloud](https://frappecloud.com/marketplace/apps/lms).
### Self-hosting
If you want to self-host, you can follow official [Frappe Bench Installation](https://github.com/frappe/bench#installation) instructions.
## Bugs and Feature Requests
If you find any bugs or have a feature idea for the app, feel free to report them here on [GitHub Issues](https://github.com/frappe/lms/issues). Make sure you share enough information (app screenshots, browser console screenshots, stack traces, etc) for project maintainers.
1. [Contribution Guidelines](Contribution.md)
## License
Distributed under [GNU AFFERO GENERAL PUBLIC LICENSE](license.txt)
[GNU AFFERO GENERAL PUBLIC LICENSE](license.txt)

View File

@@ -3,7 +3,7 @@ To setup the repository locally follow the steps mentioned below:
1. Install bench and setup a frappe-bench directory by following the [Installation Steps](https://frappeframework.com/docs/user/en/installation).
1. Start the server by running bench start.
1. In a separate terminal window, create a new site by running bench new-site lms.test.
1. Fork the LMS app
1. Fork the lms app
1. Run bench get-app <url-of-your-form>.
1. Run bench --site lms.test install-app lms.
1. Map your site to localhost with the command ```bench --site lms.test add-to-hosts```

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://dd1:8000",
},
});

View File

@@ -1,133 +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.wait(500);
cy.get("#chapter-title").type("Test Chapter");
cy.get("#chapter-description").type("Test Chapter Description");
cy.button("Save").click();
// Add Lesson
cy.wait(1000);
cy.link("Add Lesson").click();
cy.wait(1000);
cy.get("#lesson-title").type("Test Lesson");
// Content
cy.get(".collapse-section.collapsed:first").click();
cy.get("#lesson-content .ce-block")
.click()
.type(
"This is an extremely big paragraph that is meant to test the UI. This is a very long paragraph. It contains more than once sentence. Its meant to be this long as this is a UI test. Its unbearably long and I'm not sure why I'm typing this much. I'm just going to keep typing until I feel like its long enough. I think its long enough now. I'm going to stop typing now. {enter}"
);
cy.get("#lesson-content .ce-toolbar__plus").click();
cy.get('#lesson-content [data-item-name="youtube"]').click();
cy.get('input[data-fieldname="youtube"]').type("GoDtyItReto");
cy.button("Insert").click();
cy.wait(1000);
cy.button("Save").click();
// View Course
cy.wait(1000);
cy.visit("/courses");
cy.get(".course-card-title:first").contains("Test Course");
cy.get(".course-card:first").click();
cy.url().should("include", "/courses/test-course");
cy.get("#title").contains("Test Course");
cy.get(".preview-video").should(
"have.attr",
"src",
"https://www.youtube.com/embed/-LPmw2Znl2c"
);
cy.get("#intro").contains("Test Course Short Introduction");
// View Chapter
cy.get(".chapter-title-main:first").contains("Test Chapter");
cy.get(".chapter-description:first").contains(
"Test Chapter Description"
);
cy.get(".lesson-info:first").contains("Test Lesson");
cy.get(".lesson-info:first").click();
// View Lesson
cy.wait(1000);
cy.url().should("include", "learn/1.1");
cy.get("#title").contains("Test Lesson");
cy.get(".lesson-video iframe").should(
"have.attr",
"src",
"https://www.youtube.com/embed/GoDtyItReto"
);
cy.get(".lesson-content-card").contains(
"This is an extremely big paragraph that is meant to test the UI. This is a very long paragraph. It contains more than once sentence. Its meant to be this long as this is a UI test. Its unbearably long and I'm not sure why I'm typing this much. I'm just going to keep typing until I feel like its long enough. I think its long enough now. I'm going to stop typing now."
);
// Add Discussion
cy.get(".reply").click();
cy.wait(500);
cy.get(".discussion-modal").should("be.visible");
// Enter title
cy.get(".modal .topic-title")
.type("Discussion from tests")
.should("have.value", "Discussion from tests");
// Enter comment
cy.get(".modal .discussions-comment").type(
"This is a discussion from the cypress ui tests."
);
// Submit
cy.get(".modal .submit-discussion").click();
cy.wait(2000);
// Check if discussion is added to page and content is visible
cy.get(".sidebar-parent:first .discussion-topic-title").should(
"have.text",
"Discussion from tests"
);
cy.get(".sidebar-parent:first .discussion-topic-title").click();
cy.get(".discussion-on-page:visible").should("have.class", "show");
cy.get(
".discussion-on-page:visible .reply-card .reply-text .ql-editor p"
).should(
"have.text",
"This is a discussion from the cypress ui tests."
);
cy.get(".discussion-form:visible .discussions-comment").type(
"This is a discussion from the cypress ui tests. \n\nThis comment was entered through the commentbox on the page."
);
cy.get(".discussion-form:visible .submit-discussion").click();
cy.wait(3000);
cy.get(".discussion-on-page:visible").should("have.class", "show");
cy.get(".discussion-on-page:visible")
.children(".reply-card")
.eq(1)
.find(".reply-text")
.should(
"have.text",
"This is a discussion from the cypress ui tests. This comment was entered through the commentbox on the page.\n"
);
});
});

View File

@@ -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')

25
docker-compose.yml Normal file
View File

@@ -0,0 +1,25 @@
version: "3"
services:
mariadb:
image: mariadb
volumes:
- mariadb-storage:/var/lib/mysql
environment:
- MYSQL_ROOT_PASSWORD=root
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
bench:
image: anandology/frappe-bench:2021.10
volumes:
- .:/opt/frappe-bench/apps/lms
environment:
- FRAPPE_APPS=lms
- FRAPPE_ALLOW_TESTS=true
- FRAPPE_SITE_NAME=frappe.localhost
depends_on:
- mariadb
ports:
- 8000:8000
- 9000:9000
volumes:
mariadb-storage: {}

View File

@@ -1,32 +0,0 @@
version: "3.7"
name: lms
services:
mariadb:
image: mariadb:10.6
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --skip-character-set-client-handshake
- --skip-innodb-read-only-compressed # Temporary fix for MariaDB 10.6
environment:
MYSQL_ROOT_PASSWORD: 123
volumes:
- mariadb-data:/var/lib/mysql
redis:
image: redis:alpine
frappe:
image: frappe/bench:latest
command: bash /workspace/init.sh
environment:
- SHELL=/bin/bash
working_dir: /home/frappe
volumes:
- .:/workspace
ports:
- 8000:8000
- 9000:9000
volumes:
mariadb-data:

View File

@@ -1,41 +0,0 @@
#!bin/bash
if [ -d "/home/frappe/frappe-bench/apps/frappe" ]; then
echo "Bench already exists, skipping init"
cd frappe-bench
bench start
else
echo "Creating new bench..."
fi
export PATH="${NVM_DIR}/versions/node/v${NODE_VERSION_DEVELOP}/bin/:${PATH}"
bench init --skip-redis-config-generation frappe-bench
cd frappe-bench
# Use containers instead of localhost
bench set-mariadb-host mariadb
bench set-redis-cache-host redis:6379
bench set-redis-queue-host redis:6379
bench set-redis-socketio-host redis:6379
# Remove redis, watch from Procfile
sed -i '/redis/d' ./Procfile
sed -i '/watch/d' ./Procfile
bench get-app lms
bench new-site lms.localhost \
--force \
--mariadb-root-password 123 \
--admin-password admin \
--no-mariadb-socket
bench --site lms.localhost install-app lms
bench --site lms.localhost set-config developer_mode 1
bench --site lms.localhost clear-cache
bench --site lms.localhost set-config mute_emails 1
bench use lms.localhost
bench start

View File

@@ -1 +1,7 @@
__version__ = "1.0.0"
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
__version__ = '0.0.1'
# load the methods from the lms api
from .lms import api # noqa

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from frappe import _
def get_data():
return [
{
@@ -8,6 +9,6 @@ def get_data():
"color": "grey",
"icon": "octicon octicon-file-directory",
"type": "module",
"label": _("Community"),
"label": _("Community")
}
]

View File

@@ -7,6 +7,5 @@ Configuration for docs
# headline = "App that does everything"
# sub_heading = "Yes, you got that right the first time, everything"
def get_context(context):
context.brand_html = "Community"

File diff suppressed because it is too large Load Diff

View File

@@ -1,114 +0,0 @@
[
{
"docstatus": 0,
"doctype": "Function",
"function": "Consulting",
"modified": "2021-12-14 15:16:26.189604",
"name": "Consulting"
},
{
"docstatus": 0,
"doctype": "Function",
"function": "Data & Analytics",
"modified": "2021-12-14 15:16:26.341051",
"name": "Data & Analytics"
},
{
"docstatus": 0,
"doctype": "Function",
"function": "Design & Creative",
"modified": "2021-12-14 15:16:26.343820",
"name": "Design & Creative"
},
{
"docstatus": 0,
"doctype": "Function",
"function": "Engineering (Non Software)",
"modified": "2021-12-14 15:16:26.346329",
"name": "Engineering (Non Software)"
},
{
"docstatus": 0,
"doctype": "Function",
"function": "Finance, Investment & Accounting",
"modified": "2021-12-14 15:16:26.351518",
"name": "Finance, Investment & Accounting"
},
{
"docstatus": 0,
"doctype": "Function",
"function": "Human Resource & Recruiting",
"modified": "2021-12-14 15:16:26.354310",
"name": "Human Resource & Recruiting"
},
{
"docstatus": 0,
"doctype": "Function",
"function": "Legal",
"modified": "2021-12-14 15:16:26.356844",
"name": "Legal"
},
{
"docstatus": 0,
"doctype": "Function",
"function": "Marketing, Advertising & PR",
"modified": "2021-12-14 15:16:26.359511",
"name": "Marketing, Advertising & PR"
},
{
"docstatus": 0,
"doctype": "Function",
"function": "Operations & Admin",
"modified": "2021-12-14 15:16:26.362571",
"name": "Operations & Admin"
},
{
"docstatus": 0,
"doctype": "Function",
"function": "Project Management",
"modified": "2021-12-14 15:16:26.365033",
"name": "Project Management"
},
{
"docstatus": 0,
"doctype": "Function",
"function": "Product",
"modified": "2021-12-14 15:16:26.367804",
"name": "Product"
},
{
"docstatus": 0,
"doctype": "Function",
"function": "Research, Training & Education",
"modified": "2021-12-14 15:16:26.370085",
"name": "Research, Training & Education"
},
{
"docstatus": 0,
"doctype": "Function",
"function": "Sales & Customer Service",
"modified": "2021-12-14 15:16:26.372635",
"name": "Sales & Customer Service"
},
{
"docstatus": 0,
"doctype": "Function",
"function": "Supply Chain, Logistics Strategy & Management",
"modified": "2021-12-14 15:16:26.375091",
"name": "Supply Chain, Logistics Strategy & Management"
},
{
"docstatus": 0,
"doctype": "Function",
"function": "Other",
"modified": "2021-12-14 15:16:26.377761",
"name": "Other"
},
{
"docstatus": 0,
"doctype": "Function",
"function": "Engineering (Software & IT)",
"modified": "2021-12-14 15:16:26.348864",
"name": "Engineering (Software & IT)"
}
]

View File

@@ -1,170 +0,0 @@
[
{
"docstatus": 0,
"doctype": "Industry",
"industry": "Accounting",
"modified": "2021-12-14 15:21:18.044741",
"name": "Accounting"
},
{
"docstatus": 0,
"doctype": "Industry",
"industry": "Ads, Marketing, PR & Events",
"modified": "2021-12-14 15:21:18.069794",
"name": "Ads, Marketing, PR & Events"
},
{
"docstatus": 0,
"doctype": "Industry",
"industry": "Agriculture, Fishing & Forestry",
"modified": "2021-12-14 15:21:18.074148",
"name": "Agriculture, Fishing & Forestry"
},
{
"docstatus": 0,
"doctype": "Industry",
"industry": "Architecture",
"modified": "2021-12-14 15:21:18.078250",
"name": "Architecture"
},
{
"docstatus": 0,
"doctype": "Industry",
"industry": "Aviation & Aerospace",
"modified": "2021-12-14 15:21:18.082127",
"name": "Aviation & Aerospace"
},
{
"docstatus": 0,
"doctype": "Industry",
"industry": "Banking, Financial Services & Insurance",
"modified": "2021-12-14 15:21:18.087499",
"name": "Banking, Financial Services & Insurance"
},
{
"docstatus": 0,
"doctype": "Industry",
"industry": "Biotech & Pharmaceuticals",
"modified": "2021-12-14 15:21:18.092094",
"name": "Biotech & Pharmaceuticals"
},
{
"docstatus": 0,
"doctype": "Industry",
"industry": "Construction & Real Estate",
"modified": "2021-12-14 15:21:18.095728",
"name": "Construction & Real Estate"
},
{
"docstatus": 0,
"doctype": "Industry",
"industry": "Consulting & Professional Services",
"modified": "2021-12-14 15:21:18.099171",
"name": "Consulting & Professional Services"
},
{
"docstatus": 0,
"doctype": "Industry",
"industry": "Energy & Utilities",
"modified": "2021-12-14 15:21:18.114022",
"name": "Energy & Utilities"
},
{
"docstatus": 0,
"doctype": "Industry",
"industry": "Food & Beverages",
"modified": "2021-12-14 15:21:18.124655",
"name": "Food & Beverages"
},
{
"docstatus": 0,
"doctype": "Industry",
"industry": "Health & Medical",
"modified": "2021-12-14 15:21:18.127914",
"name": "Health & Medical"
},
{
"docstatus": 0,
"doctype": "Industry",
"industry": "Hospitality & Tourism",
"modified": "2021-12-14 15:21:18.130783",
"name": "Hospitality & Tourism"
},
{
"docstatus": 0,
"doctype": "Industry",
"industry": "IT / Ecommerce / Internet",
"modified": "2021-12-14 15:21:18.134069",
"name": "IT / Ecommerce / Internet"
},
{
"docstatus": 0,
"doctype": "Industry",
"industry": "Manufacturing & Production",
"modified": "2021-12-14 15:21:18.137362",
"name": "Manufacturing & Production"
},
{
"docstatus": 0,
"doctype": "Industry",
"industry": "Media & Entertainment",
"modified": "2021-12-14 15:21:18.140180",
"name": "Media & Entertainment"
},
{
"docstatus": 0,
"doctype": "Industry",
"industry": "Public Service & NGOs",
"modified": "2021-12-14 15:21:18.143125",
"name": "Public Service & NGOs"
},
{
"docstatus": 0,
"doctype": "Industry",
"industry": "Retail, Fashion & FMCG",
"modified": "2021-12-14 15:21:18.146020",
"name": "Retail, Fashion & FMCG"
},
{
"docstatus": 0,
"doctype": "Industry",
"industry": "Staffing & Recruiting",
"modified": "2021-12-14 15:21:18.149084",
"name": "Staffing & Recruiting"
},
{
"docstatus": 0,
"doctype": "Industry",
"industry": "Transportation & Logistics",
"modified": "2021-12-14 15:21:18.151952",
"name": "Transportation & Logistics"
},
{
"docstatus": 0,
"doctype": "Industry",
"industry": "Security & Law Enforcement",
"modified": "2021-12-14 15:21:18.154929",
"name": "Security & Law Enforcement"
},
{
"docstatus": 0,
"doctype": "Industry",
"industry": "Other",
"modified": "2021-12-14 15:21:18.157766",
"name": "Other"
},
{
"docstatus": 0,
"doctype": "Industry",
"industry": "Education & Training",
"modified": "2021-12-14 15:21:18.105864",
"name": "Education & Training"
},
{
"docstatus": 0,
"doctype": "Industry",
"industry": "Engineering",
"modified": "2021-12-14 15:21:18.119149",
"name": "Engineering"
}
]

View File

@@ -1,9 +1,11 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from . import __version__ as app_version
app_name = "frappe_lms"
app_title = "Frappe LMS"
app_name = "lms"
app_title = "LMS"
app_publisher = "Frappe"
app_description = "Frappe LMS App"
app_description = "LMS"
app_icon = "octicon octicon-file-directory"
app_color = "grey"
app_email = "school@frappe.io"
@@ -17,9 +19,9 @@ app_license = "AGPL"
# app_include_js = "/assets/lms/js/lms.js"
# include js, css files in header of web template
web_include_css = "lms.bundle.css"
web_include_css = "/assets/css/lms.css"
# web_include_css = "/assets/lms/css/lms.css"
web_include_js = ["website.bundle.js"]
#web_include_js = "website.bundle.js"
# include custom scss in every website theme (without file extension ".scss")
# website_theme_scss = "lms/public/scss/website"
@@ -45,7 +47,7 @@ web_include_js = ["website.bundle.js"]
# website user home page (by Role)
# role_home_page = {
# "Role": "home_page"
# "Role": "home_page"
# }
# Generators
@@ -58,12 +60,7 @@ web_include_js = ["website.bundle.js"]
# ------------
# before_install = "lms.install.before_install"
after_install = "lms.install.after_install"
after_sync = "lms.install.after_sync"
before_uninstall = "lms.install.before_uninstall"
setup_wizard_requires = "assets/lms/js/setup_wizard.js"
# after_install = "lms.install.after_install"
# Desk Notifications
# ------------------
@@ -89,7 +86,7 @@ setup_wizard_requires = "assets/lms/js/setup_wizard.js"
override_doctype_class = {
"User": "lms.overrides.user.CustomUser",
"Web Template": "lms.overrides.web_template.CustomWebTemplate",
"Web Template": "lms.overrides.web_template.CustomWebTemplate"
}
# Document Events
@@ -97,19 +94,18 @@ override_doctype_class = {
# Hook on document methods and events
doc_events = {
"Discussion Reply": {"after_insert": "lms.lms.utils.create_notification_log"},
"Course Lesson": {"on_update": "lms.lms.doctype.lms_quiz.lms_quiz.update_lesson_info"},
}
# Scheduled Tasks
# ---------------
scheduler_events = {
"hourly": [
"lms.lms.doctype.lms_certificate_request.lms_certificate_request.schedule_evals"
]
}
#scheduler_events = {
# "daily": [
# "erpnext.stock.reorder_item.reorder_item"
# ]
#}
fixtures = ["Custom Field", "Function", "Industry"]
fixtures = ["Custom Field"]
# Testing
# -------
@@ -138,129 +134,24 @@ fixtures = ["Custom Field", "Function", "Industry"]
website_route_rules = [
{"from_route": "/sketches/<sketch>", "to_route": "sketches/sketch"},
{"from_route": "/courses/<course>", "to_route": "courses/course"},
{"from_route": "/courses/<course>/edit", "to_route": "courses/create"},
{"from_route": "/courses/<course>/outline", "to_route": "courses/outline"},
{"from_route": "/courses/<course>/<certificate>", "to_route": "courses/certificate"},
{"from_route": "/hackathons/<hackathon>", "to_route": "hackathons/hackathon"},
{"from_route": "/hackathons/<hackathon>/<project>", "to_route": "hackathons/project"},
{"from_route": "/courses/<course>/learn", "to_route": "batch/learn"},
{
"from_route": "/courses/<course>/learn/<int:chapter>.<int:lesson>",
"to_route": "batch/learn",
},
{
"from_route": "/courses/<course>/learn/<int:chapter>.<int:lesson>/edit",
"to_route": "batch/edit",
},
{"from_route": "/quizzes", "to_route": "batch/quiz_list"},
{"from_route": "/quizzes/<quizname>", "to_route": "batch/quiz"},
{"from_route": "/batches/<batchname>", "to_route": "batches/batch"},
{"from_route": "/courses/<course>/learn/<int:chapter>.<int:lesson>", "to_route": "batch/learn"},
{"from_route": "/courses/<course>/progress", "to_route": "batch/progress"},
{"from_route": "/courses/<course>/join", "to_route": "batch/join"},
{"from_route": "/courses/<course>/manage", "to_route": "cohorts"},
{"from_route": "/courses/<course>/cohorts/<cohort>", "to_route": "cohorts/cohort"},
{
"from_route": "/courses/<course>/cohorts/<cohort>/<page>",
"to_route": "cohorts/cohort",
},
{
"from_route": "/courses/<course>/subgroups/<cohort>/<subgroup>",
"to_route": "cohorts/subgroup",
},
{
"from_route": "/courses/<course>/subgroups/<cohort>/<subgroup>/<page>",
"to_route": "cohorts/subgroup",
},
{
"from_route": "/courses/<course>/join/<cohort>/<subgroup>/<invite_code>",
"to_route": "cohorts/join",
},
{"from_route": "/users", "to_route": "profiles/profile"},
{"from_route": "/jobs/<job>", "to_route": "jobs/job"},
{
"from_route": "/batches/<batchname>/students/<username>",
"to_route": "/batches/progress",
},
{"from_route": "/assignments/<assignment>", "to_route": "assignments/assignment"},
{
"from_route": "/assignment-submission/<assignment>/<submission>",
"to_route": "assignment_submission/assignment_submission",
},
{
"from_route": "/quiz-submission/<quiz>/<submission>",
"to_route": "quiz_submission/quiz_submission",
},
{
"from_route": "/billing/<module>/<modulename>",
"to_route": "billing/billing",
},
{
"from_route": "/batches/details/<batchname>",
"to_route": "batches/batch_details",
},
{
"from_route": "/certified-participants",
"to_route": "certified_participants/certified_participants",
},
{"from_route": "/users/<string(minlength=4):username>", "to_route": "profiles/profile"}
]
website_redirects = [
{"source": "/update-profile", "target": "/edit-profile"},
{"source": "/dashboard", "target": "/courses"},
{"source": "/community", "target": "/people"},
]
update_website_context = [
"lms.widgets.update_website_context",
'lms.widgets.update_website_context',
]
jinja = {
"methods": [
"lms.page_renderers.get_profile_url",
"lms.overrides.user.get_enrolled_courses",
"lms.overrides.user.get_course_membership",
"lms.overrides.user.get_authored_courses",
"lms.overrides.user.get_palette",
"lms.lms.utils.get_membership",
"lms.lms.utils.get_lessons",
"lms.lms.utils.get_tags",
"lms.lms.utils.get_instructors",
"lms.lms.utils.get_students",
"lms.lms.utils.get_average_rating",
"lms.lms.utils.is_certified",
"lms.lms.utils.get_lesson_index",
"lms.lms.utils.get_lesson_url",
"lms.lms.utils.get_chapters",
"lms.lms.utils.get_slugified_chapter_title",
"lms.lms.utils.get_progress",
"lms.lms.utils.render_html",
"lms.lms.utils.is_mentor",
"lms.lms.utils.is_cohort_staff",
"lms.lms.utils.get_mentors",
"lms.lms.utils.get_reviews",
"lms.lms.utils.is_eligible_to_review",
"lms.lms.utils.get_initial_members",
"lms.lms.utils.get_sorted_reviews",
"lms.lms.utils.is_instructor",
"lms.lms.utils.convert_number_to_character",
"lms.lms.utils.get_signup_optin_checks",
"lms.lms.utils.get_popular_courses",
"lms.lms.utils.format_amount",
"lms.lms.utils.first_lesson_exists",
"lms.lms.utils.get_courses_under_review",
"lms.lms.utils.has_course_instructor_role",
"lms.lms.utils.has_course_moderator_role",
"lms.lms.utils.get_certificates",
"lms.lms.utils.format_number",
"lms.lms.utils.get_lesson_count",
"lms.lms.utils.get_all_memberships",
"lms.lms.utils.get_filtered_membership",
"lms.lms.utils.show_start_learing_cta",
"lms.lms.utils.can_create_courses",
"lms.lms.utils.get_telemetry_boot_info",
"lms.lms.utils.is_onboarding_complete",
"lms.www.utils.is_student",
],
"filters": [],
}
## Specify the additional tabs to be included in the user profile page.
## Each entry must be a subclass of lms.lms.plugins.ProfileTab
# profile_tabs = []
@@ -268,56 +159,19 @@ jinja = {
## Specify the extension to be used to control what scripts and stylesheets
## to be included in lesson pages. The specified value must be be a
## subclass of lms.plugins.PageExtension
# lms_lesson_page_extension = None
# lms = None
# lms_lesson_page_extensions = [
# "lms.plugins.LiveCodeExtension"
# ]
has_website_permission = {
"LMS Certificate Evaluation": "lms.lms.doctype.lms_certificate_evaluation.lms_certificate_evaluation.has_website_permission"
}
profile_mandatory_fields = [
"first_name",
"last_name",
"user_image",
"bio",
"linkedin",
"education",
"skill",
"preferred_functions",
"preferred_industries",
"dream_companies",
"attire",
"collaboration",
"role",
"location_preference",
"time",
"company_type",
]
#lms_lesson_page_extensions = [
# "lms.plugins.LiveCodeExtension"
#]
## Markdown Macros for Lessons
lms_markdown_macro_renderers = {
"Exercise": "lms.plugins.exercise_renderer",
"Quiz": "lms.plugins.quiz_renderer",
"YouTubeVideo": "lms.plugins.youtube_video_renderer",
"Video": "lms.plugins.video_renderer",
"Assignment": "lms.plugins.assignment_renderer",
"Embed": "lms.plugins.embed_renderer",
"Audio": "lms.plugins.audio_renderer",
"PDF": "lms.plugins.pdf_renderer",
"Exercise": "lms.plugins.exercise_renderer",
"Quiz": "lms.plugins.quiz_renderer",
"YouTubeVideo": "lms.plugins.youtube_video_renderer",
"Video": "lms.plugins.video_renderer"
}
# page_renderer to manage profile pages
page_renderer = [
"lms.page_renderers.ProfileRedirectPage",
"lms.page_renderers.ProfilePage",
]
# set this to "/" to have profiles on the top-level
profile_url_prefix = "/users/"
signup_form_template = "lms.plugins.show_custom_signup"
on_session_creation = "lms.overrides.user.on_session_creation"

View File

@@ -1,184 +0,0 @@
import frappe
from frappe.desk.page.setup_wizard.setup_wizard import add_all_roles_to
def after_install():
add_pages_to_nav()
def after_sync():
create_lms_roles()
set_default_home()
set_default_certificate_print_format()
add_all_roles_to("Administrator")
def add_pages_to_nav():
pages = [
{"label": "Explore", "idx": 1},
{"label": "Courses", "url": "/courses", "parent": "Explore", "idx": 2},
{"label": "Batches", "url": "/batches", "parent": "Explore", "idx": 3},
{"label": "Statistics", "url": "/statistics", "parent": "Explore", "idx": 4},
{"label": "Jobs", "url": "/jobs", "parent": "Explore", "idx": 5},
{"label": "People", "url": "/community", "parent": "Explore", "idx": 6},
]
for page in pages:
filters = frappe._dict()
if page.get("url"):
filters["url"] = ["like", "%" + page.get("url") + "%"]
else:
filters["label"] = page.get("label")
if not frappe.db.exists("Top Bar Item", filters):
frappe.get_doc(
{
"doctype": "Top Bar Item",
"label": page.get("label"),
"url": page.get("url"),
"parent_label": page.get("parent"),
"idx": page.get("idx"),
"parent": "Website Settings",
"parenttype": "Website Settings",
"parentfield": "top_bar_items",
}
).save()
def before_uninstall():
delete_custom_fields()
delete_lms_roles()
def create_lms_roles():
create_course_creator_role()
create_moderator_role()
create_evaluator_role()
create_lms_student_role()
def delete_lms_roles():
roles = ["Course Creator", "Moderator"]
for role in roles:
if frappe.db.exists("Role", role):
frappe.db.delete("Role", role)
def set_default_home():
frappe.db.set_single_value("Portal Settings", "default_portal_home", "/courses")
def create_course_creator_role():
if not frappe.db.exists("Role", "Course Creator"):
role = frappe.get_doc(
{
"doctype": "Role",
"role_name": "Course Creator",
"home_page": "",
"desk_access": 0,
}
)
role.save()
def create_moderator_role():
if not frappe.db.exists("Role", "Moderator"):
role = frappe.get_doc(
{
"doctype": "Role",
"role_name": "Moderator",
"home_page": "",
"desk_access": 0,
}
)
role.save()
def create_evaluator_role():
if not frappe.db.exists("Role", "Class Evaluator"):
role = frappe.new_doc("Role")
role.update(
{
"role_name": "Class Evaluator",
"home_page": "",
"desk_access": 0,
}
)
role.save()
def create_lms_student_role():
if not frappe.db.exists("Role", "LMS Student"):
role = frappe.new_doc("Role")
role.update(
{
"role_name": "LMS Student",
"home_page": "",
"desk_access": 0,
}
)
role.save()
def set_default_certificate_print_format():
filters = {
"doc_type": "LMS Certificate",
"property": "default_print_format",
}
if not frappe.db.exists("Property Setter", filters):
filters.update(
{
"doctype_or_field": "DocType",
"property_type": "Data",
"value": "Certificate",
}
)
doc = frappe.new_doc("Property Setter")
doc.update(filters)
doc.save()
def delete_custom_fields():
fields = [
"user_category",
"headline",
"college",
"city",
"verify_terms",
"country",
"preferred_location",
"preferred_functions",
"preferred_industries",
"work_environment_column",
"time",
"role",
"carrer_preference_details",
"skill",
"certification_details",
"internship",
"branch",
"github",
"medium",
"linkedin",
"profession",
"looking_for_job",
"cover_image" "work_environment",
"dream_companies",
"career_preference_column",
"attire",
"collaboration",
"location_preference",
"company_type",
"skill_details",
"certification",
"education",
"work_experience",
"education_details",
"hide_private",
"work_experience_details",
"profile_complete",
]
for field in fields:
frappe.db.delete("Custom Field", {"fieldname": field})

View File

@@ -1,9 +0,0 @@
// Copyright (c) 2021, Frappe and contributors
// For license information, please see license.txt
frappe.ui.form.on("Job Opportunity", {
refresh: (frm) => {
if (frm.doc.name)
frm.add_web_link(`/jobs/${frm.doc.name}`, "See on Website");
},
});

View File

@@ -1,157 +0,0 @@
{
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "format: JOB-{#####}",
"creation": "2022-02-07 12:01:41.074418",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"job_title",
"location",
"disabled",
"column_break_5",
"type",
"status",
"section_break_6",
"description",
"company_details_section",
"company_name",
"company_website",
"column_break_11",
"company_logo",
"application_link"
],
"fields": [
{
"fieldname": "job_title",
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Job Title",
"reqd": 1
},
{
"fieldname": "location",
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Location",
"reqd": 1
},
{
"fieldname": "column_break_5",
"fieldtype": "Column Break"
},
{
"default": "Full Time",
"fieldname": "type",
"fieldtype": "Select",
"label": "Type",
"options": "Full Time\nPart Time\nFreelance\nContract",
"reqd": 1
},
{
"default": "Open",
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Status",
"options": "Open\nClosed"
},
{
"fieldname": "section_break_6",
"fieldtype": "Section Break"
},
{
"fieldname": "description",
"fieldtype": "Text Editor",
"label": "Description",
"reqd": 1
},
{
"fieldname": "company_details_section",
"fieldtype": "Section Break",
"label": "Company Details"
},
{
"fieldname": "company_name",
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Company Name",
"reqd": 1
},
{
"fieldname": "company_website",
"fieldtype": "Data",
"label": "Company Website",
"reqd": 1
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"fieldname": "company_logo",
"fieldtype": "Attach Image",
"label": "Company Logo",
"reqd": 1
},
{
"fieldname": "application_link",
"fieldtype": "Data",
"label": "Application Form Link",
"reqd": 1
},
{
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled"
}
],
"index_web_pages_for_search": 1,
"links": [],
"make_attachments_public": 1,
"modified": "2023-09-29 17:03:30.825021",
"modified_by": "Administrator",
"module": "Job",
"name": "Job Opportunity",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"select": 1,
"share": 1,
"write": 1
},
{
"create": 1,
"email": 1,
"export": 1,
"if_owner": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "LMS Student",
"select": 1,
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "job_title"
}

View File

@@ -1,41 +0,0 @@
# Copyright (c) 2021, Frappe and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import get_link_to_form
from frappe.utils.user import get_system_managers
from lms.lms.utils import validate_image
class JobOpportunity(Document):
def validate(self):
self.validate_urls()
self.company_logo = validate_image(self.company_logo)
def validate_urls(self):
frappe.utils.validate_url(self.company_website, True)
frappe.utils.validate_url(self.application_link, True)
@frappe.whitelist()
def report(job, reason):
system_managers = get_system_managers(only_name=True)
user = frappe.db.get_value("User", frappe.session.user, "full_name")
subject = _("User {0} has reported the job post {1}").format(user, job)
args = {
"job": job,
"job_url": get_link_to_form("Job Opportunity", job),
"user": user,
"reason": reason,
}
frappe.sendmail(
recipients=system_managers,
subject=subject,
header=[subject, "green"],
template="job_report",
args=args,
now=True,
)

View File

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

View File

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

View File

@@ -1,54 +0,0 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2022-02-07 12:01:41.422955",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"allow_posting",
"title",
"subtitle"
],
"fields": [
{
"default": "0",
"fieldname": "allow_posting",
"fieldtype": "Check",
"label": "Allow Job Posting From Website"
},
{
"fieldname": "title",
"fieldtype": "Data",
"label": "Job Board Title"
},
{
"fieldname": "subtitle",
"fieldtype": "Data",
"label": "Job Board Subtitle"
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2022-02-11 15:56:38.958317",
"modified_by": "Administrator",
"module": "Job",
"name": "Job Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

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

View File

@@ -1,9 +0,0 @@
# Copyright (c) 2022, Frappe and Contributors
# See license.txt
# import frappe
import unittest
class TestJobSettings(unittest.TestCase):
pass

View File

@@ -1,28 +0,0 @@
{
"attach_print": 0,
"channel": "Email",
"creation": "2021-12-30 12:29:56.533903",
"days_in_advance": 0,
"docstatus": 0,
"doctype": "Notification",
"document_type": "Job Opportunity",
"enabled": 1,
"event": "New",
"idx": 0,
"is_standard": 1,
"message": "Hey,\n\nA new Job Opportunity has been created. \n\n<p>Company Name: {{ doc.company_name}}</p>\n<p>Job Title: {{ doc.job_title}}</p>\n<p>Job Location: {{ doc.location}}</p><br>\n<p>Job Description: {{ doc.description}}</p><br>\n\n<p>Find all the posted jobs <a href=\"{{ frappe.utils.get_url() }}/app/job-opportunity\">here</a>.</p><br>\n",
"modified": "2021-12-30 12:54:13.382512",
"modified_by": "Administrator",
"module": "Job",
"name": "New job alert",
"owner": "Administrator",
"recipients": [
{
"receiver_by_role": "System Manager"
}
],
"send_system_notification": 0,
"send_to_all_assignees": 0,
"sender_email": "",
"subject": "New job added by {{ doc.company_name}}"
}

View File

@@ -1,10 +0,0 @@
Hey,
A new Job Opportunity has been created.
<p>Company Name: {{ doc.company_name}}</p>
<p>Job Title: {{ doc.job_title}}</p>
<p>Job Location: {{ doc.location}}</p><br>
<p>Job Description: {{ doc.description}}</p><br>
<p>Find all the posted jobs <a href="{{ frappe.utils.get_url() }}/app/job-opportunity">here</a>.</p><br>

View File

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

View File

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

View File

@@ -1,151 +0,0 @@
{
"accept_payment": 0,
"allow_comments": 0,
"allow_delete": 0,
"allow_edit": 1,
"allow_incomplete": 0,
"allow_multiple": 1,
"allow_print": 0,
"amount": 0.0,
"amount_based_on_field": 0,
"apply_document_permissions": 1,
"button_label": "Save",
"creation": "2021-12-27 17:02:12.461145",
"custom_css": "[data-doctype=\"Web Form\"] {\n max-width: 720px;\n margin: 6rem auto;\n}",
"doc_type": "Job Opportunity",
"docstatus": 0,
"doctype": "Web Form",
"idx": 0,
"is_standard": 1,
"list_columns": [],
"login_required": 1,
"max_attachment_size": 0,
"modified": "2022-09-15 17:22:43.957184",
"modified_by": "Administrator",
"module": "Job",
"name": "job-opportunity",
"owner": "Administrator",
"payment_button_label": "Buy Now",
"published": 1,
"route": "job-opportunity",
"show_attachments": 0,
"show_list": 1,
"show_sidebar": 0,
"success_message": "",
"success_url": "/jobs",
"title": "Job Opportunity",
"web_form_fields": [
{
"allow_read_on_all_link_options": 0,
"fieldname": "job_title",
"fieldtype": "Data",
"hidden": 0,
"label": "Job Title",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 1,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "location",
"fieldtype": "Data",
"hidden": 0,
"label": "Location",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 1,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"default": "Full Time",
"fieldname": "type",
"fieldtype": "Select",
"hidden": 0,
"label": "Type",
"max_length": 0,
"max_value": 0,
"options": "Full Time\nPart Time\nFreelance\nContract",
"read_only": 0,
"reqd": 1,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"default": "Open",
"fieldname": "status",
"fieldtype": "Select",
"hidden": 0,
"label": "Status",
"max_length": 0,
"max_value": 0,
"options": "Open\nClosed",
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "company_name",
"fieldtype": "Data",
"hidden": 0,
"label": "Company Name",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 1,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "company_website",
"fieldtype": "Data",
"hidden": 0,
"label": "Company Website",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 1,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "company_logo",
"fieldtype": "Attach Image",
"hidden": 0,
"label": "Company Logo",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 1,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "application_link",
"fieldtype": "Data",
"hidden": 0,
"label": "Application Form Link",
"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": "Text Editor",
"hidden": 0,
"label": "Description",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 1,
"show_in_filter": 0
}
]
}

View File

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

View File

@@ -3,139 +3,45 @@
import frappe
@frappe.whitelist()
def autosave_section(section, code):
"""Saves the code edited in one of the sections."""
doc = frappe.get_doc(
doctype="Code Revision", section=section, code=code, author=frappe.session.user
)
doc.insert()
return {"name": doc.name}
"""Saves the code edited in one of the sections.
"""
doc = frappe.get_doc(
doctype="Code Revision",
section=section,
code=code,
author=frappe.session.user)
doc.insert()
return {"name": doc.name}
@frappe.whitelist()
def submit_solution(exercise, code):
"""Submits a solution.
@exerecise: name of the exercise to submit
@code: solution to the exercise
"""
ex = frappe.get_doc("LMS Exercise", exercise)
if not ex:
return
doc = ex.submit(code)
return {"name": doc.name, "creation": doc.creation}
"""Submits a solution.
@exerecise: name of the exercise to submit
@code: solution to the exercise
"""
ex = frappe.get_doc("Exercise", exercise)
if not ex:
return
doc = ex.submit(code)
return {"name": doc.name, "creation": doc.creation}
@frappe.whitelist()
def save_current_lesson(course_name, lesson_name):
"""Saves the current lesson for a student/mentor."""
name = frappe.get_value(
doctype="LMS Enrollment",
filters={"course": course_name, "member": frappe.session.user},
fieldname="name",
)
if not name:
return
doc = frappe.get_doc("LMS Enrollment", name)
doc.current_lesson = lesson_name
doc.save()
return {"current_lesson": doc.current_lesson}
@frappe.whitelist()
def join_cohort(course, cohort, subgroup, invite_code):
"""Creates a Cohort Join Request for given user."""
course_doc = frappe.get_doc("LMS Course", course)
cohort_doc = course_doc and course_doc.get_cohort(cohort)
subgroup_doc = cohort_doc and cohort_doc.get_subgroup(subgroup)
if not subgroup_doc or subgroup_doc.invite_code != invite_code:
return {"ok": False, "error": "Invalid join link"}
data = {
"doctype": "Cohort Join Request",
"cohort": cohort_doc.name,
"subgroup": subgroup_doc.name,
"email": frappe.session.user,
"status": "Pending",
}
# Don't insert duplicate records
if frappe.db.exists(data):
return {"ok": True, "status": "record found"}
else:
doc = frappe.get_doc(data)
doc.insert()
return {"ok": True, "status": "record created"}
@frappe.whitelist()
def approve_cohort_join_request(join_request):
r = frappe.get_doc("Cohort Join Request", join_request)
sg = r and frappe.get_doc("Cohort Subgroup", r.subgroup)
if not sg or r.status not in ["Pending", "Accepted"]:
return {"ok": False, "error": "Invalid Join Request"}
if (
not sg.is_manager(frappe.session.user) and "System Manager" not in frappe.get_roles()
):
return {"ok": False, "error": "Permission Deined"}
r.status = "Accepted"
r.save()
return {"ok": True}
@frappe.whitelist()
def reject_cohort_join_request(join_request):
r = frappe.get_doc("Cohort Join Request", join_request)
sg = r and frappe.get_doc("Cohort Subgroup", r.subgroup)
if not sg or r.status not in ["Pending", "Rejected"]:
return {"ok": False, "error": "Invalid Join Request"}
if (
not sg.is_manager(frappe.session.user) and "System Manager" not in frappe.get_roles()
):
return {"ok": False, "error": "Permission Deined"}
r.status = "Rejected"
r.save()
return {"ok": True}
@frappe.whitelist()
def undo_reject_cohort_join_request(join_request):
r = frappe.get_doc("Cohort Join Request", join_request)
sg = r and frappe.get_doc("Cohort Subgroup", r.subgroup)
# keeping Pending as well to consider the case of duplicate requests
if not sg or r.status not in ["Pending", "Rejected"]:
return {"ok": False, "error": "Invalid Join Request"}
if (
not sg.is_manager(frappe.session.user) and "System Manager" not in frappe.get_roles()
):
return {"ok": False, "error": "Permission Deined"}
r.status = "Pending"
r.save()
return {"ok": True}
@frappe.whitelist()
def add_mentor_to_subgroup(subgroup, email):
try:
sg = frappe.get_doc("Cohort Subgroup", subgroup)
except frappe.DoesNotExistError:
return {"ok": False, "error": f"Invalid subgroup: {subgroup}"}
if (
not sg.get_cohort().is_admin(frappe.session.user)
and "System Manager" not in frappe.get_roles()
):
return {"ok": False, "error": "Permission Deined"}
try:
user = frappe.get_doc("User", email)
except frappe.DoesNotExistError:
return {"ok": False, "error": f"Invalid user: {email}"}
sg.add_mentor(email)
return {"ok": True}
"""Saves the current lesson for a student/mentor.
"""
name = frappe.get_value(
doctype="LMS Batch Membership",
filters={
"course": course_name,
"member": frappe.session.user
},
fieldname="name")
if not name:
return
doc = frappe.get_doc("LMS Batch Membership", name)
doc.current_lesson = lesson_name
doc.save(ignore_permissions=True)
return {"current_lesson": doc.current_lesson}

View File

@@ -2,26 +2,23 @@
"based_on": "creation",
"chart_name": "Course Enrollments",
"chart_type": "Count",
"color": "#4463F0",
"creation": "2021-09-30 12:23:09.414853",
"custom_options": "{\"type\": \"line\", \"axisOptions\": {\"xIsSeries\": 1}, \"lineOptions\": {\"regionFill\": 1}}",
"docstatus": 0,
"doctype": "Dashboard Chart",
"document_type": "LMS Enrollment",
"document_type": "LMS Batch Membership",
"dynamic_filters_json": "[]",
"filters_json": "[]",
"group_by_type": "Count",
"idx": 1,
"idx": 0,
"is_public": 1,
"is_standard": 1,
"last_synced_on": "2022-10-20 10:46:56.859520",
"modified": "2022-10-20 11:30:26.863009",
"last_synced_on": "2021-10-17 13:32:21.745498",
"modified": "2021-10-21 17:31:06.997133",
"modified_by": "Administrator",
"module": "LMS",
"name": "Course Enrollments",
"number_of_groups": 0,
"owner": "basawaraj@erpnext.com",
"roles": [],
"source": "",
"time_interval": "Daily",
"timeseries": 1,

View File

@@ -1,33 +0,0 @@
{
"based_on": "creation",
"chart_name": "Lesson Completion",
"chart_type": "Count",
"color": "#4463F0",
"creation": "2022-11-09 16:52:19.021695",
"custom_options": "{\"type\": \"line\", \"axisOptions\": {\"xIsSeries\": 1}, \"lineOptions\": {\"regionFill\": 1}}",
"docstatus": 0,
"doctype": "Dashboard Chart",
"document_type": "LMS Course Progress",
"dynamic_filters_json": "[]",
"filters_json": "[]",
"group_by_type": "Count",
"idx": 0,
"is_public": 1,
"is_standard": 1,
"modified": "2022-11-09 16:52:52.241756",
"modified_by": "Administrator",
"module": "LMS",
"name": "Lesson Completion",
"number_of_groups": 0,
"owner": "Administrator",
"parent_document_type": "",
"roles": [],
"source": "",
"time_interval": "Daily",
"timeseries": 1,
"timespan": "Last Month",
"type": "Line",
"use_report_chart": 0,
"value_based_on": "",
"y_axis": []
}

View File

@@ -1,33 +0,0 @@
{
"based_on": "creation",
"chart_name": "New Signups",
"chart_type": "Count",
"color": "#4463F0",
"creation": "2021-09-28 18:57:50.047656",
"custom_options": "{\"type\": \"line\", \"axisOptions\": {\"xIsSeries\": 1}, \"lineOptions\": {\"regionFill\": 1}}",
"docstatus": 0,
"doctype": "Dashboard Chart",
"document_type": "User",
"dynamic_filters_json": "[]",
"filters_json": "[]",
"group_by_type": "Count",
"idx": 1,
"is_public": 1,
"is_standard": 1,
"last_synced_on": "2022-10-20 10:46:56.849265",
"modified": "2022-10-20 11:31:17.184897",
"modified_by": "Administrator",
"module": "LMS",
"name": "New Signups",
"number_of_groups": 0,
"owner": "basawaraj@erpnext.com",
"roles": [],
"source": "",
"time_interval": "Daily",
"timeseries": 1,
"timespan": "Last Quarter",
"type": "Line",
"use_report_chart": 0,
"value_based_on": "",
"y_axis": []
}

View File

@@ -1,51 +0,0 @@
{
"actions": [],
"autoname": "autoincrement",
"creation": "2022-11-09 16:23:26.454527",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"course",
"title",
"evaluator"
],
"fields": [
{
"fieldname": "course",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Course",
"options": "LMS Course",
"reqd": 1
},
{
"fetch_from": "course.title",
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Course Title",
"read_only": 1
},
{
"fieldname": "evaluator",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Evaluator",
"options": "Course Evaluator"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-08-28 10:03:02.960844",
"modified_by": "Administrator",
"module": "LMS",
"name": "Batch Course",
"naming_rule": "Autoincrement",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

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

View File

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

View File

@@ -1,76 +0,0 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2022-11-09 16:20:44.602545",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"student_details_section",
"student",
"payment",
"confirmation_email_sent",
"column_break_oduu",
"student_name",
"username"
],
"fields": [
{
"fieldname": "student",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Student",
"options": "User",
"reqd": 1
},
{
"fetch_from": "student.full_name",
"fieldname": "student_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Student Name",
"read_only": 1
},
{
"fetch_from": "student.username",
"fieldname": "username",
"fieldtype": "Data",
"label": "Username",
"read_only": 1
},
{
"fieldname": "student_details_section",
"fieldtype": "Section Break",
"label": "Student Details"
},
{
"fieldname": "column_break_oduu",
"fieldtype": "Column Break"
},
{
"fieldname": "payment",
"fieldtype": "Link",
"label": "Payment",
"options": "LMS Payment"
},
{
"default": "0",
"fieldname": "confirmation_email_sent",
"fieldtype": "Check",
"label": "Confirmation Email Sent"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-10-09 17:09:50.481794",
"modified_by": "Administrator",
"module": "LMS",
"name": "Batch Student",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

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

View File

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

View File

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

View File

@@ -1,74 +0,0 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "hash",
"creation": "2021-12-07 12:20:37.143096",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"certification_name",
"organization",
"description",
"column_break_4",
"expire",
"issue_date",
"expiration_date"
],
"fields": [
{
"fieldname": "certification_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Certification Name",
"reqd": 1
},
{
"fieldname": "organization",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Organization",
"reqd": 1
},
{
"fieldname": "issue_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Issue Date",
"reqd": 1
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Description"
},
{
"depends_on": "eval: !doc.expire",
"fieldname": "expiration_date",
"fieldtype": "Data",
"label": "Expiration Date"
},
{
"default": "0",
"fieldname": "expire",
"fieldtype": "Check",
"label": "This certificate does no expire"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-12-21 10:05:43.377876",
"modified_by": "Administrator",
"module": "LMS",
"name": "Certification",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC"
}

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
{
"actions": [],
"autoname": "hash",
"creation": "2021-07-27 16:25:02.903245",
"doctype": "DocType",
"editable_grid": 1,
@@ -21,15 +20,13 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2022-03-15 09:39:41.937565",
"modified": "2021-09-30 10:35:30.014950",
"modified_by": "Administrator",
"module": "LMS",
"name": "Chapter Reference",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -4,6 +4,5 @@
# import frappe
from frappe.model.document import Document
class ChapterReference(Document):
pass

View File

@@ -1,133 +0,0 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "format:{course}/{slug}",
"creation": "2021-11-19 11:45:31.016097",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"course",
"title",
"slug",
"section_break_2",
"instructor",
"status",
"column_break_4",
"begin_date",
"end_date",
"duration",
"section_break_8",
"description",
"pages"
],
"fields": [
{
"fieldname": "description",
"fieldtype": "Markdown Editor",
"label": "Description"
},
{
"fieldname": "instructor",
"fieldtype": "Link",
"label": "Instructor",
"options": "User",
"reqd": 1
},
{
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "Upcoming\nLive\nCompleted\nCancelled",
"reqd": 1
},
{
"fieldname": "begin_date",
"fieldtype": "Date",
"label": "Begin Date"
},
{
"fieldname": "end_date",
"fieldtype": "Date",
"label": "End Date"
},
{
"fieldname": "duration",
"fieldtype": "Data",
"label": "Duration"
},
{
"fieldname": "slug",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Slug",
"reqd": 1,
"unique": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_8",
"fieldtype": "Section Break"
},
{
"fieldname": "section_break_2",
"fieldtype": "Section Break"
},
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title",
"reqd": 1
},
{
"fieldname": "course",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Course",
"options": "LMS Course",
"reqd": 1
},
{
"fieldname": "pages",
"fieldtype": "Table",
"label": "Pages",
"options": "Cohort Web Page"
}
],
"index_web_pages_for_search": 1,
"links": [
{
"group": "Links",
"link_doctype": "Cohort Subgroup",
"link_fieldname": "cohort"
}
],
"modified": "2022-10-13 15:46:32.322926",
"modified_by": "Administrator",
"module": "LMS",
"name": "Cohort",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -1,79 +0,0 @@
# Copyright (c) 2021, FOSS United and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
class Cohort(Document):
def get_url(self):
return f"{frappe.utils.get_url()}/courses/{self.course}/cohorts/{self.slug}"
def get_subgroups(self, include_counts=False, sort_by=None):
names = frappe.get_all("Cohort Subgroup", filters={"cohort": self.name}, pluck="name")
subgroups = [frappe.get_cached_doc("Cohort Subgroup", name) for name in names]
subgroups = sorted(subgroups, key=lambda sg: sg.title)
if include_counts:
mentors = self._get_subgroup_counts("Cohort Mentor")
students = self._get_subgroup_counts("LMS Enrollment")
join_requests = self._get_subgroup_counts("Cohort Join Request", status="Pending")
for s in subgroups:
s.num_mentors = mentors.get(s.name, 0)
s.num_students = students.get(s.name, 0)
s.num_join_requests = join_requests.get(s.name, 0)
if sort_by:
subgroups.sort(key=lambda sg: getattr(sg, sort_by), reverse=True)
return subgroups
def _get_subgroup_counts(self, doctype, **kw):
rows = frappe.get_all(
doctype,
filters={"cohort": self.name, **kw},
fields=["subgroup", "count(*) as count"],
group_by="subgroup",
)
return {row["subgroup"]: row["count"] for row in rows}
def _get_count(self, doctype, **kw):
filters = {"cohort": self.name, **kw}
return frappe.db.count(doctype, filters=filters)
def get_page_template(self, slug, scope=None):
p = self.get_page(slug, scope=scope)
return p and p.get_template_html()
def get_page(self, slug, scope=None):
for p in self.pages:
if p.slug == slug and scope in [p.scope, None]:
return p
def get_pages(self, scope=None):
return [p for p in self.pages if scope in [p.scope, None]]
def get_stats(self):
return {
"subgroups": self._get_count("Cohort Subgroup"),
"mentors": self._get_count("Cohort Mentor"),
"students": self._get_count("LMS Enrollment"),
"join_requests": self._get_count("Cohort Join Request", status="Pending"),
}
def get_subgroup(self, slug):
q = dict(cohort=self.name, slug=slug)
name = frappe.db.get_value("Cohort Subgroup", q, "name")
return name and frappe.get_doc("Cohort Subgroup", name)
def get_mentor(self, email):
q = dict(cohort=self.name, email=email)
name = frappe.db.get_value("Cohort Mentor", q, "name")
return name and frappe.get_doc("Cohort Mentor", name)
def is_mentor(self, email):
q = {"doctype": "Cohort Mentor", "cohort": self.name, "email": email}
return frappe.db.exists(q)
def is_admin(self, email):
q = {"doctype": "Cohort Staff", "cohort": self.name, "email": email, "role": "Admin"}
return frappe.db.exists(q)

View File

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

View File

@@ -1,88 +0,0 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2021-11-19 16:27:41.716509",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"cohort",
"email",
"column_break_3",
"subgroup",
"status"
],
"fields": [
{
"fieldname": "cohort",
"fieldtype": "Link",
"label": "Cohort",
"options": "Cohort",
"reqd": 1
},
{
"fieldname": "subgroup",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Subgroup",
"options": "Cohort Subgroup",
"reqd": 1
},
{
"fieldname": "email",
"fieldtype": "Link",
"in_list_view": 1,
"label": "E-Mail",
"options": "User",
"reqd": 1
},
{
"default": "Pending",
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Pending\nAccepted\nRejected"
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-09-29 17:08:18.950560",
"modified_by": "Administrator",
"module": "LMS",
"name": "Cohort Join Request",
"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,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "LMS Student",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -1,52 +0,0 @@
# Copyright (c) 2021, FOSS United and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
class CohortJoinRequest(Document):
def on_update(self):
if self.status == "Accepted":
self.ensure_student()
def ensure_student(self):
# case 1 - user is already a member
q = {
"doctype": "LMS Enrollment",
"cohort": self.cohort,
"subgroup": self.subgroup,
"member": self.email,
"member_type": "Student",
}
if frappe.db.exists(q):
return
# case 2 - user has signed up for this course, possibly not this cohort
cohort = frappe.get_doc("Cohort", self.cohort)
q = {
"doctype": "LMS Enrollment",
"course": cohort.course,
"member": self.email,
"member_type": "Student",
}
name = frappe.db.exists(q)
if name:
doc = frappe.get_doc("LMS Enrollment", name)
doc.cohort = self.cohort
doc.subgroup = self.subgroup
doc.save(ignore_permissions=True)
else:
# case 3 - user has not signed up for this course yet
data = {
"doctype": "LMS Enrollment",
"course": cohort.course,
"cohort": self.cohort,
"subgroup": self.subgroup,
"member": self.email,
"member_type": "Student",
"role": "Member",
}
doc = frappe.get_doc(data)
doc.insert(ignore_permissions=True)

View File

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

View File

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

View File

@@ -1,73 +0,0 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2021-11-19 15:31:47.129156",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"cohort",
"email",
"subgroup",
"course"
],
"fields": [
{
"fieldname": "cohort",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Cohort",
"options": "Cohort",
"reqd": 1
},
{
"fieldname": "email",
"fieldtype": "Link",
"in_list_view": 1,
"label": "E-mail",
"options": "User",
"reqd": 1
},
{
"fieldname": "subgroup",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Primary Subgroup",
"options": "Cohort Subgroup",
"reqd": 1
},
{
"fetch_from": "cohort.course",
"fieldname": "course",
"fieldtype": "Link",
"label": "Course",
"options": "LMS Course",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-11-29 16:32:33.235281",
"modified_by": "Administrator",
"module": "LMS",
"name": "Cohort Mentor",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -1,13 +0,0 @@
# Copyright (c) 2021, FOSS United and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
class CohortMentor(Document):
def get_subgroup(self):
return frappe.get_doc("Cohort Subgroup", self.subgroup)
def get_user(self):
return frappe.get_doc("User", self.email)

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,104 +0,0 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "format:{title} ({cohort})",
"creation": "2021-11-19 11:50:27.312434",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"cohort",
"slug",
"title",
"column_break_4",
"invite_code",
"course",
"section_break_7",
"description"
],
"fields": [
{
"fieldname": "cohort",
"fieldtype": "Link",
"in_list_view": 1,
"in_preview": 1,
"in_standard_filter": 1,
"label": "Cohort",
"options": "Cohort",
"reqd": 1
},
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"in_preview": 1,
"in_standard_filter": 1,
"label": "Title",
"reqd": 1
},
{
"fieldname": "description",
"fieldtype": "Markdown Editor",
"label": "Description"
},
{
"fieldname": "invite_code",
"fieldtype": "Data",
"label": "Invite Code",
"read_only": 1
},
{
"fieldname": "slug",
"fieldtype": "Data",
"label": "Slug",
"reqd": 1
},
{
"fetch_from": "cohort.course",
"fieldname": "course",
"fieldtype": "Link",
"label": "Course",
"options": "LMS Course",
"read_only": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_7",
"fieldtype": "Section Break"
}
],
"index_web_pages_for_search": 1,
"links": [
{
"group": "Links",
"link_doctype": "Cohort Join Request",
"link_fieldname": "subgroup"
}
],
"modified": "2021-12-16 15:12:42.504883",
"modified_by": "Administrator",
"module": "LMS",
"name": "Cohort Subgroup",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -1,88 +0,0 @@
# Copyright (c) 2021, FOSS United and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
from frappe.utils import random_string
class CohortSubgroup(Document):
def before_save(self):
if not self.invite_code:
self.invite_code = random_string(8)
def get_url(self):
cohort = frappe.get_doc("Cohort", self.cohort)
return (
f"{frappe.utils.get_url()}/courses/{self.course}/subgroups/{cohort.slug}/{self.slug}"
)
def get_invite_link(self):
cohort = frappe.get_doc("Cohort", self.cohort)
return f"{frappe.utils.get_url()}/courses/{self.course}/join/{cohort.slug}/{self.slug}/{self.invite_code}"
def has_student(self, email):
"""Check if given user is a student of this subgroup."""
q = {"doctype": "LMS Enrollment", "subgroup": self.name, "member": email}
return frappe.db.exists(q)
def has_join_request(self, email):
"""Check if given user is a student of this subgroup."""
q = {"doctype": "Cohort Join Request", "subgroup": self.name, "email": email}
return frappe.db.exists(q)
def get_join_requests(self, status="Pending"):
q = {"subgroup": self.name, "status": status}
return frappe.get_all(
"Cohort Join Request", filters=q, fields=["*"], order_by="creation desc"
)
def get_mentors(self):
emails = frappe.get_all(
"Cohort Mentor", filters={"subgroup": self.name}, fields=["email"], pluck="email"
)
return self._get_users(emails)
def get_students(self):
emails = frappe.get_all(
"LMS Enrollment",
filters={"subgroup": self.name},
fields=["member"],
pluck="member",
page_length=1000,
)
return self._get_users(emails)
def _get_users(self, emails):
users = [frappe.get_cached_doc("User", email) for email in emails]
return sorted(users, key=lambda user: user.full_name)
def is_mentor(self, email):
q = {"doctype": "Cohort Mentor", "subgroup": self.name, "email": email}
return frappe.db.exists(q)
def is_manager(self, email):
"""Returns True if the given user is a manager of this subgroup.
Mentors of the subgroup, admins of the Cohort are considered as managers.
"""
return self.is_mentor(email) or self.get_cohort().is_admin(email)
def get_cohort(self):
return frappe.get_doc("Cohort", self.cohort)
def add_mentor(self, email):
d = {
"doctype": "Cohort Mentor",
"subgroup": self.name,
"cohort": self.cohort,
"email": email,
}
if frappe.db.exists(d):
return
doc = frappe.get_doc(d)
doc.insert(ignore_permissions=True)
# def after_doctype_insert():
# frappe.db.add_unique("Cohort Subgroup", ("cohort", "slug"))

View File

@@ -1,64 +0,0 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2021-12-04 23:28:40.429867",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"slug",
"title",
"template",
"scope",
"required_role"
],
"fields": [
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title",
"reqd": 1
},
{
"fieldname": "template",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Template",
"options": "Web Template",
"reqd": 1
},
{
"default": "Cohort",
"fieldname": "scope",
"fieldtype": "Select",
"label": "Scope",
"options": "Cohort\nSubgroup"
},
{
"default": "Public",
"fieldname": "required_role",
"fieldtype": "Select",
"label": "Required Role",
"options": "Public\nStudent\nMentor\nAdmin"
},
{
"fieldname": "slug",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Slug",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-12-04 23:33:03.954128",
"modified_by": "Administrator",
"module": "LMS",
"name": "Cohort Web Page",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC"
}

View File

@@ -1,10 +0,0 @@
# Copyright (c) 2021, Frappe and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
class CohortWebPage(Document):
def get_template_html(self):
return frappe.get_doc("Web Template", self.template).template

View File

@@ -1,14 +1,14 @@
// Copyright (c) 2021, FOSS United and contributors
// For license information, please see license.txt
frappe.ui.form.on("Course Chapter", {
frappe.ui.form.on('Course Chapter', {
onload: function (frm) {
frm.set_query("lesson", "lessons", function () {
return {
filters: {
chapter: frm.doc.name,
},
};
});
},
frm.set_query("lesson", "lessons", function () {
return {
filters: {
"chapter": frm.doc.name,
}
};
});
}
});

View File

@@ -59,7 +59,7 @@
"link_fieldname": "chapter"
}
],
"modified": "2023-09-29 17:03:58.013819",
"modified": "2021-09-29 15:33:44.611229",
"modified_by": "Administrator",
"module": "LMS",
"name": "Course Chapter",
@@ -77,26 +77,11 @@
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"email": 1,
"export": 1,
"if_owner": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "LMS Student",
"select": 1,
"share": 1,
"write": 1
}
],
"search_fields": "title",
"show_title_field_in_link": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "title",
"track_changes": 1
}

View File

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

View File

@@ -4,6 +4,5 @@
# import frappe
import unittest
class TestCourseChapter(unittest.TestCase):
pass

View File

@@ -1,14 +0,0 @@
// Copyright (c) 2022, Frappe and contributors
// For license information, please see license.txt
frappe.ui.form.on("Course Evaluator", {
onload: (frm) => {
frm.set_query("evaluator", function (doc) {
return {
filters: {
ignore_user_type: 1,
},
};
});
},
});

View File

@@ -1,77 +0,0 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:evaluator",
"creation": "2022-03-29 10:51:47.667284",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"evaluator",
"schedule"
],
"fields": [
{
"fieldname": "evaluator",
"fieldtype": "Link",
"label": "Evaluator",
"options": "User",
"unique": 1
},
{
"fieldname": "schedule",
"fieldtype": "Table",
"label": "Schedule",
"options": "Evaluator Schedule"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-07-13 11:30:22.641076",
"modified_by": "Administrator",
"module": "LMS",
"name": "Course Evaluator",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Moderator",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Class Evaluator",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@@ -1,63 +0,0 @@
# Copyright (c) 2022, Frappe and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
from lms.lms.utils import get_evaluator
class CourseEvaluator(Document):
def validate(self):
self.validate_time_slots()
def validate_time_slots(self):
for schedule in self.schedule:
if schedule.start_time >= schedule.end_time:
frappe.throw(_("Start Time cannot be greater than End Time"))
self.validate_overlaps(schedule)
def validate_overlaps(self, schedule):
same_day_slots = list(
filter(lambda x: x.day == schedule.day and x.name != schedule.name, self.schedule)
)
overlap = False
for slot in same_day_slots:
if schedule.start_time <= slot.start_time < schedule.end_time:
overlap = True
if schedule.start_time < slot.end_time <= schedule.end_time:
overlap = True
if slot.start_time < schedule.start_time and schedule.end_time < slot.end_time:
overlap = True
if overlap:
frappe.throw(_("Slot Times are overlapping for some schedules."))
@frappe.whitelist()
def get_schedule(course, date, batch=None):
evaluator = get_evaluator(course, batch)
all_slots = frappe.get_all(
"Evaluator Schedule",
filters={"parent": evaluator},
fields=["day", "start_time", "end_time"],
order_by="start_time",
)
booked_slots = frappe.get_all(
"LMS Certificate Request",
filters={"evaluator": evaluator, "date": date},
fields=["start_time", "day"],
)
for slot in booked_slots:
same_slot = list(
filter(lambda x: x.start_time == slot.start_time and x.day == slot.day, all_slots)
)
if len(same_slot):
all_slots.remove(same_slot[0])
return all_slots

View File

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

View File

@@ -1,32 +0,0 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2022-02-07 11:39:59.998762",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"instructor"
],
"fields": [
{
"fieldname": "instructor",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Instructor",
"options": "User"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2022-02-07 11:41:42.943250",
"modified_by": "Administrator",
"module": "LMS",
"name": "Course Instructor",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

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

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