Compare commits
12 Commits
v2.30.0
...
version-13
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f38b1226df | ||
|
|
1f3513db8b | ||
|
|
3eb0f13fb0 | ||
|
|
1277133ec6 | ||
|
|
7337aea0dc | ||
|
|
32b601cf34 | ||
|
|
d4dc901925 | ||
|
|
64e581533b | ||
|
|
0873d704d2 | ||
|
|
8ee57f0254 | ||
|
|
7c5021132d | ||
|
|
740c0d10ca |
@@ -9,7 +9,7 @@ root = true
|
|||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
indent_style = tab
|
indent_style = space
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
# Python
|
# Python
|
||||||
@@ -26,4 +26,4 @@ indent_style = tab
|
|||||||
|
|
||||||
# HTML, CSS, javascript, JSON and YAML
|
# HTML, CSS, javascript, JSON and YAML
|
||||||
[*.{html,css,js,json,yml,yaml}]
|
[*.{html,css,js,json,yml,yaml}]
|
||||||
indent_size = 4
|
indent_size = 2
|
||||||
|
|||||||
37
.flake8
@@ -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
|
|
||||||
BIN
.github/batch.png
vendored
|
Before Width: | Height: | Size: 1.1 MiB |
BIN
.github/certificate.png
vendored
|
Before Width: | Height: | Size: 912 KiB |
74
.github/helper/flake8.conf
vendored
@@ -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
|
|
||||||
46
.github/helper/install.sh
vendored
@@ -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
|
|
||||||
14
.github/helper/install_dependencies.sh
vendored
@@ -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-get install libcups2-dev redis-server mariadb-client
|
|
||||||
|
|
||||||
install_wkhtmltopdf() {
|
|
||||||
wget -q https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-1/wkhtmltox_0.12.6-1.focal_amd64.deb
|
|
||||||
sudo apt install ./wkhtmltox_0.12.6-1.focal_amd64.deb
|
|
||||||
}
|
|
||||||
install_wkhtmltopdf &
|
|
||||||
20
.github/helper/site_config.json
vendored
@@ -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
|
|
||||||
}
|
|
||||||
40
.github/helper/update_pot_file.sh
vendored
@@ -1,40 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
cd ~ || exit
|
|
||||||
|
|
||||||
echo "Setting Up Bench..."
|
|
||||||
|
|
||||||
pip install frappe-bench
|
|
||||||
bench -v init frappe-bench --skip-assets --skip-redis-config-generation --python "$(which python)" --frappe-branch "${BASE_BRANCH}"
|
|
||||||
cd ./frappe-bench || exit
|
|
||||||
|
|
||||||
echo "Get LMS..."
|
|
||||||
bench get-app --skip-assets lms "${GITHUB_WORKSPACE}"
|
|
||||||
|
|
||||||
echo "Generating POT file..."
|
|
||||||
bench generate-pot-file --app lms
|
|
||||||
|
|
||||||
cd ./apps/lms || exit
|
|
||||||
|
|
||||||
echo "Configuring git user..."
|
|
||||||
git config user.email "developers@erpnext.com"
|
|
||||||
git config user.name "frappe-pr-bot"
|
|
||||||
|
|
||||||
echo "Setting the correct git remote..."
|
|
||||||
# Here, the git remote is a local file path by default. Let's change it to the upstream repo.
|
|
||||||
git remote set-url upstream https://github.com/frappe/lms.git
|
|
||||||
|
|
||||||
echo "Creating a new branch..."
|
|
||||||
isodate=$(date -u +"%Y-%m-%d")
|
|
||||||
branch_name="pot_${BASE_BRANCH}_${isodate}"
|
|
||||||
git checkout -b "${branch_name}"
|
|
||||||
|
|
||||||
echo "Commiting changes..."
|
|
||||||
git add lms/locale/main.pot
|
|
||||||
git commit -m "chore: update POT file"
|
|
||||||
|
|
||||||
gh auth setup-git
|
|
||||||
git push -u upstream "${branch_name}"
|
|
||||||
|
|
||||||
echo "Creating a PR..."
|
|
||||||
gh pr create --fill --base "${BASE_BRANCH}" --head "${branch_name}" -R frappe/lms
|
|
||||||
BIN
.github/hero.png
vendored
|
Before Width: | Height: | Size: 2.0 MiB |
BIN
.github/lms-logo.png
vendored
|
Before Width: | Height: | Size: 5.4 KiB |
BIN
.github/quiz.png
vendored
|
Before Width: | Height: | Size: 1.0 MiB |
32
.github/try-on-f-cloud.svg
vendored
@@ -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 |
64
.github/workflows/build.yml
vendored
@@ -1,64 +0,0 @@
|
|||||||
name: Build Container Image
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
tags:
|
|
||||||
- "*"
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
arch: [amd64, arm64]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
packages: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout Entire Repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
with:
|
|
||||||
platforms: linux/${{ matrix.arch }}
|
|
||||||
- name: Login to GitHub Container Registry
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Set Branch
|
|
||||||
run: |
|
|
||||||
export APPS_JSON='[{"url": "https://github.com/frappe/lms","branch": "main"}]'
|
|
||||||
echo "APPS_JSON_BASE64=$(echo $APPS_JSON | base64 -w 0)" >> $GITHUB_ENV
|
|
||||||
echo "FRAPPE_BRANCH=version-15" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Set Image Tag
|
|
||||||
run: |
|
|
||||||
echo "IMAGE_TAG=stable" >> $GITHUB_ENV
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
repository: frappe/frappe_docker
|
|
||||||
path: builds
|
|
||||||
|
|
||||||
- name: Build and push
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
push: true
|
|
||||||
context: builds
|
|
||||||
file: builds/images/layered/Containerfile
|
|
||||||
tags: >
|
|
||||||
ghcr.io/${{ github.repository }}:${{ github.ref_name }},
|
|
||||||
ghcr.io/${{ github.repository }}:${{ env.IMAGE_TAG }}
|
|
||||||
build-args: |
|
|
||||||
"FRAPPE_BRANCH=${{ env.FRAPPE_BRANCH }}"
|
|
||||||
"APPS_JSON_BASE64=${{ env.APPS_JSON_BASE64 }}"
|
|
||||||
13
.github/workflows/ci.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Server Tests
|
name: Run tests
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
@@ -32,14 +32,14 @@ jobs:
|
|||||||
- name: setup python
|
- name: setup python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: '3.10'
|
python-version: '3.9'
|
||||||
- name: setup node
|
- name: setup node
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '14'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: setup cache for bench
|
- name: setup cache for bench
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v2
|
||||||
with:
|
with:
|
||||||
path: ~/bench-cache
|
path: ~/bench-cache
|
||||||
key: ${{ runner.os }}
|
key: ${{ runner.os }}
|
||||||
@@ -65,7 +65,7 @@ jobs:
|
|||||||
run: bench new-site --mariadb-root-password root --admin-password admin frappe.local
|
run: bench new-site --mariadb-root-password root --admin-password admin frappe.local
|
||||||
- name: install lms app
|
- name: install lms app
|
||||||
working-directory: /home/runner/frappe-bench
|
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
|
- name: setup requirements
|
||||||
working-directory: /home/runner/frappe-bench
|
working-directory: /home/runner/frappe-bench
|
||||||
run: bench setup requirements --dev
|
run: bench setup requirements --dev
|
||||||
@@ -77,4 +77,5 @@ jobs:
|
|||||||
run: bench --site frappe.local build
|
run: bench --site frappe.local build
|
||||||
- name: run tests
|
- name: run tests
|
||||||
working-directory: /home/runner/frappe-bench
|
working-directory: /home/runner/frappe-bench
|
||||||
run: bench --site frappe.local run-tests --app lms
|
run: bench --site frappe.local run-tests --app lms
|
||||||
|
|
||||||
|
|||||||
34
.github/workflows/generate-pot-file.yml
vendored
@@ -1,34 +0,0 @@
|
|||||||
name: Regenerate POT file (translatable strings)
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: "00 16 * * 5"
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
regenerate-pot-file:
|
|
||||||
name: Release
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
branch: ["develop"]
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: ${{ matrix.branch }}
|
|
||||||
|
|
||||||
- name: Setup Python
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: "3.12"
|
|
||||||
|
|
||||||
- name: Run script to update POT file
|
|
||||||
run: |
|
|
||||||
bash ${GITHUB_WORKSPACE}/.github/helper/update_pot_file.sh
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
|
||||||
BASE_BRANCH: ${{ matrix.branch }}
|
|
||||||
61
.github/workflows/linters.yml
vendored
@@ -1,61 +0,0 @@
|
|||||||
name: Linters
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
branches: [ main ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
commit-lint:
|
|
||||||
name: 'Semantic Commits'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.event_name == 'pull_request'
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 200
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
check-latest: true
|
|
||||||
|
|
||||||
- name: Check commit titles
|
|
||||||
run: |
|
|
||||||
npm install @commitlint/cli @commitlint/config-conventional
|
|
||||||
npx commitlint --verbose --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }}
|
|
||||||
|
|
||||||
linters:
|
|
||||||
name: Semgrep Rules
|
|
||||||
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: Cache pip
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: ~/.cache/pip
|
|
||||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-pip-
|
|
||||||
${{ runner.os }}-
|
|
||||||
|
|
||||||
- name: Install and Run Pre-commit
|
|
||||||
uses: pre-commit/action@v3.0.1
|
|
||||||
|
|
||||||
- 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
|
|
||||||
26
.github/workflows/make_release_pr.yml
vendored
@@ -1,26 +0,0 @@
|
|||||||
name: Create weekly release
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '30 4 15 * *'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
name: Release
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: octokit/request-action@v2.x
|
|
||||||
with:
|
|
||||||
route: POST /repos/{owner}/{repo}/pulls
|
|
||||||
owner: frappe
|
|
||||||
repo: lms
|
|
||||||
title: |-
|
|
||||||
"chore: merge 'develop' into 'main'"
|
|
||||||
body: "Automated weekly release"
|
|
||||||
base: main
|
|
||||||
head: develop
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
|
||||||
32
.github/workflows/on_release.yml
vendored
@@ -1,32 +0,0 @@
|
|||||||
name: Generate Semantic Release
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
name: Release
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout Entire Repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
persist-credentials: false
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
- name: Setup dependencies
|
|
||||||
run: |
|
|
||||||
npm install @semantic-release/git @semantic-release/exec --no-save
|
|
||||||
- name: Create Release
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
|
||||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
|
||||||
GIT_AUTHOR_NAME: "Frappe PR Bot"
|
|
||||||
GIT_AUTHOR_EMAIL: "developers@frappe.io"
|
|
||||||
GIT_COMMITTER_NAME: "Frappe PR Bot"
|
|
||||||
GIT_COMMITTER_EMAIL: "developers@frappe.io"
|
|
||||||
run: npx semantic-release
|
|
||||||
39
.github/workflows/release_notes.yml
vendored
@@ -1,39 +0,0 @@
|
|||||||
# This action:
|
|
||||||
#
|
|
||||||
# 1. Generates release notes using github API.
|
|
||||||
# 2. Strips unnecessary info like chore/style etc from notes.
|
|
||||||
# 3. Updates release info.
|
|
||||||
|
|
||||||
name: 'Release Notes'
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
tag_name:
|
|
||||||
description: 'Tag of release like v2.0.0'
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
release:
|
|
||||||
types: [released]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
regen-notes:
|
|
||||||
name: 'Regenerate release notes'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Update notes
|
|
||||||
run: |
|
|
||||||
NEW_NOTES=$(gh api --method POST -H "Accept: application/vnd.github+json" /repos/frappe/lms/releases/generate-notes -f tag_name=$RELEASE_TAG \
|
|
||||||
| jq -r '.body' \
|
|
||||||
| sed -E '/^\* (chore|ci|test|docs|style)/d' \
|
|
||||||
| sed -E 's/by @mergify //'
|
|
||||||
)
|
|
||||||
RELEASE_ID=$(gh api -H "Accept: application/vnd.github+json" /repos/frappe/lms/releases/tags/$RELEASE_TAG | jq -r '.id')
|
|
||||||
gh api --method PATCH -H "Accept: application/vnd.github+json" /repos/frappe/lms/releases/$RELEASE_ID -f body="$NEW_NOTES"
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
|
||||||
RELEASE_TAG: ${{ github.event.inputs.tag_name || github.event.release.tag_name }}
|
|
||||||
19
.github/workflows/semantic.yml
vendored
@@ -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
|
|
||||||
123
.github/workflows/ui-tests.yml
vendored
@@ -1,123 +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.8
|
|
||||||
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@v4
|
|
||||||
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@v4
|
|
||||||
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@v4
|
|
||||||
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
|
|
||||||
bench --site lms.test set-password frappe@example.com admin
|
|
||||||
bench --site lms.test execute lms.lms.utils.persona_captured
|
|
||||||
|
|
||||||
- name: cypress pre-requisites
|
|
||||||
run: |
|
|
||||||
cd ~/frappe-bench/apps/lms
|
|
||||||
yarn add cypress@^10 --no-lockfile -W
|
|
||||||
|
|
||||||
- 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
|
|
||||||
5
.gitignore
vendored
@@ -8,8 +8,3 @@ lms/public/dist
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
node_modules
|
|
||||||
package-lock.json
|
|
||||||
lms/public/frontend
|
|
||||||
lms/www/lms.html
|
|
||||||
frappe-ui
|
|
||||||
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
|||||||
[submodule "frappe-ui"]
|
|
||||||
path = frappe-ui
|
|
||||||
url = https://github.com/frappe/frappe-ui
|
|
||||||
@@ -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, vue]
|
|
||||||
# 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
|
|
||||||
21
.releaserc
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"branches": ["develop"],
|
|
||||||
"plugins": [
|
|
||||||
"@semantic-release/commit-analyzer", {
|
|
||||||
"preset": "angular"
|
|
||||||
},
|
|
||||||
"@semantic-release/release-notes-generator",
|
|
||||||
[
|
|
||||||
"@semantic-release/exec", {
|
|
||||||
"prepareCmd": 'sed -ir "s/[0-9]*\.[0-9]*\.[0-9]*/${nextRelease.version}/" lms/__init__.py'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"@semantic-release/git", {
|
|
||||||
"assets": ["lms/__init__.py"],
|
|
||||||
"message": "chore(release): Bumped to Version ${nextRelease.version}"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"@semantic-release/github"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
175
README.md
@@ -1,174 +1,25 @@
|
|||||||
<div align="center" markdown="1">
|
## LMS
|
||||||
|
|
||||||
<img src=".github/lms-logo.png" alt="Frappe Learning logo" width="80" height="80"/>
|
Create online courses without much hassle.
|
||||||
<h1>Frappe Learning</h1>
|
|
||||||
|
|
||||||
**Easy to use, open source, Learning Management System**
|

|
||||||
|
|
||||||

|
## Features
|
||||||
|
|
||||||
</div>
|
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.
|
||||||
<div align="center">
|
|
||||||
<img src=".github/hero.png?v=5" alt="Hero Image" width="72%" />
|
|
||||||
</div>
|
|
||||||
<br />
|
|
||||||
<div align="center">
|
|
||||||
<a href="https://frappe.io/learning">Website</a>
|
|
||||||
-
|
|
||||||
<a href="https://docs.frappe.io/learning">Documentation</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## Frappe Learning
|
|
||||||
Frappe Learning is an easy-to-use learning system that helps you bring structure to your content.
|
|
||||||
|
|
||||||
### Motivation
|
|
||||||
In 2021, we were looking for a Learning Management System to launch [Mon.School](https://mon.school) for FOSS United. We checked out Moodle, but it didn’t feel right. The forms were unnecessarily lengthy and the UI was confusing. It shouldn't be this hard to create a course right? So I started making a learning system for Mon.School which soon became a product in itself. The aim is to have a simple platform that anyone can use to launch a course of their own and make knowledge sharing easier.
|
|
||||||
|
|
||||||
### Key Features
|
|
||||||
|
|
||||||
- **Structured Learning**: Design a course with a 3-level hierarchy, where your courses have chapters and you can group your lessons within these chapters. This ensures that the context of the lesson is set by the chapter.
|
|
||||||
|
|
||||||
- **Live Classes**: Group learners into batches based on courses and duration. You can then create Zoom live class for these batches right from the app. Learners get to see the list of live classes they have to take as a part of this batch.
|
|
||||||
|
|
||||||
- **Quizzes and Assignments**: Create quizzes where questions can have single-choice, multiple-choice options, or can be open ended. Instructors can also add assignments which learners can submit as PDF's or Documents.
|
|
||||||
|
|
||||||
- **Getting Certified**: Once a learner has completed the course or batch, you can grant them a certificate. The app provides an inbuilt certificate template. You can use this or else create a template of your own and use that instead.
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>View Screenshots</summary>
|
|
||||||
|
|
||||||
|
|
||||||

|
|
||||||
<div align="center">
|
|
||||||
<sub>
|
|
||||||
Create batches to group your learners
|
|
||||||
</sub>
|
|
||||||
</div>
|
|
||||||
<br>
|
|
||||||
|
|
||||||
|
|
||||||

|
|
||||||
<div align="center">
|
|
||||||
<sub>
|
|
||||||
Evaluate their knowledge by quizzes
|
|
||||||
</sub>
|
|
||||||
</div>
|
|
||||||
<br>
|
|
||||||
|
|
||||||
|
|
||||||

|
|
||||||
<div align="center">
|
|
||||||
<sub>
|
|
||||||
Autenticate their work with certification
|
|
||||||
</sub>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
|
|
||||||
### Under the Hood
|
|
||||||
|
|
||||||
- [**Frappe Framework**](https://github.com/frappe/frappe): A full-stack web application framework.
|
|
||||||
|
|
||||||
- [**Frappe UI**](https://github.com/frappe/frappe-ui): A Vue-based UI library, to provide a modern user interface.
|
|
||||||
|
|
||||||
## Production Setup
|
|
||||||
|
|
||||||
### Managed Hosting
|
|
||||||
|
|
||||||
You can try [Frappe Cloud](https://frappecloud.com), a simple, user-friendly and sophisticated [open-source](https://github.com/frappe/press) platform to host Frappe applications with peace of mind.
|
|
||||||
|
|
||||||
It takes care of installation, setup, upgrades, monitoring, maintenance and support of your Frappe deployments. It is a fully featured developer platform with an ability to manage and control multiple Frappe deployments.
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<a href="https://frappecloud.com/lms/signup" target="_blank">
|
|
||||||
<picture>
|
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://frappe.io/files/try-on-fc-white.png">
|
|
||||||
<img src="https://frappe.io/files/try-on-fc-black.png" alt="Try on Frappe Cloud" height="28" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
### Self Hosting
|
|
||||||
|
|
||||||
Follow these steps to set up Frappe Learning in production:
|
|
||||||
|
|
||||||
**Step 1**: Download the easy install script
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wget https://frappe.io/easy-install.py
|
|
||||||
```
|
|
||||||
|
|
||||||
**Step 2**: Run the deployment command
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 ./easy-install.py deploy \
|
|
||||||
--project=learning_prod_setup \
|
|
||||||
--email=your_email.example.com \
|
|
||||||
--image=ghcr.io/frappe/lms \
|
|
||||||
--version=stable \
|
|
||||||
--app=lms \
|
|
||||||
--sitename subdomain.domain.tld
|
|
||||||
```
|
|
||||||
|
|
||||||
Replace the following parameters with your values:
|
|
||||||
- `your_email.example.com`: Your email address
|
|
||||||
- `subdomain.domain.tld`: Your domain name where Learning will be hosted
|
|
||||||
|
|
||||||
The script will set up a production-ready instance of Frappe Learning with all the necessary configurations in about 5 minutes.
|
|
||||||
|
|
||||||
## Development Setup
|
## Development Setup
|
||||||
|
|
||||||
### Docker
|
1. [Through Docker](docker-installation.md)
|
||||||
|
1. [Direct install through bench](bench-installation.md)
|
||||||
|
|
||||||
You need Docker, docker-compose and git setup on your machine. Refer [Docker documentation](https://docs.docker.com/). After that, follow below steps:
|
|
||||||
|
|
||||||
**Step 1**: Setup folder and download the required files
|
### Contributing
|
||||||
|
|
||||||
mkdir frappe-learning
|
1. [Contribution Guidelines](Contribution.md)
|
||||||
cd frappe-learning
|
|
||||||
|
|
||||||
# Download the docker-compose file
|
## License
|
||||||
wget -O docker-compose.yml https://raw.githubusercontent.com/frappe/lms/develop/docker/docker-compose.yml
|
|
||||||
|
|
||||||
# Download the setup script
|
[GNU AFFERO GENERAL PUBLIC LICENSE](license.txt)
|
||||||
wget -O init.sh https://raw.githubusercontent.com/frappe/lms/develop/docker/init.sh
|
|
||||||
|
|
||||||
**Step 2**: Run the container and daemonize it
|
|
||||||
|
|
||||||
docker compose up -d
|
|
||||||
|
|
||||||
**Step 3**: The site [http://lms.localhost:8000/lms](http://lms.localhost:8000/lms) should now be available. The default credentials are:
|
|
||||||
- Username: Administrator
|
|
||||||
- Password: admin
|
|
||||||
|
|
||||||
### Local
|
|
||||||
|
|
||||||
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 learning.test`
|
|
||||||
1. Map your site to localhost with the command `bench --site learning.test add-to-hosts`
|
|
||||||
1. Get the Learning app. Run `bench get-app https://github.com/frappe/lms`
|
|
||||||
1. Run `bench --site learning.test install-app lms`.
|
|
||||||
1. Now open the URL `http://learning.test:8000/lms` in your browser, you should see the app running
|
|
||||||
|
|
||||||
## Learn and connect
|
|
||||||
|
|
||||||
- [Telegram Public Group](https://t.me/frappelms)
|
|
||||||
- [Discuss Forum](https://discuss.frappe.io/c/lms/70)
|
|
||||||
- [Documentation](https://docs.frappe.io/learning)
|
|
||||||
- [YouTube](https://www.youtube.com/channel/UCn3bV5kx77HsVwtnlCeEi_A)
|
|
||||||
|
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
<div align="center" style="padding-top: 0.75rem;">
|
|
||||||
<a href="https://frappe.io" target="_blank">
|
|
||||||
<picture>
|
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://frappe.io/files/Frappe-white.png">
|
|
||||||
<img src="https://frappe.io/files/Frappe-black.png" alt="Frappe Technologies" height="28"/>
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
# Security Policy
|
|
||||||
|
|
||||||
The Frappe team and community take security issues seriously. To report a security issue, please go through the information mentioned [here](https://frappe.io/security).
|
|
||||||
|
|
||||||
We appreciate your efforts to responsibly disclose your findings. We'll endeavor to respond quickly and will keep you updated throughout the process.
|
|
||||||
@@ -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. 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. 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. 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 get-app <url-of-your-form>.
|
||||||
1. Run bench --site lms.test install-app lms.
|
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```
|
1. Map your site to localhost with the command ```bench --site lms.test add-to-hosts```
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
export default {
|
|
||||||
parserPreset: "conventional-changelog-conventionalcommits",
|
|
||||||
rules: {
|
|
||||||
"subject-empty": [2, "never"],
|
|
||||||
"type-case": [2, "always", "lower-case"],
|
|
||||||
"type-empty": [2, "never"],
|
|
||||||
"type-enum": [
|
|
||||||
2,
|
|
||||||
"always",
|
|
||||||
[
|
|
||||||
"build",
|
|
||||||
"chore",
|
|
||||||
"ci",
|
|
||||||
"docs",
|
|
||||||
"feat",
|
|
||||||
"fix",
|
|
||||||
"perf",
|
|
||||||
"refactor",
|
|
||||||
"revert",
|
|
||||||
"style",
|
|
||||||
"test",
|
|
||||||
"deprecate", // deprecation decision
|
|
||||||
],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
files:
|
|
||||||
- source: /lms/locale/main.pot
|
|
||||||
translation: /lms/locale/%two_letters_code%.po
|
|
||||||
pull_request_title: "chore: sync translations from crowdin"
|
|
||||||
pull_request_labels:
|
|
||||||
- translation
|
|
||||||
commit_message: "chore: %language% translations"
|
|
||||||
append_commit_message: false
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { defineConfig } from "cypress";
|
|
||||||
|
|
||||||
export default 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://pertest:8000",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
describe("Batch Creation", () => {
|
|
||||||
it("creates a new batch", () => {
|
|
||||||
cy.login();
|
|
||||||
cy.wait(500);
|
|
||||||
cy.visit("/lms/batches");
|
|
||||||
cy.closeOnboardingModal();
|
|
||||||
|
|
||||||
// Open Settings
|
|
||||||
cy.get("span").contains("Learning").click();
|
|
||||||
cy.get("span").contains("Settings").click();
|
|
||||||
|
|
||||||
// Add a new member
|
|
||||||
cy.get('[id^="headlessui-dialog-panel-v-"]')
|
|
||||||
.find("span")
|
|
||||||
.contains(/^Members$/)
|
|
||||||
.click();
|
|
||||||
cy.get('[id^="headlessui-dialog-panel-v-"]')
|
|
||||||
.find("button")
|
|
||||||
.contains("New")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
const dateNow = Date.now();
|
|
||||||
const randomEmail = `testuser_${dateNow}@example.com`;
|
|
||||||
const randomName = `Test User ${dateNow}`;
|
|
||||||
|
|
||||||
cy.get("input[placeholder='Email']").type(randomEmail);
|
|
||||||
cy.get("input[placeholder='First Name']").type(randomName);
|
|
||||||
cy.get("button").contains("Add").click();
|
|
||||||
|
|
||||||
// Add evaluator
|
|
||||||
cy.get('[id^="headlessui-dialog-panel-v-"]')
|
|
||||||
.find("span")
|
|
||||||
.contains(/^Evaluators$/)
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.get('[id^="headlessui-dialog-panel-v-"]')
|
|
||||||
.find("button")
|
|
||||||
.contains("New")
|
|
||||||
.click();
|
|
||||||
const randomEvaluator = `evaluator${dateNow}@example.com`;
|
|
||||||
|
|
||||||
cy.get("input[placeholder='Email']").type(randomEvaluator);
|
|
||||||
cy.get("button").contains("Add").click();
|
|
||||||
cy.get("div").contains(randomEvaluator).should("be.visible").click();
|
|
||||||
|
|
||||||
cy.visit("/lms/batches");
|
|
||||||
cy.closeOnboardingModal();
|
|
||||||
|
|
||||||
// Create a batch
|
|
||||||
cy.get("button").contains("New").click();
|
|
||||||
cy.wait(500);
|
|
||||||
cy.url().should("include", "/batches/new/edit");
|
|
||||||
cy.get("label").contains("Title").type("Test Batch");
|
|
||||||
|
|
||||||
cy.get("label").contains("Start Date").type("2030-10-01");
|
|
||||||
cy.get("label").contains("End Date").type("2030-10-31");
|
|
||||||
cy.get("label").contains("Start Time").type("10:00");
|
|
||||||
cy.get("label").contains("End Time").type("11:00");
|
|
||||||
cy.get("label").contains("Timezone").type("IST");
|
|
||||||
cy.get("label").contains("Seat Count").type("10");
|
|
||||||
cy.get("label").contains("Published").click();
|
|
||||||
|
|
||||||
cy.get("label")
|
|
||||||
.contains("Short Description")
|
|
||||||
.type("Test Batch Short Description to test the UI");
|
|
||||||
cy.get("div[contenteditable=true").invoke(
|
|
||||||
"text",
|
|
||||||
"Test Batch Description. I need a very big description to test the UI. This is a very big description. 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."
|
|
||||||
);
|
|
||||||
|
|
||||||
/* Instructor */
|
|
||||||
cy.get("label")
|
|
||||||
.contains("Instructors")
|
|
||||||
.parent()
|
|
||||||
.within(() => {
|
|
||||||
cy.get("input").click().type("evaluator");
|
|
||||||
cy.get("input")
|
|
||||||
.invoke("attr", "aria-controls")
|
|
||||||
.as("instructor_list_id");
|
|
||||||
});
|
|
||||||
cy.get("@instructor_list_id").then((instructor_list_id) => {
|
|
||||||
cy.get(`[id^=${instructor_list_id}`)
|
|
||||||
.should("be.visible")
|
|
||||||
.within(() => {
|
|
||||||
cy.get("[id^=headlessui-combobox-option-").first().click();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.button("Save").click();
|
|
||||||
cy.wait(1000);
|
|
||||||
let batchName;
|
|
||||||
cy.url().then((url) => {
|
|
||||||
console.log(url);
|
|
||||||
batchName = url.split("/").pop();
|
|
||||||
cy.wrap(batchName).as("batchName");
|
|
||||||
});
|
|
||||||
cy.wait(500);
|
|
||||||
|
|
||||||
// View Batch
|
|
||||||
cy.wait(1000);
|
|
||||||
cy.visit("/lms/batches");
|
|
||||||
cy.closeOnboardingModal();
|
|
||||||
|
|
||||||
cy.url().should("include", "/lms/batches");
|
|
||||||
|
|
||||||
cy.get('[id^="headlessui-radiogroup-v-"]')
|
|
||||||
.find("span")
|
|
||||||
.contains("Upcoming")
|
|
||||||
.should("be.visible")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.get("@batchName").then((batchName) => {
|
|
||||||
cy.get(`a[href='/lms/batches/details/${batchName}'`).within(() => {
|
|
||||||
cy.get("div").contains("Test Batch").should("be.visible");
|
|
||||||
cy.get("div")
|
|
||||||
.contains("Test Batch Short Description to test the UI")
|
|
||||||
.should("be.visible");
|
|
||||||
cy.get("span")
|
|
||||||
.contains("01 Oct 2030 - 31 Oct 2030")
|
|
||||||
.should("be.visible");
|
|
||||||
cy.get("span")
|
|
||||||
.contains("10:00 AM - 11:00 AM")
|
|
||||||
.should("be.visible");
|
|
||||||
cy.get("span").contains("IST").should("be.visible");
|
|
||||||
cy.get("a").contains("Evaluator").should("be.visible");
|
|
||||||
cy.get("div")
|
|
||||||
.contains("10")
|
|
||||||
.should("be.visible")
|
|
||||||
.get("span")
|
|
||||||
.contains("Seats Left")
|
|
||||||
.should("be.visible");
|
|
||||||
});
|
|
||||||
cy.get(`a[href='/lms/batches/details/${batchName}'`).click();
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.get("div").contains("Test Batch").should("be.visible");
|
|
||||||
cy.get("div")
|
|
||||||
.contains("Test Batch Short Description to test the UI")
|
|
||||||
.should("be.visible");
|
|
||||||
cy.get("a").contains("Evaluator").should("be.visible");
|
|
||||||
cy.get("span")
|
|
||||||
.contains("01 Oct 2030 - 31 Oct 2030")
|
|
||||||
.should("be.visible");
|
|
||||||
cy.get("span").contains("10:00 AM - 11:00 AM").should("be.visible");
|
|
||||||
cy.get("span").contains("IST").should("be.visible");
|
|
||||||
cy.get("div")
|
|
||||||
.contains("10")
|
|
||||||
.should("be.visible")
|
|
||||||
.get("span")
|
|
||||||
.contains("Seats Left")
|
|
||||||
.should("be.visible");
|
|
||||||
|
|
||||||
cy.get("p")
|
|
||||||
.contains(
|
|
||||||
"Test Batch Description. I need a very big description to test the UI. This is a very big description. 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."
|
|
||||||
)
|
|
||||||
.should("be.visible");
|
|
||||||
cy.get("button").contains("Manage Batch").click();
|
|
||||||
|
|
||||||
/* Add student to batch */
|
|
||||||
cy.get("button").contains("Add").click();
|
|
||||||
cy.get('div[id^="headlessui-dialog-panel-v-"]')
|
|
||||||
.first()
|
|
||||||
.find("button")
|
|
||||||
.eq(1)
|
|
||||||
.click();
|
|
||||||
cy.get("input[id^='headlessui-combobox-input-v-']").type(randomEmail);
|
|
||||||
cy.get("div").contains(randomEmail).click();
|
|
||||||
cy.get("button").contains("Submit").click();
|
|
||||||
|
|
||||||
// Verify Seat Count
|
|
||||||
cy.get("span").contains("Details").click();
|
|
||||||
cy.get("div")
|
|
||||||
.contains("9")
|
|
||||||
.should("be.visible")
|
|
||||||
.get("span")
|
|
||||||
.contains("Seats Left")
|
|
||||||
.should("be.visible");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
describe("Course Creation", () => {
|
|
||||||
it("creates a new course", () => {
|
|
||||||
cy.login();
|
|
||||||
cy.wait(500);
|
|
||||||
cy.visit("/lms/courses");
|
|
||||||
|
|
||||||
// Close onboarding modal
|
|
||||||
cy.closeOnboardingModal();
|
|
||||||
|
|
||||||
// Create a course
|
|
||||||
cy.get("button").contains("New").click();
|
|
||||||
cy.wait(500);
|
|
||||||
cy.url().should("include", "/courses/new/edit");
|
|
||||||
|
|
||||||
cy.get("label").contains("Title").type("Test Course");
|
|
||||||
cy.get("label")
|
|
||||||
.contains("Short Introduction")
|
|
||||||
.type("Test Course Short Introduction to test the UI");
|
|
||||||
cy.get("div[contenteditable=true").invoke(
|
|
||||||
"text",
|
|
||||||
"Test Course Description. I need a very big description to test the UI. This is a very big description. It contains more than once sentence. Its meant to be this long as this is a UI test. Its unbearably long and I'm not sure why I'm typing this much. I'm just going to keep typing until I feel like its long enough. I think its long enough now. I'm going to stop typing now."
|
|
||||||
);
|
|
||||||
|
|
||||||
cy.fixture("profile.png", "base64").then((fileContent) => {
|
|
||||||
cy.get("div")
|
|
||||||
.contains("Course Image")
|
|
||||||
.siblings("div")
|
|
||||||
.children('input[type="file"]')
|
|
||||||
.attachFile({
|
|
||||||
fileContent,
|
|
||||||
fileName: "profile.png",
|
|
||||||
mimeType: "image/png",
|
|
||||||
encoding: "base64",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.get("label")
|
|
||||||
.contains("Preview Video")
|
|
||||||
.type("https://www.youtube.com/embed/-LPmw2Znl2c");
|
|
||||||
cy.get("[id=tags]").type("Learning{enter}Frappe{enter}ERPNext{enter}");
|
|
||||||
cy.get("label")
|
|
||||||
.contains("Category")
|
|
||||||
.parent()
|
|
||||||
.within(() => {
|
|
||||||
cy.get("button").click();
|
|
||||||
});
|
|
||||||
cy.get("[id^=headlessui-combobox-option-")
|
|
||||||
.should("be.visible")
|
|
||||||
.first()
|
|
||||||
.click();
|
|
||||||
|
|
||||||
/* Instructor */
|
|
||||||
cy.get("label")
|
|
||||||
.contains("Instructors")
|
|
||||||
.parent()
|
|
||||||
.within(() => {
|
|
||||||
cy.get("input").click().type("frappe");
|
|
||||||
cy.get("input")
|
|
||||||
.invoke("attr", "aria-controls")
|
|
||||||
.as("instructor_list_id");
|
|
||||||
});
|
|
||||||
cy.get("@instructor_list_id").then((instructor_list_id) => {
|
|
||||||
cy.get(`[id^=${instructor_list_id}`)
|
|
||||||
.should("be.visible")
|
|
||||||
.within(() => {
|
|
||||||
cy.get("[id^=headlessui-combobox-option-").first().click();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.get("label").contains("Published").click();
|
|
||||||
cy.get("label").contains("Published On").type("2021-01-01");
|
|
||||||
cy.button("Save").click();
|
|
||||||
|
|
||||||
// Add Chapter
|
|
||||||
cy.wait(1000);
|
|
||||||
cy.button("Add Chapter").click();
|
|
||||||
|
|
||||||
cy.wait(1000);
|
|
||||||
cy.get("[id^=headlessui-dialog-panel-")
|
|
||||||
.should("be.visible")
|
|
||||||
.within(() => {
|
|
||||||
cy.get("label").contains("Title").type("Test Chapter");
|
|
||||||
cy.button("Create").click();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add Lesson
|
|
||||||
cy.wait(1000);
|
|
||||||
cy.button("Add Lesson").click();
|
|
||||||
cy.wait(1000);
|
|
||||||
cy.url().should("include", "/learn/1-1/edit");
|
|
||||||
cy.wait(1000);
|
|
||||||
|
|
||||||
cy.get("label").contains("Title").type("Test Lesson");
|
|
||||||
cy.get("#content .ce-block").type(
|
|
||||||
"{enter}This is an extremely big paragraph that is meant to test the UI. This is a very long paragraph. It contains more than once sentence. Its meant to be this long as this is a UI test. Its unbearably long and I'm not sure why I'm typing this much. I'm just going to keep typing until I feel like its long enough. I think its long enough now. I'm going to stop typing now."
|
|
||||||
);
|
|
||||||
cy.button("Save").click();
|
|
||||||
|
|
||||||
// View Course
|
|
||||||
cy.wait(1000);
|
|
||||||
cy.visit("/lms");
|
|
||||||
cy.closeOnboardingModal();
|
|
||||||
|
|
||||||
cy.url().should("include", "/lms/courses");
|
|
||||||
cy.get(".grid a:first").within(() => {
|
|
||||||
cy.get("div").contains("Test Course");
|
|
||||||
cy.get("div").contains(
|
|
||||||
"Test Course Short Introduction to test the UI"
|
|
||||||
);
|
|
||||||
cy.get(".course-image")
|
|
||||||
.invoke("css", "background-image")
|
|
||||||
.should("include", "/files/profile");
|
|
||||||
});
|
|
||||||
cy.get(".grid a:first").click();
|
|
||||||
cy.url().should("include", "/lms/courses/test-course");
|
|
||||||
cy.get("div").contains("Test Course");
|
|
||||||
cy.get("div").contains("Test Course Short Introduction to test the UI");
|
|
||||||
cy.get("div").contains("Learning");
|
|
||||||
cy.get("div").contains("Frappe");
|
|
||||||
cy.get("div").contains("ERPNext");
|
|
||||||
cy.get("iframe").should(
|
|
||||||
"have.attr",
|
|
||||||
"src",
|
|
||||||
"https://www.youtube.com/embed/-LPmw2Znl2c"
|
|
||||||
);
|
|
||||||
|
|
||||||
// View Chapter
|
|
||||||
cy.get("div").contains("Test Chapter");
|
|
||||||
cy.get("[id^=headlessui-disclosure-panel-").within(() => {
|
|
||||||
cy.get("div").contains("Test Lesson").click();
|
|
||||||
});
|
|
||||||
cy.wait(3000);
|
|
||||||
|
|
||||||
// View Lesson
|
|
||||||
cy.url().should("include", "/learn/1-1");
|
|
||||||
cy.get("div").contains("Test Lesson");
|
|
||||||
|
|
||||||
cy.get("div").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.button("New Question").click();
|
|
||||||
cy.wait(500);
|
|
||||||
cy.get("[id^=headlessui-dialog-panel-").within(() => {
|
|
||||||
cy.get("label").contains("Title").type("Test Discussion");
|
|
||||||
cy.get("div[contenteditable=true]").invoke(
|
|
||||||
"text",
|
|
||||||
"This is a test discussion. This will check if the UI is working properly."
|
|
||||||
);
|
|
||||||
cy.button("Post").click();
|
|
||||||
});
|
|
||||||
|
|
||||||
// View Discussion
|
|
||||||
cy.wait(500);
|
|
||||||
cy.get("div").contains("Test Discussion").click();
|
|
||||||
cy.get("div[contenteditable=true").invoke(
|
|
||||||
"text",
|
|
||||||
"This is a test comment. This will check if the UI is working properly."
|
|
||||||
);
|
|
||||||
|
|
||||||
cy.get("div").contains(
|
|
||||||
"This is a test comment. This will check if the UI is working properly."
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 1.2 MiB |
@@ -1,86 +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) => { ... })
|
|
||||||
|
|
||||||
import "cypress-file-upload";
|
|
||||||
import "cypress-real-events";
|
|
||||||
|
|
||||||
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 },
|
|
||||||
timeout: 60000,
|
|
||||||
retryOnStatusCodeFailure: true,
|
|
||||||
retryOnNetworkFailure: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
Cypress.Commands.add("paste", { prevSubject: true }, (subject, text) => {
|
|
||||||
cy.wrap(subject).then(($element) => {
|
|
||||||
const element = $element[0];
|
|
||||||
element.focus();
|
|
||||||
element.textContent = text;
|
|
||||||
const event = new Event("paste", { bubbles: true });
|
|
||||||
element.dispatchEvent(event);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
Cypress.Commands.add("closeOnboardingModal", () => {
|
|
||||||
cy.wait(500);
|
|
||||||
cy.get("body").then(($body) => {
|
|
||||||
// Check if any element with class including 'z-50' exists
|
|
||||||
if ($body.find('[class*="z-50"]').length > 0) {
|
|
||||||
cy.get('[class*="z-50"]')
|
|
||||||
.find('button:has(svg[class*="feather-x"])')
|
|
||||||
.realClick();
|
|
||||||
cy.wait(1000);
|
|
||||||
} else {
|
|
||||||
cy.log("Onboarding modal not found, skipping close.");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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
@@ -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: {}
|
||||||
@@ -4,8 +4,6 @@
|
|||||||
$ git clone https://github.com/frappe/lms.git
|
$ git clone https://github.com/frappe/lms.git
|
||||||
|
|
||||||
$ cd lms
|
$ cd lms
|
||||||
|
|
||||||
$ cd docker
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Step 2:** Run docker-compose
|
**Step 2:** Run docker-compose
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
version: "3.7"
|
|
||||||
name: lms
|
|
||||||
services:
|
|
||||||
mariadb:
|
|
||||||
image: mariadb:10.8
|
|
||||||
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:
|
|
||||||
@@ -1,40 +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://redis:6379
|
|
||||||
bench set-redis-queue-host redis://redis:6379
|
|
||||||
bench set-redis-socketio-host redis://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 use lms.localhost
|
|
||||||
|
|
||||||
bench start
|
|
||||||
5
frontend/.gitignore
vendored
@@ -1,5 +0,0 @@
|
|||||||
node_modules
|
|
||||||
.DS_Store
|
|
||||||
dist
|
|
||||||
dist-ssr
|
|
||||||
*.local
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"semi": false,
|
|
||||||
"singleQuote": true
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# Frappe UI Starter
|
|
||||||
|
|
||||||
This template should help get you started developing custom frontend for Frappe
|
|
||||||
apps with Vue 3 and the Frappe UI package.
|
|
||||||
|
|
||||||
This boilerplate sets up Vue 3, Vue Router, TailwindCSS, and Frappe UI out of
|
|
||||||
the box.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
This template is meant to be cloned inside an existing Frappe App. Assuming your
|
|
||||||
apps name is `todo`. Clone this template in the root folder of your app using `degit`.
|
|
||||||
|
|
||||||
```
|
|
||||||
cd apps/todo
|
|
||||||
npx degit netchampfaris/frappe-ui-starter frontend
|
|
||||||
cd frontend
|
|
||||||
yarn
|
|
||||||
yarn dev
|
|
||||||
```
|
|
||||||
|
|
||||||
In a development environment, you need to put the below key-value pair in your `site_config.json` file:
|
|
||||||
|
|
||||||
```
|
|
||||||
"ignore_csrf": 1
|
|
||||||
```
|
|
||||||
|
|
||||||
This will prevent `CSRFToken` errors while using the vite dev server. In production environment, the `csrf_token` is attached to the `window` object in `index.html` for you.
|
|
||||||
|
|
||||||
The Vite dev server will start on the port `8080`. This can be changed from `vite.config.js`.
|
|
||||||
The development server is configured to proxy your frappe app (usually running on port `8000`). If you have a site named `todo.test`, open `http://todo.test:8080` in your browser. If you see a button named "Click to send 'ping' request", congratulations!
|
|
||||||
|
|
||||||
If you notice the browser URL is `/frontend`, this is the base URL where your frontend app will run in production.
|
|
||||||
To change this, open `src/router.js` and change the base URL passed to `createWebHistory`.
|
|
||||||
|
|
||||||
## Resources
|
|
||||||
|
|
||||||
- [Vue 3](https://v3.vuejs.org/guide/introduction.html)
|
|
||||||
- [Vue Router](https://next.router.vuejs.org/guide/)
|
|
||||||
- [Frappe UI](https://github.com/frappe/frappe-ui)
|
|
||||||
- [TailwindCSS](https://tailwindcss.com/docs/utility-first)
|
|
||||||
- [Vite](https://vitejs.dev/guide/)
|
|
||||||
105
frontend/components.d.ts
vendored
@@ -1,105 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
// @ts-nocheck
|
|
||||||
// Generated by unplugin-vue-components
|
|
||||||
// Read more: https://github.com/vuejs/core/pull/3399
|
|
||||||
// biome-ignore lint: disable
|
|
||||||
export {}
|
|
||||||
|
|
||||||
/* prettier-ignore */
|
|
||||||
declare module 'vue' {
|
|
||||||
export interface GlobalComponents {
|
|
||||||
Annoucements: typeof import('./src/components/Annoucements.vue')['default']
|
|
||||||
AnnouncementModal: typeof import('./src/components/Modals/AnnouncementModal.vue')['default']
|
|
||||||
Apps: typeof import('./src/components/Apps.vue')['default']
|
|
||||||
AppSidebar: typeof import('./src/components/AppSidebar.vue')['default']
|
|
||||||
AssessmentModal: typeof import('./src/components/Modals/AssessmentModal.vue')['default']
|
|
||||||
AssessmentPlugin: typeof import('./src/components/AssessmentPlugin.vue')['default']
|
|
||||||
Assessments: typeof import('./src/components/Assessments.vue')['default']
|
|
||||||
Assignment: typeof import('./src/components/Assignment.vue')['default']
|
|
||||||
AssignmentForm: typeof import('./src/components/Modals/AssignmentForm.vue')['default']
|
|
||||||
AudioBlock: typeof import('./src/components/AudioBlock.vue')['default']
|
|
||||||
Autocomplete: typeof import('./src/components/Controls/Autocomplete.vue')['default']
|
|
||||||
BatchCard: typeof import('./src/components/BatchCard.vue')['default']
|
|
||||||
BatchCourseModal: typeof import('./src/components/Modals/BatchCourseModal.vue')['default']
|
|
||||||
BatchCourses: typeof import('./src/components/BatchCourses.vue')['default']
|
|
||||||
BatchDashboard: typeof import('./src/components/BatchDashboard.vue')['default']
|
|
||||||
BatchFeedback: typeof import('./src/components/BatchFeedback.vue')['default']
|
|
||||||
BatchOverlay: typeof import('./src/components/BatchOverlay.vue')['default']
|
|
||||||
BatchStudentProgress: typeof import('./src/components/Modals/BatchStudentProgress.vue')['default']
|
|
||||||
BatchStudents: typeof import('./src/components/BatchStudents.vue')['default']
|
|
||||||
BrandSettings: typeof import('./src/components/Settings/BrandSettings.vue')['default']
|
|
||||||
BulkCertificates: typeof import('./src/components/Modals/BulkCertificates.vue')['default']
|
|
||||||
Categories: typeof import('./src/components/Settings/Categories.vue')['default']
|
|
||||||
CertificationLinks: typeof import('./src/components/CertificationLinks.vue')['default']
|
|
||||||
ChapterModal: typeof import('./src/components/Modals/ChapterModal.vue')['default']
|
|
||||||
CodeEditor: typeof import('./src/components/Controls/CodeEditor.vue')['default']
|
|
||||||
CollapseSidebar: typeof import('./src/components/Icons/CollapseSidebar.vue')['default']
|
|
||||||
CourseCard: typeof import('./src/components/CourseCard.vue')['default']
|
|
||||||
CourseCardOverlay: typeof import('./src/components/CourseCardOverlay.vue')['default']
|
|
||||||
CourseInstructors: typeof import('./src/components/CourseInstructors.vue')['default']
|
|
||||||
CourseOutline: typeof import('./src/components/CourseOutline.vue')['default']
|
|
||||||
CourseReviews: typeof import('./src/components/CourseReviews.vue')['default']
|
|
||||||
CreateOutline: typeof import('./src/components/CreateOutline.vue')['default']
|
|
||||||
DateRange: typeof import('./src/components/Common/DateRange.vue')['default']
|
|
||||||
DesktopLayout: typeof import('./src/components/DesktopLayout.vue')['default']
|
|
||||||
DiscussionModal: typeof import('./src/components/Modals/DiscussionModal.vue')['default']
|
|
||||||
DiscussionReplies: typeof import('./src/components/DiscussionReplies.vue')['default']
|
|
||||||
Discussions: typeof import('./src/components/Discussions.vue')['default']
|
|
||||||
EditCoverImage: typeof import('./src/components/Modals/EditCoverImage.vue')['default']
|
|
||||||
EditProfile: typeof import('./src/components/Modals/EditProfile.vue')['default']
|
|
||||||
EmailTemplateModal: typeof import('./src/components/Modals/EmailTemplateModal.vue')['default']
|
|
||||||
EmailTemplates: typeof import('./src/components/Settings/EmailTemplates.vue')['default']
|
|
||||||
EmptyState: typeof import('./src/components/EmptyState.vue')['default']
|
|
||||||
EvaluationModal: typeof import('./src/components/Modals/EvaluationModal.vue')['default']
|
|
||||||
Evaluators: typeof import('./src/components/Settings/Evaluators.vue')['default']
|
|
||||||
Event: typeof import('./src/components/Modals/Event.vue')['default']
|
|
||||||
ExplanationVideos: typeof import('./src/components/Modals/ExplanationVideos.vue')['default']
|
|
||||||
FeedbackModal: typeof import('./src/components/Modals/FeedbackModal.vue')['default']
|
|
||||||
FrappeCloudIcon: typeof import('./src/components/Icons/FrappeCloudIcon.vue')['default']
|
|
||||||
IconPicker: typeof import('./src/components/Controls/IconPicker.vue')['default']
|
|
||||||
IndicatorIcon: typeof import('./src/components/Icons/IndicatorIcon.vue')['default']
|
|
||||||
InviteIcon: typeof import('./src/components/Icons/InviteIcon.vue')['default']
|
|
||||||
JobApplicationModal: typeof import('./src/components/Modals/JobApplicationModal.vue')['default']
|
|
||||||
JobCard: typeof import('./src/components/JobCard.vue')['default']
|
|
||||||
LessonContent: typeof import('./src/components/LessonContent.vue')['default']
|
|
||||||
LessonHelp: typeof import('./src/components/LessonHelp.vue')['default']
|
|
||||||
Link: typeof import('./src/components/Controls/Link.vue')['default']
|
|
||||||
LiveClass: typeof import('./src/components/LiveClass.vue')['default']
|
|
||||||
LiveClassAttendance: typeof import('./src/components/Modals/LiveClassAttendance.vue')['default']
|
|
||||||
LiveClassModal: typeof import('./src/components/Modals/LiveClassModal.vue')['default']
|
|
||||||
LMSLogo: typeof import('./src/components/Icons/LMSLogo.vue')['default']
|
|
||||||
Members: typeof import('./src/components/Settings/Members.vue')['default']
|
|
||||||
MobileLayout: typeof import('./src/components/MobileLayout.vue')['default']
|
|
||||||
MultiSelect: typeof import('./src/components/Controls/MultiSelect.vue')['default']
|
|
||||||
NoPermission: typeof import('./src/components/NoPermission.vue')['default']
|
|
||||||
NoSidebarLayout: typeof import('./src/components/NoSidebarLayout.vue')['default']
|
|
||||||
NotPermitted: typeof import('./src/components/NotPermitted.vue')['default']
|
|
||||||
PageModal: typeof import('./src/components/Modals/PageModal.vue')['default']
|
|
||||||
PaymentSettings: typeof import('./src/components/Settings/PaymentSettings.vue')['default']
|
|
||||||
Play: typeof import('./src/components/Icons/Play.vue')['default']
|
|
||||||
ProgressBar: typeof import('./src/components/ProgressBar.vue')['default']
|
|
||||||
Question: typeof import('./src/components/Modals/Question.vue')['default']
|
|
||||||
Quiz: typeof import('./src/components/Quiz.vue')['default']
|
|
||||||
QuizBlock: typeof import('./src/components/QuizBlock.vue')['default']
|
|
||||||
QuizInVideo: typeof import('./src/components/Modals/QuizInVideo.vue')['default']
|
|
||||||
Rating: typeof import('./src/components/Controls/Rating.vue')['default']
|
|
||||||
ReviewModal: typeof import('./src/components/Modals/ReviewModal.vue')['default']
|
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
|
||||||
SettingDetails: typeof import('./src/components/Settings/SettingDetails.vue')['default']
|
|
||||||
SettingFields: typeof import('./src/components/Settings/SettingFields.vue')['default']
|
|
||||||
Settings: typeof import('./src/components/Settings/Settings.vue')['default']
|
|
||||||
SidebarLink: typeof import('./src/components/SidebarLink.vue')['default']
|
|
||||||
StudentHeatmap: typeof import('./src/components/StudentHeatmap.vue')['default']
|
|
||||||
StudentModal: typeof import('./src/components/Modals/StudentModal.vue')['default']
|
|
||||||
Tags: typeof import('./src/components/Tags.vue')['default']
|
|
||||||
UnsplashImageBrowser: typeof import('./src/components/UnsplashImageBrowser.vue')['default']
|
|
||||||
UpcomingEvaluations: typeof import('./src/components/UpcomingEvaluations.vue')['default']
|
|
||||||
UploadPlugin: typeof import('./src/components/UploadPlugin.vue')['default']
|
|
||||||
UserAvatar: typeof import('./src/components/UserAvatar.vue')['default']
|
|
||||||
UserDropdown: typeof import('./src/components/UserDropdown.vue')['default']
|
|
||||||
VideoBlock: typeof import('./src/components/VideoBlock.vue')['default']
|
|
||||||
ZoomAccountModal: typeof import('./src/components/Modals/ZoomAccountModal.vue')['default']
|
|
||||||
ZoomSettings: typeof import('./src/components/Settings/ZoomSettings.vue')['default']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link rel="icon" href="{{ favicon }}" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>{{ title }}</title>
|
|
||||||
<meta name="title" content="{{ meta.title }}" />
|
|
||||||
<meta name="image" content="{{ meta.image }}" />
|
|
||||||
<meta name="description" content="{{ meta.description }}" />
|
|
||||||
<meta name="keywords" content="{{ meta.keywords }}" />
|
|
||||||
<meta property="og:title" content="{{ meta.title }}" />
|
|
||||||
<meta property="og:image" content="{{ meta.image }}" />
|
|
||||||
<meta property="og:description" content="{{ meta.description }}" />
|
|
||||||
<meta name="twitter:title" content="{{ meta.title }}" />
|
|
||||||
<meta name="twitter:image" content="{{ meta.image }}" />
|
|
||||||
<meta name="twitter:description" content="{{ meta.description }}" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app">
|
|
||||||
<div id="seo-content">
|
|
||||||
<h1>{{ meta.title }}</h1>
|
|
||||||
<p>
|
|
||||||
{{ meta.description }}
|
|
||||||
</p>
|
|
||||||
<a href="{{ meta.link }}">Know More</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
document.getElementById('seo-content').style.display = 'none';
|
|
||||||
</script>
|
|
||||||
<script type="module" src="/src/main.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "frappe-ui-frontend",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.0.0",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"serve": "vite preview",
|
|
||||||
"build": "vite build --base=/assets/lms/frontend/ && yarn copy-html-entry",
|
|
||||||
"copy-html-entry": "cp ../lms/public/frontend/index.html ../lms/www/lms.html"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@editorjs/checklist": "^1.6.0",
|
|
||||||
"@editorjs/code": "^2.9.0",
|
|
||||||
"@editorjs/editorjs": "^2.29.0",
|
|
||||||
"@editorjs/embed": "^2.7.0",
|
|
||||||
"@editorjs/header": "^2.8.1",
|
|
||||||
"@editorjs/inline-code": "^1.5.0",
|
|
||||||
"@editorjs/nested-list": "^1.4.2",
|
|
||||||
"@editorjs/paragraph": "^2.11.3",
|
|
||||||
"@editorjs/simple-image": "^1.6.0",
|
|
||||||
"@editorjs/table": "^2.4.2",
|
|
||||||
"@vueuse/router": "^12.7.0",
|
|
||||||
"ace-builds": "^1.36.2",
|
|
||||||
"apexcharts": "^4.3.0",
|
|
||||||
"chart.js": "^4.4.1",
|
|
||||||
"codemirror-editor-vue3": "^2.8.0",
|
|
||||||
"dayjs": "^1.11.6",
|
|
||||||
"feather-icons": "^4.28.0",
|
|
||||||
"frappe-ui": "^0.1.147",
|
|
||||||
"highlight.js": "^11.11.1",
|
|
||||||
"lucide-vue-next": "^0.383.0",
|
|
||||||
"markdown-it": "^14.0.0",
|
|
||||||
"pinia": "^2.0.33",
|
|
||||||
"plyr": "^3.7.8",
|
|
||||||
"socket.io-client": "^4.7.2",
|
|
||||||
"tailwindcss": "3.4.15",
|
|
||||||
"typescript": "^5.7.2",
|
|
||||||
"vue": "^3.4.23",
|
|
||||||
"vue-chartjs": "^5.3.0",
|
|
||||||
"vue-draggable-next": "^2.2.1",
|
|
||||||
"vue-router": "^4.0.12",
|
|
||||||
"vue3-apexcharts": "^1.8.0",
|
|
||||||
"vuedraggable": "4.1.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@vitejs/plugin-vue": "^5.0.3",
|
|
||||||
"autoprefixer": "^10.4.2",
|
|
||||||
"postcss": "^8.4.5",
|
|
||||||
"vite": "^5.0.11"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
export default {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 440 B |
@@ -1,4 +0,0 @@
|
|||||||
<svg width="80" height="79" viewBox="0 0 80 79" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M57.1285 0.580383H22.8514C10.2309 0.580383 0 10.5649 0 22.8815V56.3332C0 68.6497 10.2309 78.6343 22.8514 78.6343H57.1285C69.749 78.6343 79.9799 68.6497 79.9799 56.3332V22.8815C79.9799 10.5649 69.749 0.580383 57.1285 0.580383Z" fill="#0E7159"/>
|
|
||||||
<path d="M62.8434 23.6906L60.7869 23.1052C53.6744 21.0702 45.9048 22.4641 39.992 26.8128C35.8502 23.7742 30.7943 22.1854 25.7099 22.2133H17.1406V27.8163H25.7099C29.6232 27.8163 33.508 29.015 36.6787 31.3845L39.992 33.8377L43.3056 31.3845C47.2475 28.4575 52.3032 27.2588 57.1306 28.0393V50.647C51.1035 49.9223 44.9051 51.4834 39.992 55.0795C35.8502 52.0688 30.8515 50.4798 25.7671 50.4798C24.7959 50.4798 23.8247 50.5355 22.8535 50.647V35.0642H17.1406V57.0588H62.8434V23.7185V23.6906Z" fill="white"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 856 B |
@@ -1,55 +0,0 @@
|
|||||||
<template>
|
|
||||||
<FrappeUIProvider>
|
|
||||||
<Layout>
|
|
||||||
<router-view />
|
|
||||||
</Layout>
|
|
||||||
<Dialogs />
|
|
||||||
</FrappeUIProvider>
|
|
||||||
</template>
|
|
||||||
<script setup>
|
|
||||||
import { FrappeUIProvider } from 'frappe-ui'
|
|
||||||
import { Dialogs } from '@/utils/dialogs'
|
|
||||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
|
||||||
import { useScreenSize } from './utils/composables'
|
|
||||||
import DesktopLayout from './components/DesktopLayout.vue'
|
|
||||||
import MobileLayout from './components/MobileLayout.vue'
|
|
||||||
import NoSidebarLayout from './components/NoSidebarLayout.vue'
|
|
||||||
import { usersStore } from '@/stores/user'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { posthogSettings } from '@/telemetry'
|
|
||||||
|
|
||||||
const screenSize = useScreenSize()
|
|
||||||
const router = useRouter()
|
|
||||||
const noSidebar = ref(false)
|
|
||||||
const { userResource } = usersStore()
|
|
||||||
|
|
||||||
router.beforeEach((to, from, next) => {
|
|
||||||
if (to.query.fromLesson || to.path === '/persona') {
|
|
||||||
noSidebar.value = true
|
|
||||||
} else {
|
|
||||||
noSidebar.value = false
|
|
||||||
}
|
|
||||||
next()
|
|
||||||
})
|
|
||||||
|
|
||||||
const Layout = computed(() => {
|
|
||||||
if (noSidebar.value) {
|
|
||||||
return NoSidebarLayout
|
|
||||||
}
|
|
||||||
if (screenSize.width < 640) {
|
|
||||||
return MobileLayout
|
|
||||||
}
|
|
||||||
|
|
||||||
return DesktopLayout
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
noSidebar.value = false
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(userResource, () => {
|
|
||||||
if (userResource.data) {
|
|
||||||
posthogSettings.reload()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
@font-face {
|
|
||||||
font-family: 'Inter';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 100;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("Inter-Thin.woff2?v=3.12") format("woff2"),
|
|
||||||
url("Inter-Thin.woff?v=3.12") format("woff");
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 100;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("Inter-ThinItalic.woff2?v=3.12") format("woff2"),
|
|
||||||
url("Inter-ThinItalic.woff?v=3.12") format("woff");
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 200;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("Inter-ExtraLight.woff2?v=3.12") format("woff2"),
|
|
||||||
url("Inter-ExtraLight.woff?v=3.12") format("woff");
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 200;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("Inter-ExtraLightItalic.woff2?v=3.12") format("woff2"),
|
|
||||||
url("Inter-ExtraLightItalic.woff?v=3.12") format("woff");
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 300;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("Inter-Light.woff2?v=3.12") format("woff2"),
|
|
||||||
url("Inter-Light.woff?v=3.12") format("woff");
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 300;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("Inter-LightItalic.woff2?v=3.12") format("woff2"),
|
|
||||||
url("Inter-LightItalic.woff?v=3.12") format("woff");
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("Inter-Regular.woff2?v=3.12") format("woff2"),
|
|
||||||
url("Inter-Regular.woff?v=3.12") format("woff");
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("Inter-Italic.woff2?v=3.12") format("woff2"),
|
|
||||||
url("Inter-Italic.woff?v=3.12") format("woff");
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 500;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("Inter-Medium.woff2?v=3.12") format("woff2"),
|
|
||||||
url("Inter-Medium.woff?v=3.12") format("woff");
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 500;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("Inter-MediumItalic.woff2?v=3.12") format("woff2"),
|
|
||||||
url("Inter-MediumItalic.woff?v=3.12") format("woff");
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 600;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("Inter-SemiBold.woff2?v=3.12") format("woff2"),
|
|
||||||
url("Inter-SemiBold.woff?v=3.12") format("woff");
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 600;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("Inter-SemiBoldItalic.woff2?v=3.12") format("woff2"),
|
|
||||||
url("Inter-SemiBoldItalic.woff?v=3.12") format("woff");
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 700;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("Inter-Bold.woff2?v=3.12") format("woff2"),
|
|
||||||
url("Inter-Bold.woff?v=3.12") format("woff");
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 700;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("Inter-BoldItalic.woff2?v=3.12") format("woff2"),
|
|
||||||
url("Inter-BoldItalic.woff?v=3.12") format("woff");
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 800;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("Inter-ExtraBold.woff2?v=3.12") format("woff2"),
|
|
||||||
url("Inter-ExtraBold.woff?v=3.12") format("woff");
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 800;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("Inter-ExtraBoldItalic.woff2?v=3.12") format("woff2"),
|
|
||||||
url("Inter-ExtraBoldItalic.woff?v=3.12") format("woff");
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 900;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("Inter-Black.woff2?v=3.12") format("woff2"),
|
|
||||||
url("Inter-Black.woff?v=3.12") format("woff");
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 900;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("Inter-BlackItalic.woff2?v=3.12") format("woff2"),
|
|
||||||
url("Inter-BlackItalic.woff?v=3.12") format("woff");
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div v-if="communications.data?.length">
|
|
||||||
<div v-for="comm in communications.data">
|
|
||||||
<div class="mb-8">
|
|
||||||
<div class="flex items-center justify-between mb-2">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<Avatar :label="comm.sender_full_name" size="lg" />
|
|
||||||
<div class="ml-2 text-ink-gray-7">
|
|
||||||
{{ comm.sender_full_name }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-sm">
|
|
||||||
{{ timeAgo(comm.communication_date) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="prose prose-sm bg-surface-menu-bar !min-w-full px-4 py-2 rounded-md"
|
|
||||||
v-html="comm.content"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else class="text-sm italic text-ink-gray-5">
|
|
||||||
{{ __('No announcements') }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script setup>
|
|
||||||
import { createResource, Avatar } from 'frappe-ui'
|
|
||||||
import { timeAgo } from '@/utils'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
batch: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const communications = createResource({
|
|
||||||
url: 'lms.lms.api.get_announcements',
|
|
||||||
makeParams(value) {
|
|
||||||
return {
|
|
||||||
batch: props.batch,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
auto: true,
|
|
||||||
cache: ['announcement', props.batch],
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
<style>
|
|
||||||
.prose-sm p {
|
|
||||||
margin: 0 0 0.5rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,642 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
class="flex h-full flex-col justify-between transition-all duration-300 ease-in-out border-r bg-surface-menu-bar"
|
|
||||||
:class="sidebarStore.isSidebarCollapsed ? 'w-14' : 'w-56'"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="flex flex-col overflow-hidden"
|
|
||||||
:class="sidebarStore.isSidebarCollapsed ? 'items-center' : ''"
|
|
||||||
>
|
|
||||||
<UserDropdown :isCollapsed="sidebarStore.isSidebarCollapsed" />
|
|
||||||
<div class="flex flex-col" v-if="sidebarSettings.data">
|
|
||||||
<SidebarLink
|
|
||||||
v-for="link in sidebarLinks"
|
|
||||||
:link="link"
|
|
||||||
:isCollapsed="sidebarStore.isSidebarCollapsed"
|
|
||||||
class="mx-2 my-0.5"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="sidebarSettings.data?.web_pages?.length || isModerator"
|
|
||||||
class="mt-4"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="flex items-center justify-between pr-2 cursor-pointer"
|
|
||||||
:class="sidebarStore.isSidebarCollapsed ? 'pl-3' : 'pl-4'"
|
|
||||||
@click="toggleWebPages"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="!sidebarStore.isSidebarCollapsed"
|
|
||||||
class="flex items-center text-sm text-ink-gray-5 my-1"
|
|
||||||
>
|
|
||||||
<span class="grid h-5 w-6 flex-shrink-0 place-items-center">
|
|
||||||
<ChevronRight
|
|
||||||
class="h-4 w-4 stroke-1.5 text-ink-gray-9 transition-all duration-300 ease-in-out"
|
|
||||||
:class="{ 'rotate-90': !sidebarStore.isWebpagesCollapsed }"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<span class="ml-2">
|
|
||||||
{{ __('More') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
v-if="isModerator && !readOnlyMode"
|
|
||||||
variant="ghost"
|
|
||||||
@click="openPageModal()"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<Plus class="h-4 w-4 text-ink-gray-7 stroke-1.5" />
|
|
||||||
</template>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="sidebarSettings.data?.web_pages?.length"
|
|
||||||
class="flex flex-col transition-all duration-300 ease-in-out"
|
|
||||||
:class="!sidebarStore.isWebpagesCollapsed ? 'block' : 'hidden'"
|
|
||||||
>
|
|
||||||
<SidebarLink
|
|
||||||
v-for="link in sidebarSettings.data.web_pages"
|
|
||||||
:link="link"
|
|
||||||
:isCollapsed="sidebarStore.isSidebarCollapsed"
|
|
||||||
class="mx-2 my-0.5"
|
|
||||||
:showControls="isModerator ? true : false"
|
|
||||||
@openModal="openPageModal"
|
|
||||||
@deletePage="deletePage"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="m-2 flex flex-col gap-1">
|
|
||||||
<div
|
|
||||||
v-if="readOnlyMode && !sidebarStore.isSidebarCollapsed"
|
|
||||||
class="z-10 m-2 bg-surface-modal py-2.5 px-3 text-xs text-ink-gray-7 leading-5 rounded-md"
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
__(
|
|
||||||
'This site is being updated. You will not be able to make any changes. Full access will be restored shortly.'
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<TrialBanner
|
|
||||||
v-if="
|
|
||||||
userResource.data?.is_system_manager && userResource.data?.is_fc_site
|
|
||||||
"
|
|
||||||
:isSidebarCollapsed="sidebarStore.isSidebarCollapsed"
|
|
||||||
/>
|
|
||||||
<GettingStartedBanner
|
|
||||||
v-if="showOnboarding && !isOnboardingStepsCompleted"
|
|
||||||
:isSidebarCollapsed="sidebarStore.isSidebarCollapsed"
|
|
||||||
appName="learning"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="flex items-center mt-4"
|
|
||||||
:class="
|
|
||||||
sidebarStore.isSidebarCollapsed ? 'flex-col space-y-3' : 'flex-row'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="flex items-center flex-1"
|
|
||||||
:class="
|
|
||||||
sidebarStore.isSidebarCollapsed
|
|
||||||
? 'flex-col space-y-3'
|
|
||||||
: 'flex-row space-x-3'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<Tooltip v-if="readOnlyMode && sidebarStore.isSidebarCollapsed">
|
|
||||||
<CircleAlert
|
|
||||||
class="size-4 stroke-1.5 text-ink-gray-7 cursor-pointer"
|
|
||||||
/>
|
|
||||||
<template #body>
|
|
||||||
<div
|
|
||||||
class="max-w-[30ch] rounded bg-surface-gray-7 px-2 py-1 text-center text-p-xs text-ink-white shadow-xl"
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
__(
|
|
||||||
'This site is being updated. You will not be able to make any changes. Full access will be restored shortly.'
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip :text="__('Powered by Learning')">
|
|
||||||
<Zap
|
|
||||||
class="size-4 stroke-1.5 text-ink-gray-7 cursor-pointer"
|
|
||||||
@click="redirectToWebsite()"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip v-if="showOnboarding" :text="__('Help')">
|
|
||||||
<CircleHelp
|
|
||||||
class="size-4 stroke-1.5 text-ink-gray-7 cursor-pointer"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
showHelpModal = minimize ? true : !showHelpModal
|
|
||||||
minimize = !showHelpModal
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
<Tooltip
|
|
||||||
:text="
|
|
||||||
sidebarStore.isSidebarCollapsed ? __('Expand') : __('Collapse')
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<CollapseSidebar
|
|
||||||
class="size-4 text-ink-gray-7 duration-300 stroke-1.5 ease-in-out cursor-pointer"
|
|
||||||
:class="{
|
|
||||||
'[transform:rotateY(180deg)]': sidebarStore.isSidebarCollapsed,
|
|
||||||
}"
|
|
||||||
@click="toggleSidebar()"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<HelpModal
|
|
||||||
v-if="showOnboarding && showHelpModal"
|
|
||||||
v-model="showHelpModal"
|
|
||||||
v-model:articles="articles"
|
|
||||||
appName="learning"
|
|
||||||
title="Frappe Learning"
|
|
||||||
:logo="LMSLogo"
|
|
||||||
:afterSkip="(step) => capture('onboarding_step_skipped_' + step)"
|
|
||||||
:afterSkipAll="() => capture('onboarding_steps_skipped')"
|
|
||||||
:afterReset="(step) => capture('onboarding_step_reset_' + step)"
|
|
||||||
:afterResetAll="() => capture('onboarding_steps_reset')"
|
|
||||||
docsLink="https://docs.frappe.io/learning"
|
|
||||||
/>
|
|
||||||
<IntermediateStepModal
|
|
||||||
v-model="showIntermediateModal"
|
|
||||||
:currentStep="currentStep"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<PageModal
|
|
||||||
v-model="showPageModal"
|
|
||||||
v-model:reloadSidebar="sidebarSettings"
|
|
||||||
:page="pageToEdit"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import UserDropdown from '@/components/UserDropdown.vue'
|
|
||||||
import CollapseSidebar from '@/components/Icons/CollapseSidebar.vue'
|
|
||||||
import SidebarLink from '@/components/SidebarLink.vue'
|
|
||||||
import {
|
|
||||||
ref,
|
|
||||||
onMounted,
|
|
||||||
inject,
|
|
||||||
watch,
|
|
||||||
reactive,
|
|
||||||
markRaw,
|
|
||||||
h,
|
|
||||||
onUnmounted,
|
|
||||||
} from 'vue'
|
|
||||||
import { getSidebarLinks } from '../utils'
|
|
||||||
import { usersStore } from '@/stores/user'
|
|
||||||
import { sessionStore } from '@/stores/session'
|
|
||||||
import { useSidebar } from '@/stores/sidebar'
|
|
||||||
import { useSettings } from '@/stores/settings'
|
|
||||||
import { Button, createResource, Tooltip } from 'frappe-ui'
|
|
||||||
import PageModal from '@/components/Modals/PageModal.vue'
|
|
||||||
import { capture } from '@/telemetry'
|
|
||||||
import LMSLogo from '@/components/Icons/LMSLogo.vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import InviteIcon from './Icons/InviteIcon.vue'
|
|
||||||
import {
|
|
||||||
BookOpen,
|
|
||||||
CircleAlert,
|
|
||||||
ChevronRight,
|
|
||||||
Plus,
|
|
||||||
CircleHelp,
|
|
||||||
FolderTree,
|
|
||||||
FileText,
|
|
||||||
UserPlus,
|
|
||||||
Users,
|
|
||||||
BookText,
|
|
||||||
Zap,
|
|
||||||
} from 'lucide-vue-next'
|
|
||||||
import {
|
|
||||||
TrialBanner,
|
|
||||||
HelpModal,
|
|
||||||
GettingStartedBanner,
|
|
||||||
useOnboarding,
|
|
||||||
showHelpModal,
|
|
||||||
minimize,
|
|
||||||
IntermediateStepModal,
|
|
||||||
} from 'frappe-ui/frappe'
|
|
||||||
|
|
||||||
const { user, sidebarSettings } = sessionStore()
|
|
||||||
const { userResource } = usersStore()
|
|
||||||
let sidebarStore = useSidebar()
|
|
||||||
const socket = inject('$socket')
|
|
||||||
const unreadCount = ref(0)
|
|
||||||
const sidebarLinks = ref(getSidebarLinks())
|
|
||||||
const showPageModal = ref(false)
|
|
||||||
const isModerator = ref(false)
|
|
||||||
const isInstructor = ref(false)
|
|
||||||
const pageToEdit = ref(null)
|
|
||||||
const settingsStore = useSettings()
|
|
||||||
const showOnboarding = ref(false)
|
|
||||||
const showIntermediateModal = ref(false)
|
|
||||||
const currentStep = ref({})
|
|
||||||
const router = useRouter()
|
|
||||||
let onboardingDetails
|
|
||||||
let isOnboardingStepsCompleted = false
|
|
||||||
const readOnlyMode = window.read_only_mode
|
|
||||||
const iconProps = {
|
|
||||||
strokeWidth: 1.5,
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
addNotifications()
|
|
||||||
setSidebarLinks()
|
|
||||||
setUpOnboarding()
|
|
||||||
socket.on('publish_lms_notifications', (data) => {
|
|
||||||
unreadNotifications.reload()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const setSidebarLinks = () => {
|
|
||||||
sidebarSettings.reload(
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
onSuccess(data) {
|
|
||||||
Object.keys(data).forEach((key) => {
|
|
||||||
if (!parseInt(data[key])) {
|
|
||||||
sidebarLinks.value = sidebarLinks.value.filter(
|
|
||||||
(link) => link.label.toLowerCase().split(' ').join('_') !== key
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const unreadNotifications = createResource({
|
|
||||||
cache: 'Unread Notifications Count',
|
|
||||||
url: 'frappe.client.get_count',
|
|
||||||
makeParams(values) {
|
|
||||||
return {
|
|
||||||
doctype: 'Notification Log',
|
|
||||||
filters: {
|
|
||||||
for_user: user,
|
|
||||||
read: 0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onSuccess(data) {
|
|
||||||
unreadCount.value = data
|
|
||||||
sidebarLinks.value = sidebarLinks.value.map((link) => {
|
|
||||||
if (link.label === 'Notifications') {
|
|
||||||
link.count = data
|
|
||||||
}
|
|
||||||
return link
|
|
||||||
})
|
|
||||||
},
|
|
||||||
auto: user ? true : false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const addNotifications = () => {
|
|
||||||
if (user) {
|
|
||||||
sidebarLinks.value.push({
|
|
||||||
label: 'Notifications',
|
|
||||||
icon: 'Bell',
|
|
||||||
to: 'Notifications',
|
|
||||||
activeFor: ['Notifications'],
|
|
||||||
count: unreadCount.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const addQuizzes = () => {
|
|
||||||
if (isInstructor.value || isModerator.value) {
|
|
||||||
sidebarLinks.value.push({
|
|
||||||
label: 'Quizzes',
|
|
||||||
icon: 'CircleHelp',
|
|
||||||
to: 'Quizzes',
|
|
||||||
activeFor: [
|
|
||||||
'Quizzes',
|
|
||||||
'QuizForm',
|
|
||||||
'QuizSubmissionList',
|
|
||||||
'QuizSubmission',
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const addAssignments = () => {
|
|
||||||
if (isInstructor.value || isModerator.value) {
|
|
||||||
sidebarLinks.value.push({
|
|
||||||
label: 'Assignments',
|
|
||||||
icon: 'Pencil',
|
|
||||||
to: 'Assignments',
|
|
||||||
activeFor: [
|
|
||||||
'Assignments',
|
|
||||||
'AssignmentForm',
|
|
||||||
'AssignmentSubmissionList',
|
|
||||||
'AssignmentSubmission',
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const addPrograms = () => {
|
|
||||||
let activeFor = ['Programs', 'ProgramForm']
|
|
||||||
let index = 1
|
|
||||||
let canAddProgram = false
|
|
||||||
|
|
||||||
if (
|
|
||||||
!isInstructor.value &&
|
|
||||||
!isModerator.value &&
|
|
||||||
settingsStore.learningPaths.data
|
|
||||||
) {
|
|
||||||
sidebarLinks.value = sidebarLinks.value.filter(
|
|
||||||
(link) => link.label !== 'Courses'
|
|
||||||
)
|
|
||||||
activeFor.push('CourseDetail')
|
|
||||||
activeFor.push('Lesson')
|
|
||||||
index = 0
|
|
||||||
canAddProgram = true
|
|
||||||
} else if (isInstructor.value || isModerator.value) {
|
|
||||||
canAddProgram = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (canAddProgram) {
|
|
||||||
sidebarLinks.value.splice(index, 0, {
|
|
||||||
label: 'Programs',
|
|
||||||
icon: 'Route',
|
|
||||||
to: 'Programs',
|
|
||||||
activeFor: activeFor,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const openPageModal = (link) => {
|
|
||||||
showPageModal.value = true
|
|
||||||
pageToEdit.value = link
|
|
||||||
}
|
|
||||||
|
|
||||||
const deletePage = (link) => {
|
|
||||||
createResource({
|
|
||||||
url: 'lms.lms.api.delete_sidebar_item',
|
|
||||||
makeParams(values) {
|
|
||||||
return {
|
|
||||||
webpage: link.web_page,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}).submit(
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
onSuccess() {
|
|
||||||
sidebarSettings.reload()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleSidebar = () => {
|
|
||||||
sidebarStore.isSidebarCollapsed = !sidebarStore.isSidebarCollapsed
|
|
||||||
localStorage.setItem(
|
|
||||||
'isSidebarCollapsed',
|
|
||||||
JSON.stringify(sidebarStore.isSidebarCollapsed)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleWebPages = () => {
|
|
||||||
sidebarStore.isWebpagesCollapsed = !sidebarStore.isWebpagesCollapsed
|
|
||||||
localStorage.setItem(
|
|
||||||
'isWebpagesCollapsed',
|
|
||||||
JSON.stringify(sidebarStore.isWebpagesCollapsed)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getFirstCourse = async () => {
|
|
||||||
let firstCourse = localStorage.getItem('firstCourse')
|
|
||||||
if (firstCourse) return firstCourse
|
|
||||||
return await call('lms.lms.onboarding.get_first_course')
|
|
||||||
}
|
|
||||||
|
|
||||||
const getFirstBatch = async () => {
|
|
||||||
let firstBatch = localStorage.getItem('firstBatch')
|
|
||||||
if (firstBatch) return firstBatch
|
|
||||||
return await call('lms.lms.onboarding.get_first_batch')
|
|
||||||
}
|
|
||||||
|
|
||||||
const steps = reactive([
|
|
||||||
{
|
|
||||||
name: 'create_first_course',
|
|
||||||
title: __('Create your first course'),
|
|
||||||
icon: markRaw(h(BookOpen, iconProps)),
|
|
||||||
completed: false,
|
|
||||||
onClick: () => {
|
|
||||||
minimize.value = true
|
|
||||||
router.push({
|
|
||||||
name: 'Courses',
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'create_first_chapter',
|
|
||||||
title: __('Add your first chapter'),
|
|
||||||
icon: markRaw(h(FolderTree, iconProps)),
|
|
||||||
completed: false,
|
|
||||||
dependsOn: 'create_first_course',
|
|
||||||
onClick: async () => {
|
|
||||||
minimize.value = true
|
|
||||||
let course = await getFirstCourse()
|
|
||||||
if (course) {
|
|
||||||
router.push({ name: 'CourseForm', params: { courseName: course } })
|
|
||||||
} else {
|
|
||||||
router.push({ name: 'CourseForm' })
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'create_first_lesson',
|
|
||||||
title: __('Add your first lesson'),
|
|
||||||
icon: markRaw(h(FileText, iconProps)),
|
|
||||||
completed: false,
|
|
||||||
dependsOn: 'create_first_chapter',
|
|
||||||
onClick: async () => {
|
|
||||||
minimize.value = true
|
|
||||||
let course = await getFirstCourse()
|
|
||||||
if (course) {
|
|
||||||
router.push({
|
|
||||||
name: 'CourseForm',
|
|
||||||
params: { courseName: course },
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
router.push({ name: 'Courses' })
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'create_first_quiz',
|
|
||||||
title: __('Create your first quiz'),
|
|
||||||
icon: markRaw(h(CircleHelp, iconProps)),
|
|
||||||
completed: false,
|
|
||||||
dependsOn: 'create_first_course',
|
|
||||||
onClick: () => {
|
|
||||||
minimize.value = true
|
|
||||||
router.push({ name: 'Quizzes' })
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'invite_students',
|
|
||||||
title: __('Invite your team and students'),
|
|
||||||
icon: markRaw(h(InviteIcon, iconProps)),
|
|
||||||
completed: false,
|
|
||||||
onClick: () => {
|
|
||||||
minimize.value = true
|
|
||||||
settingsStore.activeTab = 'Members'
|
|
||||||
settingsStore.isSettingsOpen = true
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'create_first_batch',
|
|
||||||
title: __('Create your first batch'),
|
|
||||||
icon: markRaw(h(Users, iconProps)),
|
|
||||||
completed: false,
|
|
||||||
onClick: () => {
|
|
||||||
minimize.value = true
|
|
||||||
router.push({ name: 'Batches' })
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'add_batch_student',
|
|
||||||
title: __('Add students to your batch'),
|
|
||||||
icon: markRaw(h(UserPlus, iconProps)),
|
|
||||||
completed: false,
|
|
||||||
dependsOn: 'create_first_batch',
|
|
||||||
onClick: async () => {
|
|
||||||
minimize.value = true
|
|
||||||
let batch = await getFirstBatch()
|
|
||||||
if (batch) {
|
|
||||||
router.push({
|
|
||||||
name: 'Batch',
|
|
||||||
params: {
|
|
||||||
batchName: batch,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
router.push({ name: 'Batch' })
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'add_batch_course',
|
|
||||||
title: __('Add courses to your batch'),
|
|
||||||
icon: markRaw(h(BookText, iconProps)),
|
|
||||||
completed: false,
|
|
||||||
dependsOn: 'create_first_batch',
|
|
||||||
onClick: async () => {
|
|
||||||
minimize.value = true
|
|
||||||
let batch = await getFirstBatch()
|
|
||||||
if (batch) {
|
|
||||||
router.push({
|
|
||||||
name: 'Batch',
|
|
||||||
params: {
|
|
||||||
batchName: batch,
|
|
||||||
},
|
|
||||||
hash: '#courses',
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
router.push({ name: 'Batch' })
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
const articles = ref([
|
|
||||||
{
|
|
||||||
title: __('Introduction'),
|
|
||||||
opened: false,
|
|
||||||
subArticles: [
|
|
||||||
{ name: 'introduction', title: __('Introduction') },
|
|
||||||
{ name: 'setting-up', title: __('Setting up') },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: __('Creating a course'),
|
|
||||||
opened: false,
|
|
||||||
subArticles: [
|
|
||||||
{ name: 'create-a-course', title: __('Create a course') },
|
|
||||||
{ name: 'add-a-chapter', title: __('Add a chapter') },
|
|
||||||
{ name: 'add-a-lesson', title: __('Add a lesson') },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: __('Creating a batch'),
|
|
||||||
opened: false,
|
|
||||||
subArticles: [
|
|
||||||
{ name: 'create-a-batch', title: __('Create a batch') },
|
|
||||||
{ name: 'create-a-live-class', title: __('Create a live class') },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: __('Assessments'),
|
|
||||||
opened: false,
|
|
||||||
subArticles: [
|
|
||||||
{ name: 'quizzes', title: __('Quizzes') },
|
|
||||||
{ name: 'assignments', title: __('Assignments') },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: __('Certification'),
|
|
||||||
opened: false,
|
|
||||||
subArticles: [
|
|
||||||
{ name: 'issue-a-certificate', title: __('Issue a Certificate') },
|
|
||||||
{
|
|
||||||
name: 'custom-certificate-templates',
|
|
||||||
title: __('Custom Certificate Templates'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: __('Monetization'),
|
|
||||||
opened: false,
|
|
||||||
subArticles: [
|
|
||||||
{
|
|
||||||
name: 'setting-up-payment-gateway',
|
|
||||||
title: __('Setting up payment gateway'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: __('Settings'),
|
|
||||||
opened: false,
|
|
||||||
subArticles: [{ name: 'roles', title: __('Roles') }],
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
const setUpOnboarding = () => {
|
|
||||||
if (userResource.data?.is_system_manager) {
|
|
||||||
onboardingDetails = useOnboarding('learning')
|
|
||||||
onboardingDetails.setUp(steps)
|
|
||||||
isOnboardingStepsCompleted = onboardingDetails.isOnboardingStepsCompleted
|
|
||||||
showOnboarding.value = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(userResource, () => {
|
|
||||||
if (userResource.data) {
|
|
||||||
isModerator.value = userResource.data.is_moderator
|
|
||||||
isInstructor.value = userResource.data.is_instructor
|
|
||||||
addPrograms()
|
|
||||||
addQuizzes()
|
|
||||||
addAssignments()
|
|
||||||
setUpOnboarding()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const redirectToWebsite = () => {
|
|
||||||
window.open('https://frappe.io/learning', '_blank')
|
|
||||||
}
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
socket.off('publish_lms_notifications')
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Popover placement="right-start" class="flex w-full">
|
|
||||||
<template #target="{ togglePopover }">
|
|
||||||
<button
|
|
||||||
:class="[
|
|
||||||
'group w-full flex h-7 items-center justify-between rounded px-2 text-base text-ink-gray-7 hover:bg-surface-gray-2',
|
|
||||||
]"
|
|
||||||
@click.prevent="togglePopover()"
|
|
||||||
>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<LayoutGrid class="size-4 stroke-1.5" />
|
|
||||||
<span class="whitespace-nowrap">
|
|
||||||
{{ __('Apps') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<ChevronRight class="h-4 w-4 stroke-1.5" />
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
<template #body>
|
|
||||||
<div
|
|
||||||
class="grid grid-cols-3 justify-between mx-3 p-2 rounded-lg border border-gray-100 bg-surface-white shadow-xl"
|
|
||||||
>
|
|
||||||
<div v-for="app in apps.data" key="name">
|
|
||||||
<a
|
|
||||||
:href="app.route"
|
|
||||||
class="flex flex-col gap-1.5 rounded justify-center items-center py-2 px-3 hover:bg-surface-gray-2"
|
|
||||||
>
|
|
||||||
<img class="size-8" :src="app.logo" />
|
|
||||||
<div class="text-sm text-ink-gray-7" @click="app.onClick">
|
|
||||||
{{ app.title }}
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Popover>
|
|
||||||
</template>
|
|
||||||
<script setup>
|
|
||||||
import { Popover, createResource } from 'frappe-ui'
|
|
||||||
import { LayoutGrid, ChevronRight } from 'lucide-vue-next'
|
|
||||||
|
|
||||||
const apps = createResource({
|
|
||||||
url: 'frappe.apps.get_apps',
|
|
||||||
cache: 'apps',
|
|
||||||
auto: true,
|
|
||||||
transform: (data) => {
|
|
||||||
let _apps = [
|
|
||||||
{
|
|
||||||
name: 'frappe',
|
|
||||||
logo: '/assets/lms/images/desk.png',
|
|
||||||
title: __('Desk'),
|
|
||||||
route: '/app',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
data.map((app) => {
|
|
||||||
if (app.name === 'lms') return
|
|
||||||
_apps.push({
|
|
||||||
name: app.name,
|
|
||||||
logo: app.logo,
|
|
||||||
title: __(app.title),
|
|
||||||
route: app.route,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return _apps
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||